This commit is contained in:
Daniel 2024-11-27 16:16:15 +01:00
parent f91003d077
commit 706ce222d0
35 changed files with 1138 additions and 601 deletions

View file

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

View file

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

View file

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

View file

@ -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(" <SIGNAL: %v>", sig) // CLI output.
fmt.Printf(" <SIGNAL: %v>\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)
}
}

View file

@ -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(" <SIGNAL: %v>", sig) // CLI output.
fmt.Printf(" <SIGNAL: %v>\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(" <SERVICE CMD: %v>", serviceCmdName(c.Cmd)) // CLI output.
fmt.Printf(" <SERVICE CMD: %v>\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:

View file

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

20
cmds/cmdbase/version.go Normal file
View file

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

View file

@ -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(" <INTERRUPT>") // 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(" <INTERRUPT> 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,
}
}

View file

@ -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(" <INTERRUPT>") // 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(" <INTERRUPT> 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,
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import (
)
func setupDatabases(path string) error {
err := database.InitializeWithPath(path)
err := database.Initialize(path)
if err != nil {
return err
}

View file

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

View file

@ -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 = [];

View file

@ -543,7 +543,7 @@ export class SfngNetqueryLineChartComponent<D extends SeriesData = any> 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')

View file

@ -184,7 +184,7 @@ export class SfngNetquerySearchbarComponent implements ControlValueAccessor, OnI
const queries: Observable<SfngSearchbarSuggestion<any>>[] = [];
const queryKeys: (keyof Partial<NetqueryConnection>)[] = [];
// 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(&notifications.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(&notifications.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
}

View file

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

View file

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