Compare commits

...

71 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
Daniel
df4106fd70 Merge branch 'develop' 2024-11-27 16:40:03 +01:00
Daniel
614d8972a2 Improve logging 2024-11-27 16:37:59 +01:00
Daniel
acaf98144a Bump kext version 2024-11-27 16:19:09 +01:00
Daniel
473f05becc Update service deps 2024-11-27 16:19:02 +01:00
Vladimir Stoilov
1a1bc14804
Feature/systemd query events ()
* [service] Subscribe to systemd-resolver events

* [service] Add disabled state to the resolver

* [service] Add ETW DNS event listener

* [service] DNS listener refactoring

* [service] Add windows core dll project

* [service] DNSListener refactoring, small bugfixes

* [service] Change dns bypass rule

* [service] Update gitignore

* [service] Remove shim from integration module

* [service] Add DNS packet analyzer

* [service] Add self-check in dns monitor

* [service] Fix go linter errors

* [CI] Add github workflow for the windows core dll

* [service] Minor fixes to the dns monitor
2024-11-27 17:10:47 +02:00
Vladimir Stoilov
943b9b7859
Fix file permissions on windows ()
* [service] Set file permissions on windows

* [service] Fix minor windows permission bugs

* [service] Fix permission bugs

* [service] Fix windows non admin user start
2024-11-26 17:00:01 +02:00
Vladimir Stoilov
ca88c6d8ad
[desktop] Update tauri () 2024-11-26 16:59:06 +02:00
Vladimir Stoilov
fe070b4f56
Feature/kext default action drop ()
* [windows_kext] Make default action to drop

* [windows_kext] Minor improvments
2024-11-25 14:03:35 +02:00
Daniel
a4b76843e5 Bump kext version 2024-11-12 17:39:49 +01:00
Daniel
38e9e342f7 Update deps 2024-11-12 15:42:49 +01:00
Daniel Hååvi
ee58324aee
Merge pull request from safing/fix/core-deadlocks
Fix deadlocks and process username issue
2024-11-12 15:27:13 +01:00
Daniel
f4b96e1ce7 Make saving IP and CNAMEs more defensive 2024-11-12 15:13:44 +01:00
Daniel
07acb9befa Notify packet issues asynchronously 2024-11-12 15:11:07 +01:00
Daniel
a99b68ec91 Warn instead of failing when process username cannot be found 2024-11-12 15:10:41 +01:00
Vladimir Stoilov
af3bb804bf
Fix reading only the needed TCP/UDP header bytes ()
* [windows_kext] Fix reading only the needed TCP/UDP header bytes

* [windows_kext] Disable debug mode

* [windows_kext] Block all fragment packets

* [windows_kext] Improve wording for compiler error
2024-11-07 15:52:26 +02:00
Vladimir Stoilov
145f5e67de
[windows_kext] Improve documentation () 2024-10-28 08:35:02 +02:00
148 changed files with 11443 additions and 6468 deletions
.github/workflows
.gitignore.golangci.ymlEarthfile
base
cmds
hub
notifier
portmaster-start
updatemgr
desktop
go.modgo.sum
service

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

41
.github/workflows/windows-dll.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Windows Portmaster Core DLL
on:
push:
paths:
- 'windows_core_dll/**'
branches:
- master
- develop
pull_request:
paths:
- 'windows_core_dll/**'
branches:
- master
- develop
workflow_dispatch:
jobs:
build:
name: Build
runs-on: windows-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
- name: Build DLL
run: msbuild windows_core_dll\windows_core_dll.sln -t:rebuild -property:Configuration=Release
- name: Verify DLL
shell: powershell
run: |
if (!(Test-Path "windows_core_dll/x64/Release/portmaster-core.dll")) {
Write-Error "DLL build failed: portmaster-core.dll not found"
exit 1
}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: portmaster-core-dll
path: windows_core_dll/x64/Release/portmaster-core.dll

4
.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
@ -52,3 +51,4 @@ go.work.sum
# Kext releases
windows_kext/release/kext_release_*.zip
windows_core_dll/.vs/windows_core_dll

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.0.0-beta"
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

