Compare commits

...

55 commits

Author SHA1 Message Date
Alexandr Stelnykovych
eccda20802
Merge pull request from safing/fix/sqlite-busy-timeout
Add a 3s busy timeout to SQLite database backend
2025-04-07 14:04:33 +03:00
Daniel
7b05ed82b2 Add a 3s busy timeout to SQLite database backend 2025-04-04 13:52:50 +02:00
Alexandr Stelnykovych
b9a9129e81 Merge branch 'feature/sqlite-db-v1' into develop 2025-03-25 17:08:32 +02:00
Daniel
be133b8856 Improve filterlist ingestion logging 2025-03-17 15:43:59 +01:00
Alexandr Stelnykovych
32c7f6e7d1 Merge branch 'feature/sqlite-db-v1' into develop 2025-03-17 16:31:48 +02:00
Alexandr Stelnykovych
4f3ebc7156
Merge pull request from safing/fix/spn_stickedHub_logical_issue
[fix] SPN - Improve sticky domain handling in getStickiedHub function
2025-03-17 16:15:45 +02:00
Daniel
9b12dfffc2 Add option to use prepared statements for SQLite PutMany 2025-03-17 14:13:53 +01:00
Daniel
2c8ab54104 Fix SQLite maintenance methods 2025-03-10 11:44:08 +01:00
Daniel
c0d8d0c2f0 Add new PurgeOlderThan interface method to SQLite Database 2025-03-10 10:34:57 +01:00
Daniel
67cfefde9b Fix tests 2025-03-04 17:05:03 +01:00
Daniel
b8ab348095 Fix tests and linters 2025-03-04 15:25:44 +01:00
Daniel
782c07d867 Update CI Linter config 2025-02-28 14:48:09 +01:00
Daniel
a13d52b68f Bump Go versions in CI 2025-02-28 14:44:39 +01:00
Daniel
71f6f09384 Use waitgroup instead of mutex for sqlite storage 2025-02-28 11:38:27 +01:00
Daniel
c04213219b Make format and value nullable and improve maintenance and purge queries 2025-02-28 10:21:59 +01:00
Daniel Hååvi
7373b8868b
Merge pull request from safing/fix/public-suffix-domain-list
Fix domain list generation within public suffix
2025-02-27 12:10:28 +01:00
Daniel
b68646c689 Use transaction for PutMany and cursor Query 2025-02-26 16:52:34 +01:00
Daniel
130c4a427c Fix timing on database root path initialization 2025-02-26 13:25:55 +01:00
Daniel
76c352da5a Make test parallel 2025-02-26 13:21:51 +01:00
Daniel
83292d761c Fix domain list generation within public suffix 2025-02-26 13:13:28 +01:00
Alexandr Stelnykovych
4721e58727 Update Go version to 1.24 and toolchain to 1.24.0 to support the 'tool' meta-pattern introduced in go.mod with Go v1.24 2025-02-26 13:03:57 +02:00
Daniel
90ead7d5e5 Switch core and cache databases to use sqlite when bbold db is not present 2025-02-25 13:49:49 +01:00
Daniel
c742c7dfd1 Add SQLite database storage backend 2025-02-25 11:48:16 +01:00
Alexandr Stelnykovych
fe8a560f9e [fix] Improve sticky domain handling in getStickiedHub function 2025-02-24 17:50:41 +02:00
Alexandr Stelnykovych
fdca991166
Merge pull request from safing/fix/1721-Error-when-clicking_Apps
[fix] UI: Error when clicking on 'Apps' in the application configuration
2025-02-12 12:41:43 +02:00
Alexandr Stelnykovych
5e7ad95a44
Merge pull request from safing/fix/packet-layer-set
[service] Fix IPv6 payload layer set
2025-02-12 12:39:15 +02:00
Vladimir Stoilov
40b443282f
[service] Fix IPv6 payload layer set 2025-02-11 13:55:29 +02:00
Alexandr Stelnykovych
d8108bff0e [fix] UI: Error when clicking on 'Apps' in the application configuration
https://github.com/safing/portmaster/issues/1721
2025-01-31 17:50:00 +02:00
Alexandr Stelnykovych
98137ca4b6
Merge pull request from safing/fix/kext-doc
[kext] Fix dev build documentation
2025-01-28 14:39:37 +02:00
Alexandr Stelnykovych
857df4086f [kext] Fix dev build documentation (releasing procedure) 2025-01-28 14:26:44 +02:00
Vladimir Stoilov
32d6e1cb04
[kext] Fix dev build documentation 2025-01-28 11:49:23 +02:00
Vladimir Stoilov
0f28af66cd
Add PID in ETW DNS event in the integration dll ()
* [service] Add reading of PID in ETW DNS event

* [service] Use PID of the ETW DNS events

* [service] Fix use of nil pointer

