[WIP] Updater support for windows

This commit is contained in:
Vladimir Stoilov 2024-09-11 18:52:36 +03:00
parent 8c6eb04292
commit 83ec18f552
No known key found for this signature in database
GPG key ID: 2F190B67A43A81AF
11 changed files with 371 additions and 115 deletions

View file

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

View file

@ -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(" <INTERRUPT>") // 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(" <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()
// Print stack on shutdown, if enabled.
if printStackOnExit {
printStackTo(os.Stdout, "PRINTING STACK ON EXIT")
}
os.Exit(instance.ExitCode())
}

View file

@ -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(" <INTERRUPT>") // 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(" <INTERRUPT> again, but already shutting down - %d more to force\n", forceCnt)
} else {
printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT")
os.Exit(1)
}
}
}()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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