From 22253c4e9e53918d9675dff5ffed67b3baa2304f Mon Sep 17 00:00:00 2001
From: Vladimir Stoilov <vladimir@safing.io>
Date: Fri, 6 Dec 2024 12:00:20 +0200
Subject: [PATCH 01/12] [service] Fix windows permissions

---
 base/config/main.go                    |  4 +--
 base/database/main.go                  |  6 ++--
 base/database/storage/fstree/fstree.go |  7 ++--
 base/dataroot/root.go                  |  3 +-
 base/updater/fetch.go                  | 17 +++-------
 base/updater/registry.go               |  2 +-
 base/utils/fs.go                       | 19 +++++------
 base/utils/permissions.go              | 20 ++++++++++++
 base/utils/permissions_windows.go      | 35 ++++++++++++++++++++
 base/utils/structure.go                | 44 +++++++++++++++++++++++---
 cmds/portmaster-start/dirs.go          |  3 +-
 cmds/portmaster-start/main.go          |  6 ++--
 service/core/base/global.go            |  3 +-
 service/netquery/database.go           |  5 +--
 service/profile/module.go              |  3 +-
 service/ui/module.go                   |  3 +-
 service/updates/main.go                |  3 +-
 service/updates/upgrader.go            | 12 +++----
 18 files changed, 138 insertions(+), 57 deletions(-)
 create mode 100644 base/utils/permissions.go
 create mode 100644 base/utils/permissions_windows.go

diff --git a/base/config/main.go b/base/config/main.go
index 0ed0b7e6..c737dc2f 100644
--- a/base/config/main.go
+++ b/base/config/main.go
@@ -144,9 +144,9 @@ func InitializeUnitTestDataroot(testName string) (string, error) {
 		return "", fmt.Errorf("failed to make tmp dir: %w", err)
 	}
 
-	ds := utils.NewDirStructure(basePath, 0o0755)
+	ds := utils.NewDirStructure(basePath, utils.PublicReadPermission)
 	SetDataRoot(ds)
-	err = dataroot.Initialize(basePath, 0o0755)
+	err = dataroot.Initialize(basePath, utils.PublicReadPermission)
 	if err != nil {
 		return "", fmt.Errorf("failed to initialize dataroot: %w", err)
 	}