@ -288,10 +288,10 @@ 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 {
if err := t.Chmod(perm); err != nil {
return err
}
// TODO(vladimir): to set permissions on windows we need the full path of the file.
err = t.Chmod(perm)
if err != nil {
return err
}
if _, err := t.Write(data); err != nil {

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

@ -17,6 +17,7 @@ import (
"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"
)
@ -136,12 +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 {
// 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

@ -12,7 +12,7 @@ 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 {
@ -20,10 +20,11 @@ func EnsureDirectory(path string, perm os.FileMode) error {
if f.IsDir() {
// directory exists, check permissions
if isWindows {
// TODO: set correct permission on windows
// acl.Chmod(path, perm)
} else if f.Mode().Perm() != perm {
return os.Chmod(path, perm)
// Ignore windows permission error. For none admin users it will always fail.
_ = SetDirPermission(path, perm)
return nil
} else if f.Mode().Perm() != perm.AsUnixDirExecPermission() {
return SetDirPermission(path, perm)
}
return nil
}
@ -34,11 +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)
}
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

@ -1,6 +1,11 @@
package renameio
import "os"
import (
"os"
"runtime"
"github.com/hectane/go-acl"
)
// WriteFile mirrors os.WriteFile, replacing an existing file with the same
// name atomically.
@ -14,7 +19,12 @@ func WriteFile(filename string, data []byte, perm os.FileMode) error {
}()
// Set permissions before writing data, in case the data is sensitive.
if err := t.Chmod(perm); err != nil {
if runtime.GOOS == "windows" {
err = acl.Chmod(t.path, perm)
} else {
err = t.Chmod(perm)
}
if err != nil {
return err
}

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

@ -272,7 +272,7 @@ func fixExecPerm(path string) error {
return nil
}
if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec // Set execution rights.
if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec
return fmt.Errorf("failed to chmod %s: %w", 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

@ -23,13 +23,13 @@
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@tauri-apps/api": ">=2.0.0-rc.1",
"@tauri-apps/plugin-cli": ">=2.0.0-rc.1",
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0-rc.1",
"@tauri-apps/plugin-dialog": ">=2.0.0-rc.1",
"@tauri-apps/plugin-notification": ">=2.0.0-rc.1",
"@tauri-apps/plugin-os": ">=2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc",
"@tauri-apps/api": ">=2.1.1",
"@tauri-apps/plugin-cli": ">=2.0.0",
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
"@tauri-apps/plugin-dialog": ">=2.0.0",
"@tauri-apps/plugin-notification": ">=2.0.0",
"@tauri-apps/plugin-os": ">=2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"autoprefixer": "^10.4.14",
"d3": "^7.8.4",
"data-urls": "^5.0.0",
@ -4406,9 +4406,9 @@
"peer": true
},
"node_modules/@tauri-apps/api": {
"version": "2.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz",
"integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.1.1.tgz",
"integrity": "sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@ -4416,57 +4416,57 @@
}
},
"node_modules/@tauri-apps/plugin-cli": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0-rc.1.tgz",
"integrity": "sha512-EcSTRfEU3zzlNbgwVtZVzqB19z3PNjyXD9H+YXuuLpV+Hwuh6Oi1fhUdCI0mp5zr9HSMWE+HzHkpBI7sVP1RyA==",
"license": "MIT or APACHE-2.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0.tgz",
"integrity": "sha512-glQmlL1IiCGEa1FHYa/PTPSeYhfu56omLRgHXWlJECDt6DbJyRuJWVgtkQfUxtqnVdYnnU+DGIGeiInoEqtjLw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-clipboard-manager": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-rc.1.tgz",
"integrity": "sha512-hFgUABMmQuVGKwHb8PR9fuqfk0WRkedbWUt/ZV5sL4Q6kLrsp3JYJvtzVPeMYdeBvMqHl8WXNxAc/zwSld2h9w==",
"license": "MIT or APACHE-2.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz",
"integrity": "sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.1.tgz",
"integrity": "sha512-H28gh6BfZtjflHQ+HrmWwunDriBI3AQLAKnMs50GA6zeNUULqbQr7VXbAAKeJL/0CmWcecID4PKXVoSlaWRhEg==",
"license": "MIT or APACHE-2.0",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.1.tgz",
"integrity": "sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-notification": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0-rc.1.tgz",
"integrity": "sha512-ddDj7xM8XR7Zv2vdpofNXlLjcp49p/VjlL0D+/eBcMuyooaLNMor3jz/+H6s23iHerdxMWA50mzy26BRN1BySA==",
"license": "MIT or APACHE-2.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0.tgz",
"integrity": "sha512-6qEDYJS7mgXZWLXA0EFL+DVCJh8sJlzSoyw6B50pxhLPVFjc5Vr5DVzl5W3mUHaYhod5wsC984eQnlCCGqxYDA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-os": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-rc.1.tgz",
"integrity": "sha512-PV8zlSTmYfiN2xzILUmlDSEycS7UYbH2yXk/ZqF+qQU6/s+OVQvmSth4EhllFjcpvPbtqELvpzfjw+2qEouchA==",
"license": "MIT or APACHE-2.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0.tgz",
"integrity": "sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-shell": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz",
"integrity": "sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==",
"license": "MIT or APACHE-2.0",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.1.tgz",
"integrity": "sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tootallnate/once": {
@ -21067,56 +21067,56 @@
"peer": true
},
"@tauri-apps/api": {
"version": "2.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz",
"integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg=="
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.1.1.tgz",
"integrity": "sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A=="
},
"@tauri-apps/plugin-cli": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0-rc.1.tgz",
"integrity": "sha512-EcSTRfEU3zzlNbgwVtZVzqB19z3PNjyXD9H+YXuuLpV+Hwuh6Oi1fhUdCI0mp5zr9HSMWE+HzHkpBI7sVP1RyA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0.tgz",
"integrity": "sha512-glQmlL1IiCGEa1FHYa/PTPSeYhfu56omLRgHXWlJECDt6DbJyRuJWVgtkQfUxtqnVdYnnU+DGIGeiInoEqtjLw==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-clipboard-manager": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-rc.1.tgz",
"integrity": "sha512-hFgUABMmQuVGKwHb8PR9fuqfk0WRkedbWUt/ZV5sL4Q6kLrsp3JYJvtzVPeMYdeBvMqHl8WXNxAc/zwSld2h9w==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz",
"integrity": "sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-dialog": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.1.tgz",
"integrity": "sha512-H28gh6BfZtjflHQ+HrmWwunDriBI3AQLAKnMs50GA6zeNUULqbQr7VXbAAKeJL/0CmWcecID4PKXVoSlaWRhEg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.1.tgz",
"integrity": "sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-notification": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0-rc.1.tgz",
"integrity": "sha512-ddDj7xM8XR7Zv2vdpofNXlLjcp49p/VjlL0D+/eBcMuyooaLNMor3jz/+H6s23iHerdxMWA50mzy26BRN1BySA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0.tgz",
"integrity": "sha512-6qEDYJS7mgXZWLXA0EFL+DVCJh8sJlzSoyw6B50pxhLPVFjc5Vr5DVzl5W3mUHaYhod5wsC984eQnlCCGqxYDA==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-os": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-rc.1.tgz",
"integrity": "sha512-PV8zlSTmYfiN2xzILUmlDSEycS7UYbH2yXk/ZqF+qQU6/s+OVQvmSth4EhllFjcpvPbtqELvpzfjw+2qEouchA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0.tgz",
"integrity": "sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-shell": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz",
"integrity": "sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.1.tgz",
"integrity": "sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tootallnate/once": {

View file

@ -37,13 +37,13 @@
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@tauri-apps/api": ">=2.0.0-rc.1",
"@tauri-apps/plugin-cli": ">=2.0.0-rc.1",
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0-rc.1",
"@tauri-apps/plugin-dialog": ">=2.0.0-rc.1",
"@tauri-apps/plugin-notification": ">=2.0.0-rc.1",
"@tauri-apps/plugin-os": ">=2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc",
"@tauri-apps/api": ">=2.1.1",
"@tauri-apps/plugin-cli": ">=2.0.0",
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
"@tauri-apps/plugin-dialog": ">=2.0.0",
"@tauri-apps/plugin-notification": ">=2.0.0",
"@tauri-apps/plugin-os": ">=2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"autoprefixer": "^10.4.14",
"d3": "^7.8.4",
"data-urls": "^5.0.0",

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

File diff suppressed because it is too large Load diff

View file

@ -12,21 +12,21 @@ rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.0.0-rc.7", features = [] }
tauri-build = { version = "2.0.3", features = [] }
[dependencies]
# Tauri
tauri = { version = "2.0.0-rc.8", features = ["tray-icon", "image-png", "config-json5", "devtools"] }
tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-dialog = "2.0.0-rc"
tauri-plugin-clipboard-manager = "2.0.0-rc"
tauri-plugin-os = "2.0.0-rc"
tauri-plugin-single-instance = "2.0.0-rc"
tauri-plugin-notification = "2.0.0-rc"
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-window-state = "2.0.0-rc"
tauri = { version = "2.1.1", features = ["tray-icon", "image-png", "config-json5", "devtools"] }
tauri-plugin-shell = "2.0.2"
tauri-plugin-dialog = "2.0.3"
tauri-plugin-clipboard-manager = "2.0.2"
tauri-plugin-os = "2.0.1"
tauri-plugin-single-instance = "2.0.1"
tauri-plugin-notification = "2.0.1"
tauri-plugin-log = "2.0.2"
tauri-plugin-window-state = "2.0.2"
tauri-cli = "2.0.0-rc.8"
tauri-cli = "2.1.0"
clap = { version = "4" }
# General

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,7 @@ use crate::{
portmaster::PortmasterExt,
window::{create_main_window, may_navigate_to_ui, open_window},
};
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
pub type AppIcon = TrayIcon<Wry>;
@ -199,8 +199,10 @@ pub fn setup_tray_menu(
app.dialog()
.message("This does not stop the Portmaster system service")
.title("Do you really want to quit the user interface?")
.ok_button_label("Yes, exit")
.cancel_button_label("No")
.buttons(MessageDialogButtons::OkCancelCustom(
"Yes, exit".to_owned(),
"No".to_owned(),
))
.show(move |answer| {
if answer {
// let _ = handle.emit("exit-requested", "");

View file

@ -139,7 +139,7 @@
{{/each~}}
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
<File Id="Path" Source="{{main_binary_path}}" KeyPath="yes" Checksum="yes"/>
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">

View file

@ -139,7 +139,7 @@
{{/each~}}
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
<File Id="Path" Source="{{main_binary_path}}" KeyPath="yes" Checksum="yes"/>
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">

97
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
@ -9,14 +11,15 @@ require (
fyne.io/systray v1.11.0
github.com/VictoriaMetrics/metrics v1.35.1
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/coreos/go-iptables v0.7.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
github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435
@ -32,10 +35,12 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.7.0
github.com/jackc/puddle/v2 v2.2.1
github.com/lmittmann/tint v1.0.5
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb
github.com/jackc/puddle/v2 v2.2.2
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
@ -44,84 +49,138 @@ 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/safing/jess v0.3.4
github.com/safing/structures v1.1.0
github.com/rubenv/sql-migrate v1.7.1
github.com/safing/jess v0.3.5
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
github.com/tevino/abool v1.2.0
github.com/tidwall/gjson v1.17.3
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
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.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.8.0
golang.org/x/sys v0.24.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.29.0
gopkg.in/yaml.v3 v3.0.1
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.8 // indirect
github.com/maruel/panicparse/v2 v2.3.1 // 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.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.26.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/text v0.17.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/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.32.0 // 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
)

242
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.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.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
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,18 +183,31 @@ 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=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@ -161,6 +226,18 @@ github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCX
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
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=
@ -169,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=
@ -209,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=
@ -219,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=
@ -227,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=
@ -236,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=
@ -244,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.4 h1:/p6ensqEUn2jI/z1EB9JUdwH4MJQirh/C9jEwNBzxw8=
github.com/safing/jess v0.3.4/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/jess v0.3.5 h1:KS5elTKfWcDUow8SUoCj5QdyyGJNoExJNySerNkbxUU=
github.com/safing/jess v0.3.5/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI=
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=
@ -258,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=
@ -277,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=
@ -291,8 +414,8 @@ github.com/tannerryan/ring v1.1.2/go.mod h1:DkELJEjbZhJBtFKR9Xziwj3HKZnb/knRgljN
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@ -307,10 +430,14 @@ github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8t
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=
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
github.com/varlink/go v0.4.0 h1:+/BQoUO9eJK/+MTSHwFcJch7TMsb6N6Dqp6g0qaXXRo=
github.com/varlink/go v0.4.0/go.mod h1:DKg9Y2ctoNkesREGAEak58l+jOC6JU2aqZvUYs5DynU=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
@ -318,15 +445,22 @@ 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=
@ -338,10 +472,15 @@ 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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
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=
@ -351,6 +490,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
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.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=
@ -374,6 +514,8 @@ 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.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=
@ -381,18 +523,22 @@ golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJ
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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.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=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
@ -415,35 +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.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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.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=
@ -451,12 +606,15 @@ 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=
@ -469,6 +627,8 @@ 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/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=
@ -485,5 +645,7 @@ 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=
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

@ -3,6 +3,7 @@ package compat
import (
"net"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/process"
)
@ -31,10 +32,16 @@ func SubmitDNSCheckDomain(subdomain string) (respondWith net.IP) {
// ReportSecureDNSBypassIssue reports a DNS bypassing issue for the given process.
func ReportSecureDNSBypassIssue(p *process.Process) {
secureDNSBypassIssue.notify(p)
module.mgr.Go("report secure dns bypass issue", func(w *mgr.WorkerCtx) error {
secureDNSBypassIssue.notify(p)
return nil
})
}
// ReportMultiPeerUDPTunnelIssue reports a multi-peer UDP tunnel for the given process.
func ReportMultiPeerUDPTunnelIssue(p *process.Process) {
multiPeerUDPTunnelIssue.notify(p)
module.mgr.Go("report multi-peer udp tunnel issue", func(w *mgr.WorkerCtx) error {
multiPeerUDPTunnelIssue.notify(p)
return nil
})
}

View file

@ -181,4 +181,5 @@ func New(instance instance) (*Compat, error) {
type instance interface {
NetEnv() *netenv.NetEnv
Resolver() *resolver.ResolverModule
}

View file

@ -158,6 +158,12 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) {
// Step 3: Have the nameserver respond with random data in the answer section.
// Check if the resolver is enabled
if module.instance.Resolver().IsDisabled() {
// There is no control over the response, there is nothing more that can be checked.
return nil, nil
}
// Wait for the reply from the resolver.
select {
case err := <-dnsCheckLookupError:

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

@ -43,8 +43,24 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
return endpoints.NoMatch, "", nil
}
// If Portmaster resolver is disabled allow requests going to system dns resolver.
// And allow all connections out of the System Resolver.
if module.instance.Resolver().IsDisabled() {
// TODO(vladimir): Is there a more specific check that can be done?
if conn.Process().IsSystemResolver() {
return endpoints.NoMatch, "", nil
}
if conn.Entity.Port == 53 && conn.Entity.IPScope.IsLocalhost() {
return endpoints.NoMatch, "", nil
}
}
// Block bypass attempts using an (encrypted) DNS server.
switch {
case looksLikeOutgoingDNSRequest(conn) && module.instance.Resolver().IsDisabled():
// Allow. Packet will be analyzed and blocked if its not a dns request, before sent.
conn.Inspecting = true
return endpoints.NoMatch, "", nil
case conn.Entity.Port == 53:
return endpoints.Denied,
"blocked DNS query, manual dns setup required",
@ -62,3 +78,17 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
return endpoints.NoMatch, "", nil
}
func looksLikeOutgoingDNSRequest(conn *network.Connection) bool {
// Outbound on remote port 53, UDP.
if conn.Inbound {
return false
}
if conn.Entity.Port != 53 {
return false
}
if conn.IPProtocol != packet.UDP {
return false
}
return true
}

View file

@ -287,6 +287,30 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
}
}
// Create new record for this IP.
record := resolver.ResolvedDomain{
Domain: q.FQDN,
Resolver: rrCache.Resolver,
DNSRequestContext: rrCache.ToDNSRequestContext(),
Expires: rrCache.Expires,
}
// Process CNAMEs
record.AddCNAMEs(cnames)
// Link connection with cnames.
if conn.Type == network.DNSRequest {
conn.Entity.CNAME = record.CNAMEs
}
SaveIPsInCache(ips, profileID, record)
}
// formatRR is a friendlier alternative to miekg/dns.RR.String().
func formatRR(rr dns.RR) string {
return strings.ReplaceAll(rr.String(), "\t", " ")
}
// SaveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
func SaveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
// Package IPs and CNAMEs into IPInfo structs.
for _, ip := range ips {
// Never save domain attributions for localhost IPs.
@ -294,31 +318,6 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
continue
}
// Create new record for this IP.
record := resolver.ResolvedDomain{
Domain: q.FQDN,
Resolver: rrCache.Resolver,
DNSRequestContext: rrCache.ToDNSRequestContext(),
Expires: rrCache.Expires,
}
// Resolve all CNAMEs in the correct order and add the to the record.
domain := q.FQDN
for {
nextDomain, isCNAME := cnames[domain]
if !isCNAME {
break
}
record.CNAMEs = append(record.CNAMEs, nextDomain)
domain = nextDomain
}
// Update the entity to include the CNAMEs of the query response.
conn.Entity.CNAME = record.CNAMEs
// Check if there is an existing record for this DNS response.
// Else create a new one.
ipString := ip.String()
info, err := resolver.GetIPInfo(profileID, ipString)
if err != nil {
@ -341,8 +340,3 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
}
}
}
// formatRR is a friendlier alternative to miekg/dns.RR.String().
func formatRR(rr dns.RR) string {
return strings.ReplaceAll(rr.String(), "\t", " ")
}

View file

@ -0,0 +1,109 @@
//go:build windows
// +build windows
package dnsmonitor
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"github.com/safing/portmaster/service/integration"
"golang.org/x/sys/windows"
)
type ETWSession struct {
i *integration.ETWFunctions
shutdownGuard atomic.Bool
shutdownMutex sync.Mutex
state uintptr
}
// 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,
}
// Make sure session from previous instances are not running.
_ = etwSession.i.StopOldSession()
// Initialize notification activated callback
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.
etwSession.state = etwSession.i.CreateState(win32Callback)
// Make sure DestroySession is called even if caller forgets to call it.
runtime.SetFinalizer(etwSession, func(s *ETWSession) {
_ = s.i.DestroySession(s.state)
})
// Initialize session.
err := etwSession.i.InitializeSession(etwSession.state)
if err != nil {
return nil, fmt.Errorf("failed to initialize session: %q", err)
}
return etwSession, nil
}
// StartTrace starts the tracing session of dns events. This is a blocking call. It will not return until the trace is stopped.
func (l *ETWSession) StartTrace() error {
return l.i.StartTrace(l.state)
}
// IsRunning returns true if DestroySession has NOT been called.
func (l *ETWSession) IsRunning() bool {
return !l.shutdownGuard.Load()
}
// 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()
// Make sure session is still running.
if l.shutdownGuard.Load() {
return nil
}
return l.i.FlushTrace(l.state)
}
// 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()
if l.shutdownGuard.Swap(true) {
return nil
}
err := l.i.DestroySession(l.state)
if err != nil {
return err
}
l.state = 0
return nil
}

