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:
Daniel Hååvi
2024-08-09 17:15:48 +02:00
committed by GitHub
parent 10a77498f4
commit 80664d1a27
647 changed files with 37690 additions and 3366 deletions

View File

@@ -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.",

View File

@@ -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"

View File

@@ -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"

View File

@@ -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{}

View File

@@ -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

View File

@@ -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"

View File

@@ -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.",

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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]

View File

@@ -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
View File

@@ -0,0 +1,3 @@
package service
type ServiceConfig struct{}

View File

@@ -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.

View File

@@ -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).

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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.

View File

@@ -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
View 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)
}

View File

@@ -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!

View File

@@ -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 ===")
}
}

View File

@@ -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:

View File

@@ -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"
)

View File

@@ -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"

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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
}

View File

@@ -7,7 +7,7 @@ import (
"sync"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/base/log"
)
var (

View File

@@ -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{}

View File

@@ -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"
)

View File

@@ -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"
)

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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"
)

View File

@@ -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"

View File

@@ -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"
)

View File

@@ -8,7 +8,7 @@ import (
"syscall"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/base/log"
"golang.org/x/sys/windows"
)

View File

@@ -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"
)

View File

@@ -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"

View File

@@ -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"
)

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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"

View File

@@ -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
View 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())
}

View File

@@ -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"
)

View File

@@ -1,7 +1,7 @@
package customlists
import (
"github.com/safing/portbase/config"
"github.com/safing/portmaster/base/config"
)
var (

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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()

View File

@@ -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"

View File

@@ -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"
)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"sync"
"github.com/safing/portbase/database/record"
"github.com/safing/portmaster/base/database/record"
)
type entityRecord struct {

View File

@@ -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()),

View File

@@ -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 {

View 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)
}
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/umahmood/haversine"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/base/utils"
)
const (

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -1,11 +0,0 @@
package geoip
import (
"testing"
"github.com/safing/portmaster/service/core/pmtesting"
)
func TestMain(m *testing.M) {
pmtesting.TestMain(m, module)
}

View File

@@ -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.

View File

@@ -3,7 +3,7 @@ package geoip
import (
"testing"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/base/utils"
)
func TestRegions(t *testing.T) {

View File

@@ -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
View File

@@ -0,0 +1,2 @@
// Package mgr provides simple managing of flow control and logging.
package mgr

175
service/mgr/events.go Normal file
View 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
View 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
View 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
View 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{})
}

View 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
View 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
View 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
View 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:
}
}

View 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)
}
}

View File

@@ -4,7 +4,7 @@ import (
"flag"
"runtime"
"github.com/safing/portbase/config"
"github.com/safing/portmaster/base/config"
"github.com/safing/portmaster/service/core"
)

View File

@@ -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"
)

View File

@@ -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 (

View File

@@ -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{}

View File

@@ -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)

View File

@@ -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.

View File

@@ -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"
)

View File

@@ -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"
)

View File

@@ -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{})
},

View File

@@ -10,7 +10,7 @@ import (
"github.com/godbus/dbus/v5"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/base/log"
)
var (

View File

@@ -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