diff --git a/base/database/main.go b/base/database/main.go
index f84a0108..9c9aa1ed 100644
--- a/base/database/main.go
+++ b/base/database/main.go
@@ -25,7 +25,7 @@ var (
 
 // InitializeWithPath initializes the database at the specified location using a path.
 func InitializeWithPath(dirPath string) error {
-	return Initialize(utils.NewDirStructure(dirPath, 0o0755))
+	return Initialize(utils.NewDirStructure(dirPath, utils.PublicReadPermission))
 }
 
 // Initialize initializes the database at the specified location using a dir structure.
@@ -34,7 +34,7 @@ func Initialize(dirStructureRoot *utils.DirStructure) error {
 		rootStructure = dirStructureRoot
 
 		// ensure root and databases dirs
-		databasesStructure = rootStructure.ChildDir(databasesSubDir, 0o0700)
+		databasesStructure = rootStructure.ChildDir(databasesSubDir, utils.AdminOnlyPermission)
 		err := databasesStructure.Ensure()
 		if err != nil {
 			return fmt.Errorf("could not create/open database directory (%s): %w", rootStructure.Path, err)
@@ -67,7 +67,7 @@ func Shutdown() (err error) {
 
 // getLocation returns the storage location for the given name and type.
 func getLocation(name, storageType string) (string, error) {
-	location := databasesStructure.ChildDir(name, 0o0700).ChildDir(storageType, 0o0700)
+	location := databasesStructure.ChildDir(name, utils.AdminOnlyPermission).ChildDir(storageType, utils.AdminOnlyPermission)
 	// check location
 	err := location.Ensure()
 	if err != nil {
diff --git a/base/database/storage/fstree/fstree.go b/base/database/storage/fstree/fstree.go
index 7965439a..0b4f175c 100644
--- a/base/database/storage/fstree/fstree.go
+++ b/base/database/storage/fstree/fstree.go
@@ -289,11 +289,8 @@ func writeFile(filename string, data []byte, perm os.FileMode) error {
 	defer t.Cleanup() //nolint:errcheck
 
 	// Set permissions before writing data, in case the data is sensitive.
-	if onWindows {
-		err = acl.Chmod(filename, perm)
-	} else {
-		err = t.Chmod(perm)
-	}
+	// TODO(vladimir): to set permissions on windows we need the full path of the file.
+	err = t.Chmod(perm)
 	if err != nil {
 		return err
 	}
diff --git a/base/dataroot/root.go b/base/dataroot/root.go
index 296b342f..75805255 100644
--- a/base/dataroot/root.go
+++ b/base/dataroot/root.go
@@ -2,7 +2,6 @@ package dataroot
 
 import (
 	"errors"
-	"os"
 
 	"github.com/safing/portmaster/base/utils"
 )
@@ -10,7 +9,7 @@ import (
 var root *utils.DirStructure
 
 // Initialize initializes the data root directory.
-func Initialize(rootDir string, perm os.FileMode) error {
+func Initialize(rootDir string, perm utils.FSPermission) error {
 	if root != nil {
 		return errors.New("already initialized")
 	}
diff --git a/base/updater/fetch.go b/base/updater/fetch.go
index 150037ed..f7f373dc 100644
--- a/base/updater/fetch.go
+++ b/base/updater/fetch.go
@@ -14,10 +14,10 @@ import (
 	"path/filepath"
 	"time"
 
-	"github.com/hectane/go-acl"
 	"github.com/safing/jess/filesig"
 	"github.com/safing/jess/lhash"
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/base/utils/renameio"
 )
 
@@ -137,17 +137,10 @@ func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client,
 		return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err)
 	}
 	// set permissions
-	if onWindows {
-		err = acl.Chmod(rv.storagePath(), 0o0755)
-		if err != nil {
-			log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err)
-		}
-	} else {
-		// TODO: only set executable files to 0755, set other to 0644
-		err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above.
-		if err != nil {
-			log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err)
-		}
+	// TODO: distinguish between executable and non executable files.
+	err = utils.SetExecPermission(rv.storagePath(), utils.PublicReadPermission)
+	if err != nil {
+		log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err)
 	}
 
 	log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath())
diff --git a/base/updater/registry.go b/base/updater/registry.go
index 8deda74e..4450fe98 100644
--- a/base/updater/registry.go
+++ b/base/updater/registry.go
@@ -98,7 +98,7 @@ func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error {
 
 	// initialize private attributes
 	reg.storageDir = storageDir
-	reg.tmpDir = storageDir.ChildDir("tmp", 0o0700)
+	reg.tmpDir = storageDir.ChildDir("tmp", utils.AdminOnlyPermission)
 	reg.resources = make(map[string]*Resource)
 	if reg.state == nil {
 		reg.state = &RegistryState{}
diff --git a/base/utils/fs.go b/base/utils/fs.go
index bb59960f..6ede989d 100644
--- a/base/utils/fs.go
+++ b/base/utils/fs.go
@@ -6,15 +6,13 @@ import (
 	"io/fs"
 	"os"
 	"runtime"
-
-	"github.com/hectane/go-acl"
 )
 
 const isWindows = runtime.GOOS == "windows"
 
 // EnsureDirectory ensures that the given directory exists and that is has the given permissions set.
 // If path is a file, it is deleted and a directory created.
-func EnsureDirectory(path string, perm os.FileMode) error {
+func EnsureDirectory(path string, perm FSPermission) error {
 	// open path
 	f, err := os.Stat(path)
 	if err == nil {
@@ -23,10 +21,10 @@ func EnsureDirectory(path string, perm os.FileMode) error {
 			// directory exists, check permissions
 			if isWindows {
 				// Ignore windows permission error. For none admin users it will always fail.
-				acl.Chmod(path, perm)
+				SetDirPermission(path, perm)
 				return nil
-			} else if f.Mode().Perm() != perm {
-				return os.Chmod(path, perm)
+			} else if f.Mode().Perm() != perm.AsUnixDirExecPermission() {
+				return SetDirPermission(path, perm)
 			}
 			return nil
 		}
@@ -37,17 +35,16 @@ func EnsureDirectory(path string, perm os.FileMode) error {
 	}
 	// file does not exist (or has been deleted)
 	if err == nil || errors.Is(err, fs.ErrNotExist) {
-		err = os.Mkdir(path, perm)
+		err = os.Mkdir(path, perm.AsUnixDirExecPermission())
 		if err != nil {
 			return fmt.Errorf("could not create dir %s: %w", path, err)
 		}
+		// Set windows permissions. Linux permission where already set with creation.
 		if isWindows {
 			// Ignore windows permission error. For none admin users it will always fail.
-			acl.Chmod(path, perm)
-			return nil
-		} else {
-			return os.Chmod(path, perm)
+			SetDirPermission(path, perm)
 		}
+		return nil
 	}
 	// other error opening path
 	return fmt.Errorf("failed to access %s: %w", path, err)
diff --git a/base/utils/permissions.go b/base/utils/permissions.go
new file mode 100644
index 00000000..d7690724
--- /dev/null
+++ b/base/utils/permissions.go
@@ -0,0 +1,20 @@
+//go:build !windows
+
+package utils
+
+import "os"
+
+// SetDirPermission sets the permission of a directory.
+func SetDirPermission(path string, perm FSPermission) error {
+	return os.Chmod(path, perm.AsUnixDirExecPermission())
+}
+
+// SetExecPermission sets the permission of an executable file.
+func SetExecPermission(path string, perm FSPermission) error {
+	return SetDirPermission(path, perm)
+}
+
+// SetFilePermission sets the permission of a non executable file.
+func SetFilePermission(path string, perm FSPermission) error {
+	return os.Chmod(path, perm.AsUnixFilePermission())
+}
diff --git a/base/utils/permissions_windows.go b/base/utils/permissions_windows.go
new file mode 100644
index 00000000..ac48c21a
--- /dev/null
+++ b/base/utils/permissions_windows.go
@@ -0,0 +1,35 @@
+//go:build windows
+
+package utils
+
+import (
+	"github.com/hectane/go-acl"
+	"golang.org/x/sys/windows"
+)
+
+func SetDirPermission(path string, perm FSPermission) error {
+	setWindowsFilePermissions(path, perm)
+	return nil
+}
+
+// SetExecPermission sets the permission of an executable file.
+func SetExecPermission(path string, perm FSPermission) error {
+	return SetDirPermission(path, perm)
+}
+
+func setWindowsFilePermissions(path string, perm FSPermission) {
+	switch perm {
+	case AdminOnlyPermission:
+		// Set only admin rights, remove all others.
+		acl.Apply(path, true, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Administrators"))
+	case PublicReadPermission:
+		// Set admin rights and read/execute rights for users, remove all others.
+		acl.Apply(path, true, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Administrators"))
+		acl.Apply(path, false, false, acl.GrantName(windows.GENERIC_EXECUTE, "Users"))
+		acl.Apply(path, false, false, acl.GrantName(windows.GENERIC_READ, "Users"))
+	case PublicWritePermission:
+		// Set full control to admin and regular users. Guest users will not have access.
+		acl.Apply(path, true, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Administrators"))
+		acl.Apply(path, false, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Users"))
+	}
+}
diff --git a/base/utils/structure.go b/base/utils/structure.go
index 5a50d97e..e33a920f 100644
--- a/base/utils/structure.go
+++ b/base/utils/structure.go
@@ -2,25 +2,61 @@ package utils
 
 import (
 	"fmt"
-	"os"
+	"io/fs"
 	"path/filepath"
 	"strings"
 	"sync"
 )
 
+type FSPermission uint8
+
+const (
+	AdminOnlyPermission FSPermission = iota
+	PublicReadPermission
+	PublicWritePermission
+)
+
+// AsUnixDirPermission return the corresponding unix permission for a directory or executable.
+func (perm FSPermission) AsUnixDirExecPermission() fs.FileMode {
+	switch perm {
+	case AdminOnlyPermission:
+		return 0o700
+	case PublicReadPermission:
+		return 0o755
+	case PublicWritePermission:
+		return 0o777
+	}
+
+	return 0
+}
+
+// AsUnixDirPermission return the corresponding unix permission for a regular file.
+func (perm FSPermission) AsUnixFilePermission() fs.FileMode {
+	switch perm {
+	case AdminOnlyPermission:
+		return 0o600
+	case PublicReadPermission:
+		return 0o655
+	case PublicWritePermission:
+		return 0o666
+	}
+
+	return 0
+}
+
 // DirStructure represents a directory structure with permissions that should be enforced.
 type DirStructure struct {
 	sync.Mutex
 
 	Path     string
 	Dir      string
-	Perm     os.FileMode
+	Perm     FSPermission
 	Parent   *DirStructure
 	Children map[string]*DirStructure
 }
 
 // NewDirStructure returns a new DirStructure.
-func NewDirStructure(path string, perm os.FileMode) *DirStructure {
+func NewDirStructure(path string, perm FSPermission) *DirStructure {
 	return &DirStructure{
 		Path:     path,
 		Perm:     perm,
@@ -29,7 +65,7 @@ func NewDirStructure(path string, perm os.FileMode) *DirStructure {
 }
 
 // ChildDir adds a new child DirStructure and returns it. Should the child already exist, the existing child is returned and the permissions are updated.
-func (ds *DirStructure) ChildDir(dirName string, perm os.FileMode) (child *DirStructure) {
+func (ds *DirStructure) ChildDir(dirName string, perm FSPermission) (child *DirStructure) {
 	ds.Lock()
 	defer ds.Unlock()
 
diff --git a/cmds/portmaster-start/dirs.go b/cmds/portmaster-start/dirs.go
index e327963f..a95c500b 100644
--- a/cmds/portmaster-start/dirs.go
+++ b/cmds/portmaster-start/dirs.go
@@ -5,6 +5,7 @@ import (
 	"log"
 	"os"
 
+	"github.com/safing/portmaster/base/utils"
 	"github.com/spf13/cobra"
 )
 
@@ -24,7 +25,7 @@ var cleanStructureCmd = &cobra.Command{
 }
 
 func cleanAndEnsureExecDir() error {
-	execDir := dataRoot.ChildDir("exec", 0o777)
+	execDir := dataRoot.ChildDir("exec", utils.PublicWritePermission)
 
 	// Clean up and remove exec dir.
 	err := os.RemoveAll(execDir.Path)
diff --git a/cmds/portmaster-start/main.go b/cmds/portmaster-start/main.go
index f764dfbf..d13a4bd6 100644
--- a/cmds/portmaster-start/main.go
+++ b/cmds/portmaster-start/main.go
@@ -179,14 +179,14 @@ func configureRegistry(mustLoadIndex bool) error {
 	// Remove left over quotes.
 	dataDir = strings.Trim(dataDir, `\"`)
 	// Initialize data root.
-	err := dataroot.Initialize(dataDir, 0o0755)
+	err := dataroot.Initialize(dataDir, utils.PublicReadPermission)
 	if err != nil {
 		return fmt.Errorf("failed to initialize data root: %w", err)
 	}
 	dataRoot = dataroot.Root()
 
 	// Initialize registry.
-	err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755))
+	err = registry.Initialize(dataRoot.ChildDir("updates", utils.PublicReadPermission))
 	if err != nil {
 		return err
 	}
@@ -196,7 +196,7 @@ func configureRegistry(mustLoadIndex bool) error {
 
 func ensureLoggingDir() error {
 	// set up logs root
-	logsRoot = dataRoot.ChildDir("logs", 0o0777)
+	logsRoot = dataRoot.ChildDir("logs", utils.PublicWritePermission)
 	err := logsRoot.Ensure()
 	if err != nil {
 		return fmt.Errorf("failed to initialize logs root (%q): %w", logsRoot.Path, err)
diff --git a/service/core/base/global.go b/service/core/base/global.go
index 3b1cc82f..fa67048f 100644
--- a/service/core/base/global.go
+++ b/service/core/base/global.go
@@ -8,6 +8,7 @@ import (
 	"github.com/safing/portmaster/base/api"
 	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/info"
+	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/service/mgr"
 )
 
@@ -54,7 +55,7 @@ func prep(instance instance) error {
 		}
 
 		// initialize structure
-		err := dataroot.Initialize(dataDir, 0o0755)
+		err := dataroot.Initialize(dataDir, utils.PublicReadPermission)
 		if err != nil {
 			return err
 		}
diff --git a/service/netquery/database.go b/service/netquery/database.go
index a1cd6aea..912fe3cc 100644
--- a/service/netquery/database.go
+++ b/service/netquery/database.go
@@ -19,6 +19,7 @@ import (
 	"github.com/safing/portmaster/base/config"
 	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/service/netquery/orm"
 	"github.com/safing/portmaster/service/network"
 	"github.com/safing/portmaster/service/network/netutils"
@@ -127,7 +128,7 @@ type (
 // Note that write connections are serialized by the Database object before being
 // handed over to SQLite.
 func New(dbPath string) (*Database, error) {
-	historyParentDir := dataroot.Root().ChildDir("databases", 0o700)
+	historyParentDir := dataroot.Root().ChildDir("databases", utils.AdminOnlyPermission)
 	if err := historyParentDir.Ensure(); err != nil {
 		return nil, fmt.Errorf("failed to ensure database directory exists: %w", err)
 	}
@@ -225,7 +226,7 @@ func (db *Database) Close() error {
 
 // VacuumHistory rewrites the history database in order to purge deleted records.
 func VacuumHistory(ctx context.Context) (err error) {
-	historyParentDir := dataroot.Root().ChildDir("databases", 0o700)
+	historyParentDir := dataroot.Root().ChildDir("databases", utils.AdminOnlyPermission)
 	if err := historyParentDir.Ensure(); err != nil {
 		return fmt.Errorf("failed to ensure database directory exists: %w", err)
 	}
diff --git a/service/profile/module.go b/service/profile/module.go
index 911ef99c..652fcd52 100644
--- a/service/profile/module.go
+++ b/service/profile/module.go
@@ -11,6 +11,7 @@ import (
 	"github.com/safing/portmaster/base/database/migration"
 	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
 	_ "github.com/safing/portmaster/service/core/base"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/profile/binmeta"
@@ -70,7 +71,7 @@ func prep() error {
 	}
 
 	// Setup icon storage location.
-	iconsDir := dataroot.Root().ChildDir("databases", 0o0700).ChildDir("icons", 0o0700)
+	iconsDir := dataroot.Root().ChildDir("databases", utils.AdminOnlyPermission).ChildDir("icons", utils.AdminOnlyPermission)
 	if err := iconsDir.Ensure(); err != nil {
 		return fmt.Errorf("failed to create/check icons directory: %w", err)
 	}
diff --git a/service/ui/module.go b/service/ui/module.go
index 630808e5..b9f7220f 100644
--- a/service/ui/module.go
+++ b/service/ui/module.go
@@ -7,6 +7,7 @@ import (
 	"github.com/safing/portmaster/base/api"
 	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/service/mgr"
 )
 
@@ -27,7 +28,7 @@ func start() error {
 	// may seem dangerous, but proper permission on the parent directory provide
 	// (some) protection.
 	// Processes must _never_ read from this directory.
-	err := dataroot.Root().ChildDir("exec", 0o0777).Ensure()
+	err := dataroot.Root().ChildDir("exec", utils.PublicWritePermission).Ensure()
 	if err != nil {
 		log.Warningf("ui: failed to create safe exec dir: %s", err)
 	}
diff --git a/service/updates/main.go b/service/updates/main.go
index bb942993..c5a41d43 100644
--- a/service/updates/main.go
+++ b/service/updates/main.go
@@ -13,6 +13,7 @@ import (
 	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/updates/helper"
 )
@@ -138,7 +139,7 @@ func start() error {
 	}
 
 	// initialize
-	err = registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755))
+	err = registry.Initialize(dataroot.Root().ChildDir(updatesDirName, utils.PublicReadPermission))
 	if err != nil {
 		return err
 	}
diff --git a/service/updates/upgrader.go b/service/updates/upgrader.go
index 4963bced..f1d3c297 100644
--- a/service/updates/upgrader.go
+++ b/service/updates/upgrader.go
@@ -11,7 +11,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/hectane/go-acl"
 	processInfo "github.com/shirou/gopsutil/process"
 	"github.com/tevino/abool"
 
@@ -21,6 +20,7 @@ import (
 	"github.com/safing/portmaster/base/notifications"
 	"github.com/safing/portmaster/base/rng"
 	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/base/utils/renameio"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/updates/helper"
@@ -351,17 +351,15 @@ func upgradeBinary(fileToUpgrade string, file *updater.File) error {
 
 	// check permissions
 	if onWindows {
-		err = acl.Chmod(fileToUpgrade, 0o0755)
-		if err != nil {
-			return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err)
-		}
+		utils.SetExecPermission(fileToUpgrade, utils.PublicReadPermission)
 	} else {
+		perm := utils.PublicReadPermission
 		info, err := os.Stat(fileToUpgrade)
 		if err != nil {
 			return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err)
 		}
-		if info.Mode() != 0o0755 {
-			err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions.
+		if info.Mode() != perm.AsUnixDirExecPermission() {
+			err = utils.SetExecPermission(fileToUpgrade, perm)
 			if err != nil {
 				return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err)
 			}

From 05a5d5e350c57b2a047a2d5c1db5d1a8c90eef82 Mon Sep 17 00:00:00 2001
From: Vladimir Stoilov <vladimir@safing.io>
Date: Fri, 6 Dec 2024 13:28:24 +0200
Subject: [PATCH 02/12] [service] Fix unit tests

---
 base/database/storage/fstree/fstree.go |  1 -
 base/updater/registry.go               |  5 -----
 base/updater/registry_test.go          |  2 +-
 base/utils/fs.go                       | 11 ++++++-----
 base/utils/permissions_windows.go      |  3 +++
 base/utils/structure.go                |  6 +++---
 base/utils/structure_test.go           | 26 +++++++++++++-------------
 cmds/notifier/main.go                  |  4 ++--
 cmds/updatemgr/main.go                 |  2 +-
 cmds/updatemgr/release.go              |  3 ++-
 service/updates/upgrader.go            |  2 +-
 11 files changed, 32 insertions(+), 33 deletions(-)

diff --git a/base/database/storage/fstree/fstree.go b/base/database/storage/fstree/fstree.go
index 0b4f175c..4b04f41d 100644
--- a/base/database/storage/fstree/fstree.go
+++ b/base/database/storage/fstree/fstree.go
@@ -15,7 +15,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/hectane/go-acl"
 	"github.com/safing/portmaster/base/database/iterator"
 	"github.com/safing/portmaster/base/database/query"
 	"github.com/safing/portmaster/base/database/record"
diff --git a/base/updater/registry.go b/base/updater/registry.go
index 4450fe98..a2bf5fcd 100644
--- a/base/updater/registry.go
+++ b/base/updater/registry.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
-	"runtime"
 	"strings"
 	"sync"
 
@@ -13,10 +12,6 @@ import (
 	"github.com/safing/portmaster/base/utils"
 )
 
-const (
-	onWindows = runtime.GOOS == "windows"
-)
-
 // ResourceRegistry is a registry for managing update resources.
 type ResourceRegistry struct {
 	sync.RWMutex
diff --git a/base/updater/registry_test.go b/base/updater/registry_test.go
index a8978f68..1b409b25 100644
--- a/base/updater/registry_test.go
+++ b/base/updater/registry_test.go
@@ -20,7 +20,7 @@ func TestMain(m *testing.M) {
 		DevMode:        true,
 		Online:         true,
 	}
-	err = registry.Initialize(utils.NewDirStructure(tmpDir, 0o0777))
+	err = registry.Initialize(utils.NewDirStructure(tmpDir, utils.PublicWritePermission))
 	if err != nil {
 		panic(err)
 	}
diff --git a/base/utils/fs.go b/base/utils/fs.go
index 6ede989d..5eb456b1 100644
--- a/base/utils/fs.go
+++ b/base/utils/fs.go
@@ -21,7 +21,7 @@ func EnsureDirectory(path string, perm FSPermission) error {
 			// directory exists, check permissions
 			if isWindows {
 				// Ignore windows permission error. For none admin users it will always fail.
-				SetDirPermission(path, perm)
+				_ = SetDirPermission(path, perm)
 				return nil
 			} else if f.Mode().Perm() != perm.AsUnixDirExecPermission() {
 				return SetDirPermission(path, perm)
@@ -39,10 +39,11 @@ func EnsureDirectory(path string, perm FSPermission) error {
 		if err != nil {
 			return fmt.Errorf("could not create dir %s: %w", path, err)
 		}
-		// Set windows permissions. Linux permission where already set with creation.
-		if isWindows {
-			// Ignore windows permission error. For none admin users it will always fail.
-			SetDirPermission(path, perm)
+		// Set permissions.
+		err = SetDirPermission(path, perm)
+		// Ignore windows permission error. For none admin users it will always fail.
+		if !isWindows {
+			return err
 		}
 		return nil
 	}
diff --git a/base/utils/permissions_windows.go b/base/utils/permissions_windows.go
index ac48c21a..5f36ebb9 100644
--- a/base/utils/permissions_windows.go
+++ b/base/utils/permissions_windows.go
@@ -32,4 +32,7 @@ func setWindowsFilePermissions(path string, perm FSPermission) {
 		acl.Apply(path, true, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Administrators"))
 		acl.Apply(path, false, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Users"))
 	}
+
+	// For completeness
+	acl.Apply(path, false, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "SYSTEM"))
 }
diff --git a/base/utils/structure.go b/base/utils/structure.go
index e33a920f..8e1f0786 100644
--- a/base/utils/structure.go
+++ b/base/utils/structure.go
@@ -16,7 +16,7 @@ const (
 	PublicWritePermission
 )
 
-// AsUnixDirPermission return the corresponding unix permission for a directory or executable.
+// AsUnixDirExecPermission return the corresponding unix permission for a directory or executable.
 func (perm FSPermission) AsUnixDirExecPermission() fs.FileMode {
 	switch perm {
 	case AdminOnlyPermission:
@@ -30,13 +30,13 @@ func (perm FSPermission) AsUnixDirExecPermission() fs.FileMode {
 	return 0
 }
 
-// AsUnixDirPermission return the corresponding unix permission for a regular file.
+// AsUnixFilePermission return the corresponding unix permission for a regular file.
 func (perm FSPermission) AsUnixFilePermission() fs.FileMode {
 	switch perm {
 	case AdminOnlyPermission:
 		return 0o600
 	case PublicReadPermission:
-		return 0o655
+		return 0o644
 	case PublicWritePermission:
 		return 0o666
 	}
diff --git a/base/utils/structure_test.go b/base/utils/structure_test.go
index 2acfebd2..d3debcf7 100644
--- a/base/utils/structure_test.go
+++ b/base/utils/structure_test.go
@@ -13,13 +13,13 @@ func ExampleDirStructure() {
 	// output:
 	// / [755]
 	// /repo [777]
-	// /repo/b [707]
-	// /repo/b/c [750]
-	// /repo/b/d [707]
-	// /repo/b/d/e [707]
-	// /repo/b/d/f [707]
-	// /repo/b/d/f/g [707]
-	// /repo/b/d/f/g/h [707]
+	// /repo/b [755]
+	// /repo/b/c [777]
+	// /repo/b/d [755]
+	// /repo/b/d/e [755]
+	// /repo/b/d/f [755]
+	// /repo/b/d/f/g [755]
+	// /repo/b/d/f/g/h [755]
 	// /secret [700]
 
 	basePath, err := os.MkdirTemp("", "")
@@ -28,12 +28,12 @@ func ExampleDirStructure() {
 		return
 	}
 
-	ds := NewDirStructure(basePath, 0o0755)
-	secret := ds.ChildDir("secret", 0o0700)
-	repo := ds.ChildDir("repo", 0o0777)
-	_ = repo.ChildDir("a", 0o0700)
-	b := repo.ChildDir("b", 0o0707)
-	c := b.ChildDir("c", 0o0750)
+	ds := NewDirStructure(basePath, PublicReadPermission)
+	secret := ds.ChildDir("secret", AdminOnlyPermission)
+	repo := ds.ChildDir("repo", PublicWritePermission)
+	_ = repo.ChildDir("a", AdminOnlyPermission)
+	b := repo.ChildDir("b", PublicReadPermission)
+	c := b.ChildDir("c", PublicWritePermission)
 
 	err = ds.Ensure()
 	if err != nil {
diff --git a/cmds/notifier/main.go b/cmds/notifier/main.go
index e40487bb..a11bea2a 100644
--- a/cmds/notifier/main.go
+++ b/cmds/notifier/main.go
@@ -225,14 +225,14 @@ func configureRegistry(mustLoadIndex bool) error {
 	// Remove left over quotes.
 	dataDir = strings.Trim(dataDir, `\"`)
 	// Initialize data root.
-	err := dataroot.Initialize(dataDir, 0o0755)
+	err := dataroot.Initialize(dataDir, utils.PublicReadPermission)
 	if err != nil {
 		return fmt.Errorf("failed to initialize data root: %w", err)
 	}
 	dataRoot = dataroot.Root()
 
 	// Initialize registry.
-	err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755))
+	err = registry.Initialize(dataRoot.ChildDir("updates", utils.PublicReadPermission))
 	if err != nil {
 		return err
 	}
diff --git a/cmds/updatemgr/main.go b/cmds/updatemgr/main.go
index acd9a0d4..4a999f49 100644
--- a/cmds/updatemgr/main.go
+++ b/cmds/updatemgr/main.go
@@ -31,7 +31,7 @@ var rootCmd = &cobra.Command{
 		}
 
 		registry = &updater.ResourceRegistry{}
-		err = registry.Initialize(utils.NewDirStructure(absDistPath, 0o0755))
+		err = registry.Initialize(utils.NewDirStructure(absDistPath, utils.PublicReadPermission))
 		if err != nil {
 			return err
 		}
diff --git a/cmds/updatemgr/release.go b/cmds/updatemgr/release.go
index 0f5d596e..e6642a63 100644
--- a/cmds/updatemgr/release.go
+++ b/cmds/updatemgr/release.go
@@ -11,6 +11,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/base/utils"
 )
 
 var (
@@ -63,7 +64,7 @@ func release(cmd *cobra.Command, args []string) error {
 			fmt.Println("aborted...")
 			return nil
 		}
-		symlinksDir := registry.StorageDir().ChildDir("latest", 0o755)
+		symlinksDir := registry.StorageDir().ChildDir("latest", utils.PublicReadPermission)
 		err = registry.CreateSymlinks(symlinksDir)
 		if err != nil {
 			return err
diff --git a/service/updates/upgrader.go b/service/updates/upgrader.go
index f1d3c297..b07d649e 100644
--- a/service/updates/upgrader.go
+++ b/service/updates/upgrader.go
@@ -351,7 +351,7 @@ func upgradeBinary(fileToUpgrade string, file *updater.File) error {
 
 	// check permissions
 	if onWindows {
-		utils.SetExecPermission(fileToUpgrade, utils.PublicReadPermission)
+		_ = utils.SetExecPermission(fileToUpgrade, utils.PublicReadPermission)
 	} else {
 		perm := utils.PublicReadPermission
 		info, err := os.Stat(fileToUpgrade)

From 9d874daed2ff360e016821fc4da0a09ae5eef536 Mon Sep 17 00:00:00 2001
From: Daniel <dhaavi@users.noreply.github.com>
Date: Fri, 6 Dec 2024 14:34:54 +0100
Subject: [PATCH 03/12] Simplify windows acl calls and switch to using SIDs

---
 base/utils/permissions_windows.go | 63 +++++++++++++++++++++++++------
 1 file changed, 51 insertions(+), 12 deletions(-)

diff --git a/base/utils/permissions_windows.go b/base/utils/permissions_windows.go
index 5f36ebb9..47aa0c26 100644
--- a/base/utils/permissions_windows.go
+++ b/base/utils/permissions_windows.go
@@ -7,32 +7,71 @@ import (
 	"golang.org/x/sys/windows"
 )
 
+var (
+	systemSID *windows.SID
+	adminsSID *windows.SID
+	usersSID  *windows.SID
+)
+
+func init() {
+	var err error
+	systemSID, err = windows.StringToSid("S-1-5") // NT Authority / SYSTEM
+	if err != nil {
+		panic(err)
+	}
+	adminsSID, err = windows.StringToSid("S-1-5-32-544") // Administrators
+	if err != nil {
+		panic(err)
+	}
+	usersSID, err = windows.StringToSid("S-1-5-32-545") // Users
+	if err != nil {
+		panic(err)
+	}
+}
+
+// SetDirPermission sets the permission of a directory.
 func SetDirPermission(path string, perm FSPermission) error {
-	setWindowsFilePermissions(path, perm)
+	SetFilePermission(path, perm)
 	return nil
 }
 
 // SetExecPermission sets the permission of an executable file.
 func SetExecPermission(path string, perm FSPermission) error {
-	return SetDirPermission(path, perm)
+	SetFilePermission(path, perm)
+	return nil
 }
 
-func setWindowsFilePermissions(path string, perm FSPermission) {
+// SetFilePermission sets the permission of a non executable file.
+func SetFilePermission(path string, perm FSPermission) {
 	switch perm {
 	case AdminOnlyPermission:
 		// Set only admin rights, remove all others.
-		acl.Apply(path, true, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Administrators"))
+		acl.Apply(
+			path,
+			true,
+			false,
+			acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, systemSID),
+			acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, adminsSID),
+		)
 	case PublicReadPermission:
 		// Set admin rights and read/execute rights for users, remove all others.
-		acl.Apply(path, true, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Administrators"))
-		acl.Apply(path, false, false, acl.GrantName(windows.GENERIC_EXECUTE, "Users"))
-		acl.Apply(path, false, false, acl.GrantName(windows.GENERIC_READ, "Users"))
+		acl.Apply(
+			path,
+			true,
+			false,
+			acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, systemSID),
+			acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, adminsSID),
+			acl.GrantSid(windows.GENERIC_READ|windows.GENERIC_EXECUTE, usersSID),
+		)
 	case PublicWritePermission:
 		// Set full control to admin and regular users. Guest users will not have access.
-		acl.Apply(path, true, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Administrators"))
-		acl.Apply(path, false, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "Users"))
+		acl.Apply(
+			path,
+			true,
+			false,
+			acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, systemSID),
+			acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, adminsSID),
+			acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, usersSID),
+		)
 	}
-
-	// For completeness
-	acl.Apply(path, false, false, acl.GrantName(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, "SYSTEM"))
 }

From 475d69f8a241f52e3c082a4a6f98a3fcabf89d19 Mon Sep 17 00:00:00 2001
From: Vladimir Stoilov <vladimir@safing.io>
Date: Fri, 6 Dec 2024 16:45:37 +0200
Subject: [PATCH 04/12] [service] Fix windows system SID

---
 base/utils/permissions_windows.go | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/base/utils/permissions_windows.go b/base/utils/permissions_windows.go
index 47aa0c26..13cbf583 100644
--- a/base/utils/permissions_windows.go
+++ b/base/utils/permissions_windows.go
@@ -14,8 +14,10 @@ var (
 )
 
 func init() {
+	// Initialize Security ID for all need groups.
+	// Reference: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers
 	var err error
-	systemSID, err = windows.StringToSid("S-1-5") // NT Authority / SYSTEM
+	systemSID, err = windows.StringToSid("S-1-5-18") // SYSTEM (Local System)
 	if err != nil {
 		panic(err)
 	}

From 692838b696c7375552eb0d836d54298d0d286f0b Mon Sep 17 00:00:00 2001
From: Alexandr Stelnykovych <stenya@gmail.com>
Date: Fri, 13 Dec 2024 17:02:38 +0200
Subject: [PATCH 05/12] [fix] Logical mistake while determining local resolvers

---
 service/resolver/resolvers.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/service/resolver/resolvers.go b/service/resolver/resolvers.go
index 45876b4e..6510036b 100644
--- a/service/resolver/resolvers.go
+++ b/service/resolver/resolvers.go
@@ -510,7 +510,7 @@ func setScopedResolvers(resolvers []*Resolver) {
 	for _, resolver := range resolvers {
 		if resolver.Info.IPScope.IsLAN() {
 			localResolvers = append(localResolvers, resolver)
-		} else if _, err := netenv.GetLocalNetwork(resolver.Info.IP); err != nil {
+		} else if net, _ := netenv.GetLocalNetwork(resolver.Info.IP); net != nil {
 			localResolvers = append(localResolvers, resolver)
 		}
 

From df70c70ab5b97081a34934881e5af50b49e3971c Mon Sep 17 00:00:00 2001
From: Alexandr Stelnykovych <stenya@gmail.com>
Date: Mon, 16 Dec 2024 16:01:42 +0200
Subject: [PATCH 06/12] [improvement] Small cache size (2 ^ 8) = (2 XOR 8) =
 10. Was it intended to be 256?

---
 service/intel/filterlists/database.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/service/intel/filterlists/database.go b/service/intel/filterlists/database.go
index 5f55323c..6de070bb 100644
--- a/service/intel/filterlists/database.go
+++ b/service/intel/filterlists/database.go
@@ -50,7 +50,7 @@ var (
 var cache = database.NewInterface(&database.Options{
 	Local:     true,
 	Internal:  true,
-	CacheSize: 2 ^ 8,
+	CacheSize: 256,
 })
 
 // getFileFunc is the function used to get a file from

From 6c014d227c0cbaea5fe1333b31ba5e9d57229f5e Mon Sep 17 00:00:00 2001
From: Alexandr Stelnykovych <stenya@gmail.com>
Date: Tue, 17 Dec 2024 15:49:52 +0200
Subject: [PATCH 07/12] [fix] Panic while accessing SleepyTicker methods
 Stop()/SetSleep()

The time.Ticker object was stored as a value type, but it is expected to be a pointer according to its implementation:
```
func (t *Ticker) Stop()
func (t *Ticker) Reset(d Duration)
```

This was leading to an application crash.

STR 1:
Run `portmaster-core` without privileged rights. It will not be able to start the kernel driver (Windows).
During unloading of already initialized modules, the process crashes because of stopping SleepyTicker instances in workers of the "network" module.

STR 2:
Run tests from `service\mgr\sleepyticker_test.go`
---
 service/mgr/sleepyticker.go      |  4 +--
 service/mgr/sleepyticker_test.go | 57 ++++++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+), 2 deletions(-)
 create mode 100644 service/mgr/sleepyticker_test.go

diff --git a/service/mgr/sleepyticker.go b/service/mgr/sleepyticker.go
index 075912a1..ce0b20b4 100644
--- a/service/mgr/sleepyticker.go
+++ b/service/mgr/sleepyticker.go
@@ -4,7 +4,7 @@ import "time"
 
 // SleepyTicker is wrapper over time.Ticker that respects the sleep mode of the module.
 type SleepyTicker struct {
-	ticker         time.Ticker
+	ticker         *time.Ticker
 	normalDuration time.Duration
 	sleepDuration  time.Duration
 	sleepMode      bool
@@ -16,7 +16,7 @@ type SleepyTicker struct {
 // If sleepDuration is set to 0 ticker will not tick during sleep.
 func NewSleepyTicker(normalDuration time.Duration, sleepDuration time.Duration) *SleepyTicker {
 	st := &SleepyTicker{
-		ticker:         *time.NewTicker(normalDuration),
+		ticker:         time.NewTicker(normalDuration),
 		normalDuration: normalDuration,
 		sleepDuration:  sleepDuration,
 		sleepMode:      false,
diff --git a/service/mgr/sleepyticker_test.go b/service/mgr/sleepyticker_test.go
new file mode 100644
index 00000000..9e2175c7
--- /dev/null
+++ b/service/mgr/sleepyticker_test.go
@@ -0,0 +1,57 @@
+package mgr
+
+import (
+	"testing"
+	"time"
+)
+
+func TestSleepyTickerStop(t *testing.T) {
+	normalDuration := 100 * time.Millisecond
+	sleepDuration := 200 * time.Millisecond
+
+	st := NewSleepyTicker(normalDuration, sleepDuration)
+	st.Stop() // no panic expected here
+}
+
+func TestSleepyTicker(t *testing.T) {
+	normalDuration := 100 * time.Millisecond
+	sleepDuration := 200 * time.Millisecond
+
+	st := NewSleepyTicker(normalDuration, sleepDuration)
+
+	// Test normal mode
+	select {
+	case <-st.Wait():
+		// Expected tick
+	case <-time.After(normalDuration + 50*time.Millisecond):
+		t.Error("expected tick in normal mode")
+	}
+
+	// Test sleep mode
+	st.SetSleep(true)
+	select {
+	case <-st.Wait():
+		// Expected tick
+	case <-time.After(sleepDuration + 50*time.Millisecond):
+		t.Error("expected tick in sleep mode")
+	}
+
+	// Test sleep mode with sleepDuration == 0
+	st = NewSleepyTicker(normalDuration, 0)
+	st.SetSleep(true)
+	select {
+	case <-st.Wait():
+		t.Error("did not expect tick when sleepDuration is 0")
+	case <-time.After(normalDuration):
+		// Expected no tick
+	}
+
+	// Test stopping the ticker
+	st.Stop()
+	select {
+	case <-st.Wait():
+		t.Error("did not expect tick after stopping the ticker")
+	case <-time.After(normalDuration):
+		// Expected no tick
+	}
+}

From c7f3475382e36e7daa353b5c9a4cde2f209c8497 Mon Sep 17 00:00:00 2001
From: Daniel <dhaavi@users.noreply.github.com>
Date: Fri, 20 Dec 2024 13:31:52 +0100
Subject: [PATCH 08/12] Add spn testing setup

---
 .gitignore                              |   3 +-
 spn/testing/README.md                   |  25 +++++
 spn/testing/simple/README.md            |  48 ++++++++
 spn/testing/simple/clientsim.sh         |  41 +++++++
 spn/testing/simple/config-template.json |  19 ++++
 spn/testing/simple/docker-compose.yml   | 139 ++++++++++++++++++++++++
 spn/testing/simple/entrypoint.sh        |  17 +++
 spn/testing/simple/inject-intel.sh      |  35 ++++++
 spn/testing/simple/intel-client.yaml    |  25 +++++
 spn/testing/simple/intel-testnet.json   |  17 +++
 spn/testing/simple/join.sh              |  42 +++++++
 spn/testing/simple/reset-databases.sh   |   7 ++
 spn/testing/simple/run.sh               |  52 +++++++++
 spn/testing/simple/stop.sh              |  15 +++
 14 files changed, 483 insertions(+), 2 deletions(-)
 create mode 100644 spn/testing/README.md
 create mode 100644 spn/testing/simple/README.md
 create mode 100755 spn/testing/simple/clientsim.sh
 create mode 100644 spn/testing/simple/config-template.json
 create mode 100644 spn/testing/simple/docker-compose.yml
 create mode 100755 spn/testing/simple/entrypoint.sh
 create mode 100755 spn/testing/simple/inject-intel.sh
 create mode 100644 spn/testing/simple/intel-client.yaml
 create mode 100644 spn/testing/simple/intel-testnet.json
 create mode 100755 spn/testing/simple/join.sh
 create mode 100755 spn/testing/simple/reset-databases.sh
 create mode 100755 spn/testing/simple/run.sh
 create mode 100755 spn/testing/simple/stop.sh

diff --git a/.gitignore b/.gitignore
index 03d8b25f..0b8e5e9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,8 +12,7 @@ go.mod.*
 vendor
 
 # testing
-testing
-spn/testing/simple/testdata
+testdata
 
 # Compiled Object files, Static and Dynamic libs (Shared Objects)
 *.a
diff --git a/spn/testing/README.md b/spn/testing/README.md
new file mode 100644
index 00000000..72c7ff74
--- /dev/null
+++ b/spn/testing/README.md
@@ -0,0 +1,25 @@
+# Testing Port17
+
+## Simple Docker Setup
+
+Run `run.sh` to start the docker compose test network.
+Then, connect to the test network, by starting the core with the "test" spn map and the correct bootstrap file.
+
+Run `stop.sh` to remove all docker resources again.
+
+Setup Guide can be found in the directory.
+
+## Advanced Setup with Shadow
+
+For advanced testing we use [shadow](https://github.com/shadow/shadow).
+The following section will help you set up shadow and will guide you how to test Port17 in a local Shadow environment.
+
+### Setting up
+
+Download the docker version from here: [https://security.cs.georgetown.edu/shadow-docker-images/shadow-standalone.tar.gz](https://security.cs.georgetown.edu/shadow-docker-images/shadow-standalone.tar.gz)
+
+Then import the image into docker with `gunzip -c shadow-standalone.tar.gz | sudo docker load`.
+
+### Running
+
+Execute `sudo docker run -t -i -u shadow shadow-standalone /bin/bash` to start an interactive container with shadow.
diff --git a/spn/testing/simple/README.md b/spn/testing/simple/README.md
new file mode 100644
index 00000000..678e4e96
--- /dev/null
+++ b/spn/testing/simple/README.md
@@ -0,0 +1,48 @@
+# Setup Guide
+
+1. Build SPN Hub
+
+```
+cd ../../../cmds/hub/
+./build
+```
+
+2. Reset any previous state (for a fresh test)
+
+```
+./reset-databases.sh
+```
+
+3. Change compose file and config template as required
+
+Files:
+- `docker-compose.yml`
+- `config-template.json`
+
+4. Start test network
+
+```
+./run.sh
+```
+
+5. Option 1: Join as Hub
+
+For testing just one Hub with a different build or config, you can simply use `./join.sh` to join the network with the most recently build hub binary.
+
+6. Option 2: Join as Portmaster
+
+For connecting to the SPN test network with Portmaster, execute portmaster like this:
+
+sudo ../../../cmds/portmaster-core/portmaster-core --disable-shutdown-event --devmode --log debug --data /opt/safing/portmaster
+
+Note:
+This uses the same portmaster data and config as your installed version.
+As the SPN Test net operates under a different ID ("test" instead of "main"), this will not pollute the SPN state of your installed Portmaster.
+
+7. Stop the test net
+
+This is important, as just stopping the `./run.sh` script will leave you with interfaces with public IPs!
+
+```
+./stop.sh
+```
diff --git a/spn/testing/simple/clientsim.sh b/spn/testing/simple/clientsim.sh
new file mode 100755
index 00000000..a25cf3c4
--- /dev/null
+++ b/spn/testing/simple/clientsim.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+
+realpath() {
+    path=`eval echo "$1"`
+    folder=$(dirname "$path")
+    echo $(cd "$folder"; pwd)/$(basename "$path"); 
+}
+
+if [[ ! -f "../../client" ]]; then
+  echo "please compile client.go in main directory (output: client)"
+  exit 1
+fi
+
+bin_path="$(realpath ../../client)"
+data_path="$(realpath ./testdata)"
+if [[ ! -d "$data_path" ]]; then
+  mkdir "$data_path"
+fi
+shared_path="$(realpath ./testdata/shared)"
+if [[ ! -d "$shared_path" ]]; then
+  mkdir "$shared_path"
+fi
+
+docker network ls | grep spn-simpletest-network >/dev/null 2>&1
+if [[ $? -ne 0 ]]; then
+  docker network create spn-simpletest-network --subnet 6.0.0.0/24
+fi
+
+docker run -ti --rm \
+--name spn-simpletest-clientsim \
+--network spn-simpletest-network \
+-v $bin_path:/opt/client:ro \
+-v $data_path/clientsim:/opt/data \
+-v $shared_path:/opt/shared \
+--entrypoint /opt/client \
+toolset.safing.network/dev \
+--data /opt/data \
+--bootstrap-file /opt/shared/bootstrap.dsd \
+--log trace $*
\ No newline at end of file
diff --git a/spn/testing/simple/config-template.json b/spn/testing/simple/config-template.json
new file mode 100644
index 00000000..c9baca7e
--- /dev/null
+++ b/spn/testing/simple/config-template.json
@@ -0,0 +1,19 @@
+{
+  "core": {
+    "log": {
+      "level": "trace"
+    },
+    "metrics": {
+      "instance": "test_$HUBNAME",
+      "push": ""
+    }
+  },
+  "spn": {
+    "publicHub": {
+      "name": "test-$HUBNAME",
+      "transports": ["http:80", "http:8080", "tcp:17"],
+      "allowUnencrypted": true,
+      "bindToAdvertised": true
+    }
+  }
+}
diff --git a/spn/testing/simple/docker-compose.yml b/spn/testing/simple/docker-compose.yml
new file mode 100644
index 00000000..3d48eb10
--- /dev/null
+++ b/spn/testing/simple/docker-compose.yml
@@ -0,0 +1,139 @@
+version: "2.4"
+
+networks:
+  default:
+    ipam:
+      driver: default
+      config:
+        - subnet: 6.0.0.0/24
+
+services:
+  hub1:
+    container_name: spn-test-simple-hub1
+    hostname: hub1
+    image: toolset.safing.network/dev
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_BIN}:/opt/hub1:ro
+      - ${SPN_TEST_DATA_DIR}/hub1:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.11
+
+  hub2:
+    container_name: spn-test-simple-hub2
+    hostname: hub2
+    image: alpine
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_BIN}:/opt/hub2:ro
+      - ${SPN_TEST_DATA_DIR}/hub2:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.12
+
+  hub3:
+    container_name: spn-test-simple-hub3
+    hostname: hub3
+    image: toolset.safing.network/dev
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_BIN}:/opt/hub3:ro
+      - ${SPN_TEST_DATA_DIR}/hub3:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.13
+
+  hub4:
+    container_name: spn-test-simple-hub4
+    hostname: hub4
+    image: toolset.safing.network/dev
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_BIN}:/opt/hub4:ro
+      - ${SPN_TEST_DATA_DIR}/hub4:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.14
+
+  hub5:
+    container_name: spn-test-simple-hub5
+    hostname: hub5
+    image: toolset.safing.network/dev
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_BIN}:/opt/hub5:ro
+      - ${SPN_TEST_DATA_DIR}/hub5:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.15
+
+  hub6:
+    container_name: spn-test-simple-hub6
+    hostname: hub6
+    image: toolset.safing.network/dev
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_OLD_BIN}:/opt/hub6:ro
+      - ${SPN_TEST_DATA_DIR}/hub6:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.16
+
+  hub7:
+    container_name: spn-test-simple-hub7
+    hostname: hub7
+    image: toolset.safing.network/dev
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_OLD_BIN}:/opt/hub7:ro
+      - ${SPN_TEST_DATA_DIR}/hub7:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.17
+
+  hub8:
+    container_name: spn-test-simple-hub8
+    hostname: hub8
+    image: toolset.safing.network/dev
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_OLD_BIN}:/opt/hub8:ro
+      - ${SPN_TEST_DATA_DIR}/hub8:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.18
+
+  hub9:
+    container_name: spn-test-simple-hub9
+    hostname: hub9
+    image: toolset.safing.network/dev
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_OLD_BIN}:/opt/hub9:ro
+      - ${SPN_TEST_DATA_DIR}/hub9:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.19
+
+  hub10:
+    container_name: spn-test-simple-hub10
+    hostname: hub10
+    image: toolset.safing.network/dev
+    entrypoint: "/opt/shared/entrypoint.sh"
+    volumes:
+      - ${SPN_TEST_OLD_BIN}:/opt/hub10:ro
+      - ${SPN_TEST_DATA_DIR}/hub10:/opt/data
+      - ${SPN_TEST_SHARED_DATA_DIR}:/opt/shared
+    networks:
+      default:
+        ipv4_address: 6.0.0.20
diff --git a/spn/testing/simple/entrypoint.sh b/spn/testing/simple/entrypoint.sh
new file mode 100755
index 00000000..5fe516e0
--- /dev/null
+++ b/spn/testing/simple/entrypoint.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Get hostname.
+HUBNAME=$HOSTNAME
+if [ "$HUBNAME" = "" ]; then
+  HUBNAME=$(cat /etc/hostname)
+fi
+export HUBNAME
+
+# Read, process and write config.
+cat /opt/shared/config-template.json | sed "s/\$HUBNAME/$HUBNAME/g" > /opt/data/config.json
+
+# Get binary to start.
+BIN=$(ls /opt/ | grep hub)
+
+# Start Hub.
+/opt/$BIN --data /opt/data --log trace --spn-map test --bootstrap-file /opt/shared/bootstrap.dsd --api-address 0.0.0.0:817 --devmode
diff --git a/spn/testing/simple/inject-intel.sh b/spn/testing/simple/inject-intel.sh
new file mode 100755
index 00000000..a57cd72b
--- /dev/null
+++ b/spn/testing/simple/inject-intel.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+
+MAIN_INTEL_FILE="intel-testnet.json"
+
+if [[ ! -f $MAIN_INTEL_FILE ]]; then
+  echo "missing $MAIN_INTEL_FILE"
+  exit 1
+fi
+
+echo "if the containing directory cannot be created, you might need to adjust permissions, as nodes are run with root in test containers..."
+echo "$ sudo chmod -R 777 data/hub*/updates"
+echo "starting to update..."
+
+for hubDir in data/hub*; do
+  # Build destination path
+  hubIntelFile="${hubDir}/updates/all/intel/spn/main-intel_v0-0-0.dsd"
+
+  # Copy file
+  mkdir -p "${hubDir}/updates/all/intel/spn"
+  echo -n "J" > "$hubIntelFile"
+  cat $MAIN_INTEL_FILE >> "$hubIntelFile"
+
+  echo "updated $hubIntelFile"
+done
+
+if [[ -d /var/lib/portmaster ]]; then
+  echo "updating intel for local portmaster installation..."
+
+  portmasterSPNIntelFile="/var/lib/portmaster/updates/all/intel/spn/main-intel_v0-0-0.dsd"
+  echo -n "J" > "$portmasterSPNIntelFile"
+  cat $MAIN_INTEL_FILE >> "$portmasterSPNIntelFile"
+  echo "updated $portmasterSPNIntelFile"
+fi
diff --git a/spn/testing/simple/intel-client.yaml b/spn/testing/simple/intel-client.yaml
new file mode 100644
index 00000000..28f0a685
--- /dev/null
+++ b/spn/testing/simple/intel-client.yaml
@@ -0,0 +1,25 @@
+# Get current list of IDs from test net:
+#   curl http://127.0.0.1:817/api/v1/spn/map/test/pins | jq ".[] | .ID"
+# Then import into test client with:
+#   curl -X POST --upload-file intel-client.yaml http://127.0.0.1:817/api/v1/spn/map/test/intel/update
+Hubs:
+  Zwm48YWWFGdwXjhE1MyEkWfqxPr9DiUBoXpusTZ1FMQnuK:
+    Trusted: true
+  Zwu5LkkbfCbAcYxWG3vtWF1VvWjgWpc1GJfkwRdLFNtytV:
+    Trusted: true
+  ZwuQpz5CqYmYoLnt9KXQ8oxnmosBzfrCYwCGhxT4fsG1Dz:
+    Trusted: true
+  ZwwmC3dHzr7J6XW9mc2KD6FDNuXwPVJUFi9dLnDSNMyjLk:
+    Trusted: true
+  ZwxSBdvqtJyz8zRWKZe6QyK51KH9av6VFay2GQvpFrWKHq:
+    Trusted: true
+  ZwxnuL6zMLj4AxJX8Bj369w2tNrVtYxzffVcXZuMxdxbGj:
+    Trusted: true
+  ZwyXdnC8JkC7m796skGD7QWGoYycByR3KVntkXMY8CxRZQ:
+    Trusted: true
+  Zwz7AHiH1EevD9eYFqvQQPbVWyBBcksTRxxafbRx5Cvc4F:
+    Trusted: true
+  ZwzMtc65t9XBMwmLm2xNSL69FvqHGPLiqeNBZ3eEN5a9sS:
+    Trusted: true
+  ZwzjnCUNGsuWnkYmN3QEj8JPLxU6V1QQFk9b47AigmPKiH:
+    Trusted: true
diff --git a/spn/testing/simple/intel-testnet.json b/spn/testing/simple/intel-testnet.json
new file mode 100644
index 00000000..388fa0e1
--- /dev/null
+++ b/spn/testing/simple/intel-testnet.json
@@ -0,0 +1,17 @@
+{
+	"BootstrapHubs": [
+	],
+	"TrustedHubs": [
+		"ZwrY9G9HDo1J3qQrrQs8VF2KD99bj7KyWesJ5kWFUDBU6r",
+		"Zwj56ZFXrsud8gc1Rw3zuxRwMLhGkwvtvnTxCVtJ8EWLhQ",
+		"ZwpdW87ityD9i3N9x8oweCJnbZEqo346VBg4mCsCvTr1Zo",
+		"ZwpJ6ebddk1sccUVpo92JUqicBfKzBN2w4pEGoEY7UsNhX",
+		"Zwte3Jffp9PWmeWfrn8RyGuvZZFCg3v7XR3tpQjdo9TpVt",
+		"ZwrTcdiPF5zR5h9q9EdjHCrrXzYVBdQe5HmEYUWXdLkke3",
+		"Zwv7tSn5iU6bYKn53NaGCxPtL8vSxSK7F9VdQezDaDCLBt",
+		"Zwvtdq3K9knP9iNaRS1Ju8CETWTqy7oRwbScjBtJGBpqhB"
+	],
+	"AdviseOnlyTrustedHubs": true,
+	"AdviseOnlyTrustedHomeHubs": true,
+	"AdviseOnlyTrustedDestinationHubs": true
+}
diff --git a/spn/testing/simple/join.sh b/spn/testing/simple/join.sh
new file mode 100755
index 00000000..b5ddf912
--- /dev/null
+++ b/spn/testing/simple/join.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+
+realpath() {
+    path=`eval echo "$1"`
+    folder=$(dirname "$path")
+    echo $(cd "$folder"; pwd)/$(basename "$path"); 
+}
+
+leftover=$(docker ps -a | grep spn-test-simple-me | cut -d" " -f1)
+if [[ $leftover != "" ]]; then
+  docker stop $leftover
+  docker rm $leftover
+fi
+
+if [[ ! -f "../../../cmds/hub/hub" ]]; then
+  echo "please build the hub cmd using cmds/hub/build"
+  exit 1
+fi
+
+SPN_TEST_BIN="$(realpath ../../../cmds/hub/hub)"
+SPN_TEST_DATA_DIR="$(realpath ./testdata)"
+if [[ ! -d "$SPN_TEST_DATA_DIR" ]]; then
+  mkdir "$SPN_TEST_DATA_DIR"
+fi
+SPN_TEST_SHARED_DATA_DIR="$(realpath ./testdata/shared)"
+if [[ ! -d "$SPN_TEST_SHARED_DATA_DIR" ]]; then
+  mkdir "$SPN_TEST_SHARED_DATA_DIR"
+fi
+
+docker run -ti \
+--name spn-test-simple-me \
+--hostname me \
+--network spn-test-simple_default \
+-v $SPN_TEST_BIN:/opt/hub_me:ro \
+-v $SPN_TEST_DATA_DIR/me:/opt/data \
+-v $SPN_TEST_SHARED_DATA_DIR:/opt/shared \
+--entrypoint /opt/hub_me \
+toolset.safing.network/dev \
+--devmode --api-address 0.0.0.0:8081 \
+--data /opt/data -log trace --spn-map test --bootstrap-file /opt/shared/bootstrap.dsd
diff --git a/spn/testing/simple/reset-databases.sh b/spn/testing/simple/reset-databases.sh
new file mode 100755
index 00000000..79be6890
--- /dev/null
+++ b/spn/testing/simple/reset-databases.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+
+rm -rf data/me/*
+rm -rf data/shared/*
+rm -rf data/hub*/databases
diff --git a/spn/testing/simple/run.sh b/spn/testing/simple/run.sh
new file mode 100755
index 00000000..728cad3f
--- /dev/null
+++ b/spn/testing/simple/run.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+
+realpath() {
+    path=`eval echo "$1"`
+    folder=$(dirname "$path")
+    echo $(cd "$folder"; pwd)/$(basename "$path"); 
+}
+
+leftovers=$(docker ps -a | grep spn-test-simple | cut -d" " -f1)
+if [[ $leftovers != "" ]]; then
+  docker stop $leftovers
+  docker rm $leftovers
+fi
+
+if [[ ! -f "../../../cmds/hub/hub" ]]; then
+  echo "please build the hub cmd using cmds/hub/build"
+  exit 1
+fi
+
+# Create variables.
+SPN_TEST_BIN="$(realpath ../../../cmds/hub/hub)"
+SPN_TEST_DATA_DIR="$(realpath ./testdata)"
+if [[ ! -d "$SPN_TEST_DATA_DIR" ]]; then
+  mkdir "$SPN_TEST_DATA_DIR"
+fi
+SPN_TEST_SHARED_DATA_DIR="$(realpath ./testdata/shared)"
+if [[ ! -d "$SPN_TEST_SHARED_DATA_DIR" ]]; then
+  mkdir "$SPN_TEST_SHARED_DATA_DIR"
+fi
+
+# Check if there is an old binary for testing.
+SPN_TEST_OLD_BIN=$SPN_TEST_BIN
+if [[ -f "./testdata/old-hub" ]]; then
+  SPN_TEST_OLD_BIN="$(realpath ./testdata/old-hub)"
+  echo "WARNING: running in hybrid mode with old version at $SPN_TEST_OLD_BIN"
+fi
+
+# Export variables
+export SPN_TEST_BIN
+export SPN_TEST_OLD_BIN
+export SPN_TEST_DATA_DIR
+export SPN_TEST_SHARED_DATA_DIR
+
+# Copy files.
+cp config-template.json ./testdata/shared/config-template.json
+cp entrypoint.sh ./testdata/shared/entrypoint.sh
+chmod 555 ./testdata/shared/entrypoint.sh
+
+# Run!
+docker compose -p spn-test-simple up --remove-orphans
diff --git a/spn/testing/simple/stop.sh b/spn/testing/simple/stop.sh
new file mode 100755
index 00000000..f5af89a4
--- /dev/null
+++ b/spn/testing/simple/stop.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+
+docker compose -p spn-test-simple stop
+docker compose -p spn-test-simple rm
+
+oldnet=$(docker network ls | grep spn-test-simple | cut -d" " -f1)
+if [[ $oldnet != "" ]]; then
+  docker network rm $oldnet
+fi
+
+if [[ -d "data/shared" ]]; then
+  rm -r "data/shared"
+fi

From 1e9e6263d4caf88d6e700bc02d93eb001272da0b Mon Sep 17 00:00:00 2001
From: Daniel <dhaavi@users.noreply.github.com>
Date: Fri, 20 Dec 2024 13:36:15 +0100
Subject: [PATCH 09/12] Fix SPN testnet portmaster args

---
 spn/testing/simple/README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spn/testing/simple/README.md b/spn/testing/simple/README.md
index 678e4e96..f5df45b5 100644
--- a/spn/testing/simple/README.md
+++ b/spn/testing/simple/README.md
@@ -33,7 +33,7 @@ For testing just one Hub with a different build or config, you can simply use `.
 
 For connecting to the SPN test network with Portmaster, execute portmaster like this:
 
-sudo ../../../cmds/portmaster-core/portmaster-core --disable-shutdown-event --devmode --log debug --data /opt/safing/portmaster
+sudo ../../../cmds/portmaster-core/portmaster-core --disable-shutdown-event --devmode --log debug --data /opt/safing/portmaster --spn-map test --bootstrap-file ./testdata/shared/bootstrap.dsd
 
 Note:
 This uses the same portmaster data and config as your installed version.

From ef7b129cedaef935af010ed087da1ecfbf5c6d08 Mon Sep 17 00:00:00 2001
From: Daniel <dhaavi@users.noreply.github.com>
Date: Fri, 20 Dec 2024 13:37:01 +0100
Subject: [PATCH 10/12] Use code quotes for cmds in docs

---
 spn/testing/simple/README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/spn/testing/simple/README.md b/spn/testing/simple/README.md
index f5df45b5..bec39dfb 100644
--- a/spn/testing/simple/README.md
+++ b/spn/testing/simple/README.md
@@ -33,7 +33,9 @@ For testing just one Hub with a different build or config, you can simply use `.
 
 For connecting to the SPN test network with Portmaster, execute portmaster like this:
 
+```
 sudo ../../../cmds/portmaster-core/portmaster-core --disable-shutdown-event --devmode --log debug --data /opt/safing/portmaster --spn-map test --bootstrap-file ./testdata/shared/bootstrap.dsd
+```
 
 Note:
 This uses the same portmaster data and config as your installed version.

From 96209c28cfedfb5023c81eebddc3eb546be2d13b Mon Sep 17 00:00:00 2001
From: Daniel <dhaavi@users.noreply.github.com>
Date: Mon, 13 Jan 2025 10:09:11 +0100
Subject: [PATCH 11/12] Fix SPN build

---
 Earthfile                             |  5 +++++
 base/info/version.go                  |  1 +
 cmds/hub/main.go                      | 10 +++++-----
 spn/testing/simple/reset-databases.sh |  6 +++---
 4 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/Earthfile b/Earthfile
index de711d17..bc0bc6c0 100644
--- a/Earthfile
+++ b/Earthfile
@@ -70,6 +70,11 @@ build:
     # ./dist/all/assets.zip
     BUILD +assets
 
+build-spn:
+    BUILD +go-build --CMDS="hub" --GOOS="linux"   --GOARCH="amd64"
+    BUILD +go-build --CMDS="hub" --GOOS="linux"   --GOARCH="arm64"
+    # TODO: Add other platforms
+
 go-ci:
     BUILD +go-build --GOOS="linux"   --GOARCH="amd64"
     BUILD +go-build --GOOS="linux"   --GOARCH="arm64"
diff --git a/base/info/version.go b/base/info/version.go
index 91bad092..6c27096e 100644
--- a/base/info/version.go
+++ b/base/info/version.go
@@ -74,6 +74,7 @@ func Set(setName string, setVersion string, setLicenseName string) {
 
 	if setVersion != "" {
 		version = setVersion
+		versionNumber = setVersion
 	}
 }
 
diff --git a/cmds/hub/main.go b/cmds/hub/main.go
index 3db002b3..74390f27 100644
--- a/cmds/hub/main.go
+++ b/cmds/hub/main.go
@@ -33,7 +33,7 @@ func main() {
 	flag.Parse()
 
 	// Set name and license.
-	info.Set("SPN Hub", "", "GPLv3")
+	info.Set("SPN Hub", "0.7.8", "GPLv3")
 
 	// Configure metrics.
 	_ = metrics.SetNamespace("hub")
@@ -45,10 +45,6 @@ func main() {
 	// Set SPN public hub mode.
 	conf.EnablePublicHub(true)
 
-	// Set default log level.
-	log.SetLogLevel(log.WarningLevel)
-	_ = log.Start()
-
 	// Create instance.
 	var execCmdLine bool
 	instance, err := spn.New()
@@ -79,6 +75,10 @@ func main() {
 		os.Exit(0)
 	}
 
+	// Set default log level.
+	log.SetLogLevel(log.WarningLevel)
+	_ = log.Start()
+
 	// Start
 	go func() {
 		err = instance.Start()
diff --git a/spn/testing/simple/reset-databases.sh b/spn/testing/simple/reset-databases.sh
index 79be6890..3c8a2d19 100755
--- a/spn/testing/simple/reset-databases.sh
+++ b/spn/testing/simple/reset-databases.sh
@@ -2,6 +2,6 @@
 
 cd "$( dirname "${BASH_SOURCE[0]}" )"
 
-rm -rf data/me/*
-rm -rf data/shared/*
-rm -rf data/hub*/databases
+rm -rf testdata/me/*
+rm -rf testdata/shared/*
+rm -rf testdata/hub*/databases

From 3478622eb861eef4b2770f5c264d84d5964e98d5 Mon Sep 17 00:00:00 2001
From: Alexandr Stelnykovych <alexandr.stelnykovych@ivpn.net>
Date: Mon, 13 Jan 2025 14:15:48 +0000
Subject: [PATCH 12/12] update deps

---
 go.mod |  43 ++++++++++----------
 go.sum | 123 ++++++++++++++++++++++++++-------------------------------
 2 files changed, 77 insertions(+), 89 deletions(-)

diff --git a/go.mod b/go.mod
index 27dc3f2b..49103f19 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@ require (
 	github.com/awalterschulze/gographviz v2.0.3+incompatible
 	github.com/bluele/gcache v0.0.2
 	github.com/brianvoe/gofakeit v3.18.0+incompatible
-	github.com/cilium/ebpf v0.16.0
+	github.com/cilium/ebpf v0.17.1
 	github.com/coreos/go-iptables v0.8.0
 	github.com/davecgh/go-spew v1.1.1
 	github.com/dgraph-io/badger v1.6.2
@@ -34,10 +34,10 @@ require (
 	github.com/hashicorp/go-version v1.7.0
 	github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb
 	github.com/jackc/puddle/v2 v2.2.2
-	github.com/lmittmann/tint v1.0.5
-	github.com/maruel/panicparse/v2 v2.3.1
+	github.com/lmittmann/tint v1.0.6
+	github.com/maruel/panicparse/v2 v2.4.0
 	github.com/mat/besticon v3.12.0+incompatible
-	github.com/mattn/go-colorable v0.1.13
+	github.com/mattn/go-colorable v0.1.14
 	github.com/mattn/go-isatty v0.0.20
 	github.com/miekg/dns v1.1.62
 	github.com/mitchellh/copystructure v1.2.0
@@ -62,22 +62,22 @@ require (
 	github.com/varlink/go v0.4.0
 	github.com/vincent-petithory/dataurl v1.0.0
 	go.etcd.io/bbolt v1.3.11
-	golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
-	golang.org/x/image v0.22.0
-	golang.org/x/net v0.31.0
-	golang.org/x/sync v0.9.0
-	golang.org/x/sys v0.27.0
+	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
+	golang.org/x/image v0.23.0
+	golang.org/x/net v0.34.0
+	golang.org/x/sync v0.10.0
+	golang.org/x/sys v0.29.0
 	gopkg.in/yaml.v3 v3.0.1
 	zombiezen.com/go/sqlite v1.4.0
 )
 
 require (
+	al.essio.dev/pkg/shellescape v1.5.1 // indirect
 	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
 	github.com/aead/ecdh v0.2.0 // indirect
-	github.com/alessio/shellescape v1.4.2 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/danieljoos/wincred v1.2.1 // indirect
-	github.com/dgraph-io/ristretto v0.1.1 // indirect
+	github.com/danieljoos/wincred v1.2.2 // indirect
+	github.com/dgraph-io/ristretto v0.2.0 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
@@ -85,7 +85,6 @@ require (
 	github.com/godbus/dbus v4.1.0+incompatible // indirect
 	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
 	github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect
-	github.com/golang/glog v1.2.1 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
@@ -114,16 +113,16 @@ require (
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
-	github.com/zalando/go-keyring v0.2.5 // indirect
+	github.com/zalando/go-keyring v0.2.6 // indirect
 	github.com/zeebo/blake3 v0.2.4 // indirect
-	golang.org/x/crypto v0.29.0 // indirect
+	golang.org/x/crypto v0.32.0 // indirect
 	golang.org/x/mod v0.22.0 // indirect
-	golang.org/x/text v0.20.0 // indirect
-	golang.org/x/tools v0.27.0 // indirect
-	google.golang.org/protobuf v1.34.2 // indirect
+	golang.org/x/text v0.21.0 // indirect
+	golang.org/x/tools v0.29.0 // indirect
+	google.golang.org/protobuf v1.36.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	modernc.org/libc v1.61.2 // indirect
-	modernc.org/mathutil v1.6.0 // indirect
-	modernc.org/memory v1.8.0 // indirect
-	modernc.org/sqlite v1.34.1 // indirect
+	modernc.org/libc v1.61.7 // indirect
+	modernc.org/mathutil v1.7.1 // indirect
+	modernc.org/memory v1.8.1 // indirect
+	modernc.org/sqlite v1.34.4 // indirect
 )
diff --git a/go.sum b/go.sum
index e75afa2b..1a291410 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
+al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
 cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
 fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
@@ -16,8 +18,6 @@ github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpY
 github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w=
 github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
 github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
-github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
-github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
 github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -29,13 +29,12 @@ github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6
 github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8=
 github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
 github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
-github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
-github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
+github.com/cilium/ebpf v0.17.1 h1:G8mzU81R2JA1nE5/8SRubzqvBMmAmri2VL8BIZPWvV0=
+github.com/cilium/ebpf v0.17.1/go.mod h1:vay2FaYSmIlv3r8dNACd4mW/OCaZLJKJOo+IHBvCIO8=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
@@ -43,18 +42,19 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
-github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
+github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
+github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
 github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
 github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
-github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
-github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
-github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
+github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
+github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435 h1:AnwbdEI8eV3GzLM3SlrJlYmYa6OB5X8RwY4A8QJOCP0=
 github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435/go.mod h1:EMJ8XWTopp8OLRBMUm9vHE8Wn48CNpU21HM817OKNrc=
 github.com/dhaavi/winres v0.2.2 h1:SUago7FwhgLSMyDdeuV6enBZ+ZQSl0KwcnbWzvlfBls=
@@ -96,9 +96,6 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
 github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
 github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
 github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
-github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -171,21 +168,18 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw=
-github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
+github.com/lmittmann/tint v1.0.6 h1:vkkuDAZXc0EFGNzYjWcV0h7eEX+uujH48f/ifSkJWgc=
+github.com/lmittmann/tint v1.0.6/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
 github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/maruel/panicparse/v2 v2.3.1 h1:NtJavmbMn0DyzmmSStE8yUsmPZrZmudPH7kplxBinOA=
-github.com/maruel/panicparse/v2 v2.3.1/go.mod h1:s3UmQB9Fm/n7n/prcD2xBGDkwXD6y2LeZnhbEXvs9Dg=
+github.com/maruel/panicparse/v2 v2.4.0 h1:yQKMIbQ0DKfinzVkTkcUzQyQ60UCiNnYfR7PWwTs2VI=
+github.com/maruel/panicparse/v2 v2.4.0/go.mod h1:nOY2OKe8csO3F3SA5+hsxot05JLgukrF54B9x88fVp4=
 github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1LhCkL9Q47H9Bg=
 github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU=
 github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
@@ -211,7 +205,6 @@ github.com/mdlayher/socket v0.1.0/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5A
 github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
 github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
 github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
-github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
 github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
@@ -329,8 +322,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
 github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
-github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
-github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
+github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
+github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
 github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
 github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
 github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
@@ -344,12 +337,12 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
-golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
-golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
-golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
-golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
-golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
+golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
+golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -378,15 +371,15 @@ golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
-golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
-golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -412,7 +405,6 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -420,13 +412,10 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
-golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -434,16 +423,16 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
-golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
-golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
-golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
+golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
+golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -452,8 +441,8 @@ google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
+google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -465,28 +454,28 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
 honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
-modernc.org/cc/v4 v4.23.1 h1:WqJoPL3x4cUufQVHkXpXX7ThFJ1C4ik80i2eXEXbhD8=
-modernc.org/cc/v4 v4.23.1/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
-modernc.org/ccgo/v4 v4.22.3 h1:C7AW89Zw3kygesTQWBzApwIn9ldM+cb/plrTIKq41Os=
-modernc.org/ccgo/v4 v4.22.3/go.mod h1:Dz7n0/UkBbH3pnYaxgi1mFSfF4REqUOZNziphZASx6k=
+modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
+modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.23.10 h1:DnDZT/H6TtoJvQmVf7d8W+lVqEZpIJY/+0ENFh1LIHE=
+modernc.org/ccgo/v4 v4.23.10/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
 modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
 modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
-modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
-modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
-modernc.org/libc v1.61.2 h1:dkO4DlowfClcJYsvf/RiK6fUwvzCQTmB34bJLt0CAGQ=
-modernc.org/libc v1.61.2/go.mod h1:4QGjNyX3h+rn7V5oHpJY2yH0QN6frt1X+5BkXzwLPCo=
-modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
-modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
-modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
-modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
-modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
-modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
-modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
-modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk=
-modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
-modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
-modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
+modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/libc v1.61.7 h1:exz8rasFniviSgh3dH7QBnQHqYh9lolA5hVYfsiwkfo=
+modernc.org/libc v1.61.7/go.mod h1:xspSrXRNVSfWfcfqgvZDVe/Hw5kv4FVC6IRfoms5v/0=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.8.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A=
+modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
+modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
 modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=