View file

@ -0,0 +1,19 @@
//go:build !linux && !windows
// +build !linux,!windows
package dnsmonitor
type Listener struct{}
func newListener(_ *DNSMonitor) (*Listener, error) {
return &Listener{}, nil
}
func (l *Listener) flush() error {
// Nothing to flush
return nil
}
func (l *Listener) stop() error {
return nil
}

View file

@ -0,0 +1,145 @@
//go:build linux
// +build linux
package dnsmonitor
import (
"errors"
"fmt"
"net"
"os"
"github.com/miekg/dns"
"github.com/varlink/go/varlink"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/resolver"
)
type Listener struct {
varlinkConn *varlink.Connection
}
func newListener(module *DNSMonitor) (*Listener, error) {
// Set source of the resolver.
ResolverInfo.Source = resolver.ServerSourceSystemd
// Check if the system has systemd-resolver.
_, err := os.Stat("/run/systemd/resolve/io.systemd.Resolve.Monitor")
if err != nil {
return nil, fmt.Errorf("system does not support systemd resolver monitor")
}
listener := &Listener{}
restartAttempts := 0
module.mgr.Go("systemd-resolver-event-listener", func(w *mgr.WorkerCtx) error {
// Abort initialization if the connection failed after too many tries.
if restartAttempts > 10 {
return nil
}
restartAttempts += 1
// Initialize varlink connection
varlinkConn, err := varlink.NewConnection(module.mgr.Ctx(), "unix:/run/systemd/resolve/io.systemd.Resolve.Monitor")
if err != nil {
return fmt.Errorf("failed to connect to systemd-resolver varlink service: %w", err)
}
defer func() {
if varlinkConn != nil {
err = varlinkConn.Close()
if err != nil {
log.Errorf("dnsmonitor: failed to close varlink connection: %s", err)
}
}
}()
listener.varlinkConn = varlinkConn
// Subscribe to the dns query events
receive, err := listener.varlinkConn.Send(w.Ctx(), "io.systemd.Resolve.Monitor.SubscribeQueryResults", nil, varlink.More)
if err != nil {
var varlinkErr *varlink.Error
if errors.As(err, &varlinkErr) {
return fmt.Errorf("failed to issue Varlink call: %+v", varlinkErr.Parameters)
} else {
return fmt.Errorf("failed to issue Varlink call: %w", err)
}
}
for {
queryResult := QueryResult{}
// Receive the next event from the resolver.
flags, err := receive(w.Ctx(), &queryResult)
if err != nil {
var varlinkErr *varlink.Error
if errors.As(err, &varlinkErr) {
return fmt.Errorf("failed to receive Varlink reply: %+v", varlinkErr.Parameters)
} else {
return fmt.Errorf("failed to receive Varlink reply: %w", err)
}
}
// Check if the reply indicates the end of the stream
if flags&varlink.Continues == 0 {
break
}
// Ignore if there is no question.
if queryResult.Question == nil || len(*queryResult.Question) == 0 {
continue
}
// Protmaster self check
domain := (*queryResult.Question)[0].Name
if processIfSelfCheckDomain(dns.Fqdn(domain)) {
// Not need to process result.
continue
}
if queryResult.Rcode != nil {
continue // Ignore DNS errors
}
listener.processAnswer(domain, &queryResult)
}
return nil
})
return listener, nil
}
func (l *Listener) flush() error {
// Nothing to flush
return nil
}
func (l *Listener) stop() error {
return nil
}
func (l *Listener) processAnswer(domain string, queryResult *QueryResult) {
// Allocated data struct for the parsed result.
cnames := make(map[string]string)
ips := make([]net.IP, 0, 5)
// Check if the query is valid
if queryResult.Answer == nil {
return
}
// Go trough each answer entry.
for _, a := range *queryResult.Answer {
if a.RR.Address != nil {
ip := net.IP(*a.RR.Address)
// Answer contains ip address.
ips = append(ips, ip)
} else if a.RR.Name != nil {
// Answer is a CNAME.
cnames[domain] = *a.RR.Name
}
}
saveDomain(domain, ips, cnames, resolver.IPInfoProfileScopeGlobal)
}