* [service] Fix compiler error
2025-01-27 17:21:54 +02:00
Daniel Hååvi
726159427b
Merge pull request from safing/dhaavi-patch-1
Fix rust-base build
2025-01-16 13:55:15 +01:00
Daniel Hååvi
88b92dcc93
Fix rust-base build
Sometimes rust attempts to upgrade crates. This stops it from doing that.
2025-01-14 14:28:56 +01:00
Alexandr Stelnykovych
f021ec2444
Merge pull request from safing/develop
v1.6.29
2025-01-13 16:27:48 +02:00
Alexandr Stelnykovych
3478622eb8 update deps 2025-01-13 14:15:48 +00:00
Daniel Hååvi
b4fda1bdce
Merge pull request from stenya/fix_logical_mistake
[fix] Logical mistake while determining local resolvers
2025-01-13 13:45:11 +01:00
Daniel Hååvi
0937bedd6c
Merge pull request from stenya/fix_crash_SleepyTicker
[fix] Panic while accessing SleepyTicker methods Stop()/SetSleep()
2025-01-13 13:44:59 +01:00
Daniel Hååvi
241bf20c7a
Merge pull request from stenya/intel_small_cache
[improvement] Small cache size
2025-01-13 13:44:31 +01:00
Daniel Hååvi
3ee214abaf
Merge pull request from safing/fix/windows-permission2
Fix windows permissions
2025-01-13 13:41:28 +01:00
Daniel
96209c28cf Fix SPN build 2025-01-13 10:09:11 +01:00
Daniel
ef7b129ced Use code quotes for cmds in docs 2024-12-20 13:37:01 +01:00
Daniel
1e9e6263d4 Fix SPN testnet portmaster args 2024-12-20 13:36:15 +01:00
Daniel
c7f3475382 Add spn testing setup 2024-12-20 13:31:52 +01:00
Alexandr Stelnykovych
6c014d227c [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`
2024-12-17 15:49:52 +02:00
Alexandr Stelnykovych
df70c70ab5 [improvement] Small cache size
(2 ^ 8) = (2 XOR 8) = 10.
Was it intended to be 256?
2024-12-16 16:01:42 +02:00
Alexandr Stelnykovych
692838b696 [fix] Logical mistake while determining local resolvers 2024-12-13 17:02:38 +02:00
Vladimir Stoilov
475d69f8a2
[service] Fix windows system SID 2024-12-06 16:45:37 +02:00
Daniel
9d874daed2 Simplify windows acl calls and switch to using SIDs 2024-12-06 14:34:54 +01:00
Vladimir Stoilov
05a5d5e350
[service] Fix unit tests 2024-12-06 14:47:24 +02:00
Vladimir Stoilov
22253c4e9e
[service] Fix windows permissions 2024-12-06 12:00:20 +02:00
Daniel Hååvi
6e173e3b96
Merge pull request from safing/fix/missing-dll-failure
Fix Missing dll failure
2024-12-02 15:23:07 +01:00
Vladimir Stoilov
ed2338fdb9
[service] Fix error on unitilized dns monitor 2024-12-02 15:25:58 +02:00
Vladimir Stoilov
2a9d75433f
[service] Fix module failure when dll is missing 2024-12-02 14:02:49 +02:00
Daniel
ef0995b1f7 Define identifier for portmaster-core.dll and make it mandatory 2024-11-28 11:45:29 +01:00
93 changed files with 3253 additions and 313 deletions

View file

@ -38,9 +38,9 @@ jobs:
cache: false
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v4
uses: golangci/golangci-lint-action@v6
with:
version: v1.60.3
version: v1.64.6
only-new-issues: true
args: -c ./.golangci.yml --timeout 15m

3
.gitignore vendored
View file

@ -12,8 +12,7 @@ go.mod.*
vendor
# testing
testing
spn/testing/simple/testdata
testdata
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.a

View file

@ -8,7 +8,6 @@ linters:
- contextcheck
- cyclop
- depguard
- exhaustivestruct
- exhaustruct
- forbidigo
- funlen
@ -16,12 +15,8 @@ linters:
- gochecknoinits
- gocognit
- gocyclo
- goerr113
- gomnd
- gomoddirectives
- ifshort
- interfacebloat
- interfacer
- ireturn
- lll
- mnd
@ -32,7 +27,6 @@ linters:
- noctx
- nolintlint
- nonamedreturns
- nosnakecase
- perfsprint # TODO(ppacher): we should re-enanble this one to avoid costly fmt.* calls in the hot-path
- revive
- tagliatelle
@ -42,15 +36,14 @@ linters:
- whitespace
- wrapcheck
- wsl
- gci
- tenv # Deprecated
linters-settings:
revive:
# See https://github.com/mgechev/revive#available-rules for details.
enable-all-rules: true
gci:
# put imports beginning with prefix after 3rd-party packages;
# only support one prefix
# if not set, use goimports.local-prefixes
goimports:
local-prefixes: github.com/safing
godox:
# report any comments starting with keywords, this is useful for TODO or FIXME comments that

View file

@ -1,9 +1,9 @@
VERSION --arg-scope-and-set --global-cache 0.8
ARG --global go_version = 1.22
ARG --global go_version = 1.24
ARG --global node_version = 18
ARG --global rust_version = 1.79
ARG --global golangci_lint_version = 1.57.1
ARG --global golangci_lint_version = 1.64.6
ARG --global go_builder_image = "golang:${go_version}-alpine"
ARG --global node_builder_image = "node:${node_version}"
@ -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"
@ -420,7 +425,7 @@ rust-base:
DO rust+INIT --keep_fingerprints=true
# For now we need tauri-cli 2.0.0 for bulding
DO rust+CARGO --args="install tauri-cli --version 2.1.0"
DO rust+CARGO --args="install tauri-cli --version 2.1.0 --locked"
# Explicitly cache here.
SAVE IMAGE --cache-hint

View file

@ -10,6 +10,7 @@ import (
"path/filepath"
"sort"
"github.com/safing/portmaster/base/database/dbmodule"
"github.com/safing/portmaster/base/dataroot"
"github.com/safing/portmaster/base/utils"
"github.com/safing/portmaster/base/utils/debug"
@ -144,12 +145,13 @@ 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)
}
dbmodule.SetDatabaseLocation(dataroot.Root())
return basePath, nil
}

View file

@ -264,6 +264,20 @@ func (c *Controller) Purge(ctx context.Context, q *query.Query, local, internal
return 0, ErrNotImplemented
}
// PurgeOlderThan deletes all records last updated before the given time.
// It returns the number of successful deletes and an error.
func (c *Controller) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time, local, internal bool) (int, error) {
if shuttingDown.IsSet() {
return 0, ErrShuttingDown
}
if purger, ok := c.storage.(storage.PurgeOlderThan); ok {
return purger.PurgeOlderThan(ctx, prefix, purgeBefore, local, internal, c.shadowDelete)
}
return 0, ErrNotImplemented
}
// Shutdown shuts down the storage.
func (c *Controller) Shutdown() error {
return c.storage.Shutdown()

View file

@ -5,7 +5,6 @@ import (
"sync/atomic"
"github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/dataroot"
"github.com/safing/portmaster/base/utils"
"github.com/safing/portmaster/service/mgr"
)
@ -37,7 +36,6 @@ func SetDatabaseLocation(dirStructureRoot *utils.DirStructure) {
}
func prep() error {
SetDatabaseLocation(dataroot.Root())
if databaseStructureRoot == nil {
return errors.New("database location not specified")
}

View file

@ -562,6 +562,27 @@ func (i *Interface) Purge(ctx context.Context, q *query.Query) (int, error) {
return db.Purge(ctx, q, i.options.Local, i.options.Internal)
}
// PurgeOlderThan deletes all records last updated before the given time.
// It returns the number of successful deletes and an error.
func (i *Interface) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time) (int, error) {
dbName, dbKeyPrefix := record.ParseKey(prefix)
if dbName == "" {
return 0, errors.New("unknown database")
}
db, err := getController(dbName)
if err != nil {
return 0, err
}
// Check if database is read only before we add to the cache.
if db.ReadOnly() {
return 0, ErrReadOnly
}
return db.PurgeOlderThan(ctx, dbKeyPrefix, purgeBefore, i.options.Local, i.options.Internal)
}
// Subscribe subscribes to updates matching the given query.
func (i *Interface) Subscribe(q *query.Query) (*Subscription, error) {
_, err := q.Check()

View file

@ -9,9 +9,8 @@ import (
"github.com/safing/portmaster/base/utils"
)
const (
databasesSubDir = "databases"
)
// DatabasesSubDir defines the sub directory where the databases are stored.
const DatabasesSubDir = "databases"
var (
initialized = abool.NewBool(false)
@ -25,7 +24,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 +33,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 +66,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 {

View file

@ -99,6 +99,11 @@ func (q *Query) MatchesKey(dbKey string) bool {
return strings.HasPrefix(dbKey, q.dbKeyPrefix)
}
// HasWhereCondition returns whether the query has a "where" condition set.
func (q *Query) HasWhereCondition() bool {
return q.where != nil
}
// MatchesRecord checks whether the query matches the supplied database record (value only).
func (q *Query) MatchesRecord(r record.Record) bool {
if q.where == nil {

View file

@ -102,7 +102,7 @@ func (b *Base) SetMeta(meta *Meta) {
b.meta = meta
}
// Marshal marshals the object, without the database key or metadata. It returns nil if the record is deleted.
// Marshal marshals the format and data.
func (b *Base) Marshal(self Record, format uint8) ([]byte, error) {
if b.Meta() == nil {
return nil, errors.New("missing meta")
@ -119,7 +119,20 @@ func (b *Base) Marshal(self Record, format uint8) ([]byte, error) {
return dumped, nil
}
// MarshalRecord packs the object, including metadata, into a byte array for saving in a database.
// MarshalDataOnly marshals the data only.
func (b *Base) MarshalDataOnly(self Record, format uint8) ([]byte, error) {
if b.Meta() == nil {
return nil, errors.New("missing meta")
}
if b.Meta().Deleted > 0 {
return nil, nil
}
return dsd.DumpWithoutIdentifier(self, format, "")
}
// MarshalRecord marshals the data, format and metadata.
func (b *Base) MarshalRecord(self Record) ([]byte, error) {
if b.Meta() == nil {
return nil, errors.New("missing meta")

View file

@ -49,11 +49,21 @@ func (m *Meta) MakeCrownJewel() {
m.cronjewel = true
}
// IsCrownJewel returns whether the database record is marked as a crownjewel.
func (m *Meta) IsCrownJewel() bool {
return m.cronjewel
}
// MakeSecret sets the database record as secret, meaning that it may only be used internally, and not by interfacing processes, such as the UI.
func (m *Meta) MakeSecret() {
m.secret = true
}
// IsSecret returns whether the database record is marked as a secret.
func (m *Meta) IsSecret() bool {
return m.secret
}
// Update updates the internal meta states and should be called before writing the record to the database.
func (m *Meta) Update() {
now := time.Now().Unix()

View file

@ -20,6 +20,7 @@ type Record interface {
// Serialization.
Marshal(self Record, format uint8) ([]byte, error)
MarshalDataOnly(self Record, format uint8) ([]byte, error)
MarshalRecord(self Record) ([]byte, error)
GetAccessor(self Record) accessor.Accessor

View file

@ -79,7 +79,21 @@ func NewWrapper(key string, meta *Meta, format uint8, data []byte) (*Wrapper, er
}, nil
}
// Marshal marshals the object, without the database key or metadata.
// NewWrapperFromDatabase returns a new record wrapper for the given data.
func NewWrapperFromDatabase(dbName, dbKey string, meta *Meta, format uint8, data []byte) (*Wrapper, error) {
return &Wrapper{
Base{
dbName: dbName,
dbKey: dbKey,
meta: meta,
},
sync.Mutex{},
format,
data,
}, nil
}
// Marshal marshals the format and data.
func (w *Wrapper) Marshal(r Record, format uint8) ([]byte, error) {
if w.Meta() == nil {
return nil, errors.New("missing meta")
@ -100,7 +114,24 @@ func (w *Wrapper) Marshal(r Record, format uint8) ([]byte, error) {
return data, nil
}
// MarshalRecord packs the object, including metadata, into a byte array for saving in a database.
// MarshalDataOnly marshals the data only.
func (w *Wrapper) MarshalDataOnly(self Record, format uint8) ([]byte, error) {
if w.Meta() == nil {
return nil, errors.New("missing meta")
}
if w.Meta().Deleted > 0 {
return nil, nil
}
if format != dsd.AUTO && format != w.Format {
return nil, errors.New("could not dump model, wrapped object format mismatch")
}
return w.Data, nil
}
// MarshalRecord marshals the data, format and metadata.
func (w *Wrapper) MarshalRecord(r Record) ([]byte, error) {
// Duplication necessary, as the version from Base would call Base.Marshal instead of Wrapper.Marshal

View file

@ -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"
@ -289,11 +288,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
}

View file

@ -46,3 +46,8 @@ type Batcher interface {
type Purger interface {
Purge(ctx context.Context, q *query.Query, local, internal, shadowDelete bool) (int, error)
}
// PurgeOlderThan defines the database storage API for backends that support the PurgeOlderThan operation.
type PurgeOlderThan interface {
PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time, local, internal, shadowDelete bool) (int, error)
}

View file

@ -0,0 +1,6 @@
sqlite:
dsn: "testdata/schema.db"
except:
migrations:
no_factory: true

View file

@ -0,0 +1,7 @@
-- +migrate Up
-- SQL in section 'Up' is executed when this migration is applied
PRAGMA auto_vacuum = INCREMENTAL; -- https://sqlite.org/pragma.html#pragma_auto_vacuum
-- +migrate Down
-- SQL section 'Down' is executed when this migration is rolled back
PRAGMA auto_vacuum = NONE; -- https://sqlite.org/pragma.html#pragma_auto_vacuum

View file

@ -0,0 +1,19 @@
-- +migrate Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE records (
key TEXT PRIMARY KEY,
format SMALLINT,
value BLOB,
created BIGINT NOT NULL,
modified BIGINT NOT NULL,
expires BIGINT DEFAULT 0 NOT NULL,
deleted BIGINT DEFAULT 0 NOT NULL,
secret BOOLEAN DEFAULT FALSE NOT NULL,
crownjewel BOOLEAN DEFAULT FALSE NOT NULL
);
-- +migrate Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE records;

View file

@ -0,0 +1,5 @@
development:
dialect: sqlite3
datasource: testdata/schema.db
dir: migrations
table: migrations

View file

@ -0,0 +1,131 @@
// Code generated by BobGen sqlite v0.30.0. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import (
"hash/maphash"
"strings"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/clause"
"github.com/stephenafamo/bob/dialect/sqlite"
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
sqliteDriver "modernc.org/sqlite"
)
var TableNames = struct {
Records string
}{
Records: "records",
}
var ColumnNames = struct {
Records recordColumnNames
}{
Records: recordColumnNames{
Key: "key",
Format: "format",
Value: "value",
Created: "created",
Modified: "modified",
Expires: "expires",
Deleted: "deleted",
Secret: "secret",
Crownjewel: "crownjewel",
},
}
var (
SelectWhere = Where[*dialect.SelectQuery]()
InsertWhere = Where[*dialect.InsertQuery]()
UpdateWhere = Where[*dialect.UpdateQuery]()
DeleteWhere = Where[*dialect.DeleteQuery]()
)
func Where[Q sqlite.Filterable]() struct {
Records recordWhere[Q]
} {
return struct {
Records recordWhere[Q]
}{
Records: buildRecordWhere[Q](RecordColumns),
}
}
var (
SelectJoins = getJoins[*dialect.SelectQuery]
UpdateJoins = getJoins[*dialect.UpdateQuery]
)
type joinSet[Q interface{ aliasedAs(string) Q }] struct {
InnerJoin Q
LeftJoin Q
RightJoin Q
}
func (j joinSet[Q]) AliasedAs(alias string) joinSet[Q] {
return joinSet[Q]{
InnerJoin: j.InnerJoin.aliasedAs(alias),
LeftJoin: j.LeftJoin.aliasedAs(alias),
RightJoin: j.RightJoin.aliasedAs(alias),
}
}
type joins[Q dialect.Joinable] struct{}
func buildJoinSet[Q interface{ aliasedAs(string) Q }, C any, F func(C, string) Q](c C, f F) joinSet[Q] {
return joinSet[Q]{
InnerJoin: f(c, clause.InnerJoin),
LeftJoin: f(c, clause.LeftJoin),
RightJoin: f(c, clause.RightJoin),
}
}
func getJoins[Q dialect.Joinable]() joins[Q] {
return joins[Q]{}
}
type modAs[Q any, C interface{ AliasedAs(string) C }] struct {
c C
f func(C) bob.Mod[Q]
}
func (m modAs[Q, C]) Apply(q Q) {
m.f(m.c).Apply(q)
}
func (m modAs[Q, C]) AliasedAs(alias string) bob.Mod[Q] {
m.c = m.c.AliasedAs(alias)
return m
}
func randInt() int64 {
out := int64(new(maphash.Hash).Sum64())
if out < 0 {
return -out % 10000
}
return out % 10000
}
// ErrUniqueConstraint captures all unique constraint errors by explicitly leaving `s` empty.
var ErrUniqueConstraint = &UniqueConstraintError{s: ""}
type UniqueConstraintError struct {
// s is a string uniquely identifying the constraint in the raw error message returned from the database.
s string
}
func (e *UniqueConstraintError) Error() string {
return e.s
}
func (e *UniqueConstraintError) Is(target error) bool {
err, ok := target.(*sqliteDriver.Error)
if !ok {
return false
}
return err.Code() == 2067 && strings.Contains(err.Error(), e.s)
}

View file

@ -0,0 +1,9 @@
// Code generated by BobGen sqlite v0.30.0. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import "github.com/stephenafamo/bob"
// Make sure the type Record runs hooks after queries
var _ bob.HookableType = &Record{}

View file

@ -0,0 +1,555 @@
// Code generated by BobGen sqlite v0.30.0. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import (
"context"
"io"
"github.com/aarondl/opt/null"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite"
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
"github.com/stephenafamo/bob/dialect/sqlite/dm"
"github.com/stephenafamo/bob/dialect/sqlite/sm"
"github.com/stephenafamo/bob/dialect/sqlite/um"
"github.com/stephenafamo/bob/expr"
)
// Record is an object representing the database table.
type Record struct {
Key string `db:"key,pk" `
Format null.Val[int16] `db:"format" `
Value null.Val[[]byte] `db:"value" `
Created int64 `db:"created" `
Modified int64 `db:"modified" `
Expires int64 `db:"expires" `
Deleted int64 `db:"deleted" `
Secret bool `db:"secret" `
Crownjewel bool `db:"crownjewel" `
}
// RecordSlice is an alias for a slice of pointers to Record.
// This should almost always be used instead of []*Record.
type RecordSlice []*Record
// Records contains methods to work with the records table
var Records = sqlite.NewTablex[*Record, RecordSlice, *RecordSetter]("", "records")
// RecordsQuery is a query on the records table
type RecordsQuery = *sqlite.ViewQuery[*Record, RecordSlice]
type recordColumnNames struct {
Key string
Format string
Value string
Created string
Modified string
Expires string
Deleted string
Secret string
Crownjewel string
}
var RecordColumns = buildRecordColumns("records")
type recordColumns struct {
tableAlias string
Key sqlite.Expression
Format sqlite.Expression
Value sqlite.Expression
Created sqlite.Expression
Modified sqlite.Expression
Expires sqlite.Expression
Deleted sqlite.Expression
Secret sqlite.Expression
Crownjewel sqlite.Expression
}
func (c recordColumns) Alias() string {
return c.tableAlias
}
func (recordColumns) AliasedAs(alias string) recordColumns {
return buildRecordColumns(alias)
}
func buildRecordColumns(alias string) recordColumns {
return recordColumns{
tableAlias: alias,
Key: sqlite.Quote(alias, "key"),
Format: sqlite.Quote(alias, "format"),
Value: sqlite.Quote(alias, "value"),
Created: sqlite.Quote(alias, "created"),
Modified: sqlite.Quote(alias, "modified"),
Expires: sqlite.Quote(alias, "expires"),
Deleted: sqlite.Quote(alias, "deleted"),
Secret: sqlite.Quote(alias, "secret"),
Crownjewel: sqlite.Quote(alias, "crownjewel"),
}
}
type recordWhere[Q sqlite.Filterable] struct {
Key sqlite.WhereMod[Q, string]
Format sqlite.WhereNullMod[Q, int16]
Value sqlite.WhereNullMod[Q, []byte]
Created sqlite.WhereMod[Q, int64]
Modified sqlite.WhereMod[Q, int64]
Expires sqlite.WhereMod[Q, int64]
Deleted sqlite.WhereMod[Q, int64]
Secret sqlite.WhereMod[Q, bool]
Crownjewel sqlite.WhereMod[Q, bool]
}
func (recordWhere[Q]) AliasedAs(alias string) recordWhere[Q] {
return buildRecordWhere[Q](buildRecordColumns(alias))
}
func buildRecordWhere[Q sqlite.Filterable](cols recordColumns) recordWhere[Q] {
return recordWhere[Q]{
Key: sqlite.Where[Q, string](cols.Key),
Format: sqlite.WhereNull[Q, int16](cols.Format),
Value: sqlite.WhereNull[Q, []byte](cols.Value),
Created: sqlite.Where[Q, int64](cols.Created),
Modified: sqlite.Where[Q, int64](cols.Modified),
Expires: sqlite.Where[Q, int64](cols.Expires),
Deleted: sqlite.Where[Q, int64](cols.Deleted),
Secret: sqlite.Where[Q, bool](cols.Secret),
Crownjewel: sqlite.Where[Q, bool](cols.Crownjewel),
}
}
// RecordSetter is used for insert/upsert/update operations
// All values are optional, and do not have to be set
// Generated columns are not included
type RecordSetter struct {
Key omit.Val[string] `db:"key,pk" `
Format omitnull.Val[int16] `db:"format" `
Value omitnull.Val[[]byte] `db:"value" `
Created omit.Val[int64] `db:"created" `
Modified omit.Val[int64] `db:"modified" `
Expires omit.Val[int64] `db:"expires" `
Deleted omit.Val[int64] `db:"deleted" `
Secret omit.Val[bool] `db:"secret" `
Crownjewel omit.Val[bool] `db:"crownjewel" `
}
func (s RecordSetter) SetColumns() []string {
vals := make([]string, 0, 9)
if !s.Key.IsUnset() {
vals = append(vals, "key")
}
if !s.Format.IsUnset() {
vals = append(vals, "format")
}
if !s.Value.IsUnset() {
vals = append(vals, "value")
}
if !s.Created.IsUnset() {
vals = append(vals, "created")
}
if !s.Modified.IsUnset() {
vals = append(vals, "modified")
}
if !s.Expires.IsUnset() {
vals = append(vals, "expires")
}
if !s.Deleted.IsUnset() {
vals = append(vals, "deleted")
}
if !s.Secret.IsUnset() {
vals = append(vals, "secret")
}
if !s.Crownjewel.IsUnset() {
vals = append(vals, "crownjewel")
}
return vals
}
func (s RecordSetter) Overwrite(t *Record) {
if !s.Key.IsUnset() {
t.Key, _ = s.Key.Get()
}
if !s.Format.IsUnset() {
t.Format, _ = s.Format.GetNull()
}
if !s.Value.IsUnset() {
t.Value, _ = s.Value.GetNull()
}
if !s.Created.IsUnset() {
t.Created, _ = s.Created.Get()
}
if !s.Modified.IsUnset() {
t.Modified, _ = s.Modified.Get()
}
if !s.Expires.IsUnset() {
t.Expires, _ = s.Expires.Get()
}
if !s.Deleted.IsUnset() {
t.Deleted, _ = s.Deleted.Get()
}
if !s.Secret.IsUnset() {
t.Secret, _ = s.Secret.Get()
}
if !s.Crownjewel.IsUnset() {
t.Crownjewel, _ = s.Crownjewel.Get()
}
}
func (s *RecordSetter) Apply(q *dialect.InsertQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Records.BeforeInsertHooks.RunHooks(ctx, exec, s)
})
if len(q.Table.Columns) == 0 {
q.Table.Columns = s.SetColumns()
}
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 0, 9)
if !s.Key.IsUnset() {
vals = append(vals, sqlite.Arg(s.Key))
}
if !s.Format.IsUnset() {
vals = append(vals, sqlite.Arg(s.Format))
}
if !s.Value.IsUnset() {
vals = append(vals, sqlite.Arg(s.Value))
}
if !s.Created.IsUnset() {
vals = append(vals, sqlite.Arg(s.Created))
}
if !s.Modified.IsUnset() {
vals = append(vals, sqlite.Arg(s.Modified))
}
if !s.Expires.IsUnset() {
vals = append(vals, sqlite.Arg(s.Expires))
}
if !s.Deleted.IsUnset() {
vals = append(vals, sqlite.Arg(s.Deleted))
}
if !s.Secret.IsUnset() {
vals = append(vals, sqlite.Arg(s.Secret))
}
if !s.Crownjewel.IsUnset() {
vals = append(vals, sqlite.Arg(s.Crownjewel))
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
}))
}
func (s RecordSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return um.Set(s.Expressions()...)
}
func (s RecordSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 9)
if !s.Key.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "key")...),
sqlite.Arg(s.Key),
}})
}
if !s.Format.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "format")...),
sqlite.Arg(s.Format),
}})
}
if !s.Value.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "value")...),
sqlite.Arg(s.Value),
}})
}
if !s.Created.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "created")...),
sqlite.Arg(s.Created),
}})
}
if !s.Modified.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "modified")...),
sqlite.Arg(s.Modified),
}})
}
if !s.Expires.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "expires")...),
sqlite.Arg(s.Expires),
}})
}
if !s.Deleted.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "deleted")...),
sqlite.Arg(s.Deleted),
}})
}
if !s.Secret.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "secret")...),
sqlite.Arg(s.Secret),
}})
}
if !s.Crownjewel.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "crownjewel")...),
sqlite.Arg(s.Crownjewel),
}})
}
return exprs
}
// FindRecord retrieves a single record by primary key
// If cols is empty Find will return all columns.
func FindRecord(ctx context.Context, exec bob.Executor, KeyPK string, cols ...string) (*Record, error) {
if len(cols) == 0 {
return Records.Query(
SelectWhere.Records.Key.EQ(KeyPK),
).One(ctx, exec)
}
return Records.Query(
SelectWhere.Records.Key.EQ(KeyPK),
sm.Columns(Records.Columns().Only(cols...)),
).One(ctx, exec)
}
// RecordExists checks the presence of a single record by primary key
func RecordExists(ctx context.Context, exec bob.Executor, KeyPK string) (bool, error) {
return Records.Query(
SelectWhere.Records.Key.EQ(KeyPK),
).Exists(ctx, exec)
}
// AfterQueryHook is called after Record is retrieved from the database
func (o *Record) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = Records.AfterSelectHooks.RunHooks(ctx, exec, RecordSlice{o})
case bob.QueryTypeInsert:
ctx, err = Records.AfterInsertHooks.RunHooks(ctx, exec, RecordSlice{o})
case bob.QueryTypeUpdate:
ctx, err = Records.AfterUpdateHooks.RunHooks(ctx, exec, RecordSlice{o})
case bob.QueryTypeDelete:
ctx, err = Records.AfterDeleteHooks.RunHooks(ctx, exec, RecordSlice{o})
}
return err
}
// PrimaryKeyVals returns the primary key values of the Record
func (o *Record) PrimaryKeyVals() bob.Expression {
return sqlite.Arg(o.Key)
}
func (o *Record) pkEQ() dialect.Expression {
return sqlite.Quote("records", "key").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
return o.PrimaryKeyVals().WriteSQL(ctx, w, d, start)
}))
}
// Update uses an executor to update the Record
func (o *Record) Update(ctx context.Context, exec bob.Executor, s *RecordSetter) error {
v, err := Records.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec)
if err != nil {
return err
}
*o = *v
return nil
}
// Delete deletes a single Record record with an executor
func (o *Record) Delete(ctx context.Context, exec bob.Executor) error {
_, err := Records.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec)
return err
}
// Reload refreshes the Record using the executor
func (o *Record) Reload(ctx context.Context, exec bob.Executor) error {
o2, err := Records.Query(
SelectWhere.Records.Key.EQ(o.Key),
).One(ctx, exec)
if err != nil {
return err
}
*o = *o2
return nil
}
// AfterQueryHook is called after RecordSlice is retrieved from the database
func (o RecordSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = Records.AfterSelectHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeInsert:
ctx, err = Records.AfterInsertHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeUpdate:
ctx, err = Records.AfterUpdateHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeDelete:
ctx, err = Records.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}
func (o RecordSlice) pkIN() dialect.Expression {
if len(o) == 0 {
return sqlite.Raw("NULL")
}
return sqlite.Quote("records", "key").In(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
pkPairs := make([]bob.Expression, len(o))
for i, row := range o {
pkPairs[i] = row.PrimaryKeyVals()
}
return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "")
}))
}
// copyMatchingRows finds models in the given slice that have the same primary key
// then it first copies the existing relationships from the old model to the new model
// and then replaces the old model in the slice with the new model
func (o RecordSlice) copyMatchingRows(from ...*Record) {
for i, old := range o {
for _, new := range from {
if new.Key != old.Key {
continue
}
o[i] = new
break
}
}
}
// UpdateMod modifies an update query with "WHERE primary_key IN (o...)"
func (o RecordSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Records.BeforeUpdateHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *Record:
o.copyMatchingRows(retrieved)
case []*Record:
o.copyMatchingRows(retrieved...)
case RecordSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a Record or a slice of Record
// then run the AfterUpdateHooks on the slice
_, err = Records.AfterUpdateHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)"
func (o RecordSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] {
return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Records.BeforeDeleteHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *Record:
o.copyMatchingRows(retrieved)
case []*Record:
o.copyMatchingRows(retrieved...)
case RecordSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a Record or a slice of Record
// then run the AfterDeleteHooks on the slice
_, err = Records.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
func (o RecordSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals RecordSetter) error {
if len(o) == 0 {
return nil
}
_, err := Records.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec)
return err
}
func (o RecordSlice) DeleteAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
_, err := Records.Delete(o.DeleteMod()).Exec(ctx, exec)
return err
}
func (o RecordSlice) ReloadAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
o2, err := Records.Query(sm.Where(o.pkIN())).All(ctx, exec)
if err != nil {
return err
}
o.copyMatchingRows(o2...)
return nil
}

View file

@ -0,0 +1,125 @@
package sqlite
import (
"context"
"strconv"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite/im"
"github.com/stephenafamo/bob/expr"
"github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/database/storage/sqlite/models"
"github.com/safing/structures/dsd"
)
var UsePreparedStatements bool = true
// PutMany stores many records in the database.
func (db *SQLite) putManyWithPreparedStmts(shadowDelete bool) (chan<- record.Record, <-chan error) {
batch := make(chan record.Record, 100)
errs := make(chan error, 1)
// Simulate upsert with custom selection on conflict.
rawQuery, _, err := models.Records.Insert(
im.Into("records", "key", "format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"),
im.Values(expr.Arg("key"), expr.Arg("format"), expr.Arg("value"), expr.Arg("created"), expr.Arg("modified"), expr.Arg("expires"), expr.Arg("deleted"), expr.Arg("secret"), expr.Arg("crownjewel")),
im.OnConflict("key").DoUpdate(
im.SetExcluded("format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"),
),
).Build(db.ctx)
if err != nil {
errs <- err
return batch, errs
}
// Start transaction.
tx, err := db.bob.BeginTx(db.ctx, nil)
if err != nil {
errs <- err
return batch, errs
}
// Create prepared statement WITHIN TRANSACTION.
preparedStmt, err := tx.PrepareContext(db.ctx, rawQuery)
if err != nil {
errs <- err
return batch, errs
}
// start handler
go func() {
// Read all put records.
writeBatch:
for {
select {
case r := <-batch:
if r != nil {
// Write record.
err := writeWithPreparedStatement(db.ctx, &preparedStmt, r)
if err != nil {
errs <- err
break writeBatch
}
} else {
// Finalize transcation.
errs <- tx.Commit()
return
}
case <-db.ctx.Done():
break writeBatch
}
}
// Rollback transaction.
errs <- tx.Rollback()
}()
return batch, errs
}
func writeWithPreparedStatement(ctx context.Context, pStmt *bob.StdPrepared, r record.Record) error {
r.Lock()
defer r.Unlock()
// Serialize to JSON.
data, err := r.MarshalDataOnly(r, dsd.JSON)
if err != nil {
return err
}
// Get Meta.
m := r.Meta()
// Insert.
if len(data) > 0 {
format := strconv.Itoa(dsd.JSON)
_, err = pStmt.ExecContext(
ctx,
r.DatabaseKey(),
format,
data,
m.Created,
m.Modified,
m.Expires,
m.Deleted,
m.IsSecret(),
m.IsCrownJewel(),
)
} else {
_, err = pStmt.ExecContext(
ctx,
r.DatabaseKey(),
nil,
nil,
m.Created,
m.Modified,
m.Expires,
m.Deleted,
m.IsSecret(),
m.IsCrownJewel(),
)
}
return err
}

View file

@ -0,0 +1,86 @@
package sqlite
import (
"strconv"
"testing"
)
func BenchmarkPutMany(b *testing.B) {
// Configure prepared statement usage.
origSetting := UsePreparedStatements
UsePreparedStatements = false
defer func() {
UsePreparedStatements = origSetting
}()
// Run benchmark.
benchPutMany(b)
}
func BenchmarkPutManyPreparedStmt(b *testing.B) {
// Configure prepared statement usage.
origSetting := UsePreparedStatements
UsePreparedStatements = true
defer func() {
UsePreparedStatements = origSetting
}()
// Run benchmark.
benchPutMany(b)
}
func benchPutMany(b *testing.B) { //nolint:thelper
// Start database.
testDir := b.TempDir()
db, err := openSQLite("test", testDir, false)
if err != nil {
b.Fatal(err)
}
defer func() {
// shutdown
err = db.Shutdown()
if err != nil {
b.Fatal(err)
}
}()
// Start benchmarking.
b.ResetTimer()
// Benchmark PutMany.
records, errs := db.PutMany(false)
for i := range b.N {
// Create test record.
newTestRecord := &TestRecord{
S: "banana",
I: 42,
I8: 42,
I16: 42,
I32: 42,
I64: 42,
UI: 42,
UI8: 42,
UI16: 42,
UI32: 42,
UI64: 42,
F32: 42.42,
F64: 42.42,
B: true,
}
newTestRecord.UpdateMeta()
newTestRecord.SetKey("test:" + strconv.Itoa(i))
select {
case records <- newTestRecord:
case err := <-errs:
b.Fatal(err)
}
}
// Finalize.
close(records)
err = <-errs
if err != nil {
b.Fatal(err)
}
}

View file

@ -0,0 +1,51 @@
package sqlite
// Base command for sql-migrate:
//go:generate -command migrate go tool github.com/rubenv/sql-migrate/sql-migrate
// Run missing migrations:
//go:generate migrate up --config=migrations_config.yml
// Redo last migration:
// x go:generate migrate redo --config=migrations_config.yml
// Undo all migrations:
// x go:generate migrate down --config=migrations_config.yml
// Generate models with bob:
//go:generate go tool github.com/stephenafamo/bob/gen/bobgen-sqlite
import (
"embed"
migrate "github.com/rubenv/sql-migrate"
"github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/database/storage/sqlite/models"
)
//go:embed migrations/*
var dbMigrations embed.FS
func getMigrations() migrate.EmbedFileSystemMigrationSource {
return migrate.EmbedFileSystemMigrationSource{
FileSystem: dbMigrations,
Root: "migrations",
}
}
func getMeta(r *models.Record) *record.Meta {
meta := &record.Meta{
Created: r.Created,
Modified: r.Modified,
Expires: r.Expires,
Deleted: r.Deleted,
}
if r.Secret {
meta.MakeSecret()
}
if r.Crownjewel {
meta.MakeCrownJewel()
}
return meta
}

View file

@ -0,0 +1,566 @@
package sqlite
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
"sync"
"time"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
migrate "github.com/rubenv/sql-migrate"
sqldblogger "github.com/simukti/sqldb-logger"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite"
"github.com/stephenafamo/bob/dialect/sqlite/im"
"github.com/stephenafamo/bob/dialect/sqlite/um"
_ "modernc.org/sqlite"
"github.com/safing/portmaster/base/database/accessor"
"github.com/safing/portmaster/base/database/iterator"
"github.com/safing/portmaster/base/database/query"
"github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/database/storage"
"github.com/safing/portmaster/base/database/storage/sqlite/models"
"github.com/safing/portmaster/base/log"
"github.com/safing/structures/dsd"
)
// Errors.
var (
ErrQueryTimeout = errors.New("query timeout")
)
// SQLite storage.
type SQLite struct {
name string
db *sql.DB
bob bob.DB
wg sync.WaitGroup
ctx context.Context
cancelCtx context.CancelFunc
}
func init() {
_ = storage.Register("sqlite", func(name, location string) (storage.Interface, error) {
return NewSQLite(name, location)
})
}
// NewSQLite creates a sqlite database.
func NewSQLite(name, location string) (*SQLite, error) {
return openSQLite(name, location, false)
}
// openSQLite creates a sqlite database.
func openSQLite(name, location string, printStmts bool) (*SQLite, error) {
dbFile := filepath.Join(location, "db.sqlite")
// Open database file.
// Default settings:
// _time_format = YYYY-MM-DDTHH:MM:SS.SSS
// _txlock = deferred
db, err := sql.Open("sqlite", dbFile)
if err != nil {
return nil, fmt.Errorf("open sqlite: %w", err)
}
// Enable statement printing.
if printStmts {
db = sqldblogger.OpenDriver(dbFile, db.Driver(), &statementLogger{})
}
// Set other settings.
pragmas := []string{
"PRAGMA journal_mode=WAL;", // Corruption safe write ahead log for txs.
"PRAGMA synchronous=NORMAL;", // Best for WAL.
"PRAGMA cache_size=-10000;", // 10MB Cache.
"PRAGMA busy_timeout=3000;", // 3s (3000ms) timeout for locked tables.
}
for _, pragma := range pragmas {
_, err := db.Exec(pragma)
if err != nil {
return nil, fmt.Errorf("failed to init sqlite with %s: %w", pragma, err)
}
}
// Run migrations on database.
n, err := migrate.Exec(db, "sqlite3", getMigrations(), migrate.Up)
if err != nil {
return nil, fmt.Errorf("migrate sqlite: %w", err)
}
log.Debugf("database/sqlite: ran %d migrations on %s database", n, name)
// Return as bob database.
ctx, cancelCtx := context.WithCancel(context.Background())
return &SQLite{
name: name,
db: db,
bob: bob.NewDB(db),
ctx: ctx,
cancelCtx: cancelCtx,
}, nil
}
// Get returns a database record.
func (db *SQLite) Get(key string) (record.Record, error) {
db.wg.Add(1)
defer db.wg.Done()
// Get record from database.
r, err := models.FindRecord(db.ctx, db.bob, key)
if err != nil {
return nil, fmt.Errorf("%w: %w", storage.ErrNotFound, err)
}
// Return data in wrapper.
return record.NewWrapperFromDatabase(
db.name,
key,
getMeta(r),
uint8(r.Format.GetOrZero()), //nolint:gosec // Values are within uint8.
r.Value.GetOrZero(),
)
}
// GetMeta returns the metadata of a database record.
func (db *SQLite) GetMeta(key string) (*record.Meta, error) {
r, err := db.Get(key)
if err != nil {
return nil, err
}
return r.Meta(), nil
}
// Put stores a record in the database.
func (db *SQLite) Put(r record.Record) (record.Record, error) {
return db.putRecord(r, nil)
}
func (db *SQLite) putRecord(r record.Record, tx *bob.Tx) (record.Record, error) {
db.wg.Add(1)
defer db.wg.Done()
// Lock record if in a transaction.
if tx != nil {
r.Lock()
defer r.Unlock()
}
// Serialize to JSON.
data, err := r.MarshalDataOnly(r, dsd.JSON)
if err != nil {
return nil, err
}
// Prepare for setter.
setFormat := omitnull.From(int16(dsd.JSON))
setData := omitnull.From(data)
if len(data) == 0 {
setFormat.Null()
setData.Null()
}
// Create structure for insert.
m := r.Meta()
setter := models.RecordSetter{
Key: omit.From(r.DatabaseKey()),
Format: setFormat,
Value: setData,
Created: omit.From(m.Created),
Modified: omit.From(m.Modified),
Expires: omit.From(m.Expires),
Deleted: omit.From(m.Deleted),
Secret: omit.From(m.IsSecret()),
Crownjewel: omit.From(m.IsCrownJewel()),
}
// Simulate upsert with custom selection on conflict.
dbQuery := models.Records.Insert(
&setter,
im.OnConflict("key").DoUpdate(
im.SetExcluded("format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"),
),
)
// Execute in transaction or directly.
if tx != nil {
_, err = dbQuery.Exec(db.ctx, tx)
} else {
_, err = dbQuery.Exec(db.ctx, db.bob)
}
if err != nil {
return nil, err
}
return r, nil
}
// PutMany stores many records in the database.
func (db *SQLite) PutMany(shadowDelete bool) (chan<- record.Record, <-chan error) {
db.wg.Add(1)
defer db.wg.Done()
// Check if we should use prepared statement optimized inserting.
if UsePreparedStatements {
return db.putManyWithPreparedStmts(shadowDelete)
}
batch := make(chan record.Record, 100)
errs := make(chan error, 1)
tx, err := db.bob.BeginTx(db.ctx, nil)
if err != nil {
errs <- err
return batch, errs
}
// start handler
go func() {
// Read all put records.
writeBatch:
for {
select {
case r := <-batch:
if r != nil {
// Write record.
_, err := db.putRecord(r, &tx)
if err != nil {
errs <- err
break writeBatch
}
} else {
// Finalize transcation.
errs <- tx.Commit()
return
}
case <-db.ctx.Done():
break writeBatch
}
}
// Rollback transaction.
errs <- tx.Rollback()
}()
return batch, errs
}
// Delete deletes a record from the database.
func (db *SQLite) Delete(key string) error {
db.wg.Add(1)
defer db.wg.Done()
toDelete := &models.Record{Key: key}
return toDelete.Delete(db.ctx, db.bob)
}
// Query returns a an iterator for the supplied query.
func (db *SQLite) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) {
db.wg.Add(1)
defer db.wg.Done()
_, err := q.Check()
if err != nil {
return nil, fmt.Errorf("invalid query: %w", err)
}
queryIter := iterator.New()
go db.queryExecutor(queryIter, q, local, internal)
return queryIter, nil
}
func (db *SQLite) queryExecutor(queryIter *iterator.Iterator, q *query.Query, local, internal bool) {
db.wg.Add(1)
defer db.wg.Done()
// Build query.
var recordQuery *sqlite.ViewQuery[*models.Record, models.RecordSlice]
if q.DatabaseKeyPrefix() != "" {
recordQuery = models.Records.View.Query(
models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix() + "%"),
)
} else {
recordQuery = models.Records.View.Query()
}
// Get cursor to go over all records in the query.
cursor, err := models.RecordsQuery.Cursor(recordQuery, db.ctx, db.bob)
if err != nil {
queryIter.Finish(err)
return
}
defer func() {
_ = cursor.Close()
}()
recordsLoop:
for cursor.Next() {
// Get next record
r, cErr := cursor.Get()
if cErr != nil {
err = fmt.Errorf("cursor error: %w", cErr)
break recordsLoop
}
// Check if key matches.
if !q.MatchesKey(r.Key) {
continue recordsLoop
}
// Check Meta.
m := getMeta(r)
if !m.CheckValidity() ||
!m.CheckPermission(local, internal) {
continue recordsLoop
}
// Check Data.
if q.HasWhereCondition() {
if r.Format.IsNull() || r.Value.IsNull() {
continue recordsLoop
}
jsonData := string(r.Value.GetOrZero())
jsonAccess := accessor.NewJSONAccessor(&jsonData)
if !q.MatchesAccessor(jsonAccess) {
continue recordsLoop
}
}
// Build database record.
matched, _ := record.NewWrapperFromDatabase(
db.name,
r.Key,
m,
uint8(r.Format.GetOrZero()), //nolint:gosec // Values are within uint8.
r.Value.GetOrZero(),
)
select {
case <-queryIter.Done:
break recordsLoop
case queryIter.Next <- matched:
default:
select {
case <-queryIter.Done:
break recordsLoop
case queryIter.Next <- matched:
case <-time.After(1 * time.Second):
err = ErrQueryTimeout
break recordsLoop
}
}
}
queryIter.Finish(err)
}
// Purge deletes all records that match the given query. It returns the number of successful deletes and an error.
func (db *SQLite) Purge(ctx context.Context, q *query.Query, local, internal, shadowDelete bool) (int, error) {
db.wg.Add(1)
defer db.wg.Done()
// Optimize for local and internal queries without where clause and without shadow delete.
if local && internal && !shadowDelete && !q.HasWhereCondition() {
// First count entries (SQLite does not support affected rows)
n, err := models.Records.Query(
models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"),
).Count(db.ctx, db.bob)
if err != nil || n == 0 {
return int(n), err
}
// Delete entries.
_, err = models.Records.Delete(
models.DeleteWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"),
).Exec(db.ctx, db.bob)
return int(n), err
}
// Optimize for local and internal queries without where clause, but with shadow delete.
if local && internal && shadowDelete && !q.HasWhereCondition() {
// First count entries (SQLite does not support affected rows)
n, err := models.Records.Query(
models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"),
).Count(db.ctx, db.bob)
if err != nil || n == 0 {
return int(n), err
}
// Mark purged records as deleted.
now := time.Now().Unix()
_, err = models.Records.Update(
um.SetCol("format").ToArg(nil),
um.SetCol("value").ToArg(nil),
um.SetCol("deleted").ToArg(now),
models.UpdateWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"),
).Exec(db.ctx, db.bob)
return int(n), err
}
// Otherwise, iterate over all entries and delete matching ones.
// TODO: Non-local, non-internal or content matching queries are not supported at the moment.
return 0, storage.ErrNotImplemented
}
// PurgeOlderThan deletes all records last updated before the given time. It returns the number of successful deletes and an error.
func (db *SQLite) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time, local, internal, shadowDelete bool) (int, error) {
db.wg.Add(1)
defer db.wg.Done()
purgeBeforeInt := purgeBefore.Unix()
// Optimize for local and internal queries without where clause and without shadow delete.
if local && internal && !shadowDelete {
// First count entries (SQLite does not support affected rows)
n, err := models.Records.Query(
models.SelectWhere.Records.Key.Like(prefix+"%"),
models.SelectWhere.Records.Modified.LT(purgeBeforeInt),
).Count(db.ctx, db.bob)
if err != nil || n == 0 {
return int(n), err
}
// Delete entries.
_, err = models.Records.Delete(
models.DeleteWhere.Records.Key.Like(prefix+"%"),
models.DeleteWhere.Records.Modified.LT(purgeBeforeInt),
).Exec(db.ctx, db.bob)
return int(n), err
}
// Optimize for local and internal queries without where clause, but with shadow delete.
if local && internal && shadowDelete {
// First count entries (SQLite does not support affected rows)
n, err := models.Records.Query(
models.SelectWhere.Records.Key.Like(prefix+"%"),
models.SelectWhere.Records.Modified.LT(purgeBeforeInt),
).Count(db.ctx, db.bob)
if err != nil || n == 0 {
return int(n), err
}
// Mark purged records as deleted.
now := time.Now().Unix()
_, err = models.Records.Update(
um.SetCol("format").ToArg(nil),
um.SetCol("value").ToArg(nil),
um.SetCol("deleted").ToArg(now),
models.UpdateWhere.Records.Key.Like(prefix+"%"),
models.UpdateWhere.Records.Modified.LT(purgeBeforeInt),
).Exec(db.ctx, db.bob)
return int(n), err
}
// TODO: Non-local or non-internal queries are not supported at the moment.
return 0, storage.ErrNotImplemented
}
// ReadOnly returns whether the database is read only.
func (db *SQLite) ReadOnly() bool {
return false
}
// Injected returns whether the database is injected.
func (db *SQLite) Injected() bool {
return false
}
// MaintainRecordStates maintains records states in the database.
func (db *SQLite) MaintainRecordStates(ctx context.Context, purgeDeletedBefore time.Time, shadowDelete bool) error {
db.wg.Add(1)
defer db.wg.Done()
now := time.Now().Unix()
purgeThreshold := purgeDeletedBefore.Unix()
// Option 1: Using shadow delete.
if shadowDelete {
// Mark expired records as deleted.
_, err := models.Records.Update(
um.SetCol("format").ToArg(nil),
um.SetCol("value").ToArg(nil),
um.SetCol("deleted").ToArg(now),
models.UpdateWhere.Records.Deleted.EQ(0),
models.UpdateWhere.Records.Expires.GT(0),
models.UpdateWhere.Records.Expires.LT(now),
).Exec(db.ctx, db.bob)
if err != nil {
return fmt.Errorf("failed to shadow delete expired records: %w", err)
}
// Purge deleted records before threshold.
_, err = models.Records.Delete(
models.DeleteWhere.Records.Deleted.GT(0),
models.DeleteWhere.Records.Deleted.LT(purgeThreshold),
).Exec(db.ctx, db.bob)
if err != nil {
return fmt.Errorf("failed to purge deleted records (before threshold): %w", err)
}
return nil
}
// Option 2: Immediate delete.
// Delete expired record.
_, err := models.Records.Delete(
models.DeleteWhere.Records.Expires.GT(0),
models.DeleteWhere.Records.Expires.LT(now),
).Exec(db.ctx, db.bob)
if err != nil {
return fmt.Errorf("failed to delete expired records: %w", err)
}
// Delete shadow deleted records.
_, err = models.Records.Delete(
models.DeleteWhere.Records.Deleted.GT(0),
).Exec(db.ctx, db.bob)
if err != nil {
return fmt.Errorf("failed to purge deleted records: %w", err)
}
return nil
}
func (db *SQLite) Maintain(ctx context.Context) error {
db.wg.Add(1)
defer db.wg.Done()
// Remove up to about 100KB of SQLite pages from the freelist on every run.
// (Assuming 4KB page size.)
_, err := db.db.ExecContext(ctx, "PRAGMA incremental_vacuum(25);")
return err
}
func (db *SQLite) MaintainThorough(ctx context.Context) error {
db.wg.Add(1)
defer db.wg.Done()
// Remove all pages from the freelist.
_, err := db.db.ExecContext(ctx, "PRAGMA incremental_vacuum;")
return err
}
// Shutdown shuts down the database.
func (db *SQLite) Shutdown() error {
db.wg.Wait()
db.cancelCtx()
return db.bob.Close()
}
type statementLogger struct{}
func (sl statementLogger) Log(ctx context.Context, level sqldblogger.Level, msg string, data map[string]interface{}) {
fmt.Printf("SQL: %s --- %+v\n", msg, data)
}

View file

@ -0,0 +1,216 @@
package sqlite
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/safing/portmaster/base/database/query"
"github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/database/storage"
)
var (
// Compile time interface checks.
_ storage.Interface = &SQLite{}
_ storage.Batcher = &SQLite{}
_ storage.Purger = &SQLite{}
)
type TestRecord struct { //nolint:maligned
record.Base
sync.Mutex
S string
I int
I8 int8
I16 int16
I32 int32
I64 int64
UI uint
UI8 uint8
UI16 uint16
UI32 uint32
UI64 uint64
F32 float32
F64 float64
B bool
}
func TestSQLite(t *testing.T) {
t.Parallel()
// start
testDir := t.TempDir()
db, err := openSQLite("test", testDir, true)
if err != nil {
t.Fatal(err)
}
defer func() {
// shutdown
err = db.Shutdown()
if err != nil {
t.Fatal(err)
}
}()
a := &TestRecord{
S: "banana",
I: 42,
I8: 42,
I16: 42,
I32: 42,
I64: 42,
UI: 42,
UI8: 42,
UI16: 42,
UI32: 42,
UI64: 42,
F32: 42.42,
F64: 42.42,
B: true,
}
a.SetMeta(&record.Meta{})
a.Meta().Update()
a.SetKey("test:A")
// put record
_, err = db.Put(a)
if err != nil {
t.Fatal(err)
}
// get and compare
r1, err := db.Get("A")
if err != nil {
t.Fatal(err)
}
a1 := &TestRecord{}
err = record.Unwrap(r1, a1)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, a, a1, "struct must match")
// setup query test records
qA := &TestRecord{}
qA.SetKey("test:path/to/A")
qA.UpdateMeta()
qB := &TestRecord{}
qB.SetKey("test:path/to/B")
qB.UpdateMeta()
// Set creation/modification in the past.
qB.Meta().Created = time.Now().Add(-time.Hour).Unix()
qB.Meta().Modified = time.Now().Add(-time.Hour).Unix()
qC := &TestRecord{}
qC.SetKey("test:path/to/C")
qC.UpdateMeta()
// Set expiry in the past.
qC.Meta().Expires = time.Now().Add(-time.Hour).Unix()
qZ := &TestRecord{}
qZ.SetKey("test:z")
qZ.UpdateMeta()
put, errs := db.PutMany(false)
put <- qA
put <- qB
put <- qC
put <- qZ
close(put)
err = <-errs
if err != nil {
t.Fatal(err)
}
// test query
q := query.New("test:path/to/").MustBeValid()
it, err := db.Query(q, true, true)
if err != nil {
t.Fatal(err)
}
cnt := 0
for range it.Next {
cnt++
}
if it.Err() != nil {
t.Fatal(it.Err())
}
if cnt != 2 {
// Note: One is expired.
t.Fatalf("unexpected query result count: %d", cnt)
}
// delete
err = db.Delete("A")
if err != nil {
t.Fatal(err)
}
// check if its gone
_, err = db.Get("A")
if err == nil {
t.Fatal("should fail")
}
// purge older than
n, err := db.PurgeOlderThan(t.Context(), "path/to/", time.Now().Add(-30*time.Minute), true, true, false)
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("unexpected purge older than delete count: %d", n)
}
// maintenance
err = db.MaintainRecordStates(t.Context(), time.Now().Add(-time.Minute), true)
if err != nil {
t.Fatal(err)
}
// maintenance
err = db.MaintainRecordStates(t.Context(), time.Now(), false)
if err != nil {
t.Fatal(err)
}
// purge
n, err = db.Purge(t.Context(), query.New("test:path/to/").MustBeValid(), true, true, true)
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("unexpected purge delete count: %d", n)
}
// Maintenance
err = db.Maintain(t.Context())
if err != nil {
t.Fatalf("Maintain: %s", err)
}
err = db.MaintainThorough(t.Context())
if err != nil {
t.Fatalf("MaintainThorough: %s", err)
}
// test query
q = query.New("test").MustBeValid()
it, err = db.Query(q, true, true)
if err != nil {
t.Fatal(err)
}
cnt = 0
for range it.Next {
cnt++
}
if it.Err() != nil {
t.Fatal(it.Err())
}
if cnt != 1 {
t.Fatalf("unexpected query result count: %d", cnt)
}
}

View file

View file

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

View file

@ -74,6 +74,7 @@ func Set(setName string, setVersion string, setLicenseName string) {
if setVersion != "" {
version = setVersion
versionNumber = setVersion
}
}

View file

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

View file

@ -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
@ -98,7 +93,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{}

View file

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

View file

@ -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,17 @@ 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)
}
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)
// Set permissions.
err = SetDirPermission(path, perm)
// Ignore windows permission error. For none admin users it will always fail.
if !isWindows {
return err
}
return nil
}
// other error opening path
return fmt.Errorf("failed to access %s: %w", path, err)

20
base/utils/permissions.go Normal file
View file

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

View file

@ -0,0 +1,79 @@
//go:build windows
package utils
import (
"github.com/hectane/go-acl"
"golang.org/x/sys/windows"
)
var (
systemSID *windows.SID
adminsSID *windows.SID
usersSID *windows.SID
)
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-18") // SYSTEM (Local 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 {
SetFilePermission(path, perm)
return nil
}
// SetExecPermission sets the permission of an executable file.
func SetExecPermission(path string, perm FSPermission) error {
SetFilePermission(path, perm)
return nil
}
// 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.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.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.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),
)
}
}

View file

@ -2,25 +2,61 @@ package utils
import (
"fmt"
"os"
"io/fs"
"path/filepath"
"strings"
"sync"
)
type FSPermission uint8
const (
AdminOnlyPermission FSPermission = iota
PublicReadPermission
PublicWritePermission
)
// AsUnixDirExecPermission 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
}
// 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 0o644
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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@
<div class="flex justify-between items-center p-4 px-12 text-xxs">
<!-- Breadcrumbs -->
<div class="flex items-center">
<a class="text-secondary hover:text-primary" [routerLink]="['/app/overview']">Apps</a>
<div class="cursor-pointer text-secondary hover:text-primary" [routerLink]="['/app/overview']">Apps</div>
<svg viewBox="0 0 24 24" class="inline-block w-4 h-4 text-secondary">
<g fill="none" class="inner" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.2" d="M10 16l4-4-4-4" />

View file

@ -27,9 +27,15 @@ if (typeof (CSS as any)['registerProperty'] === 'function') {
}
function handleExternalResources(e: Event) {
// TODO:
// This code executes "openExternal()" when any "<a />" element in the app is clicked.
// This could potentially be a security issue.
// We should consider restricting this to only external links that belong to a certain domain (e.g., https://safing.io).
// get click target
let target: HTMLElement | null = e.target as HTMLElement;
// traverse until we reach an a tag
// traverse until we reach element "<a />"
while (!!target && target.tagName !== "A") {
target = target.parentElement;
}

109
go.mod
View file

@ -1,6 +1,8 @@
module github.com/safing/portmaster
go 1.22.0
go 1.24
toolchain go1.24.0
// TODO: Remove when https://github.com/tc-hib/winres/pull/4 is released.
replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2
@ -8,14 +10,15 @@ replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2
require (
fyne.io/systray v1.11.0
github.com/VictoriaMetrics/metrics v1.35.1
github.com/Xuanwo/go-locale v1.1.2
github.com/Xuanwo/go-locale v1.1.1
github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0
github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6
github.com/agext/levenshtein v1.2.3
github.com/armon/go-radix v1.0.0
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 +37,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
@ -46,12 +49,15 @@ require (
github.com/oschwald/maxminddb-golang v1.13.1
github.com/r3labs/diff/v3 v3.0.1
github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5
github.com/rubenv/sql-migrate v1.7.1
github.com/safing/jess v0.3.5
github.com/safing/structures v1.1.0
github.com/safing/structures v1.2.0
github.com/seehuhn/fortuna v1.0.1
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551
github.com/spf13/cobra v1.8.1
github.com/spkg/zipfs v0.7.1
github.com/stephenafamo/bob v0.30.0
github.com/stretchr/testify v1.9.0
github.com/tannerryan/ring v1.1.2
github.com/tc-hib/winres v0.3.1
@ -61,69 +67,120 @@ require (
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
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
go.etcd.io/bbolt v1.3.10
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
golang.org/x/image v0.19.0
golang.org/x/net v0.28.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
modernc.org/sqlite v1.32.0
zombiezen.com/go/sqlite v1.3.0
)
require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
github.com/aead/ecdh v0.2.0 // indirect
github.com/alessio/shellescape v1.4.2 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/danieljoos/wincred v1.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/denisenkom/go-mssqldb v0.9.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d // indirect
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/godbus/dbus v4.1.0+incompatible // indirect
github.com/godror/godror v0.40.4 // indirect
github.com/godror/knownpb v0.1.1 // indirect
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // 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/glog v0.0.0-20160126235308-23def4e6c14b // 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
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect
github.com/knadh/koanf/providers/confmap v0.1.0 // indirect
github.com/knadh/koanf/providers/env v0.1.0 // indirect
github.com/knadh/koanf/providers/file v0.1.0 // indirect
github.com/knadh/koanf/v2 v2.1.0 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/mattn/go-oci8 v0.1.1 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-sqlite3 v1.14.19 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/cli v1.1.5 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/seehuhn/sha256d v1.0.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stephenafamo/scan v0.6.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/urfave/cli/v2 v2.23.7 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/volatiletech/inflect v0.0.1 // indirect
github.com/volatiletech/strmangle v0.0.6 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // 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/mod v0.22.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/tools v0.27.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.61.2 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.59.9 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.34.1 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
mvdan.cc/gofumpt v0.5.0 // indirect
)
tool (
github.com/rubenv/sql-migrate/sql-migrate
github.com/stephenafamo/bob/gen/bobgen-sqlite
)

278
go.sum
View file

@ -1,28 +1,49 @@
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=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/UNO-SOFT/zlog v0.8.1 h1:TEFkGJHtUfTRgMkLZiAjLSHALjwSBdw6/zByMC5GJt4=
github.com/UNO-SOFT/zlog v0.8.1/go.mod h1:yqFOjn3OhvJ4j7ArJqQNA+9V+u6t9zSAyIZdWdMweWc=
github.com/VictoriaMetrics/metrics v1.35.1 h1:o84wtBKQbzLdDy14XeskkCZih6anG+veZ1SwJHFGwrU=
github.com/VictoriaMetrics/metrics v1.35.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
github.com/Xuanwo/go-locale v1.1.2 h1:6H+olvrQcyVOZ+GAC2rXu4armacTT4ZrFCA0mB24XVo=
github.com/Xuanwo/go-locale v1.1.2/go.mod h1:1JBER4QV7Ji39GJ4AvVlfvqmTUqopzxQxdg2mXYOw94=
github.com/Xuanwo/go-locale v1.1.1 h1:nhvzo1phY4LRwdrwVwKWXn5iZ0pMwwsa3o29yiDRuZc=
github.com/Xuanwo/go-locale v1.1.1/go.mod h1:ldC3FzZeMYALkL3YYpwhr4iVYdOIUx42kORcnAHdKUo=
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf h1:+edM69bH/X6JpYPmJYBRLanAMe1V5yRXYU3hHUovGcE=
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf/go.mod h1:FZqLhJSj2tg0ZN48GB1zvj00+ZYcHPqgsC7yzcgCq6k=
github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0 h1:vLrhbOWVPxtHao/QthU8pcpI4DbtSGnWgH7qIJf8F6k=
github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0/go.mod h1:l4/5NZtYd/SIohsFhaJQQe+sPOTG22furpZ5FvcYOzk=
github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
github.com/aead/ecdh v0.2.0/go.mod h1:a9HHtXuSo8J1Js1MwLQx2mBhkXMT6YwUmVVEY4tTB8U=
github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpYk2gxGJnDjsYuboNTcRmbtGKGs=
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/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E=
github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
@ -34,20 +55,23 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
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=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
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 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
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/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk=
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
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=
@ -62,6 +86,9 @@ github.com/dhaavi/winres v0.2.2/go.mod h1:1NTs+/DtKP1BplIL1+XQSoq4X1PUfLczexS7gf
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
@ -72,33 +99,54 @@ github.com/florianl/go-nfqueue v1.3.2/go.mod h1:eSnAor2YCfMCVYrVNEhkLGN/r1L+J4uD
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk=
github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI=
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d h1:QQP1nE4qh5aHTGvI1LgOFxZYVxYoGeMfbNHikogPyoA=
github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godror/godror v0.40.4 h1:X1e7hUd02GDaLWKZj40Z7L0CP0W9TrGgmPQZw6+anBg=
github.com/godror/godror v0.40.4/go.mod h1:i8YtVTHUJKfFT3wTat4A9UoqScUtZXiYB9Rf3SVARgc=
github.com/godror/knownpb v0.1.1 h1:A4J7jdx7jWBhJm18NntafzSC//iZDHkDi1+juwQ5pTI=
github.com/godror/knownpb v0.1.1/go.mod h1:4nRFbQo1dDuwKnblRXDxrfCFYeT4hjg3GjMqef58eRE=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
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 h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
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=
@ -118,8 +166,12 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
@ -131,14 +183,25 @@ github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:Fecb
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb h1:PGufWXXDq9yaev6xX1YQauaO1MV90e6Mpoq1I7Lz/VM=
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@ -161,8 +224,20 @@ github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786 h1:N527AHMa79
github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs=
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8=
github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -171,23 +246,33 @@ 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/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
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.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
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.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
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/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM=
github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/ethtool v0.0.0-20211028163843-288d040e9d60/go.mod h1:aYbhishWc4Ai3I2U4Gaa2n3kHWSwzme6EsG/46HRQbE=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
@ -211,9 +296,11 @@ 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/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng=
github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -221,6 +308,7 @@ github.com/mitchellh/go-server-timing v1.0.1 h1:f00/aIe8T3MrnLhQHu3tSWvnwc5GV/p5
github.com/mitchellh/go-server-timing v1.0.1/go.mod h1:Mo6GKi9FSLwWFAMn3bqVPWe20y5ri5QGQuO9D9MCOxk=
github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
@ -229,6 +317,12 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@ -238,6 +332,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc=
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4=
github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg=
github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@ -246,12 +347,15 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5 h1:R/qQ2Hw5/BgVQS87pg/mRTep8RxqDY0rcj4pbpBvNF8=
github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5/go.mod h1:NTdpGnZ/E2cKXTiAz824w1p6OIm0mBbXcyuiYPCi/Ps=
github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4=
github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safing/jess v0.3.5 h1:KS5elTKfWcDUow8SUoCj5QdyyGJNoExJNySerNkbxUU=
github.com/safing/jess v0.3.5/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI=
github.com/safing/structures v1.1.0 h1:QzHBQBjaZSLzw2f6PM4ibSmPcfBHAOB5CKJ+k4FYkhQ=
github.com/safing/structures v1.1.0/go.mod h1:QUrB74FcU41ahQ5oy3YNFCoSq+twE/n3+vNZc2K35II=
github.com/safing/structures v1.2.0 h1:S6EzKxxGYTO6P9P3Dkab9gisLOrfAyvy7JzFOUSkOUk=
github.com/safing/structures v1.2.0/go.mod h1:zIun7mz3xV0dJ3vXRZuU71ATzT8D/0hGJO+u+bk5Kvs=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/seehuhn/fortuna v1.0.1 h1:lu9+CHsmR0bZnx5Ay646XvCSRJ8PJTi5UYJwDBX68H0=
@ -260,12 +364,20 @@ github.com/seehuhn/sha256d v1.0.0 h1:TXTsAuEWr02QjRm153Fnvvb6fXXDo7Bmy1FizxarGYw
github.com/seehuhn/sha256d v1.0.0/go.mod h1:PEuxg9faClSveVuFXacQmi+NtDI/PX8bpKjtNzf2+s4=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551 h1:+EXKKt7RC4HyE/iE8zSeFL+7YBL8Z7vpBaEE3c7lCnk=
github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551/go.mod h1:ztTX0ctjRZ1wn9OXrzhonvNmv43yjFUXJYJR95JQAJE=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
@ -279,12 +391,21 @@ github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spkg/zipfs v0.7.1 h1:+2X5lvNHTybnDMQZAIHgedRXZK1WXdc+94R/P5v2XWE=
github.com/spkg/zipfs v0.7.1/go.mod h1:48LW+/Rh1G7aAav1ew1PdlYn52T+LM+ARmSHfDNJvg8=
github.com/stephenafamo/bob v0.30.0 h1:pGCS3iMh1xr2xlAP20eBV6gmqun6CCH/05l7uJJfvJI=
github.com/stephenafamo/bob v0.30.0/go.mod h1:0z9AeWTOTJmGsokEtQReTEJry4iI9J+SCyKMcr40mOo=
github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97 h1:XItoZNmhOih06TC02jK7l3wlpZ0XT/sPQYutDcGOQjg=
github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97/go.mod h1:bM3Vmw1IakoaXocHmMIGgJFYob0vuK+CFWiJHQvz0jQ=
github.com/stephenafamo/scan v0.6.1 h1:nXokGCQwYazMuyvdNAoK0T8Z76FWcpMvDdtengpz6PU=
github.com/stephenafamo/scan v0.6.1/go.mod h1:FhIUJ8pLNyex36xGFiazDJJ5Xry0UkAi+RkWRrEcRMg=
github.com/stephenafamo/sqlparser v0.0.0-20241111104950-b04fa8a26c9c h1:JFga++XBnZG2xlnvQyHJkeBWZ9G9mGdtgvLeSRbp/BA=
github.com/stephenafamo/sqlparser v0.0.0-20241111104950-b04fa8a26c9c/go.mod h1:4iveRk8mkzQZxDuK/W0MGLrGmu/igyDYWNDD4a6v0r0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@ -304,11 +425,13 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 h1:UFHFmFfixpmfRBcxuu+LA9l8MdURWVdVNUHxO5n1d2w=
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26/go.mod h1:IGhd0qMDsUa9acVjsbsT7bu3ktadtGOHI79+idTew/M=
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
@ -322,41 +445,54 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU=
github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA=
github.com/volatiletech/strmangle v0.0.6 h1:AdOYE3B2ygRDq4rXDij/MMwq6KVK/pWAYxpC7CLrkKQ=
github.com/volatiletech/strmangle v0.0.6/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/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-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
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=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
@ -378,15 +514,18 @@ 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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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=
@ -397,7 +536,9 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -420,34 +561,44 @@ 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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.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/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
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=
@ -455,26 +606,31 @@ google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7ya
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.20.7 h1:skrinQsjxWfvj6nbC3ztZPJy+NuwmB3hV9zX/pthNYQ=
modernc.org/ccgo/v4 v4.20.7/go.mod h1:UOkI3JSG2zT4E2ioHlncSOZsXbuDCZLvPi3uMlZT5GY=
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/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.59.9 h1:k+nNDDakwipimgmJ1D9H466LhFeSkaPPycAs1OZiDmY=
modernc.org/libc v1.59.9/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A=
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=
@ -483,11 +639,13 @@ 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/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
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=
zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs=
zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY=

View file

@ -1,43 +1,52 @@
package base
import (
"path/filepath"
"github.com/safing/portmaster/base/database"
_ "github.com/safing/portmaster/base/database/dbmodule"
_ "github.com/safing/portmaster/base/database/storage/bbolt"
_ "github.com/safing/portmaster/base/database/storage/sqlite"
"github.com/safing/portmaster/base/dataroot"
"github.com/safing/portmaster/base/utils"
)
// Default Values (changeable for testing).
var (
DefaultDatabaseStorageType = "bbolt"
DefaultDatabaseStorageType = "sqlite"
)
func registerDatabases() error {
// If there is an existing bbolt core database, use it instead.
coreStorageType := DefaultDatabaseStorageType
if utils.PathExists(filepath.Join(dataroot.Root().Path, database.DatabasesSubDir, "core", "bbolt")) {
coreStorageType = "bbolt"
}
// Register core database.
_, err := database.Register(&database.Database{
Name: "core",
Description: "Holds core data, such as settings and profiles",
StorageType: DefaultDatabaseStorageType,
StorageType: coreStorageType,
})
if err != nil {
return err
}
// If there is an existing cache bbolt database, use it instead.
cacheStorageType := DefaultDatabaseStorageType
if utils.PathExists(filepath.Join(dataroot.Root().Path, database.DatabasesSubDir, "cache", "bbolt")) {
cacheStorageType = "bbolt"
}
// Register cache database.
_, err = database.Register(&database.Database{
Name: "cache",
Description: "Cached data, such as Intelligence and DNS Records",
StorageType: DefaultDatabaseStorageType,
StorageType: cacheStorageType,
})
if err != nil {
return err
}
// _, err = database.Register(&database.Database{
// Name: "history",
// Description: "Historic event data",
// StorageType: DefaultDatabaseStorageType,
// })
// if err != nil {
// return err
// }
return nil
}

View file

@ -6,8 +6,10 @@ import (
"fmt"
"github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/database/dbmodule"
"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,10 +56,13 @@ func prep(instance instance) error {
}
// initialize structure
err := dataroot.Initialize(dataDir, 0o0755)
err := dataroot.Initialize(dataDir, utils.PublicReadPermission)
if err != nil {
return err
}
// Set root dir for the databases.
dbmodule.SetDatabaseLocation(dataroot.Root())
}
// set api listen address

View file

@ -14,7 +14,7 @@ import (
)
type ETWSession struct {
i integration.ETWFunctions
i *integration.ETWFunctions
shutdownGuard atomic.Bool
shutdownMutex sync.Mutex
@ -22,8 +22,11 @@ type ETWSession struct {
state uintptr
}
// NewSession creates new ETW event listener and initilizes it. This is a low level interface, make sure to call DestorySession when you are done using it.
func NewSession(etwInterface integration.ETWFunctions, callback func(domain string, result string)) (*ETWSession, error) {
// NewSession creates new ETW event listener and initializes it. This is a low level interface, make sure to call DestroySession when you are done using it.
func NewSession(etwInterface *integration.ETWFunctions, callback func(domain string, pid uint32, result string)) (*ETWSession, error) {
if etwInterface == nil {
return nil, fmt.Errorf("etw interface was nil")
}
etwSession := &ETWSession{
i: etwInterface,
}
@ -32,8 +35,8 @@ func NewSession(etwInterface integration.ETWFunctions, callback func(domain stri
_ = etwSession.i.StopOldSession()
// Initialize notification activated callback
win32Callback := windows.NewCallback(func(domain *uint16, result *uint16) uintptr {
callback(windows.UTF16PtrToString(domain), windows.UTF16PtrToString(result))
win32Callback := windows.NewCallback(func(domain *uint16, pid uint32, result *uint16) uintptr {
callback(windows.UTF16PtrToString(domain), pid, windows.UTF16PtrToString(result))
return 0
})
// The function only allocates memory it will not fail.
@ -47,7 +50,7 @@ func NewSession(etwInterface integration.ETWFunctions, callback func(domain stri
// Initialize session.
err := etwSession.i.InitializeSession(etwSession.state)
if err != nil {
return nil, fmt.Errorf("failed to initialzie session: %q", err)
return nil, fmt.Errorf("failed to initialize session: %q", err)
}
return etwSession, nil
@ -65,6 +68,10 @@ func (l *ETWSession) IsRunning() bool {
// FlushTrace flushes the trace buffer.
func (l *ETWSession) FlushTrace() error {
if l.i == nil {
return fmt.Errorf("session not initialized")
}
l.shutdownMutex.Lock()
defer l.shutdownMutex.Unlock()
@ -76,13 +83,16 @@ func (l *ETWSession) FlushTrace() error {
return l.i.FlushTrace(l.state)
}
// StopTrace stopes the trace. This will cause StartTrace to return.
// StopTrace stops the trace. This will cause StartTrace to return.
func (l *ETWSession) StopTrace() error {
return l.i.StopTrace(l.state)
}
// DestroySession closes the session and frees the allocated memory. Listener cannot be used after this function is called.
func (l *ETWSession) DestroySession() error {
if l.i == nil {
return fmt.Errorf("session not initialized")
}
l.shutdownMutex.Lock()
defer l.shutdownMutex.Unlock()

View file

@ -141,5 +141,5 @@ func (l *Listener) processAnswer(domain string, queryResult *QueryResult) {
}
}
saveDomain(domain, ips, cnames)
saveDomain(domain, ips, cnames, resolver.IPInfoProfileScopeGlobal)
}

View file

@ -4,6 +4,7 @@
package dnsmonitor
import (
"context"
"fmt"
"net"
"strconv"
@ -11,6 +12,7 @@ import (
"github.com/miekg/dns"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/process"
"github.com/safing/portmaster/service/resolver"
)
@ -23,22 +25,38 @@ func newListener(module *DNSMonitor) (*Listener, error) {
ResolverInfo.Source = resolver.ServerSourceETW
listener := &Listener{}
var err error
// Initialize new dns event session.
listener.etw, err = NewSession(module.instance.OSIntegration().GetETWInterface(), listener.processEvent)
err := initializeSessions(module, listener)
if err != nil {
return nil, err
// Listen for event if the dll has been loaded
module.instance.OSIntegration().OnInitializedEvent.AddCallback("loader-listener", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
err = initializeSessions(module, listener)
if err != nil {
return false, err
}
return true, nil
})
}
// Start listening for events.
module.mgr.Go("etw-dns-event-listener", func(w *mgr.WorkerCtx) error {
return listener.etw.StartTrace()
})
return listener, nil
}
func initializeSessions(module *DNSMonitor, listener *Listener) error {
var err error
listener.etw, err = NewSession(module.instance.OSIntegration().GetETWInterface(), listener.processEvent)
if err != nil {
return err
}
// Start listener
module.mgr.Go("etw-dns-event-listener", func(w *mgr.WorkerCtx) error {
return listener.etw.StartTrace()
})
return nil
}
func (l *Listener) flush() error {
if l.etw == nil {
return fmt.Errorf("etw not initialized")
}
return l.etw.FlushTrace()
}
@ -63,7 +81,7 @@ func (l *Listener) stop() error {
return nil
}
func (l *Listener) processEvent(domain string, result string) {
func (l *Listener) processEvent(domain string, pid uint32, result string) {
if processIfSelfCheckDomain(dns.Fqdn(domain)) {
// Not need to process result.
return
@ -74,6 +92,15 @@ func (l *Listener) processEvent(domain string, result string) {
return
}
profileScope := resolver.IPInfoProfileScopeGlobal
// Get the profile ID if the process can be found
if proc, err := process.GetOrFindProcess(context.Background(), int(pid)); err == nil {
if profile := proc.Profile(); profile != nil {
if localProfile := profile.LocalProfile(); localProfile != nil {
profileScope = localProfile.ID
}
}
}
cnames := make(map[string]string)
ips := []net.IP{}
@ -99,5 +126,5 @@ func (l *Listener) processEvent(domain string, result string) {
}
}
}
saveDomain(domain, ips, cnames)
saveDomain(domain, ips, cnames, profileScope)
}

View file

@ -61,7 +61,7 @@ func (dl *DNSMonitor) Flush() error {
return dl.listener.flush()
}
func saveDomain(domain string, ips []net.IP, cnames map[string]string) {
func saveDomain(domain string, ips []net.IP, cnames map[string]string, profileScope string) {
fqdn := dns.Fqdn(domain)
// Create new record for this IP.
record := resolver.ResolvedDomain{
@ -75,7 +75,7 @@ func saveDomain(domain string, ips []net.IP, cnames map[string]string) {
record.AddCNAMEs(cnames)
// Add to cache
saveIPsInCache(ips, resolver.IPInfoProfileScopeGlobal, record)
saveIPsInCache(ips, profileScope, record)
}
func New(instance instance) (*DNSMonitor, error) {

View file

@ -107,6 +107,7 @@ func Handler(ctx context.Context, packets chan packet.Packet, bandwidthUpdate ch
newPacket := &Packet{
verdictRequest: conn.ID,
payload: conn.Payload,
payloadLayer: conn.PayloadLayer,
verdictSet: abool.NewBool(false),
}
info := newPacket.Info()

View file

@ -19,8 +19,8 @@ type ETWFunctions struct {
stopOldSession *windows.Proc
}
func initializeETW(dll *windows.DLL) (ETWFunctions, error) {
var functions ETWFunctions
func initializeETW(dll *windows.DLL) (*ETWFunctions, error) {
functions := &ETWFunctions{}
var err error
functions.createState, err = dll.FindProc("PM_ETWCreateState")
if err != nil {

View file

@ -5,24 +5,55 @@ package integration
import (
"fmt"
"sync"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates"
"golang.org/x/sys/windows"
)
type OSSpecific struct {
dll *windows.DLL
etwFunctions ETWFunctions
etwFunctions *ETWFunctions
}
// Initialize loads the dll and finds all the needed functions from it.
func (i *OSIntegration) Initialize() error {
// Try to load dll
err := i.loadDLL()
if err != nil {
log.Errorf("integration: failed to load dll: %s", err)
callbackLock := sync.Mutex{}
// listen for event from the updater and try to load again if any.
i.instance.Updates().EventResourcesUpdated.AddCallback("core-dll-loader", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
// Make sure no multiple callas are executed at the same time.
callbackLock.Lock()
defer callbackLock.Unlock()
// Try to load again.
err = i.loadDLL()
if err != nil {
log.Errorf("integration: failed to load dll: %s", err)
} else {
log.Info("integration: initialize successful after updater event")
}
return false, nil
})
} else {
log.Info("integration: initialize successful")
}
return nil
}
func (i *OSIntegration) loadDLL() error {
// Find path to the dll.
file, err := updates.GetFile("portmaster-core.dll")
file, err := updates.GetPlatformFile("dll/portmaster-core.dll")
if err != nil {
return err
}
// Load the DLL.
i.os.dll, err = windows.LoadDLL(file.Path())
if err != nil {
@ -35,10 +66,13 @@ func (i *OSIntegration) Initialize() error {
return err
}
// Notify listeners
i.OnInitializedEvent.Submit(struct{}{})
return nil
}
// CleanUp releases any resourses allocated during initializaion.
// CleanUp releases any resources allocated during initialization.
func (i *OSIntegration) CleanUp() error {
if i.os.dll != nil {
return i.os.dll.Release()
@ -46,7 +80,7 @@ func (i *OSIntegration) CleanUp() error {
return nil
}
// GetETWInterface return struct containing all the ETW related functions.
func (i *OSIntegration) GetETWInterface() ETWFunctions {
// GetETWInterface return struct containing all the ETW related functions, and nil if it was not loaded yet
func (i *OSIntegration) GetETWInterface() *ETWFunctions {
return i.os.etwFunctions
}

View file

@ -7,8 +7,9 @@ import (
// OSIntegration module provides special integration with the OS.
type OSIntegration struct {
m *mgr.Manager
states *mgr.StateMgr
m *mgr.Manager
OnInitializedEvent *mgr.EventMgr[struct{}]
//nolint:unused
os OSSpecific
@ -20,10 +21,9 @@ type OSIntegration struct {
func New(instance instance) (*OSIntegration, error) {
m := mgr.New("OSIntegration")
module := &OSIntegration{
m: m,
states: m.NewStateMgr(),
instance: instance,
m: m,
OnInitializedEvent: mgr.NewEventMgr[struct{}]("on-initialized", m),
instance: instance,
}
return module, nil

View file

@ -397,28 +397,32 @@ func (e *Entity) getDomainLists(ctx context.Context) {
}
func splitDomain(domain string) []string {
domain = strings.Trim(domain, ".")
suffix, _ := publicsuffix.PublicSuffix(domain)
if suffix == domain {
// Get suffix.
d := strings.TrimSuffix(domain, ".")
suffix, icann := publicsuffix.PublicSuffix(d)
if suffix == d {
return []string{domain}
}
domainWithoutSuffix := domain[:len(domain)-len(suffix)]
domainWithoutSuffix = strings.Trim(domainWithoutSuffix, ".")
splitted := strings.FieldsFunc(domainWithoutSuffix, func(r rune) bool {
// Split all subdomain into labels.
labels := strings.FieldsFunc(d[:len(d)-len(suffix)], func(r rune) bool {
return r == '.'
})
domains := make([]string, 0, len(splitted))
for idx := range splitted {
d := strings.Join(splitted[idx:], ".") + "." + suffix
if d[len(d)-1] != '.' {
d += "."
}
domains = append(domains, d)
// Build list of all domains up to the public suffix.
domains := make([]string, 0, len(labels)+1)
for idx := range labels {
domains = append(
domains,
strings.Join(labels[idx:], ".")+"."+suffix+".",
)
}
// If the suffix is not a real TLD, but a public suffix, add it to the list.
if !icann {
domains = append(domains, suffix+".")
}
return domains
}

View file

@ -0,0 +1,33 @@
package intel
import (
"testing"
"github.com/stretchr/testify/assert"
)
var splitDomainTestCases = [][]string{
// Regular registered domains and subdomains.
{"example.com."},
{"www.example.com.", "example.com."},
{"sub.domain.example.com.", "domain.example.com.", "example.com."},
{"example.co.uk."},
{"www.example.co.uk.", "example.co.uk."},
// TLD or public suffix: Return as is.
{"com."},
{"googleapis.com."},
// Public suffix domains: Return including public suffix.
{"test.googleapis.com.", "googleapis.com."},
{"sub.domain.googleapis.com.", "domain.googleapis.com.", "googleapis.com."},
}
func TestSplitDomain(t *testing.T) {
t.Parallel()
for _, testCase := range splitDomainTestCases {
splitted := splitDomain(testCase[0])
assert.Equal(t, testCase, splitted, "result must match")
}
}

View file

@ -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
@ -146,7 +146,7 @@ func persistRecords(startJob func(func() error), records <-chan record.Record) {
timePerEntity := time.Since(start) / time.Duration(cnt)
speed := float64(time.Second) / float64(timePerEntity)
log.Debugf("processed %d entities in %s with %s / entity (%.2f entities/second)", cnt, time.Since(start), timePerEntity, speed)
log.Debugf("intel/filterlists: processed %d entities in %s with %s / entity (%.2f entities/second)", cnt, time.Since(start), timePerEntity, speed)
}
batch := database.NewInterface(&database.Options{Local: true, Internal: true})

View file

@ -12,6 +12,7 @@ import (
"github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/database/query"
"github.com/safing/portmaster/base/database/storage"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/updater"
"github.com/safing/portmaster/service/mgr"
@ -158,9 +159,25 @@ func performUpdate(ctx context.Context) error {
func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error {
log.Debugf("intel/filterlists: cleanup task started, removing obsolete filter list entries ...")
n, err := cache.Purge(wc.Ctx(), query.New(filterListKeyPrefix).Where(
// TODO(ppacher): remember the timestamp we started the last update
// and use that rather than "one hour ago"
// TODO: Remember the timestamp we started the last update and use that rather than "one hour ago".
// First try to purge with PurgeOlderThan.
n, err := cache.PurgeOlderThan(wc.Ctx(), filterListKeyPrefix, time.Now().Add(-time.Hour))
switch {
case err == nil:
// Success!
log.Debugf("intel/filterlists: successfully removed %d obsolete entries", n)
return nil
case errors.Is(err, database.ErrNotImplemented) || errors.Is(err, storage.ErrNotImplemented):
// Try next method.
default:
// Return error.
return err
}
// Try with regular purge.
n, err = cache.Purge(wc.Ctx(), query.New(filterListKeyPrefix).Where(
query.Where("UpdatedAt", query.LessThan, time.Now().Add(-time.Hour).Unix()),
))
if err != nil {

View file

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

View file

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

View file

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

View file

@ -538,8 +538,9 @@ func (conn *Connection) GatherConnectionInfo(pkt packet.Packet) (err error) {
// Find domain and DNS context of entity.
if conn.Entity.Domain == "" && conn.process.Profile() != nil {
profileScope := conn.process.Profile().LocalProfile().ID
// check if we can find a domain for that IP
ipinfo, err := resolver.GetIPInfo(conn.process.Profile().LocalProfile().ID, pkt.Info().RemoteIP().String())
ipinfo, err := resolver.GetIPInfo(profileScope, pkt.Info().RemoteIP().String())
if err != nil {
// Try again with the global scope, in case DNS went through the system resolver.
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
@ -550,7 +551,18 @@ func (conn *Connection) GatherConnectionInfo(pkt packet.Packet) (err error) {
if module.instance.Resolver().IsDisabled() && conn.shouldWaitForDomain() {
// Flush the dns listener buffer and try again.
for i := range 4 {
_ = module.instance.DNSMonitor().Flush()
err = module.instance.DNSMonitor().Flush()
if err != nil {
// Error flushing, dont try again.
break
}
// Try with profile scope
ipinfo, err = resolver.GetIPInfo(profileScope, pkt.Info().RemoteIP().String())
if err == nil {
log.Tracer(pkt.Ctx()).Debugf("network: found domain with scope (%s) from dnsmonitor after %d tries", profileScope, +1)
break
}
// Try again with the global scope
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
if err == nil {
log.Tracer(pkt.Ctx()).Debugf("network: found domain from dnsmonitor after %d tries", i+1)

View file

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

View file

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

View file

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

View file

@ -52,6 +52,7 @@ func MandatoryUpdates() (identifiers []string) {
identifiers = append(
identifiers,
PlatformIdentifier("core/portmaster-core.exe"),
PlatformIdentifier("dll/portmaster-core.dll"),
PlatformIdentifier("kext/portmaster-kext.sys"),
PlatformIdentifier("kext/portmaster-kext.pdb"),
PlatformIdentifier("start/portmaster-start.exe"),

View file

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

View file

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

View file

@ -71,7 +71,8 @@ func getStickiedHub(conn *network.Connection) (sticksTo *stickyHub) {
// If the IP did not stick and we have a domain, check if that sticks.
if sticksTo == nil && conn.Entity.Domain != "" {
sticksTo, ok := stickyDomains[makeStickyDomainKey(conn)]
var ok bool
sticksTo, ok = stickyDomains[makeStickyDomainKey(conn)]
if ok && !sticksTo.isExpired() {
sticksTo.LastSeen = time.Now()
}
@ -146,6 +147,8 @@ func (t *Tunnel) avoidDestinationHub() {
Avoid: true,
}
log.Warningf("spn/crew: avoiding %s for %s", t.dstPin.Hub, ipKey)
// TODO: Question: Should we avoid the domain as well? (stickyDomains)
}
func cleanStickyHubs(ctx *mgr.WorkerCtx) error {

25
spn/testing/README.md Normal file
View file

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

View file

@ -0,0 +1,50 @@
# 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 --spn-map test --bootstrap-file ./testdata/shared/bootstrap.dsd
```
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
```

41
spn/testing/simple/clientsim.sh Executable file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,17 @@
{
"BootstrapHubs": [
],
"TrustedHubs": [
"ZwrY9G9HDo1J3qQrrQs8VF2KD99bj7KyWesJ5kWFUDBU6r",
"Zwj56ZFXrsud8gc1Rw3zuxRwMLhGkwvtvnTxCVtJ8EWLhQ",
"ZwpdW87ityD9i3N9x8oweCJnbZEqo346VBg4mCsCvTr1Zo",
"ZwpJ6ebddk1sccUVpo92JUqicBfKzBN2w4pEGoEY7UsNhX",
"Zwte3Jffp9PWmeWfrn8RyGuvZZFCg3v7XR3tpQjdo9TpVt",
"ZwrTcdiPF5zR5h9q9EdjHCrrXzYVBdQe5HmEYUWXdLkke3",
"Zwv7tSn5iU6bYKn53NaGCxPtL8vSxSK7F9VdQezDaDCLBt",
"Zwvtdq3K9knP9iNaRS1Ju8CETWTqy7oRwbScjBtJGBpqhB"
],
"AdviseOnlyTrustedHubs": true,
"AdviseOnlyTrustedHomeHubs": true,
"AdviseOnlyTrustedDestinationHubs": true
}

42
spn/testing/simple/join.sh Executable file
View file

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

View file

@ -0,0 +1,7 @@
#!/bin/bash
cd "$( dirname "${BASH_SOURCE[0]}" )"
rm -rf testdata/me/*
rm -rf testdata/shared/*
rm -rf testdata/hub*/databases

52
spn/testing/simple/run.sh Executable file
View file

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

15
spn/testing/simple/stop.sh Executable file
View file

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

View file

@ -22,7 +22,7 @@ static const GUID PORTMASTER_ETW_SESSION_GUID = {
#define LOGSESSION_NAME L"PortmasterDNSEventListener"
// Fuction type of the callback that will be called on each event.
typedef uint64_t(*GoEventRecordCallback)(wchar_t* domain, wchar_t* result);
typedef uint64_t(*GoEventRecordCallback)(wchar_t* domain, uint32_t pid, wchar_t* result);
// Holds the state of the ETW Session.
struct ETWSessionState {
@ -41,7 +41,7 @@ static bool getPropertyValue(PEVENT_RECORD evt, LPWSTR prop, PBYTE* pData) {
DataDescriptor.ArrayIndex = 0;
DWORD PropertySize = 0;
// Check if the data is avaliable and what is the size of it.
// Check if the data is available and what is the size of it.
DWORD status =
TdhGetPropertySize(evt, 0, NULL, 1, &DataDescriptor, &PropertySize);
if (ERROR_SUCCESS != status) {
@ -79,7 +79,7 @@ static void WINAPI EventRecordCallback(PEVENT_RECORD eventRecord) {
ETWSessionState* state = (ETWSessionState*)eventRecord->UserContext;
if (resultValue != NULL && domainValue != NULL) {
state->callback((wchar_t*)domainValue, (wchar_t*)resultValue);
state->callback((wchar_t*)domainValue, eventRecord->EventHeader.ProcessId, (wchar_t*)resultValue);
}
free(resultValue);
@ -160,7 +160,7 @@ extern "C" {
EVENT_TRACE_CONTROL_STOP);
}
// PM_ETWFlushTrace Closes the session and frees resourses.
// PM_ETWFlushTrace Closes the session and frees recourses.
__declspec(dllexport) uint32_t PM_ETWDestroySession(ETWSessionState* state) {
if (state == NULL) {
return 1;

View file

@ -1,7 +1,7 @@
# Portmaster Windows kext
Implementation of Safing's Portmaster Windows kernel extension in Rust.
### Documentation
### Documentation
- [Driver](driver/README.md) -> entry point.
- [WDK](wdk/README.md) -> Windows Driver Kit interface.
@ -9,8 +9,11 @@ Implementation of Safing's Portmaster Windows kernel extension in Rust.
- [Release](release/README.md) -> Guide how to do a release build.
- [Windows Filtering Platform - MS](https://learn.microsoft.com/en-us/windows-hardware/drivers/network/roadmap-for-developing-wfp-callout-drivers) -> The driver is build on top of WFP.
### Building (For release)
### Building
Please refer to [release/README.md](release/README.md) for details about the release procedure.
### Building (For testing and development)
The Windows Portmaster Kernel Extension is currently only developed and tested for the amd64 (64-bit) architecture.
@ -53,23 +56,18 @@ __Build driver:__
```sh
cd driver
cargo build
cargo build --release
```
> Build also works on linux
__Link and sign:__
On a windows machine copy `driver.lib` form the project target directory (`driver/target/x86_64-pc-windows-msvc/debug/driver.lib`) in the same folder as `link.bat`.
Run `link.bat`.
On a windows machine copy `driver.lib` from the project target directory (`driver/target/x86_64-pc-windows-msvc/release/driver.lib`) in the same folder as `link-dev.ps1`.
Run `link-dev.ps1`.
`driver.sys` should appear in the folder. Load and use the driver.
`driver.sys` should appear in the folder.
### Test
- Install go
- https://go.dev/dl/
```sh
cd kext_tester
go run .
Sign the driver with the test certificate:
```
> make sure the hardcoded path in main.go is pointing to the correct `.sys` file
SignTool sign /v /s TestCertStoreName /n TestCertName driver.sys
```
Load and use the driver.

21
windows_kext/link-dev.ps1 Normal file
View file

@ -0,0 +1,21 @@
# Example script for creating debug builds. Libraries may change depending on the version of the WDK that is installed.
$SDK_Version = "10.0.26100.0"
link.exe /OUT:driver.sys `
/MANIFEST:NO /PROFILE /Driver `
"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\wdmsec.lib" `
"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\ndis.lib" `
"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\fwpkclnt.lib" `
"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\BufferOverflowK.lib" `
"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\ntoskrnl.lib" `
"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\hal.lib" `
"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\wmilib.lib" `
"C:\Program Files (x86)\Windows Kits\10\lib\wdf\kmdf\x64\1.15\WdfLdr.lib" `
"C:\Program Files (x86)\Windows Kits\10\lib\wdf\kmdf\x64\1.15\WdfDriverEntry.lib" `
"driver.lib" `
/RELEASE /VERSION:"10.0" /DEBUG /MACHINE:X64 /ENTRY:"FxDriverEntry" /OPT:REF /INCREMENTAL:NO /SUBSYSTEM:NATIVE",6.01" /OPT:ICF /ERRORREPORT:PROMPT /MERGE:"_TEXT=.text;_PAGE=PAGE" /NOLOGO /NODEFAULTLIB /SECTION:"INIT,d"
if(!$?) {
Exit $LASTEXITCODE
}

View file

@ -1,25 +1,31 @@
# Kext release tool
### Generate the zip file
## Generate the zip file
- Make sure the deriver version in `kextinterface/version.txt` is up to date
- Make sure `kextinterface/version.txt` is up to date
- Execute: `cargo run`
* This will generate release `kext_release_vX-X-X.zip` file. Which contains all the necessary files to make the release.
_This will generate release `portmaster-kext-release-bundle-vX-X-X-X.zip` file. Which contains all the necessary files to make the release._
### Generate the cab file
## Generate the cab file
- Copy the zip and extract it on a windows machine.
* Visual Studio 2022 and WDK need to be installed.
- From VS Command Prompt / PowerShell run:
```
cd kext_release_v.../
./build_cab.bat
```
> Script is written for VS `$SDK_Version = "10.0.22621.0"`. If different version is used update the script.
**Precondition:** Visual Studio 2022 and WDK need to be installed.
- Sing the cab file
- copy the zip and extract it on a windows machine.
### Let Microsoft Sign
- update `.\build_cab.ps1`: set correct SDK version you use.
_e.g.: $SDK_Version = "10.0.26100.0" (see in `C:\Program Files (x86)\Windows Kits\10\Lib`)_
- Use "Developer PowerShell for VS":
```powershell
cd portmaster-kext-release-bundle-v...
.\build_cab.ps1
```
- Sing the the output cab file: `portmaster-kext-release-bundle-v...\PortmasterKext_v....cab`
## Let Microsoft Sign
- Go to https://partner.microsoft.com/en-us/dashboard/hardware/driver/New
- Enter "PortmasterKext vX.X.X #1" as the product name

View file

@ -10,13 +10,17 @@ static LIB_PATH: &'static str = "./build/x86_64-pc-windows-msvc/release/driver.l
fn main() {
build_driver();
println!(
"Building kext v{}-{}-{} #{}",
let filename = format!(
"portmaster-kext-release-bundle-v{}-{}-{}-{}.zip",
VERSION[0], VERSION[1], VERSION[2], VERSION[3]
);
println!("Building KEXT: {}", filename);
// Create Zip that will hold all the release files and scripts.
let file = File::create("portmaster-kext-release-bundle.zip").unwrap();
let file = File::create(&filename).unwrap();
let mut zip = zip::ZipWriter::new(file);
// Write files to zip

View file

@ -1,7 +1,7 @@
# Remove previous cab build
Remove-Item -Path "PortmasterKext_v2-0-0.cab" -ErrorAction SilentlyContinue
$SDK_Version = "10.0.22621.0"
$SDK_Version = "10.0.26100.0"
# Build metadata file
rc -I "C:\Program Files (x86)\Windows Kits\10\Include\$SDK_Version\um" `