Restructure modules (#1572)
* Move portbase into monorepo * Add new simple module mgr * [WIP] Switch to new simple module mgr * Add StateMgr and more worker variants * [WIP] Switch more modules * [WIP] Switch more modules * [WIP] swtich more modules * [WIP] switch all SPN modules * [WIP] switch all service modules * [WIP] Convert all workers to the new module system * [WIP] add new task system to module manager * [WIP] Add second take for scheduling workers * [WIP] Add FIXME for bugs in new scheduler * [WIP] Add minor improvements to scheduler * [WIP] Add new worker scheduler * [WIP] Fix more bug related to new module system * [WIP] Fix start handing of the new module system * [WIP] Improve startup process * [WIP] Fix minor issues * [WIP] Fix missing subsystem in settings * [WIP] Initialize managers in constructor * [WIP] Move module event initialization to constrictors * [WIP] Fix setting for enabling and disabling the SPN module * [WIP] Move API registeration into module construction * [WIP] Update states mgr for all modules * [WIP] Add CmdLine operation support * Add state helper methods to module group and instance * Add notification and module status handling to status package * Fix starting issues * Remove pilot widget and update security lock to new status data * Remove debug logs * Improve http server shutdown * Add workaround for cleanly shutting down firewall+netquery * Improve logging * Add syncing states with notifications for new module system * Improve starting, stopping, shutdown; resolve FIXMEs/TODOs * [WIP] Fix most unit tests * Review new module system and fix minor issues * Push shutdown and restart events again via API * Set sleep mode via interface * Update example/template module * [WIP] Fix spn/cabin unit test * Remove deprecated UI elements * Make log output more similar for the logging transition phase * Switch spn hub and observer cmds to new module system * Fix log sources * Make worker mgr less error prone * Fix tests and minor issues * Fix observation hub * Improve shutdown and restart handling * Split up big connection.go source file * Move varint and dsd packages to structures repo * Improve expansion test * Fix linter warnings * Fix interception module on windows * Fix linter errors --------- Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
This commit is contained in:
@@ -7,16 +7,15 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/accessor"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/accessor"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: `broadcasts/matching-data`,
|
||||
Read: api.PermitAdmin,
|
||||
BelongsTo: module,
|
||||
StructFunc: handleMatchingData,
|
||||
Name: "Get Broadcast Notifications Matching Data",
|
||||
Description: "Returns the data used by the broadcast notifications to match the instance.",
|
||||
@@ -28,7 +27,6 @@ func registerAPIEndpoints() error {
|
||||
Path: `broadcasts/reset-state`,
|
||||
Write: api.PermitAdmin,
|
||||
WriteMethod: http.MethodPost,
|
||||
BelongsTo: module,
|
||||
ActionFunc: handleResetState,
|
||||
Name: "Resets the Broadcast Notification States",
|
||||
Description: "Delete the cache of Broadcast Notifications, making them appear again.",
|
||||
@@ -40,7 +38,6 @@ func registerAPIEndpoints() error {
|
||||
Path: `broadcasts/simulate`,
|
||||
Write: api.PermitAdmin,
|
||||
WriteMethod: http.MethodPost,
|
||||
BelongsTo: module,
|
||||
ActionFunc: handleSimulate,
|
||||
Name: "Simulate Broadcast Notifications",
|
||||
Description: "Test broadcast notifications by sending a valid source file in the body.",
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
|
||||
semver "github.com/hashicorp/go-version"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/query"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/info"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
)
|
||||
|
||||
const installInfoDBKey = "core:status/install-info"
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
package broadcasts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
type Broadcasts struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
|
||||
states *mgr.StateMgr
|
||||
}
|
||||
|
||||
func (b *Broadcasts) Manager() *mgr.Manager {
|
||||
return b.mgr
|
||||
}
|
||||
|
||||
func (b *Broadcasts) Start() error {
|
||||
return start()
|
||||
}
|
||||
|
||||
func (b *Broadcasts) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Broadcasts) States() *mgr.StateMgr {
|
||||
return b.states
|
||||
}
|
||||
|
||||
var (
|
||||
db = database.NewInterface(&database.Options{
|
||||
Local: true,
|
||||
Internal: true,
|
||||
@@ -20,7 +43,7 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("broadcasts", prep, start, nil, "updates", "netenv", "notifications")
|
||||
// module = modules.Register("broadcasts", prep, start, nil, "updates", "netenv", "notifications")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
@@ -38,9 +61,34 @@ func start() error {
|
||||
|
||||
// Start broadcast notifier task.
|
||||
startOnce.Do(func() {
|
||||
module.NewTask("broadcast notifier", broadcastNotify).
|
||||
Repeat(10 * time.Minute).Queue()
|
||||
module.mgr.Repeat("broadcast notifier", 10*time.Minute, broadcastNotify)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
module *Broadcasts
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
// New returns a new Config module.
|
||||
func New(instance instance) (*Broadcasts, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
m := mgr.New("Broadcasts")
|
||||
module = &Broadcasts{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
states: m.NewStateMgr(),
|
||||
}
|
||||
|
||||
if err := prep(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface{}
|
||||
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/accessor"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/accessor"
|
||||
"github.com/safing/portmaster/base/database/query"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ type BroadcastNotification struct {
|
||||
repeatDuration time.Duration
|
||||
}
|
||||
|
||||
func broadcastNotify(ctx context.Context, t *modules.Task) error {
|
||||
func broadcastNotify(ctx *mgr.WorkerCtx) error {
|
||||
// Get broadcast notifications file, load it from disk and parse it.
|
||||
broadcastsResource, err := updates.GetFile(broadcastsResourcePath)
|
||||
if err != nil {
|
||||
@@ -210,10 +210,8 @@ func handleBroadcast(bn *BroadcastNotification, matchingDataAccessor accessor.Ac
|
||||
|
||||
// Display notification.
|
||||
n.Save()
|
||||
|
||||
// Attach to module to raise more awareness.
|
||||
if bn.AttachToModule {
|
||||
n.AttachToModule(module)
|
||||
n.SyncWithState(module.states)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
)
|
||||
|
||||
const broadcastStatesDBKey = "core:broadcasts/state"
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package compat
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "compat/self-check",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
ActionFunc: selfcheckViaAPI,
|
||||
Name: "Run Integration Self-Check",
|
||||
Description: "Runs a couple integration self-checks in order to see if the system integration works.",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
package compat
|
||||
|
||||
import "github.com/safing/portbase/utils/debug"
|
||||
import "github.com/safing/portmaster/base/utils/debug"
|
||||
|
||||
// AddToDebugInfo adds compatibility data to the given debug.Info.
|
||||
func AddToDebugInfo(di *debug.Info) {
|
||||
|
||||
@@ -3,7 +3,7 @@ package compat
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/utils/debug"
|
||||
"github.com/safing/portmaster/base/utils/debug"
|
||||
)
|
||||
|
||||
// AddToDebugInfo adds compatibility data to the given debug.Info.
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/utils/debug"
|
||||
"github.com/safing/portmaster/base/utils/debug"
|
||||
)
|
||||
|
||||
// AddToDebugInfo adds compatibility data to the given debug.Info.
|
||||
|
||||
@@ -1,22 +1,50 @@
|
||||
package compat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
// Compat is the compatibility check module.
|
||||
type Compat struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
|
||||
selfcheckTask *modules.Task
|
||||
selfcheckWorkerMgr *mgr.WorkerMgr
|
||||
cleanNotifyThresholdWorkerMgr *mgr.WorkerMgr
|
||||
|
||||
states *mgr.StateMgr
|
||||
}
|
||||
|
||||
// Manager returns the module manager.
|
||||
func (u *Compat) Manager() *mgr.Manager {
|
||||
return u.mgr
|
||||
}
|
||||
|
||||
// States returns the module state manager.
|
||||
func (u *Compat) States() *mgr.StateMgr {
|
||||
return u.states
|
||||
}
|
||||
|
||||
// Start starts the module.
|
||||
func (u *Compat) Start() error {
|
||||
return start()
|
||||
}
|
||||
|
||||
// Stop stops the module.
|
||||
func (u *Compat) Stop() error {
|
||||
return stop()
|
||||
}
|
||||
|
||||
var (
|
||||
selfcheckTaskRetryAfter = 15 * time.Second
|
||||
|
||||
// selfCheckIsFailing holds whether or not the self-check is currently
|
||||
@@ -38,8 +66,6 @@ var (
|
||||
const selfcheckFailThreshold = 10
|
||||
|
||||
func init() {
|
||||
module = modules.Register("compat", prep, start, stop, "base", "network", "interception", "netenv", "notifications")
|
||||
|
||||
// Workaround resolver integration.
|
||||
// See resolver/compat.go for details.
|
||||
resolver.CompatDNSCheckInternalDomainScope = DNSCheckInternalDomainScope
|
||||
@@ -55,35 +81,26 @@ func start() error {
|
||||
startNotify()
|
||||
|
||||
selfcheckNetworkChangedFlag.Refresh()
|
||||
selfcheckTask = module.NewTask("compatibility self-check", selfcheckTaskFunc).
|
||||
Repeat(5 * time.Minute).
|
||||
MaxDelay(selfcheckTaskRetryAfter).
|
||||
Schedule(time.Now().Add(selfcheckTaskRetryAfter))
|
||||
module.selfcheckWorkerMgr.Repeat(5 * time.Minute).Delay(selfcheckTaskRetryAfter)
|
||||
module.cleanNotifyThresholdWorkerMgr.Repeat(1 * time.Hour)
|
||||
|
||||
module.NewTask("clean notify thresholds", cleanNotifyThreshold).
|
||||
Repeat(1 * time.Hour)
|
||||
|
||||
return module.RegisterEventHook(
|
||||
netenv.ModuleName,
|
||||
netenv.NetworkChangedEvent,
|
||||
"trigger compat self-check",
|
||||
func(_ context.Context, _ interface{}) error {
|
||||
selfcheckTask.Schedule(time.Now().Add(selfcheckTaskRetryAfter))
|
||||
return nil
|
||||
},
|
||||
)
|
||||
module.instance.NetEnv().EventNetworkChange.AddCallback("trigger compat self-check", func(_ *mgr.WorkerCtx, _ struct{}) (bool, error) {
|
||||
module.selfcheckWorkerMgr.Delay(selfcheckTaskRetryAfter)
|
||||
return false, nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
selfcheckTask.Cancel()
|
||||
selfcheckTask = nil
|
||||
// selfcheckTask.Cancel()
|
||||
// selfcheckTask = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error {
|
||||
func selfcheckTaskFunc(wc *mgr.WorkerCtx) error {
|
||||
// Create tracing logger.
|
||||
ctx, tracer := log.AddTracer(ctx)
|
||||
ctx, tracer := log.AddTracer(wc.Ctx())
|
||||
defer tracer.Submit()
|
||||
tracer.Tracef("compat: running self-check")
|
||||
|
||||
@@ -115,7 +132,7 @@ func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error {
|
||||
}
|
||||
|
||||
// Retry quicker when failed.
|
||||
task.Schedule(time.Now().Add(selfcheckTaskRetryAfter))
|
||||
module.selfcheckWorkerMgr.Delay(selfcheckTaskRetryAfter)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -135,3 +152,33 @@ func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error {
|
||||
func SelfCheckIsFailing() bool {
|
||||
return selfCheckIsFailing.IsSet()
|
||||
}
|
||||
|
||||
var (
|
||||
module *Compat
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
// New returns a new Compat module.
|
||||
func New(instance instance) (*Compat, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
m := mgr.New("Compat")
|
||||
module = &Compat{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
|
||||
selfcheckWorkerMgr: m.NewWorkerMgr("compatibility self-check", selfcheckTaskFunc, nil),
|
||||
cleanNotifyThresholdWorkerMgr: m.NewWorkerMgr("clean notify thresholds", cleanNotifyThreshold, nil),
|
||||
|
||||
states: mgr.NewStateMgr(m),
|
||||
}
|
||||
if err := prep(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
NetEnv() *netenv.NetEnv
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package compat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
)
|
||||
@@ -112,7 +111,7 @@ func systemCompatOrManualDNSIssue() *systemIssue {
|
||||
return manualDNSSetupRequired
|
||||
}
|
||||
|
||||
func (issue *systemIssue) notify(err error) {
|
||||
func (issue *systemIssue) notify(err error) { //nolint // TODO: Should we use the error?
|
||||
systemIssueNotificationLock.Lock()
|
||||
defer systemIssueNotificationLock.Unlock()
|
||||
|
||||
@@ -138,10 +137,7 @@ func (issue *systemIssue) notify(err error) {
|
||||
notifications.Notify(n)
|
||||
|
||||
systemIssueNotification = n
|
||||
n.AttachToModule(module)
|
||||
|
||||
// Report the raw error as module error.
|
||||
module.NewErrorMessage("selfcheck", err).Report()
|
||||
n.SyncWithState(module.states)
|
||||
}
|
||||
|
||||
func resetSystemIssue() {
|
||||
@@ -214,7 +210,7 @@ func (issue *appIssue) notify(proc *process.Process) {
|
||||
notifications.Notify(n)
|
||||
|
||||
// Set warning on profile.
|
||||
module.StartWorker("set app compat warning", func(ctx context.Context) error {
|
||||
module.mgr.Go("set app compat warning", func(ctx *mgr.WorkerCtx) error {
|
||||
var changed bool
|
||||
|
||||
func() {
|
||||
@@ -273,7 +269,7 @@ func isOverThreshold(id string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func cleanNotifyThreshold(ctx context.Context, task *modules.Task) error {
|
||||
func cleanNotifyThreshold(ctx *mgr.WorkerCtx) error {
|
||||
notifyThresholdsLock.Lock()
|
||||
defer notifyThresholdsLock.Unlock()
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/rng"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/rng"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
@@ -130,7 +131,7 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) {
|
||||
}
|
||||
|
||||
// Start worker for the DNS lookup.
|
||||
module.StartWorker("dns check lookup", func(_ context.Context) error {
|
||||
module.mgr.Go("dns check lookup", func(_ *mgr.WorkerCtx) error {
|
||||
ips, err := net.LookupIP(randomSubdomain + DNSCheckInternalDomainScope)
|
||||
if err == nil && len(ips) > 0 {
|
||||
dnsCheckReturnedIP = ips[0]
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/safing/portbase/utils/osdetail"
|
||||
"github.com/safing/portmaster/base/utils/osdetail"
|
||||
)
|
||||
|
||||
// GetWFPState queries the system for the WFP state and returns a simplified
|
||||
|
||||
3
service/config.go
Normal file
3
service/config.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package service
|
||||
|
||||
type ServiceConfig struct{}
|
||||
@@ -9,13 +9,12 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portbase/rng"
|
||||
"github.com/safing/portbase/utils/debug"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/base/rng"
|
||||
"github.com/safing/portmaster/base/utils/debug"
|
||||
"github.com/safing/portmaster/service/compat"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
@@ -54,7 +53,6 @@ func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "debug/core",
|
||||
Read: api.PermitAnyone,
|
||||
BelongsTo: module,
|
||||
DataFunc: debugInfo,
|
||||
Name: "Get Debug Information",
|
||||
Description: "Returns network debugging information, similar to debug/info, but with system status data.",
|
||||
@@ -71,7 +69,6 @@ func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "app/auth",
|
||||
Read: api.PermitAnyone,
|
||||
BelongsTo: module,
|
||||
StructFunc: authorizeApp,
|
||||
Name: "Request an authentication token with a given set of permissions. The user will be prompted to either authorize or deny the request. Used for external or third-party tool integrations.",
|
||||
Parameters: []api.Parameter{
|
||||
@@ -103,7 +100,6 @@ func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "app/profile",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: getMyProfile,
|
||||
Name: "Get the ID of the calling profile",
|
||||
}); err != nil {
|
||||
@@ -117,9 +113,7 @@ func registerAPIEndpoints() error {
|
||||
func shutdown(_ *api.Request) (msg string, err error) {
|
||||
log.Warning("core: user requested shutdown via action")
|
||||
|
||||
// Do not run in worker, as this would block itself here.
|
||||
go modules.Shutdown() //nolint:errcheck
|
||||
|
||||
module.instance.Shutdown()
|
||||
return "shutdown initiated", nil
|
||||
}
|
||||
|
||||
@@ -145,8 +139,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) {
|
||||
di.AddVersionInfo()
|
||||
di.AddPlatformInfo(ar.Context())
|
||||
|
||||
// Errors and unexpected logs.
|
||||
di.AddLastReportedModuleError()
|
||||
// Unexpected logs.
|
||||
di.AddLastUnexpectedLogs()
|
||||
|
||||
// Status Information from various modules.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/database"
|
||||
_ "github.com/safing/portbase/database/dbmodule"
|
||||
_ "github.com/safing/portbase/database/storage/bbolt"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
_ "github.com/safing/portmaster/base/database/dbmodule"
|
||||
_ "github.com/safing/portmaster/base/database/storage/bbolt"
|
||||
)
|
||||
|
||||
// Default Values (changeable for testing).
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/dataroot"
|
||||
"github.com/safing/portmaster/base/info"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
// Default Values (changeable for testing).
|
||||
@@ -24,11 +24,9 @@ func init() {
|
||||
flag.StringVar(&dataDir, "data", "", "set data directory")
|
||||
flag.StringVar(&databaseDir, "db", "", "alias to --data (deprecated)")
|
||||
flag.BoolVar(&showVersion, "version", false, "show version and exit")
|
||||
|
||||
modules.SetGlobalPrepFn(globalPrep)
|
||||
}
|
||||
|
||||
func globalPrep() error {
|
||||
func prep(instance instance) error {
|
||||
// check if meta info is ok
|
||||
err := info.CheckVersion()
|
||||
if err != nil {
|
||||
@@ -37,8 +35,8 @@ func globalPrep() error {
|
||||
|
||||
// print version
|
||||
if showVersion {
|
||||
fmt.Println(info.FullVersion())
|
||||
return modules.ErrCleanExit
|
||||
instance.SetCmdLineOperation(printVersion)
|
||||
return mgr.ErrExecuteCmdLineOp
|
||||
}
|
||||
|
||||
// check data root
|
||||
@@ -67,3 +65,8 @@ func globalPrep() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printVersion() error {
|
||||
fmt.Println(info.FullVersion())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/base/dataroot"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,12 +19,10 @@ const (
|
||||
)
|
||||
|
||||
func registerLogCleaner() {
|
||||
module.NewTask("log cleaner", logCleaner).
|
||||
Repeat(24 * time.Hour).
|
||||
Schedule(time.Now().Add(15 * time.Minute))
|
||||
_ = module.mgr.Delay("log cleaner", 15*time.Minute, logCleaner).Repeat(24 * time.Hour)
|
||||
}
|
||||
|
||||
func logCleaner(_ context.Context, _ *modules.Task) error {
|
||||
func logCleaner(_ *mgr.WorkerCtx) error {
|
||||
ageThreshold := time.Now().Add(-logTTL)
|
||||
|
||||
return filepath.Walk(
|
||||
|
||||
@@ -1,38 +1,62 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
_ "github.com/safing/portbase/config"
|
||||
_ "github.com/safing/portbase/metrics"
|
||||
"github.com/safing/portbase/modules"
|
||||
_ "github.com/safing/portbase/rng"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
var module *modules.Module
|
||||
|
||||
func init() {
|
||||
module = modules.Register("base", nil, start, nil, "database", "config", "rng", "metrics")
|
||||
|
||||
// For prettier subsystem graph, printed with --print-subsystem-graph
|
||||
/*
|
||||
subsystems.Register(
|
||||
"base",
|
||||
"Base",
|
||||
"THE GROUND.",
|
||||
baseModule,
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
*/
|
||||
// Base is the base module.
|
||||
type Base struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
}
|
||||
|
||||
func start() error {
|
||||
// Manager returns the module manager.
|
||||
func (b *Base) Manager() *mgr.Manager {
|
||||
return b.mgr
|
||||
}
|
||||
|
||||
// Start starts the module.
|
||||
func (b *Base) Start() error {
|
||||
startProfiling()
|
||||
|
||||
if err := registerDatabases(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registerLogCleaner()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the module.
|
||||
func (b *Base) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
module *Base
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
// New returns a new Base module.
|
||||
func New(instance instance) (*Base, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
m := mgr.New("Base")
|
||||
module = &Base{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
}
|
||||
|
||||
if err := prep(instance); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := registerDatabases(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
SetCmdLineOperation(f func() error)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
var cpuProfile string
|
||||
@@ -16,11 +17,11 @@ func init() {
|
||||
|
||||
func startProfiling() {
|
||||
if cpuProfile != "" {
|
||||
module.StartWorker("cpu profiler", cpuProfiler)
|
||||
module.mgr.Go("cpu profiler", cpuProfiler)
|
||||
}
|
||||
}
|
||||
|
||||
func cpuProfiler(ctx context.Context) error {
|
||||
func cpuProfiler(ctx *mgr.WorkerCtx) error {
|
||||
f, err := os.Create(cpuProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create CPU profile: %w", err)
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
locale "github.com/Xuanwo/go-locale"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
)
|
||||
|
||||
// Configuration Keys.
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"time"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/metrics"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/metrics"
|
||||
_ "github.com/safing/portmaster/service/broadcasts"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
_ "github.com/safing/portmaster/service/netenv"
|
||||
_ "github.com/safing/portmaster/service/netquery"
|
||||
_ "github.com/safing/portmaster/service/status"
|
||||
_ "github.com/safing/portmaster/service/sync"
|
||||
_ "github.com/safing/portmaster/service/ui"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
const (
|
||||
eventShutdown = "shutdown"
|
||||
eventRestart = "restart"
|
||||
)
|
||||
// Core is the core service module.
|
||||
type Core struct {
|
||||
m *mgr.Manager
|
||||
instance instance
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
EventShutdown *mgr.EventMgr[struct{}]
|
||||
EventRestart *mgr.EventMgr[struct{}]
|
||||
}
|
||||
|
||||
disableShutdownEvent bool
|
||||
)
|
||||
// Manager returns the manager.
|
||||
func (c *Core) Manager() *mgr.Manager {
|
||||
return c.m
|
||||
}
|
||||
|
||||
// Start starts the module.
|
||||
func (c *Core) Start() error {
|
||||
return start()
|
||||
}
|
||||
|
||||
// Stop stops the module.
|
||||
func (c *Core) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var disableShutdownEvent bool
|
||||
|
||||
func init() {
|
||||
module = modules.Register("core", prep, start, nil, "base", "subsystems", "status", "updates", "api", "notifications", "ui", "netenv", "network", "netquery", "interception", "compat", "broadcasts", "sync")
|
||||
subsystems.Register(
|
||||
"core",
|
||||
"Core",
|
||||
"Base Structure and System Integration",
|
||||
module,
|
||||
"config:core/",
|
||||
nil,
|
||||
)
|
||||
|
||||
flag.BoolVar(
|
||||
&disableShutdownEvent,
|
||||
"disable-shutdown-event",
|
||||
false,
|
||||
"disable shutdown event to keep app and notifier open when core shuts down",
|
||||
)
|
||||
|
||||
modules.SetGlobalShutdownFn(shutdownHook)
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
registerEvents()
|
||||
|
||||
// init config
|
||||
err := registerConfig()
|
||||
if err != nil {
|
||||
@@ -63,6 +63,10 @@ func prep() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initModulesIntegration(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -79,22 +83,33 @@ func start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerEvents() {
|
||||
module.RegisterEvent(eventShutdown, true)
|
||||
module.RegisterEvent(eventRestart, true)
|
||||
}
|
||||
var (
|
||||
module *Core
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
func shutdownHook() {
|
||||
// Notify everyone of the restart/shutdown.
|
||||
if !updates.IsRestarting() {
|
||||
// Only trigger shutdown event if not disabled.
|
||||
if !disableShutdownEvent {
|
||||
module.TriggerEvent(eventShutdown, nil)
|
||||
}
|
||||
} else {
|
||||
module.TriggerEvent(eventRestart, nil)
|
||||
// New returns a new NetEnv module.
|
||||
func New(instance instance) (*Core, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
|
||||
// Wait a bit for the event to propagate.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
m := mgr.New("Core")
|
||||
module = &Core{
|
||||
m: m,
|
||||
instance: instance,
|
||||
|
||||
EventShutdown: mgr.NewEventMgr[struct{}]("shutdown", m),
|
||||
EventRestart: mgr.NewEventMgr[struct{}]("restart", m),
|
||||
}
|
||||
|
||||
if err := prep(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
79
service/core/events.go
Normal file
79
service/core/events.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/runtime"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
var modulesIntegrationUpdatePusher func(...record.Record)
|
||||
|
||||
func initModulesIntegration() (err error) {
|
||||
modulesIntegrationUpdatePusher, err = runtime.Register("modules/", &ModulesIntegration{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Push events via API.
|
||||
module.EventRestart.AddCallback("expose restart event", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
|
||||
// Send event as runtime:modules/core/event/restart
|
||||
pushModuleEvent("core", "restart", false, nil)
|
||||
return false, nil
|
||||
})
|
||||
module.EventShutdown.AddCallback("expose shutdown event", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
|
||||
// Send event as runtime:modules/core/event/shutdown
|
||||
pushModuleEvent("core", "shutdown", false, nil)
|
||||
return false, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ModulesIntegration provides integration with the modules system.
|
||||
type ModulesIntegration struct{}
|
||||
|
||||
// Set is called when the value is set from outside.
|
||||
// If the runtime value is considered read-only ErrReadOnly
|
||||
// should be returned. It is guaranteed that the key of
|
||||
// the record passed to Set is prefixed with the key used
|
||||
// to register the value provider.
|
||||
func (mi *ModulesIntegration) Set(record.Record) (record.Record, error) {
|
||||
return nil, runtime.ErrReadOnly
|
||||
}
|
||||
|
||||
// Get should return one or more records that match keyOrPrefix.
|
||||
// keyOrPrefix is guaranteed to be at least the prefix used to
|
||||
// register the ValueProvider.
|
||||
func (mi *ModulesIntegration) Get(keyOrPrefix string) ([]record.Record, error) {
|
||||
return nil, database.ErrNotFound
|
||||
}
|
||||
|
||||
type eventData struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func pushModuleEvent(moduleName, eventName string, internal bool, data interface{}) {
|
||||
// Create event record and set key.
|
||||
eventRecord := &eventData{
|
||||
Data: data,
|
||||
}
|
||||
eventRecord.SetKey(fmt.Sprintf(
|
||||
"runtime:modules/%s/event/%s",
|
||||
moduleName,
|
||||
eventName,
|
||||
))
|
||||
eventRecord.UpdateMeta()
|
||||
if internal {
|
||||
eventRecord.Meta().MakeSecret()
|
||||
eventRecord.Meta().MakeCrownJewel()
|
||||
}
|
||||
|
||||
// Push event to database subscriptions.
|
||||
modulesIntegrationUpdatePusher(eventRecord)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils/osdetail"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/utils/osdetail"
|
||||
)
|
||||
|
||||
// only return on Fatal error!
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
// Package pmtesting provides a simple unit test setup routine.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// package name
|
||||
//
|
||||
// import (
|
||||
// "testing"
|
||||
//
|
||||
// "github.com/safing/portmaster/service/core/pmtesting"
|
||||
// )
|
||||
//
|
||||
// func TestMain(m *testing.M) {
|
||||
// pmtesting.TestMain(m, module)
|
||||
// }
|
||||
package pmtesting
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/pprof"
|
||||
"testing"
|
||||
|
||||
_ "github.com/safing/portbase/database/storage/hashmap"
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/service/core/base"
|
||||
)
|
||||
|
||||
var printStackOnExit bool
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down")
|
||||
}
|
||||
|
||||
// TestHookFunc describes the functions passed to TestMainWithHooks.
|
||||
type TestHookFunc func() error
|
||||
|
||||
// TestMain provides a simple unit test setup routine.
|
||||
func TestMain(m *testing.M, module *modules.Module) {
|
||||
TestMainWithHooks(m, module, nil, nil)
|
||||
}
|
||||
|
||||
// TestMainWithHooks provides a simple unit test setup routine and calls
|
||||
// afterStartFn after modules have started and beforeStopFn before modules
|
||||
// are shutdown.
|
||||
func TestMainWithHooks(m *testing.M, module *modules.Module, afterStartFn, beforeStopFn TestHookFunc) {
|
||||
// Only enable needed modules.
|
||||
modules.EnableModuleManagement(nil)
|
||||
|
||||
// Enable this module for testing.
|
||||
if module != nil {
|
||||
module.Enable()
|
||||
}
|
||||
|
||||
// switch databases to memory only
|
||||
base.DefaultDatabaseStorageType = "hashmap"
|
||||
|
||||
// switch API to high port
|
||||
base.DefaultAPIListenAddress = "127.0.0.1:10817"
|
||||
|
||||
// set log level
|
||||
log.SetLogLevel(log.TraceLevel)
|
||||
|
||||
// tmp dir for data root (db & config)
|
||||
tmpDir := filepath.Join(os.TempDir(), "portmaster-testing")
|
||||
// initialize data dir
|
||||
err := dataroot.Initialize(tmpDir, 0o0755)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to initialize data root: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// start modules
|
||||
var exitCode int
|
||||
err = modules.Start()
|
||||
if err != nil {
|
||||
// starting failed
|
||||
fmt.Fprintf(os.Stderr, "failed to setup test: %s\n", err)
|
||||
exitCode = 1
|
||||
} else {
|
||||
runTests := true
|
||||
if afterStartFn != nil {
|
||||
if err := afterStartFn(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to run test start hook: %s\n", err)
|
||||
runTests = false
|
||||
exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
if runTests {
|
||||
// run tests
|
||||
exitCode = m.Run()
|
||||
}
|
||||
}
|
||||
|
||||
if beforeStopFn != nil {
|
||||
if err := beforeStopFn(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to run test shutdown hook: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown
|
||||
_ = modules.Shutdown()
|
||||
if modules.GetExitStatusCode() != 0 {
|
||||
exitCode = modules.GetExitStatusCode()
|
||||
fmt.Fprintf(os.Stderr, "failed to cleanly shutdown test: %s\n", err)
|
||||
}
|
||||
printStack()
|
||||
|
||||
// clean up and exit
|
||||
|
||||
// Important: Do not remove tmpDir, as it is used as a cache for updates.
|
||||
// remove config
|
||||
_ = os.Remove(filepath.Join(tmpDir, "config.json"))
|
||||
// remove databases
|
||||
_ = os.Remove(filepath.Join(tmpDir, "databases.json"))
|
||||
_ = os.RemoveAll(filepath.Join(tmpDir, "databases"))
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func printStack() {
|
||||
if printStackOnExit {
|
||||
fmt.Println("=== PRINTING TRACES ===")
|
||||
fmt.Println("=== GOROUTINES ===")
|
||||
_ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)
|
||||
fmt.Println("=== BLOCKING ===")
|
||||
_ = pprof.Lookup("block").WriteTo(os.Stdout, 2)
|
||||
fmt.Println("=== MUTEXES ===")
|
||||
_ = pprof.Lookup("mutex").WriteTo(os.Stdout, 2)
|
||||
fmt.Println("=== END TRACES ===")
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/dataroot"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
@@ -98,7 +98,7 @@ func apiAuthenticator(r *http.Request, s *http.Server) (token *api.AuthToken, er
|
||||
|
||||
// It is important that this works, retry 5 times: every 500ms for 2.5s.
|
||||
var retry bool
|
||||
for tries := 0; tries < 5; tries++ {
|
||||
for range 5 {
|
||||
retry, err = authenticateAPIRequest(
|
||||
r.Context(),
|
||||
&packet.Info{
|
||||
@@ -161,7 +161,7 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo
|
||||
// Find parent for up to two levels, if we don't match the path.
|
||||
checkLevels := 2
|
||||
checkLevelsLoop:
|
||||
for i := 0; i < checkLevels+1; i++ {
|
||||
for i := range checkLevels + 1 {
|
||||
// Check for eligible path.
|
||||
switch proc.Pid {
|
||||
case process.UnidentifiedProcessID, process.SystemProcessID:
|
||||
|
||||
@@ -3,9 +3,9 @@ package firewall
|
||||
import (
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/core"
|
||||
"github.com/safing/portmaster/spn/captain"
|
||||
)
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
//nolint:golint,stylecheck // FIXME
|
||||
//nolint:golint,stylecheck
|
||||
const (
|
||||
DO_NOTHING uint8 = iota
|
||||
BLOCK_PACKET
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/cilium/ebpf/rlimit"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
@@ -182,11 +182,11 @@ func convertArrayToIP(input [4]uint32, ipv6 bool) net.IP {
|
||||
addressBuf := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(addressBuf, input[0])
|
||||
return net.IP(addressBuf)
|
||||
} else {
|
||||
addressBuf := make([]byte, 16)
|
||||
for i := range 4 {
|
||||
binary.LittleEndian.PutUint32(addressBuf[i*4:i*4+4], input[i])
|
||||
}
|
||||
return net.IP(addressBuf)
|
||||
}
|
||||
|
||||
addressBuf := make([]byte, 16)
|
||||
for i := 0; i < 4; i++ {
|
||||
binary.LittleEndian.PutUint32(addressBuf[i*4:i*4+4], input[i])
|
||||
}
|
||||
return net.IP(addressBuf)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/cilium/ebpf/ringbuf"
|
||||
"github.com/cilium/ebpf/rlimit"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
@@ -169,7 +169,7 @@ func convertArrayToIPv4(input [4]uint32, ipVersion packet.IPVersion) net.IP {
|
||||
}
|
||||
|
||||
addressBuf := make([]byte, 16)
|
||||
for i := 0; i < 4; i++ {
|
||||
for i := range 4 {
|
||||
binary.LittleEndian.PutUint32(addressBuf[i*4:i*4+4], input[i])
|
||||
}
|
||||
return net.IP(addressBuf)
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -Werror" bpf ../programs/exec.c
|
||||
@@ -202,7 +202,7 @@ func (t *Tracer) Read() (*Event, error) {
|
||||
if argc > arglen {
|
||||
argc = arglen
|
||||
}
|
||||
for i := 0; i < argc; i++ {
|
||||
for i := range argc {
|
||||
str := unix.ByteSliceToString(rawEvent.Argv[i][:])
|
||||
if strings.TrimSpace(str) != "" {
|
||||
ev.Argv = append(ev.Argv, str)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
package interception
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package interception
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
bandwidth "github.com/safing/portmaster/service/firewall/interception/ebpf/bandwidth"
|
||||
conn_listener "github.com/safing/portmaster/service/firewall/interception/ebpf/connection_listener"
|
||||
"github.com/safing/portmaster/service/firewall/interception/nfq"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
@@ -20,13 +20,13 @@ func startInterception(packets chan packet.Packet) error {
|
||||
}
|
||||
|
||||
// Start ebpf new connection listener.
|
||||
module.StartServiceWorker("ebpf connection listener", 0, func(ctx context.Context) error {
|
||||
return conn_listener.ConnectionListenerWorker(ctx, packets)
|
||||
module.mgr.Go("ebpf connection listener", func(wc *mgr.WorkerCtx) error {
|
||||
return conn_listener.ConnectionListenerWorker(wc.Ctx(), packets)
|
||||
})
|
||||
|
||||
// Start ebpf bandwidth stats monitor.
|
||||
module.StartServiceWorker("ebpf bandwidth stats monitor", 0, func(ctx context.Context) error {
|
||||
return bandwidth.BandwidthStatsWorker(ctx, 1*time.Second, BandwidthUpdates)
|
||||
module.mgr.Go("ebpf bandwidth stats monitor", func(wc *mgr.WorkerCtx) error {
|
||||
return bandwidth.BandwidthStatsWorker(wc.Ctx(), 1*time.Second, BandwidthUpdates)
|
||||
})
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package interception
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
kext1 "github.com/safing/portmaster/service/firewall/interception/windowskext"
|
||||
kext2 "github.com/safing/portmaster/service/firewall/interception/windowskext2"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
@@ -46,25 +46,25 @@ func startInterception(packets chan packet.Packet) error {
|
||||
kext1.SetKextService(kext2.GetKextServiceHandle(), kextFile.Path())
|
||||
|
||||
// Start packet handler.
|
||||
module.StartServiceWorker("kext packet handler", 0, func(ctx context.Context) error {
|
||||
kext1.Handler(ctx, packets)
|
||||
module.mgr.Go("kext packet handler", func(w *mgr.WorkerCtx) error {
|
||||
kext1.Handler(w.Ctx(), packets)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Start bandwidth stats monitor.
|
||||
module.StartServiceWorker("kext bandwidth stats monitor", 0, func(ctx context.Context) error {
|
||||
return kext1.BandwidthStatsWorker(ctx, 1*time.Second, BandwidthUpdates)
|
||||
module.mgr.Go("kext bandwidth stats monitor", func(w *mgr.WorkerCtx) error {
|
||||
return kext1.BandwidthStatsWorker(w.Ctx(), 1*time.Second, BandwidthUpdates)
|
||||
})
|
||||
} else {
|
||||
|
||||
// Start packet handler.
|
||||
module.StartServiceWorker("kext packet handler", 0, func(ctx context.Context) error {
|
||||
kext2.Handler(ctx, packets, BandwidthUpdates)
|
||||
module.mgr.Go("kext packet handler", func(w *mgr.WorkerCtx) error {
|
||||
kext2.Handler(w.Ctx(), packets, BandwidthUpdates)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Start bandwidth stats monitor.
|
||||
module.StartServiceWorker("kext bandwidth request worker", 0, func(ctx context.Context) error {
|
||||
module.mgr.Go("kext bandwidth request worker", func(w *mgr.WorkerCtx) error {
|
||||
timer := time.NewTicker(1 * time.Second)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
@@ -74,7 +74,7 @@ func startInterception(packets chan packet.Packet) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
case <-w.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func startInterception(packets chan packet.Packet) error {
|
||||
})
|
||||
|
||||
// Start kext logging. The worker will periodically send request to the kext to send logs.
|
||||
module.StartServiceWorker("kext log request worker", 0, func(ctx context.Context) error {
|
||||
module.mgr.Go("kext log request worker", func(w *mgr.WorkerCtx) error {
|
||||
timer := time.NewTicker(1 * time.Second)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
@@ -92,14 +92,14 @@ func startInterception(packets chan packet.Packet) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
case <-w.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
module.StartServiceWorker("kext clean ended connection worker", 0, func(ctx context.Context) error {
|
||||
module.mgr.Go("kext clean ended connection worker", func(w *mgr.WorkerCtx) error {
|
||||
timer := time.NewTicker(30 * time.Second)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
@@ -109,7 +109,7 @@ func startInterception(packets chan packet.Packet) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
case <-w.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
package interception
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
// Interception is the packet interception module.
|
||||
type Interception struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
}
|
||||
|
||||
// Packets is a stream of interception network packest.
|
||||
// Manager returns the module manager.
|
||||
func (i *Interception) Manager() *mgr.Manager {
|
||||
return i.mgr
|
||||
}
|
||||
|
||||
// Start starts the module.
|
||||
func (i *Interception) Start() error {
|
||||
return start()
|
||||
}
|
||||
|
||||
// Stop stops the module.
|
||||
func (i *Interception) Stop() error {
|
||||
return stop()
|
||||
}
|
||||
|
||||
var (
|
||||
// Packets is a stream of interception network packets.
|
||||
Packets = make(chan packet.Packet, 1000)
|
||||
|
||||
// BandwidthUpdates is a stream of bandwidth usage update for connections.
|
||||
@@ -22,12 +43,6 @@ var (
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&disableInterception, "disable-interception", false, "disable packet interception; this breaks a lot of functionality")
|
||||
|
||||
module = modules.Register("interception", prep, start, stop, "base", "updates", "network", "notifications", "profiles")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the interception.
|
||||
@@ -58,6 +73,28 @@ func stop() error {
|
||||
}
|
||||
|
||||
close(metrics.done)
|
||||
|
||||
return stopInterception()
|
||||
if err := stopInterception(); err != nil {
|
||||
log.Errorf("failed to stop interception module: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
module *Interception
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
// New returns a new Interception module.
|
||||
func New(instance instance) (*Interception, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
m := mgr.New("Interception")
|
||||
module = &Interception{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
}
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface{}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
ct "github.com/florianl/go-conntrack"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/tevino/abool"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
pmpacket "github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/florianl/go-nfqueue"
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
pmpacket "github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package interception
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"sort"
|
||||
@@ -10,8 +9,9 @@ import (
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/firewall/interception/nfq"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
@@ -258,30 +258,25 @@ func StartNfqueueInterception(packets chan<- packet.Packet) (err error) {
|
||||
|
||||
err = activateNfqueueFirewall()
|
||||
if err != nil {
|
||||
_ = StopNfqueueInterception()
|
||||
return fmt.Errorf("could not initialize nfqueue: %w", err)
|
||||
}
|
||||
|
||||
out4Queue, err = nfq.New(17040, false)
|
||||
if err != nil {
|
||||
_ = StopNfqueueInterception()
|
||||
return fmt.Errorf("nfqueue(IPv4, out): %w", err)
|
||||
}
|
||||
in4Queue, err = nfq.New(17140, false)
|
||||
if err != nil {
|
||||
_ = StopNfqueueInterception()
|
||||
return fmt.Errorf("nfqueue(IPv4, in): %w", err)
|
||||
}
|
||||
|
||||
if netenv.IPv6Enabled() {
|
||||
out6Queue, err = nfq.New(17060, true)
|
||||
if err != nil {
|
||||
_ = StopNfqueueInterception()
|
||||
return fmt.Errorf("nfqueue(IPv6, out): %w", err)
|
||||
}
|
||||
in6Queue, err = nfq.New(17160, true)
|
||||
if err != nil {
|
||||
_ = StopNfqueueInterception()
|
||||
return fmt.Errorf("nfqueue(IPv6, in): %w", err)
|
||||
}
|
||||
} else {
|
||||
@@ -290,7 +285,7 @@ func StartNfqueueInterception(packets chan<- packet.Packet) (err error) {
|
||||
in6Queue = &disabledNfQueue{}
|
||||
}
|
||||
|
||||
module.StartServiceWorker("nfqueue packet handler", 0, func(_ context.Context) error {
|
||||
module.mgr.Go("nfqueue packet handler", func(_ *mgr.WorkerCtx) error {
|
||||
return handleInterception(packets)
|
||||
})
|
||||
return nil
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ package windowskext
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/windows_kext/kextinterface"
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/windows_kext/kextinterface"
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/agext/levenshtein"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/detection/dga"
|
||||
"github.com/safing/portmaster/service/intel/customlists"
|
||||
"github.com/safing/portmaster/service/intel/filterlists"
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
_ "github.com/safing/portmaster/service/core"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/netquery"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/spn/access"
|
||||
@@ -29,132 +31,97 @@ func (ss *stringSliceFlag) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
allowedClients stringSliceFlag
|
||||
)
|
||||
var allowedClients stringSliceFlag
|
||||
|
||||
type Firewall struct {
|
||||
mgr *mgr.Manager
|
||||
|
||||
instance instance
|
||||
}
|
||||
|
||||
func init() {
|
||||
module = modules.Register("filter", prep, start, stop, "core", "interception", "intel", "netquery")
|
||||
subsystems.Register(
|
||||
"filter",
|
||||
"Privacy Filter",
|
||||
"DNS and Network Filter",
|
||||
module,
|
||||
"config:filter/",
|
||||
nil,
|
||||
)
|
||||
|
||||
flag.Var(&allowedClients, "allowed-clients", "A list of binaries that are allowed to connect to the Portmaster API")
|
||||
}
|
||||
|
||||
func (f *Firewall) Manager() *mgr.Manager {
|
||||
return f.mgr
|
||||
}
|
||||
|
||||
func (f *Firewall) Start() error {
|
||||
if err := prep(); err != nil {
|
||||
log.Errorf("Failed to prepare firewall module %q", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return start()
|
||||
}
|
||||
|
||||
func (f *Firewall) Stop() error {
|
||||
// Cancel all workers and give them a little time.
|
||||
// The bandwidth updater can crash the sqlite DB for some reason.
|
||||
// TODO: Investigate.
|
||||
f.mgr.Cancel()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
return stop()
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
network.SetDefaultFirewallHandler(defaultFirewallHandler)
|
||||
|
||||
// Reset connections every time configuration changes
|
||||
// this will be triggered on spn enable/disable
|
||||
err := module.RegisterEventHook(
|
||||
"config",
|
||||
config.ChangeEvent,
|
||||
"reset connection verdicts after global config change",
|
||||
func(ctx context.Context, _ interface{}) error {
|
||||
resetAllConnectionVerdicts()
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("filter: failed to register event hook: %s", err)
|
||||
}
|
||||
module.instance.Config().EventConfigChange.AddCallback("reset connection verdicts after global config change", func(w *mgr.WorkerCtx, _ struct{}) (bool, error) {
|
||||
resetAllConnectionVerdicts()
|
||||
return false, nil
|
||||
})
|
||||
|
||||
// Reset connections every time profile changes
|
||||
err = module.RegisterEventHook(
|
||||
"profiles",
|
||||
profile.ConfigChangeEvent,
|
||||
"reset connection verdicts after profile config change",
|
||||
func(ctx context.Context, eventData interface{}) error {
|
||||
module.instance.Profile().EventConfigChange.AddCallback("reset connection verdicts after profile config change",
|
||||
func(m *mgr.WorkerCtx, profileID string) (bool, error) {
|
||||
// Expected event data: scoped profile ID.
|
||||
profileID, ok := eventData.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("event data is not a string: %v", eventData)
|
||||
}
|
||||
profileSource, profileID, ok := strings.Cut(profileID, "/")
|
||||
if !ok {
|
||||
return fmt.Errorf("event data does not seem to be a scoped profile ID: %v", eventData)
|
||||
return false, fmt.Errorf("event data does not seem to be a scoped profile ID: %v", profileID)
|
||||
}
|
||||
|
||||
resetProfileConnectionVerdict(profileSource, profileID)
|
||||
return nil
|
||||
return false, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("filter: failed to register event hook: %s", err)
|
||||
}
|
||||
|
||||
// Reset connections when spn is connected
|
||||
// connect and disconnecting is triggered on config change event but connecting takеs more time
|
||||
err = module.RegisterEventHook(
|
||||
"captain",
|
||||
captain.SPNConnectedEvent,
|
||||
"reset connection verdicts on SPN connect",
|
||||
func(ctx context.Context, _ interface{}) error {
|
||||
resetAllConnectionVerdicts()
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("filter: failed to register event hook: %s", err)
|
||||
}
|
||||
module.instance.Captain().EventSPNConnected.AddCallback("reset connection verdicts on SPN connect", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
|
||||
resetAllConnectionVerdicts()
|
||||
return false, err
|
||||
})
|
||||
|
||||
// Reset connections when account is updated.
|
||||
// This will not change verdicts, but will update the feature flags on connections.
|
||||
err = module.RegisterEventHook(
|
||||
"access",
|
||||
access.AccountUpdateEvent,
|
||||
"update connection feature flags after account update",
|
||||
func(ctx context.Context, _ interface{}) error {
|
||||
resetAllConnectionVerdicts()
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("filter: failed to register event hook: %s", err)
|
||||
}
|
||||
module.instance.Access().EventAccountUpdate.AddCallback("update connection feature flags after account update", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
|
||||
resetAllConnectionVerdicts()
|
||||
return false, err
|
||||
})
|
||||
|
||||
err = module.RegisterEventHook(
|
||||
"network",
|
||||
network.ConnectionReattributedEvent,
|
||||
"reset verdict of re-attributed connection",
|
||||
func(ctx context.Context, eventData interface{}) error {
|
||||
// Expected event data: connection ID.
|
||||
connID, ok := eventData.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("event data is not a string: %v", eventData)
|
||||
}
|
||||
resetSingleConnectionVerdict(connID)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("filter: failed to register event hook: %s", err)
|
||||
}
|
||||
module.instance.Network().EventConnectionReattributed.AddCallback("reset connection verdicts after connection re-attribution", func(wc *mgr.WorkerCtx, connID string) (cancel bool, err error) {
|
||||
// Expected event data: connection ID.
|
||||
resetSingleConnectionVerdict(connID)
|
||||
return false, err
|
||||
})
|
||||
|
||||
if err := registerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return prepAPIAuth()
|
||||
return nil
|
||||
}
|
||||
|
||||
func start() error {
|
||||
getConfig()
|
||||
startAPIAuth()
|
||||
|
||||
module.StartServiceWorker("packet handler", 0, packetHandler)
|
||||
module.StartServiceWorker("bandwidth update handler", 0, bandwidthUpdateHandler)
|
||||
module.mgr.Go("packet handler", packetHandler)
|
||||
module.mgr.Go("bandwidth update handler", bandwidthUpdateHandler)
|
||||
|
||||
// Start stat logger if logging is set to trace.
|
||||
if log.GetLogLevel() == log.TraceLevel {
|
||||
module.StartServiceWorker("stat logger", 0, statLogger)
|
||||
module.mgr.Go("stat logger", statLogger)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -163,3 +130,39 @@ func start() error {
|
||||
func stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
module *Firewall
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
func New(instance instance) (*Firewall, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
|
||||
m := mgr.New("Firewall")
|
||||
module = &Firewall{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
}
|
||||
|
||||
if err := prepAPIAuth(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := registerConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
Config() *config.Config
|
||||
Profile() *profile.ProfileModule
|
||||
Captain() *captain.Captain
|
||||
Access() *access.Access
|
||||
Network() *network.Network
|
||||
NetQuery() *netquery.NetQuery
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/compat"
|
||||
_ "github.com/safing/portmaster/service/core/base"
|
||||
"github.com/safing/portmaster/service/firewall/inspection"
|
||||
"github.com/safing/portmaster/service/firewall/interception"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/netquery"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
@@ -720,10 +720,10 @@ func issueVerdict(conn *network.Connection, pkt packet.Packet, verdict network.V
|
||||
// return
|
||||
// }
|
||||
|
||||
func packetHandler(ctx context.Context) error {
|
||||
func packetHandler(w *mgr.WorkerCtx) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-w.Done():
|
||||
return nil
|
||||
case pkt := <-interception.Packets:
|
||||
if pkt != nil {
|
||||
@@ -735,16 +735,16 @@ func packetHandler(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func bandwidthUpdateHandler(ctx context.Context) error {
|
||||
func bandwidthUpdateHandler(w *mgr.WorkerCtx) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-w.Done():
|
||||
return nil
|
||||
case bwUpdate := <-interception.BandwidthUpdates:
|
||||
if bwUpdate != nil {
|
||||
// DEBUG:
|
||||
// log.Debugf("filter: bandwidth update: %s", bwUpdate)
|
||||
updateBandwidth(ctx, bwUpdate)
|
||||
updateBandwidth(w.Ctx(), bwUpdate)
|
||||
} else {
|
||||
return errors.New("received nil bandwidth update from interception")
|
||||
}
|
||||
@@ -793,8 +793,8 @@ func updateBandwidth(ctx context.Context, bwUpdate *packet.BandwidthUpdate) {
|
||||
}
|
||||
|
||||
// Update bandwidth in the netquery module.
|
||||
if netquery.DefaultModule != nil && conn.BandwidthEnabled {
|
||||
if err := netquery.DefaultModule.Store.UpdateBandwidth(
|
||||
if module.instance.NetQuery() != nil && conn.BandwidthEnabled {
|
||||
if err := module.instance.NetQuery().Store.UpdateBandwidth(
|
||||
ctx,
|
||||
conn.HistoryEnabled,
|
||||
fmt.Sprintf("%s/%s", conn.ProcessContext.Source, conn.ProcessContext.Profile),
|
||||
@@ -808,10 +808,10 @@ func updateBandwidth(ctx context.Context, bwUpdate *packet.BandwidthUpdate) {
|
||||
}
|
||||
}
|
||||
|
||||
func statLogger(ctx context.Context) error {
|
||||
func statLogger(w *mgr.WorkerCtx) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-w.Done():
|
||||
return nil
|
||||
case <-time.After(10 * time.Second):
|
||||
log.Tracef(
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
|
||||
632
service/instance.go
Normal file
632
service/instance.go
Normal file
@@ -0,0 +1,632 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/database/dbmodule"
|
||||
"github.com/safing/portmaster/base/metrics"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/base/rng"
|
||||
"github.com/safing/portmaster/base/runtime"
|
||||
"github.com/safing/portmaster/service/broadcasts"
|
||||
"github.com/safing/portmaster/service/compat"
|
||||
"github.com/safing/portmaster/service/core"
|
||||
"github.com/safing/portmaster/service/core/base"
|
||||
"github.com/safing/portmaster/service/firewall"
|
||||
"github.com/safing/portmaster/service/firewall/interception"
|
||||
"github.com/safing/portmaster/service/intel/customlists"
|
||||
"github.com/safing/portmaster/service/intel/filterlists"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/nameserver"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/netquery"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
"github.com/safing/portmaster/service/status"
|
||||
"github.com/safing/portmaster/service/sync"
|
||||
"github.com/safing/portmaster/service/ui"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
"github.com/safing/portmaster/spn/access"
|
||||
"github.com/safing/portmaster/spn/cabin"
|
||||
"github.com/safing/portmaster/spn/captain"
|
||||
"github.com/safing/portmaster/spn/crew"
|
||||
"github.com/safing/portmaster/spn/docks"
|
||||
"github.com/safing/portmaster/spn/navigator"
|
||||
"github.com/safing/portmaster/spn/patrol"
|
||||
"github.com/safing/portmaster/spn/ships"
|
||||
"github.com/safing/portmaster/spn/sluice"
|
||||
"github.com/safing/portmaster/spn/terminal"
|
||||
)
|
||||
|
||||
// Instance is an instance of a Portmaster service.
|
||||
type Instance struct {
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
serviceGroup *mgr.Group
|
||||
|
||||
exitCode atomic.Int32
|
||||
|
||||
database *dbmodule.DBModule
|
||||
config *config.Config
|
||||
api *api.API
|
||||
metrics *metrics.Metrics
|
||||
runtime *runtime.Runtime
|
||||
notifications *notifications.Notifications
|
||||
rng *rng.Rng
|
||||
base *base.Base
|
||||
|
||||
core *core.Core
|
||||
updates *updates.Updates
|
||||
geoip *geoip.GeoIP
|
||||
netenv *netenv.NetEnv
|
||||
ui *ui.UI
|
||||
profile *profile.ProfileModule
|
||||
network *network.Network
|
||||
netquery *netquery.NetQuery
|
||||
firewall *firewall.Firewall
|
||||
filterLists *filterlists.FilterLists
|
||||
interception *interception.Interception
|
||||
customlist *customlists.CustomList
|
||||
status *status.Status
|
||||
broadcasts *broadcasts.Broadcasts
|
||||
compat *compat.Compat
|
||||
nameserver *nameserver.NameServer
|
||||
process *process.ProcessModule
|
||||
resolver *resolver.ResolverModule
|
||||
sync *sync.Sync
|
||||
|
||||
access *access.Access
|
||||
|
||||
// SPN modules
|
||||
SpnGroup *mgr.ExtendedGroup
|
||||
cabin *cabin.Cabin
|
||||
navigator *navigator.Navigator
|
||||
captain *captain.Captain
|
||||
crew *crew.Crew
|
||||
docks *docks.Docks
|
||||
patrol *patrol.Patrol
|
||||
ships *ships.Ships
|
||||
sluice *sluice.SluiceModule
|
||||
terminal *terminal.TerminalModule
|
||||
|
||||
CommandLineOperation func() error
|
||||
}
|
||||
|
||||
// New returns a new Portmaster service instance.
|
||||
func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
|
||||
// Create instance to pass it to modules.
|
||||
instance := &Instance{}
|
||||
instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
|
||||
|
||||
var err error
|
||||
|
||||
// Base modules
|
||||
instance.base, err = base.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create base module: %w", err)
|
||||
}
|
||||
instance.database, err = dbmodule.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create database module: %w", err)
|
||||
}
|
||||
instance.config, err = config.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create config module: %w", err)
|
||||
}
|
||||
instance.api, err = api.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create api module: %w", err)
|
||||
}
|
||||
instance.metrics, err = metrics.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create metrics module: %w", err)
|
||||
}
|
||||
instance.runtime, err = runtime.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create runtime module: %w", err)
|
||||
}
|
||||
instance.notifications, err = notifications.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create runtime module: %w", err)
|
||||
}
|
||||
instance.rng, err = rng.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create rng module: %w", err)
|
||||
}
|
||||
|
||||
// Service modules
|
||||
instance.core, err = core.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create core module: %w", err)
|
||||
}
|
||||
instance.updates, err = updates.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create updates module: %w", err)
|
||||
}
|
||||
instance.geoip, err = geoip.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create customlist module: %w", err)
|
||||
}
|
||||
instance.netenv, err = netenv.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create netenv module: %w", err)
|
||||
}
|
||||
instance.ui, err = ui.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create ui module: %w", err)
|
||||
}
|
||||
instance.profile, err = profile.NewModule(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create profile module: %w", err)
|
||||
}
|
||||
instance.network, err = network.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create network module: %w", err)
|
||||
}
|
||||
instance.netquery, err = netquery.NewModule(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create netquery module: %w", err)
|
||||
}
|
||||
instance.firewall, err = firewall.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create firewall module: %w", err)
|
||||
}
|
||||
instance.filterLists, err = filterlists.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create filterLists module: %w", err)
|
||||
}
|
||||
instance.interception, err = interception.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create interception module: %w", err)
|
||||
}
|
||||
instance.customlist, err = customlists.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create customlist module: %w", err)
|
||||
}
|
||||
instance.status, err = status.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create status module: %w", err)
|
||||
}
|
||||
instance.broadcasts, err = broadcasts.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create broadcasts module: %w", err)
|
||||
}
|
||||
instance.compat, err = compat.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create compat module: %w", err)
|
||||
}
|
||||
instance.nameserver, err = nameserver.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create nameserver module: %w", err)
|
||||
}
|
||||
instance.process, err = process.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create process module: %w", err)
|
||||
}
|
||||
instance.resolver, err = resolver.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create resolver module: %w", err)
|
||||
}
|
||||
instance.sync, err = sync.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create sync module: %w", err)
|
||||
}
|
||||
instance.access, err = access.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create access module: %w", err)
|
||||
}
|
||||
|
||||
// SPN modules
|
||||
instance.cabin, err = cabin.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create cabin module: %w", err)
|
||||
}
|
||||
instance.navigator, err = navigator.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create navigator module: %w", err)
|
||||
}
|
||||
instance.captain, err = captain.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create captain module: %w", err)
|
||||
}
|
||||
instance.crew, err = crew.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create crew module: %w", err)
|
||||
}
|
||||
instance.docks, err = docks.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create docks module: %w", err)
|
||||
}
|
||||
instance.patrol, err = patrol.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create patrol module: %w", err)
|
||||
}
|
||||
instance.ships, err = ships.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create ships module: %w", err)
|
||||
}
|
||||
instance.sluice, err = sluice.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create sluice module: %w", err)
|
||||
}
|
||||
instance.terminal, err = terminal.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create terminal module: %w", err)
|
||||
}
|
||||
|
||||
// Add all modules to instance group.
|
||||
instance.serviceGroup = mgr.NewGroup(
|
||||
instance.base,
|
||||
instance.database,
|
||||
instance.config,
|
||||
instance.api,
|
||||
instance.metrics,
|
||||
instance.runtime,
|
||||
instance.notifications,
|
||||
instance.rng,
|
||||
|
||||
instance.core,
|
||||
instance.updates,
|
||||
instance.geoip,
|
||||
instance.netenv,
|
||||
|
||||
instance.ui,
|
||||
instance.profile,
|
||||
instance.netquery,
|
||||
instance.network,
|
||||
instance.firewall,
|
||||
instance.filterLists,
|
||||
instance.interception,
|
||||
instance.customlist,
|
||||
instance.status,
|
||||
instance.broadcasts,
|
||||
instance.compat,
|
||||
instance.nameserver,
|
||||
instance.process,
|
||||
instance.resolver,
|
||||
instance.sync,
|
||||
|
||||
instance.access,
|
||||
)
|
||||
|
||||
// SPN Group
|
||||
instance.SpnGroup = mgr.NewExtendedGroup(
|
||||
instance.cabin,
|
||||
instance.navigator,
|
||||
instance.captain,
|
||||
instance.crew,
|
||||
instance.docks,
|
||||
instance.patrol,
|
||||
instance.ships,
|
||||
instance.sluice,
|
||||
instance.terminal,
|
||||
)
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// SleepyModule is an interface for modules that can enter some sort of sleep mode.
|
||||
type SleepyModule interface {
|
||||
SetSleep(enabled bool)
|
||||
}
|
||||
|
||||
// SetSleep sets sleep mode on all modules that satisfy the SleepyModule interface.
|
||||
func (i *Instance) SetSleep(enabled bool) {
|
||||
for _, module := range i.serviceGroup.Modules() {
|
||||
if sm, ok := module.(SleepyModule); ok {
|
||||
sm.SetSleep(enabled)
|
||||
}
|
||||
}
|
||||
for _, module := range i.SpnGroup.Modules() {
|
||||
if sm, ok := module.(SleepyModule); ok {
|
||||
sm.SetSleep(enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Database returns the database module.
|
||||
func (i *Instance) Database() *dbmodule.DBModule {
|
||||
return i.database
|
||||
}
|
||||
|
||||
// Config returns the config module.
|
||||
func (i *Instance) Config() *config.Config {
|
||||
return i.config
|
||||
}
|
||||
|
||||
// API returns the api module.
|
||||
func (i *Instance) API() *api.API {
|
||||
return i.api
|
||||
}
|
||||
|
||||
// Metrics returns the metrics module.
|
||||
func (i *Instance) Metrics() *metrics.Metrics {
|
||||
return i.metrics
|
||||
}
|
||||
|
||||
// Runtime returns the runtime module.
|
||||
func (i *Instance) Runtime() *runtime.Runtime {
|
||||
return i.runtime
|
||||
}
|
||||
|
||||
// Notifications returns the notifications module.
|
||||
func (i *Instance) Notifications() *notifications.Notifications {
|
||||
return i.notifications
|
||||
}
|
||||
|
||||
// Rng returns the rng module.
|
||||
func (i *Instance) Rng() *rng.Rng {
|
||||
return i.rng
|
||||
}
|
||||
|
||||
// Base returns the base module.
|
||||
func (i *Instance) Base() *base.Base {
|
||||
return i.base
|
||||
}
|
||||
|
||||
// Updates returns the updates module.
|
||||
func (i *Instance) Updates() *updates.Updates {
|
||||
return i.updates
|
||||
}
|
||||
|
||||
// GeoIP returns the geoip module.
|
||||
func (i *Instance) GeoIP() *geoip.GeoIP {
|
||||
return i.geoip
|
||||
}
|
||||
|
||||
// NetEnv returns the netenv module.
|
||||
func (i *Instance) NetEnv() *netenv.NetEnv {
|
||||
return i.netenv
|
||||
}
|
||||
|
||||
// Access returns the access module.
|
||||
func (i *Instance) Access() *access.Access {
|
||||
return i.access
|
||||
}
|
||||
|
||||
// Cabin returns the cabin module.
|
||||
func (i *Instance) Cabin() *cabin.Cabin {
|
||||
return i.cabin
|
||||
}
|
||||
|
||||
// Captain returns the captain module.
|
||||
func (i *Instance) Captain() *captain.Captain {
|
||||
return i.captain
|
||||
}
|
||||
|
||||
// Crew returns the crew module.
|
||||
func (i *Instance) Crew() *crew.Crew {
|
||||
return i.crew
|
||||
}
|
||||
|
||||
// Docks returns the crew module.
|
||||
func (i *Instance) Docks() *docks.Docks {
|
||||
return i.docks
|
||||
}
|
||||
|
||||
// Navigator returns the navigator module.
|
||||
func (i *Instance) Navigator() *navigator.Navigator {
|
||||
return i.navigator
|
||||
}
|
||||
|
||||
// Patrol returns the patrol module.
|
||||
func (i *Instance) Patrol() *patrol.Patrol {
|
||||
return i.patrol
|
||||
}
|
||||
|
||||
// Ships returns the ships module.
|
||||
func (i *Instance) Ships() *ships.Ships {
|
||||
return i.ships
|
||||
}
|
||||
|
||||
// Sluice returns the ships module.
|
||||
func (i *Instance) Sluice() *sluice.SluiceModule {
|
||||
return i.sluice
|
||||
}
|
||||
|
||||
// Terminal returns the terminal module.
|
||||
func (i *Instance) Terminal() *terminal.TerminalModule {
|
||||
return i.terminal
|
||||
}
|
||||
|
||||
// UI returns the ui module.
|
||||
func (i *Instance) UI() *ui.UI {
|
||||
return i.ui
|
||||
}
|
||||
|
||||
// Profile returns the profile module.
|
||||
func (i *Instance) Profile() *profile.ProfileModule {
|
||||
return i.profile
|
||||
}
|
||||
|
||||
// Firewall returns the firewall module.
|
||||
func (i *Instance) Firewall() *firewall.Firewall {
|
||||
return i.firewall
|
||||
}
|
||||
|
||||
// FilterLists returns the filterLists module.
|
||||
func (i *Instance) FilterLists() *filterlists.FilterLists {
|
||||
return i.filterLists
|
||||
}
|
||||
|
||||
// Interception returns the interception module.
|
||||
func (i *Instance) Interception() *interception.Interception {
|
||||
return i.interception
|
||||
}
|
||||
|
||||
// CustomList returns the customlist module.
|
||||
func (i *Instance) CustomList() *customlists.CustomList {
|
||||
return i.customlist
|
||||
}
|
||||
|
||||
// Status returns the status module.
|
||||
func (i *Instance) Status() *status.Status {
|
||||
return i.status
|
||||
}
|
||||
|
||||
// Broadcasts returns the broadcast module.
|
||||
func (i *Instance) Broadcasts() *broadcasts.Broadcasts {
|
||||
return i.broadcasts
|
||||
}
|
||||
|
||||
// Compat returns the compat module.
|
||||
func (i *Instance) Compat() *compat.Compat {
|
||||
return i.compat
|
||||
}
|
||||
|
||||
// NameServer returns the nameserver module.
|
||||
func (i *Instance) NameServer() *nameserver.NameServer {
|
||||
return i.nameserver
|
||||
}
|
||||
|
||||
// NetQuery returns the netquery module.
|
||||
func (i *Instance) NetQuery() *netquery.NetQuery {
|
||||
return i.netquery
|
||||
}
|
||||
|
||||
// Network returns the network module.
|
||||
func (i *Instance) Network() *network.Network {
|
||||
return i.network
|
||||
}
|
||||
|
||||
// Process returns the process module.
|
||||
func (i *Instance) Process() *process.ProcessModule {
|
||||
return i.process
|
||||
}
|
||||
|
||||
// Resolver returns the resolver module.
|
||||
func (i *Instance) Resolver() *resolver.ResolverModule {
|
||||
return i.resolver
|
||||
}
|
||||
|
||||
// Sync returns the sync module.
|
||||
func (i *Instance) Sync() *sync.Sync {
|
||||
return i.sync
|
||||
}
|
||||
|
||||
// Core returns the core module.
|
||||
func (i *Instance) Core() *core.Core {
|
||||
return i.core
|
||||
}
|
||||
|
||||
// SPNGroup returns the group of all SPN modules.
|
||||
func (i *Instance) SPNGroup() *mgr.ExtendedGroup {
|
||||
return i.SpnGroup
|
||||
}
|
||||
|
||||
// Events
|
||||
|
||||
// GetEventSPNConnected return the event manager for the SPN connected event.
|
||||
func (i *Instance) GetEventSPNConnected() *mgr.EventMgr[struct{}] {
|
||||
return i.captain.EventSPNConnected
|
||||
}
|
||||
|
||||
// Special functions
|
||||
|
||||
// SetCmdLineOperation sets a command line operation to be executed instead of starting the system. This is useful when functions need all modules to be prepared for a special operation.
|
||||
func (i *Instance) SetCmdLineOperation(f func() error) {
|
||||
i.CommandLineOperation = f
|
||||
}
|
||||
|
||||
// GetStates returns the current states of all group modules.
|
||||
func (i *Instance) GetStates() []mgr.StateUpdate {
|
||||
mainStates := i.serviceGroup.GetStates()
|
||||
spnStates := i.SpnGroup.GetStates()
|
||||
|
||||
updates := make([]mgr.StateUpdate, 0, len(mainStates)+len(spnStates))
|
||||
updates = append(updates, mainStates...)
|
||||
updates = append(updates, spnStates...)
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
// AddStatesCallback adds the given callback function to all group modules that
|
||||
// expose a state manager at States().
|
||||
func (i *Instance) AddStatesCallback(callbackName string, callback mgr.EventCallbackFunc[mgr.StateUpdate]) {
|
||||
i.serviceGroup.AddStatesCallback(callbackName, callback)
|
||||
i.SpnGroup.AddStatesCallback(callbackName, callback)
|
||||
}
|
||||
|
||||
// Ready returns whether all modules in the main service module group have been started and are still running.
|
||||
func (i *Instance) Ready() bool {
|
||||
return i.serviceGroup.Ready()
|
||||
}
|
||||
|
||||
// Ctx returns the instance context.
|
||||
// It is only canceled on shutdown.
|
||||
func (i *Instance) Ctx() context.Context {
|
||||
return i.ctx
|
||||
}
|
||||
|
||||
// Start starts the instance.
|
||||
func (i *Instance) Start() error {
|
||||
return i.serviceGroup.Start()
|
||||
}
|
||||
|
||||
// Stop stops the instance and cancels the instance context when done.
|
||||
func (i *Instance) Stop() error {
|
||||
defer i.cancelCtx()
|
||||
return i.serviceGroup.Stop()
|
||||
}
|
||||
|
||||
// RestartExitCode will instruct portmaster-start to restart the process immediately, potentially with a new version.
|
||||
const RestartExitCode = 23
|
||||
|
||||
// Restart asynchronously restarts the instance.
|
||||
// This only works if the underlying system/process supports this.
|
||||
func (i *Instance) Restart() {
|
||||
// Send a restart event, give it 10ms extra to propagate.
|
||||
i.core.EventRestart.Submit(struct{}{})
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
i.shutdown(RestartExitCode)
|
||||
}
|
||||
|
||||
// Shutdown asynchronously stops the instance.
|
||||
func (i *Instance) Shutdown() {
|
||||
// Send a shutdown event, give it 10ms extra to propagate.
|
||||
i.core.EventShutdown.Submit(struct{}{})
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
i.shutdown(0)
|
||||
}
|
||||
|
||||
func (i *Instance) shutdown(exitCode int) {
|
||||
// Set given exit code.
|
||||
i.exitCode.Store(int32(exitCode))
|
||||
|
||||
m := mgr.New("instance")
|
||||
m.Go("shutdown", func(w *mgr.WorkerCtx) error {
|
||||
for {
|
||||
if err := i.Stop(); err != nil {
|
||||
w.Error("failed to shutdown", "err", err, "retry", "1s")
|
||||
time.Sleep(1 * time.Second)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Stopping returns whether the instance is shutting down.
|
||||
func (i *Instance) Stopping() bool {
|
||||
return i.ctx.Err() == nil
|
||||
}
|
||||
|
||||
// Stopped returns a channel that is triggered when the instance has shut down.
|
||||
func (i *Instance) Stopped() <-chan struct{} {
|
||||
return i.ctx.Done()
|
||||
}
|
||||
|
||||
// ExitCode returns the set exit code of the instance.
|
||||
func (i *Instance) ExitCode() int {
|
||||
return int(i.exitCode.Load())
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/nameserver/nsutil"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package customlists
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
)
|
||||
|
||||
@@ -79,8 +80,15 @@ func parseFile(filePath string) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Warningf("intel/customlists: failed to parse file %s", err)
|
||||
module.Warning(parseWarningNotificationID, "Failed to open custom filter list", err.Error())
|
||||
module.states.Add(mgr.State{
|
||||
ID: parseWarningNotificationID,
|
||||
Name: "Failed to open custom filter list",
|
||||
Message: err.Error(),
|
||||
Type: mgr.StateTypeWarning,
|
||||
})
|
||||
return err
|
||||
} else {
|
||||
module.states.Remove(parseWarningNotificationID)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
@@ -107,11 +115,15 @@ func parseFile(filePath string) error {
|
||||
|
||||
if invalidLinesRation > rationForInvalidLinesUntilWarning {
|
||||
log.Warning("intel/customlists: Too many invalid lines")
|
||||
module.Warning(zeroIPNotificationID, "Custom filter list has many invalid lines",
|
||||
fmt.Sprintf(`%d out of %d lines are invalid.
|
||||
Check if you are using the correct file format and if the path to the custom filter list is correct.`, invalidLinesCount, allLinesCount))
|
||||
module.states.Add(mgr.State{
|
||||
ID: zeroIPNotificationID,
|
||||
Name: "Custom filter list has many invalid lines",
|
||||
Message: fmt.Sprintf(`%d out of %d lines are invalid.
|
||||
Check if you are using the correct file format and if the path to the custom filter list is correct.`, invalidLinesCount, allLinesCount),
|
||||
Type: mgr.StateTypeWarning,
|
||||
})
|
||||
} else {
|
||||
module.Resolve(zeroIPNotificationID)
|
||||
module.states.Remove(zeroIPNotificationID)
|
||||
}
|
||||
|
||||
allEntriesCount := len(domainsFilterList) + len(ipAddressesFilterList) + len(autonomousSystemsFilterList) + len(countryCodesFilterList)
|
||||
@@ -130,8 +142,6 @@ func parseFile(filePath string) error {
|
||||
len(autonomousSystemsFilterList),
|
||||
len(countryCodesFilterList)))
|
||||
|
||||
module.Resolve(parseWarningNotificationID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,46 @@
|
||||
package customlists
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
var module *modules.Module
|
||||
type CustomList struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
|
||||
const (
|
||||
configModuleName = "config"
|
||||
configChangeEvent = "config change"
|
||||
)
|
||||
updateFilterListWorkerMgr *mgr.WorkerMgr
|
||||
|
||||
states *mgr.StateMgr
|
||||
}
|
||||
|
||||
func (cl *CustomList) Manager() *mgr.Manager {
|
||||
return cl.mgr
|
||||
}
|
||||
|
||||
func (cl *CustomList) States() *mgr.StateMgr {
|
||||
return cl.states
|
||||
}
|
||||
|
||||
func (cl *CustomList) Start() error {
|
||||
return start()
|
||||
}
|
||||
|
||||
func (cl *CustomList) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper variables for parsing the input file.
|
||||
var (
|
||||
@@ -34,17 +53,12 @@ var (
|
||||
filterListFileModifiedTime time.Time
|
||||
|
||||
filterListLock sync.RWMutex
|
||||
parserTask *modules.Task
|
||||
|
||||
// ErrNotConfigured is returned when updating the custom filter list, but it
|
||||
// is not configured.
|
||||
ErrNotConfigured = errors.New("custom filter list not configured")
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("customlists", prep, start, nil, "base")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
initFilterLists()
|
||||
|
||||
@@ -56,11 +70,10 @@ func prep() error {
|
||||
|
||||
// Register api endpoint for updating the filter list.
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "customlists/update",
|
||||
Write: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
Path: "customlists/update",
|
||||
Write: api.PermitUser,
|
||||
ActionFunc: func(ar *api.Request) (msg string, err error) {
|
||||
errCheck := checkAndUpdateFilterList()
|
||||
errCheck := checkAndUpdateFilterList(nil)
|
||||
if errCheck != nil {
|
||||
return "", errCheck
|
||||
}
|
||||
@@ -77,30 +90,23 @@ func prep() error {
|
||||
|
||||
func start() error {
|
||||
// Register to hook to update after config change.
|
||||
if err := module.RegisterEventHook(
|
||||
configModuleName,
|
||||
configChangeEvent,
|
||||
module.instance.Config().EventConfigChange.AddCallback(
|
||||
"update custom filter list",
|
||||
func(ctx context.Context, obj interface{}) error {
|
||||
if err := checkAndUpdateFilterList(); !errors.Is(err, ErrNotConfigured) {
|
||||
return err
|
||||
func(wc *mgr.WorkerCtx, _ struct{}) (bool, error) {
|
||||
if err := checkAndUpdateFilterList(wc); !errors.Is(err, ErrNotConfigured) {
|
||||
return false, err
|
||||
}
|
||||
return nil
|
||||
return false, nil
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
)
|
||||
|
||||
// Create parser task and enqueue for execution. "checkAndUpdateFilterList" will schedule the next execution.
|
||||
parserTask = module.NewTask("intel/customlists:file-update-check", func(context.Context, *modules.Task) error {
|
||||
_ = checkAndUpdateFilterList()
|
||||
return nil
|
||||
}).Schedule(time.Now().Add(20 * time.Second))
|
||||
module.updateFilterListWorkerMgr.Delay(20 * time.Second).Repeat(1 * time.Minute)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndUpdateFilterList() error {
|
||||
func checkAndUpdateFilterList(_ *mgr.WorkerCtx) error {
|
||||
filterListLock.Lock()
|
||||
defer filterListLock.Unlock()
|
||||
|
||||
@@ -110,9 +116,6 @@ func checkAndUpdateFilterList() error {
|
||||
return ErrNotConfigured
|
||||
}
|
||||
|
||||
// Schedule next update check
|
||||
parserTask.Schedule(time.Now().Add(1 * time.Minute))
|
||||
|
||||
// Try to get file info
|
||||
modifiedTime := time.Now()
|
||||
if fileInfo, err := os.Stat(filePath); err == nil {
|
||||
@@ -205,3 +208,33 @@ func splitDomain(domain string) []string {
|
||||
}
|
||||
return domains
|
||||
}
|
||||
|
||||
var (
|
||||
module *CustomList
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
// New returns a new CustomList module.
|
||||
func New(instance instance) (*CustomList, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
m := mgr.New("CustomList")
|
||||
module = &CustomList{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
|
||||
states: mgr.NewStateMgr(m),
|
||||
updateFilterListWorkerMgr: m.NewWorkerMgr("update custom filter list", checkAndUpdateFilterList, nil),
|
||||
}
|
||||
|
||||
if err := prep(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
Config() *config.Config
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/intel/filterlists"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
"github.com/tannerryan/ring"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
)
|
||||
|
||||
var defaultFilter = newScopedBloom()
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
)
|
||||
|
||||
const resetVersion = "v0.6.0"
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/updater"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/safing/portbase/formats/dsd"
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/structures/dsd"
|
||||
)
|
||||
|
||||
type listEntry struct {
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/formats/dsd"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/updater"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
"github.com/safing/structures/dsd"
|
||||
)
|
||||
|
||||
// the following definitions are copied from the intelhub repository
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
)
|
||||
|
||||
// lookupBlockLists loads the entity record for key from
|
||||
|
||||
@@ -1,25 +1,50 @@
|
||||
package filterlists
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
var module *modules.Module
|
||||
|
||||
const (
|
||||
filterlistsDisabled = "filterlists:disabled"
|
||||
filterlistsUpdateFailed = "filterlists:update-failed"
|
||||
filterlistsStaleDataSurvived = "filterlists:staledata"
|
||||
)
|
||||
|
||||
type FilterLists struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
|
||||
states *mgr.StateMgr
|
||||
}
|
||||
|
||||
func (fl *FilterLists) Manager() *mgr.Manager {
|
||||
return fl.mgr
|
||||
}
|
||||
|
||||
func (fl *FilterLists) States() *mgr.StateMgr {
|
||||
return fl.states
|
||||
}
|
||||
|
||||
func (fl *FilterLists) Start() error {
|
||||
if err := prep(); err != nil {
|
||||
return err
|
||||
}
|
||||
return start()
|
||||
}
|
||||
|
||||
func (fl *FilterLists) Stop() error {
|
||||
return stop()
|
||||
}
|
||||
|
||||
// booleans mainly used to decouple the module
|
||||
// during testing.
|
||||
var (
|
||||
@@ -29,45 +54,30 @@ var (
|
||||
|
||||
func init() {
|
||||
ignoreNetEnvEvents.Set()
|
||||
|
||||
module = modules.Register("filterlists", prep, start, stop, "base", "updates")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
if err := module.RegisterEventHook(
|
||||
updates.ModuleName,
|
||||
updates.ResourceUpdateEvent,
|
||||
"Check for blocklist updates",
|
||||
func(ctx context.Context, _ interface{}) error {
|
||||
module.instance.Updates().EventResourcesUpdated.AddCallback("Check for blocklist updates",
|
||||
func(wc *mgr.WorkerCtx, s struct{}) (bool, error) {
|
||||
if ignoreUpdateEvents.IsSet() {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return tryListUpdate(ctx)
|
||||
},
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to register resource update event handler: %w", err)
|
||||
}
|
||||
return false, tryListUpdate(wc.Ctx())
|
||||
})
|
||||
|
||||
if err := module.RegisterEventHook(
|
||||
netenv.ModuleName,
|
||||
netenv.OnlineStatusChangedEvent,
|
||||
"Check for blocklist updates",
|
||||
func(ctx context.Context, _ interface{}) error {
|
||||
module.instance.NetEnv().EventOnlineStatusChange.AddCallback("Check for blocklist updates",
|
||||
func(wc *mgr.WorkerCtx, s netenv.OnlineStatus) (bool, error) {
|
||||
if ignoreNetEnvEvents.IsSet() {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Nothing to do if we went offline.
|
||||
if !netenv.Online() {
|
||||
return nil
|
||||
if s == netenv.StatusOffline {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return tryListUpdate(ctx)
|
||||
},
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to register online status changed event handler: %w", err)
|
||||
}
|
||||
return false, tryListUpdate(wc.Ctx())
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -102,9 +112,35 @@ func stop() error {
|
||||
}
|
||||
|
||||
func warnAboutDisabledFilterLists() {
|
||||
module.Warning(
|
||||
filterlistsDisabled,
|
||||
"Filter Lists Are Initializing",
|
||||
"Filter lists are being downloaded and set up in the background. They will be activated as configured when finished.",
|
||||
)
|
||||
module.states.Add(mgr.State{
|
||||
ID: filterlistsDisabled,
|
||||
Name: "Filter Lists Are Initializing",
|
||||
Message: "Filter lists are being downloaded and set up in the background. They will be activated as configured when finished.",
|
||||
Type: mgr.StateTypeWarning,
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
module *FilterLists
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
// New returns a new FilterLists module.
|
||||
func New(instance instance) (*FilterLists, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
m := mgr.New("FilterLists")
|
||||
module = &FilterLists{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
|
||||
states: mgr.NewStateMgr(m),
|
||||
}
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
Updates() *updates.Updates
|
||||
NetEnv() *netenv.NetEnv
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
)
|
||||
|
||||
type entityRecord struct {
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/query"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/updater"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
var updateInProgress = abool.New()
|
||||
@@ -24,20 +24,27 @@ var updateInProgress = abool.New()
|
||||
func tryListUpdate(ctx context.Context) error {
|
||||
err := performUpdate(ctx)
|
||||
if err != nil {
|
||||
// Check if we are shutting down.
|
||||
if module.IsStopping() {
|
||||
// Check if we are shutting down, as to not raise a false alarm.
|
||||
if module.mgr.IsDone() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the module already has a failure status set. If not, set a
|
||||
// generic one with the returned error.
|
||||
failureStatus, _, _ := module.FailureStatus()
|
||||
if failureStatus < modules.FailureWarning {
|
||||
module.Warning(
|
||||
filterlistsUpdateFailed,
|
||||
"Filter Lists Update Failed",
|
||||
fmt.Sprintf("The Portmaster failed to process a filter lists update. Filtering capabilities are currently either impaired or not available at all. Error: %s", err.Error()),
|
||||
)
|
||||
|
||||
hasWarningState := false
|
||||
for _, state := range module.states.Export().States {
|
||||
if state.Type == mgr.StateTypeWarning {
|
||||
hasWarningState = true
|
||||
}
|
||||
}
|
||||
if !hasWarningState {
|
||||
module.states.Add(mgr.State{
|
||||
ID: filterlistsUpdateFailed,
|
||||
Name: "Filter Lists Update Failed",
|
||||
Message: fmt.Sprintf("The Portmaster failed to process a filter lists update. Filtering capabilities are currently either impaired or not available at all. Error: %s", err.Error()),
|
||||
Type: mgr.StateTypeWarning,
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -122,15 +129,16 @@ func performUpdate(ctx context.Context) error {
|
||||
// been updated now. Once we are done, start a worker
|
||||
// for that purpose.
|
||||
if cleanupRequired {
|
||||
if err := module.RunWorker("filterlists:cleanup", removeAllObsoleteFilterEntries); err != nil {
|
||||
if err := module.mgr.Do("filterlists:cleanup", removeAllObsoleteFilterEntries); err != nil {
|
||||
// if we failed to remove all stale cache entries
|
||||
// we abort now WITHOUT updating the database version. This means
|
||||
// we'll try again during the next update.
|
||||
module.Warning(
|
||||
filterlistsStaleDataSurvived,
|
||||
"Filter Lists May Overblock",
|
||||
fmt.Sprintf("The Portmaster failed to delete outdated filter list data. Filtering capabilities are fully available, but overblocking may occur. Error: %s", err.Error()), //nolint:misspell // overblocking != overclocking
|
||||
)
|
||||
module.states.Add(mgr.State{
|
||||
ID: filterlistsStaleDataSurvived,
|
||||
Name: "Filter Lists May Overblock",
|
||||
Message: fmt.Sprintf("The Portmaster failed to delete outdated filter list data. Filtering capabilities are fully available, but overblocking may occur. Error: %s", err.Error()), //nolint:misspell // overblocking != overclocking
|
||||
Type: mgr.StateTypeWarning,
|
||||
})
|
||||
return fmt.Errorf("failed to cleanup stale cache records: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -144,13 +152,13 @@ func performUpdate(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// The list update succeeded, resolve any states.
|
||||
module.Resolve("")
|
||||
module.states.Clear()
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeAllObsoleteFilterEntries(ctx context.Context) error {
|
||||
func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error {
|
||||
log.Debugf("intel/filterlists: cleanup task started, removing obsolete filter list entries ...")
|
||||
n, err := cache.Purge(ctx, query.New(filterListKeyPrefix).Where(
|
||||
n, err := cache.Purge(wc.Ctx(), query.New(filterListKeyPrefix).Where(
|
||||
// TODO(ppacher): remember the timestamp we started the last update
|
||||
// and use that rather than "one hour ago"
|
||||
query.Where("UpdatedAt", query.LessThan, time.Now().Add(-time.Hour).Unix()),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
maxminddb "github.com/oschwald/maxminddb-golang"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/updater"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
@@ -148,11 +148,11 @@ func (upd *updateWorker) triggerUpdate() {
|
||||
|
||||
func (upd *updateWorker) start() {
|
||||
upd.once.Do(func() {
|
||||
module.StartServiceWorker("geoip-updater", time.Second*10, upd.run)
|
||||
module.mgr.Go("geoip-updater", upd.run)
|
||||
})
|
||||
}
|
||||
|
||||
func (upd *updateWorker) run(ctx context.Context) error {
|
||||
func (upd *updateWorker) run(ctx *mgr.WorkerCtx) error {
|
||||
for {
|
||||
if upd.v4.NeedsUpdate() {
|
||||
if v4, err := getGeoIPDB(v4MMDBResource); err == nil {
|
||||
|
||||
109
service/intel/geoip/init_test.go
Normal file
109
service/intel/geoip/init_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/database/dbmodule"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
type testInstance struct {
|
||||
db *dbmodule.DBModule
|
||||
api *api.API
|
||||
config *config.Config
|
||||
updates *updates.Updates
|
||||
}
|
||||
|
||||
var _ instance = &testInstance{}
|
||||
|
||||
func (stub *testInstance) Updates() *updates.Updates {
|
||||
return stub.updates
|
||||
}
|
||||
|
||||
func (stub *testInstance) API() *api.API {
|
||||
return stub.api
|
||||
}
|
||||
|
||||
func (stub *testInstance) Config() *config.Config {
|
||||
return stub.config
|
||||
}
|
||||
|
||||
func (stub *testInstance) Notifications() *notifications.Notifications {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (stub *testInstance) Ready() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (stub *testInstance) Restart() {}
|
||||
|
||||
func (stub *testInstance) Shutdown() {}
|
||||
|
||||
func (stub *testInstance) SetCmdLineOperation(f func() error) {}
|
||||
|
||||
func runTest(m *testing.M) error {
|
||||
api.SetDefaultAPIListenAddress("0.0.0.0:8080")
|
||||
ds, err := config.InitializeUnitTestDataroot("test-geoip")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize dataroot: %w", err)
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(ds) }()
|
||||
|
||||
stub := &testInstance{}
|
||||
stub.db, err = dbmodule.New(stub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create database: %w", err)
|
||||
}
|
||||
stub.config, err = config.New(stub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config: %w", err)
|
||||
}
|
||||
stub.api, err = api.New(stub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create api: %w", err)
|
||||
}
|
||||
stub.updates, err = updates.New(stub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create updates: %w", err)
|
||||
}
|
||||
module, err = New(stub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize module: %w", err)
|
||||
}
|
||||
|
||||
err = stub.db.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to start database: %w", err)
|
||||
}
|
||||
err = stub.config.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to start config: %w", err)
|
||||
}
|
||||
err = stub.api.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to start api: %w", err)
|
||||
}
|
||||
err = stub.updates.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to start updates: %w", err)
|
||||
}
|
||||
err = module.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start module: %w", err)
|
||||
}
|
||||
m.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := runTest(m); err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/umahmood/haversine"
|
||||
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -3,6 +3,7 @@ package geoip
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLocationLookup(t *testing.T) {
|
||||
@@ -12,6 +13,17 @@ func TestLocationLookup(t *testing.T) {
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
// Wait for db to be initialized
|
||||
worker.v4.rw.Lock()
|
||||
waiter := worker.v4.getWaiter()
|
||||
worker.v4.rw.Unlock()
|
||||
|
||||
worker.triggerUpdate()
|
||||
select {
|
||||
case <-waiter:
|
||||
case <-time.After(15 * time.Second):
|
||||
}
|
||||
|
||||
ip1 := net.ParseIP("81.2.69.142")
|
||||
loc1, err := GetLocation(ip1)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,20 +1,54 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
var module *modules.Module
|
||||
|
||||
func init() {
|
||||
module = modules.Register("geoip", prep, nil, nil, "base", "updates")
|
||||
type GeoIP struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
func (g *GeoIP) Manager() *mgr.Manager {
|
||||
return g.mgr
|
||||
}
|
||||
|
||||
func (g *GeoIP) Start() error {
|
||||
module.instance.Updates().EventResourcesUpdated.AddCallback(
|
||||
"Check for GeoIP database updates",
|
||||
func(_ *mgr.WorkerCtx, _ struct{}) (bool, error) {
|
||||
worker.triggerUpdate()
|
||||
return false, nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GeoIP) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
module *GeoIP
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
// New returns a new GeoIP module.
|
||||
func New(instance instance) (*GeoIP, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
|
||||
m := mgr.New("geoip")
|
||||
module = &GeoIP{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "intel/geoip/countries",
|
||||
Read: api.PermitUser,
|
||||
@@ -25,16 +59,12 @@ func prep() error {
|
||||
Name: "Get Country Information",
|
||||
Description: "Returns a map of country information centers indexed by ISO-A2 country code",
|
||||
}); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module.RegisterEventHook(
|
||||
updates.ModuleName,
|
||||
updates.ResourceUpdateEvent,
|
||||
"Check for GeoIP database updates",
|
||||
func(c context.Context, i interface{}) error {
|
||||
worker.triggerUpdate()
|
||||
return nil
|
||||
},
|
||||
)
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
Updates() *updates.Updates
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/service/core/pmtesting"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pmtesting.TestMain(m, module)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
)
|
||||
|
||||
// IsRegionalNeighbor returns whether the supplied location is a regional neighbor.
|
||||
|
||||
@@ -3,7 +3,7 @@ package geoip
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
)
|
||||
|
||||
func TestRegions(t *testing.T) {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package intel
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/modules"
|
||||
_ "github.com/safing/portmaster/service/intel/customlists"
|
||||
)
|
||||
|
||||
// Module of this package. Export needed for testing of the endpoints package.
|
||||
var Module *modules.Module
|
||||
|
||||
func init() {
|
||||
Module = modules.Register("intel", nil, nil, nil, "geoip", "filterlists", "customlists")
|
||||
}
|
||||
2
service/mgr/doc.go
Normal file
2
service/mgr/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package mgr provides simple managing of flow control and logging.
|
||||
package mgr
|
||||
175
service/mgr/events.go
Normal file
175
service/mgr/events.go
Normal file
@@ -0,0 +1,175 @@
|
||||
//nolint:structcheck,golint // TODO: Seems broken for generics.
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// EventMgr is a simple event manager.
|
||||
type EventMgr[T any] struct {
|
||||
name string
|
||||
mgr *Manager
|
||||
lock sync.Mutex
|
||||
|
||||
subs []*EventSubscription[T]
|
||||
callbacks []*EventCallback[T]
|
||||
}
|
||||
|
||||
// EventSubscription is a subscription to an event.
|
||||
type EventSubscription[T any] struct {
|
||||
name string
|
||||
events chan T
|
||||
canceled atomic.Bool
|
||||
}
|
||||
|
||||
// EventCallback is a registered callback to an event.
|
||||
type EventCallback[T any] struct {
|
||||
name string
|
||||
callback EventCallbackFunc[T]
|
||||
canceled atomic.Bool
|
||||
}
|
||||
|
||||
// EventCallbackFunc defines the event callback function.
|
||||
type EventCallbackFunc[T any] func(*WorkerCtx, T) (cancel bool, err error)
|
||||
|
||||
// NewEventMgr returns a new event manager.
|
||||
// It is easiest used as a public field on a struct,
|
||||
// so that others can simply Subscribe() oder AddCallback().
|
||||
func NewEventMgr[T any](eventName string, mgr *Manager) *EventMgr[T] {
|
||||
return &EventMgr[T]{
|
||||
name: eventName,
|
||||
mgr: mgr,
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe subscribes to events.
|
||||
// The received events are shared among all subscribers and callbacks.
|
||||
// Be sure to apply proper concurrency safeguards, if applicable.
|
||||
func (em *EventMgr[T]) Subscribe(subscriberName string, chanSize int) *EventSubscription[T] {
|
||||
em.lock.Lock()
|
||||
defer em.lock.Unlock()
|
||||
|
||||
es := &EventSubscription[T]{
|
||||
name: subscriberName,
|
||||
events: make(chan T, chanSize),
|
||||
}
|
||||
|
||||
em.subs = append(em.subs, es)
|
||||
return es
|
||||
}
|
||||
|
||||
// AddCallback adds a callback to executed on events.
|
||||
// The received events are shared among all subscribers and callbacks.
|
||||
// Be sure to apply proper concurrency safeguards, if applicable.
|
||||
func (em *EventMgr[T]) AddCallback(callbackName string, callback EventCallbackFunc[T]) {
|
||||
em.lock.Lock()
|
||||
defer em.lock.Unlock()
|
||||
|
||||
ec := &EventCallback[T]{
|
||||
name: callbackName,
|
||||
callback: callback,
|
||||
}
|
||||
|
||||
em.callbacks = append(em.callbacks, ec)
|
||||
}
|
||||
|
||||
// Submit submits a new event.
|
||||
func (em *EventMgr[T]) Submit(event T) {
|
||||
em.lock.Lock()
|
||||
defer em.lock.Unlock()
|
||||
|
||||
var anyCanceled bool
|
||||
|
||||
// Send to subscriptions.
|
||||
for _, sub := range em.subs {
|
||||
// Check if subscription was canceled.
|
||||
if sub.canceled.Load() {
|
||||
anyCanceled = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Submit via channel.
|
||||
select {
|
||||
case sub.events <- event:
|
||||
default:
|
||||
if em.mgr != nil {
|
||||
em.mgr.Warn(
|
||||
"event subscription channel overflow",
|
||||
"event", em.name,
|
||||
"subscriber", sub.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run callbacks.
|
||||
for _, ec := range em.callbacks {
|
||||
// Execute callback.
|
||||
var (
|
||||
cancel bool
|
||||
err error
|
||||
)
|
||||
if em.mgr != nil {
|
||||
// Prefer executing in worker.
|
||||
wkrErr := em.mgr.Do("execute event callback", func(w *WorkerCtx) error {
|
||||
cancel, err = ec.callback(w, event) //nolint:scopelint // Execution is within scope.
|
||||
return nil
|
||||
})
|
||||
if wkrErr != nil {
|
||||
err = fmt.Errorf("callback execution failed: %w", wkrErr)
|
||||
}
|
||||
} else {
|
||||
cancel, err = ec.callback(nil, event)
|
||||
}
|
||||
|
||||
// Handle error and cancelation.
|
||||
if err != nil && em.mgr != nil {
|
||||
em.mgr.Warn(
|
||||
"event callback failed",
|
||||
"event", em.name,
|
||||
"callback", ec.name,
|
||||
"err", err,
|
||||
)
|
||||
}
|
||||
if cancel {
|
||||
ec.canceled.Store(true)
|
||||
anyCanceled = true
|
||||
}
|
||||
}
|
||||
|
||||
// If any canceled subscription/callback was seen, clean the slices.
|
||||
if anyCanceled {
|
||||
em.clean()
|
||||
}
|
||||
}
|
||||
|
||||
// clean removes all canceled subscriptions and callbacks.
|
||||
func (em *EventMgr[T]) clean() {
|
||||
em.subs = slices.DeleteFunc[[]*EventSubscription[T], *EventSubscription[T]](em.subs, func(es *EventSubscription[T]) bool {
|
||||
return es.canceled.Load()
|
||||
})
|
||||
em.callbacks = slices.DeleteFunc[[]*EventCallback[T], *EventCallback[T]](em.callbacks, func(ec *EventCallback[T]) bool {
|
||||
return ec.canceled.Load()
|
||||
})
|
||||
}
|
||||
|
||||
// Events returns a read channel for the events.
|
||||
// The received events are shared among all subscribers and callbacks.
|
||||
// Be sure to apply proper concurrency safeguards, if applicable.
|
||||
func (es *EventSubscription[T]) Events() <-chan T {
|
||||
return es.events
|
||||
}
|
||||
|
||||
// Cancel cancels the subscription.
|
||||
// The events channel is not closed, but will not receive new events.
|
||||
func (es *EventSubscription[T]) Cancel() {
|
||||
es.canceled.Store(true)
|
||||
}
|
||||
|
||||
// Done returns whether the event subscription has been canceled.
|
||||
func (es *EventSubscription[T]) Done() bool {
|
||||
return es.canceled.Load()
|
||||
}
|
||||
286
service/mgr/group.go
Normal file
286
service/mgr/group.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnsuitableGroupState is returned when an operation cannot be executed due to an unsuitable state.
|
||||
ErrUnsuitableGroupState = errors.New("unsuitable group state")
|
||||
|
||||
// ErrInvalidGroupState is returned when a group is in an invalid state and cannot be recovered.
|
||||
ErrInvalidGroupState = errors.New("invalid group state")
|
||||
|
||||
// ErrExecuteCmdLineOp is returned when modules are created, but request
|
||||
// execution of a (somewhere else set) command line operation instead.
|
||||
ErrExecuteCmdLineOp = errors.New("command line operation execution requested")
|
||||
)
|
||||
|
||||
const (
|
||||
groupStateOff int32 = iota
|
||||
groupStateStarting
|
||||
groupStateRunning
|
||||
groupStateStopping
|
||||
groupStateInvalid
|
||||
)
|
||||
|
||||
func groupStateToString(state int32) string {
|
||||
switch state {
|
||||
case groupStateOff:
|
||||
return "off"
|
||||
case groupStateStarting:
|
||||
return "starting"
|
||||
case groupStateRunning:
|
||||
return "running"
|
||||
case groupStateStopping:
|
||||
return "stopping"
|
||||
case groupStateInvalid:
|
||||
return "invalid"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// Group describes a group of modules.
|
||||
type Group struct {
|
||||
modules []*groupModule
|
||||
|
||||
state atomic.Int32
|
||||
}
|
||||
|
||||
type groupModule struct {
|
||||
module Module
|
||||
mgr *Manager
|
||||
}
|
||||
|
||||
// Module is an manage-able instance of some component.
|
||||
type Module interface {
|
||||
Manager() *Manager
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// NewGroup returns a new group of modules.
|
||||
func NewGroup(modules ...Module) *Group {
|
||||
// Create group.
|
||||
g := &Group{
|
||||
modules: make([]*groupModule, 0, len(modules)),
|
||||
}
|
||||
|
||||
// Initialize groups modules.
|
||||
for _, m := range modules {
|
||||
g.Add(m)
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// Add validates the given module and adds it to the group, if all requirements are met.
|
||||
// Not safe for concurrent use with any other method.
|
||||
// All modules must be added before anything else is done with the group.
|
||||
func (g *Group) Add(m Module) {
|
||||
mgr := m.Manager()
|
||||
|
||||
// Check module.
|
||||
switch {
|
||||
case m == nil:
|
||||
// Skip nil values to allow for cleaner code.
|
||||
return
|
||||
case reflect.ValueOf(m).IsNil():
|
||||
// If nil values are given via a struct, they are will be interfaces to a
|
||||
// nil type. Ignore these too.
|
||||
return
|
||||
case mgr == nil:
|
||||
// Ignore modules that do not return a manager.
|
||||
return
|
||||
case mgr.Name() == "":
|
||||
// Force name if none is set.
|
||||
// TODO: Unsafe if module is already logging, etc.
|
||||
mgr.setName(makeModuleName(m))
|
||||
}
|
||||
|
||||
// Add module to group.
|
||||
g.modules = append(g.modules, &groupModule{
|
||||
module: m,
|
||||
mgr: mgr,
|
||||
})
|
||||
}
|
||||
|
||||
// Start starts all modules in the group in the defined order.
|
||||
// If a module fails to start, itself and all previous modules
|
||||
// will be stopped in the reverse order.
|
||||
func (g *Group) Start() error {
|
||||
// Check group state.
|
||||
switch g.state.Load() {
|
||||
case groupStateRunning:
|
||||
// Already running.
|
||||
return nil
|
||||
case groupStateInvalid:
|
||||
// Something went terribly wrong, cannot recover from here.
|
||||
return fmt.Errorf("%w: cannot recover", ErrInvalidGroupState)
|
||||
default:
|
||||
if !g.state.CompareAndSwap(groupStateOff, groupStateStarting) {
|
||||
return fmt.Errorf("%w: group is not off, state: %s", ErrUnsuitableGroupState, groupStateToString(g.state.Load()))
|
||||
}
|
||||
}
|
||||
|
||||
// Start modules.
|
||||
for i, m := range g.modules {
|
||||
m.mgr.Debug("starting")
|
||||
startTime := time.Now()
|
||||
|
||||
err := m.mgr.Do("start module "+m.mgr.name, func(_ *WorkerCtx) error {
|
||||
return m.module.Start() //nolint:scopelint // Execution is synchronous.
|
||||
})
|
||||
if err != nil {
|
||||
m.mgr.Error(
|
||||
"failed to start",
|
||||
"err", err,
|
||||
"time", time.Since(startTime),
|
||||
)
|
||||
if !g.stopFrom(i) {
|
||||
g.state.Store(groupStateInvalid)
|
||||
} else {
|
||||
g.state.Store(groupStateOff)
|
||||
}
|
||||
return fmt.Errorf("failed to start %s: %w", m.mgr.name, err)
|
||||
}
|
||||
m.mgr.Info("started", "time", time.Since(startTime))
|
||||
}
|
||||
|
||||
g.state.Store(groupStateRunning)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops all modules in the group in the reverse order.
|
||||
func (g *Group) Stop() error {
|
||||
// Check group state.
|
||||
switch g.state.Load() {
|
||||
case groupStateOff:
|
||||
// Already stopped.
|
||||
return nil
|
||||
case groupStateInvalid:
|
||||
// Something went terribly wrong, cannot recover from here.
|
||||
return fmt.Errorf("%w: cannot recover", ErrInvalidGroupState)
|
||||
default:
|
||||
if !g.state.CompareAndSwap(groupStateRunning, groupStateStopping) {
|
||||
return fmt.Errorf("%w: group is not running, state: %s", ErrUnsuitableGroupState, groupStateToString(g.state.Load()))
|
||||
}
|
||||
}
|
||||
|
||||
// Stop modules.
|
||||
if !g.stopFrom(len(g.modules) - 1) {
|
||||
g.state.Store(groupStateInvalid)
|
||||
return errors.New("failed to stop")
|
||||
}
|
||||
|
||||
g.state.Store(groupStateOff)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Group) stopFrom(index int) (ok bool) {
|
||||
ok = true
|
||||
|
||||
// Stop modules.
|
||||
for i := index; i >= 0; i-- {
|
||||
m := g.modules[i]
|
||||
m.mgr.Debug("stopping")
|
||||
startTime := time.Now()
|
||||
|
||||
err := m.mgr.Do("stop module "+m.mgr.name, func(_ *WorkerCtx) error {
|
||||
return m.module.Stop()
|
||||
})
|
||||
if err != nil {
|
||||
m.mgr.Error(
|
||||
"failed to stop",
|
||||
"err", err,
|
||||
"time", time.Since(startTime),
|
||||
)
|
||||
ok = false
|
||||
}
|
||||
m.mgr.Cancel()
|
||||
if m.mgr.WaitForWorkers(0) {
|
||||
m.mgr.Info("stopped", "time", time.Since(startTime))
|
||||
} else {
|
||||
ok = false
|
||||
m.mgr.Error(
|
||||
"failed to stop",
|
||||
"err", "timed out",
|
||||
"workerCnt", m.mgr.workerCnt.Load(),
|
||||
"time", time.Since(startTime),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset modules.
|
||||
if !ok {
|
||||
// Stopping failed somewhere, reset anyway after a short wait.
|
||||
// This will be very uncommon and can help to mitigate race conditions in these events.
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
for _, m := range g.modules {
|
||||
m.mgr.Reset()
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// Ready returns whether all modules in the group have been started and are still running.
|
||||
func (g *Group) Ready() bool {
|
||||
return g.state.Load() == groupStateRunning
|
||||
}
|
||||
|
||||
// GetStates returns the current states of all group modules.
|
||||
func (g *Group) GetStates() []StateUpdate {
|
||||
updates := make([]StateUpdate, 0, len(g.modules))
|
||||
for _, gm := range g.modules {
|
||||
if stateful, ok := gm.module.(StatefulModule); ok {
|
||||
updates = append(updates, stateful.States().Export())
|
||||
}
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|
||||
// AddStatesCallback adds the given callback function to all group modules that
|
||||
// expose a state manager at States().
|
||||
func (g *Group) AddStatesCallback(callbackName string, callback EventCallbackFunc[StateUpdate]) {
|
||||
for _, gm := range g.modules {
|
||||
if stateful, ok := gm.module.(StatefulModule); ok {
|
||||
stateful.States().AddCallback(callbackName, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modules returns a copy of the modules.
|
||||
func (g *Group) Modules() []Module {
|
||||
copied := make([]Module, 0, len(g.modules))
|
||||
for _, gm := range g.modules {
|
||||
copied = append(copied, gm.module)
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
// RunModules is a simple wrapper function to start modules and stop them again
|
||||
// when the given context is canceled.
|
||||
func RunModules(ctx context.Context, modules ...Module) error {
|
||||
g := NewGroup(modules...)
|
||||
|
||||
// Start module.
|
||||
if err := g.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start: %w", err)
|
||||
}
|
||||
|
||||
// Stop module when context is canceled.
|
||||
<-ctx.Done()
|
||||
return g.Stop()
|
||||
}
|
||||
|
||||
func makeModuleName(m Module) string {
|
||||
return strings.TrimPrefix(fmt.Sprintf("%T", m), "*")
|
||||
}
|
||||
92
service/mgr/group_ext.go
Normal file
92
service/mgr/group_ext.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExtendedGroup extends the group with additional helpful functionality.
|
||||
type ExtendedGroup struct {
|
||||
*Group
|
||||
|
||||
ensureCtx context.Context
|
||||
ensureCancel context.CancelFunc
|
||||
ensureLock sync.Mutex
|
||||
}
|
||||
|
||||
// NewExtendedGroup returns a new extended group.
|
||||
func NewExtendedGroup(modules ...Module) *ExtendedGroup {
|
||||
return UpgradeGroup(NewGroup(modules...))
|
||||
}
|
||||
|
||||
// UpgradeGroup upgrades a regular group to an extended group.
|
||||
func UpgradeGroup(g *Group) *ExtendedGroup {
|
||||
return &ExtendedGroup{
|
||||
Group: g,
|
||||
ensureCancel: func() {},
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureStartedWorker tries to start the group until it succeeds or fails permanently.
|
||||
func (eg *ExtendedGroup) EnsureStartedWorker(wCtx *WorkerCtx) error {
|
||||
// Setup worker.
|
||||
var ctx context.Context
|
||||
func() {
|
||||
eg.ensureLock.Lock()
|
||||
defer eg.ensureLock.Unlock()
|
||||
eg.ensureCancel()
|
||||
eg.ensureCtx, eg.ensureCancel = context.WithCancel(wCtx.Ctx())
|
||||
ctx = eg.ensureCtx
|
||||
}()
|
||||
|
||||
for {
|
||||
err := eg.Group.Start()
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case errors.Is(err, ErrInvalidGroupState):
|
||||
wCtx.Debug("group start delayed", "err", err)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureStoppedWorker tries to stop the group until it succeeds or fails permanently.
|
||||
func (eg *ExtendedGroup) EnsureStoppedWorker(wCtx *WorkerCtx) error {
|
||||
// Setup worker.
|
||||
var ctx context.Context
|
||||
func() {
|
||||
eg.ensureLock.Lock()
|
||||
defer eg.ensureLock.Unlock()
|
||||
eg.ensureCancel()
|
||||
eg.ensureCtx, eg.ensureCancel = context.WithCancel(wCtx.Ctx())
|
||||
ctx = eg.ensureCtx
|
||||
}()
|
||||
|
||||
for {
|
||||
err := eg.Group.Stop()
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case errors.Is(err, ErrInvalidGroupState):
|
||||
wCtx.Debug("group stop delayed", "err", err)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
226
service/mgr/manager.go
Normal file
226
service/mgr/manager.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ManagerNameSLogKey is used as the logging key for the name of the manager.
|
||||
var ManagerNameSLogKey = "manager"
|
||||
|
||||
// Manager manages workers.
|
||||
type Manager struct {
|
||||
name string
|
||||
logger *slog.Logger
|
||||
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
|
||||
workerCnt atomic.Int32
|
||||
workersDone chan struct{}
|
||||
}
|
||||
|
||||
// New returns a new manager.
|
||||
func New(name string) *Manager {
|
||||
return newManager(name)
|
||||
}
|
||||
|
||||
func newManager(name string) *Manager {
|
||||
m := &Manager{
|
||||
name: name,
|
||||
logger: slog.Default().With(ManagerNameSLogKey, name),
|
||||
workersDone: make(chan struct{}),
|
||||
}
|
||||
m.ctx, m.cancelCtx = context.WithCancel(context.Background())
|
||||
return m
|
||||
}
|
||||
|
||||
// Name returns the manager name.
|
||||
func (m *Manager) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
// setName sets the manager name and resets the logger to use that name.
|
||||
// Not safe for concurrent use with any other module methods.
|
||||
func (m *Manager) setName(newName string) {
|
||||
m.name = newName
|
||||
m.logger = slog.Default().With(ManagerNameSLogKey, m.name)
|
||||
}
|
||||
|
||||
// Ctx returns the worker context.
|
||||
func (m *Manager) Ctx() context.Context {
|
||||
return m.ctx
|
||||
}
|
||||
|
||||
// Cancel cancels the worker context.
|
||||
func (m *Manager) Cancel() {
|
||||
m.cancelCtx()
|
||||
}
|
||||
|
||||
// Done returns the context Done channel.
|
||||
func (m *Manager) Done() <-chan struct{} {
|
||||
return m.ctx.Done()
|
||||
}
|
||||
|
||||
// IsDone checks whether the manager context is done.
|
||||
func (m *Manager) IsDone() bool {
|
||||
return m.ctx.Err() != nil
|
||||
}
|
||||
|
||||
// LogEnabled reports whether the logger emits log records at the given level.
|
||||
// The manager context is automatically supplied.
|
||||
func (m *Manager) LogEnabled(level slog.Level) bool {
|
||||
return m.logger.Enabled(m.ctx, level)
|
||||
}
|
||||
|
||||
// Debug logs at LevelDebug.
|
||||
// The manager context is automatically supplied.
|
||||
func (m *Manager) Debug(msg string, args ...any) {
|
||||
if !m.logger.Enabled(m.ctx, slog.LevelDebug) {
|
||||
return
|
||||
}
|
||||
m.writeLog(slog.LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
// Info logs at LevelInfo.
|
||||
// The manager context is automatically supplied.
|
||||
func (m *Manager) Info(msg string, args ...any) {
|
||||
if !m.logger.Enabled(m.ctx, slog.LevelInfo) {
|
||||
return
|
||||
}
|
||||
m.writeLog(slog.LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
// Warn logs at LevelWarn.
|
||||
// The manager context is automatically supplied.
|
||||
func (m *Manager) Warn(msg string, args ...any) {
|
||||
if !m.logger.Enabled(m.ctx, slog.LevelWarn) {
|
||||
return
|
||||
}
|
||||
m.writeLog(slog.LevelWarn, msg, args...)
|
||||
}
|
||||
|
||||
// Error logs at LevelError.
|
||||
// The manager context is automatically supplied.
|
||||
func (m *Manager) Error(msg string, args ...any) {
|
||||
if !m.logger.Enabled(m.ctx, slog.LevelError) {
|
||||
return
|
||||
}
|
||||
m.writeLog(slog.LevelError, msg, args...)
|
||||
}
|
||||
|
||||
// Log emits a log record with the current time and the given level and message.
|
||||
// The manager context is automatically supplied.
|
||||
func (m *Manager) Log(level slog.Level, msg string, args ...any) {
|
||||
if !m.logger.Enabled(m.ctx, level) {
|
||||
return
|
||||
}
|
||||
m.writeLog(level, msg, args...)
|
||||
}
|
||||
|
||||
// LogAttrs is a more efficient version of Log() that accepts only Attrs.
|
||||
// The manager context is automatically supplied.
|
||||
func (m *Manager) LogAttrs(level slog.Level, msg string, attrs ...slog.Attr) {
|
||||
if !m.logger.Enabled(m.ctx, level) {
|
||||
return
|
||||
}
|
||||
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(2, pcs[:]) // skip "Callers" and "LogAttrs".
|
||||
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
||||
r.AddAttrs(attrs...)
|
||||
_ = m.logger.Handler().Handle(m.ctx, r)
|
||||
}
|
||||
|
||||
func (m *Manager) writeLog(level slog.Level, msg string, args ...any) {
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(3, pcs[:]) // skip "Callers", "writeLog" and the calling function.
|
||||
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
||||
r.Add(args...)
|
||||
_ = m.logger.Handler().Handle(m.ctx, r)
|
||||
}
|
||||
|
||||
// WaitForWorkers waits for all workers of this manager to be done.
|
||||
// The default maximum waiting time is one minute.
|
||||
func (m *Manager) WaitForWorkers(max time.Duration) (done bool) {
|
||||
return m.waitForWorkers(max, 0)
|
||||
}
|
||||
|
||||
// WaitForWorkersFromStop is a special version of WaitForWorkers, meant to be called from the stop routine.
|
||||
// It waits for all workers of this manager to be done, except for the Stop function.
|
||||
// The default maximum waiting time is one minute.
|
||||
func (m *Manager) WaitForWorkersFromStop(max time.Duration) (done bool) {
|
||||
return m.waitForWorkers(max, 1)
|
||||
}
|
||||
|
||||
func (m *Manager) waitForWorkers(max time.Duration, limit int32) (done bool) {
|
||||
// Return immediately if there are no workers.
|
||||
if m.workerCnt.Load() <= limit {
|
||||
return true
|
||||
}
|
||||
|
||||
// Setup timers.
|
||||
reCheckDuration := 10 * time.Millisecond
|
||||
if max <= 0 {
|
||||
max = time.Minute
|
||||
}
|
||||
reCheck := time.NewTimer(reCheckDuration)
|
||||
maxWait := time.NewTimer(max)
|
||||
defer reCheck.Stop()
|
||||
defer maxWait.Stop()
|
||||
|
||||
// Wait for workers to finish, plus check the count in intervals.
|
||||
for {
|
||||
if m.workerCnt.Load() <= limit {
|
||||
return true
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.workersDone:
|
||||
if m.workerCnt.Load() <= limit {
|
||||
return true
|
||||
}
|
||||
|
||||
case <-reCheck.C:
|
||||
// Check worker count again.
|
||||
// This is a dead simple and effective way to avoid all the channel race conditions.
|
||||
reCheckDuration *= 2
|
||||
reCheck.Reset(reCheckDuration)
|
||||
|
||||
case <-maxWait.C:
|
||||
return m.workerCnt.Load() <= limit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) workerStart() {
|
||||
m.workerCnt.Add(1)
|
||||
}
|
||||
|
||||
func (m *Manager) workerDone() {
|
||||
if m.workerCnt.Add(-1) <= 1 {
|
||||
// Notify all waiters.
|
||||
for {
|
||||
select {
|
||||
case m.workersDone <- struct{}{}:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset resets the manager in order to be able to be used again.
|
||||
// In the process, the current context is canceled.
|
||||
// As part of a module (in a group), the module might be stopped and started again.
|
||||
// This method is not goroutine-safe. The caller must make sure the manager is
|
||||
// not being used in any way during execution.
|
||||
func (m *Manager) Reset() {
|
||||
m.cancelCtx()
|
||||
m.ctx, m.cancelCtx = context.WithCancel(context.Background())
|
||||
m.workerCnt.Store(0)
|
||||
m.workersDone = make(chan struct{})
|
||||
}
|
||||
58
service/mgr/sleepyticker.go
Normal file
58
service/mgr/sleepyticker.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package mgr
|
||||
|
||||
import "time"
|
||||
|
||||
// SleepyTicker is wrapper over time.Ticker that respects the sleep mode of the module.
|
||||
type SleepyTicker struct {
|
||||
ticker time.Ticker
|
||||
normalDuration time.Duration
|
||||
sleepDuration time.Duration
|
||||
sleepMode bool
|
||||
|
||||
sleepChannel chan time.Time
|
||||
}
|
||||
|
||||
// NewSleepyTicker returns a new SleepyTicker. This is a wrapper of the standard time.Ticker but it respects modules.Module sleep mode. Check https://pkg.go.dev/time#Ticker.
|
||||
// If sleepDuration is set to 0 ticker will not tick during sleep.
|
||||
func NewSleepyTicker(normalDuration time.Duration, sleepDuration time.Duration) *SleepyTicker {
|
||||
st := &SleepyTicker{
|
||||
ticker: *time.NewTicker(normalDuration),
|
||||
normalDuration: normalDuration,
|
||||
sleepDuration: sleepDuration,
|
||||
sleepMode: false,
|
||||
}
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
// Wait waits until the module is not in sleep mode and returns time.Ticker.C channel.
|
||||
func (st *SleepyTicker) Wait() <-chan time.Time {
|
||||
if st.sleepMode && st.sleepDuration == 0 {
|
||||
return st.sleepChannel
|
||||
}
|
||||
return st.ticker.C
|
||||
}
|
||||
|
||||
// Stop turns off a ticker. After Stop, no more ticks will be sent. Stop does not close the channel, to prevent a concurrent goroutine reading from the channel from seeing an erroneous "tick".
|
||||
func (st *SleepyTicker) Stop() {
|
||||
st.ticker.Stop()
|
||||
}
|
||||
|
||||
// SetSleep sets the sleep mode of the ticker. If enabled is true, the ticker will tick with sleepDuration. If enabled is false, the ticker will tick with normalDuration.
|
||||
func (st *SleepyTicker) SetSleep(enabled bool) {
|
||||
st.sleepMode = enabled
|
||||
if enabled {
|
||||
if st.sleepDuration > 0 {
|
||||
st.ticker.Reset(st.sleepDuration)
|
||||
} else {
|
||||
// Next call to Wait will wait until SetSleep is called with enabled == false
|
||||
st.sleepChannel = make(chan time.Time)
|
||||
}
|
||||
} else {
|
||||
st.ticker.Reset(st.normalDuration)
|
||||
if st.sleepDuration > 0 {
|
||||
// Notify that we are not sleeping anymore.
|
||||
close(st.sleepChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
188
service/mgr/states.go
Normal file
188
service/mgr/states.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StateMgr is a simple state manager.
|
||||
type StateMgr struct {
|
||||
states []State
|
||||
statesLock sync.Mutex
|
||||
|
||||
statesEventMgr *EventMgr[StateUpdate]
|
||||
|
||||
mgr *Manager
|
||||
}
|
||||
|
||||
// State describes the state of a manager or module.
|
||||
type State struct {
|
||||
// ID is a program-unique ID.
|
||||
// It must not only be unique within the StateMgr, but for the whole program,
|
||||
// as it may be re-used with related systems.
|
||||
// Required.
|
||||
ID string
|
||||
|
||||
// Name is the name of the state.
|
||||
// This may also serve as a notification title.
|
||||
// Required.
|
||||
Name string
|
||||
|
||||
// Message is a more detailed message about the state.
|
||||
// Optional.
|
||||
Message string
|
||||
|
||||
// Type defines the type of the state.
|
||||
// Optional.
|
||||
Type StateType
|
||||
|
||||
// Time is the time when the state was created or the originating incident occurred.
|
||||
// Optional, will be set to current time if not set.
|
||||
Time time.Time
|
||||
|
||||
// Data can hold any additional data necessary for further processing of connected systems.
|
||||
// Optional.
|
||||
Data any
|
||||
}
|
||||
|
||||
// StateType defines commonly used states.
|
||||
type StateType string
|
||||
|
||||
// State Types.
|
||||
const (
|
||||
StateTypeUndefined = ""
|
||||
StateTypeHint = "hint"
|
||||
StateTypeWarning = "warning"
|
||||
StateTypeError = "error"
|
||||
)
|
||||
|
||||
// Severity returns a number representing the gravity of the state for ordering.
|
||||
func (st StateType) Severity() int {
|
||||
switch st {
|
||||
case StateTypeUndefined:
|
||||
return 0
|
||||
case StateTypeHint:
|
||||
return 1
|
||||
case StateTypeWarning:
|
||||
return 2
|
||||
case StateTypeError:
|
||||
return 3
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// StateUpdate is used to update others about a state change.
|
||||
type StateUpdate struct {
|
||||
Module string
|
||||
States []State
|
||||
}
|
||||
|
||||
// StatefulModule is used for interface checks on modules.
|
||||
type StatefulModule interface {
|
||||
States() *StateMgr
|
||||
}
|
||||
|
||||
// NewStateMgr returns a new state manager.
|
||||
func NewStateMgr(mgr *Manager) *StateMgr {
|
||||
return &StateMgr{
|
||||
statesEventMgr: NewEventMgr[StateUpdate]("state update", mgr),
|
||||
mgr: mgr,
|
||||
}
|
||||
}
|
||||
|
||||
// NewStateMgr returns a new state manager.
|
||||
func (m *Manager) NewStateMgr() *StateMgr {
|
||||
return NewStateMgr(m)
|
||||
}
|
||||
|
||||
// Add adds a state.
|
||||
// If a state with the same ID already exists, it is replaced.
|
||||
func (m *StateMgr) Add(s State) {
|
||||
m.statesLock.Lock()
|
||||
defer m.statesLock.Unlock()
|
||||
|
||||
if s.Time.IsZero() {
|
||||
s.Time = time.Now()
|
||||
}
|
||||
|
||||
// Update or add state.
|
||||
index := slices.IndexFunc(m.states, func(es State) bool {
|
||||
return es.ID == s.ID
|
||||
})
|
||||
if index >= 0 {
|
||||
m.states[index] = s
|
||||
} else {
|
||||
m.states = append(m.states, s)
|
||||
}
|
||||
|
||||
m.statesEventMgr.Submit(m.export())
|
||||
}
|
||||
|
||||
// Remove removes the state with the given ID.
|
||||
func (m *StateMgr) Remove(id string) {
|
||||
m.statesLock.Lock()
|
||||
defer m.statesLock.Unlock()
|
||||
|
||||
// Quick check if slice is empty.
|
||||
// It is a common pattern to remove a state when no error was encountered at
|
||||
// a critical operation. This means that StateMgr.Remove will be called often.
|
||||
if len(m.states) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var entryRemoved bool
|
||||
m.states = slices.DeleteFunc(m.states, func(s State) bool {
|
||||
if s.ID == id {
|
||||
entryRemoved = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if entryRemoved {
|
||||
m.statesEventMgr.Submit(m.export())
|
||||
}
|
||||
}
|
||||
|
||||
// Clear removes all states.
|
||||
func (m *StateMgr) Clear() {
|
||||
m.statesLock.Lock()
|
||||
defer m.statesLock.Unlock()
|
||||
|
||||
m.states = nil
|
||||
|
||||
m.statesEventMgr.Submit(m.export())
|
||||
}
|
||||
|
||||
// Export returns the current states.
|
||||
func (m *StateMgr) Export() StateUpdate {
|
||||
m.statesLock.Lock()
|
||||
defer m.statesLock.Unlock()
|
||||
|
||||
return m.export()
|
||||
}
|
||||
|
||||
// export returns the current states.
|
||||
func (m *StateMgr) export() StateUpdate {
|
||||
name := ""
|
||||
if m.mgr != nil {
|
||||
name = m.mgr.name
|
||||
}
|
||||
|
||||
return StateUpdate{
|
||||
Module: name,
|
||||
States: slices.Clone(m.states),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe subscribes to state update events.
|
||||
func (m *StateMgr) Subscribe(subscriberName string, chanSize int) *EventSubscription[StateUpdate] {
|
||||
return m.statesEventMgr.Subscribe(subscriberName, chanSize)
|
||||
}
|
||||
|
||||
// AddCallback adds a callback to state update events.
|
||||
func (m *StateMgr) AddCallback(callbackName string, callback EventCallbackFunc[StateUpdate]) {
|
||||
m.statesEventMgr.AddCallback(callbackName, callback)
|
||||
}
|
||||
351
service/mgr/worker.go
Normal file
351
service/mgr/worker.go
Normal file
@@ -0,0 +1,351 @@
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// workerContextKey is a key used for the context key/value storage.
|
||||
type workerContextKey struct{}
|
||||
|
||||
// WorkerCtxContextKey is the key used to add the WorkerCtx to a context.
|
||||
var WorkerCtxContextKey = workerContextKey{}
|
||||
|
||||
// WorkerCtx provides workers with the necessary environment for flow control
|
||||
// and logging.
|
||||
type WorkerCtx struct {
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
|
||||
workerMgr *WorkerMgr // TODO: Attach to context instead?
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// AddToCtx adds the WorkerCtx to the given context.
|
||||
func (w *WorkerCtx) AddToCtx(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, WorkerCtxContextKey, w)
|
||||
}
|
||||
|
||||
// WorkerFromCtx returns the WorkerCtx from the given context.
|
||||
func WorkerFromCtx(ctx context.Context) *WorkerCtx {
|
||||
v := ctx.Value(WorkerCtxContextKey)
|
||||
if w, ok := v.(*WorkerCtx); ok {
|
||||
return w
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ctx returns the worker context.
|
||||
// Is automatically canceled after the worker stops/returns, regardless of error.
|
||||
func (w *WorkerCtx) Ctx() context.Context {
|
||||
return w.ctx
|
||||
}
|
||||
|
||||
// Cancel cancels the worker context.
|
||||
// Is automatically called after the worker stops/returns, regardless of error.
|
||||
func (w *WorkerCtx) Cancel() {
|
||||
w.cancelCtx()
|
||||
}
|
||||
|
||||
// WorkerMgr returns the worker manager the worker was started from.
|
||||
// Returns nil if the worker is not associated with a scheduler.
|
||||
func (w *WorkerCtx) WorkerMgr() *WorkerMgr {
|
||||
return w.workerMgr
|
||||
}
|
||||
|
||||
// Done returns the context Done channel.
|
||||
func (w *WorkerCtx) Done() <-chan struct{} {
|
||||
return w.ctx.Done()
|
||||
}
|
||||
|
||||
// IsDone checks whether the worker context is done.
|
||||
func (w *WorkerCtx) IsDone() bool {
|
||||
return w.ctx.Err() != nil
|
||||
}
|
||||
|
||||
// Logger returns the logger used by the worker context.
|
||||
func (w *WorkerCtx) Logger() *slog.Logger {
|
||||
return w.logger
|
||||
}
|
||||
|
||||
// LogEnabled reports whether the logger emits log records at the given level.
|
||||
// The worker context is automatically supplied.
|
||||
func (w *WorkerCtx) LogEnabled(level slog.Level) bool {
|
||||
return w.logger.Enabled(w.ctx, level)
|
||||
}
|
||||
|
||||
// Debug logs at LevelDebug.
|
||||
// The worker context is automatically supplied.
|
||||
func (w *WorkerCtx) Debug(msg string, args ...any) {
|
||||
if !w.logger.Enabled(w.ctx, slog.LevelDebug) {
|
||||
return
|
||||
}
|
||||
w.writeLog(slog.LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
// Info logs at LevelInfo.
|
||||
// The worker context is automatically supplied.
|
||||
func (w *WorkerCtx) Info(msg string, args ...any) {
|
||||
if !w.logger.Enabled(w.ctx, slog.LevelInfo) {
|
||||
return
|
||||
}
|
||||
w.writeLog(slog.LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
// Warn logs at LevelWarn.
|
||||
// The worker context is automatically supplied.
|
||||
func (w *WorkerCtx) Warn(msg string, args ...any) {
|
||||
if !w.logger.Enabled(w.ctx, slog.LevelWarn) {
|
||||
return
|
||||
}
|
||||
w.writeLog(slog.LevelWarn, msg, args...)
|
||||
}
|
||||
|
||||
// Error logs at LevelError.
|
||||
// The worker context is automatically supplied.
|
||||
func (w *WorkerCtx) Error(msg string, args ...any) {
|
||||
if !w.logger.Enabled(w.ctx, slog.LevelError) {
|
||||
return
|
||||
}
|
||||
w.writeLog(slog.LevelError, msg, args...)
|
||||
}
|
||||
|
||||
// Log emits a log record with the current time and the given level and message.
|
||||
// The worker context is automatically supplied.
|
||||
func (w *WorkerCtx) Log(level slog.Level, msg string, args ...any) {
|
||||
if !w.logger.Enabled(w.ctx, level) {
|
||||
return
|
||||
}
|
||||
w.writeLog(level, msg, args...)
|
||||
}
|
||||
|
||||
// LogAttrs is a more efficient version of Log() that accepts only Attrs.
|
||||
// The worker context is automatically supplied.
|
||||
func (w *WorkerCtx) LogAttrs(level slog.Level, msg string, attrs ...slog.Attr) {
|
||||
if !w.logger.Enabled(w.ctx, level) {
|
||||
return
|
||||
}
|
||||
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(2, pcs[:]) // skip "Callers" and "LogAttrs".
|
||||
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
||||
r.AddAttrs(attrs...)
|
||||
_ = w.logger.Handler().Handle(w.ctx, r)
|
||||
}
|
||||
|
||||
func (w *WorkerCtx) writeLog(level slog.Level, msg string, args ...any) {
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(3, pcs[:]) // skip "Callers", "writeLog" and the calling function.
|
||||
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
||||
r.Add(args...)
|
||||
_ = w.logger.Handler().Handle(w.ctx, r)
|
||||
}
|
||||
|
||||
// Go starts the given function in a goroutine (as a "worker").
|
||||
// The worker context has
|
||||
// - A separate context which is canceled when the functions returns.
|
||||
// - Access to named structure logging.
|
||||
// - Given function is re-run after failure (with backoff).
|
||||
// - Panic catching.
|
||||
// - Flow control helpers.
|
||||
func (m *Manager) Go(name string, fn func(w *WorkerCtx) error) {
|
||||
// m.logger.Log(m.ctx, slog.LevelInfo, "worker started", "name", name)
|
||||
go m.manageWorker(name, fn)
|
||||
}
|
||||
|
||||
func (m *Manager) manageWorker(name string, fn func(w *WorkerCtx) error) {
|
||||
m.workerStart()
|
||||
defer m.workerDone()
|
||||
|
||||
w := &WorkerCtx{
|
||||
logger: m.logger.With("worker", name),
|
||||
}
|
||||
w.ctx = m.ctx
|
||||
|
||||
backoff := time.Second
|
||||
failCnt := 0
|
||||
|
||||
for {
|
||||
panicInfo, err := m.runWorker(w, fn)
|
||||
switch {
|
||||
case err == nil:
|
||||
// No error means that the worker is finished.
|
||||
return
|
||||
|
||||
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
|
||||
// A canceled context or exceeded deadline also means that the worker is finished.
|
||||
return
|
||||
|
||||
default:
|
||||
// Any other errors triggers a restart with backoff.
|
||||
|
||||
// If manager is stopping, just log error and return.
|
||||
if m.IsDone() {
|
||||
if panicInfo != "" {
|
||||
w.Error(
|
||||
"worker failed",
|
||||
"err", err,
|
||||
"file", panicInfo,
|
||||
)
|
||||
} else {
|
||||
w.Error(
|
||||
"worker failed",
|
||||
"err", err,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Count failure and increase backoff (up to limit),
|
||||
failCnt++
|
||||
backoff *= 2
|
||||
if backoff > time.Minute {
|
||||
backoff = time.Minute
|
||||
}
|
||||
|
||||
// Log error and retry after backoff duration.
|
||||
if panicInfo != "" {
|
||||
w.Error(
|
||||
"worker failed",
|
||||
"failCnt", failCnt,
|
||||
"backoff", backoff,
|
||||
"err", err,
|
||||
"file", panicInfo,
|
||||
)
|
||||
} else {
|
||||
w.Error(
|
||||
"worker failed",
|
||||
"failCnt", failCnt,
|
||||
"backoff", backoff,
|
||||
"err", err,
|
||||
)
|
||||
}
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
case <-m.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do directly executes the given function (as a "worker").
|
||||
// The worker context has
|
||||
// - A separate context which is canceled when the functions returns.
|
||||
// - Access to named structure logging.
|
||||
// - Given function is re-run after failure (with backoff).
|
||||
// - Panic catching.
|
||||
// - Flow control helpers.
|
||||
func (m *Manager) Do(name string, fn func(w *WorkerCtx) error) error {
|
||||
m.workerStart()
|
||||
defer m.workerDone()
|
||||
|
||||
// Create context.
|
||||
w := &WorkerCtx{
|
||||
ctx: m.Ctx(),
|
||||
logger: m.logger.With("worker", name),
|
||||
}
|
||||
|
||||
// Run worker.
|
||||
panicInfo, err := m.runWorker(w, fn)
|
||||
switch {
|
||||
case err == nil:
|
||||
// No error means that the worker is finished.
|
||||
return nil
|
||||
|
||||
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
|
||||
// A canceled context or exceeded deadline also means that the worker is finished.
|
||||
return err
|
||||
|
||||
default:
|
||||
// Log error and return.
|
||||
if panicInfo != "" {
|
||||
w.Error(
|
||||
"worker failed",
|
||||
"err", err,
|
||||
"file", panicInfo,
|
||||
)
|
||||
} else {
|
||||
w.Error(
|
||||
"worker failed",
|
||||
"err", err,
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) runWorker(w *WorkerCtx, fn func(w *WorkerCtx) error) (panicInfo string, err error) {
|
||||
// Create worker context that is canceled when worker finished or dies.
|
||||
w.ctx, w.cancelCtx = context.WithCancel(w.ctx)
|
||||
defer w.Cancel()
|
||||
|
||||
// Recover from panic.
|
||||
defer func() {
|
||||
panicVal := recover()
|
||||
if panicVal != nil {
|
||||
err = fmt.Errorf("panic: %s", panicVal)
|
||||
|
||||
// Print panic to stderr.
|
||||
stackTrace := string(debug.Stack())
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"===== PANIC =====\n%s\n\n%s===== END =====\n",
|
||||
panicVal,
|
||||
stackTrace,
|
||||
)
|
||||
|
||||
// Find the line in the stack trace that refers to where the panic occurred.
|
||||
stackLines := strings.Split(stackTrace, "\n")
|
||||
foundPanic := false
|
||||
for i, line := range stackLines {
|
||||
if !foundPanic {
|
||||
if strings.Contains(line, "panic(") {
|
||||
foundPanic = true
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(line, "portmaster") {
|
||||
if i+1 < len(stackLines) {
|
||||
panicInfo = strings.SplitN(strings.TrimSpace(stackLines[i+1]), " ", 2)[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = fn(w)
|
||||
return //nolint
|
||||
}
|
||||
|
||||
// Repeat executes the given function periodically in a goroutine (as a "worker").
|
||||
// The worker context has
|
||||
// - A separate context which is canceled when the functions returns.
|
||||
// - Access to named structure logging.
|
||||
// - By default error/panic will be logged. For custom behavior supply errorFn, the argument is optional.
|
||||
// - Flow control helpers.
|
||||
// - Repeat is intended for long running tasks that are mostly idle.
|
||||
func (m *Manager) Repeat(name string, period time.Duration, fn func(w *WorkerCtx) error) *WorkerMgr {
|
||||
t := m.NewWorkerMgr(name, fn, nil)
|
||||
return t.Repeat(period)
|
||||
}
|
||||
|
||||
// Delay starts the given function delayed in a goroutine (as a "worker").
|
||||
// The worker context has
|
||||
// - A separate context which is canceled when the functions returns.
|
||||
// - Access to named structure logging.
|
||||
// - By default error/panic will be logged. For custom behavior supply errorFn, the argument is optional.
|
||||
// - Panic catching.
|
||||
// - Flow control helpers.
|
||||
func (m *Manager) Delay(name string, period time.Duration, fn func(w *WorkerCtx) error) *WorkerMgr {
|
||||
t := m.NewWorkerMgr(name, fn, nil)
|
||||
return t.Delay(period)
|
||||
}
|
||||
319
service/mgr/workermgr.go
Normal file
319
service/mgr/workermgr.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WorkerMgr schedules a worker.
|
||||
type WorkerMgr struct {
|
||||
mgr *Manager
|
||||
ctx *WorkerCtx
|
||||
|
||||
// Definition.
|
||||
name string
|
||||
fn func(w *WorkerCtx) error
|
||||
errorFn func(c *WorkerCtx, err error, panicInfo string)
|
||||
|
||||
// Manual trigger.
|
||||
run chan struct{}
|
||||
|
||||
// Actions.
|
||||
actionLock sync.Mutex
|
||||
selectAction chan struct{}
|
||||
delay *workerMgrDelay
|
||||
repeat *workerMgrRepeat
|
||||
keepAlive *workerMgrNoop
|
||||
}
|
||||
|
||||
type taskAction interface {
|
||||
Wait() <-chan time.Time
|
||||
Ack()
|
||||
}
|
||||
|
||||
// Delay.
|
||||
type workerMgrDelay struct {
|
||||
s *WorkerMgr
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
func (s *WorkerMgr) newDelay(duration time.Duration) *workerMgrDelay {
|
||||
return &workerMgrDelay{
|
||||
s: s,
|
||||
timer: time.NewTimer(duration),
|
||||
}
|
||||
}
|
||||
func (sd *workerMgrDelay) Wait() <-chan time.Time { return sd.timer.C }
|
||||
|
||||
func (sd *workerMgrDelay) Ack() {
|
||||
sd.s.actionLock.Lock()
|
||||
defer sd.s.actionLock.Unlock()
|
||||
|
||||
// Remove delay, as it can only fire once.
|
||||
sd.s.delay = nil
|
||||
|
||||
// Reset repeat.
|
||||
sd.s.repeat.Reset()
|
||||
|
||||
// Stop timer.
|
||||
sd.timer.Stop()
|
||||
}
|
||||
|
||||
func (sd *workerMgrDelay) Stop() {
|
||||
if sd == nil {
|
||||
return
|
||||
}
|
||||
sd.timer.Stop()
|
||||
}
|
||||
|
||||
// Repeat.
|
||||
type workerMgrRepeat struct {
|
||||
ticker *time.Ticker
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func (s *WorkerMgr) newRepeat(interval time.Duration) *workerMgrRepeat {
|
||||
return &workerMgrRepeat{
|
||||
ticker: time.NewTicker(interval),
|
||||
interval: interval,
|
||||
}
|
||||
}
|
||||
|
||||
func (sr *workerMgrRepeat) Wait() <-chan time.Time { return sr.ticker.C }
|
||||
func (sr *workerMgrRepeat) Ack() {}
|
||||
|
||||
func (sr *workerMgrRepeat) Reset() {
|
||||
if sr == nil {
|
||||
return
|
||||
}
|
||||
sr.ticker.Reset(sr.interval)
|
||||
}
|
||||
|
||||
func (sr *workerMgrRepeat) Stop() {
|
||||
if sr == nil {
|
||||
return
|
||||
}
|
||||
sr.ticker.Stop()
|
||||
}
|
||||
|
||||
// Noop.
|
||||
type workerMgrNoop struct{}
|
||||
|
||||
func (sn *workerMgrNoop) Wait() <-chan time.Time { return nil }
|
||||
func (sn *workerMgrNoop) Ack() {}
|
||||
|
||||
// NewWorkerMgr creates a new scheduler for the given worker function.
|
||||
// Errors and panic will only be logged by default.
|
||||
// If custom behavior is required, supply an errorFn.
|
||||
// If all scheduling has ended, the scheduler will end itself,
|
||||
// including all related workers, except if keep-alive is enabled.
|
||||
func (m *Manager) NewWorkerMgr(name string, fn func(w *WorkerCtx) error, errorFn func(c *WorkerCtx, err error, panicInfo string)) *WorkerMgr {
|
||||
// Create task context.
|
||||
wCtx := &WorkerCtx{
|
||||
logger: m.logger.With("worker", name),
|
||||
}
|
||||
wCtx.ctx, wCtx.cancelCtx = context.WithCancel(m.Ctx())
|
||||
|
||||
s := &WorkerMgr{
|
||||
mgr: m,
|
||||
ctx: wCtx,
|
||||
name: name,
|
||||
fn: fn,
|
||||
errorFn: errorFn,
|
||||
run: make(chan struct{}, 1),
|
||||
selectAction: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
go s.taskMgr()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *WorkerMgr) taskMgr() {
|
||||
s.mgr.workerStart()
|
||||
defer s.mgr.workerDone()
|
||||
|
||||
// If the task manager ends, end all descendants too.
|
||||
defer s.ctx.cancelCtx()
|
||||
|
||||
// Timers and tickers.
|
||||
var (
|
||||
action taskAction
|
||||
)
|
||||
defer func() {
|
||||
s.delay.Stop()
|
||||
s.repeat.Stop()
|
||||
}()
|
||||
|
||||
// Wait for the first action.
|
||||
select {
|
||||
case <-s.selectAction:
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
manage:
|
||||
for {
|
||||
// Select action.
|
||||
func() {
|
||||
s.actionLock.Lock()
|
||||
defer s.actionLock.Unlock()
|
||||
|
||||
switch {
|
||||
case s.delay != nil:
|
||||
action = s.delay
|
||||
case s.repeat != nil:
|
||||
action = s.repeat
|
||||
case s.keepAlive != nil:
|
||||
action = s.keepAlive
|
||||
default:
|
||||
action = nil
|
||||
}
|
||||
}()
|
||||
if action == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for trigger or action.
|
||||
select {
|
||||
case <-action.Wait():
|
||||
action.Ack()
|
||||
// Time-triggered execution.
|
||||
case <-s.run:
|
||||
// Manually triggered execution.
|
||||
case <-s.selectAction:
|
||||
// Re-select action.
|
||||
continue manage
|
||||
case <-s.ctx.Done():
|
||||
// Abort!
|
||||
return
|
||||
}
|
||||
|
||||
// Run worker.
|
||||
wCtx := &WorkerCtx{
|
||||
workerMgr: s,
|
||||
logger: s.ctx.logger,
|
||||
}
|
||||
wCtx.ctx, wCtx.cancelCtx = context.WithCancel(s.ctx.ctx)
|
||||
panicInfo, err := s.mgr.runWorker(wCtx, s.fn)
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
// Continue with scheduling.
|
||||
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
|
||||
// Worker was canceled, continue with scheduling.
|
||||
// A canceled context or exceeded deadline also means that the worker is finished.
|
||||
|
||||
default:
|
||||
// Log error and return.
|
||||
if panicInfo != "" {
|
||||
wCtx.Error(
|
||||
"worker failed",
|
||||
"err", err,
|
||||
"file", panicInfo,
|
||||
)
|
||||
} else {
|
||||
wCtx.Error(
|
||||
"worker failed",
|
||||
"err", err,
|
||||
)
|
||||
}
|
||||
|
||||
// Delegate error handling to the error function, otherwise just continue the scheduler.
|
||||
// The error handler can stop the scheduler if it wants to.
|
||||
if s.errorFn != nil {
|
||||
s.errorFn(s.ctx, err, panicInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go executes the worker immediately.
|
||||
// If the worker is currently being executed,
|
||||
// the next execution will commence afterwards.
|
||||
// Calling Go() implies KeepAlive() if nothing else was specified yet.
|
||||
func (s *WorkerMgr) Go() {
|
||||
s.actionLock.Lock()
|
||||
defer s.actionLock.Unlock()
|
||||
|
||||
// Check if any action is already defined.
|
||||
switch {
|
||||
case s.delay != nil:
|
||||
case s.repeat != nil:
|
||||
case s.keepAlive != nil:
|
||||
default:
|
||||
s.keepAlive = &workerMgrNoop{}
|
||||
s.check()
|
||||
}
|
||||
|
||||
// Reset repeat if set.
|
||||
s.repeat.Reset()
|
||||
|
||||
// Stop delay if set.
|
||||
s.delay.Stop()
|
||||
s.delay = nil
|
||||
|
||||
// Send run command
|
||||
select {
|
||||
case s.run <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Stop immediately stops the scheduler and all related workers.
|
||||
func (s *WorkerMgr) Stop() {
|
||||
s.ctx.cancelCtx()
|
||||
}
|
||||
|
||||
// Delay will schedule the worker to run after the given duration.
|
||||
// If set, the repeat schedule will continue afterwards.
|
||||
// Disable the delay by passing 0.
|
||||
func (s *WorkerMgr) Delay(duration time.Duration) *WorkerMgr {
|
||||
s.actionLock.Lock()
|
||||
defer s.actionLock.Unlock()
|
||||
|
||||
s.delay.Stop()
|
||||
s.delay = nil
|
||||
if duration > 0 {
|
||||
s.delay = s.newDelay(duration)
|
||||
}
|
||||
|
||||
s.check()
|
||||
return s
|
||||
}
|
||||
|
||||
// Repeat will repeatedly execute the worker using the given interval.
|
||||
// Disable repeating by passing 0.
|
||||
func (s *WorkerMgr) Repeat(interval time.Duration) *WorkerMgr {
|
||||
s.actionLock.Lock()
|
||||
defer s.actionLock.Unlock()
|
||||
|
||||
s.repeat.Stop()
|
||||
s.repeat = nil
|
||||
if interval > 0 {
|
||||
s.repeat = s.newRepeat(interval)
|
||||
}
|
||||
|
||||
s.check()
|
||||
return s
|
||||
}
|
||||
|
||||
// KeepAlive instructs the scheduler to not self-destruct,
|
||||
// even if all scheduled work is complete.
|
||||
func (s *WorkerMgr) KeepAlive() *WorkerMgr {
|
||||
s.actionLock.Lock()
|
||||
defer s.actionLock.Unlock()
|
||||
|
||||
s.keepAlive = &workerMgrNoop{}
|
||||
|
||||
s.check()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *WorkerMgr) check() {
|
||||
select {
|
||||
case s.selectAction <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
157
service/mgr/workermgr_test.go
Normal file
157
service/mgr/workermgr_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWorkerMgrDelay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := New("DelayTest")
|
||||
|
||||
value := atomic.Bool{}
|
||||
value.Store(false)
|
||||
|
||||
// Create a task that will after 1 second.
|
||||
m.NewWorkerMgr("test", func(w *WorkerCtx) error {
|
||||
value.Store(true)
|
||||
return nil
|
||||
}, nil).Delay(1 * time.Second)
|
||||
|
||||
// Check if value is set after 1 second and not before or after.
|
||||
iterations := 0
|
||||
for !value.Load() {
|
||||
iterations++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 5% difference is acceptable since time.Sleep can't be perfect and it may very on different computers.
|
||||
if iterations < 95 || iterations > 105 {
|
||||
t.Errorf("WorkerMgr did not delay for a whole second it=%d", iterations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerMgrRepeat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := New("RepeatTest")
|
||||
|
||||
value := atomic.Bool{}
|
||||
value.Store(false)
|
||||
|
||||
// Create a task that should repeat every 100 milliseconds.
|
||||
m.NewWorkerMgr("test", func(w *WorkerCtx) error {
|
||||
value.Store(true)
|
||||
return nil
|
||||
}, nil).Repeat(100 * time.Millisecond)
|
||||
|
||||
// Check 10 consecutive runs they should be delayed for around 100 milliseconds each.
|
||||
for range 10 {
|
||||
iterations := 0
|
||||
for !value.Load() {
|
||||
iterations++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 10% difference is acceptable at this scale since time.Sleep can't be perfect and it may very on different computers.
|
||||
if iterations < 9 || iterations > 11 {
|
||||
t.Errorf("Worker was not delayed for a 100 milliseconds it=%d", iterations)
|
||||
return
|
||||
}
|
||||
// Reset value
|
||||
value.Store(false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerMgrDelayAndRepeat(t *testing.T) { //nolint:dupl
|
||||
t.Parallel()
|
||||
|
||||
m := New("DelayAndRepeatTest")
|
||||
|
||||
value := atomic.Bool{}
|
||||
value.Store(false)
|
||||
|
||||
// Create a task that should delay for 1 second and then repeat every 100 milliseconds.
|
||||
m.NewWorkerMgr("test", func(w *WorkerCtx) error {
|
||||
value.Store(true)
|
||||
return nil
|
||||
}, nil).Delay(1 * time.Second).Repeat(100 * time.Millisecond)
|
||||
|
||||
iterations := 0
|
||||
for !value.Load() {
|
||||
iterations++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 5% difference is acceptable since time.Sleep can't be perfect and it may very on different computers.
|
||||
if iterations < 95 || iterations > 105 {
|
||||
t.Errorf("WorkerMgr did not delay for a whole second it=%d", iterations)
|
||||
}
|
||||
|
||||
// Reset value
|
||||
value.Store(false)
|
||||
|
||||
// Check 10 consecutive runs they should be delayed for around 100 milliseconds each.
|
||||
for range 10 {
|
||||
iterations = 0
|
||||
for !value.Load() {
|
||||
iterations++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 10% difference is acceptable at this scale since time.Sleep can't be perfect and it may very on different computers.
|
||||
if iterations < 9 || iterations > 11 {
|
||||
t.Errorf("Worker was not delayed for a 100 milliseconds it=%d", iterations)
|
||||
return
|
||||
}
|
||||
// Reset value
|
||||
value.Store(false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerMgrRepeatAndDelay(t *testing.T) { //nolint:dupl
|
||||
t.Parallel()
|
||||
|
||||
m := New("RepeatAndDelayTest")
|
||||
|
||||
value := atomic.Bool{}
|
||||
value.Store(false)
|
||||
|
||||
// Create a task that should delay for 1 second and then repeat every 100 milliseconds but with reverse command order.
|
||||
m.NewWorkerMgr("test", func(w *WorkerCtx) error {
|
||||
value.Store(true)
|
||||
return nil
|
||||
}, nil).Repeat(100 * time.Millisecond).Delay(1 * time.Second)
|
||||
|
||||
iterations := 0
|
||||
for !value.Load() {
|
||||
iterations++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 5% difference is acceptable since time.Sleep can't be perfect and it may very on different computers.
|
||||
if iterations < 95 || iterations > 105 {
|
||||
t.Errorf("WorkerMgr did not delay for a whole second it=%d", iterations)
|
||||
}
|
||||
// Reset value
|
||||
value.Store(false)
|
||||
|
||||
// Check 10 consecutive runs they should be delayed for around 100 milliseconds each.
|
||||
for range 10 {
|
||||
iterations := 0
|
||||
for !value.Load() {
|
||||
iterations++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 10% difference is acceptable at this scale since time.Sleep can't be perfect and it may very on different computers.
|
||||
if iterations < 9 || iterations > 11 {
|
||||
t.Errorf("Worker was not delayed for a 100 milliseconds it=%d", iterations)
|
||||
return
|
||||
}
|
||||
// Reset value
|
||||
value.Store(false)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"flag"
|
||||
"runtime"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/service/core"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
processInfo "github.com/shirou/gopsutil/process"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/service/network/state"
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/metrics"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,27 +1,48 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/compat"
|
||||
"github.com/safing/portmaster/service/firewall"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
type NameServer struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
|
||||
states *mgr.StateMgr
|
||||
}
|
||||
|
||||
func (ns *NameServer) Manager() *mgr.Manager {
|
||||
return ns.mgr
|
||||
}
|
||||
|
||||
func (ns *NameServer) States() *mgr.StateMgr {
|
||||
return ns.states
|
||||
}
|
||||
|
||||
func (ns *NameServer) Start() error {
|
||||
return start()
|
||||
}
|
||||
|
||||
func (ns *NameServer) Stop() error {
|
||||
return stop()
|
||||
}
|
||||
|
||||
var (
|
||||
stopListeners bool
|
||||
stopListener1 func() error
|
||||
stopListener2 func() error
|
||||
@@ -31,18 +52,6 @@ var (
|
||||
eventIDListenerFailed = "nameserver:listener-failed"
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("nameserver", prep, start, stop, "core", "resolver")
|
||||
subsystems.Register(
|
||||
"dns",
|
||||
"Secure DNS",
|
||||
"DNS resolver with scoping and DNS-over-TLS",
|
||||
module,
|
||||
"config:dns/",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
return registerConfig()
|
||||
}
|
||||
@@ -101,7 +110,7 @@ func start() error {
|
||||
|
||||
func startListener(ip net.IP, port uint16, first bool) {
|
||||
// Start DNS server as service worker.
|
||||
module.StartServiceWorker("dns resolver", 0, func(ctx context.Context) error {
|
||||
module.mgr.Go("dns resolver", func(ctx *mgr.WorkerCtx) error {
|
||||
// Create DNS server.
|
||||
dnsServer := &dns.Server{
|
||||
Addr: net.JoinHostPort(
|
||||
@@ -139,7 +148,7 @@ func startListener(ip net.IP, port uint16, first bool) {
|
||||
|
||||
// Resolve generic listener error, if primary listener.
|
||||
if first {
|
||||
module.Resolve(eventIDListenerFailed)
|
||||
module.states.Remove(eventIDListenerFailed)
|
||||
}
|
||||
|
||||
// Start listening.
|
||||
@@ -147,7 +156,7 @@ func startListener(ip net.IP, port uint16, first bool) {
|
||||
err := dnsServer.ListenAndServe()
|
||||
if err != nil {
|
||||
// Stop worker without error if we are shutting down.
|
||||
if module.IsStopping() {
|
||||
if module.mgr.IsDone() {
|
||||
return nil
|
||||
}
|
||||
log.Warningf("nameserver: failed to listen on %s: %s", dnsServer.Addr, err)
|
||||
@@ -218,7 +227,7 @@ func handleListenError(err error, ip net.IP, port uint16, primaryListener bool)
|
||||
|
||||
// Attach error to module, if primary listener.
|
||||
if primaryListener {
|
||||
n.AttachToModule(module)
|
||||
n.SyncWithState(module.states)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,3 +295,29 @@ func getListenAddresses(listenAddress string) (ip1, ip2 net.IP, port uint16, err
|
||||
|
||||
return ip1, ip2, uint16(port64), nil
|
||||
}
|
||||
|
||||
var (
|
||||
module *NameServer
|
||||
shimLoaded atomic.Bool
|
||||
)
|
||||
|
||||
// New returns a new NameServer module.
|
||||
func New(instance instance) (*NameServer, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
m := mgr.New("NameServer")
|
||||
module = &NameServer{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
|
||||
states: mgr.NewStateMgr(m),
|
||||
}
|
||||
if err := prep(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface{}
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/firewall"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/nameserver/nsutil"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
@@ -24,8 +25,8 @@ var hostname string
|
||||
const internalError = "internal error: "
|
||||
|
||||
func handleRequestAsWorker(w dns.ResponseWriter, query *dns.Msg) {
|
||||
err := module.RunWorker("handle dns request", func(ctx context.Context) error {
|
||||
return handleRequest(ctx, w, query)
|
||||
err := module.mgr.Do("handle dns request", func(wc *mgr.WorkerCtx) error {
|
||||
return handleRequest(wc.Ctx(), w, query)
|
||||
})
|
||||
if err != nil {
|
||||
log.Warningf("nameserver: failed to handle dns request: %s", err)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
)
|
||||
|
||||
// ErrNilRR is returned when a parsed RR is nil.
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/nameserver/nsutil"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@ package netenv
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portmaster/base/api"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "network/gateways",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
Path: "network/gateways",
|
||||
Read: api.PermitUser,
|
||||
StructFunc: func(ar *api.Request) (i interface{}, err error) {
|
||||
return Gateways(), nil
|
||||
},
|
||||
@@ -21,9 +20,8 @@ func registerAPIEndpoints() error {
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "network/nameservers",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
Path: "network/nameservers",
|
||||
Read: api.PermitUser,
|
||||
StructFunc: func(ar *api.Request) (i interface{}, err error) {
|
||||
return Nameservers(), nil
|
||||
},
|
||||
@@ -34,9 +32,8 @@ func registerAPIEndpoints() error {
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "network/location",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
Path: "network/location",
|
||||
Read: api.PermitUser,
|
||||
StructFunc: func(ar *api.Request) (i interface{}, err error) {
|
||||
locs, ok := GetInternetLocation()
|
||||
if ok {
|
||||
@@ -51,9 +48,8 @@ func registerAPIEndpoints() error {
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "network/location/traceroute",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
Path: "network/location/traceroute",
|
||||
Read: api.PermitUser,
|
||||
StructFunc: func(ar *api.Request) (i interface{}, err error) {
|
||||
return getLocationFromTraceroute(&DeviceLocations{})
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user