View file

@ -0,0 +1,130 @@
//go:build windows
// +build windows
package dnsmonitor
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"github.com/miekg/dns"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/process"
"github.com/safing/portmaster/service/resolver"
)
type Listener struct {
etw *ETWSession
}
func newListener(module *DNSMonitor) (*Listener, error) {
// Set source of the resolver.
ResolverInfo.Source = resolver.ServerSourceETW
listener := &Listener{}
// Initialize new dns event session.
err := initializeSessions(module, listener)
if err != nil {
// 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
})
}
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()
}
func (l *Listener) stop() error {
if l == nil {
return fmt.Errorf("listener is nil")
}
if l.etw == nil {
return fmt.Errorf("invalid etw session")
}
// Stop and destroy trace. Destroy should be called even if stop fails for some reason.
err := l.etw.StopTrace()
err2 := l.etw.DestroySession()
if err != nil {
return fmt.Errorf("StopTrace failed: %w", err)
}
if err2 != nil {
return fmt.Errorf("DestroySession failed: %w", err2)
}
return nil
}
func (l *Listener) processEvent(domain string, pid uint32, result string) {
if processIfSelfCheckDomain(dns.Fqdn(domain)) {
// Not need to process result.
return
}
// Ignore empty results
if len(result) == 0 {
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{}
resultArray := strings.Split(result, ";")
for _, r := range resultArray {
// For results other than IP addresses, the string starts with "type:"
if strings.HasPrefix(r, "type:") {
dnsValueArray := strings.Split(r, " ")
if len(dnsValueArray) < 3 {
continue
}
// Ignore everything except CNAME records
if value, err := strconv.ParseInt(dnsValueArray[1], 10, 16); err == nil && value == int64(dns.TypeCNAME) {
cnames[domain] = dnsValueArray[2]
}
} else {
// If the event doesn't start with "type:", it's an IP address
ip := net.ParseIP(r)
if ip != nil {
ips = append(ips, ip)
}
}
}
saveDomain(domain, ips, cnames, profileScope)
}

View file

@ -0,0 +1,139 @@
package dnsmonitor
import (
"errors"
"net"
"strings"
"github.com/miekg/dns"
"github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/compat"
"github.com/safing/portmaster/service/integration"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/resolver"
)
var ResolverInfo = resolver.ResolverInfo{
Name: "SystemResolver",
Type: resolver.ServerTypeMonitor,
}
type DNSMonitor struct {
instance instance
mgr *mgr.Manager
listener *Listener
}
// Manager returns the module manager.
func (dl *DNSMonitor) Manager() *mgr.Manager {
return dl.mgr
}
// Start starts the module.
func (dl *DNSMonitor) Start() error {
// Initialize dns event listener
var err error
dl.listener, err = newListener(dl)
if err != nil {
log.Warningf("dnsmonitor: failed to start dns listener: %s", err)
}
return nil
}
// Stop stops the module.
func (dl *DNSMonitor) Stop() error {
if dl.listener != nil {
err := dl.listener.stop()
if err != nil {
log.Errorf("dnsmonitor: failed to close listener: %s", err)
}
}
return nil
}
// Flush flushes the buffer forcing all events to be processed.
func (dl *DNSMonitor) Flush() error {
return dl.listener.flush()
}
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{
Domain: fqdn,
Resolver: &ResolverInfo,
DNSRequestContext: &resolver.DNSRequestContext{},
Expires: 0,
}
// Process cnames
record.AddCNAMEs(cnames)
// Add to cache
saveIPsInCache(ips, profileScope, record)
}
func New(instance instance) (*DNSMonitor, error) {
// Initialize module
m := mgr.New("DNSMonitor")
module := &DNSMonitor{
mgr: m,
instance: instance,
}
return module, nil
}
type instance interface {
OSIntegration() *integration.OSIntegration
}
func processIfSelfCheckDomain(fqdn string) bool {
// Check for compat check dns request.
if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
_ = compat.SubmitDNSCheckDomain(subdomain)
log.Infof("dnsmonitor: self-check domain received")
// No need to parse the answer.
return true
}
return false
}
// saveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
func saveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
// Package IPs and CNAMEs into IPInfo structs.
for _, ip := range ips {
// Never save domain attributions for localhost IPs.
if netutils.GetIPScope(ip) == netutils.HostLocal {
continue
}
ipString := ip.String()
info, err := resolver.GetIPInfo(profileID, ipString)
if err != nil {
if !errors.Is(err, database.ErrNotFound) {
log.Errorf("dnsmonitor: failed to search for IP info record: %s", err)
}
info = &resolver.IPInfo{
IP: ipString,
ProfileID: profileID,
}
}
// Add the new record to the resolved domains for this IP and scope.
info.AddDomain(record)
// Save if the record is new or has been updated.
if err := info.Save(); err != nil {
log.Errorf("dnsmonitor: failed to save IP info record: %s", err)
}
}
}

