From 83ec18f552cca4a7ef62f381f0f5f631243e740b Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Wed, 11 Sep 2024 18:52:36 +0300 Subject: [PATCH] [WIP] Updater support for windows --- cmds/portmaster-core/main.go | 94 +-------- cmds/portmaster-core/main_linux.go | 100 ++++++++++ cmds/portmaster-core/main_windows.go | 180 ++++++++++++++++++ desktop/tauri/src-tauri/src/main.rs | 1 + desktop/tauri/src-tauri/tauri.conf.json5 | 23 ++- .../interception/interception_windows.go | 7 +- service/firewall/interception/module.go | 5 +- .../interception/windowskext2/kext.go | 3 + service/instance.go | 60 ++++-- service/updates/registry/bundle.go | 9 + service/updates/registry/registry.go | 4 +- 11 files changed, 371 insertions(+), 115 deletions(-) create mode 100644 cmds/portmaster-core/main_linux.go create mode 100644 cmds/portmaster-core/main_windows.go diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index bc9f4f28..630f5827 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -8,14 +8,11 @@ import ( "io" "log/slog" "os" - "os/signal" "runtime" "runtime/pprof" "syscall" - "time" "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/service" "github.com/safing/portmaster/service/mgr" @@ -36,6 +33,11 @@ func init() { } func main() { + instance := initialize() + run(instance) +} + +func initialize() *service.Instance { flag.Parse() // set information @@ -80,91 +82,7 @@ func main() { } os.Exit(0) } - - // Set default log level. - log.SetLogLevel(log.WarningLevel) - _ = log.Start() - - // Start - go func() { - err = instance.Start() - if err != nil { - fmt.Printf("instance start failed: %s\n", err) - - // Print stack on start failure, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") - } - - os.Exit(1) - } - }() - - // Wait for signal. - signalCh := make(chan os.Signal, 1) - if enableInputSignals { - go inputSignals(signalCh) - } - 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.Stopped(): - 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() - - // Print stack on shutdown, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON EXIT") - } - - os.Exit(instance.ExitCode()) + return instance } func printStackTo(writer io.Writer, msg string) { diff --git a/cmds/portmaster-core/main_linux.go b/cmds/portmaster-core/main_linux.go new file mode 100644 index 00000000..aa8d9a60 --- /dev/null +++ b/cmds/portmaster-core/main_linux.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "github.com/safing/portmaster/base/log" + "github.com/safing/portmaster/service" +) + +func run(instance *service.Instance) { + // Set default log level. + log.SetLogLevel(log.WarningLevel) + _ = log.Start() + + // Start + go func() { + err := instance.Start() + if err != nil { + fmt.Printf("instance start failed: %s\n", err) + + // Print stack on start failure, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") + } + + os.Exit(1) + } + }() + + // Wait for signal. + signalCh := make(chan os.Signal, 1) + if enableInputSignals { + go inputSignals(signalCh) + } + 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.Stopped(): + 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() + + // Print stack on shutdown, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON EXIT") + } + + os.Exit(instance.ExitCode()) +} diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go new file mode 100644 index 00000000..61d4f840 --- /dev/null +++ b/cmds/portmaster-core/main_windows.go @@ -0,0 +1,180 @@ +package main + +// Based on the official Go examples from +// https://github.com/golang/sys/blob/master/windows/svc/example +// by The Go Authors. +// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory. + +import ( + "fmt" + "log/slog" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/safing/portmaster/base/log" + "github.com/safing/portmaster/service" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" +) + +var ( + // wait groups + runWg sync.WaitGroup + finishWg sync.WaitGroup +) + +const serviceName = "PortmasterCore" + +type windowsService struct { + instance *service.Instance +} + +func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + + startupComplete := make(chan struct{}) + go func() { + for !ws.instance.Ready() { + time.Sleep(1 * time.Second) + } + startupComplete <- struct{}{} + }() + +service: + for { + select { + case <-startupComplete: + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + case <-ws.instance.Stopped(): + changes <- svc.Status{State: svc.StopPending} + break service + case c := <-changeRequests: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + ws.instance.Shutdown() + default: + log.Errorf("unexpected control request: #%d\n", c) + } + } + } + + // wait until everything else is finished + finishWg.Wait() + + log.Shutdown() + + // send stopped status + changes <- svc.Status{State: svc.Stopped} + // wait a little for the status to reach Windows + time.Sleep(100 * time.Millisecond) + + return ssec, errno +} + +func run(instance *service.Instance) error { + log.SetLogLevel(log.WarningLevel) + _ = log.Start() + // check if we are running interactively + isService, err := svc.IsWindowsService() + if err != nil { + return fmt.Errorf("could not determine if running interactively: %s", err) + } + // select service run type + svcRun := svc.Run + if !isService { + log.Warningf("running interactively, switching to debug execution (no real service).\n") + svcRun = debug.Run + go registerSignalHandler(instance) + } + + runWg.Add(2) + + // run service client + go func() { + sErr := svcRun(serviceName, &windowsService{ + instance: instance, + }) + if sErr != nil { + log.Infof("shuting down service with error: %s", sErr) + } else { + log.Infof("shuting down service") + } + instance.Shutdown() + runWg.Done() + }() + + finishWg.Add(1) + // run service + go func() { + // run slightly delayed + time.Sleep(250 * time.Millisecond) + instance.Start() + + if err != nil { + fmt.Printf("instance start failed: %s\n", err) + + // Print stack on start failure, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") + } + + } + runWg.Done() + finishWg.Done() + }() + + runWg.Wait() + + return err +} + +func registerSignalHandler(instance *service.Instance) { + // Wait for signal. + signalCh := make(chan os.Signal, 1) + if enableInputSignals { + go inputSignals(signalCh) + } + 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") + instance.Shutdown() + } + } + + // 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) + } + } + }() +} diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index 7d605021..7dd9df42 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -120,6 +120,7 @@ fn show_webview_not_installed_dialog() -> i32 { } fn main() { + env::set_var("GDK_BACKEND", "x11"); if tauri::webview_version().is_err() { std::process::exit(show_webview_not_installed_dialog()); } diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5 index beba5cd7..a2d11d8a 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json5 +++ b/desktop/tauri/src-tauri/tauri.conf.json5 @@ -60,6 +60,17 @@ "desktopTemplate": "../../../packaging/linux/portmaster.desktop", "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", + "/usr/lib/portmaster/bin-index.json": "binaries/bin-index.json", + "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", + "/usr/lib/portmaster/portmaster.zip": "binaries/portmaster.zip", + "/usr/lib/portmaster/assets.zip": "binaries/assets.zip", + "/var/lib/portmaster/intel/intel-index.json": "binaries/intel-index.json", + "/var/lib/portmaster/intel/base.dsdl": "binaries/base.dsdl", + "/var/lib/portmaster/intel/geoipv4.mmdb": "binaries/geoipv4.mmdb", + "/var/lib/portmaster/intel/geoipv6.mmdb": "binaries/geoipv6.mmdb", + "/var/lib/portmaster/intel/index.dsd": "binaries/index.dsd", + "/var/lib/portmaster/intel/intermediate.dsdl": "binaries/intermediate.dsdl", + "/var/lib/portmaster/intel/urgent.dsdl": "binaries/urgent.dsdl", "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", @@ -73,7 +84,17 @@ "release": "1", "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", + "/usr/lib/portmaster/bin-index.json": "binaries/bin-index.json", "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", + "/usr/lib/portmaster/portmaster.zip": "binaries/portmaster.zip", + "/usr/lib/portmaster/assets.zip": "binaries/assets.zip", + "/var/lib/portmaster/intel/intel-index.json": "binaries/intel-index.json", + "/var/lib/portmaster/intel/base.dsdl": "binaries/base.dsdl", + "/var/lib/portmaster/intel/geoipv4.mmdb": "binaries/geoipv4.mmdb", + "/var/lib/portmaster/intel/geoipv6.mmdb": "binaries/geoipv6.mmdb", + "/var/lib/portmaster/intel/index.dsd": "binaries/index.dsd", + "/var/lib/portmaster/intel/intermediate.dsdl": "binaries/intermediate.dsdl", + "/var/lib/portmaster/intel/urgent.dsdl": "binaries/urgent.dsdl", "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", @@ -106,4 +127,4 @@ "../../../assets/data/icons/pm_light.ico" ] } -} \ No newline at end of file +} diff --git a/service/firewall/interception/interception_windows.go b/service/firewall/interception/interception_windows.go index cb97376b..3f9df6f8 100644 --- a/service/firewall/interception/interception_windows.go +++ b/service/firewall/interception/interception_windows.go @@ -10,14 +10,13 @@ import ( "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/network" "github.com/safing/portmaster/service/network/packet" - "github.com/safing/portmaster/service/updates" ) var useOldKext = false // start starts the interception. func startInterception(packets chan packet.Packet) error { - kextFile, err := updates.GetPlatformFile("kext/portmaster-kext.sys") + kextFile, err := module.instance.BinaryUpdates().GetFile("portmaster-kext.sys") if err != nil { return fmt.Errorf("interception: could not get kext sys: %s", err) } @@ -77,7 +76,6 @@ func startInterception(packets chan packet.Packet) error { case <-w.Done(): return nil } - } }) @@ -95,7 +93,6 @@ func startInterception(packets chan packet.Packet) error { case <-w.Done(): return nil } - } }) @@ -112,7 +109,6 @@ func startInterception(packets chan packet.Packet) error { case <-w.Done(): return nil } - } }) } @@ -159,5 +155,4 @@ func GetKextVersion() (string, error) { } return version.String(), nil } - } diff --git a/service/firewall/interception/module.go b/service/firewall/interception/module.go index 072eb3b5..158432e0 100644 --- a/service/firewall/interception/module.go +++ b/service/firewall/interception/module.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/updates" ) // Interception is the packet interception module. @@ -97,4 +98,6 @@ func New(instance instance) (*Interception, error) { return module, nil } -type instance interface{} +type instance interface { + BinaryUpdates() *updates.Updates +} diff --git a/service/firewall/interception/windowskext2/kext.go b/service/firewall/interception/windowskext2/kext.go index ed15476e..43be43cd 100644 --- a/service/firewall/interception/windowskext2/kext.go +++ b/service/firewall/interception/windowskext2/kext.go @@ -62,6 +62,9 @@ func GetKextServiceHandle() windows.Handle { // Stop intercepting. func Stop() error { + if kextFile == nil { + return fmt.Errorf("kextfile is nil") + } // Prepare kernel for shutdown err := shutdownRequest() if err != nil { diff --git a/service/instance.go b/service/instance.go index 43c7562b..8f63a78b 100644 --- a/service/instance.go +++ b/service/instance.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + go_runtime "runtime" "sync/atomic" "time" @@ -47,23 +48,6 @@ import ( "github.com/safing/portmaster/spn/terminal" ) -var binaryUpdateIndex = registry.UpdateIndex{ - Directory: "/usr/lib/portmaster", - DownloadDirectory: "/var/lib/portmaster/new_bin", - Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: []string{"http://localhost:8000/test-binary.json"}, - IndexFile: "bin-index.json", - AutoApply: false, -} - -var intelUpdateIndex = registry.UpdateIndex{ - Directory: "/var/lib/portmaster/intel", - DownloadDirectory: "/var/lib/portmaster/new_intel", - IndexURLs: []string{"http://localhost:8000/test-intel.json"}, - IndexFile: "intel-index.json", - AutoApply: true, -} - // Instance is an instance of a Portmaster service. type Instance struct { ctx context.Context @@ -121,6 +105,48 @@ type Instance struct { // New returns a new Portmaster service instance. func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx + var binaryUpdateIndex registry.UpdateIndex + var intelUpdateIndex registry.UpdateIndex + if go_runtime.GOOS == "windows" { + binaryUpdateIndex = registry.UpdateIndex{ + Directory: "C:/Program Files/Portmaster/binary", + DownloadDirectory: "C:/Program Files/Portmaster/new_binary", + PurgeDirectory: "C:/Program Files/Portmaster/old_binary", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://192.168.88.11:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, + } + + intelUpdateIndex = registry.UpdateIndex{ + Directory: "C:/Program Files/Portmaster/intel", + DownloadDirectory: "C:/Program Files/Portmaster/new_intel", + PurgeDirectory: "C:/Program Files/Portmaster/old_intel", + IndexURLs: []string{"http://192.168.88.11:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, + } + } else if go_runtime.GOOS == "linux" { + binaryUpdateIndex = registry.UpdateIndex{ + Directory: "/usr/lib/portmaster", + DownloadDirectory: "/var/lib/portmaster/new_bin", + PurgeDirectory: "/var/lib/portmaster/old_bin", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://localhost:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, + } + + intelUpdateIndex = registry.UpdateIndex{ + Directory: "/var/lib/portmaster/intel", + DownloadDirectory: "/var/lib/portmaster/new_intel", + PurgeDirectory: "/var/lib/portmaster/intel_bin", + IndexURLs: []string{"http://localhost:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, + } + } + // Create instance to pass it to modules. instance := &Instance{} instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) diff --git a/service/updates/registry/bundle.go b/service/updates/registry/bundle.go index 24953966..4df887ad 100644 --- a/service/updates/registry/bundle.go +++ b/service/updates/registry/bundle.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" "time" "github.com/safing/portmaster/base/log" @@ -19,6 +20,8 @@ import ( const MaxUnpackSize = 1 << 30 // 2^30 == 1GB +const current_platform = runtime.GOOS + "_" + runtime.GOARCH + type Artifact struct { Filename string `json:"Filename"` SHA256 string `json:"SHA256"` @@ -107,6 +110,11 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { } func processArtifact(client *http.Client, artifact Artifact, filePath string) error { + // Skip artifacts not meant for this machine. + if artifact.Platform != "" && artifact.Platform != current_platform { + return nil + } + providedHash, err := hex.DecodeString(artifact.SHA256) if err != nil || len(providedHash) != sha256.Size { return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) @@ -149,6 +157,7 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er if err != nil { return fmt.Errorf("failed to write to file: %w", err) } + file.Close() // Rename err = os.Rename(tmpFilename, filePath) diff --git a/service/updates/registry/registry.go b/service/updates/registry/registry.go index f0e77adb..f670df26 100644 --- a/service/updates/registry/registry.go +++ b/service/updates/registry/registry.go @@ -125,7 +125,7 @@ func (reg *Registry) DownloadUpdates() error { // ApplyUpdates removes the current binary folder and replaces it with the downloaded one. func (reg *Registry) ApplyUpdates() error { // Create purge dir. - err := os.MkdirAll(filepath.Dir(reg.updateIndex.PurgeDirectory), defaultDirMode) + err := os.MkdirAll(reg.updateIndex.PurgeDirectory, defaultDirMode) if err != nil { return fmt.Errorf("failed to create directory: %w", err) } @@ -198,7 +198,7 @@ func deleteUnfinishedDownloads(rootDir string) error { // Check if the current file has the specified extension if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { - log.Warningf("updates deleting unfinished: %s\n", path) + log.Warningf("updates: deleting unfinished: %s\n", path) err := os.Remove(path) if err != nil { return fmt.Errorf("failed to delete file %s: %w", path, err)