Merge branch 'v2.0' into task/refactor-spn

This commit is contained in:
Natanael Rodriguez Ramos
2025-05-28 11:41:10 +01:00
53 changed files with 2417 additions and 141 deletions

View File

@@ -1,43 +1,51 @@
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/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(module.instance.DataDir(), "databases", "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(module.instance.DataDir(), "databases", "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

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

View File

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

@@ -144,7 +144,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/service/mgr"
"github.com/safing/portmaster/service/updates"
@@ -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

@@ -118,6 +118,8 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool {
var previousPid int
proc := p
hasPmWebviewEnvVar := false
for i := 0; i < checkLevels; i++ {
if proc.Pid == UnidentifiedProcessID || proc.Pid == SystemProcessID {
break
@@ -125,7 +127,43 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool {
realPath, err := filepath.EvalSymlinks(proc.Path)
if err == nil && realPath == module.portmasterUIPath {
return true
if runtime.GOOS != "windows" {
return true
}
// On Windows, avoid false positive detection of the Portmaster UI.
// For example:
// There may be cases where a system browser is launched from the Portmaster UI,
// making it a child of the Portmaster UI process (e.g., user clicked a link in the UI).
// In this case, the parent process tree may look like this:
// Portmaster.exe
// ├─ WebView (PM UI)
// │ └─ WebView (PM UI child)
// └─ System Web Browser ...
//
// To ensure that 'p' is the actual Portmaster UI process, we check for the presence
// of the 'PORTMASTER_UI_WEBVIEW_PROCESS' environment variable in the process and its parents.
// If the env var is set, we are a child (WebView window) of the Portmaster UI process.
// Otherwise, the process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process.
if i == 0 {
return true // We are the main Portmaster UI process.
}
if hasPmWebviewEnvVar {
return true // We are a WebView window of the Portmaster UI process.
}
// The process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process.
log.Tracer(ctx).Warningf("process: %d %q is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path)
return false
}
// Check if the process has the environment variable set.
//
// It is OK to check for the existence of the environment variable in all
// processes in the parent chain (on all loop iterations). This increases the
// chance of correct detection, even if a child or grandchild WebView process
// did not inherit the environment variable for some reason.
if _, ok := proc.Env["PORTMASTER_UI_WEBVIEW_PROCESS"]; ok {
hasPmWebviewEnvVar = true
}
if i < checkLevels-1 { // no need to check parent if we are at the last level

View File

@@ -237,14 +237,20 @@ func createSpecialProfile(profileID string, path string) *Profile {
Source: SourceLocal,
PresentationPath: path,
Config: map[string]interface{}{
// Block all connections by default for the Portmaster UI profile,
// since the only required connections are to the Portmaster Core,
// which are fast-tracked.
//
// This ensures that any unexpected connections —
// possibly made by the internal WebView implementation —
// are blocked.
CfgOptionDefaultActionKey: DefaultActionBlockValue,
CfgOptionBlockScopeInternetKey: false,
CfgOptionBlockScopeLANKey: false,
CfgOptionBlockScopeLocalKey: false,
CfgOptionBlockP2PKey: false,
CfgOptionBlockScopeInternetKey: false, // This is stronger than the rules, and thus must be false in order to access safing.io.
CfgOptionBlockScopeLANKey: true,
CfgOptionBlockScopeLocalKey: true,
CfgOptionBlockP2PKey: true,
CfgOptionBlockInboundKey: true,
CfgOptionEndpointsKey: []string{
"+ Localhost",
"+ .safing.io",
},
},