View file

@ -0,0 +1,83 @@
//go:build linux
// +build linux
package dnsmonitor
// List of struct that define the systemd-resolver varlink dns event protocol.
// Source: `sudo varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve.Monitor io.systemd.Resolve.Monitor`
type ResourceKey struct {
Class int `json:"class"`
Type int `json:"type"`
Name string `json:"name"`
}
type ResourceRecord struct {
Key ResourceKey `json:"key"`
Name *string `json:"name,omitempty"`
Address *[]byte `json:"address,omitempty"`
// Rest of the fields are not used.
// Priority *int `json:"priority,omitempty"`
// Weight *int `json:"weight,omitempty"`
// Port *int `json:"port,omitempty"`
// CPU *string `json:"cpu,omitempty"`
// OS *string `json:"os,omitempty"`
// Items *[]string `json:"items,omitempty"`
// MName *string `json:"mname,omitempty"`
// RName *string `json:"rname,omitempty"`
// Serial *int `json:"serial,omitempty"`
// Refresh *int `json:"refresh,omitempty"`
// Expire *int `json:"expire,omitempty"`
// Minimum *int `json:"minimum,omitempty"`
// Exchange *string `json:"exchange,omitempty"`
// Version *int `json:"version,omitempty"`
// Size *int `json:"size,omitempty"`
// HorizPre *int `json:"horiz_pre,omitempty"`
// VertPre *int `json:"vert_pre,omitempty"`
// Latitude *int `json:"latitude,omitempty"`
// Longitude *int `json:"longitude,omitempty"`
// Altitude *int `json:"altitude,omitempty"`
// KeyTag *int `json:"key_tag,omitempty"`
// Algorithm *int `json:"algorithm,omitempty"`
// DigestType *int `json:"digest_type,omitempty"`
// Digest *string `json:"digest,omitempty"`
// FPType *int `json:"fptype,omitempty"`
// Fingerprint *string `json:"fingerprint,omitempty"`
// Flags *int `json:"flags,omitempty"`
// Protocol *int `json:"protocol,omitempty"`
// DNSKey *string `json:"dnskey,omitempty"`
// Signer *string `json:"signer,omitempty"`
// TypeCovered *int `json:"type_covered,omitempty"`
// Labels *int `json:"labels,omitempty"`
// OriginalTTL *int `json:"original_ttl,omitempty"`
// Expiration *int `json:"expiration,omitempty"`
// Inception *int `json:"inception,omitempty"`
// Signature *string `json:"signature,omitempty"`
// NextDomain *string `json:"next_domain,omitempty"`
// Types *[]int `json:"types,omitempty"`
// Iterations *int `json:"iterations,omitempty"`
// Salt *string `json:"salt,omitempty"`
// Hash *string `json:"hash,omitempty"`
// CertUsage *int `json:"cert_usage,omitempty"`
// Selector *int `json:"selector,omitempty"`
// MatchingType *int `json:"matching_type,omitempty"`
// Data *string `json:"data,omitempty"`
// Tag *string `json:"tag,omitempty"`
// Value *string `json:"value,omitempty"`
}
type Answer struct {
RR *ResourceRecord `json:"rr,omitempty"`
Raw string `json:"raw"`
IfIndex *int `json:"ifindex,omitempty"`
}
type QueryResult struct {
Ready *bool `json:"ready,omitempty"`
State *string `json:"state,omitempty"`
Rcode *int `json:"rcode,omitempty"`
Errno *int `json:"errno,omitempty"`
Question *[]ResourceKey `json:"question,omitempty"`
CollectedQuestions *[]ResourceKey `json:"collectedQuestions,omitempty"`
Answer *[]Answer `json:"answer,omitempty"`
}

