From 706ce222d02a4f8a1062dfa64ae77eec58c6656d Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 27 Nov 2024 16:16:15 +0100 Subject: [PATCH] WIP --- base/info/version.go | 6 +- base/log/slog.go | 22 +- .../run.go => cmdbase/service.go} | 111 +++++++--- .../run_linux.go => cmdbase/service_linux.go} | 27 +-- .../service_windows.go} | 16 +- cmds/{portmaster-core => cmdbase}/update.go | 16 +- cmds/cmdbase/version.go | 20 ++ cmds/hub/main.go | 184 ++++++---------- cmds/observation-hub/main.go | 198 +++++++----------- cmds/portmaster-core/main.go | 52 +++-- cmds/portmaster-core/main_linux.go | 21 ++ cmds/portmaster-core/main_windows.go | 13 ++ cmds/portmaster-core/recover_linux.go | 19 +- cmds/testsuite/db.go | 2 +- cmds/trafficgen/main.go | 3 +- .../edit-profile-dialog.ts | 6 +- .../shared/netquery/line-chart/line-chart.ts | 2 +- .../shared/netquery/searchbar/searchbar.ts | 2 +- service/broadcasts/notify.go | 4 +- service/config.go | 75 ++++++- service/configure/updates.go | 65 ++++++ service/core/api.go | 128 ++++++++++- service/core/base/global.go | 46 ---- service/core/base/module.go | 10 +- service/core/core.go | 18 +- service/core/update_config.go | 134 ++++++++++++ service/core/update_versions.go | 176 ++++++++++++++++ service/instance.go | 28 ++- service/updates.go | 120 ----------- service/updates/downloader.go | 2 +- service/updates/index.go | 11 +- service/updates/index_scan.go | 11 +- service/updates/module.go | 132 +++++++++++- service/updates/upgrade.go | 2 - spn/instance.go | 57 ++--- 35 files changed, 1138 insertions(+), 601 deletions(-) rename cmds/{portmaster-core/run.go => cmdbase/service.go} (53%) rename cmds/{portmaster-core/run_linux.go => cmdbase/service_linux.go} (81%) rename cmds/{portmaster-core/run_windows.go => cmdbase/service_windows.go} (93%) rename cmds/{portmaster-core => cmdbase}/update.go (88%) create mode 100644 cmds/cmdbase/version.go create mode 100644 cmds/portmaster-core/main_linux.go create mode 100644 cmds/portmaster-core/main_windows.go create mode 100644 service/configure/updates.go delete mode 100644 service/core/base/global.go create mode 100644 service/core/update_config.go create mode 100644 service/core/update_versions.go delete mode 100644 service/updates.go diff --git a/base/info/version.go b/base/info/version.go index 2c6c3058..24247b0d 100644 --- a/base/info/version.go +++ b/base/info/version.go @@ -10,8 +10,6 @@ import ( "sync" ) -// FIXME: version does not show in portmaster - var ( name string license string @@ -167,9 +165,9 @@ func CondensedVersion() string { } return fmt.Sprintf( - "%s %s (%s; built with %s [%s %s] from %s [%s] at %s)", + "%s %s (%s/%s; built with %s [%s %s] from %s [%s] at %s)", info.Name, version, - runtime.GOOS, + runtime.GOOS, runtime.GOARCH, runtime.Version(), runtime.Compiler, cgoInfo, info.Commit, dirtyInfo, info.CommitTime, ) diff --git a/base/log/slog.go b/base/log/slog.go index 5901c146..65dec5ab 100644 --- a/base/log/slog.go +++ b/base/log/slog.go @@ -1,14 +1,19 @@ package log import ( + "io" "log/slog" "os" "runtime" "github.com/lmittmann/tint" + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" ) func setupSLog(level Severity) { + // TODO: Changes in the log level are not yet reflected onto the slog handlers in the modules. + // Set highest possible level, so it can be changed in runtime. handlerLogLevel := level.toSLogLevel() @@ -17,21 +22,23 @@ func setupSLog(level Severity) { switch runtime.GOOS { case "windows": logHandler = tint.NewHandler( - GlobalWriter, + windowsColoring(GlobalWriter), // Enable coloring on Windows. &tint.Options{ AddSource: true, Level: handlerLogLevel, TimeFormat: timeFormat, - NoColor: !GlobalWriter.IsStdout(), // FIXME: also check for tty. + NoColor: !( /* Color: */ GlobalWriter.IsStdout() && isatty.IsTerminal(GlobalWriter.file.Fd())), }, ) + case "linux": logHandler = tint.NewHandler(GlobalWriter, &tint.Options{ AddSource: true, Level: handlerLogLevel, TimeFormat: timeFormat, - NoColor: !GlobalWriter.IsStdout(), // FIXME: also check for tty. + NoColor: !( /* Color: */ GlobalWriter.IsStdout() && isatty.IsTerminal(GlobalWriter.file.Fd())), }) + default: logHandler = tint.NewHandler(os.Stdout, &tint.Options{ AddSource: true, @@ -43,6 +50,11 @@ func setupSLog(level Severity) { // Set as default logger. slog.SetDefault(slog.New(logHandler)) - // Set actual log level. - slog.SetLogLoggerLevel(handlerLogLevel) +} + +func windowsColoring(lw *LogWriter) io.Writer { + if lw.IsStdout() { + return colorable.NewColorable(lw.file) + } + return lw } diff --git a/cmds/portmaster-core/run.go b/cmds/cmdbase/service.go similarity index 53% rename from cmds/portmaster-core/run.go rename to cmds/cmdbase/service.go index 6c41bd56..28bc5e60 100644 --- a/cmds/portmaster-core/run.go +++ b/cmds/cmdbase/service.go @@ -1,12 +1,14 @@ -package main +package cmdbase import ( + "context" "errors" - "flag" "fmt" "io" "log/slog" "os" + "os/exec" + "runtime" "runtime/pprof" "time" @@ -15,14 +17,12 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/spn/conf" ) -var printStackOnExit bool - -func init() { - flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") -} +var ( + RebootOnRestart bool + PrintStackOnExit bool +) type SystemService interface { Run() @@ -30,21 +30,47 @@ type SystemService interface { RestartService() error } -func cmdRun(cmd *cobra.Command, args []string) { - // Run platform specific setup or switches. - runPlatformSpecifics(cmd, args) +type ServiceInstance interface { + Ready() bool + Start() error + Stop() error + Restart() + Shutdown() + Ctx() context.Context + IsShuttingDown() bool + ShuttingDown() <-chan struct{} + ShutdownCtx() context.Context + IsShutDown() bool + ShutdownComplete() <-chan struct{} + ExitCode() int + ShouldRestartIsSet() bool + CommandLineOperationIsSet() bool + CommandLineOperationExecute() error +} - // SETUP +var ( + SvcFactory func(*service.ServiceConfig) (ServiceInstance, error) + SvcConfig *service.ServiceConfig +) - // Enable SPN client mode. - // TODO: Move this to service config. - conf.EnableClient(true) - conf.EnableIntegration(true) +func RunService(cmd *cobra.Command, args []string) { + if SvcFactory == nil || SvcConfig == nil { + fmt.Fprintln(os.Stderr, "internal error: service not set up in cmdbase") + os.Exit(1) + } + + // Start logging. + // Note: Must be created before the service instance, so that they use the right logger. + err := log.Start(SvcConfig.LogLevel, SvcConfig.LogToStdout, SvcConfig.LogDir) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(4) + } // Create instance. // Instance modules might request a cmdline execution of a function. var execCmdLine bool - instance, err := service.New(svcCfg) + instance, err := SvcFactory(SvcConfig) switch { case err == nil: // Continue @@ -59,13 +85,13 @@ func cmdRun(cmd *cobra.Command, args []string) { switch { case !execCmdLine: // Run service. - case instance.CommandLineOperation == nil: + case !instance.CommandLineOperationIsSet(): fmt.Println("command line operation execution requested, but not set") os.Exit(3) default: // Run the function and exit. fmt.Println("executing cmdline op") - err = instance.CommandLineOperation() + err = instance.CommandLineOperationExecute() if err != nil { fmt.Fprintf(os.Stderr, "command line operation failed: %s\n", err) os.Exit(3) @@ -75,16 +101,6 @@ func cmdRun(cmd *cobra.Command, args []string) { // START - // FIXME: fix color and duplicate level when logging with slog - // FIXME: check for tty for color enabling - - // Start logging. - err = log.Start(svcCfg.LogLevel, svcCfg.LogToStdout, svcCfg.LogDir) - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(4) - } - // Create system service. service := NewSystemService(instance) @@ -102,7 +118,7 @@ func cmdRun(cmd *cobra.Command, args []string) { select { case <-instance.ShutdownComplete(): // Print stack on shutdown, if enabled. - if printStackOnExit { + if PrintStackOnExit { printStackTo(log.GlobalWriter, "PRINTING STACK ON EXIT") } case <-time.After(3 * time.Minute): @@ -110,9 +126,22 @@ func cmdRun(cmd *cobra.Command, args []string) { } // Check if restart was triggered and send start service command if true. - if instance.ShouldRestart && service.IsService() { - if err := service.RestartService(); err != nil { - slog.Error("failed to restart service", "err", err) + if instance.ShouldRestartIsSet() && service.IsService() { + // Check if we should reboot instead. + var rebooting bool + if RebootOnRestart { + // Trigger system reboot and record success. + rebooting = triggerSystemReboot() + if !rebooting { + log.Warningf("updates: rebooting failed, only restarting service instead") + } + } + + // Restart service if not rebooting. + if !rebooting { + if err := service.RestartService(); err != nil { + slog.Error("failed to restart service", "err", err) + } } } @@ -138,3 +167,19 @@ func printStackTo(writer io.Writer, msg string) { slog.Error("failed to write stack trace", "err", err) } } + +func triggerSystemReboot() (success bool) { + switch runtime.GOOS { + case "linux": + err := exec.Command("systemctl", "reboot").Run() + if err != nil { + log.Errorf("updates: triggering reboot with systemctl failed: %s", err) + return false + } + default: + log.Warningf("updates: rebooting is not support on %s", runtime.GOOS) + return false + } + + return true +} diff --git a/cmds/portmaster-core/run_linux.go b/cmds/cmdbase/service_linux.go similarity index 81% rename from cmds/portmaster-core/run_linux.go rename to cmds/cmdbase/service_linux.go index 858ed022..085b8649 100644 --- a/cmds/portmaster-core/run_linux.go +++ b/cmds/cmdbase/service_linux.go @@ -1,4 +1,4 @@ -package main +package cmdbase import ( "fmt" @@ -9,17 +9,15 @@ import ( "syscall" processInfo "github.com/shirou/gopsutil/process" - "github.com/spf13/cobra" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/service" ) type LinuxSystemService struct { - instance *service.Instance + instance ServiceInstance } -func NewSystemService(instance *service.Instance) *LinuxSystemService { +func NewSystemService(instance ServiceInstance) *LinuxSystemService { return &LinuxSystemService{instance: instance} } @@ -30,7 +28,7 @@ func (s *LinuxSystemService) Run() { slog.Error("failed to start", "err", err) // Print stack on start failure, if enabled. - if printStackOnExit { + if PrintStackOnExit { printStackTo(log.GlobalWriter, "PRINTING STACK ON START FAILURE") } @@ -62,7 +60,7 @@ wait: continue wait } else { // Trigger shutdown. - fmt.Printf(" ", sig) // CLI output. + fmt.Printf(" \n", sig) // CLI output. slog.Warn("received stop signal", "signal", sig) s.instance.Shutdown() break wait @@ -128,18 +126,3 @@ func (s *LinuxSystemService) IsService() bool { // Check if the parent process ID is 1 == init system return ppid == 1 } - -func runPlatformSpecifics(cmd *cobra.Command, args []string) { - // If recover-iptables flag is set, run the recover-iptables command. - // This is for backwards compatibility - if recoverIPTables { - exitCode := 0 - err := recover(cmd, args) - if err != nil { - fmt.Printf("failed: %s", err) - exitCode = 1 - } - - os.Exit(exitCode) - } -} diff --git a/cmds/portmaster-core/run_windows.go b/cmds/cmdbase/service_windows.go similarity index 93% rename from cmds/portmaster-core/run_windows.go rename to cmds/cmdbase/service_windows.go index 09593630..fdb6df74 100644 --- a/cmds/portmaster-core/run_windows.go +++ b/cmds/cmdbase/service_windows.go @@ -1,4 +1,4 @@ -package main +package cmdbase // Based on the official Go examples from // https://github.com/golang/sys/blob/master/windows/svc/example @@ -13,21 +13,19 @@ import ( "os/signal" "syscall" - "github.com/spf13/cobra" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/service" ) const serviceName = "PortmasterCore" type WindowsSystemService struct { - instance *service.Instance + instance ServiceInstance } -func NewSystemService(instance *service.Instance) *WindowsSystemService { +func NewSystemService(instance ServiceInstance) *WindowsSystemService { return &WindowsSystemService{instance: instance} } @@ -67,7 +65,7 @@ func (s *WindowsSystemService) Execute(args []string, changeRequests <-chan svc. fmt.Printf("failed to start: %s\n", err) // Print stack on start failure, if enabled. - if printStackOnExit { + if PrintStackOnExit { printStackTo(log.GlobalWriter, "PRINTING STACK ON START FAILURE") } @@ -102,7 +100,7 @@ waitSignal: select { case sig := <-signalCh: // Trigger shutdown. - fmt.Printf(" ", sig) // CLI output. + fmt.Printf(" \n", sig) // CLI output. slog.Warn("received stop signal", "signal", sig) break waitSignal @@ -112,7 +110,7 @@ waitSignal: changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: - fmt.Printf(" ", serviceCmdName(c.Cmd)) // CLI output. + fmt.Printf(" \n", serviceCmdName(c.Cmd)) // CLI output. slog.Warn("received service shutdown command", "cmd", c.Cmd) break waitSignal @@ -201,8 +199,6 @@ sc.exe start $serviceName` return nil } -func runPlatformSpecifics(cmd *cobra.Command, args []string) - func serviceCmdName(cmd svc.Cmd) string { switch cmd { case svc.Stop: diff --git a/cmds/portmaster-core/update.go b/cmds/cmdbase/update.go similarity index 88% rename from cmds/portmaster-core/update.go rename to cmds/cmdbase/update.go index 866aab61..65c20a83 100644 --- a/cmds/portmaster-core/update.go +++ b/cmds/cmdbase/update.go @@ -1,4 +1,4 @@ -package main +package cmdbase import ( "fmt" @@ -12,32 +12,28 @@ import ( "github.com/safing/portmaster/service/updates" ) -var updateCmd = &cobra.Command{ +var UpdateCmd = &cobra.Command{ Use: "update", Short: "Force an update of all components.", RunE: update, } -func init() { - rootCmd.AddCommand(updateCmd) -} - func update(cmd *cobra.Command, args []string) error { // Finalize config. - err := svcCfg.Init() + err := SvcConfig.Init() if err != nil { return fmt.Errorf("internal configuration error: %w", err) } // Force logging to stdout. - svcCfg.LogToStdout = true + SvcConfig.LogToStdout = true // Start logging. - _ = log.Start(svcCfg.LogLevel, svcCfg.LogToStdout, svcCfg.LogDir) + _ = log.Start(SvcConfig.LogLevel, SvcConfig.LogToStdout, SvcConfig.LogDir) defer log.Shutdown() // Create updaters. instance := &updateDummyInstance{} - binaryUpdateConfig, intelUpdateConfig, err := service.MakeUpdateConfigs(svcCfg) + binaryUpdateConfig, intelUpdateConfig, err := service.MakeUpdateConfigs(SvcConfig) if err != nil { return fmt.Errorf("init updater config: %w", err) } diff --git a/cmds/cmdbase/version.go b/cmds/cmdbase/version.go new file mode 100644 index 00000000..86244dac --- /dev/null +++ b/cmds/cmdbase/version.go @@ -0,0 +1,20 @@ +package cmdbase + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/safing/portmaster/base/info" +) + +var VersionCmd = &cobra.Command{ + Use: "version", + Short: "Show version and related metadata.", + RunE: Version, +} + +func Version(cmd *cobra.Command, args []string) error { + fmt.Println(info.FullVersion()) + return nil +} diff --git a/cmds/hub/main.go b/cmds/hub/main.go index 1fdc8809..1e4e5116 100644 --- a/cmds/hub/main.go +++ b/cmds/hub/main.go @@ -1,158 +1,94 @@ package main import ( - "errors" "flag" "fmt" - "io" - "log/slog" "os" - "os/signal" "runtime" - "runtime/pprof" - "syscall" - "time" + + "github.com/spf13/cobra" "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/metrics" - "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/cmds/cmdbase" + "github.com/safing/portmaster/service" + "github.com/safing/portmaster/service/configure" "github.com/safing/portmaster/service/updates" - "github.com/safing/portmaster/spn" "github.com/safing/portmaster/spn/conf" ) +var ( + rootCmd = &cobra.Command{ + Use: "spn-hub", + PersistentPreRun: initializeGlobals, + Run: cmdbase.RunService, + } + + binDir string + dataDir string + + logToStdout bool + logDir string + logLevel string +) + func init() { - // flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade") - // FIXME + // Add persisent flags for all commands. + rootCmd.PersistentFlags().StringVar(&binDir, "bin-dir", "", "set directory for executable binaries (rw/ro)") + rootCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "", "set directory for variable data (rw)") + + // Add flags for service only. + rootCmd.Flags().BoolVar(&logToStdout, "log-stdout", false, "log to stdout instead of file") + rootCmd.Flags().StringVar(&logDir, "log-dir", "", "set directory for logs") + rootCmd.Flags().StringVar(&logLevel, "log", "", "set log level to [trace|debug|info|warning|error|critical]") + rootCmd.Flags().BoolVar(&cmdbase.PrintStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") + rootCmd.Flags().BoolVar(&cmdbase.RebootOnRestart, "reboot-on-restart", false, "reboot server instead of service restart") + + // Add other commands. + rootCmd.AddCommand(cmdbase.VersionCmd) + rootCmd.AddCommand(cmdbase.UpdateCmd) } -var sigUSR1 = syscall.Signal(0xa) - func main() { - flag.Parse() + // Add Go's default flag set. + // TODO: Move flags throughout Portmaster to here and add their values to the service config. + rootCmd.Flags().AddGoFlagSet(flag.CommandLine) - // Set name and license. + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func initializeGlobals(cmd *cobra.Command, args []string) { + // Set version info. info.Set("SPN Hub", "", "GPLv3") // Configure metrics. _ = metrics.SetNamespace("hub") - // Configure user agent and updates. + // Configure user agent. updates.UserAgent = fmt.Sprintf("SPN Hub (%s %s)", runtime.GOOS, runtime.GOARCH) - // helper.IntelOnly() // Set SPN public hub mode. conf.EnablePublicHub(true) - // Start logger with default log level. - _ = log.Start(log.WarningLevel) - - // FIXME: Use service? - - // Create instance. - var execCmdLine bool - instance, err := spn.New() - switch { - case err == nil: - // Continue - case errors.Is(err, mgr.ErrExecuteCmdLineOp): - execCmdLine = true - default: - fmt.Printf("error creating an instance: %s\n", err) - os.Exit(2) + // Configure service. + cmdbase.SvcFactory = func(svcCfg *service.ServiceConfig) (cmdbase.ServiceInstance, error) { + svc, err := service.New(svcCfg) + return svc, err } + cmdbase.SvcConfig = &service.ServiceConfig{ + BinDir: binDir, + DataDir: dataDir, - // Execute command line operation, if requested or available. - switch { - case !execCmdLine: - // Run service. - case instance.CommandLineOperation == nil: - fmt.Println("command line operation execution requested, but not set") - os.Exit(3) - default: - // Run the function and exit. - err = instance.CommandLineOperation() - if err != nil { - fmt.Fprintf(os.Stderr, "command line operation failed: %s\n", err) - os.Exit(3) - } - os.Exit(0) - } + LogToStdout: logToStdout, + LogDir: logDir, + LogLevel: logLevel, - // Start - go func() { - err = instance.Start() - if err != nil { - fmt.Printf("instance start failed: %s\n", err) - os.Exit(1) - } - }() - - // Wait for signal. - signalCh := make(chan os.Signal, 1) - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - sigUSR1, - ) - - select { - case sig := <-signalCh: - // Only print and continue to wait if SIGUSR1 - if sig == sigUSR1 { - printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") - } else { - fmt.Println(" ") // CLI output. - slog.Warn("program was interrupted, stopping") - } - - case <-instance.ShutdownComplete(): - log.Shutdown() - os.Exit(instance.ExitCode()) - } - - // Catch signals during shutdown. - // Rapid unplanned disassembly after 5 interrupts. - go func() { - forceCnt := 5 - for { - <-signalCh - forceCnt-- - if forceCnt > 0 { - fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) - } else { - printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") - os.Exit(1) - } - } - }() - - // Rapid unplanned disassembly after 3 minutes. - go func() { - time.Sleep(3 * time.Minute) - printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") - os.Exit(1) - }() - - // Stop instance. - if err := instance.Stop(); err != nil { - slog.Error("failed to stop", "err", err) - } - log.Shutdown() - os.Exit(instance.ExitCode()) -} - -func printStackTo(writer io.Writer, msg string) { - _, err := fmt.Fprintf(writer, "===== %s =====\n", msg) - if err == nil { - err = pprof.Lookup("goroutine").WriteTo(writer, 1) - } - if err != nil { - slog.Error("failed to write stack trace", "err", err) + BinariesIndexURLs: configure.DefaultStableBinaryIndexURLs, + IntelIndexURLs: configure.DefaultIntelIndexURLs, + VerifyBinaryUpdates: configure.BinarySigningTrustStore, + VerifyIntelUpdates: configure.BinarySigningTrustStore, } } diff --git a/cmds/observation-hub/main.go b/cmds/observation-hub/main.go index 598680f0..ec34d53b 100644 --- a/cmds/observation-hub/main.go +++ b/cmds/observation-hub/main.go @@ -1,41 +1,75 @@ package main import ( - "errors" "flag" "fmt" - "io" - "log/slog" "os" - "os/signal" "runtime" - "runtime/pprof" - "syscall" - "time" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/metrics" - "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/cmds/cmdbase" + "github.com/safing/portmaster/service" + "github.com/safing/portmaster/service/configure" "github.com/safing/portmaster/service/updates" - "github.com/safing/portmaster/spn" "github.com/safing/portmaster/spn/captain" "github.com/safing/portmaster/spn/conf" "github.com/safing/portmaster/spn/sluice" + "github.com/spf13/cobra" ) -var sigUSR1 = syscall.Signal(0xa) +var ( + rootCmd = &cobra.Command{ + Use: "observation-hub", + PersistentPreRun: initializeGlobals, + Run: cmdbase.RunService, + } + + binDir string + dataDir string + + logToStdout bool + logDir string + logLevel string +) + +func init() { + // Add persisent flags for all commands. + rootCmd.PersistentFlags().StringVar(&binDir, "bin-dir", "", "set directory for executable binaries (rw/ro)") + rootCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "", "set directory for variable data (rw)") + + // Add flags for service only. + rootCmd.Flags().BoolVar(&logToStdout, "log-stdout", false, "log to stdout instead of file") + rootCmd.Flags().StringVar(&logDir, "log-dir", "", "set directory for logs") + rootCmd.Flags().StringVar(&logLevel, "log", "", "set log level to [trace|debug|info|warning|error|critical]") + rootCmd.Flags().BoolVar(&cmdbase.PrintStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") + rootCmd.Flags().BoolVar(&cmdbase.RebootOnRestart, "reboot-on-restart", false, "reboot server instead of service restart") + + // Add other commands. + rootCmd.AddCommand(cmdbase.VersionCmd) + rootCmd.AddCommand(cmdbase.UpdateCmd) +} func main() { - flag.Parse() + // Add Go's default flag set. + // TODO: Move flags throughout Portmaster to here and add their values to the service config. + rootCmd.Flags().AddGoFlagSet(flag.CommandLine) + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func initializeGlobals(cmd *cobra.Command, args []string) { + // Set version info. info.Set("SPN Observation Hub", "", "GPLv3") // Configure metrics. _ = metrics.SetNamespace("observer") - // Configure user agent and updates. + // Configure user agent. updates.UserAgent = fmt.Sprintf("SPN Observation Hub (%s %s)", runtime.GOOS, runtime.GOARCH) // Configure SPN mode. @@ -46,129 +80,37 @@ func main() { sluice.EnableListener = false api.EnableServer = false - // Start logger with default log level. - _ = log.Start(log.WarningLevel) + // Configure service. + cmdbase.SvcFactory = func(svcCfg *service.ServiceConfig) (cmdbase.ServiceInstance, error) { + svc, err := service.New(svcCfg) - // Create instance. - var execCmdLine bool - instance, err := spn.New() - switch { - case err == nil: - // Continue - case errors.Is(err, mgr.ErrExecuteCmdLineOp): - execCmdLine = true - default: - fmt.Printf("error creating an instance: %s\n", err) - os.Exit(2) - } - - // Add additional modules. - observer, err := New(instance) - if err != nil { - fmt.Printf("error creating an instance: create observer module: %s\n", err) - os.Exit(2) - } - instance.AddModule(observer) - - _, err = NewApprise(instance) - if err != nil { - fmt.Printf("error creating an instance: create apprise module: %s\n", err) - os.Exit(2) - } - instance.AddModule(observer) - - // FIXME: Use service? - - // Execute command line operation, if requested or available. - switch { - case !execCmdLine: - // Run service. - case instance.CommandLineOperation == nil: - fmt.Println("command line operation execution requested, but not set") - os.Exit(3) - default: - // Run the function and exit. - err = instance.CommandLineOperation() + // Add additional modules. + observer, err := New(svc) if err != nil { - fmt.Fprintf(os.Stderr, "command line operation failed: %s\n", err) - os.Exit(3) + fmt.Printf("error creating an instance: create observer module: %s\n", err) + os.Exit(2) } - os.Exit(0) - } - - // Start - go func() { - err = instance.Start() + svc.AddModule(observer) + _, err = NewApprise(svc) if err != nil { - fmt.Printf("instance start failed: %s\n", err) - os.Exit(1) + fmt.Printf("error creating an instance: create apprise module: %s\n", err) + os.Exit(2) } - }() + svc.AddModule(observer) - // Wait for signal. - signalCh := make(chan os.Signal, 1) - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - sigUSR1, - ) - - select { - case sig := <-signalCh: - // Only print and continue to wait if SIGUSR1 - if sig == sigUSR1 { - printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") - } else { - fmt.Println(" ") // CLI output. - slog.Warn("program was interrupted, stopping") - } - - case <-instance.ShuttingDown(): - log.Shutdown() - os.Exit(instance.ExitCode()) + return svc, err } + cmdbase.SvcConfig = &service.ServiceConfig{ + BinDir: binDir, + DataDir: dataDir, - // Catch signals during shutdown. - // Rapid unplanned disassembly after 5 interrupts. - go func() { - forceCnt := 5 - for { - <-signalCh - forceCnt-- - if forceCnt > 0 { - fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) - } else { - printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") - os.Exit(1) - } - } - }() + LogToStdout: logToStdout, + LogDir: logDir, + LogLevel: logLevel, - // Rapid unplanned disassembly after 3 minutes. - go func() { - time.Sleep(3 * time.Minute) - printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") - os.Exit(1) - }() - - // Stop instance. - if err := instance.Stop(); err != nil { - slog.Error("failed to stop", "err", err) - } - log.Shutdown() - os.Exit(instance.ExitCode()) -} - -func printStackTo(writer io.Writer, msg string) { - _, err := fmt.Fprintf(writer, "===== %s =====\n", msg) - if err == nil { - err = pprof.Lookup("goroutine").WriteTo(writer, 1) - } - if err != nil { - slog.Error("failed to write stack trace", "err", err) + BinariesIndexURLs: configure.DefaultStableBinaryIndexURLs, + IntelIndexURLs: configure.DefaultIntelIndexURLs, + VerifyBinaryUpdates: configure.BinarySigningTrustStore, + VerifyIntelUpdates: configure.BinarySigningTrustStore, } } diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index 8cd80864..12456f58 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -10,7 +10,9 @@ import ( "github.com/safing/portmaster/base/info" "github.com/safing/portmaster/base/metrics" + "github.com/safing/portmaster/cmds/cmdbase" "github.com/safing/portmaster/service" + "github.com/safing/portmaster/service/configure" "github.com/safing/portmaster/service/updates" ) @@ -18,7 +20,7 @@ var ( rootCmd = &cobra.Command{ Use: "portmaster-core", PersistentPreRun: initializeGlobals, - Run: cmdRun, + Run: mainRun, } binDir string @@ -28,14 +30,10 @@ var ( logDir string logLevel string - svcCfg *service.ServiceConfig + printVersion bool ) func init() { - // Add Go's default flag set. - // TODO: Move flags throughout Portmaster to here and add their values to the service config. - rootCmd.Flags().AddGoFlagSet(flag.CommandLine) - // Add persisent flags for all commands. rootCmd.PersistentFlags().StringVar(&binDir, "bin-dir", "", "set directory for executable binaries (rw/ro)") rootCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "", "set directory for variable data (rw)") @@ -44,17 +42,32 @@ func init() { rootCmd.Flags().BoolVar(&logToStdout, "log-stdout", false, "log to stdout instead of file") rootCmd.Flags().StringVar(&logDir, "log-dir", "", "set directory for logs") rootCmd.Flags().StringVar(&logLevel, "log", "", "set log level to [trace|debug|info|warning|error|critical]") + rootCmd.Flags().BoolVar(&printVersion, "version", false, "print version (backward compatibility; use command instead)") + rootCmd.Flags().BoolVar(&cmdbase.PrintStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") + + // Add other commands. + rootCmd.AddCommand(cmdbase.VersionCmd) + rootCmd.AddCommand(cmdbase.UpdateCmd) } func main() { + // Add Go's default flag set. + // TODO: Move flags throughout Portmaster to here and add their values to the service config. + rootCmd.Flags().AddGoFlagSet(flag.CommandLine) + if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } +func mainRun(cmd *cobra.Command, args []string) { + runPlatformSpecifics(cmd, args) + cmdbase.RunService(cmd, args) +} + func initializeGlobals(cmd *cobra.Command, args []string) { - // set information + // Set version info. info.Set("Portmaster", "", "GPLv3") // Configure metrics. @@ -63,8 +76,12 @@ func initializeGlobals(cmd *cobra.Command, args []string) { // Configure user agent. updates.UserAgent = fmt.Sprintf("Portmaster Core (%s %s)", runtime.GOOS, runtime.GOARCH) - // Create service config. - svcCfg = &service.ServiceConfig{ + // Configure service. + cmdbase.SvcFactory = func(svcCfg *service.ServiceConfig) (cmdbase.ServiceInstance, error) { + svc, err := service.New(svcCfg) + return svc, err + } + cmdbase.SvcConfig = &service.ServiceConfig{ BinDir: binDir, DataDir: dataDir, @@ -72,9 +89,18 @@ func initializeGlobals(cmd *cobra.Command, args []string) { LogDir: logDir, LogLevel: logLevel, - BinariesIndexURLs: service.DefaultStableBinaryIndexURLs, - IntelIndexURLs: service.DefaultIntelIndexURLs, - VerifyBinaryUpdates: service.BinarySigningTrustStore, - VerifyIntelUpdates: service.BinarySigningTrustStore, + BinariesIndexURLs: configure.DefaultStableBinaryIndexURLs, + IntelIndexURLs: configure.DefaultIntelIndexURLs, + VerifyBinaryUpdates: configure.BinarySigningTrustStore, + VerifyIntelUpdates: configure.BinarySigningTrustStore, } } + +func runFlagCmd(fn func(cmd *cobra.Command, args []string) error, cmd *cobra.Command, args []string) { + if err := fn(cmd, args); err != nil { + fmt.Printf("failed: %s\n", err) + os.Exit(1) + } + + os.Exit(0) +} diff --git a/cmds/portmaster-core/main_linux.go b/cmds/portmaster-core/main_linux.go new file mode 100644 index 00000000..e4d4783d --- /dev/null +++ b/cmds/portmaster-core/main_linux.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/safing/portmaster/cmds/cmdbase" + "github.com/spf13/cobra" +) + +var recoverIPTablesFlag bool + +func init() { + rootCmd.Flags().BoolVar(&recoverIPTablesFlag, "recover-iptables", false, "recovers ip table rules (backward compatibility; use command instead)") +} + +func runPlatformSpecifics(cmd *cobra.Command, args []string) { + switch { + case printVersion: + runFlagCmd(cmdbase.Version, cmd, args) + case recoverIPTablesFlag: + runFlagCmd(recoverIPTables, cmd, args) + } +} diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go new file mode 100644 index 00000000..5d2066f3 --- /dev/null +++ b/cmds/portmaster-core/main_windows.go @@ -0,0 +1,13 @@ +package main + +import ( + "github.com/safing/portmaster/cmds/cmdbase" + "github.com/spf13/cobra" +) + +func runPlatformSpecifics(cmd *cobra.Command, args []string) { + switch { + case printVersion: + runFlagCmd(cmdbase.Version, cmd, args) + } +} diff --git a/cmds/portmaster-core/recover_linux.go b/cmds/portmaster-core/recover_linux.go index 6f5532c2..61fecda7 100644 --- a/cmds/portmaster-core/recover_linux.go +++ b/cmds/portmaster-core/recover_linux.go @@ -2,7 +2,6 @@ package main import ( "errors" - "flag" "fmt" "os" "strings" @@ -13,23 +12,17 @@ import ( "github.com/safing/portmaster/service/firewall/interception" ) -var ( - recoverCmd = &cobra.Command{ - Use: "recover-iptables", - Short: "Force an update of all components.", - RunE: update, - } - - recoverIPTables bool -) +var recoverCmd = &cobra.Command{ + Use: "recover-iptables", + Short: "Clean up Portmaster rules in iptables", + RunE: recoverIPTables, +} func init() { rootCmd.AddCommand(recoverCmd) - - flag.BoolVar(&recoverIPTables, "recover-iptables", false, "recovers ip table rules (backward compatibility; use command instead)") } -func recover(cmd *cobra.Command, args []string) error { +func recoverIPTables(cmd *cobra.Command, args []string) error { // interception.DeactiveNfqueueFirewall uses coreos/go-iptables // which shells out to the /sbin/iptables binary. As a result, // we don't get the errno of the actual error and need to parse the diff --git a/cmds/testsuite/db.go b/cmds/testsuite/db.go index b23a6fd7..44286a8c 100644 --- a/cmds/testsuite/db.go +++ b/cmds/testsuite/db.go @@ -6,7 +6,7 @@ import ( ) func setupDatabases(path string) error { - err := database.InitializeWithPath(path) + err := database.Initialize(path) if err != nil { return err } diff --git a/cmds/trafficgen/main.go b/cmds/trafficgen/main.go index efcfd390..59f8aeca 100644 --- a/cmds/trafficgen/main.go +++ b/cmds/trafficgen/main.go @@ -37,13 +37,12 @@ func main() { } // Start logging. - err := log.Start() + err := log.Start("trace", true, "") if err != nil { fmt.Printf("failed to start logging: %s\n", err) os.Exit(1) } defer log.Shutdown() - log.SetLogLevel(log.TraceLevel) log.Info("starting traffic generator") // Execute requests diff --git a/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.ts b/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.ts index 60b5b514..2528d81a 100644 --- a/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.ts +++ b/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.ts @@ -238,13 +238,13 @@ export class EditProfileDialog implements OnInit, OnDestroy { this.portapi.delete(icon.Value).subscribe(); } - // FIXME(ppacher): we cannot yet delete API based icons ... + // TODO(ppacher): we cannot yet delete API based icons ... }); if (this.iconData !== '') { // save the new icon in the cache database - // FIXME(ppacher): we currently need to calls because the icon API in portmaster + // TODO(ppacher): we currently need to calls because the icon API in portmaster // does not update the profile but just saves the file and returns the filename. // So we still need to update the profile manually. updateIcon = this.profileService @@ -261,7 +261,7 @@ export class EditProfileDialog implements OnInit, OnDestroy { }) ); - // FIXME(ppacher): reset presentationpath + // TODO(ppacher): reset presentationpath } else { // just clear out that there was an icon this.profile.Icons = []; diff --git a/desktop/angular/src/app/shared/netquery/line-chart/line-chart.ts b/desktop/angular/src/app/shared/netquery/line-chart/line-chart.ts index 8d95a061..23a9007c 100644 --- a/desktop/angular/src/app/shared/netquery/line-chart/line-chart.ts +++ b/desktop/angular/src/app/shared/netquery/line-chart/line-chart.ts @@ -543,7 +543,7 @@ export class SfngNetqueryLineChartComponent implemen .append("title") .text(d => d.text) - // FIXME(ppacher): somehow d3 does not recognize which data points must be removed + // TODO(ppacher): somehow d3 does not recognize which data points must be removed // or re-placed. For now, just remove them all this.svgInner .select('.points') diff --git a/desktop/angular/src/app/shared/netquery/searchbar/searchbar.ts b/desktop/angular/src/app/shared/netquery/searchbar/searchbar.ts index 46a42f51..044f8618 100644 --- a/desktop/angular/src/app/shared/netquery/searchbar/searchbar.ts +++ b/desktop/angular/src/app/shared/netquery/searchbar/searchbar.ts @@ -184,7 +184,7 @@ export class SfngNetquerySearchbarComponent implements ControlValueAccessor, OnI const queries: Observable>[] = []; const queryKeys: (keyof Partial)[] = []; - // FIXME(ppacher): confirm .type is an actually allowed field + // TODO(ppacher): confirm .type is an actually allowed field if (!!parser.lastUnterminatedCondition) { fields = [parser.lastUnterminatedCondition.type as keyof NetqueryConnection]; limit = 0; diff --git a/service/broadcasts/notify.go b/service/broadcasts/notify.go index 235a1bf0..73c9f232 100644 --- a/service/broadcasts/notify.go +++ b/service/broadcasts/notify.go @@ -21,7 +21,7 @@ import ( ) const ( - broadcastsResourcePath = "intel/portmaster/notifications.yaml" + broadcastsResourceName = "notifications.yaml" broadcastNotificationIDPrefix = "broadcasts:" @@ -67,7 +67,7 @@ type BroadcastNotification struct { func broadcastNotify(ctx *mgr.WorkerCtx) error { // Get broadcast notifications file, load it from disk and parse it. - broadcastsResource, err := module.instance.IntelUpdates().GetFile(broadcastsResourcePath) + broadcastsResource, err := module.instance.IntelUpdates().GetFile(broadcastsResourceName) if err != nil { return fmt.Errorf("failed to get broadcast notifications update: %w", err) } diff --git a/service/config.go b/service/config.go index 7b7e0f08..f7b1f12c 100644 --- a/service/config.go +++ b/service/config.go @@ -9,6 +9,8 @@ import ( "github.com/safing/jess" "github.com/safing/portmaster/base/log" + "github.com/safing/portmaster/service/configure" + "github.com/safing/portmaster/service/updates" ) type ServiceConfig struct { @@ -76,11 +78,10 @@ func (sc *ServiceConfig) Init() error { // Apply defaults for required fields. if len(sc.BinariesIndexURLs) == 0 { - // FIXME: Select based on setting. - sc.BinariesIndexURLs = DefaultStableBinaryIndexURLs + sc.BinariesIndexURLs = configure.DefaultStableBinaryIndexURLs } if len(sc.IntelIndexURLs) == 0 { - sc.IntelIndexURLs = DefaultIntelIndexURLs + sc.IntelIndexURLs = configure.DefaultIntelIndexURLs } // Check log level. @@ -109,3 +110,71 @@ func getCurrentBinaryFolder() (string, error) { return installDir, nil } + +func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateConfig *updates.Config, err error) { + switch runtime.GOOS { + case "windows": + binaryUpdateConfig = &updates.Config{ + Name: "binaries", + Directory: svcCfg.BinDir, + DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"), + PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"), + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup. + IndexFile: "index.json", + Verify: svcCfg.VerifyBinaryUpdates, + AutoCheck: true, // May be changed by config during instance startup. + AutoDownload: false, + AutoApply: false, + NeedsRestart: true, + Notify: true, + } + intelUpdateConfig = &updates.Config{ + Name: "intel", + Directory: filepath.Join(svcCfg.DataDir, "intel"), + DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"), + PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"), + IndexURLs: svcCfg.IntelIndexURLs, + IndexFile: "index.json", + Verify: svcCfg.VerifyIntelUpdates, + AutoCheck: true, // May be changed by config during instance startup. + AutoDownload: true, + AutoApply: true, + NeedsRestart: false, + Notify: false, + } + + case "linux": + binaryUpdateConfig = &updates.Config{ + Name: "binaries", + Directory: svcCfg.BinDir, + DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"), + PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"), + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup. + IndexFile: "index.json", + Verify: svcCfg.VerifyBinaryUpdates, + AutoCheck: true, // May be changed by config during instance startup. + AutoDownload: false, + AutoApply: false, + NeedsRestart: true, + Notify: true, + } + intelUpdateConfig = &updates.Config{ + Name: "intel", + Directory: filepath.Join(svcCfg.DataDir, "intel"), + DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"), + PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"), + IndexURLs: svcCfg.IntelIndexURLs, + IndexFile: "index.json", + Verify: svcCfg.VerifyIntelUpdates, + AutoCheck: true, // May be changed by config during instance startup. + AutoDownload: true, + AutoApply: true, + NeedsRestart: false, + Notify: false, + } + } + + return +} diff --git a/service/configure/updates.go b/service/configure/updates.go new file mode 100644 index 00000000..3fec4afc --- /dev/null +++ b/service/configure/updates.go @@ -0,0 +1,65 @@ +package configure + +import ( + "github.com/safing/jess" +) + +var ( + DefaultStableBinaryIndexURLs = []string{ + "https://updates.safing.io/stable.v3.json", + } + DefaultBetaBinaryIndexURLs = []string{ + "https://updates.safing.io/beta.v3.json", + } + DefaultStagingBinaryIndexURLs = []string{ + "https://updates.safing.io/staging.v3.json", + } + DefaultSupportBinaryIndexURLs = []string{ + "https://updates.safing.io/support.v3.json", + } + + DefaultIntelIndexURLs = []string{ + "https://updates.safing.io/intel.v3.json", + } + + // BinarySigningKeys holds the signing keys in text format. + BinarySigningKeys = []string{ + // Safing Code Signing Key #1 + "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", + // Safing Code Signing Key #2 + "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", + } + + // BinarySigningTrustStore is an in-memory trust store with the signing keys. + BinarySigningTrustStore = jess.NewMemTrustStore() +) + +func init() { + for _, signingKey := range BinarySigningKeys { + rcpt, err := jess.RecipientFromTextFormat(signingKey) + if err != nil { + panic(err) + } + err = BinarySigningTrustStore.StoreSignet(rcpt) + if err != nil { + panic(err) + } + } +} + +// GetBinaryUpdateURLs returns the correct binary update URLs for the given release channel. +// Silently falls back to stable if release channel is invalid. +func GetBinaryUpdateURLs(releaseChannel string) []string { + switch releaseChannel { + case "stable": + return DefaultStableBinaryIndexURLs + case "beta": + return DefaultBetaBinaryIndexURLs + case "staging": + return DefaultStagingBinaryIndexURLs + case "support": + return DefaultSupportBinaryIndexURLs + default: + return DefaultStableBinaryIndexURLs + } +} diff --git a/service/core/api.go b/service/core/api.go index ea4f18d1..6f419ced 100644 --- a/service/core/api.go +++ b/service/core/api.go @@ -1,19 +1,27 @@ package core import ( + "bytes" "context" "encoding/hex" "errors" "fmt" + "io" "net/http" "net/url" + "os" + "path/filepath" + "strings" "time" + "github.com/ghodss/yaml" + "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" "github.com/safing/portmaster/base/utils/debug" "github.com/safing/portmaster/service/compat" "github.com/safing/portmaster/service/process" @@ -149,6 +157,17 @@ func registerAPIEndpoints() error { return err } + if err := api.RegisterEndpoint(api.Endpoint{ + Name: "Get Resource", + Description: "Returns the requested resource from the udpate system", + Path: `updates/get/?{artifact_path:[A-Za-z0-9/\.\-_]{1,255}}/{artifact_name:[A-Za-z0-9\.\-_]{1,255}}`, + Read: api.PermitUser, + ReadMethod: http.MethodGet, + HandlerFunc: getUpdateResource, + }); err != nil { + return err + } + return nil } @@ -170,6 +189,113 @@ func restart(_ *api.Request) (msg string, err error) { return "restart initiated", nil } +func getUpdateResource(w http.ResponseWriter, r *http.Request) { + // Get identifier from URL. + var identifier string + if ar := api.GetAPIRequest(r); ar != nil { + identifier = ar.URLVars["artifact_name"] + } + if identifier == "" { + http.Error(w, "no resource specified", http.StatusBadRequest) + return + } + + // Get resource. + artifact, err := module.instance.BinaryUpdates().GetFile(identifier) + if err != nil { + intelArtifact, intelErr := module.instance.IntelUpdates().GetFile(identifier) + if intelErr == nil { + artifact = intelArtifact + } else { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + } + + // Open file for reading. + file, err := os.Open(artifact.Path()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() //nolint:errcheck,gosec + + // Assign file to reader + var reader io.Reader = file + + // Add version and hash to header. + if artifact.Version != "" { + w.Header().Set("Resource-Version", artifact.Version) + } + if artifact.SHA256 != "" { + w.Header().Set("Resource-SHA256", artifact.SHA256) + } + + // Set Content-Type. + contentType, _ := utils.MimeTypeByExtension(filepath.Ext(artifact.Path())) + w.Header().Set("Content-Type", contentType) + + // Check if the content type may be returned. + accept := r.Header.Get("Accept") + if accept != "" { + mimeTypes := strings.Split(accept, ",") + // First, clean mime types. + for i, mimeType := range mimeTypes { + mimeType = strings.TrimSpace(mimeType) + mimeType, _, _ = strings.Cut(mimeType, ";") + mimeTypes[i] = mimeType + } + // Second, check if we may return anything. + var acceptsAny bool + for _, mimeType := range mimeTypes { + switch mimeType { + case "*", "*/*": + acceptsAny = true + } + } + // Third, check if we can convert. + if !acceptsAny { + var converted bool + sourceType, _, _ := strings.Cut(contentType, ";") + findConvertiblePair: + for _, mimeType := range mimeTypes { + switch { + case sourceType == "application/yaml" && mimeType == "application/json": + yamlData, err := io.ReadAll(reader) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + jsonData, err := yaml.YAMLToJSON(yamlData) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + reader = bytes.NewReader(jsonData) + converted = true + break findConvertiblePair + } + } + + // If we could not convert to acceptable format, return an error. + if !converted { + http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable) + return + } + } + } + + // Write file. + w.WriteHeader(http.StatusOK) + if r.Method != http.MethodHead { + _, err = io.Copy(w, reader) + if err != nil { + log.Errorf("updates: failed to serve resource file: %s", err) + return + } + } +} + // debugInfo returns the debugging information for support requests. func debugInfo(ar *api.Request) (data []byte, err error) { // Create debug information helper. @@ -192,7 +318,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) { config.AddToDebugInfo(di) // Detailed information. - // TODO(vladimir): updates.AddToDebugInfo(di) + AddVersionsToDebugInfo(di) compat.AddToDebugInfo(di) module.instance.AddWorkerInfoToDebugInfo(di) di.AddGoroutineStack() diff --git a/service/core/base/global.go b/service/core/base/global.go deleted file mode 100644 index 912e975a..00000000 --- a/service/core/base/global.go +++ /dev/null @@ -1,46 +0,0 @@ -package base - -import ( - "errors" - "flag" - "fmt" - - "github.com/safing/portmaster/base/api" - "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/service/mgr" -) - -// Default Values (changeable for testing). -var ( - DefaultAPIListenAddress = "127.0.0.1:817" - - showVersion bool -) - -func init() { - flag.BoolVar(&showVersion, "version", false, "show version and exit") -} - -func prep(instance instance) error { - // check if meta info is ok - err := info.CheckVersion() - if err != nil { - return errors.New("compile error: please compile using the provided build script") - } - - // print version - if showVersion { - instance.SetCmdLineOperation(printVersion) - return mgr.ErrExecuteCmdLineOp - } - - // set api listen address - api.SetDefaultAPIListenAddress(DefaultAPIListenAddress) - - return nil -} - -func printVersion() error { - fmt.Println(info.FullVersion()) - return nil -} diff --git a/service/core/base/module.go b/service/core/base/module.go index 0bb7aba2..05eacec9 100644 --- a/service/core/base/module.go +++ b/service/core/base/module.go @@ -4,9 +4,13 @@ import ( "errors" "sync/atomic" + "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/service/mgr" ) +// DefaultAPIListenAddress is the default listen address for the API. +var DefaultAPIListenAddress = "127.0.0.1:817" + // Base is the base module. type Base struct { mgr *mgr.Manager @@ -47,9 +51,9 @@ func New(instance instance) (*Base, error) { instance: instance, } - if err := prep(instance); err != nil { - return nil, err - } + // Set api listen address. + api.SetDefaultAPIListenAddress(DefaultAPIListenAddress) + if err := registerDatabases(); err != nil { return nil, err } diff --git a/service/core/core.go b/service/core/core.go index 356bd2eb..48f262a1 100644 --- a/service/core/core.go +++ b/service/core/core.go @@ -6,6 +6,8 @@ import ( "fmt" "sync/atomic" + "github.com/safing/portmaster/base/config" + "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/base/utils/debug" @@ -19,6 +21,11 @@ import ( "github.com/safing/portmaster/service/updates" ) +var db = database.NewInterface(&database.Options{ + Local: true, + Internal: true, +}) + // Core is the core service module. type Core struct { m *mgr.Manager @@ -56,8 +63,10 @@ func init() { func prep() error { // init config - err := registerConfig() - if err != nil { + if err := registerConfig(); err != nil { + return err + } + if err := registerUpdateConfig(); err != nil { return err } @@ -77,6 +86,10 @@ func start() error { return fmt.Errorf("failed to start plattform-specific components: %w", err) } + // Setup update system. + initUpdateConfig() + initVersionExport() + // Enable persistent metrics. if err := metrics.EnableMetricPersistence("core:metrics/storage"); err != nil { log.Warningf("core: failed to enable persisted metrics: %s", err) @@ -116,6 +129,7 @@ type instance interface { Shutdown() Restart() AddWorkerInfoToDebugInfo(di *debug.Info) + Config() *config.Config BinaryUpdates() *updates.Updater IntelUpdates() *updates.Updater } diff --git a/service/core/update_config.go b/service/core/update_config.go new file mode 100644 index 00000000..10fdb84c --- /dev/null +++ b/service/core/update_config.go @@ -0,0 +1,134 @@ +package core + +import ( + "github.com/safing/portmaster/base/config" + "github.com/safing/portmaster/service/configure" + "github.com/safing/portmaster/service/mgr" +) + +// Release Channel Configuration Keys. +const ( + ReleaseChannelKey = "core/releaseChannel" + ReleaseChannelJSONKey = "core.releaseChannel" +) + +// Release Channels. +const ( + ReleaseChannelStable = "stable" + ReleaseChannelBeta = "beta" + ReleaseChannelStaging = "staging" + ReleaseChannelSupport = "support" +) + +const ( + enableSoftwareUpdatesKey = "core/automaticUpdates" + enableIntelUpdatesKey = "core/automaticIntelUpdates" +) + +var ( + releaseChannel config.StringOption + enableSoftwareUpdates config.BoolOption + enableIntelUpdates config.BoolOption + + initialReleaseChannel string +) + +func registerUpdateConfig() error { + err := config.Register(&config.Option{ + Name: "Release Channel", + Key: ReleaseChannelKey, + Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`, + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + ReleaseLevel: config.ReleaseLevelStable, + RequiresRestart: true, + DefaultValue: ReleaseChannelStable, + PossibleValues: []config.PossibleValue{ + { + Name: "Stable", + Description: "Production releases.", + Value: ReleaseChannelStable, + }, + { + Name: "Beta", + Description: "Production releases for testing new features that may break and cause interruption.", + Value: ReleaseChannelBeta, + }, + { + Name: "Support", + Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.", + Value: ReleaseChannelSupport, + }, + { + Name: "Staging", + Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.", + Value: ReleaseChannelStaging, + }, + }, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: -4, + config.DisplayHintAnnotation: config.DisplayHintOneOf, + config.CategoryAnnotation: "Updates", + }, + }) + if err != nil { + return err + } + + err = config.Register(&config.Option{ + Name: "Automatic Software Updates", + Key: enableSoftwareUpdatesKey, + Description: "Automatically check for and download software updates. This does not include intelligence data updates.", + OptType: config.OptTypeBool, + ExpertiseLevel: config.ExpertiseLevelExpert, + ReleaseLevel: config.ReleaseLevelStable, + RequiresRestart: false, + DefaultValue: true, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: -12, + config.CategoryAnnotation: "Updates", + }, + }) + if err != nil { + return err + } + + err = config.Register(&config.Option{ + Name: "Automatic Intelligence Data Updates", + Key: enableIntelUpdatesKey, + Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.", + OptType: config.OptTypeBool, + ExpertiseLevel: config.ExpertiseLevelExpert, + ReleaseLevel: config.ReleaseLevelStable, + RequiresRestart: false, + DefaultValue: true, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: -11, + config.CategoryAnnotation: "Updates", + }, + }) + if err != nil { + return err + } + + return nil +} + +func initUpdateConfig() { + releaseChannel = config.Concurrent.GetAsString(ReleaseChannelKey, ReleaseChannelStable) + enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true) + enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true) + + initialReleaseChannel = releaseChannel() + + module.instance.Config().EventConfigChange.AddCallback("configure updates", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) { + configureUpdates() + return false, nil + }) + configureUpdates() +} + +func configureUpdates() { + module.instance.BinaryUpdates().Configure(enableSoftwareUpdates(), configure.GetBinaryUpdateURLs(releaseChannel())) + module.instance.IntelUpdates().Configure(enableIntelUpdates(), configure.DefaultIntelIndexURLs) +} diff --git a/service/core/update_versions.go b/service/core/update_versions.go new file mode 100644 index 00000000..8427a505 --- /dev/null +++ b/service/core/update_versions.go @@ -0,0 +1,176 @@ +package core + +import ( + "bytes" + "fmt" + "sync" + "text/tabwriter" + + "github.com/safing/portmaster/base/database/record" + "github.com/safing/portmaster/base/info" + "github.com/safing/portmaster/base/utils/debug" + "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates" +) + +const ( + // versionsDBKey is the database key for update version information. + versionsDBKey = "core:status/versions" + + // versionsDBKey is the database key for simple update version information. + simpleVersionsDBKey = "core:status/simple-versions" +) + +// Versions holds update versions and status information. +type Versions struct { + record.Base + sync.Mutex + + Core *info.Info + Resources map[string]*updates.Artifact + Channel string + Beta bool + Staging bool +} + +// SimpleVersions holds simplified update versions and status information. +type SimpleVersions struct { + record.Base + sync.Mutex + + Build *info.Info + Resources map[string]*SimplifiedResourceVersion + Channel string +} + +// SimplifiedResourceVersion holds version information about one resource. +type SimplifiedResourceVersion struct { + Version string +} + +// GetVersions returns the update versions and status information. +// Resources must be locked when accessed. +func GetVersions() *Versions { + // Get all artifacts. + resources := make(map[string]*updates.Artifact) + if artifacts, err := module.instance.BinaryUpdates().GetFiles(); err == nil { + for _, artifact := range artifacts { + resources[artifact.Filename] = artifact + } + } + if artifacts, err := module.instance.IntelUpdates().GetFiles(); err == nil { + for _, artifact := range artifacts { + resources[artifact.Filename] = artifact + } + } + + return &Versions{ + Core: info.GetInfo(), + Resources: resources, + Channel: initialReleaseChannel, + Beta: initialReleaseChannel == ReleaseChannelBeta, + Staging: initialReleaseChannel == ReleaseChannelStaging, + } +} + +// GetSimpleVersions returns the simplified update versions and status information. +func GetSimpleVersions() *SimpleVersions { + // Get all artifacts, simply map. + resources := make(map[string]*SimplifiedResourceVersion) + if artifacts, err := module.instance.BinaryUpdates().GetFiles(); err == nil { + for _, artifact := range artifacts { + resources[artifact.Filename] = &SimplifiedResourceVersion{ + Version: artifact.Version, + } + } + } + if artifacts, err := module.instance.IntelUpdates().GetFiles(); err == nil { + for _, artifact := range artifacts { + resources[artifact.Filename] = &SimplifiedResourceVersion{ + Version: artifact.Version, + } + } + } + + // Fill base info. + return &SimpleVersions{ + Build: info.GetInfo(), + Resources: resources, + Channel: initialReleaseChannel, + } +} + +func initVersionExport() { + module.instance.BinaryUpdates().EventResourcesUpdated.AddCallback("export version status", export) + module.instance.IntelUpdates().EventResourcesUpdated.AddCallback("export version status", export) + + _, _ = export(nil, struct{}{}) +} + +func (v *Versions) save() error { + if !v.KeyIsSet() { + v.SetKey(versionsDBKey) + } + return db.Put(v) +} + +func (v *SimpleVersions) save() error { + if !v.KeyIsSet() { + v.SetKey(simpleVersionsDBKey) + } + return db.Put(v) +} + +// export is an event hook. +func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { + // Export versions. + if err := GetVersions().save(); err != nil { + return false, err + } + if err := GetSimpleVersions().save(); err != nil { + return false, err + } + + return false, nil +} + +// AddVersionsToDebugInfo adds the update system status to the given debug.Info. +func AddVersionsToDebugInfo(di *debug.Info) { + overviewBuf := bytes.NewBuffer(nil) + tableBuf := bytes.NewBuffer(nil) + tabWriter := tabwriter.NewWriter(tableBuf, 8, 4, 3, ' ', 0) + fmt.Fprint(tabWriter, "\nFile\tVersion\tIndex\tSHA256\n") + + // Collect data for debug info. + var cnt int + if index, err := module.instance.BinaryUpdates().GetIndex(); err == nil { + fmt.Fprintf(overviewBuf, "Binaries Index: v%s from %s\n", index.Version, index.Published) + for _, artifact := range index.Artifacts { + fmt.Fprintf(tabWriter, "\n%s\t%s\t%s\t%s", artifact.Filename, vStr(artifact.Version), "binaries", artifact.SHA256) + cnt++ + } + } + if index, err := module.instance.IntelUpdates().GetIndex(); err == nil { + fmt.Fprintf(overviewBuf, "Intel Index: v%s from %s\n", index.Version, index.Published) + for _, artifact := range index.Artifacts { + fmt.Fprintf(tabWriter, "\n%s\t%s\t%s\t%s", artifact.Filename, vStr(artifact.Version), "intel", artifact.SHA256) + cnt++ + } + } + _ = tabWriter.Flush() + + // Add section. + di.AddSection( + fmt.Sprintf("Updates: %s (%d)", initialReleaseChannel, cnt), + debug.UseCodeSection, + overviewBuf.String(), + tableBuf.String(), + ) +} + +func vStr(v string) string { + if v != "" { + return v + } + return "unknown" +} diff --git a/service/instance.go b/service/instance.go index ddb23671..840bcdb0 100644 --- a/service/instance.go +++ b/service/instance.go @@ -167,10 +167,6 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx } // Service modules - instance.core, err = core.New(instance) - if err != nil { - return instance, fmt.Errorf("create core module: %w", err) - } binaryUpdateConfig, intelUpdateConfig, err := MakeUpdateConfigs(svcCfg) if err != nil { return instance, fmt.Errorf("create updates config: %w", err) @@ -183,6 +179,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx if err != nil { return instance, fmt.Errorf("create updates module: %w", err) } + instance.core, err = core.New(instance) + if err != nil { + return instance, fmt.Errorf("create core module: %w", err) + } instance.geoip, err = geoip.New(instance) if err != nil { return instance, fmt.Errorf("create customlist module: %w", err) @@ -708,3 +708,23 @@ func (i *Instance) ShutdownComplete() <-chan struct{} { func (i *Instance) ExitCode() int { return int(i.exitCode.Load()) } + +// ShouldRestartIsSet returns whether the service/instance should be restarted. +func (i *Instance) ShouldRestartIsSet() bool { + return i.ShouldRestart +} + +// CommandLineOperationIsSet returns whether the command line option is set. +func (i *Instance) CommandLineOperationIsSet() bool { + return i.CommandLineOperation != nil +} + +// CommandLineOperationExecute executes the set command line option. +func (i *Instance) CommandLineOperationExecute() error { + return i.CommandLineOperation() +} + +// AddModule adds a module to the service group. +func (i *Instance) AddModule(m mgr.Module) { + i.serviceGroup.Add(m) +} diff --git a/service/updates.go b/service/updates.go deleted file mode 100644 index 3c717951..00000000 --- a/service/updates.go +++ /dev/null @@ -1,120 +0,0 @@ -package service - -import ( - "path/filepath" - go_runtime "runtime" - - "github.com/safing/jess" - "github.com/safing/portmaster/service/updates" -) - -var ( - DefaultStableBinaryIndexURLs = []string{ - "https://updates.safing.io/stable.v3.json", - } - DefaultBetaBinaryIndexURLs = []string{ - "https://updates.safing.io/beta.v3.json", - } - DefaultStagingBinaryIndexURLs = []string{ - "https://updates.safing.io/staging.v3.json", - } - DefaultSupportBinaryIndexURLs = []string{ - "https://updates.safing.io/support.v3.json", - } - - DefaultIntelIndexURLs = []string{ - "https://updates.safing.io/intel.v3.json", - } - - // BinarySigningKeys holds the signing keys in text format. - BinarySigningKeys = []string{ - // Safing Code Signing Key #1 - "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", - // Safing Code Signing Key #2 - "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", - } - - // BinarySigningTrustStore is an in-memory trust store with the signing keys. - BinarySigningTrustStore = jess.NewMemTrustStore() -) - -func init() { - for _, signingKey := range BinarySigningKeys { - rcpt, err := jess.RecipientFromTextFormat(signingKey) - if err != nil { - panic(err) - } - err = BinarySigningTrustStore.StoreSignet(rcpt) - if err != nil { - panic(err) - } - } -} - -func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateConfig *updates.Config, err error) { - switch go_runtime.GOOS { - case "windows": - binaryUpdateConfig = &updates.Config{ - Name: "binaries", - Directory: svcCfg.BinDir, - DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"), - PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"), - Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: svcCfg.BinariesIndexURLs, - IndexFile: "index.json", - Verify: svcCfg.VerifyBinaryUpdates, - AutoCheck: true, // FIXME: Get from setting. - AutoDownload: false, - AutoApply: false, - NeedsRestart: true, - Notify: true, - } - intelUpdateConfig = &updates.Config{ - Name: "intel", - Directory: filepath.Join(svcCfg.DataDir, "intel"), - DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"), - PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"), - IndexURLs: svcCfg.IntelIndexURLs, - IndexFile: "index.json", - Verify: svcCfg.VerifyIntelUpdates, - AutoCheck: true, // FIXME: Get from setting. - AutoDownload: true, - AutoApply: true, - NeedsRestart: false, - Notify: false, - } - - case "linux": - binaryUpdateConfig = &updates.Config{ - Name: "binaries", - Directory: svcCfg.BinDir, - DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"), - PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"), - Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: svcCfg.BinariesIndexURLs, - IndexFile: "index.json", - Verify: svcCfg.VerifyBinaryUpdates, - AutoCheck: true, // FIXME: Get from setting. - AutoDownload: false, - AutoApply: false, - NeedsRestart: true, - Notify: true, - } - intelUpdateConfig = &updates.Config{ - Name: "intel", - Directory: filepath.Join(svcCfg.DataDir, "intel"), - DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"), - PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"), - IndexURLs: svcCfg.IntelIndexURLs, - IndexFile: "index.json", - Verify: svcCfg.VerifyIntelUpdates, - AutoCheck: true, // FIXME: Get from setting. - AutoDownload: true, - AutoApply: true, - NeedsRestart: false, - Notify: false, - } - } - - return -} diff --git a/service/updates/downloader.go b/service/updates/downloader.go index 30c32261..27836cac 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -192,7 +192,7 @@ artifacts: return nil } -func (d *Downloader) getArtifact(ctx context.Context, artifact Artifact, url string) ([]byte, error) { +func (d *Downloader) getArtifact(ctx context.Context, artifact *Artifact, url string) ([]byte, error) { // Download data from URL. artifactData, err := d.downloadData(ctx, url) if err != nil { diff --git a/service/updates/index.go b/service/updates/index.go index 8a30f643..baa76329 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -117,10 +117,10 @@ func (a *Artifact) export(dir string, indexVersion *semver.Version) *Artifact { // Index represents a collection of artifacts with metadata. type Index struct { - Name string `json:"Name"` - Version string `json:"Version"` - Published time.Time `json:"Published"` - Artifacts []Artifact `json:"Artifacts"` + Name string `json:"Name"` + Version string `json:"Version"` + Published time.Time `json:"Published"` + Artifacts []*Artifact `json:"Artifacts"` versionNum *semver.Version } @@ -173,7 +173,7 @@ func (index *Index) init() error { } // Filter artifacts by current platform. - filtered := make([]Artifact, 0) + filtered := make([]*Artifact, 0) for _, a := range index.Artifacts { if a.Platform == "" || a.Platform == currentPlatform { filtered = append(filtered, a) @@ -189,6 +189,7 @@ func (index *Index) init() error { a.versionNum = v } } else { + a.Version = index.Version a.versionNum = index.versionNum } } diff --git a/service/updates/index_scan.go b/service/updates/index_scan.go index 6e6a6af2..bc5cc0e8 100644 --- a/service/updates/index_scan.go +++ b/service/updates/index_scan.go @@ -95,7 +95,7 @@ settings: // GenerateIndexFromDir generates a index from a given folder. func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) { //nolint:maintidx - artifacts := make(map[string]Artifact) + artifacts := make(map[string]*Artifact) // Initialize. err := cfg.init() @@ -187,11 +187,12 @@ func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) // Step 3: Create new Artifact. - artifact := Artifact{} + artifact := &Artifact{} // Check if the caller provided a template for the artifact. if t, ok := cfg.Templates[identifier]; ok { - artifact = t + fromTemplate := t + artifact = &fromTemplate } // Set artifact properties. @@ -249,7 +250,7 @@ func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) } // Convert to slice and compute hashes. - export := make([]Artifact, 0, len(artifacts)) + export := make([]*Artifact, 0, len(artifacts)) for _, artifact := range artifacts { // Compute hash. hash, err := getSHA256(artifact.localFile, artifact.Unpack) @@ -273,7 +274,7 @@ func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) } // Sort final artifacts. - slices.SortFunc(export, func(a, b Artifact) int { + slices.SortFunc(export, func(a, b *Artifact) int { switch { case a.Filename != b.Filename: return strings.Compare(a.Filename, b.Filename) diff --git a/service/updates/module.go b/service/updates/module.go index e5b32129..2e008d48 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "sync" "time" @@ -132,9 +133,11 @@ type Updater struct { EventResourcesUpdated *mgr.EventMgr[struct{}] - corruptedInstallation bool + corruptedInstallation error isUpdateRunning *abool.AtomicBool + started *abool.AtomicBool + configureLock sync.Mutex instance instance } @@ -150,6 +153,7 @@ func New(instance instance, name string, cfg Config) (*Updater, error) { EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), isUpdateRunning: abool.NewBool(false), + started: abool.NewBool(false), instance: instance, } @@ -166,6 +170,12 @@ func New(instance instance, name string, cfg Config) (*Updater, error) { // Load index. index, err := LoadIndex(filepath.Join(cfg.Directory, cfg.IndexFile), cfg.Verify) if err == nil { + // Verify artifacts. + if err := index.VerifyArtifacts(cfg.Directory); err != nil { + module.corruptedInstallation = fmt.Errorf("invalid artifact: %w", err) + } + + // Save index to module and return. module.index = index return module, nil } @@ -173,6 +183,7 @@ func New(instance instance, name string, cfg Config) (*Updater, error) { // Fall back to scanning the directory. if !errors.Is(err, os.ErrNotExist) { log.Errorf("updates/%s: invalid index file, falling back to dir scan: %s", cfg.Name, err) + module.corruptedInstallation = fmt.Errorf("invalid index: %w", err) } index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{Version: "0.0.0"}) if err == nil && index.init() == nil { @@ -259,6 +270,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV // Check if automatic downloads are enabled. if !u.cfg.AutoDownload && !forceApply { + log.Infof("updates/%s: new update to v%s available, action required to download and upgrade", u.cfg.Name, downloader.index.Version) if u.cfg.Notify && u.instance.Notifications() != nil { u.instance.Notifications().Notify(¬ifications.Notification{ EventID: updateAvailableNotificationID, @@ -304,6 +316,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV // Notify the user that an upgrade is available. if !u.cfg.AutoApply && !forceApply { + log.Infof("updates/%s: new update to v%s available, action required to upgrade", u.cfg.Name, downloader.index.Version) if u.cfg.Notify && u.instance.Notifications() != nil { u.instance.Notifications().Notify(¬ifications.Notification{ EventID: updateAvailableNotificationID, @@ -387,8 +400,15 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV return nil } +func (u *Updater) getIndexURLsWithLock() []string { + u.configureLock.Lock() + defer u.configureLock.Unlock() + + return u.cfg.IndexURLs +} + func (u *Updater) updateCheckWorker(w *mgr.WorkerCtx) error { - err := u.updateAndUpgrade(w, u.cfg.IndexURLs, false, false) + err := u.updateAndUpgrade(w, u.getIndexURLsWithLock(), false, false) switch { case err == nil: return nil // Success! @@ -404,7 +424,7 @@ func (u *Updater) updateCheckWorker(w *mgr.WorkerCtx) error { } func (u *Updater) upgradeWorker(w *mgr.WorkerCtx) error { - err := u.updateAndUpgrade(w, u.cfg.IndexURLs, false, true) + err := u.updateAndUpgrade(w, u.getIndexURLsWithLock(), false, true) switch { case err == nil: return nil // Success! @@ -423,7 +443,7 @@ func (u *Updater) upgradeWorker(w *mgr.WorkerCtx) error { // and is intended to be used only within a tool, not a service. func (u *Updater) ForceUpdate() error { return u.m.Do("update and upgrade", func(w *mgr.WorkerCtx) error { - return u.updateAndUpgrade(w, u.cfg.IndexURLs, true, true) + return u.updateAndUpgrade(w, u.getIndexURLsWithLock(), true, true) }) } @@ -437,6 +457,33 @@ func (u *Updater) UpdateFromURL(url string) error { return nil } +// Configure makes slight configuration changes to the updater. +// It locks the index, which can take a while an update is running. +func (u *Updater) Configure(autoCheck bool, indexURLs []string) { + u.configureLock.Lock() + defer u.configureLock.Unlock() + + // Apply new config. + var changed bool + if u.cfg.AutoCheck != autoCheck { + u.cfg.AutoCheck = autoCheck + changed = true + } + if !slices.Equal(u.cfg.IndexURLs, indexURLs) { + u.cfg.IndexURLs = indexURLs + changed = true + } + + // Trigger update check if enabled and something changed. + if changed && u.started.IsSet() { + if autoCheck { + u.updateCheckWorkerMgr.Repeat(updateTaskRepeatDuration).Go() + } else { + u.updateCheckWorkerMgr.Repeat(0) + } + } +} + // TriggerUpdateCheck triggers an update check. func (u *Updater) TriggerUpdateCheck() { u.updateCheckWorkerMgr.Go() @@ -459,13 +506,17 @@ func (u *Updater) Manager() *mgr.Manager { // Start starts the module. func (u *Updater) Start() error { - if u.corruptedInstallation && u.cfg.Notify && u.instance.Notifications() != nil { - // FIXME: this might make sense as a module state - u.instance.Notifications().NotifyError( - corruptInstallationNotificationID, - "Install Corruption", - "Portmaster has detected that one or more of its own files have been corrupted. Please re-install the software.", - ) + u.configureLock.Lock() + defer u.configureLock.Unlock() + + if u.corruptedInstallation != nil && u.cfg.Notify && u.instance.Notifications() != nil { + u.states.Add(mgr.State{ + ID: corruptInstallationNotificationID, + Name: "Install Corruption", + Message: "Portmaster has detected that one or more of its own files have been corrupted. Please re-install the software. Error: " + u.corruptedInstallation.Error(), + Type: mgr.StateTypeError, + Data: u.corruptedInstallation, + }) } // Check for updates automatically, if enabled. @@ -474,6 +525,8 @@ func (u *Updater) Start() error { Repeat(updateTaskRepeatDuration). Delay(15 * time.Second) } + + u.started.SetTo(true) return nil } @@ -481,6 +534,62 @@ func (u *Updater) GetMainDir() string { return u.cfg.Directory } +// GetIndex returns a copy of the index. +func (u *Updater) GetIndex() (*Index, error) { + // Copy Artifacts. + artifacts, err := u.GetFiles() + if err != nil { + return nil, err + } + + u.indexLock.Lock() + defer u.indexLock.Unlock() + + // Check if any index is active. + if u.index == nil { + return nil, ErrNotFound + } + + return &Index{ + Name: u.index.Name, + Version: u.index.Version, + Published: u.index.Published, + Artifacts: artifacts, + versionNum: u.index.versionNum, + }, nil +} + +// GetFiles returns all artifacts. Returns ErrNotFound if no artifacts are found. +func (u *Updater) GetFiles() ([]*Artifact, error) { + u.indexLock.Lock() + defer u.indexLock.Unlock() + + // Check if any index is active. + if u.index == nil { + return nil, ErrNotFound + } + + // Export all artifacts. + export := make([]*Artifact, 0, len(u.index.Artifacts)) + for _, artifact := range u.index.Artifacts { + switch { + case artifact.Platform != "" && artifact.Platform != currentPlatform: + // Platform is defined and does not match. + // Platforms are usually pre-filtered, but just to be sure. + default: + // Artifact matches! + export = append(export, artifact.export(u.cfg.Directory, u.index.versionNum)) + } + } + + // Check if anything was exported. + if len(export) == 0 { + return nil, ErrNotFound + } + + return export, nil +} + // GetFile returns the path of a file given the name. Returns ErrNotFound if file is not found. func (u *Updater) GetFile(name string) (*Artifact, error) { u.indexLock.Lock() @@ -509,6 +618,7 @@ func (u *Updater) GetFile(name string) (*Artifact, error) { // Stop stops the module. func (u *Updater) Stop() error { + u.started.SetTo(false) return nil } diff --git a/service/updates/upgrade.go b/service/updates/upgrade.go index 86077ee1..1008dcdd 100644 --- a/service/updates/upgrade.go +++ b/service/updates/upgrade.go @@ -12,8 +12,6 @@ import ( "github.com/safing/portmaster/base/log" ) -// FIXME: previous update system did in-place service file upgrades. Check if this is still necessary and if changes are in current installers. - const ( defaultFileMode = os.FileMode(0o0644) executableFileMode = os.FileMode(0o0744) diff --git a/spn/instance.go b/spn/instance.go index dcfe01aa..33af8ea5 100644 --- a/spn/instance.go +++ b/spn/instance.go @@ -3,17 +3,18 @@ package spn import ( "context" "fmt" + "os" "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/log" "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" "github.com/safing/portmaster/service/core" "github.com/safing/portmaster/service/core/base" "github.com/safing/portmaster/service/intel/filterlists" @@ -79,27 +80,27 @@ type Instance struct { } // New returns a new Portmaster service instance. -func New() (*Instance, error) { +func New(svcCfg *service.ServiceConfig) (*Instance, error) { + // Initialize config. + err := svcCfg.Init() + if err != nil { + return nil, fmt.Errorf("internal service config error: %w", err) + } + + // Make sure data dir exists, so that child directories don't dictate the permissions. + err = os.MkdirAll(svcCfg.DataDir, 0o0755) + if err != nil { + return nil, fmt.Errorf("data directory %s is not accessible: %w", svcCfg.DataDir, err) + } + // Create instance to pass it to modules. - instance := &Instance{} + instance := &Instance{ + binDir: svcCfg.BinDir, + dataDir: svcCfg.DataDir, + } instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) instance.shutdownCtx, instance.cancelShutdownCtx = context.WithCancel(context.Background()) - binaryUpdateIndex := updates.Config{ - // FIXME: fill - } - - intelUpdateIndex := updates.Config{ - // FIXME: fill - } - - // Initialize log - log.GlobalWriter = log.NewStdoutWriter() - - // FIXME: initialize log file. - - var err error - // Base modules instance.base, err = base.New(instance) if err != nil { @@ -131,18 +132,22 @@ func New() (*Instance, error) { } // Service modules + binaryUpdateConfig, intelUpdateConfig, err := service.MakeUpdateConfigs(svcCfg) + if err != nil { + return instance, fmt.Errorf("create updates config: %w", err) + } + instance.binaryUpdates, err = updates.New(instance, "Binary Updater", *binaryUpdateConfig) + if err != nil { + return instance, fmt.Errorf("create updates module: %w", err) + } + instance.intelUpdates, err = updates.New(instance, "Intel Updater", *intelUpdateConfig) + if err != nil { + return instance, fmt.Errorf("create updates module: %w", err) + } instance.core, err = core.New(instance) if err != nil { return instance, fmt.Errorf("create core module: %w", err) } - instance.binaryUpdates, err = updates.New(instance, "Binary Updater", binaryUpdateIndex) - if err != nil { - return instance, fmt.Errorf("create updates module: %w", err) - } - instance.intelUpdates, err = updates.New(instance, "Intel Updater", intelUpdateIndex) - 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)