View file

@ -188,7 +188,7 @@ func (q *Queue) packetHandler(ctx context.Context) func(nfqueue.Attribute) int {
return 0
}
if err := pmpacket.Parse(*attrs.Payload, &pkt.Base); err != nil {
if err := pmpacket.ParseLayer3(*attrs.Payload, &pkt.Base); err != nil {
log.Warningf("nfqueue: failed to parse payload: %s", err)
_ = pkt.Drop()
return 0

View file

@ -59,7 +59,7 @@ func (pkt *Packet) LoadPacketData() error {
return packet.ErrFailedToLoadPayload
}
err = packet.Parse(payload, &pkt.Base)
err = packet.ParseLayer3(payload, &pkt.Base)
if err != nil {
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)
return packet.ErrFailedToLoadPayload

View file

@ -55,6 +55,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()
@ -106,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

@ -4,6 +4,7 @@
package windowskext
import (
"fmt"
"sync"
"github.com/tevino/abool"
@ -19,6 +20,7 @@ type Packet struct {
verdictRequest uint64
payload []byte
payloadLayer uint8
verdictSet *abool.AtomicBool
payloadLoaded bool
@ -51,7 +53,15 @@ func (pkt *Packet) LoadPacketData() error {
pkt.payloadLoaded = true
if len(pkt.payload) > 0 {
err := packet.Parse(pkt.payload, &pkt.Base)
var err error
switch pkt.payloadLayer {
case 3:
err = packet.ParseLayer3(pkt.payload, &pkt.Base)
case 4:
err = packet.ParseLayer4(pkt.payload, &pkt.Base)
default:
err = fmt.Errorf("unsupported payload layer: %d", pkt.payloadLayer)
}
if err != nil {
log.Tracef("payload: %#v", pkt.payload)
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)

View file

@ -16,6 +16,7 @@ import (
"github.com/safing/portmaster/service/netquery"
"github.com/safing/portmaster/service/network"
"github.com/safing/portmaster/service/profile"
"github.com/safing/portmaster/service/resolver"
"github.com/safing/portmaster/spn/access"
"github.com/safing/portmaster/spn/captain"
)
@ -34,8 +35,7 @@ func (ss *stringSliceFlag) Set(value string) error {
var allowedClients stringSliceFlag
type Firewall struct {
mgr *mgr.Manager
mgr *mgr.Manager
instance instance
}
@ -165,4 +165,5 @@ type instance interface {
Access() *access.Access
Network() *network.Network
NetQuery() *netquery.NetQuery
Resolver() *resolver.ResolverModule
}

View file

@ -6,10 +6,12 @@ import (
"fmt"
"net"
"os"
"strings"
"sync/atomic"
"time"
"github.com/google/gopacket/layers"
"github.com/miekg/dns"
"github.com/tevino/abool"
"github.com/safing/portmaster/base/log"
@ -23,6 +25,7 @@ import (
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/process"
"github.com/safing/portmaster/service/resolver"
"github.com/safing/portmaster/spn/access"
)
@ -444,8 +447,9 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
filterConnection = false
log.Tracer(pkt.Ctx()).Infof("filter: granting own pre-authenticated connection %s", conn)
// Redirect outbound DNS packets if enabled,
// Redirect outbound DNS packets if enabled,
case dnsQueryInterception() &&
!module.instance.Resolver().IsDisabled() &&
pkt.IsOutbound() &&
pkt.Info().DstPort == 53 &&
// that don't match the address of our nameserver,
@ -478,11 +482,13 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
// Decide how to continue handling connection.
switch {
case conn.Inspecting && looksLikeOutgoingDNSRequest(conn):
inspectDNSPacket(conn, pkt)
conn.UpdateFirewallHandler(inspectDNSPacket)
case conn.Inspecting:
log.Tracer(pkt.Ctx()).Trace("filter: start inspecting")
conn.UpdateFirewallHandler(inspectAndVerdictHandler)
inspectAndVerdictHandler(conn, pkt)
default:
conn.StopFirewallHandler()
verdictHandler(conn, pkt)
@ -506,7 +512,7 @@ func FilterConnection(ctx context.Context, conn *network.Connection, pkt packet.
}
// TODO: Enable inspection framework again.
conn.Inspecting = false
// conn.Inspecting = false
// TODO: Quick fix for the SPN.
// Use inspection framework for proper encryption detection.
@ -580,6 +586,98 @@ func inspectAndVerdictHandler(conn *network.Connection, pkt packet.Packet) {
issueVerdict(conn, pkt, 0, true)
}
func inspectDNSPacket(conn *network.Connection, pkt packet.Packet) {
// Ignore info-only packets in this handler.
if pkt.InfoOnly() {
return
}
dnsPacket := new(dns.Msg)
err := pkt.LoadPacketData()
if err != nil {
_ = pkt.Block()
log.Errorf("filter: failed to load packet payload: %s", err)
return
}
// Parse and block invalid packets.
err = dnsPacket.Unpack(pkt.Payload())
if err != nil {
err = pkt.PermanentBlock()
if err != nil {
log.Errorf("filter: failed to block packet: %s", err)
}
_ = conn.SetVerdict(network.VerdictBlock, "none DNS data on DNS port", "", nil)
conn.VerdictPermanent = true
conn.Save()
return
}
// Packet was parsed.
// Allow it but only after the answer was added to the cache.
defer func() {
err = pkt.Accept()
if err != nil {
log.Errorf("filter: failed to accept dns packet: %s", err)
}
}()
// Check if packet has a question.
if len(dnsPacket.Question) == 0 {
return
}
// Read create structs with the needed data.
question := dnsPacket.Question[0]
fqdn := dns.Fqdn(question.Name)
// Check for compat check dns request.
if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
_ = compat.SubmitDNSCheckDomain(subdomain)
log.Infof("packet_handler: self-check domain received")
// No need to parse the answer.
return
}
// Check if there is an answer.
if len(dnsPacket.Answer) == 0 {
return
}
resolverInfo := &resolver.ResolverInfo{
Name: "DNSRequestObserver",
Type: resolver.ServerTypeFirewall,
Source: resolver.ServerSourceFirewall,
IP: conn.Entity.IP,
Domain: conn.Entity.Domain,
IPScope: conn.Entity.IPScope,
}
rrCache := &resolver.RRCache{
Domain: fqdn,
Question: dns.Type(question.Qtype),
RCode: dnsPacket.Rcode,
Answer: dnsPacket.Answer,
Ns: dnsPacket.Ns,
Extra: dnsPacket.Extra,
Resolver: resolverInfo,
}
query := &resolver.Query{
FQDN: fqdn,
QType: dns.Type(question.Qtype),
NoCaching: false,
IgnoreFailing: false,
LocalResolversOnly: false,
ICANNSpace: false,
DomainRoot: "",
}
// Save to cache
UpdateIPsAndCNAMEs(query, rrCache, conn)
}
func icmpFilterHandler(conn *network.Connection, pkt packet.Packet) {
// Load packet data.
err := pkt.LoadPacketData()

View file

@ -19,6 +19,8 @@ import (
"github.com/safing/portmaster/service/core/base"
"github.com/safing/portmaster/service/firewall"
"github.com/safing/portmaster/service/firewall/interception"
"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
"github.com/safing/portmaster/service/integration"
"github.com/safing/portmaster/service/intel/customlists"
"github.com/safing/portmaster/service/intel/filterlists"
"github.com/safing/portmaster/service/intel/geoip"
@ -65,6 +67,7 @@ type Instance struct {
core *core.Core
updates *updates.Updates
integration *integration.OSIntegration
geoip *geoip.GeoIP
netenv *netenv.NetEnv
ui *ui.UI
@ -74,6 +77,7 @@ type Instance struct {
firewall *firewall.Firewall
filterLists *filterlists.FilterLists
interception *interception.Interception
dnsmonitor *dnsmonitor.DNSMonitor
customlist *customlists.CustomList
status *status.Status
broadcasts *broadcasts.Broadcasts
@ -107,7 +111,6 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
var err error
// Base modules
instance.base, err = base.New(instance)
if err != nil {
@ -151,6 +154,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
if err != nil {
return instance, fmt.Errorf("create updates module: %w", err)
}
instance.integration, err = integration.New(instance)
if err != nil {
return instance, fmt.Errorf("create integration module: %w", err)
}
instance.geoip, err = geoip.New(instance)
if err != nil {
return instance, fmt.Errorf("create customlist module: %w", err)
@ -187,6 +194,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
if err != nil {
return instance, fmt.Errorf("create interception module: %w", err)
}
instance.dnsmonitor, err = dnsmonitor.New(instance)
if err != nil {
return instance, fmt.Errorf("create dns-listener module: %w", err)
}
instance.customlist, err = customlists.New(instance)
if err != nil {
return instance, fmt.Errorf("create customlist module: %w", err)
@ -275,6 +286,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.core,
instance.updates,
instance.integration,
instance.geoip,
instance.netenv,
@ -288,6 +300,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.filterLists,
instance.customlist,
instance.interception,
instance.dnsmonitor,
instance.compat,
instance.status,
@ -378,6 +391,11 @@ func (i *Instance) Updates() *updates.Updates {
return i.updates
}
// OSIntegration returns the integration module.
func (i *Instance) OSIntegration() *integration.OSIntegration {
return i.integration
}
// GeoIP returns the geoip module.
func (i *Instance) GeoIP() *geoip.GeoIP {
return i.geoip
@ -463,6 +481,11 @@ func (i *Instance) Interception() *interception.Interception {
return i.interception
}
// DNSMonitor returns the dns-listener module.
func (i *Instance) DNSMonitor() *dnsmonitor.DNSMonitor {
return i.dnsmonitor
}
// CustomList returns the customlist module.
func (i *Instance) CustomList() *customlists.CustomList {
return i.customlist

View file

@ -0,0 +1,114 @@
//go:build windows
// +build windows
package integration
import (
"fmt"
"golang.org/x/sys/windows"
)
type ETWFunctions struct {
createState *windows.Proc
initializeSession *windows.Proc
startTrace *windows.Proc
flushTrace *windows.Proc
stopTrace *windows.Proc
destroySession *windows.Proc
stopOldSession *windows.Proc
}
func initializeETW(dll *windows.DLL) (*ETWFunctions, error) {
functions := &ETWFunctions{}
var err error
functions.createState, err = dll.FindProc("PM_ETWCreateState")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWCreateState: %q", err)
}
functions.initializeSession, err = dll.FindProc("PM_ETWInitializeSession")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWInitializeSession: %q", err)
}
functions.startTrace, err = dll.FindProc("PM_ETWStartTrace")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWStartTrace: %q", err)
}
functions.flushTrace, err = dll.FindProc("PM_ETWFlushTrace")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWFlushTrace: %q", err)
}
functions.stopTrace, err = dll.FindProc("PM_ETWStopTrace")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWStopTrace: %q", err)
}
functions.destroySession, err = dll.FindProc("PM_ETWDestroySession")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
}
functions.stopOldSession, err = dll.FindProc("PM_ETWStopOldSession")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
}
return functions, nil
}
// CreateState calls the dll createState C function.
func (etw ETWFunctions) CreateState(callback uintptr) uintptr {
state, _, _ := etw.createState.Call(callback)
return state
}
// InitializeSession calls the dll initializeSession C function.
func (etw ETWFunctions) InitializeSession(state uintptr) error {
rc, _, _ := etw.initializeSession.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// StartTrace calls the dll startTrace C function.
func (etw ETWFunctions) StartTrace(state uintptr) error {
rc, _, _ := etw.startTrace.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// FlushTrace calls the dll flushTrace C function.
func (etw ETWFunctions) FlushTrace(state uintptr) error {
rc, _, _ := etw.flushTrace.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// StopTrace calls the dll stopTrace C function.
func (etw ETWFunctions) StopTrace(state uintptr) error {
rc, _, _ := etw.stopTrace.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// DestroySession calls the dll destroySession C function.
func (etw ETWFunctions) DestroySession(state uintptr) error {
rc, _, _ := etw.destroySession.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// StopOldSession calls the dll stopOldSession C function.
func (etw ETWFunctions) StopOldSession() error {
rc, _, _ := etw.stopOldSession.Call()
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}

View file

@ -0,0 +1,16 @@
//go:build !windows
// +build !windows
package integration
type OSSpecific struct{}
// Initialize is empty on any OS different then Windows.
func (i *OSIntegration) Initialize() error {
return nil
}
// CleanUp releases any resourses allocated during initializaion.
func (i *OSIntegration) CleanUp() error {
return nil
}

View file

@ -0,0 +1,86 @@
//go:build windows
// +build windows
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
}
// 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.GetPlatformFile("dll/portmaster-core.dll")
if err != nil {
return err
}
// Load the DLL.
i.os.dll, err = windows.LoadDLL(file.Path())
if err != nil {
return fmt.Errorf("failed to load dll: %q", err)
}
// Enumerate all needed dll functions.
i.os.etwFunctions, err = initializeETW(i.os.dll)
if err != nil {
return err
}
// Notify listeners
i.OnInitializedEvent.Submit(struct{}{})
return nil
}
// CleanUp releases any resources allocated during initialization.
func (i *OSIntegration) CleanUp() error {
if i.os.dll != nil {
return i.os.dll.Release()
}
return nil
}
// 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

@ -0,0 +1,49 @@
package integration
import (
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates"
)
// OSIntegration module provides special integration with the OS.
type OSIntegration struct {
m *mgr.Manager
OnInitializedEvent *mgr.EventMgr[struct{}]
//nolint:unused
os OSSpecific
instance instance
}
// New returns a new OSIntegration module.
func New(instance instance) (*OSIntegration, error) {
m := mgr.New("OSIntegration")
module := &OSIntegration{
m: m,
OnInitializedEvent: mgr.NewEventMgr[struct{}]("on-initialized", m),
instance: instance,
}
return module, nil
}
// Manager returns the module manager.
func (i *OSIntegration) Manager() *mgr.Manager {
return i.m
}
// Start starts the module.
func (i *OSIntegration) Start() error {
return i.Initialize()
}
// Stop stops the module.
func (i *OSIntegration) Stop() error {
return i.CleanUp()
}
type instance interface {
Updates() *updates.Updates
}

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

@ -224,8 +224,8 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
}
// Save the request as open, as we don't know if there will be a connection or not.
network.SaveOpenDNSRequest(q, rrCache, conn)
firewall.UpdateIPsAndCNAMEs(q, rrCache, conn)
network.SaveOpenDNSRequest(q, rrCache, conn)
case network.VerdictUndeterminable:
fallthrough

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

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net"
"runtime"
"sync"
"sync/atomic"
"time"
@ -18,6 +19,7 @@ import (
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/network/reference"
"github.com/safing/portmaster/service/process"
_ "github.com/safing/portmaster/service/process/tags"
"github.com/safing/portmaster/service/resolver"
@ -536,12 +538,41 @@ 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())
}
if runtime.GOOS == "windows" && err != nil {
// On windows domains may come with delay.
if module.instance.Resolver().IsDisabled() && conn.shouldWaitForDomain() {
// Flush the dns listener buffer and try again.
for i := range 4 {
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)
break
}
time.Sleep(5 * time.Millisecond)
}
}
}
if err == nil {
lastResolvedDomain := ipinfo.MostRecentDomain()
if lastResolvedDomain != nil {
@ -869,3 +900,17 @@ func (conn *Connection) String() string {
return fmt.Sprintf("%s -> %s", conn.process, conn.Entity.IP)
}
}
func (conn *Connection) shouldWaitForDomain() bool {
// Should wait for Global Unicast, outgoing and not ICMP connections
switch {
case conn.Entity.IPScope != netutils.Global:
return false
case conn.Inbound:
return false
case reference.IsICMP(conn.Entity.Protocol):
return false
}
return true
}

View file

@ -9,10 +9,12 @@ import (
"sync/atomic"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/state"
"github.com/safing/portmaster/service/profile"
"github.com/safing/portmaster/service/resolver"
)
// Events.
@ -188,4 +190,6 @@ func New(instance instance) (*Network, error) {
type instance interface {
Profile() *profile.ProfileModule
Resolver() *resolver.ResolverModule
DNSMonitor() *dnsmonitor.DNSMonitor
}

View file

@ -106,11 +106,12 @@ func checkError(packet gopacket.Packet, info *Info) error {
return nil
}
// Parse parses an IP packet and saves the information in the given packet object.
func Parse(packetData []byte, pktBase *Base) (err error) {
// ParseLayer3 parses an IP packet and saves the information in the given packet object.
func ParseLayer3(packetData []byte, pktBase *Base) (err error) {
if len(packetData) == 0 {
return errors.New("empty packet")
}
pktBase.layer3Data = packetData
ipVersion := packetData[0] >> 4
@ -155,6 +156,62 @@ func Parse(packetData []byte, pktBase *Base) (err error) {
return nil
}
// ParseLayer4 parses an layer 4 packet and saves the information in the given packet object.
func ParseLayer4(packetData []byte, pktBase *Base) (err error) {
if len(packetData) == 0 {
return errors.New("empty packet")
}
var layer gopacket.LayerType
switch pktBase.info.Protocol {
case ICMP:
layer = layers.LayerTypeICMPv4
case IGMP:
layer = layers.LayerTypeIGMP
case TCP:
layer = layers.LayerTypeTCP
case UDP:
layer = layers.LayerTypeUDP
case ICMPv6:
layer = layers.LayerTypeICMPv6
case UDPLite:
return fmt.Errorf("UDPLite not supported")
case RAW:
return fmt.Errorf("RAW protocol not supported")
case AnyHostInternalProtocol61:
return fmt.Errorf("AnyHostInternalProtocol61 protocol not supported")
default:
return fmt.Errorf("protocol not supported")
}
packet := gopacket.NewPacket(packetData, layer, gopacket.DecodeOptions{
Lazy: true,
NoCopy: true,
})
availableDecoders := []func(gopacket.Packet, *Info) error{
parseTCP,
parseUDP,
// parseUDPLite, // We don't yet support udplite.
parseICMPv4,
parseICMPv6,
parseIGMP,
checkError,
}
for _, dec := range availableDecoders {
if err := dec(packet, pktBase.Info()); err != nil {
return err
}
}
pktBase.layers = packet
if transport := packet.TransportLayer(); transport != nil {
pktBase.layer5Data = transport.LayerPayload()
}
return nil
}
func init() {
genIPProtocolFromLayerType()
}

View file

@ -256,7 +256,7 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
// Username
process.UserName, err = pInfo.UsernameWithContext(ctx)
if err != nil {
return nil, fmt.Errorf("process: failed to get Username for p%d: %w", pInfo.Pid, err)
log.Tracer(ctx).Warningf("process: failed to get username (PID %d): %s", pInfo.Pid, err)
}
// TODO: User Home

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

Some files were not shown because too many files have changed in this diff Show more