Merge pull request #22 from safing/feature/config-consolidation

Config consolidation
This commit is contained in:
Daniel
2020-04-01 19:42:37 +02:00
committed by GitHub
108 changed files with 3809 additions and 2877 deletions

View File

@@ -7,4 +7,15 @@ linters:
- funlen
- whitespace
- wsl
- godox
- gomnd
linters-settings:
godox:
# report any comments starting with keywords, this is useful for TODO or FIXME comments that
# might be left in the code accidentally and should be resolved before merging
keywords:
- FIXME
# gocognit:
# min-complexity: 50
# gocyclo:
# min-complexity: 50

View File

@@ -1,5 +1,8 @@
language: go
go:
- 1.x
os:
- linux
- windows

143
Gopkg.lock generated
View File

@@ -2,137 +2,118 @@
[[projects]]
digest = "1:e92f5581902c345eb4ceffdcd4a854fb8f73cf436d47d837d1ec98ef1fe0a214"
digest = "1:f82b8ac36058904227087141017bb82f4b0fc58272990a4cdae3e2d6d222644e"
name = "github.com/StackExchange/wmi"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "5d049714c4a64225c3c79a7cf7d02f7fb5b96338"
version = "1.0.0"
[[projects]]
digest = "1:90afd0cfdffcc3df7855160ee2954cbca286e23c4eb9cb3d075536c9e4e1b04f"
digest = "1:3c753679736345f50125ae993e0a2614da126859921ea7faeecda6d217501ce2"
name = "github.com/agext/levenshtein"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "0ded9c86537917af2ff89bc9c78de6bd58477894"
version = "v1.2.2"
[[projects]]
digest = "1:e097b70eeb55d10325f576ec2f97221d8ae18a3569cb0bc404863bde944788b8"
name = "github.com/cloudflare/cfssl"
packages = [
"crypto/pkcs7",
"errors",
]
pruneopts = "UT"
revision = "768cd563887febaad559b511aaa5964823ccb4ab"
version = "1.3.3"
[[projects]]
branch = "v2.1"
digest = "1:1a2c0956b86c464f8ee91a46e0947dac7251f1d6fb7d3ad89cb9b376273ece82"
digest = "1:3fc5d0d9cb474736e8e6c2f2292e0763b5132c6e7d8cbedf7bde404a470c8c3b"
name = "github.com/cookieo9/resources-go"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "d27c04069d0d5dfe11c202dacbf745ae8d1ab181"
[[projects]]
digest = "1:3b4fcce8036672adc21c09d8e288f7b7590e76a468fae5a13346366e778e5ff8"
digest = "1:166e24c91c2732657d2f791d3ee3897e7d85ece7cbb62ad991250e6b51fc1d4c"
name = "github.com/coreos/go-iptables"
packages = ["iptables"]
pruneopts = "UT"
pruneopts = ""
revision = "78b5fff24e6df8886ef8eca9411f683a884349a5"
version = "v0.4.1"
[[projects]]
digest = "1:440028f55cb322d8cb5b9d5ebec298a00b7d74690a658fe6b1c0c0b44341bfae"
digest = "1:b6581f9180e0f2d5549280d71819ab951db9d511478c87daca95669589d505c0"
name = "github.com/go-ole/go-ole"
packages = [
".",
"oleutil",
]
pruneopts = "UT"
pruneopts = ""
revision = "97b6244175ae18ea6eef668034fd6565847501c9"
version = "v1.2.4"
[[projects]]
digest = "1:b3d20bcdedab2050e6bc58e52f4fdc46f710b4c74e1a1ecee262ebec1aee7b6e"
digest = "1:cc1255e2fef3819bfab3540277001e602892dd431ef9ab5499bcdbc425923d64"
name = "github.com/godbus/dbus"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "2ff6f7ffd60f0f2410b3105864bdd12c7894f844"
version = "v5.0.1"
[[projects]]
digest = "1:0d40612c9acf7173aa742ee335bf344524add9c8d7e818992b610f3982350101"
digest = "1:e85e59c4152d8576341daf54f40d96c404c264e04941a4a36b97a0f427eb9e5e"
name = "github.com/google/gopacket"
packages = [
".",
"layers",
"tcpassembly",
]
pruneopts = "UT"
pruneopts = ""
revision = "6d3e2615da4ed2ed2a349918fe74e7e6d03482fa"
version = "v1.1.17"
[[projects]]
digest = "1:c3388642e07731a240e14f4bc7207df59cfcc009447c657b9de87fec072d07e3"
digest = "1:20dc576ad8f98fe64777c62f090a9b37dd67c62b23fe42b429c2c41936aa8a9c"
name = "github.com/google/renameio"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "f0e32980c006571efd537032e5f9cd8c1a92819e"
version = "v0.1.0"
[[projects]]
digest = "1:88e0b0baeb9072f0a4afbcf12dda615fc8be001d1802357538591155998da21b"
name = "github.com/hashicorp/go-version"
packages = ["."]
pruneopts = "UT"
revision = "ac23dc3fea5d1a983c43f6a0f6e2c13f0195d8bd"
version = "v1.2.0"
[[projects]]
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
branch = "master"
digest = "1:f433da6fbabb0bd1bd730fd340782015c644123d02eb78c20c69ce03352daad1"
digest = "1:e71cc6b377264002aec0d9c235087e51ad7a3c1fb341bb4baa84709308b94fe8"
name = "github.com/kardianos/osext"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "2bc1f35cddc0cc527b4bc3dce8578fc2a6c11384"
[[projects]]
digest = "1:66def2f80d5127123c81ffa876242c12d3b7b06188f6645d79b3cc4acba637b3"
digest = "1:0b6694f306890ddbb69c96a16776510bd24e07436fae3f9b0a4e5b650f1e6fb7"
name = "github.com/miekg/dns"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "b13675009d59c97f3721247d9efa8914e1866a5b"
version = "v1.1.15"
[[projects]]
digest = "1:8186cc518f5dcea736d7725e284632937971fdf29fa2953fdfa53f631e21a799"
digest = "1:3819cd861b7abd7d12dc1ea52ecb998ad1171826a76ecf0aefa09545781091f9"
name = "github.com/oschwald/maxminddb-golang"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "2905694a1b00c5574f1418a7dbf8a22a7d247559"
version = "v1.3.1"
[[projects]]
digest = "1:274f67cb6fed9588ea2521ecdac05a6d62a8c51c074c1fccc6a49a40ba80e925"
digest = "1:7f569d906bdd20d906b606415b7d794f798f91a62fcfb6a4daa6d50690fb7a3f"
name = "github.com/satori/go.uuid"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
digest = "1:36ff5e7a8aa3e7d01b9ca22d441484d9c43247b6e34e379c404d17a2e0098091"
digest = "1:8bf42eb2ded52ed2678b0716dbfbf30628765bc12b13222c4d5669ba4c1310e4"
name = "github.com/shirou/gopsutil"
packages = [
"cpu",
@@ -141,7 +122,7 @@
"net",
"process",
]
pruneopts = "UT"
pruneopts = ""
revision = "4c8b404ee5c53b04b04f34b1744a26bf5d2910de"
version = "v2.19.6"
@@ -150,79 +131,74 @@
digest = "1:99c6a6dab47067c9b898e8c8b13d130c6ab4ffbcc4b7cc6236c2cd0b1e344f5b"
name = "github.com/shirou/w32"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b"
[[projects]]
digest = "1:e096613fb7cf34743d49af87d197663cfccd61876e2219853005a57baedfa562"
digest = "1:0c63b3c7ad6d825a898f28cb854252a3b29d37700c68a117a977263f5ec94efe"
name = "github.com/spf13/cobra"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "f2b07da1e2c38d5f12845a4f607e2e1018cbb1f5"
version = "v0.0.5"
[[projects]]
digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2"
digest = "1:cbaf13cdbfef0e4734ed8a7504f57fe893d471d62a35b982bf6fb3f036449a66"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
version = "v1.0.3"
[[projects]]
branch = "master"
digest = "1:93d6687fc19da8a35c7352d72117a6acd2072dfb7e9bfd65646227bf2a913b2a"
digest = "1:86e6712cfd4070a2120c03fcec41cfcbbc51813504a74e28d74479edfaf669ee"
name = "github.com/tevino/abool"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "9b9efcf221b50905aab9bbabd3daed56dc10f339"
[[projects]]
branch = "master"
digest = "1:2a9f7ca80c3035b7e68a927db100922d4a266a31cea2568d9a5185a90b469279"
digest = "1:21097653bd7914de1262f2429e277933507442f892815a791ce1c0dbf0a8dc20"
name = "github.com/umahmood/haversine"
packages = ["."]
pruneopts = "UT"
pruneopts = ""
revision = "808ab04add26660fd241ddb7973886c6dd6669e8"
[[projects]]
branch = "master"
digest = "1:bf3f59dec5aa79945bd4b45c0c649c99d03609565acdd6a087f4cd6e9e5b5544"
digest = "1:086760278d762dbb0e9a26e09b57f04c89178c86467d8d94fae47d64c222f328"
name = "golang.org/x/crypto"
packages = [
"chacha20poly1305",
"curve25519",
"ed25519",
"ed25519/internal/edwards25519",
"internal/chacha20",
"internal/subtle",
"ocsp",
"poly1305",
]
pruneopts = "UT"
pruneopts = ""
revision = "4def268fd1a49955bfb3dda92fe3db4f924f2285"
[[projects]]
branch = "master"
digest = "1:caffb9a4f8c756941de4b3eb577abd167e7fd4b570f2078c05ceb8835a1514cb"
digest = "1:31cd6e3c114e17c5f0c9e8b0bcaa3025ab3c221ce36323c7ce1acaa753d0d0aa"
name = "golang.org/x/net"
packages = [
"bpf",
"icmp",
"idna",
"internal/iana",
"internal/socket",
"ipv4",
"ipv6",
"publicsuffix",
]
pruneopts = "UT"
pruneopts = ""
revision = "da137c7871d730100384dbcf36e6f8fa493aef5b"
[[projects]]
branch = "master"
digest = "1:84945c0665ea5fc3ccbd067c35890a7d28e369131ac411b8a820b40115245c19"
digest = "1:2579a16d8afda9c9a475808c13324f5e572852e8927905ffa15bb14e71baba4f"
name = "golang.org/x/sys"
packages = [
"cpu",
"unix",
"windows",
"windows/registry",
@@ -231,15 +207,39 @@
"windows/svc/eventlog",
"windows/svc/mgr",
]
pruneopts = "UT"
pruneopts = ""
revision = "04f50cda93cbb67f2afa353c52f342100e80e625"
[[projects]]
digest = "1:740b51a55815493a8d0f2b1e0d0ae48fe48953bf7eaf3fcc4198823bf67768c0"
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/language",
"internal/language/compact",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable",
]
pruneopts = ""
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
version = "v0.3.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/agext/levenshtein",
"github.com/cloudflare/cfssl/crypto/pkcs7",
"github.com/cookieo9/resources-go",
"github.com/coreos/go-iptables/iptables",
"github.com/godbus/dbus",
@@ -247,7 +247,6 @@
"github.com/google/gopacket/layers",
"github.com/google/gopacket/tcpassembly",
"github.com/google/renameio",
"github.com/hashicorp/go-version",
"github.com/miekg/dns",
"github.com/oschwald/maxminddb-golang",
"github.com/satori/go.uuid",
@@ -255,11 +254,9 @@
"github.com/spf13/cobra",
"github.com/tevino/abool",
"github.com/umahmood/haversine",
"golang.org/x/crypto/chacha20poly1305",
"golang.org/x/crypto/curve25519",
"golang.org/x/crypto/ocsp",
"golang.org/x/net/icmp",
"golang.org/x/net/ipv4",
"golang.org/x/net/publicsuffix",
"golang.org/x/sys/windows",
"golang.org/x/sys/windows/svc",
"golang.org/x/sys/windows/svc/debug",

View File

@@ -25,79 +25,3 @@
# unused-packages = true
ignored = ["github.com/safing/portbase/*"]
[[constraint]]
name = "github.com/agext/levenshtein"
version = "1.2.2"
[[constraint]]
name = "github.com/cloudflare/cfssl"
version = "1.3.3"
[[constraint]]
branch = "v2.1"
name = "github.com/cookieo9/resources-go"
[[constraint]]
name = "github.com/coreos/go-iptables"
version = "0.4.1"
[[constraint]]
name = "github.com/godbus/dbus"
version = "5.0.1"
[[constraint]]
name = "github.com/google/gopacket"
version = "1.1.17"
[[constraint]]
name = "github.com/google/renameio"
version = "0.1.0"
[[constraint]]
name = "github.com/miekg/dns"
version = "1.1.15"
[[constraint]]
name = "github.com/oschwald/maxminddb-golang"
version = "1.3.1"
[[constraint]]
name = "github.com/satori/go.uuid"
version = "1.2.0"
[[constraint]]
name = "github.com/shirou/gopsutil"
version = "2.19.6"
[[constraint]]
name = "github.com/spf13/cobra"
version = "0.0.5"
[[constraint]]
branch = "master"
name = "github.com/tevino/abool"
[[constraint]]
branch = "master"
name = "github.com/umahmood/haversine"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
branch = "master"
name = "golang.org/x/net"
[[constraint]]
branch = "master"
name = "golang.org/x/sys"
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "github.com/hashicorp/go-version"
version = "1.2.0"

View File

@@ -1,64 +0,0 @@
package core
import (
"errors"
"flag"
"github.com/safing/portbase/config"
"github.com/safing/portbase/api"
"github.com/safing/portbase/database/dbmodule"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/notifications"
"github.com/safing/portmaster/core/structure"
)
var (
dataDir string
databaseDir string
)
func init() {
flag.StringVar(&dataDir, "data", "", "set data directory")
flag.StringVar(&databaseDir, "db", "", "alias to --data (deprecated)")
modules.Register("base", prepBase, nil, nil, "info")
}
func prepBase() error {
// backwards compatibility
if dataDir == "" {
dataDir = databaseDir
}
// check data dir
if dataDir == "" {
return errors.New("please set the data directory using --data=/path/to/data/dir")
}
// initialize structure
err := structure.Initialize(dataDir, 0755)
if err != nil {
return err
}
// set data location
dbmodule.SetDatabaseLocation("", structure.Root())
config.SetDataRoot(structure.Root())
// init config
logFlagOverrides()
err = registerConfig()
if err != nil {
return err
}
// set api listen address
api.SetDefaultAPIListenAddress("127.0.0.1:817")
// set notification persistence
notifications.SetPersistenceBasePath("core:notifications")
return nil
}

View File

@@ -8,6 +8,7 @@ import (
)
var (
CfgDevModeKey = "core/devMode"
defaultDevMode bool
)
@@ -24,7 +25,7 @@ func logFlagOverrides() {
func registerConfig() error {
err := config.Register(&config.Option{
Name: "Development Mode",
Key: "core/devMode",
Key: CfgDevModeKey,
Description: "In Development Mode security restrictions are lifted/softened to enable easier access to Portmaster for debugging and testing purposes.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelDeveloper,

View File

@@ -3,14 +3,28 @@ package core
import (
"fmt"
"github.com/safing/portbase/modules/subsystems"
"github.com/safing/portbase/modules"
)
var (
module *modules.Module
)
func init() {
modules.Register("core", nil, startCore, nil, "base", "database", "config", "api", "random")
module = modules.Register("core", nil, start, nil, "database", "config", "api", "random", "notifications", "subsystems", "ui", "updates", "status")
subsystems.Register(
"core",
"Core",
"Base Structure and System Integration",
module,
"config:core/",
nil,
)
}
func startCore() error {
func start() error {
if err := startPlatformSpecific(); err != nil {
return fmt.Errorf("failed to start plattform-specific components: %s", err)
}

View File

@@ -3,15 +3,22 @@ package core
import (
"github.com/safing/portbase/database"
// database module
_ "github.com/safing/portbase/database/dbmodule"
// module dependencies
_ "github.com/safing/portbase/database/storage/bbolt"
)
var (
DefaultDatabaseStorageType = "bbolt"
)
func registerDatabases() error {
_, err := database.Register(&database.Database{
Name: "core",
Description: "Holds core data, such as settings and profiles",
StorageType: "bbolt",
StorageType: DefaultDatabaseStorageType,
PrimaryAPI: "",
})
if err != nil {
@@ -21,7 +28,7 @@ func registerDatabases() error {
_, err = database.Register(&database.Database{
Name: "cache",
Description: "Cached data, such as Intelligence and DNS Records",
StorageType: "bbolt",
StorageType: DefaultDatabaseStorageType,
PrimaryAPI: "",
})
if err != nil {
@@ -31,7 +38,7 @@ func registerDatabases() error {
// _, err = database.Register(&database.Database{
// Name: "history",
// Description: "Historic event data",
// StorageType: "bbolt",
// StorageType: DefaultDatabaseStorageType,
// PrimaryAPI: "",
// })
// if err != nil {

65
core/global.go Normal file
View File

@@ -0,0 +1,65 @@
package core
import (
"errors"
"flag"
"github.com/safing/portbase/modules/subsystems"
"github.com/safing/portbase/api"
"github.com/safing/portbase/dataroot"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/notifications"
)
var (
dataDir string
databaseDir string
)
func init() {
flag.StringVar(&dataDir, "data", "", "set data directory")
flag.StringVar(&databaseDir, "db", "", "alias to --data (deprecated)")
modules.SetGlobalPrepFn(globalPrep)
}
func globalPrep() error {
if dataroot.Root() == nil {
// initialize data dir
// backwards compatibility
if dataDir == "" {
dataDir = databaseDir
}
// check data dir
if dataDir == "" {
return errors.New("please set the data directory using --data=/path/to/data/dir")
}
// initialize structure
err := dataroot.Initialize(dataDir, 0755)
if err != nil {
return err
}
}
// init config
logFlagOverrides()
err := registerConfig()
if err != nil {
return err
}
// set api listen address
api.SetDefaultAPIListenAddress("127.0.0.1:817")
// set notification persistence
notifications.SetPersistenceBasePath("core:notifications")
// set subsystem status dir
subsystems.SetDatabaseKeySpace("core:status/subsystems")
return nil
}

81
core/pmtesting/testing.go Normal file
View File

@@ -0,0 +1,81 @@
// package coretest provides a simple unit test setup routine.
//
// Just include `_ "github.com/safing/portmaster/core/pmtesting"`
//
package pmtesting
import (
"flag"
"fmt"
"os"
"path/filepath"
"runtime/pprof"
"testing"
"github.com/safing/portbase/dataroot"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/core"
// module dependencies
_ "github.com/safing/portbase/database/storage/hashmap"
)
var (
printStackOnExit bool
)
func init() {
flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down")
}
func TestMain(m *testing.M) {
// switch databases to memory only
core.DefaultDatabaseStorageType = "hashmap"
// set log level
log.SetLogLevel(log.TraceLevel)
// tmp dir for data root (db & config)
tmpDir := filepath.Join(os.TempDir(), "portmaster-testing")
// initialize data dir
err := dataroot.Initialize(tmpDir, 0755)
// start modules
if err == nil {
err = modules.Start()
}
// handle setup error
if err != nil {
fmt.Fprintf(os.Stderr, "failed to setup test: %s\n", err)
printStack()
os.Exit(1)
}
// run tests
exitCode := m.Run()
// shutdown
_ = modules.Shutdown()
if modules.GetExitStatusCode() != 0 {
exitCode = modules.GetExitStatusCode()
fmt.Fprintf(os.Stderr, "failed to cleanly shutdown test: %s\n", err)
}
printStack()
// clean up and exit
// keep! os.RemoveAll(tmpDir)
os.Exit(exitCode)
}
func printStack() {
if printStackOnExit {
fmt.Println("=== PRINTING TRACES ===")
fmt.Println("=== GOROUTINES ===")
_ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)
fmt.Println("=== BLOCKING ===")
_ = pprof.Lookup("block").WriteTo(os.Stdout, 2)
fmt.Println("=== MUTEXES ===")
_ = pprof.Lookup("mutex").WriteTo(os.Stdout, 2)
fmt.Println("=== END TRACES ===")
}
}

View File

@@ -1,27 +0,0 @@
package structure
import (
"os"
"github.com/safing/portbase/utils"
)
var (
root *utils.DirStructure
)
// Initialize initializes the data root directory
func Initialize(rootDir string, perm os.FileMode) error {
root = utils.NewDirStructure(rootDir, perm)
return root.Ensure()
}
// Root returns the data root directory.
func Root() *utils.DirStructure {
return root
}
// NewRootDir calls ChildDir() on the data root directory.
func NewRootDir(dirName string, perm os.FileMode) (childDir *utils.DirStructure) {
return root.ChildDir(dirName, perm)
}

View File

@@ -1,70 +0,0 @@
package core
import (
"io/ioutil"
"os"
"github.com/safing/portbase/log"
"github.com/safing/portbase/database"
// module dependencies
_ "github.com/safing/portbase/database/storage/hashmap"
)
// InitForTesting initializes the core module directly. This is intended to be only used by unit tests that require the core (and depending) modules.
func InitForTesting() (tmpDir string, err error) {
tmpDir, err = ioutil.TempDir(os.TempDir(), "pm-testing-")
if err != nil {
return "", err
}
err = database.Initialize(tmpDir, nil)
if err != nil {
return "", err
}
_, err = database.Register(&database.Database{
Name: "core",
Description: "Holds core data, such as settings and profiles",
StorageType: "hashmap",
PrimaryAPI: "",
})
if err != nil {
return "", err
}
_, err = database.Register(&database.Database{
Name: "cache",
Description: "Cached data, such as Intelligence and DNS Records",
StorageType: "hashmap",
PrimaryAPI: "",
})
if err != nil {
return "", err
}
// _, err = database.Register(&database.Database{
// Name: "history",
// Description: "Historic event data",
// StorageType: "hashmap",
// PrimaryAPI: "",
// })
// if err != nil {
// return err
// }
// start logging
err = log.Start()
if err != nil {
return "", err
}
log.SetLogLevel(log.TraceLevel)
return tmpDir, nil
}
// StopTesting shuts the test environment.
func StopTesting() {
log.Shutdown()
}

View File

@@ -9,15 +9,12 @@ import (
"strconv"
"strings"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/core/structure"
"github.com/safing/portbase/api"
"github.com/safing/portbase/dataroot"
"github.com/safing/portbase/log"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process"
"github.com/safing/portbase/api"
)
var (
@@ -28,7 +25,7 @@ var (
)
func prepAPIAuth() error {
dataRoot = structure.Root()
dataRoot = dataroot.Root()
return api.SetAuthenticator(apiAuthenticator)
}

View File

@@ -2,14 +2,16 @@ package firewall
import (
"github.com/safing/portbase/config"
"github.com/safing/portmaster/status"
)
var (
permanentVerdicts config.BoolOption
filterDNSByScope status.SecurityLevelOption
filterDNSByProfile status.SecurityLevelOption
promptTimeout config.IntOption
CfgOptionEnableFilterKey = "filter/enable"
CfgOptionPermanentVerdictsKey = "filter/permanentVerdicts"
permanentVerdicts config.BoolOption
CfgOptionPromptTimeoutKey = "filter/promptTimeout"
promptTimeout config.IntOption
devMode config.BoolOption
apiListenAddress config.StringOption
@@ -18,7 +20,7 @@ var (
func registerConfig() error {
err := config.Register(&config.Option{
Name: "Permanent Verdicts",
Key: "firewall/permanentVerdicts",
Key: CfgOptionPermanentVerdictsKey,
Description: "With permanent verdicts, control of a connection is fully handed back to the OS after the initial decision. This brings a great performance increase, but makes it impossible to change the decision of a link later on.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelExpert,
@@ -28,43 +30,11 @@ func registerConfig() error {
if err != nil {
return err
}
permanentVerdicts = config.Concurrent.GetAsBool("firewall/permanentVerdicts", true)
err = config.Register(&config.Option{
Name: "Filter DNS Responses by Server Scope",
Key: "firewall/filterDNSByScope",
Description: "This option will filter out DNS answers that are outside of the scope of the server. A server on the public Internet may not respond with a private LAN address.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelBeta,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
filterDNSByScope = status.ConfigIsActiveConcurrent("firewall/filterDNSByScope")
err = config.Register(&config.Option{
Name: "Filter DNS Responses by Application Profile",
Key: "firewall/filterDNSByProfile",
Description: "This option will filter out DNS answers that an application would not be allowed to connect, based on its profile.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelBeta,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
filterDNSByProfile = status.ConfigIsActiveConcurrent("firewall/filterDNSByProfile")
permanentVerdicts = config.Concurrent.GetAsBool(CfgOptionPermanentVerdictsKey, true)
err = config.Register(&config.Option{
Name: "Timeout for prompt notifications",
Key: "firewall/promptTimeout",
Key: CfgOptionPromptTimeoutKey,
Description: "Amount of time how long Portmaster will wait for a response when prompting about a connection via a notification. In seconds.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelUser,
@@ -74,9 +44,9 @@ func registerConfig() error {
if err != nil {
return err
}
promptTimeout = config.Concurrent.GetAsInt("firewall/promptTimeout", 30)
promptTimeout = config.Concurrent.GetAsInt(CfgOptionPromptTimeoutKey, 60)
devMode = config.Concurrent.GetAsBool("firewall/permanentVerdicts", false)
devMode = config.Concurrent.GetAsBool("core/devMode", false)
apiListenAddress = config.GetAsString("api/listenAddress", "")
return nil

View File

@@ -4,12 +4,12 @@ import (
"fmt"
"net"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network/environment"
"github.com/safing/portmaster/resolver"
)
func init() {
intel.SetLocalAddrFactory(PermittedAddr)
resolver.SetLocalAddrFactory(PermittedAddr)
environment.SetLocalAddrFactory(PermittedAddr)
}

View File

@@ -7,6 +7,9 @@ import (
"sync/atomic"
"time"
"github.com/safing/portbase/config"
"github.com/safing/portbase/modules/subsystems"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/firewall/inspection"
@@ -41,11 +44,26 @@ var (
)
func init() {
module = modules.Register("firewall", prep, start, stop, "core", "network", "nameserver", "profile", "updates")
module = modules.Register("firewall", prep, start, stop, "core", "network", "resolver", "intel", "processes")
subsystems.Register(
"filter",
"Privacy Filter",
"DNS and Network Filter",
module,
"config:filter/",
&config.Option{
Name: "Enable Privacy Filter",
Key: CfgOptionEnableFilterKey,
Description: "Enable the Privacy Filter Subsystem to filter DNS queries and network requests.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelBeta,
DefaultValue: true,
},
)
}
func prep() (err error) {
err = registerConfig()
if err != nil {
return err
@@ -188,22 +206,18 @@ func handlePacket(pkt packet.Packet) {
// associate packet to link and handle
link, created := network.GetOrCreateLinkByPacket(pkt)
defer func() {
go link.SaveIfNeeded()
}()
if created {
link.SetFirewallHandler(initialHandler)
link.HandlePacket(pkt)
return
}
if link.FirewallHandlerIsSet() {
link.HandlePacket(pkt)
return
}
issueVerdict(pkt, link, 0, true)
link.HandlePacket(pkt)
}
func initialHandler(pkt packet.Packet, link *network.Link) {
defer func() {
go link.SaveIfNeeded()
}()
log.Tracer(pkt.Ctx()).Trace("firewall: [initial handler]")
// check for internal firewall bypass
@@ -217,9 +231,6 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
} else {
comm.AddLink(link)
}
defer func() {
go comm.SaveIfNeeded()
}()
// approve
link.Accept("internally approved")
@@ -249,9 +260,6 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
return
}
}
defer func() {
go comm.SaveIfNeeded()
}()
// add new Link to Communication (and save both)
comm.AddLink(link)
@@ -267,7 +275,8 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
log.Tracer(pkt.Ctx()).Trace("firewall: starting decision process")
DecideOnCommunication(comm, pkt)
// TODO: filter lists may have IPs in the future!
DecideOnCommunication(comm)
DecideOnLink(comm, link, pkt)
// TODO: link this to real status
@@ -380,7 +389,7 @@ func issueVerdict(pkt packet.Packet, link *network.Link, verdict network.Verdict
func run() {
for {
select {
case <-modules.ShuttingDown():
case <-module.Stopping():
return
case pkt := <-interception.Packets:
handlePacket(pkt)
@@ -391,7 +400,7 @@ func run() {
func statLogger() {
for {
select {
case <-modules.ShuttingDown():
case <-module.Stopping():
return
case <-time.After(10 * time.Second):
log.Tracef("firewall: packets accepted %d, blocked %d, dropped %d", atomic.LoadUint64(packetsAccepted), atomic.LoadUint64(packetsBlocked), atomic.LoadUint64(packetsDropped))

View File

@@ -4,19 +4,20 @@ import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process"
"github.com/safing/portmaster/profile"
"github.com/safing/portmaster/status"
"github.com/safing/portmaster/profile/endpoints"
"github.com/safing/portmaster/resolver"
"github.com/agext/levenshtein"
"github.com/miekg/dns"
)
// Call order:
@@ -30,11 +31,10 @@ import (
// 4. DecideOnLink
// is called when when the first packet of a link arrives only if communication has verdict UNDECIDED or CANTSAY
// DecideOnCommunicationBeforeIntel makes a decision about a communication before the dns query is resolved and intel is gathered.
func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) {
// check if communication needs reevaluation
if comm.NeedsReevaluation() {
// DecideOnCommunicationBeforeDNS makes a decision about a communication before the dns query is resolved and intel is gathered.
func DecideOnCommunicationBeforeDNS(comm *network.Communication) {
// update profiles and check if communication needs reevaluation
if comm.UpdateAndCheck() {
log.Infof("firewall: re-evaluating verdict on %s", comm)
comm.ResetVerdict()
}
@@ -51,116 +51,74 @@ func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string)
return
}
// get and check profile set
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
comm.Deny("no Profile Set")
return
}
profileSet.Update(status.ActiveSecurityLevel())
// get profile
p := comm.Process().Profile()
// check for any network access
if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LAN) {
if p.BlockScopeInternet() && p.BlockScopeLAN() {
log.Infof("firewall: denying communication %s, accessing Internet or LAN not permitted", comm)
comm.Deny("accessing Internet or LAN not permitted")
return
}
// continueing with access to either Internet or LAN
// check endpoint list
result, reason := profileSet.CheckEndpointDomain(fqdn)
// FIXME: comm.Entity.Lock()
result, reason := p.MatchEndpoint(comm.Entity)
// FIXME: comm.Entity.Unlock()
switch result {
case profile.NoMatch:
comm.UpdateVerdict(network.VerdictUndecided)
if profileSet.GetProfileMode() == profile.Whitelist {
log.Infof("firewall: denying communication %s, domain is not whitelisted", comm)
comm.Deny("domain is not whitelisted")
}
case profile.Undeterminable:
case endpoints.Undeterminable:
comm.UpdateVerdict(network.VerdictUndeterminable)
case profile.Denied:
log.Infof("firewall: denying communication %s, endpoint is blacklisted: %s", comm, reason)
comm.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason))
case profile.Permitted:
log.Infof("firewall: permitting communication %s, endpoint is whitelisted: %s", comm, reason)
comm.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
}
}
// DecideOnCommunicationAfterIntel makes a decision about a communication after the dns query is resolved and intel is gathered.
func DecideOnCommunicationAfterIntel(comm *network.Communication, fqdn string, rrCache *intel.RRCache) {
// rrCache may be nil, when function is called for re-evaluation by DecideOnCommunication
// check if need to run
if comm.GetVerdict() != network.VerdictUndecided {
return
case endpoints.Denied:
log.Infof("firewall: denying communication %s, domain is blacklisted: %s", comm, reason)
comm.Deny(fmt.Sprintf("domain is blacklisted: %s", reason))
return
case endpoints.Permitted:
log.Infof("firewall: permitting communication %s, domain is whitelisted: %s", comm, reason)
comm.Accept(fmt.Sprintf("domain is whitelisted: %s", reason))
return
}
// continueing with result == NoMatch
// grant self - should not get here
if comm.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own communication %s", comm)
comm.Accept("")
// check default action
if p.DefaultAction() == profile.DefaultActionPermit {
log.Infof("firewall: permitting communication %s, domain is not blacklisted (default=permit)", comm)
comm.Accept("domain is not blacklisted (default=permit)")
return
}
// check if there is a profile
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
comm.Deny("no Profile Set")
return
}
profileSet.Update(status.ActiveSecurityLevel())
// TODO: Stamp integration
switch profileSet.GetProfileMode() {
case profile.Whitelist:
log.Infof("firewall: denying communication %s, domain is not whitelisted", comm)
comm.Deny("domain is not whitelisted")
return
case profile.Blacklist:
log.Infof("firewall: permitting communication %s, domain is not blacklisted", comm)
comm.Accept("domain is not blacklisted")
return
}
// ProfileMode == Prompt
// check relation
if profileSet.CheckFlag(profile.Related) {
if checkRelation(comm, fqdn) {
if !p.DisableAutoPermit() {
if checkRelation(comm) {
return
}
}
// prompt
prompt(comm, nil, nil, fqdn)
if p.DefaultAction() == profile.DefaultActionAsk {
prompt(comm, nil, nil)
return
}
// DefaultAction == DefaultActionBlock
log.Infof("firewall: denying communication %s, domain is not whitelisted (default=block)", comm)
comm.Deny("domain is not whitelisted (default=block)")
return
}
// FilterDNSResponse filters a dns response according to the application profile and settings.
//nolint:gocognit // FIXME
func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *intel.RRCache) *intel.RRCache {
func FilterDNSResponse(comm *network.Communication, q *resolver.Query, rrCache *resolver.RRCache) *resolver.RRCache { //nolint:gocognit // TODO
// do not modify own queries - this should not happen anyway
if comm.Process().Pid == os.Getpid() {
return rrCache
}
// check if there is a profile
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
log.Infof("firewall: blocking dns query of communication %s, no Profile Set", comm)
return nil
}
profileSet.Update(status.ActiveSecurityLevel())
// save config for consistency during function call
secLevel := profileSet.SecurityLevel()
filterByScope := filterDNSByScope(secLevel)
filterByProfile := filterDNSByProfile(secLevel)
// get profile
p := comm.Process().Profile()
// check if DNS response filtering is completely turned off
if !filterByScope && !filterByProfile {
if !p.RemoveOutOfScopeDNS() && !p.RemoveBlockedDNS() {
return rrCache
}
@@ -175,7 +133,6 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int
// loop vars
var classification int8
var ip net.IP
var result profile.EPResult
// filter function
filterEntries := func(entries []dns.RR) (goodEntries []dns.RR) {
@@ -196,7 +153,7 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int
}
classification = netutils.ClassifyIP(ip)
if filterByScope {
if p.RemoveOutOfScopeDNS() {
switch {
case classification == netutils.HostLocal:
// No DNS should return localhost addresses
@@ -211,30 +168,24 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int
}
}
if filterByProfile {
if p.RemoveBlockedDNS() {
// filter by flags
switch {
case !profileSet.CheckFlag(profile.Internet) && classification == netutils.Global:
case p.BlockScopeInternet() && classification == netutils.Global:
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
case !profileSet.CheckFlag(profile.LAN) && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
case !profileSet.CheckFlag(profile.Localhost) && classification == netutils.HostLocal:
case p.BlockScopeLocal() && classification == netutils.HostLocal:
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
}
// filter by endpoints
result, _ = profileSet.CheckEndpointIP(q.FQDN, ip, 0, 0, false)
if result == profile.Denied {
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
}
// TODO: filter by endpoint list (IP only)
}
// if survived, add to good entries
@@ -267,17 +218,15 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int
}
// DecideOnCommunication makes a decision about a communication with its first packet.
func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
// check if communication needs reevaluation, if it's not with a domain
if comm.NeedsReevaluation() {
func DecideOnCommunication(comm *network.Communication) {
// update profiles and check if communication needs reevaluation
if comm.UpdateAndCheck() {
log.Infof("firewall: re-evaluating verdict on %s", comm)
comm.ResetVerdict()
// if communicating with a domain entity, re-evaluate with Before/AfterIntel
if strings.HasSuffix(comm.Domain, ".") {
DecideOnCommunicationBeforeIntel(comm, comm.Domain)
DecideOnCommunicationAfterIntel(comm, comm.Domain, nil)
// if communicating with a domain entity, re-evaluate with BeforeDNS
if strings.HasSuffix(comm.Scope, ".") {
DecideOnCommunicationBeforeDNS(comm)
}
}
@@ -293,29 +242,24 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
return
}
// check if there is a profile
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
comm.Deny("no Profile Set")
return
}
profileSet.Update(status.ActiveSecurityLevel())
// get profile
p := comm.Process().Profile()
// check comm type
switch comm.Domain {
switch comm.Scope {
case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid:
if !profileSet.CheckFlag(profile.Service) {
if p.BlockInbound() {
log.Infof("firewall: denying communication %s, not a service", comm)
if comm.Domain == network.IncomingHost {
if comm.Scope == network.IncomingHost {
comm.Block("not a service")
} else {
comm.Deny("not a service")
}
return
}
case network.PeerLAN, network.PeerInternet, network.PeerInvalid: // Important: PeerHost is and should be missing!
if !profileSet.CheckFlag(profile.PeerToPeer) {
case network.PeerLAN, network.PeerInternet, network.PeerInvalid:
// Important: PeerHost is and should be missing!
if p.BlockP2P() {
log.Infof("firewall: denying communication %s, peer to peer comms (to an IP) not allowed", comm)
comm.Deny("peer to peer comms (to an IP) not allowed")
return
@@ -323,21 +267,21 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
}
// check network scope
switch comm.Domain {
switch comm.Scope {
case network.IncomingHost:
if !profileSet.CheckFlag(profile.Localhost) {
if p.BlockScopeLocal() {
log.Infof("firewall: denying communication %s, serving localhost not allowed", comm)
comm.Block("serving localhost not allowed")
return
}
case network.IncomingLAN:
if !profileSet.CheckFlag(profile.LAN) {
if p.BlockScopeLAN() {
log.Infof("firewall: denying communication %s, serving LAN not allowed", comm)
comm.Deny("serving LAN not allowed")
return
}
case network.IncomingInternet:
if !profileSet.CheckFlag(profile.Internet) {
if p.BlockScopeInternet() {
log.Infof("firewall: denying communication %s, serving Internet not allowed", comm)
comm.Deny("serving Internet not allowed")
return
@@ -347,19 +291,19 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
comm.Drop("invalid IP address")
return
case network.PeerHost:
if !profileSet.CheckFlag(profile.Localhost) {
if p.BlockScopeLocal() {
log.Infof("firewall: denying communication %s, accessing localhost not allowed", comm)
comm.Block("accessing localhost not allowed")
return
}
case network.PeerLAN:
if !profileSet.CheckFlag(profile.LAN) {
if p.BlockScopeLAN() {
log.Infof("firewall: denying communication %s, accessing the LAN not allowed", comm)
comm.Deny("accessing the LAN not allowed")
return
}
case network.PeerInternet:
if !profileSet.CheckFlag(profile.Internet) {
if p.BlockScopeInternet() {
log.Infof("firewall: denying communication %s, accessing the Internet not allowed", comm)
comm.Deny("accessing the Internet not allowed")
return
@@ -384,7 +328,7 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa
return
}
// check if communicating with self
// check if process is communicating with itself
if comm.Process().Pid >= 0 && pkt.Info().Src.Equal(pkt.Info().Dst) {
// get PID
otherPid, _, err := process.GetPidByEndpoints(
@@ -424,86 +368,80 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa
return
}
// check if there is a profile
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
log.Infof("firewall: no Profile Set, denying %s", link)
link.Deny("no Profile Set")
return
}
profileSet.Update(status.ActiveSecurityLevel())
// get domain
var fqdn string
if strings.HasSuffix(comm.Domain, ".") {
fqdn = comm.Domain
}
// remoteIP
var remoteIP net.IP
if comm.Direction {
remoteIP = pkt.Info().Src
} else {
remoteIP = pkt.Info().Dst
}
// protocol and destination port
protocol := uint8(pkt.Info().Protocol)
dstPort := pkt.Info().DstPort
// get profile
p := comm.Process().Profile()
// check endpoints list
result, reason := profileSet.CheckEndpointIP(fqdn, remoteIP, protocol, dstPort, comm.Direction)
var result endpoints.EPResult
var reason string
// FIXME: link.Entity.Lock()
if comm.Direction {
result, reason = p.MatchServiceEndpoint(link.Entity)
} else {
result, reason = p.MatchEndpoint(link.Entity)
}
// FIXME: link.Entity.Unlock()
switch result {
case profile.Denied:
case endpoints.Denied:
log.Infof("firewall: denying link %s, endpoint is blacklisted: %s", link, reason)
link.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason))
return
case profile.Permitted:
case endpoints.Permitted:
log.Infof("firewall: permitting link %s, endpoint is whitelisted: %s", link, reason)
link.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
return
}
// continueing with result == NoMatch
// TODO: Stamp integration
switch profileSet.GetProfileMode() {
case profile.Whitelist:
log.Infof("firewall: denying link %s: endpoint is not whitelisted", link)
link.Deny("endpoint is not whitelisted")
return
case profile.Blacklist:
log.Infof("firewall: permitting link %s: endpoint is not blacklisted", link)
link.Accept("endpoint is not blacklisted")
// implicit default=block for incoming
if comm.Direction {
log.Infof("firewall: denying link %s: endpoint is not whitelisted (incoming is always default=block)", link)
link.Deny("endpoint is not whitelisted (incoming is always default=block)")
return
}
// ProfileMode == Prompt
// check default action
if p.DefaultAction() == profile.DefaultActionPermit {
log.Infof("firewall: permitting link %s: endpoint is not blacklisted (default=permit)", link)
link.Accept("endpoint is not blacklisted (default=permit)")
return
}
// check relation
if fqdn != "" && profileSet.CheckFlag(profile.Related) {
if checkRelation(comm, fqdn) {
if !p.DisableAutoPermit() {
if checkRelation(comm) {
return
}
}
// prompt
prompt(comm, link, pkt, fqdn)
}
func checkRelation(comm *network.Communication, fqdn string) (related bool) {
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
if p.DefaultAction() == profile.DefaultActionAsk {
prompt(comm, link, pkt)
return
}
// TODO: add #AI
// DefaultAction == DefaultActionBlock
log.Infof("firewall: denying link %s: endpoint is not whitelisted (default=block)", link)
link.Deny("endpoint is not whitelisted (default=block)")
return
}
pathElements := strings.Split(comm.Process().Path, "/") // FIXME: path separator
// checkRelation tries to find a relation between a process and a communication. This is for better out of the box experience and is _not_ meant to thwart intentional malware.
func checkRelation(comm *network.Communication) (related bool) {
if comm.Entity.Domain != "" {
return false
}
// don't check for unknown processes
if comm.Process().Pid < 0 {
return false
}
pathElements := strings.Split(comm.Process().Path, string(filepath.Separator))
// only look at the last two path segments
if len(pathElements) > 2 {
pathElements = pathElements[len(pathElements)-2:]
}
domainElements := strings.Split(fqdn, ".")
domainElements := strings.Split(comm.Entity.Domain, ".")
var domainElement string
var processElement string
@@ -517,11 +455,6 @@ matchLoop:
break matchLoop
}
}
if levenshtein.Match(domainElement, profileSet.UserProfile().Name, nil) > 0.5 {
related = true
processElement = profileSet.UserProfile().Name
break matchLoop
}
if levenshtein.Match(domainElement, comm.Process().Name, nil) > 0.5 {
related = true
processElement = comm.Process().Name

View File

@@ -71,8 +71,12 @@ func GetPermittedPort() uint16 {
func portsInUseCleaner() {
for {
time.Sleep(cleanerTickDuration)
cleanPortsInUse()
select {
case <-module.Stopping():
return
case <-time.After(cleanerTickDuration):
cleanPortsInUse()
}
}
}

View File

@@ -1,15 +1,15 @@
package firewall
import (
"context"
"fmt"
"time"
"github.com/safing/portmaster/profile/endpoints"
"github.com/safing/portbase/log"
"github.com/safing/portbase/notifications"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/profile"
)
const (
@@ -30,14 +30,14 @@ var (
)
//nolint:gocognit // FIXME
func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, fqdn string) {
func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet) {
nTTL := time.Duration(promptTimeout()) * time.Second
// first check if there is an existing notification for this.
// build notification ID
var nID string
switch {
case comm.Direction, fqdn == "": // connection to/from IP
case comm.Direction, comm.Entity.Domain == "": // connection to/from IP
if pkt == nil {
log.Error("firewall: could not prompt for incoming/direct connection: missing pkt")
if link != nil {
@@ -47,9 +47,9 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
}
return
}
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s", comm.Process().Pid, comm.Domain, pkt.Info().RemoteIP())
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s", comm.Process().Pid, comm.Scope, pkt.Info().RemoteIP())
default: // connection to domain
nID = fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Domain)
nID = fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Scope)
}
n := notifications.Get(nID)
saveResponse := true
@@ -70,7 +70,7 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
// add message and actions
switch {
case comm.Direction: // incoming
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().LocalPort())
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", comm.Process(), link.Entity.IP.String(), link.Entity.Protocol, link.Entity.Port)
n.AvailableActions = []*notifications.Action{
{
ID: permitServingIP,
@@ -81,8 +81,8 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
Text: "Deny",
},
}
case fqdn == "": // direct connection
n.Message = fmt.Sprintf("Application %s wants to connect to %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort())
case comm.Entity.Domain == "": // direct connection
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", comm.Process(), link.Entity.IP.String(), link.Entity.Protocol, link.Entity.Port)
n.AvailableActions = []*notifications.Action{
{
ID: permitIP,
@@ -94,10 +94,10 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
},
}
default: // connection to domain
if pkt != nil {
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Domain, pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort())
if link != nil {
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Entity.Domain, link.Entity.IP.String(), link.Entity.Protocol, link.Entity.Port)
} else {
n.Message = fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Domain)
n.Message = fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Entity.Domain)
}
n.AvailableActions = []*notifications.Action{
{
@@ -141,62 +141,57 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
return
}
new := &profile.EndpointPermission{
Type: profile.EptDomain,
Value: comm.Domain,
Permit: false,
Created: time.Now().Unix(),
}
// get profile
p := comm.Process().Profile()
// permission type
var ep endpoints.Endpoint
switch promptResponse {
case permitDomainAll, denyDomainAll:
new.Value = "." + new.Value
case permitIP, permitServingIP, denyIP, denyServingIP:
if pkt == nil {
log.Warningf("firewall: received invalid prompt response: %s for %s", promptResponse, comm.Domain)
return
case permitDomainAll:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: true},
Domain: "." + comm.Entity.Domain,
}
if pkt.Info().Version == packet.IPv4 {
new.Type = profile.EptIPv4
} else {
new.Type = profile.EptIPv6
case permitDomainDistinct:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: true},
Domain: comm.Entity.Domain,
}
new.Value = pkt.Info().RemoteIP().String()
case denyDomainAll:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: false},
Domain: "." + comm.Entity.Domain,
}
case denyDomainDistinct:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: false},
Domain: comm.Entity.Domain,
}
case permitIP, permitServingIP:
ep = &endpoints.EndpointIP{
EndpointBase: endpoints.EndpointBase{Permitted: true},
IP: comm.Entity.IP,
}
case denyIP, denyServingIP:
ep = &endpoints.EndpointIP{
EndpointBase: endpoints.EndpointBase{Permitted: false},
IP: comm.Entity.IP,
}
default:
log.Warningf("filter: unknown prompt response: %s", promptResponse)
}
// permission verdict
switch promptResponse {
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
new.Permit = false
}
// get user profile
profileSet := comm.Process().ProfileSet()
profileSet.Lock()
defer profileSet.Unlock()
userProfile := profileSet.UserProfile()
userProfile.Lock()
defer userProfile.Unlock()
// add to correct list
switch promptResponse {
case permitServingIP, denyServingIP:
userProfile.ServiceEndpoints = append(userProfile.ServiceEndpoints, new)
p.AddServiceEndpoint(ep.String())
default:
userProfile.Endpoints = append(userProfile.Endpoints, new)
p.AddEndpoint(ep.String())
}
// save!
module.StartMicroTask(&mtSaveProfile, func(ctx context.Context) error {
return userProfile.Save("")
})
case <-n.Expired():
if link != nil {
link.Accept("no response to prompt")
link.Deny("no response to prompt")
} else {
comm.Accept("no response to prompt")
comm.Deny("no response to prompt")
}
}
}

View File

@@ -1,155 +0,0 @@
package intel
import (
"fmt"
"strings"
"github.com/safing/portbase/config"
"github.com/safing/portmaster/status"
)
var (
configuredNameServers config.StringArrayOption
defaultNameServers = []string{
// "dot://9.9.9.9:853?verify=dns.quad9.net&", // Quad9
// "dot|149.112.112.112:853|dns.quad9.net", // Quad9
// "dot://[2620:fe::fe]:853?verify=dns.quad9.net&name=Quad9" // Quad9
// "dot://[2620:fe::9]:853?verify=dns.quad9.net&name=Quad9" // Quad9
"dot|1.1.1.1:853|cloudflare-dns.com", // Cloudflare
"dot|1.0.0.1:853|cloudflare-dns.com", // Cloudflare
"dns|9.9.9.9:53", // Quad9
"dns|149.112.112.112:53", // Quad9
"dns|1.1.1.1:53", // Cloudflare
"dns|1.0.0.1:53", // Cloudflare
// "doh|cloudflare-dns.com/dns-query", // DoH still experimental
}
nameserverRetryRate config.IntOption
doNotUseMulticastDNS status.SecurityLevelOption
doNotUseAssignedNameservers status.SecurityLevelOption
doNotUseInsecureProtocols status.SecurityLevelOption
doNotResolveSpecialDomains status.SecurityLevelOption
doNotResolveTestDomains status.SecurityLevelOption
)
func prepConfig() error {
err := config.Register(&config.Option{
Name: "Nameservers (DNS)",
Key: "intel/nameservers",
Description: "Nameserver to use for resolving DNS requests.",
OptType: config.OptTypeStringArray,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: defaultNameServers,
ValidationRegex: "^(dns|tcp|tls|https)|[a-z0-9\\.|-]+$",
})
if err != nil {
return err
}
configuredNameServers = config.Concurrent.GetAsStringArray("intel/nameservers", defaultNameServers)
err = config.Register(&config.Option{
Name: "Nameserver Retry Rate",
Key: "intel/nameserverRetryRate",
Description: "Rate at which to retry failed nameservers, in seconds.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: 600,
})
if err != nil {
return err
}
nameserverRetryRate = config.Concurrent.GetAsInt("intel/nameserverRetryRate", 0)
err = config.Register(&config.Option{
Name: "Do not use Multicast DNS",
Key: "intel/doNotUseMulticastDNS",
Description: "Multicast DNS queries other devices in the local network",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 6,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
doNotUseMulticastDNS = status.ConfigIsActiveConcurrent("intel/doNotUseMulticastDNS")
err = config.Register(&config.Option{
Name: "Do not use assigned Nameservers",
Key: "intel/doNotUseAssignedNameservers",
Description: "that were acquired by the network (dhcp) or system",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 4,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
doNotUseAssignedNameservers = status.ConfigIsActiveConcurrent("intel/doNotUseAssignedNameservers")
err = config.Register(&config.Option{
Name: "Do not resolve insecurely",
Key: "intel/doNotUseInsecureProtocols",
Description: "Do not resolve domains with insecure protocols, ie. plain DNS",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 4,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
doNotUseInsecureProtocols = status.ConfigIsActiveConcurrent("intel/doNotUseInsecureProtocols")
err = config.Register(&config.Option{
Name: "Do not resolve special domains",
Key: "intel/doNotResolveSpecialDomains",
Description: fmt.Sprintf("Do not resolve the special top level domains %s", formatScopeList(specialServiceScopes)),
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
doNotResolveSpecialDomains = status.ConfigIsActiveConcurrent("intel/doNotResolveSpecialDomains")
err = config.Register(&config.Option{
Name: "Do not resolve test domains",
Key: "intel/doNotResolveTestDomains",
Description: fmt.Sprintf("Do not resolve the special testing top level domains %s", formatScopeList(localTestScopes)),
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 6,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
doNotResolveTestDomains = status.ConfigIsActiveConcurrent("intel/doNotResolveTestDomains")
return nil
}
func formatScopeList(list []string) string {
formatted := make([]string, 0, len(list))
for _, domain := range list {
formatted = append(formatted, strings.Trim(domain, "."))
}
return strings.Join(formatted, ", ")
}

201
intel/entity.go Normal file
View File

@@ -0,0 +1,201 @@
package intel
import (
"context"
"net"
"sync"
"github.com/tevino/abool"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/intel/geoip"
"github.com/safing/portmaster/status"
)
// Entity describes a remote endpoint in many different ways.
type Entity struct {
sync.Mutex
Domain string
IP net.IP
Protocol uint8
Port uint16
doReverseResolve bool
reverseResolveDone *abool.AtomicBool
Country string
ASN uint
location *geoip.Location
locationFetched *abool.AtomicBool
Lists []string
listsFetched *abool.AtomicBool
}
// Init initializes the internal state and returns the entity.
func (e *Entity) Init() *Entity {
e.reverseResolveDone = abool.New()
e.locationFetched = abool.New()
e.listsFetched = abool.New()
return e
}
// FetchData fetches additional information, meant to be called before persisting an entity record.
func (e *Entity) FetchData() {
e.getLocation()
e.getLists()
}
// Domain and IP
// EnableReverseResolving enables reverse resolving the domain from the IP on demand.
func (e *Entity) EnableReverseResolving() {
e.Lock()
defer e.Lock()
e.doReverseResolve = true
}
func (e *Entity) reverseResolve() {
// only get once
if !e.reverseResolveDone.IsSet() {
e.Lock()
defer e.Unlock()
// check for concurrent request
if e.reverseResolveDone.IsSet() {
return
}
defer e.reverseResolveDone.Set()
// check if we should resolve
if !e.doReverseResolve {
return
}
// need IP!
if e.IP == nil {
return
}
// reverse resolve
if reverseResolver == nil {
return
}
// TODO: security level
domain, err := reverseResolver(context.TODO(), e.IP.String(), status.SecurityLevelDynamic)
if err != nil {
log.Warningf("intel: failed to resolve IP %s: %s", e.IP, err)
return
}
e.Domain = domain
}
}
// GetDomain returns the domain and whether it is set.
func (e *Entity) GetDomain() (string, bool) {
e.reverseResolve()
if e.Domain == "" {
return "", false
}
return e.Domain, true
}
// GetIP returns the IP and whether it is set.
func (e *Entity) GetIP() (net.IP, bool) {
if e.IP == nil {
return nil, false
}
return e.IP, true
}
// Location
func (e *Entity) getLocation() {
// only get once
if !e.locationFetched.IsSet() {
e.Lock()
defer e.Unlock()
// check for concurrent request
if e.locationFetched.IsSet() {
return
}
defer e.locationFetched.Set()
// need IP!
if e.IP == nil {
log.Warningf("intel: cannot get location for %s data without IP", e.Domain)
return
}
// get location data
loc, err := geoip.GetLocation(e.IP)
if err != nil {
log.Warningf("intel: failed to get location data for %s: %s", e.IP, err)
return
}
e.location = loc
e.Country = loc.Country.ISOCode
e.ASN = loc.AutonomousSystemNumber
}
}
// GetLocation returns the raw location data and whether it is set.
func (e *Entity) GetLocation() (*geoip.Location, bool) {
e.getLocation()
if e.location == nil {
return nil, false
}
return e.location, true
}
// GetCountry returns the two letter ISO country code and whether it is set.
func (e *Entity) GetCountry() (string, bool) {
e.getLocation()
if e.Country == "" {
return "", false
}
return e.Country, true
}
// GetASN returns the AS number and whether it is set.
func (e *Entity) GetASN() (uint, bool) {
e.getLocation()
if e.ASN == 0 {
return 0, false
}
return e.ASN, true
}
// Lists
func (e *Entity) getLists() {
// only get once
if !e.listsFetched.IsSet() {
e.Lock()
defer e.Unlock()
// check for concurrent request
if e.listsFetched.IsSet() {
return
}
defer e.listsFetched.Set()
// TODO: fetch lists
}
}
// GetLists returns the filter list identifiers the entity matched and whether this data is set.
func (e *Entity) GetLists() ([]string, bool) {
e.getLists()
if e.Lists == nil {
return nil, false
}
return e.Lists, true
}

View File

@@ -41,5 +41,6 @@ func GetLocation(ip net.IP) (record *Location, err error) {
if err != nil {
return nil, err
}
return record, nil
}

View File

@@ -3,16 +3,9 @@ package geoip
import (
"net"
"testing"
"github.com/safing/portmaster/updates"
)
func TestLocationLookup(t *testing.T) {
err := updates.InitForTesting()
if err != nil {
t.Fatal(err)
}
ip1 := net.ParseIP("81.2.69.142")
loc1, err := GetLocation(ip1)
if err != nil {
@@ -39,7 +32,21 @@ func TestLocationLookup(t *testing.T) {
if err != nil {
t.Fatal(err)
}
t.Logf("%v", loc1)
t.Logf("%v", loc4)
ip5 := net.ParseIP("194.232.1.1")
loc5, err := GetLocation(ip5)
if err != nil {
t.Fatal(err)
}
t.Logf("%v", loc5)
ip6 := net.ParseIP("151.101.1.164")
loc6, err := GetLocation(ip6)
if err != nil {
t.Fatal(err)
}
t.Logf("%v", loc6)
dist1 := loc1.EstimateNetworkProximity(loc2)
dist2 := loc2.EstimateNetworkProximity(loc3)
@@ -50,5 +57,4 @@ func TestLocationLookup(t *testing.T) {
t.Logf("proximity %s <> %s: %d", ip2, ip3, dist2)
t.Logf("proximity %s <> %s: %d", ip1, ip3, dist3)
t.Logf("proximity %s <> %s: %d", ip1, ip4, dist4)
}

View File

@@ -2,7 +2,6 @@ package geoip
import (
"context"
"fmt"
"github.com/safing/portbase/modules"
)
@@ -12,15 +11,10 @@ var (
)
func init() {
module = modules.Register("geoip", nil, start, nil, "updates")
module = modules.Register("geoip", prep, nil, nil, "core")
}
func start() error {
err := prepDatabaseForUse()
if err != nil {
return fmt.Errorf("goeip: failed to load databases: %s", err)
}
func prep() error {
return module.RegisterEventHook(
"updates",
"resource update",

View File

@@ -0,0 +1,11 @@
package geoip
import (
"testing"
"github.com/safing/portmaster/core/pmtesting"
)
func TestMain(m *testing.M) {
pmtesting.TestMain(m)
}

View File

@@ -1,75 +0,0 @@
package intel
import (
"context"
"fmt"
"sync"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
)
var (
intelDatabase = database.NewInterface(&database.Options{
AlwaysSetRelativateExpiry: 2592000, // 30 days
})
)
// Intel holds intelligence data for a domain.
type Intel struct {
record.Base
sync.Mutex
Domain string
}
func makeIntelKey(domain string) string {
return fmt.Sprintf("cache:intel/domain/%s", domain)
}
// GetIntelFromDB gets an Intel record from the database.
func GetIntelFromDB(domain string) (*Intel, error) {
key := makeIntelKey(domain)
r, err := intelDatabase.Get(key)
if err != nil {
return nil, err
}
// unwrap
if r.IsWrapped() {
// only allocate a new struct, if we need it
new := &Intel{}
err = record.Unwrap(r, new)
if err != nil {
return nil, err
}
return new, nil
}
// or adjust type
new, ok := r.(*Intel)
if !ok {
return nil, fmt.Errorf("record not of type *Intel, but %T", r)
}
return new, nil
}
// Save saves the Intel record to the database.
func (intel *Intel) Save() error {
intel.SetKey(makeIntelKey(intel.Domain))
return intelDatabase.PutNew(intel)
}
// GetIntel fetches intelligence data for the given domain.
func GetIntel(ctx context.Context, q *Query) (*Intel, error) {
// sanity check
if q == nil || !q.check() {
return nil, ErrInvalid
}
log.Tracer(ctx).Trace("intel: getting intel")
// TODO
return &Intel{Domain: q.FQDN}, nil
}

40
intel/lists.go Normal file
View File

@@ -0,0 +1,40 @@
package intel
// ListSet holds a set of list IDs.
type ListSet struct {
match []string
}
// NewListSet returns a new ListSet with the given list IDs.
func NewListSet(lists []string) *ListSet {
// TODO: validate lists
return &ListSet{
match: lists,
}
}
// Matches returns whether there is a match in the given list IDs.
func (ls *ListSet) Matches(lists []string) (matches bool) {
for _, list := range lists {
for _, entry := range ls.match {
if entry == list {
return true
}
}
}
return false
}
// MatchSet returns the matching list IDs.
func (ls *ListSet) MatchSet(lists []string) (matched []string) {
for _, list := range lists {
for _, entry := range ls.match {
if entry == list {
matched = append(matched, list)
}
}
}
return
}

View File

@@ -1,33 +0,0 @@
package intel
import (
"os"
"testing"
"github.com/safing/portmaster/core"
)
func TestMain(m *testing.M) {
// setup
tmpDir, err := core.InitForTesting()
if err != nil {
panic(err)
}
// setup package
err = prep()
if err != nil {
panic(err)
}
loadResolvers()
// run tests
rv := m.Run()
// teardown
core.StopTesting()
_ = os.RemoveAll(tmpDir)
// exit with test run return value
os.Exit(rv)
}

9
intel/module.go Normal file
View File

@@ -0,0 +1,9 @@
package intel
import (
"github.com/safing/portbase/modules"
)
func init() {
modules.Register("intel", nil, nil, nil, "geoip")
}

View File

@@ -2,152 +2,15 @@ package intel
import (
"context"
"net"
"sync"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/environment"
)
// DNS Resolver Attributes
const (
ServerTypeDNS = "dns"
ServerTypeTCP = "tcp"
ServerTypeDoT = "dot"
ServerTypeDoH = "doh"
ServerSourceConfigured = "config"
ServerSourceAssigned = "dhcp"
ServerSourceMDNS = "mdns"
var (
reverseResolver func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error)
)
// Resolver holds information about an active resolver.
type Resolver struct {
// Server config url (and ID)
Server string
// Parsed config
ServerType string
ServerAddress string
ServerIP net.IP
ServerIPScope int8
ServerPort uint16
// Special Options
VerifyDomain string
Search []string
SkipFQDN string
Source string
// logic interface
Conn ResolverConn
}
// String returns the URL representation of the resolver.
func (resolver *Resolver) String() string {
return resolver.Server
}
// ResolverConn is an interface to implement different types of query backends.
type ResolverConn interface {
Query(ctx context.Context, q *Query) (*RRCache, error)
MarkFailed()
LastFail() time.Time
}
// BasicResolverConn implements ResolverConn for standard dns clients.
type BasicResolverConn struct {
sync.Mutex // for lastFail
resolver *Resolver
clientManager *clientManager
lastFail time.Time
}
// MarkFailed marks the resolver as failed.
func (brc *BasicResolverConn) MarkFailed() {
if !environment.Online() {
// don't mark failed if we are offline
return
// SetReverseResolver allows the resolver module to register a function to allow reverse resolving IPs to domains.
func SetReverseResolver(fn func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error)) {
if reverseResolver == nil {
reverseResolver = fn
}
brc.Lock()
defer brc.Unlock()
brc.lastFail = time.Now()
}
// LastFail returns the internal lastfail value while locking the Resolver.
func (brc *BasicResolverConn) LastFail() time.Time {
brc.Lock()
defer brc.Unlock()
return brc.lastFail
}
// Query executes the given query against the resolver.
func (brc *BasicResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) {
// convenience
resolver := brc.resolver
// create query
dnsQuery := new(dns.Msg)
dnsQuery.SetQuestion(q.FQDN, uint16(q.QType))
// start
var reply *dns.Msg
var err error
for i := 0; i < 3; i++ {
// log query time
// qStart := time.Now()
reply, _, err = brc.clientManager.getDNSClient().Exchange(dnsQuery, resolver.ServerAddress)
// log.Tracef("intel: query to %s took %s", resolver.Server, time.Now().Sub(qStart))
// error handling
if err != nil {
log.Tracer(ctx).Tracef("intel: query to %s encountered error: %s", resolver.Server, err)
// TODO: handle special cases
// 1. connect: network is unreachable
// 2. timeout
// hint network environment at failed connection
environment.ReportFailedConnection()
// temporary error
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
log.Tracer(ctx).Tracef("intel: retrying to resolve %s%s with %s, error is temporary", q.FQDN, q.QType, resolver.Server)
continue
}
// permanent error
break
}
// no error
break
}
if err != nil {
return nil, err
// FIXME: mark as failed
}
// hint network environment at successful connection
environment.ReportSuccessfulConnection()
new := &RRCache{
Domain: q.FQDN,
Question: q.QType,
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
Server: resolver.Server,
ServerScope: resolver.ServerIPScope,
}
// TODO: check if reply.Answer is valid
return new, nil
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/safing/portbase/run"
// include packages here
_ "github.com/safing/portbase/modules/subsystems"
_ "github.com/safing/portmaster/core"
_ "github.com/safing/portmaster/firewall"
_ "github.com/safing/portmaster/nameserver"
@@ -14,13 +15,6 @@ import (
)
func main() {
/*go func() {
time.Sleep(10 * time.Second)
fmt.Fprintln(os.Stderr, "===== TAKING TOO LONG FOR SHUTDOWN - PRINTING STACK TRACES =====")
_ = pprof.Lookup("goroutine").WriteTo(os.Stderr, 2)
os.Exit(1)
}()*/
info.Set("Portmaster", "0.3.9", "AGPLv3", true)
os.Exit(run.Run())
}

View File

@@ -5,18 +5,18 @@ import (
"net"
"strings"
"github.com/safing/portmaster/network/environment"
"github.com/miekg/dns"
"github.com/safing/portbase/modules/subsystems"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/detection/dga"
"github.com/safing/portmaster/firewall"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/environment"
"github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/resolver"
"github.com/miekg/dns"
)
var (
@@ -30,10 +30,18 @@ var (
)
func init() {
module = modules.Register("nameserver", initLocalhostRRs, start, stop, "core", "intel", "network")
module = modules.Register("nameserver", prep, start, stop, "core", "resolver", "network")
subsystems.Register(
"dns",
"Secure DNS",
"DNS resolver with scoping and DNS-over-TLS",
module,
"config:dns/",
nil,
)
}
func initLocalhostRRs() error {
func prep() error {
localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1")
if err != nil {
return err
@@ -45,6 +53,7 @@ func initLocalhostRRs() error {
}
localhostRRs = []dns.RR{localhostIPv4, localhostIPv6}
return nil
}
@@ -56,7 +65,7 @@ func start() error {
err := dnsServer.ListenAndServe()
if err != nil {
// check if we are shutting down
if module.ShutdownInProgress() {
if module.IsStopping() {
return nil
}
// is something blocking our port?
@@ -108,7 +117,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
// only process first question, that's how everyone does it.
question := query.Question[0]
q := &intel.Query{
q := &resolver.Query{
FQDN: question.Name,
QType: dns.Type(question.Qtype),
}
@@ -176,7 +185,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
}()
// save security level to query
q.SecurityLevel = comm.Process().ProfileSet().SecurityLevel()
q.SecurityLevel = comm.Process().Profile().SecurityLevel()
// check for possible DNS tunneling / data transmission
// TODO: improve this
@@ -189,7 +198,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
}
// check profile before we even get intel and rr
firewall.DecideOnCommunicationBeforeIntel(comm, q.FQDN)
firewall.DecideOnCommunicationBeforeDNS(comm)
comm.Lock()
comm.SaveWhenFinished()
comm.Unlock()
@@ -200,8 +209,8 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
return nil
}
// get intel and RRs
rrCache, err := intel.Resolve(ctx, q)
// resolve
rrCache, err := resolver.Resolve(ctx, q)
if err != nil {
// TODO: analyze nxdomain requests, malware could be trying DGA-domains
tracer.Warningf("nameserver: %s requested %s%s: %s", comm.Process(), q.FQDN, q.QType, err)
@@ -209,31 +218,6 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
return nil
}
// get current intel
comm.Lock()
domainIntel := comm.Intel
comm.Unlock()
if domainIntel == nil {
// fetch intel
domainIntel, err = intel.GetIntel(ctx, q)
if err != nil {
tracer.Warningf("nameserver: failed to get intel for %s%s: %s", q.FQDN, q.QType, err)
returnNXDomain(w, query)
}
comm.Lock()
comm.Intel = domainIntel
comm.Unlock()
}
// check with intel
firewall.DecideOnCommunicationAfterIntel(comm, q.FQDN, rrCache)
switch comm.GetVerdict() {
case network.VerdictUndecided, network.VerdictBlock, network.VerdictDrop:
tracer.Infof("nameserver: %s denied after intel, returning nxdomain", comm)
returnNXDomain(w, query)
return nil
}
// filter DNS response
rrCache = firewall.FilterDNSResponse(comm, q, rrCache)
if rrCache == nil {
@@ -246,9 +230,9 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
for _, rr := range append(rrCache.Answer, rrCache.Extra...) {
switch v := rr.(type) {
case *dns.A:
ipInfo, err := intel.GetIPInfo(v.A.String())
ipInfo, err := resolver.GetIPInfo(v.A.String())
if err != nil {
ipInfo = &intel.IPInfo{
ipInfo = &resolver.IPInfo{
IP: v.A.String(),
Domains: []string{q.FQDN},
}
@@ -260,9 +244,9 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
}
}
case *dns.AAAA:
ipInfo, err := intel.GetIPInfo(v.AAAA.String())
ipInfo, err := resolver.GetIPInfo(v.AAAA.String())
if err != nil {
ipInfo = &intel.IPInfo{
ipInfo = &resolver.IPInfo{
IP: v.AAAA.String(),
Domains: []string{q.FQDN},
}

View File

@@ -40,9 +40,9 @@ func cleanLinks() (activeComms map[string]struct{}) {
for key, link := range links {
// delete dead links
link.Lock()
link.lock.Lock()
deleteThis := link.Ended > 0 && link.Ended < deleteOlderThan
link.Unlock()
link.lock.Unlock()
if deleteThis {
log.Tracef("network.clean: deleted %s (ended at %d)", link.DatabaseKey(), link.Ended)
go link.Delete()
@@ -51,9 +51,9 @@ func cleanLinks() (activeComms map[string]struct{}) {
// not yet deleted, so its still a valid link regarding link count
comm := link.Communication()
comm.Lock()
comm.lock.Lock()
markActive(activeComms, comm.DatabaseKey())
comm.Unlock()
comm.lock.Unlock()
// check if link is dead
found = false
@@ -66,9 +66,9 @@ func cleanLinks() (activeComms map[string]struct{}) {
if !found {
// mark end time
link.Lock()
link.lock.Lock()
link.Ended = now
link.Unlock()
link.lock.Unlock()
log.Tracef("network.clean: marked %s as ended", link.DatabaseKey())
// save
linkToSave := link
@@ -95,9 +95,9 @@ func cleanComms(activeLinks map[string]struct{}) (activeComms map[string]struct{
_, hasLinks := activeLinks[comm.DatabaseKey()]
// comm created
comm.Lock()
comm.lock.Lock()
created := comm.Meta().Created
comm.Unlock()
comm.lock.Unlock()
if !hasLinks && created < threshold {
log.Tracef("network.clean: deleted %s", comm.DatabaseKey())

View File

@@ -8,48 +8,63 @@ import (
"sync"
"time"
"github.com/safing/portmaster/resolver"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process"
"github.com/safing/portmaster/profile"
)
// Communication describes a logical connection between a process and a domain.
//nolint:maligned // TODO: fix alignment
type Communication struct {
record.Base
sync.Mutex
lock sync.Mutex
Domain string
Scope string
Entity *intel.Entity
Direction bool
Intel *intel.Intel
process *process.Process
Verdict Verdict
Reason string
Inspect bool
Verdict Verdict
Reason string
ReasonID string // format source[:id[:id]]
Inspect bool
process *process.Process
profileRevisionCounter uint64
FirstLinkEstablished int64
LastLinkEstablished int64
profileUpdateVersion uint32
saveWhenFinished bool
saveWhenFinished bool
}
// Lock locks the communication and the communication's Entity.
func (comm *Communication) Lock() {
comm.lock.Lock()
comm.Entity.Lock()
}
// Lock unlocks the communication and the communication's Entity.
func (comm *Communication) Unlock() {
comm.Entity.Unlock()
comm.lock.Unlock()
}
// Process returns the process that owns the connection.
func (comm *Communication) Process() *process.Process {
comm.Lock()
defer comm.Unlock()
comm.lock.Lock()
defer comm.lock.Unlock()
return comm.process
}
// ResetVerdict resets the verdict to VerdictUndecided.
func (comm *Communication) ResetVerdict() {
comm.Lock()
defer comm.Unlock()
comm.lock.Lock()
defer comm.lock.Unlock()
comm.Verdict = VerdictUndecided
comm.Reason = ""
@@ -58,8 +73,8 @@ func (comm *Communication) ResetVerdict() {
// GetVerdict returns the current verdict.
func (comm *Communication) GetVerdict() Verdict {
comm.Lock()
defer comm.Unlock()
comm.lock.Lock()
defer comm.lock.Unlock()
return comm.Verdict
}
@@ -93,8 +108,8 @@ func (comm *Communication) Drop(reason string) {
// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts.
func (comm *Communication) UpdateVerdict(newVerdict Verdict) {
comm.Lock()
defer comm.Unlock()
comm.lock.Lock()
defer comm.lock.Unlock()
if newVerdict > comm.Verdict {
comm.Verdict = newVerdict
@@ -108,8 +123,8 @@ func (comm *Communication) SetReason(reason string) {
return
}
comm.Lock()
defer comm.Unlock()
comm.lock.Lock()
defer comm.lock.Unlock()
comm.Reason = reason
comm.saveWhenFinished = true
}
@@ -120,8 +135,8 @@ func (comm *Communication) AddReason(reason string) {
return
}
comm.Lock()
defer comm.Unlock()
comm.lock.Lock()
defer comm.lock.Unlock()
if comm.Reason != "" {
comm.Reason += " | "
@@ -129,21 +144,18 @@ func (comm *Communication) AddReason(reason string) {
comm.Reason += reason
}
// NeedsReevaluation returns whether the decision on this communication should be re-evaluated.
func (comm *Communication) NeedsReevaluation() bool {
comm.Lock()
defer comm.Unlock()
// UpdateAndCheck updates profiles and checks whether a reevaluation is needed.
func (comm *Communication) UpdateAndCheck() (needsReevaluation bool) {
revCnt := comm.Process().Profile().Update()
oldVersion := comm.profileUpdateVersion
comm.profileUpdateVersion = profile.GetUpdateVersion()
comm.lock.Lock()
defer comm.lock.Unlock()
if comm.profileRevisionCounter != revCnt {
comm.profileRevisionCounter = revCnt
needsReevaluation = true
}
if oldVersion == 0 {
return false
}
if oldVersion != comm.profileUpdateVersion {
return true
}
return false
return
}
// GetCommunicationByFirstPacket returns the matching communication from the internal storage.
@@ -153,25 +165,26 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
if err != nil {
return nil, err
}
var domain string
var scope string
// Incoming
if direction {
switch netutils.ClassifyIP(pkt.Info().Src) {
case netutils.HostLocal:
domain = IncomingHost
scope = IncomingHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = IncomingLAN
scope = IncomingLAN
case netutils.Global, netutils.GlobalMulticast:
domain = IncomingInternet
scope = IncomingInternet
case netutils.Invalid:
domain = IncomingInvalid
scope = IncomingInvalid
}
communication, ok := GetCommunication(proc.Pid, domain)
communication, ok := GetCommunication(proc.Pid, scope)
if !ok {
communication = &Communication{
Domain: domain,
Scope: scope,
Entity: (&intel.Entity{}).Init(),
Direction: Inbound,
process: proc,
Inspect: true,
@@ -184,7 +197,7 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
}
// get domain
ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP())
ipinfo, err := resolver.GetIPInfo(pkt.FmtRemoteIP())
// PeerToPeer
if err != nil {
@@ -192,19 +205,20 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
switch netutils.ClassifyIP(pkt.Info().Dst) {
case netutils.HostLocal:
domain = PeerHost
scope = PeerHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = PeerLAN
scope = PeerLAN
case netutils.Global, netutils.GlobalMulticast:
domain = PeerInternet
scope = PeerInternet
case netutils.Invalid:
domain = PeerInvalid
scope = PeerInvalid
}
communication, ok := GetCommunication(proc.Pid, domain)
communication, ok := GetCommunication(proc.Pid, scope)
if !ok {
communication = &Communication{
Domain: domain,
Scope: scope,
Entity: (&intel.Entity{}).Init(),
Direction: Outbound,
process: proc,
Inspect: true,
@@ -221,7 +235,10 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
communication, ok := GetCommunication(proc.Pid, ipinfo.Domains[0])
if !ok {
communication = &Communication{
Domain: ipinfo.Domains[0],
Scope: ipinfo.Domains[0],
Entity: (&intel.Entity{
Domain: ipinfo.Domains[0],
}).Init(),
Direction: Outbound,
process: proc,
Inspect: true,
@@ -251,7 +268,10 @@ func GetCommunicationByDNSRequest(ctx context.Context, ip net.IP, port uint16, f
communication, ok := GetCommunication(proc.Pid, fqdn)
if !ok {
communication = &Communication{
Domain: fqdn,
Scope: fqdn,
Entity: (&intel.Entity{
Domain: fqdn,
}).Init(),
process: proc,
Inspect: true,
saveWhenFinished: true,
@@ -271,7 +291,7 @@ func GetCommunication(pid int, domain string) (comm *Communication, ok bool) {
}
func (comm *Communication) makeKey() string {
return fmt.Sprintf("%d/%s", comm.process.Pid, comm.Domain)
return fmt.Sprintf("%d/%s", comm.process.Pid, comm.Scope)
}
// SaveWhenFinished marks the Connection for saving after all current actions are finished.
@@ -281,12 +301,12 @@ func (comm *Communication) SaveWhenFinished() {
// SaveIfNeeded saves the Connection if it is marked for saving when finished.
func (comm *Communication) SaveIfNeeded() {
comm.Lock()
comm.lock.Lock()
save := comm.saveWhenFinished
if save {
comm.saveWhenFinished = false
}
comm.Unlock()
comm.lock.Unlock()
if save {
err := comm.save()
@@ -299,14 +319,14 @@ func (comm *Communication) SaveIfNeeded() {
// Save saves the Connection object in the storage and propagates the change.
func (comm *Communication) save() error {
// update comm
comm.Lock()
comm.lock.Lock()
if comm.process == nil {
comm.Unlock()
comm.lock.Unlock()
return errors.New("cannot save connection without process")
}
if !comm.KeyIsSet() {
comm.SetKey(fmt.Sprintf("network:tree/%d/%s", comm.process.Pid, comm.Domain))
comm.SetKey(fmt.Sprintf("network:tree/%d/%s", comm.process.Pid, comm.Scope))
comm.UpdateMeta()
}
if comm.Meta().Deleted > 0 {
@@ -315,7 +335,7 @@ func (comm *Communication) save() error {
}
key := comm.makeKey()
comm.saveWhenFinished = false
comm.Unlock()
comm.lock.Unlock()
// save comm
commsLock.RLock()
@@ -336,8 +356,8 @@ func (comm *Communication) save() error {
func (comm *Communication) Delete() {
commsLock.Lock()
defer commsLock.Unlock()
comm.Lock()
defer comm.Unlock()
comm.lock.Lock()
defer comm.lock.Unlock()
delete(comms, comm.makeKey())
@@ -347,16 +367,18 @@ func (comm *Communication) Delete() {
// AddLink applies the Communication to the Link and sets timestamps.
func (comm *Communication) AddLink(link *Link) {
comm.lock.Lock()
defer comm.lock.Unlock()
// apply comm to link
link.Lock()
link.lock.Lock()
link.comm = comm
link.Verdict = comm.Verdict
link.Inspect = comm.Inspect
// FIXME: use new copy methods
link.Entity.Domain = comm.Entity.Domain
link.saveWhenFinished = true
link.Unlock()
// update comm LastLinkEstablished
comm.Lock()
link.lock.Unlock()
// check if we should save
if comm.LastLinkEstablished < time.Now().Add(-3*time.Second).Unix() {
@@ -368,8 +390,6 @@ func (comm *Communication) AddLink(link *Link) {
if comm.FirstLinkEstablished == 0 {
comm.FirstLinkEstablished = comm.LastLinkEstablished
}
comm.Unlock()
}
// String returns a string representation of Communication.
@@ -377,7 +397,7 @@ func (comm *Communication) String() string {
comm.Lock()
defer comm.Unlock()
switch comm.Domain {
switch comm.Scope {
case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
if comm.process == nil {
return "? <- *"
@@ -390,8 +410,8 @@ func (comm *Communication) String() string {
return fmt.Sprintf("%s -> *", comm.process.String())
default:
if comm.process == nil {
return fmt.Sprintf("? -> %s", comm.Domain)
return fmt.Sprintf("? -> %s", comm.Scope)
}
return fmt.Sprintf("%s -> %s", comm.process.String(), comm.Domain)
return fmt.Sprintf("%s -> %s", comm.process.String(), comm.Scope)
}
}

View File

@@ -7,6 +7,8 @@ import (
"sync"
"time"
"github.com/safing/portmaster/intel"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/packet"
@@ -16,21 +18,22 @@ import (
type FirewallHandler func(pkt packet.Packet, link *Link)
// Link describes a distinct physical connection (e.g. TCP connection) - like an instance - of a Connection.
//nolint:maligned // TODO: fix alignment
type Link struct {
type Link struct { //nolint:maligned // TODO: fix alignment
record.Base
sync.Mutex
lock sync.Mutex
ID string
ID string
Entity *intel.Entity
Direction bool
Verdict Verdict
Reason string
ReasonID string // format source[:id[:id]]
Tunneled bool
VerdictPermanent bool
Inspect bool
Started int64
Ended int64
RemoteAddress string
pktQueue chan packet.Packet
firewallHandler FirewallHandler
@@ -41,70 +44,82 @@ type Link struct {
saveWhenFinished bool
}
// Lock locks the link and the link's Entity.
func (link *Link) Lock() {
link.lock.Lock()
link.Entity.Lock()
}
// Lock unlocks the link and the link's Entity.
func (link *Link) Unlock() {
link.Entity.Unlock()
link.lock.Unlock()
}
// Communication returns the Communication the Link is part of
func (link *Link) Communication() *Communication {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
return link.comm
}
// GetVerdict returns the current verdict.
func (link *Link) GetVerdict() Verdict {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
return link.Verdict
}
// FirewallHandlerIsSet returns whether a firewall handler is set or not
func (link *Link) FirewallHandlerIsSet() bool {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
return link.firewallHandler != nil
}
// SetFirewallHandler sets the firewall handler for this link
func (link *Link) SetFirewallHandler(handler FirewallHandler) {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
if link.firewallHandler == nil {
link.firewallHandler = handler
link.pktQueue = make(chan packet.Packet, 1000)
// start handling
module.StartWorker("", func(ctx context.Context) error {
module.StartWorker("packet handler", func(ctx context.Context) error {
link.packetHandler()
return nil
})
return
}
link.firewallHandler = handler
}
// StopFirewallHandler unsets the firewall handler
func (link *Link) StopFirewallHandler() {
link.Lock()
link.lock.Lock()
link.firewallHandler = nil
link.Unlock()
link.lock.Unlock()
link.pktQueue <- nil
}
// HandlePacket queues packet of Link for handling
func (link *Link) HandlePacket(pkt packet.Packet) {
link.Lock()
defer link.Unlock()
// get handler
link.lock.Lock()
handler := link.firewallHandler
link.lock.Unlock()
if link.firewallHandler != nil {
// send to queue
if handler != nil {
link.pktQueue <- pkt
return
}
// no handler!
log.Warningf("network: link %s does not have a firewallHandler, dropping packet", link)
err := pkt.Drop()
if err != nil {
log.Warningf("network: failed to drop packet %s: %s", pkt, err)
@@ -119,7 +134,7 @@ func (link *Link) Accept(reason string) {
// Deny blocks or drops the link depending on the connection direction and adds the given reason.
func (link *Link) Deny(reason string) {
if link.comm != nil && link.comm.Direction {
if link.Direction {
link.Drop(reason)
} else {
link.Block(reason)
@@ -151,8 +166,8 @@ func (link *Link) RerouteToTunnel(reason string) {
// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts
func (link *Link) UpdateVerdict(newVerdict Verdict) {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
if newVerdict > link.Verdict {
link.Verdict = newVerdict
@@ -166,8 +181,8 @@ func (link *Link) AddReason(reason string) {
return
}
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
if link.Reason != "" {
link.Reason += " | "
@@ -185,9 +200,9 @@ func (link *Link) packetHandler() {
return
}
// get handler
link.Lock()
link.lock.Lock()
handler := link.firewallHandler
link.Unlock()
link.lock.Unlock()
// execute handler or verdict
if handler != nil {
handler(pkt, link)
@@ -201,8 +216,8 @@ func (link *Link) packetHandler() {
// ApplyVerdict appies the link verdict to a packet.
func (link *Link) ApplyVerdict(pkt packet.Packet) {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
var err error
@@ -251,12 +266,12 @@ func (link *Link) SaveWhenFinished() {
// SaveIfNeeded saves the Link if it is marked for saving when finished.
func (link *Link) SaveIfNeeded() {
link.Lock()
link.lock.Lock()
save := link.saveWhenFinished
if save {
link.saveWhenFinished = false
}
link.Unlock()
link.lock.Unlock()
if save {
link.saveAndLog()
@@ -274,18 +289,18 @@ func (link *Link) saveAndLog() {
// save saves the link object in the storage and propagates the change.
func (link *Link) save() error {
// update link
link.Lock()
link.lock.Lock()
if link.comm == nil {
link.Unlock()
link.lock.Unlock()
return errors.New("cannot save link without comms")
}
if !link.KeyIsSet() {
link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.comm.Process().Pid, link.comm.Domain, link.ID))
link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.comm.Process().Pid, link.comm.Scope, link.ID))
link.UpdateMeta()
}
link.saveWhenFinished = false
link.Unlock()
link.lock.Unlock()
// save link
linksLock.RLock()
@@ -306,8 +321,8 @@ func (link *Link) save() error {
func (link *Link) Delete() {
linksLock.Lock()
defer linksLock.Unlock()
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
delete(links, link.ID)
@@ -339,10 +354,15 @@ func GetOrCreateLinkByPacket(pkt packet.Packet) (*Link, bool) {
// CreateLinkFromPacket creates a new Link based on Packet.
func CreateLinkFromPacket(pkt packet.Packet) *Link {
link := &Link{
ID: pkt.GetLinkID(),
ID: pkt.GetLinkID(),
Entity: (&intel.Entity{
IP: pkt.Info().RemoteIP(),
Protocol: uint8(pkt.Info().Protocol),
Port: pkt.Info().RemotePort(),
}).Init(),
Direction: pkt.IsInbound(),
Verdict: VerdictUndecided,
Started: time.Now().Unix(),
RemoteAddress: pkt.FmtRemoteAddress(),
saveWhenFinished: true,
}
return link
@@ -350,59 +370,59 @@ func CreateLinkFromPacket(pkt packet.Packet) *Link {
// GetActiveInspectors returns the list of active inspectors.
func (link *Link) GetActiveInspectors() []bool {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
return link.activeInspectors
}
// SetActiveInspectors sets the list of active inspectors.
func (link *Link) SetActiveInspectors(new []bool) {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
link.activeInspectors = new
}
// GetInspectorData returns the list of inspector data.
func (link *Link) GetInspectorData() map[uint8]interface{} {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
return link.inspectorData
}
// SetInspectorData set the list of inspector data.
func (link *Link) SetInspectorData(new map[uint8]interface{}) {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
link.inspectorData = new
}
// String returns a string representation of Link.
func (link *Link) String() string {
link.Lock()
defer link.Unlock()
link.lock.Lock()
defer link.lock.Unlock()
if link.comm == nil {
return fmt.Sprintf("? <-> %s", link.RemoteAddress)
return fmt.Sprintf("? <-> %s", link.Entity.IP.String())
}
switch link.comm.Domain {
case "I":
switch link.comm.Scope {
case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
if link.comm.process == nil {
return fmt.Sprintf("? <- %s", link.RemoteAddress)
return fmt.Sprintf("? <- %s", link.Entity.IP.String())
}
return fmt.Sprintf("%s <- %s", link.comm.process.String(), link.RemoteAddress)
case "D":
return fmt.Sprintf("%s <- %s", link.comm.process.String(), link.Entity.IP.String())
case PeerHost, PeerLAN, PeerInternet, PeerInvalid:
if link.comm.process == nil {
return fmt.Sprintf("? -> %s", link.RemoteAddress)
return fmt.Sprintf("? -> %s", link.Entity.IP.String())
}
return fmt.Sprintf("%s -> %s", link.comm.process.String(), link.RemoteAddress)
return fmt.Sprintf("%s -> %s", link.comm.process.String(), link.Entity.IP.String())
default:
if link.comm.process == nil {
return fmt.Sprintf("? -> %s (%s)", link.comm.Domain, link.RemoteAddress)
return fmt.Sprintf("? -> %s (%s)", link.comm.Scope, link.Entity.IP.String())
}
return fmt.Sprintf("%s to %s (%s)", link.comm.process.String(), link.comm.Domain, link.RemoteAddress)
return fmt.Sprintf("%s to %s (%s)", link.comm.process.String(), link.comm.Scope, link.Entity.IP.String())
}
}

View File

@@ -0,0 +1,78 @@
package reference
import (
"strconv"
"strings"
)
var (
portNames = map[uint16]string{
20: "FTP-DATA",
21: "FTP",
22: "SSH",
23: "TELNET",
25: "SMTP",
43: "WHOIS",
53: "DNS",
67: "DHCP-SERVER",
68: "DHCP-CLIENT",
69: "TFTP",
80: "HTTP",
110: "POP3",
123: "NTP",
143: "IMAP",
161: "SNMP",
179: "BGP",
194: "IRC",
389: "LDAP",
443: "HTTPS",
587: "SMTP-ALT",
465: "SMTP-SSL",
993: "IMAP-SSL",
995: "POP3-SSL",
}
portNumbers = map[string]uint16{
"FTP-DATA": 20,
"FTP": 21,
"SSH": 22,
"TELNET": 23,
"SMTP": 25,
"WHOIS": 43,
"DNS": 53,
"DHCP-SERVER": 67,
"DHCP-CLIENT": 68,
"TFTP": 69,
"HTTP": 80,
"POP3": 110,
"NTP": 123,
"IMAP": 143,
"SNMP": 161,
"BGP": 179,
"IRC": 194,
"LDAP": 389,
"HTTPS": 443,
"SMTP-ALT": 587,
"SMTP-SSL": 465,
"IMAP-SSL": 993,
"POP3-SSL": 995,
}
)
// GetPortName returns the name of a port number.
func GetPortName(port uint16) (name string) {
name, ok := portNames[port]
if ok {
return name
}
return strconv.Itoa(int(port))
}
// GetPortNumber returns the number of a port name.
func GetPortNumber(port string) (number uint16, ok bool) {
number, ok = portNumbers[strings.ToUpper(port)]
if ok {
return number, true
}
return 0, false
}

View File

@@ -1,6 +1,9 @@
package reference
import "strconv"
import (
"strconv"
"strings"
)
var (
protocolNames = map[uint8]string{
@@ -9,20 +12,20 @@ var (
6: "TCP",
17: "UDP",
27: "RDP",
58: "ICMPv6",
58: "ICMP6",
33: "DCCP",
136: "UDPLite",
136: "UDP-LITE",
}
protocolNumbers = map[string]uint8{
"ICMP": 1,
"IGMP": 2,
"TCP": 6,
"UDP": 17,
"RDP": 27,
"DCCP": 33,
"ICMPv6": 58,
"UDPLite": 136,
"ICMP": 1,
"IGMP": 2,
"TCP": 6,
"UDP": 17,
"RDP": 27,
"DCCP": 33,
"ICMP6": 58,
"UDP-LITE": 136,
}
)
@@ -37,7 +40,7 @@ func GetProtocolName(protocol uint8) (name string) {
// GetProtocolNumber returns the number of a IP protocol name.
func GetProtocolNumber(protocol string) (number uint8, ok bool) {
number, ok = protocolNumbers[protocol]
number, ok = protocolNumbers[strings.ToUpper(protocol)]
if ok {
return number, true
}

View File

@@ -5,36 +5,38 @@ import (
"os"
"time"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process"
)
// GetOwnComm returns the communication for the given packet, that originates from
// GetOwnComm returns the communication for the given packet, that originates from the Portmaster itself.
func GetOwnComm(pkt packet.Packet) (*Communication, error) {
var domain string
var scope string
// Incoming
if pkt.IsInbound() {
switch netutils.ClassifyIP(pkt.Info().RemoteIP()) {
case netutils.HostLocal:
domain = IncomingHost
scope = IncomingHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = IncomingLAN
scope = IncomingLAN
case netutils.Global, netutils.GlobalMulticast:
domain = IncomingInternet
scope = IncomingInternet
case netutils.Invalid:
domain = IncomingInvalid
scope = IncomingInvalid
}
communication, ok := GetCommunication(os.Getpid(), domain)
communication, ok := GetCommunication(os.Getpid(), scope)
if !ok {
proc, err := process.GetOrFindProcess(pkt.Ctx(), os.Getpid())
if err != nil {
return nil, fmt.Errorf("could not get own process")
}
communication = &Communication{
Domain: domain,
Scope: scope,
Entity: (&intel.Entity{}).Init(),
Direction: Inbound,
process: proc,
Inspect: true,
@@ -48,23 +50,24 @@ func GetOwnComm(pkt packet.Packet) (*Communication, error) {
// PeerToPeer
switch netutils.ClassifyIP(pkt.Info().RemoteIP()) {
case netutils.HostLocal:
domain = PeerHost
scope = PeerHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = PeerLAN
scope = PeerLAN
case netutils.Global, netutils.GlobalMulticast:
domain = PeerInternet
scope = PeerInternet
case netutils.Invalid:
domain = PeerInvalid
scope = PeerInvalid
}
communication, ok := GetCommunication(os.Getpid(), domain)
communication, ok := GetCommunication(os.Getpid(), scope)
if !ok {
proc, err := process.GetOrFindProcess(pkt.Ctx(), os.Getpid())
if err != nil {
return nil, fmt.Errorf("could not get own process")
}
communication = &Communication{
Domain: domain,
Scope: scope,
Entity: (&intel.Entity{}).Init(),
Direction: Outbound,
process: proc,
Inspect: true,

View File

@@ -42,7 +42,7 @@ const (
Outbound = false
)
// Non-Domain Connections
// Non-Domain Scopes
const (
IncomingHost = "IH"
IncomingLAN = "IL"

View File

@@ -3,6 +3,7 @@ package network
import (
"time"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process"
@@ -43,11 +44,12 @@ func GetUnknownCommunication(pkt packet.Packet) (*Communication, error) {
return getOrCreateUnknownCommunication(pkt, PeerInvalid)
}
func getOrCreateUnknownCommunication(pkt packet.Packet, connClass string) (*Communication, error) {
connection, ok := GetCommunication(process.UnknownProcess.Pid, connClass)
func getOrCreateUnknownCommunication(pkt packet.Packet, connScope string) (*Communication, error) {
connection, ok := GetCommunication(process.UnknownProcess.Pid, connScope)
if !ok {
connection = &Communication{
Domain: connClass,
Scope: connScope,
Entity: (&intel.Entity{}).Init(),
Direction: pkt.IsInbound(),
Verdict: VerdictDrop,
Reason: ReasonUnknownProcess,

29
process/config.go Normal file
View File

@@ -0,0 +1,29 @@
package process
import (
"github.com/safing/portbase/config"
)
var (
CfgOptionEnableProcessDetectionKey = "core/enableProcessDetection"
enableProcessDetection config.BoolOption
)
func registerConfiguration() error {
// Enable Process Detection
// This should be always enabled. Provided as an option to disable in case there are severe problems on a system, or for debugging.
err := config.Register(&config.Option{
Name: "Enable Process Detection",
Key: CfgOptionEnableProcessDetectionKey,
Description: "This option enables the attribution of network traffic to processes. This should be always enabled, and effectively disables app profiles if disabled.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
DefaultValue: true,
})
if err != nil {
return err
}
enableProcessDetection = config.Concurrent.GetAsBool(CfgOptionEnableProcessDetectionKey, true)
return nil
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/safing/portbase/database"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/profile"
"github.com/tevino/abool"
)
@@ -90,11 +89,7 @@ func (p *Process) Delete() {
go dbController.PushUpdate(p)
}
// deactivate profile
// TODO: check if there is another process using the same profile set
if p.profileSet != nil {
profile.DeactivateProfileSet(p.profileSet)
}
// TODO: maybe mark the assigned profiles as no longer needed?
}
// CleanProcessStorage cleans the storage from old processes.

View File

@@ -56,6 +56,11 @@ func GetPidByPacket(pkt packet.Packet) (pid int, direction bool, err error) {
// GetProcessByPacket returns the process that owns the given packet.
func GetProcessByPacket(pkt packet.Packet) (process *Process, direction bool, err error) {
if !enableProcessDetection() {
log.Tracer(pkt.Ctx()).Tracef("process: process detection disabled")
return UnknownProcess, direction, nil
}
log.Tracer(pkt.Ctx()).Tracef("process: getting process and profile by packet")
var pid int
@@ -75,10 +80,9 @@ func GetProcessByPacket(pkt packet.Packet) (process *Process, direction bool, er
return nil, direction, err
}
err = process.FindProfiles(pkt.Ctx())
err = process.GetProfile(pkt.Ctx())
if err != nil {
log.Tracer(pkt.Ctx()).Errorf("process: failed to find profiles for process %s: %s", process, err)
log.Errorf("failed to find profiles for process %s: %s", process, err)
log.Tracer(pkt.Ctx()).Errorf("process: failed to get profile for process %s: %s", process, err)
}
return process, direction, nil
@@ -110,6 +114,11 @@ func GetPidByEndpoints(localIP net.IP, localPort uint16, remoteIP net.IP, remote
// GetProcessByEndpoints returns the process that owns the described link.
func GetProcessByEndpoints(ctx context.Context, localIP net.IP, localPort uint16, remoteIP net.IP, remotePort uint16, protocol packet.IPProtocol) (process *Process, err error) {
if !enableProcessDetection() {
log.Tracer(ctx).Tracef("process: process detection disabled")
return UnknownProcess, nil
}
log.Tracer(ctx).Tracef("process: getting process and profile by endpoints")
var pid int
@@ -129,10 +138,9 @@ func GetProcessByEndpoints(ctx context.Context, localIP net.IP, localPort uint16
return nil, err
}
err = process.FindProfiles(ctx)
err = process.GetProfile(ctx)
if err != nil {
log.Tracer(ctx).Errorf("process: failed to find profiles for process %s: %s", process, err)
log.Errorf("process: failed to find profiles for process %s: %s", process, err)
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
}
return process, nil

View File

@@ -1,108 +0,0 @@
package process
import (
"context"
"fmt"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/profile"
)
var (
profileDB = database.NewInterface(nil)
)
// FindProfiles finds and assigns a profile set to the process.
func (p *Process) FindProfiles(ctx context.Context) error {
log.Tracer(ctx).Trace("process: loading profile set")
p.Lock()
defer p.Unlock()
// only find profiles if not already done.
if p.profileSet != nil {
return nil
}
// User Profile
it, err := profileDB.Query(query.New(profile.MakeProfileKey(profile.UserNamespace, "")).Where(query.Where("LinkedPath", query.SameAs, p.Path)))
if err != nil {
return err
}
var userProfile *profile.Profile
// get first result
r := <-it.Next
// cancel immediately
it.Cancel()
// ensure its a profile
userProfile, err = profile.EnsureProfile(r)
if err != nil {
return err
}
// create new profile if it does not exist.
if userProfile == nil {
// create new profile
userProfile = profile.New()
userProfile.Name = p.ExecName
userProfile.LinkedPath = p.Path
}
if userProfile.MarkUsed() {
_ = userProfile.Save(profile.UserNamespace)
}
// Stamp
// Find/Re-evaluate Stamp profile
// 1. check linked stamp profile
// 2. if last check is was more than a week ago, fetch from stamp:
// 3. send path identifier to stamp
// 4. evaluate all returned profiles
// 5. select best
// 6. link stamp profile to user profile
// FIXME: implement!
p.UserProfileKey = userProfile.Key()
p.profileSet = profile.NewSet(ctx, fmt.Sprintf("%d-%s", p.Pid, p.Path), userProfile, nil)
go p.Save()
return nil
}
//nolint:deadcode,unused // FIXME
func matchProfile(p *Process, prof *profile.Profile) (score int) {
for _, fp := range prof.Fingerprints {
score += matchFingerprint(p, fp)
}
return
}
//nolint:deadcode,unused // FIXME
func matchFingerprint(p *Process, fp *profile.Fingerprint) (score int) {
if !fp.MatchesOS() {
return 0
}
switch fp.Type {
case "full_path":
if p.Path == fp.Value {
return profile.GetFingerprintWeight(fp.Type)
}
case "partial_path":
// FIXME: if full_path matches, do not match partial paths
return profile.GetFingerprintWeight(fp.Type)
case "md5_sum", "sha1_sum", "sha256_sum":
// FIXME: one sum is enough, check sums in a grouped form, start with the best
sum, err := p.GetExecHash(fp.Type)
if err != nil {
log.Errorf("process: failed to get hash of executable: %s", err)
} else if sum == fp.Value {
return profile.GetFingerprintWeight(fp.Type)
}
}
return 0
}

17
process/module.go Normal file
View File

@@ -0,0 +1,17 @@
package process
import (
"github.com/safing/portbase/modules"
)
var (
module *modules.Module
)
func init() {
module = modules.Register("processes", prep, nil, nil, "profiles")
}
func prep() error {
return registerConfiguration()
}

View File

@@ -17,7 +17,7 @@ var (
)
// GetPidOfInode returns the pid of the given uid and socket inode.
func GetPidOfInode(uid, inode int) (int, bool) {
func GetPidOfInode(uid, inode int) (int, bool) { //nolint:gocognit // TODO
pidsByUserLock.Lock()
defer pidsByUserLock.Unlock()

View File

@@ -40,10 +40,10 @@ type Process struct {
// ExecOwner ...
// ExecSignature ...
UserProfileKey string
profileSet *profile.Set
Name string
Icon string
LocalProfileKey string
profile *profile.LayeredProfile
Name string
Icon string
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for database cache path or "c:"/"a:" for a the icon key to fetch it from a company / authoritative node and cache it in its own cache.
FirstCommEstablished int64
@@ -53,12 +53,12 @@ type Process struct {
Error string // If this is set, the process is invalid. This is used to cache failing or inexistent processes.
}
// ProfileSet returns the assigned profile set.
func (p *Process) ProfileSet() *profile.Set {
// Profile returns the assigned layered profile.
func (p *Process) Profile() *profile.LayeredProfile {
p.Lock()
defer p.Unlock()
return p.profileSet
return p.profile
}
// Strings returns a string representation of process.
@@ -208,13 +208,14 @@ func GetOrFindProcess(ctx context.Context, pid int) (*Process, error) {
func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) {
dupReqLock.Lock()
defer dupReqLock.Unlock()
// get duplicate request waitgroup
wg, requestActive := dupReqMap[pid]
// someone else is already on it!
if requestActive {
dupReqLock.Unlock()
// log that we are waiting
log.Tracer(ctx).Tracef("intel: waiting for duplicate request for PID %d to complete", pid)
// wait
@@ -232,6 +233,8 @@ func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) {
// add to registry
dupReqMap[pid] = wg
dupReqLock.Unlock()
// return function to mark request as finished
return func() {
dupReqLock.Lock()

42
process/profile.go Normal file
View File

@@ -0,0 +1,42 @@
package process
import (
"context"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/profile"
)
// GetProfile finds and assigns a profile set to the process.
func (p *Process) GetProfile(ctx context.Context) error {
p.Lock()
defer p.Unlock()
// only find profiles if not already done.
if p.profile != nil {
log.Tracer(ctx).Trace("process: profile already loaded")
return nil
}
log.Tracer(ctx).Trace("process: loading profile")
// get profile
localProfile, new, err := profile.FindOrCreateLocalProfileByPath(p.Path)
if err != nil {
return err
}
// add more information if new
if new {
localProfile.Name = p.ExecName
}
// mark as used and save
if localProfile.MarkUsed() {
_ = localProfile.Save()
}
p.LocalProfileKey = localProfile.Key()
p.profile = profile.NewLayeredProfile(localProfile)
go p.Save()
return nil
}

View File

@@ -1,76 +1,44 @@
package profile
import (
"context"
"sync"
"github.com/safing/portbase/log"
)
var (
activeProfileSets = make(map[string]*Set)
activeProfileSetsLock sync.RWMutex
// TODO: periodically clean up inactive profiles
activeProfiles = make(map[string]*Profile)
activeProfilesLock sync.RWMutex
)
func activateProfileSet(ctx context.Context, set *Set) {
activeProfileSetsLock.Lock()
defer activeProfileSetsLock.Unlock()
set.Lock()
defer set.Unlock()
activeProfileSets[set.id] = set
log.Tracer(ctx).Tracef("profile: activated profile set %s", set.id)
}
// getActiveProfile returns a cached copy of an active profile and nil if it isn't found.
func getActiveProfile(scopedID string) *Profile {
activeProfilesLock.Lock()
defer activeProfilesLock.Unlock()
// DeactivateProfileSet marks a profile set as not active.
func DeactivateProfileSet(set *Set) {
activeProfileSetsLock.Lock()
defer activeProfileSetsLock.Unlock()
set.Lock()
defer set.Unlock()
delete(activeProfileSets, set.id)
log.Tracef("profile: deactivated profile set %s", set.id)
}
func updateActiveProfile(profile *Profile, userProfile bool) {
activeProfileSetsLock.RLock()
defer activeProfileSetsLock.RUnlock()
var activeProfile *Profile
var profilesUpdated bool
// iterate all active profile sets
for _, activeSet := range activeProfileSets {
activeSet.Lock()
if userProfile {
activeProfile = activeSet.profiles[0]
} else {
activeProfile = activeSet.profiles[2]
}
// check if profile exists (for stamp profiles)
if activeProfile != nil {
activeProfile.Lock()
// check if the stamp profile has the same ID
if activeProfile.ID == profile.ID {
if userProfile {
activeSet.profiles[0] = profile
log.Infof("profile: updated active user profile %s (%s)", profile.ID, profile.LinkedPath)
} else {
activeSet.profiles[2] = profile
log.Infof("profile: updated active stamp profile %s", profile.ID)
}
profilesUpdated = true
}
activeProfile.Unlock()
}
activeSet.Unlock()
profile, ok := activeProfiles[scopedID]
if ok {
return profile
}
if profilesUpdated {
increaseUpdateVersion()
return nil
}
// markProfileActive registers a profile as active.
func markProfileActive(profile *Profile) {
activeProfilesLock.Lock()
defer activeProfilesLock.Unlock()
activeProfiles[profile.ScopedID()] = profile
}
// markActiveProfileAsOutdated marks an active profile as outdated, so that it will be refetched from the database.
func markActiveProfileAsOutdated(scopedID string) {
activeProfilesLock.Lock()
defer activeProfilesLock.Unlock()
profile, ok := activeProfiles[scopedID]
if ok {
profile.oudated.Set()
delete(activeProfiles, scopedID)
}
}

94
profile/config-update.go Normal file
View File

@@ -0,0 +1,94 @@
package profile
import (
"context"
"fmt"
"sync"
"github.com/safing/portmaster/profile/endpoints"
)
var (
cfgLock sync.RWMutex
cfgDefaultAction uint8
cfgEndpoints endpoints.Endpoints
cfgServiceEndpoints endpoints.Endpoints
)
func registerConfigUpdater() error {
return module.RegisterEventHook(
"config",
"config change",
"update global config profile",
updateGlobalConfigProfile,
)
}
func updateGlobalConfigProfile(ctx context.Context, data interface{}) error {
cfgLock.Lock()
defer cfgLock.Unlock()
var err error
var lastErr error
action := cfgOptionDefaultAction()
switch action {
case "permit":
cfgDefaultAction = DefaultActionPermit
case "ask":
cfgDefaultAction = DefaultActionAsk
case "block":
cfgDefaultAction = DefaultActionBlock
default:
// TODO: module error?
lastErr = fmt.Errorf(`default action "%s" invalid`, action)
cfgDefaultAction = DefaultActionBlock // default to block in worst case
}
list := cfgOptionEndpoints()
cfgEndpoints, err = endpoints.ParseEndpoints(list)
if err != nil {
// TODO: module error?
lastErr = err
}
list = cfgOptionServiceEndpoints()
cfgServiceEndpoints, err = endpoints.ParseEndpoints(list)
if err != nil {
// TODO: module error?
lastErr = err
}
// build global profile for reference
profile := &Profile{
ID: "config",
Source: SourceGlobal,
Name: "Global Configuration",
Config: make(map[string]interface{}),
internalSave: true,
}
// fill profile config options
for key, value := range cfgStringOptions {
profile.Config[key] = value
}
for key, value := range cfgStringArrayOptions {
profile.Config[key] = value
}
for key, value := range cfgIntOptions {
profile.Config[key] = value
}
for key, value := range cfgBoolOptions {
profile.Config[key] = value
}
// save profile
err = profile.Save()
if err != nil && lastErr == nil {
// other errors are more important
lastErr = err
}
return lastErr
}

270
profile/config.go Normal file
View File

@@ -0,0 +1,270 @@
package profile
import (
"github.com/safing/portbase/config"
)
var (
cfgStringOptions = make(map[string]config.StringOption)
cfgStringArrayOptions = make(map[string]config.StringArrayOption)
cfgIntOptions = make(map[string]config.IntOption)
cfgBoolOptions = make(map[string]config.BoolOption)
CfgOptionDefaultActionKey = "filter/defaultAction"
cfgOptionDefaultAction config.StringOption
CfgOptionDisableAutoPermitKey = "filter/disableAutoPermit"
cfgOptionDisableAutoPermit config.IntOption // security level option
CfgOptionEndpointsKey = "filter/endpoints"
cfgOptionEndpoints config.StringArrayOption
CfgOptionServiceEndpointsKey = "filter/serviceEndpoints"
cfgOptionServiceEndpoints config.StringArrayOption
CfgOptionBlockScopeLocalKey = "filter/blockLocal"
cfgOptionBlockScopeLocal config.IntOption // security level option
CfgOptionBlockScopeLANKey = "filter/blockLAN"
cfgOptionBlockScopeLAN config.IntOption // security level option
CfgOptionBlockScopeInternetKey = "filter/blockInternet"
cfgOptionBlockScopeInternet config.IntOption // security level option
CfgOptionBlockP2PKey = "filter/blockP2P"
cfgOptionBlockP2P config.IntOption // security level option
CfgOptionBlockInboundKey = "filter/blockInbound"
cfgOptionBlockInbound config.IntOption // security level option
CfgOptionEnforceSPNKey = "filter/enforceSPN"
cfgOptionEnforceSPN config.IntOption // security level option
CfgOptionRemoveOutOfScopeDNSKey = "filter/removeOutOfScopeDNS"
cfgOptionRemoveOutOfScopeDNS config.IntOption // security level option
CfgOptionRemoveBlockedDNSKey = "filter/removeBlockedDNS"
cfgOptionRemoveBlockedDNS config.IntOption // security level option
)
func registerConfiguration() error {
// Default Filter Action
// permit - blacklist mode: everything is permitted unless blocked
// ask - ask mode: if not verdict is found, the user is consulted
// block - whitelist mode: everything is blocked unless permitted
err := config.Register(&config.Option{
Name: "Default Filter Action",
Key: CfgOptionDefaultActionKey,
Description: `The default filter action when nothing else permits or blocks a connection.`,
OptType: config.OptTypeString,
DefaultValue: "permit",
ValidationRegex: "^(permit|ask|block)$",
})
if err != nil {
return err
}
cfgOptionDefaultAction = config.Concurrent.GetAsString(CfgOptionDefaultActionKey, "permit")
cfgStringOptions[CfgOptionDefaultActionKey] = cfgOptionDefaultAction
// Disable Auto Permit
err = config.Register(&config.Option{
Name: "Disable Auto Permit",
Key: CfgOptionDisableAutoPermitKey,
Description: "Auto Permit searches for a relation between an app and the destionation of a connection - if there is a correlation, the connection will be permitted. This setting is negated in order to provide a streamlined user experience, where higher settings are better.",
OptType: config.OptTypeInt,
ExternalOptType: "security level",
DefaultValue: 4,
ValidationRegex: "^(4|6|7)$",
})
if err != nil {
return err
}
cfgOptionDisableAutoPermit = config.Concurrent.GetAsInt(CfgOptionDisableAutoPermitKey, 4)
cfgIntOptions[CfgOptionDisableAutoPermitKey] = cfgOptionDisableAutoPermit
// Endpoint Filter List
err = config.Register(&config.Option{
Name: "Endpoint Filter List",
Key: CfgOptionEndpointsKey,
Description: "Filter outgoing connections by matching the destination endpoint. Network Scope restrictions still apply.",
Help: `Format:
Permission:
"+": permit
"-": block
Host Matching:
IP, CIDR, Country Code, ASN, "*" for any
Domains:
"example.com": exact match
".example.com": exact match + subdomains
"*xample.com": prefix wildcard
"example.*": suffix wildcard
"*example*": prefix and suffix wildcard
Protocol and Port Matching (optional):
<protocol>/<port>
Examples:
+ .example.com */HTTP
- .example.com
+ 192.168.0.1/24`,
OptType: config.OptTypeStringArray,
DefaultValue: []string{},
ExternalOptType: "endpoint list",
ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`,
})
if err != nil {
return err
}
cfgOptionEndpoints = config.Concurrent.GetAsStringArray(CfgOptionEndpointsKey, []string{})
cfgStringArrayOptions[CfgOptionEndpointsKey] = cfgOptionEndpoints
// Service Endpoint Filter List
err = config.Register(&config.Option{
Name: "Service Endpoint Filter List",
Key: CfgOptionServiceEndpointsKey,
Description: "Filter incoming connections by matching the source endpoint. Network Scope restrictions and the inbound permission still apply. Also not that the implicit default action of this list is to always block.",
OptType: config.OptTypeStringArray,
DefaultValue: []string{},
ExternalOptType: "endpoint list",
ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`,
})
if err != nil {
return err
}
cfgOptionServiceEndpoints = config.Concurrent.GetAsStringArray(CfgOptionServiceEndpointsKey, []string{})
cfgStringArrayOptions[CfgOptionServiceEndpointsKey] = cfgOptionServiceEndpoints
// Block Scope Local
err = config.Register(&config.Option{
Name: "Block Scope Local",
Key: CfgOptionBlockScopeLocalKey,
Description: "Block connections to your own device, ie. localhost.",
OptType: config.OptTypeInt,
ExternalOptType: "security level",
DefaultValue: 0,
ValidationRegex: "^(0|4|6|7)$",
})
if err != nil {
return err
}
cfgOptionBlockScopeLocal = config.Concurrent.GetAsInt(CfgOptionBlockScopeLocalKey, 0)
cfgIntOptions[CfgOptionBlockScopeLocalKey] = cfgOptionBlockScopeLocal
// Block Scope LAN
err = config.Register(&config.Option{
Name: "Block Scope LAN",
Key: CfgOptionBlockScopeLANKey,
Description: "Block connections to the Local Area Network.",
OptType: config.OptTypeInt,
ExternalOptType: "security level",
DefaultValue: 0,
ValidationRegex: "^(0|4|6|7)$",
})
if err != nil {
return err
}
cfgOptionBlockScopeLAN = config.Concurrent.GetAsInt(CfgOptionBlockScopeLANKey, 0)
cfgIntOptions[CfgOptionBlockScopeLANKey] = cfgOptionBlockScopeLAN
// Block Scope Internet
err = config.Register(&config.Option{
Name: "Block Scope Internet",
Key: CfgOptionBlockScopeInternetKey,
Description: "Block connections to the Internet.",
OptType: config.OptTypeInt,
ExternalOptType: "security level",
DefaultValue: 0,
ValidationRegex: "^(0|4|6|7)$",
})
if err != nil {
return err
}
cfgOptionBlockScopeInternet = config.Concurrent.GetAsInt(CfgOptionBlockScopeInternetKey, 0)
cfgIntOptions[CfgOptionBlockScopeInternetKey] = cfgOptionBlockScopeInternet
// Block Peer to Peer Connections
err = config.Register(&config.Option{
Name: "Block Peer to Peer Connections",
Key: CfgOptionBlockP2PKey,
Description: "Block peer to peer connections. These are connections that are established directly to an IP address on the Internet without resolving a domain name via DNS first.",
OptType: config.OptTypeInt,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(4|6|7)$",
})
if err != nil {
return err
}
cfgOptionBlockP2P = config.Concurrent.GetAsInt(CfgOptionBlockP2PKey, 7)
cfgIntOptions[CfgOptionBlockP2PKey] = cfgOptionBlockP2P
// Block Inbound Connections
err = config.Register(&config.Option{
Name: "Block Inbound Connections",
Key: CfgOptionBlockInboundKey,
Description: "Block inbound connections to your device. This will usually only be the case if you are running a network service or are using peer to peer software.",
OptType: config.OptTypeInt,
ExternalOptType: "security level",
DefaultValue: 4,
ValidationRegex: "^(4|6|7)$",
})
if err != nil {
return err
}
cfgOptionBlockInbound = config.Concurrent.GetAsInt(CfgOptionBlockInboundKey, 6)
cfgIntOptions[CfgOptionBlockInboundKey] = cfgOptionBlockInbound
// Enforce SPN
err = config.Register(&config.Option{
Name: "Enforce SPN",
Key: CfgOptionEnforceSPNKey,
Description: "This setting enforces connections to be routed over the SPN. If this is not possible for any reason, connections will be blocked.",
OptType: config.OptTypeInt,
ReleaseLevel: config.ReleaseLevelExperimental,
ExternalOptType: "security level",
DefaultValue: 0,
ValidationRegex: "^(0|4|6|7)$",
})
if err != nil {
return err
}
cfgOptionEnforceSPN = config.Concurrent.GetAsInt(CfgOptionEnforceSPNKey, 0)
cfgIntOptions[CfgOptionEnforceSPNKey] = cfgOptionEnforceSPN
// Filter Out-of-Scope DNS Records
err = config.Register(&config.Option{
Name: "Filter Out-of-Scope DNS Records",
Key: CfgOptionRemoveOutOfScopeDNSKey,
Description: "Filter DNS answers that are outside of the scope of the server. A server on the public Internet may not respond with a private LAN address.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelBeta,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
cfgOptionRemoveOutOfScopeDNS = config.Concurrent.GetAsInt(CfgOptionRemoveOutOfScopeDNSKey, 7)
cfgIntOptions[CfgOptionRemoveOutOfScopeDNSKey] = cfgOptionEnforceSPN
// Filter DNS Records that would be blocked
err = config.Register(&config.Option{
Name: "Filter DNS Records that would be blocked",
Key: CfgOptionRemoveBlockedDNSKey,
Description: "Pre-filter DNS answers that an application would not be allowed to connect to.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelBeta,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
cfgOptionRemoveBlockedDNS = config.Concurrent.GetAsInt(CfgOptionRemoveBlockedDNSKey, 7)
cfgIntOptions[CfgOptionRemoveBlockedDNSKey] = cfgOptionEnforceSPN
return nil
}

View File

@@ -1,22 +1,105 @@
package profile
import (
"context"
"errors"
"strings"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/database/record"
)
// core:profiles/user/12345-1234-125-1234-1235
// core:profiles/special/default
// /global
// core:profiles/stamp/12334-1235-1234-5123-1234
// core:profiles/identifier/base64
// Database paths:
// core:profiles/<scope>/<id>
// cache:profiles/index/<identifier>/<value>
// Namespaces
const (
UserNamespace = "user"
StampNamespace = "stamp"
SpecialNamespace = "special"
profilesDBPath = "core:profiles/"
)
var (
profileDB = database.NewInterface(nil)
)
func makeScopedID(source, id string) string {
return source + "/" + id
}
func makeProfileKey(source, id string) string {
return profilesDBPath + source + "/" + id
}
func registerValidationDBHook() (err error) {
_, err = database.RegisterHook(query.New(profilesDBPath), &databaseHook{})
return
}
func startProfileUpdateChecker() error {
profilesSub, err := profileDB.Subscribe(query.New(profilesDBPath))
if err != nil {
return err
}
module.StartServiceWorker("update active profiles", 0, func(ctx context.Context) (err error) {
feedSelect:
for {
select {
case r := <-profilesSub.Feed:
// check if nil
if r == nil {
return errors.New("subscription canceled")
}
// check if internal save
if !r.IsWrapped() {
profile, ok := r.(*Profile)
if ok && profile.internalSave {
continue feedSelect
}
}
// mark as outdated
markActiveProfileAsOutdated(strings.TrimPrefix(r.Key(), profilesDBPath))
case <-ctx.Done():
return profilesSub.Cancel()
}
}
})
return nil
}
type databaseHook struct {
database.HookBase
}
// UsesPrePut implements the Hook interface and returns false.
func (h *databaseHook) UsesPrePut() bool {
return true
}
// PrePut implements the Hook interface.
func (h *databaseHook) PrePut(r record.Record) (record.Record, error) {
// convert
profile, err := EnsureProfile(r)
if err != nil {
return nil, err
}
// prepare config
err = profile.prepConfig()
if err != nil {
// error here, warning when loading
return nil, err
}
// parse config
err = profile.parseConfig()
if err != nil {
// error here, warning when loading
return nil, err
}
return profile, nil
}

View File

@@ -1,41 +0,0 @@
package profile
import (
"github.com/safing/portmaster/status"
)
func makeDefaultGlobalProfile() *Profile {
return &Profile{
ID: "global",
Name: "Global Profile",
}
}
func makeDefaultFallbackProfile() *Profile {
return &Profile{
ID: "fallback",
Name: "Fallback Profile",
Flags: map[uint8]uint8{
// Profile Modes
Blacklist: status.SecurityLevelsDynamicAndSecure,
Whitelist: status.SecurityLevelFortress,
// Network Locations
Internet: status.SecurityLevelsAll,
LAN: status.SecurityLevelDynamic,
Localhost: status.SecurityLevelsAll,
// Specials
Related: status.SecurityLevelDynamic,
},
ServiceEndpoints: []*EndpointPermission{
{
Type: EptAny,
Protocol: 0,
StartPort: 0,
EndPort: 0,
Permit: false,
},
},
}
}

View File

@@ -1,319 +0,0 @@
package profile
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"github.com/safing/portmaster/intel"
)
// Endpoints is a list of permitted or denied endpoints.
type Endpoints []*EndpointPermission
// EndpointPermission holds a decision about an endpoint.
type EndpointPermission struct {
Value string
Type EPType
Protocol uint8
StartPort uint16
EndPort uint16
Permit bool
Created int64
}
// EPType represents the type of an EndpointPermission
type EPType uint8
// EPType values
const (
EptUnknown EPType = 0
EptAny EPType = 1
EptDomain EPType = 2
EptIPv4 EPType = 3
EptIPv6 EPType = 4
EptIPv4Range EPType = 5
EptIPv6Range EPType = 6
EptASN EPType = 7
EptCountry EPType = 8
)
// EPResult represents the result of a check against an EndpointPermission
type EPResult uint8
// EndpointPermission return values
const (
NoMatch EPResult = iota
Undeterminable
Denied
Permitted
)
// IsSet returns whether the Endpoints object is "set".
func (e Endpoints) IsSet() bool {
return len(e) > 0
}
// CheckDomain checks the if the given endpoint matches a EndpointPermission in the list.
func (e Endpoints) CheckDomain(domain string) (result EPResult, reason string) {
if domain == "" {
return Denied, "internal error"
}
for _, entry := range e {
if entry != nil {
if result, reason = entry.MatchesDomain(domain); result != NoMatch {
return
}
}
}
return NoMatch, ""
}
// CheckIP checks the if the given endpoint matches a EndpointPermission in the list. If _checkReverseIP_ and no domain is given, the IP will be resolved to a domain, if necessary.
func (e Endpoints) CheckIP(domain string, ip net.IP, protocol uint8, port uint16, checkReverseIP bool, securityLevel uint8) (result EPResult, reason string) {
if ip == nil {
return Denied, "internal error"
}
// ip resolving
var cachedGetDomainOfIP func() string
if checkReverseIP {
var ipResolved bool
var ipName string
// setup caching wrapper
cachedGetDomainOfIP = func() string {
if !ipResolved {
result, err := intel.ResolveIPAndValidate(context.TODO(), ip.String(), securityLevel)
if err != nil {
// log.Debug()
ipName = result
}
ipResolved = true
}
return ipName
}
}
for _, entry := range e {
if entry != nil {
if result, reason := entry.MatchesIP(domain, ip, protocol, port, cachedGetDomainOfIP); result != NoMatch {
return result, reason
}
}
}
return NoMatch, ""
}
func (ep EndpointPermission) matchesDomainOnly(domain string) (matches bool, reason string) {
dotInFront := strings.HasPrefix(ep.Value, ".")
wildcardInFront := strings.HasPrefix(ep.Value, "*")
wildcardInBack := strings.HasSuffix(ep.Value, "*")
switch {
case dotInFront && !wildcardInFront && !wildcardInBack:
// subdomain or domain
if strings.HasSuffix(domain, ep.Value) || domain == strings.TrimPrefix(ep.Value, ".") {
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
}
case wildcardInFront && wildcardInBack:
if strings.Contains(domain, strings.Trim(ep.Value, "*")) {
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
}
case wildcardInFront:
if strings.HasSuffix(domain, strings.TrimLeft(ep.Value, "*")) {
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
}
case wildcardInBack:
if strings.HasPrefix(domain, strings.TrimRight(ep.Value, "*")) {
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
}
default:
if domain == ep.Value {
return true, ""
}
}
return false, ""
}
func (ep EndpointPermission) matchProtocolAndPortsAndReturn(protocol uint8, port uint16) (result EPResult) {
// only check if protocol is defined
if ep.Protocol > 0 {
// if protocol is unknown, return Undeterminable
if protocol == 0 {
return Undeterminable
}
// if protocol does not match, return NoMatch
if protocol != ep.Protocol {
return NoMatch
}
}
// only check if port is defined
if ep.StartPort > 0 {
// if port is unknown, return Undeterminable
if port == 0 {
return Undeterminable
}
// if port does not match, return NoMatch
if port < ep.StartPort || port > ep.EndPort {
return NoMatch
}
}
// protocol and port matched or were defined as any
if ep.Permit {
return Permitted
}
return Denied
}
// MatchesDomain checks if the given endpoint matches the EndpointPermission.
func (ep EndpointPermission) MatchesDomain(domain string) (result EPResult, reason string) {
switch ep.Type {
case EptAny:
// always matches
case EptDomain:
var matched bool
matched, reason = ep.matchesDomainOnly(domain)
if !matched {
return NoMatch, ""
}
case EptIPv4:
return Undeterminable, ""
case EptIPv6:
return Undeterminable, ""
case EptIPv4Range:
return Undeterminable, ""
case EptIPv6Range:
return Undeterminable, ""
case EptASN:
return Undeterminable, ""
case EptCountry:
return Undeterminable, ""
default:
return Denied, "encountered unknown enpoint permission type"
}
return ep.matchProtocolAndPortsAndReturn(0, 0), reason
}
// MatchesIP checks if the given endpoint matches the EndpointPermission. _getDomainOfIP_, if given, will be used to get the domain if not given.
func (ep EndpointPermission) MatchesIP(domain string, ip net.IP, protocol uint8, port uint16, getDomainOfIP func() string) (result EPResult, reason string) {
switch ep.Type {
case EptAny:
// always matches
case EptDomain:
if domain == "" {
if getDomainOfIP == nil {
return NoMatch, ""
}
domain = getDomainOfIP()
}
var matched bool
matched, reason = ep.matchesDomainOnly(domain)
if !matched {
return NoMatch, ""
}
case EptIPv4, EptIPv6:
if ep.Value != ip.String() {
return NoMatch, ""
}
case EptIPv4Range:
return Denied, "endpoint type IP Range not yet implemented"
case EptIPv6Range:
return Denied, "endpoint type IP Range not yet implemented"
case EptASN:
return Denied, "endpoint type ASN not yet implemented"
case EptCountry:
return Denied, "endpoint type country not yet implemented"
default:
return Denied, "encountered unknown enpoint permission type"
}
return ep.matchProtocolAndPortsAndReturn(protocol, port), reason
}
func (e Endpoints) String() string {
s := make([]string, 0, len(e))
for _, entry := range e {
s = append(s, entry.String())
}
return fmt.Sprintf("[%s]", strings.Join(s, ", "))
}
func (ept EPType) String() string {
switch ept {
case EptAny:
return "Any"
case EptDomain:
return "Domain"
case EptIPv4:
return "IPv4"
case EptIPv6:
return "IPv6"
case EptIPv4Range:
return "IPv4-Range"
case EptIPv6Range:
return "IPv6-Range"
case EptASN:
return "ASN"
case EptCountry:
return "Country"
default:
return "Unknown"
}
}
func (ep EndpointPermission) String() string {
s := ep.Type.String()
if ep.Type != EptAny {
s += ":"
s += ep.Value
}
s += " "
if ep.Protocol > 0 {
s += strconv.Itoa(int(ep.Protocol))
} else {
s += "*"
}
s += "/"
if ep.StartPort > 0 {
if ep.StartPort == ep.EndPort {
s += strconv.Itoa(int(ep.StartPort))
} else {
s += fmt.Sprintf("%d-%d", ep.StartPort, ep.EndPort)
}
} else {
s += "*"
}
return s
}
func (epr EPResult) String() string {
switch epr {
case NoMatch:
return "No Match"
case Undeterminable:
return "Undeterminable"
case Denied:
return "Denied"
case Permitted:
return "Permitted"
default:
return "Unknown"
}
}

View File

@@ -0,0 +1,25 @@
package endpoints
import "github.com/safing/portmaster/intel"
// EndpointAny matches anything.
type EndpointAny struct {
EndpointBase
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointAny) Matches(entity *intel.Entity) (result EPResult, reason string) {
return ep.matchesPPP(entity), "matches *"
}
func (ep *EndpointAny) String() string {
return ep.renderPPP("*")
}
func parseTypeAny(fields []string) (Endpoint, error) {
if fields[1] == "*" {
ep := &EndpointAny{}
return ep.parsePPP(ep, fields)
}
return nil, nil
}

View File

@@ -0,0 +1,58 @@
package endpoints
import (
"fmt"
"regexp"
"strconv"
"github.com/safing/portmaster/intel"
)
var (
asnRegex = regexp.MustCompile("^(AS)?[0-9]+$")
)
// EndpointASN matches ASNs.
type EndpointASN struct {
EndpointBase
ASN uint
Reason string
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointASN) Matches(entity *intel.Entity) (result EPResult, reason string) {
if entity.IP == nil {
return Undeterminable, ""
}
asn, ok := entity.GetASN()
if !ok {
return Undeterminable, ""
}
if asn == ep.ASN {
return ep.matchesPPP(entity), ep.Reason
}
return NoMatch, ""
}
func (ep *EndpointASN) String() string {
return ep.renderPPP("AS" + strconv.FormatInt(int64(ep.ASN), 10))
}
func parseTypeASN(fields []string) (Endpoint, error) {
if asnRegex.MatchString(fields[1]) {
asn, err := strconv.ParseUint(fields[1][2:], 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse AS number %s", fields[1])
}
ep := &EndpointASN{
ASN: uint(asn),
Reason: "IP is part of AS" + strconv.FormatInt(int64(asn), 10),
}
return ep.parsePPP(ep, fields)
}
return nil, nil
}

View File

@@ -0,0 +1,50 @@
package endpoints
import (
"regexp"
"strings"
"github.com/safing/portmaster/intel"
)
var (
countryRegex = regexp.MustCompile(`^[A-Z]{2}$`)
)
// EndpointCountry matches countries.
type EndpointCountry struct {
EndpointBase
Country string
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointCountry) Matches(entity *intel.Entity) (result EPResult, reason string) {
if entity.IP == nil {
return Undeterminable, ""
}
country, ok := entity.GetCountry()
if !ok {
return Undeterminable, ""
}
if country == ep.Country {
return ep.matchesPPP(entity), "IP is located in " + country
}
return NoMatch, ""
}
func (ep *EndpointCountry) String() string {
return ep.renderPPP(ep.Country)
}
func parseTypeCountry(fields []string) (Endpoint, error) {
if countryRegex.MatchString(fields[1]) {
ep := &EndpointCountry{
Country: strings.ToUpper(fields[1]),
}
return ep.parsePPP(ep, fields)
}
return nil, nil
}

View File

@@ -0,0 +1,123 @@
package endpoints
import (
"regexp"
"strings"
"github.com/safing/portmaster/intel"
)
const (
domainMatchTypeExact uint8 = iota
domainMatchTypeZone
domainMatchTypeSuffix
domainMatchTypePrefix
domainMatchTypeContains
)
var (
domainRegex = regexp.MustCompile(`^\*?(([a-z0-9][a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z]{2,}\.?$`)
altDomainRegex = regexp.MustCompile(`^\*?[a-z0-9\.-]+\*$`)
)
// EndpointDomain matches domains.
type EndpointDomain struct {
EndpointBase
OriginalValue string
Domain string
DomainZone string
MatchType uint8
Reason string
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointDomain) Matches(entity *intel.Entity) (result EPResult, reason string) {
if entity.Domain == "" {
return NoMatch, ""
}
switch ep.MatchType {
case domainMatchTypeExact:
if entity.Domain == ep.Domain {
return ep.matchesPPP(entity), ep.Reason
}
case domainMatchTypeZone:
if entity.Domain == ep.Domain {
return ep.matchesPPP(entity), ep.Reason
}
if strings.HasSuffix(entity.Domain, ep.DomainZone) {
return ep.matchesPPP(entity), ep.Reason
}
case domainMatchTypeSuffix:
if strings.HasSuffix(entity.Domain, ep.Domain) {
return ep.matchesPPP(entity), ep.Reason
}
case domainMatchTypePrefix:
if strings.HasPrefix(entity.Domain, ep.Domain) {
return ep.matchesPPP(entity), ep.Reason
}
case domainMatchTypeContains:
if strings.Contains(entity.Domain, ep.Domain) {
return ep.matchesPPP(entity), ep.Reason
}
}
return NoMatch, ""
}
func (ep *EndpointDomain) String() string {
return ep.renderPPP(ep.OriginalValue)
}
func parseTypeDomain(fields []string) (Endpoint, error) {
domain := fields[1]
if domainRegex.MatchString(domain) || altDomainRegex.MatchString(domain) {
ep := &EndpointDomain{
OriginalValue: domain,
Reason: "domain matches " + domain,
}
// fix domain ending
switch domain[len(domain)-1] {
case '.':
case '*':
default:
domain += "."
}
// fix domain case
domain = strings.ToLower(domain)
switch {
case strings.HasPrefix(domain, "*") && strings.HasSuffix(domain, "*"):
ep.MatchType = domainMatchTypeContains
ep.Domain = strings.Trim(domain, "*")
return ep.parsePPP(ep, fields)
case strings.HasSuffix(domain, "*"):
ep.MatchType = domainMatchTypePrefix
ep.Domain = strings.Trim(domain, "*")
return ep.parsePPP(ep, fields)
case strings.HasPrefix(domain, "*"):
ep.MatchType = domainMatchTypeSuffix
ep.Domain = strings.Trim(domain, "*")
return ep.parsePPP(ep, fields)
case strings.HasPrefix(domain, "."):
ep.MatchType = domainMatchTypeZone
ep.Domain = strings.TrimLeft(domain, ".")
ep.DomainZone = "." + ep.Domain
return ep.parsePPP(ep, fields)
default:
ep.MatchType = domainMatchTypeExact
ep.Domain = domain
return ep.parsePPP(ep, fields)
}
}
return nil, nil
}

View File

@@ -0,0 +1,42 @@
package endpoints
import (
"net"
"github.com/safing/portmaster/intel"
)
// EndpointIP matches IPs.
type EndpointIP struct {
EndpointBase
IP net.IP
Reason string
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointIP) Matches(entity *intel.Entity) (result EPResult, reason string) {
if entity.IP == nil {
return Undeterminable, ""
}
if ep.IP.Equal(entity.IP) {
return ep.matchesPPP(entity), ep.Reason
}
return NoMatch, ""
}
func (ep *EndpointIP) String() string {
return ep.renderPPP(ep.IP.String())
}
func parseTypeIP(fields []string) (Endpoint, error) {
ip := net.ParseIP(fields[1])
if ip != nil {
ep := &EndpointIP{
IP: ip,
Reason: "IP is " + ip.String(),
}
return ep.parsePPP(ep, fields)
}
return nil, nil
}

View File

@@ -0,0 +1,42 @@
package endpoints
import (
"net"
"github.com/safing/portmaster/intel"
)
// EndpointIPRange matches IP ranges.
type EndpointIPRange struct {
EndpointBase
Net *net.IPNet
Reason string
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointIPRange) Matches(entity *intel.Entity) (result EPResult, reason string) {
if entity.IP == nil {
return Undeterminable, ""
}
if ep.Net.Contains(entity.IP) {
return ep.matchesPPP(entity), ep.Reason
}
return NoMatch, ""
}
func (ep *EndpointIPRange) String() string {
return ep.renderPPP(ep.Net.String())
}
func parseTypeIPRange(fields []string) (Endpoint, error) {
_, net, err := net.ParseCIDR(fields[1])
if err == nil {
ep := &EndpointIPRange{
Net: net,
Reason: "IP is part of " + net.String(),
}
return ep.parsePPP(ep, fields)
}
return nil, nil
}

View File

@@ -0,0 +1,46 @@
package endpoints
import (
"strings"
"github.com/safing/portmaster/intel"
)
// EndpointLists matches endpoint lists.
type EndpointLists struct {
EndpointBase
ListSet *intel.ListSet
Lists string
Reason string
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointLists) Matches(entity *intel.Entity) (result EPResult, reason string) {
lists, ok := entity.GetLists()
if !ok {
return Undeterminable, ""
}
matched := ep.ListSet.MatchSet(lists)
if len(matched) > 0 {
return ep.matchesPPP(entity), ep.Reason
}
return NoMatch, ""
}
func (ep *EndpointLists) String() string {
return ep.renderPPP(ep.Lists)
}
func parseTypeList(fields []string) (Endpoint, error) {
if strings.HasPrefix(fields[1], "L:") {
lists := strings.Split(strings.TrimPrefix(fields[1], "L:"), ",")
ep := &EndpointLists{
ListSet: intel.NewListSet(lists),
Lists: "L:" + strings.Join(lists, ","),
Reason: "matched lists " + strings.Join(lists, ","),
}
return ep.parsePPP(ep, fields)
}
return nil, nil
}

View File

@@ -0,0 +1,211 @@
package endpoints
import (
"fmt"
"strconv"
"strings"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network/reference"
)
// Endpoint describes an Endpoint Matcher
type Endpoint interface {
Matches(entity *intel.Entity) (result EPResult, reason string)
String() string
}
// EndpointBase provides general functions for implementing an Endpoint to reduce boilerplate.
type EndpointBase struct { //nolint:maligned // TODO
Protocol uint8
StartPort uint16
EndPort uint16
Permitted bool
}
func (ep *EndpointBase) matchesPPP(entity *intel.Entity) (result EPResult) {
// only check if protocol is defined
if ep.Protocol > 0 {
// if protocol is unknown, return Undeterminable
if entity.Protocol == 0 {
return Undeterminable
}
// if protocol does not match, return NoMatch
if entity.Protocol != ep.Protocol {
return NoMatch
}
}
// only check if port is defined
if ep.StartPort > 0 {
// if port is unknown, return Undeterminable
if entity.Port == 0 {
return Undeterminable
}
// if port does not match, return NoMatch
if entity.Port < ep.StartPort || entity.Port > ep.EndPort {
return NoMatch
}
}
// protocol and port matched or were defined as any
if ep.Permitted {
return Permitted
}
return Denied
}
func (ep *EndpointBase) renderPPP(s string) string {
var rendered string
if ep.Permitted {
rendered = "+ " + s
} else {
rendered = "- " + s
}
if ep.Protocol > 0 || ep.StartPort > 0 {
if ep.Protocol > 0 {
rendered += " " + reference.GetProtocolName(ep.Protocol)
} else {
rendered += " *"
}
if ep.StartPort > 0 {
if ep.StartPort == ep.EndPort {
rendered += "/" + reference.GetPortName(ep.StartPort)
} else {
rendered += "/" + strconv.Itoa(int(ep.StartPort)) + "-" + strconv.Itoa(int(ep.EndPort))
}
}
}
return rendered
}
func (ep *EndpointBase) parsePPP(typedEp Endpoint, fields []string) (Endpoint, error) { //nolint:gocognit // TODO
switch len(fields) {
case 2:
// nothing else to do here
case 3:
// parse protocol and port(s)
var ok bool
splitted := strings.Split(fields[2], "/")
if len(splitted) > 2 {
return nil, invalidDefinitionError(fields, "protocol and port must be in format <protocol>/<port>")
}
// protocol
switch splitted[0] {
case "":
return nil, invalidDefinitionError(fields, "protocol can't be empty")
case "*":
// any protocol that supports ports
default:
n, err := strconv.ParseUint(splitted[0], 10, 8)
n8 := uint8(n)
if err != nil {
// maybe it's a name?
n8, ok = reference.GetProtocolNumber(splitted[0])
if !ok {
return nil, invalidDefinitionError(fields, "protocol number parsing error")
}
}
ep.Protocol = n8
}
// port(s)
if len(splitted) > 1 {
switch splitted[1] {
case "", "*":
return nil, invalidDefinitionError(fields, "omit port if should match any")
default:
portSplitted := strings.Split(splitted[1], "-")
if len(portSplitted) > 2 {
return nil, invalidDefinitionError(fields, "ports must be in format from-to")
}
// parse start port
n, err := strconv.ParseUint(portSplitted[0], 10, 16)
n16 := uint16(n)
if err != nil {
// maybe it's a name?
n16, ok = reference.GetPortNumber(portSplitted[0])
if !ok {
return nil, invalidDefinitionError(fields, "port number parsing error")
}
}
ep.StartPort = n16
// parse end port
if len(portSplitted) > 1 {
n, err = strconv.ParseUint(portSplitted[1], 10, 16)
n16 = uint16(n)
if err != nil {
// maybe it's a name?
n16, ok = reference.GetPortNumber(portSplitted[1])
if !ok {
return nil, invalidDefinitionError(fields, "port number parsing error")
}
}
}
ep.EndPort = n16
}
}
// check if anything was parsed
if ep.Protocol == 0 && ep.StartPort == 0 {
return nil, invalidDefinitionError(fields, "omit protocol/port if should match any")
}
default:
return nil, invalidDefinitionError(fields, "there should be only 2 or 3 segments")
}
switch fields[0] {
case "+":
ep.Permitted = true
case "-":
ep.Permitted = false
default:
return nil, invalidDefinitionError(fields, "invalid permission prefix")
}
return typedEp, nil
}
func invalidDefinitionError(fields []string, msg string) error {
return fmt.Errorf(`invalid endpoint definition: "%s" - %s`, strings.Join(fields, " "), msg)
}
func parseEndpoint(value string) (endpoint Endpoint, err error) {
fields := strings.Fields(value)
if len(fields) < 2 {
return nil, fmt.Errorf(`invalid endpoint definition: "%s"`, value)
}
// any
if endpoint, err = parseTypeAny(fields); endpoint != nil || err != nil {
return
}
// ip
if endpoint, err = parseTypeIP(fields); endpoint != nil || err != nil {
return
}
// ip range
if endpoint, err = parseTypeIPRange(fields); endpoint != nil || err != nil {
return
}
// domain
if endpoint, err = parseTypeDomain(fields); endpoint != nil || err != nil {
return
}
// country
if endpoint, err = parseTypeCountry(fields); endpoint != nil || err != nil {
return
}
// asn
if endpoint, err = parseTypeASN(fields); endpoint != nil || err != nil {
return
}
// lists
if endpoint, err = parseTypeList(fields); endpoint != nil || err != nil {
return
}
return nil, fmt.Errorf(`unknown endpoint definition: "%s"`, value)
}

View File

@@ -0,0 +1,83 @@
package endpoints
import (
"strings"
"testing"
)
func TestEndpointParsing(t *testing.T) {
// any (basics)
testParsing(t, "- *")
testParsing(t, "+ *")
// domain
testDomainParsing(t, "- *bad*", domainMatchTypeContains, "bad")
testDomainParsing(t, "- bad*", domainMatchTypePrefix, "bad")
testDomainParsing(t, "- *bad.com", domainMatchTypeSuffix, "bad.com.")
testDomainParsing(t, "- .bad.com", domainMatchTypeZone, "bad.com.")
testDomainParsing(t, "- bad.com", domainMatchTypeExact, "bad.com.")
testDomainParsing(t, "- www.bad.com.", domainMatchTypeExact, "www.bad.com.")
testDomainParsing(t, "- www.bad.com", domainMatchTypeExact, "www.bad.com.")
// ip
testParsing(t, "+ 127.0.0.1")
testParsing(t, "+ 192.168.0.1")
testParsing(t, "+ ::1")
testParsing(t, "+ 2606:4700:4700::1111")
// ip
testParsing(t, "+ 127.0.0.0/8")
testParsing(t, "+ 192.168.0.0/24")
testParsing(t, "+ 2606:4700:4700::/48")
// country
testParsing(t, "+ DE")
testParsing(t, "+ AT")
testParsing(t, "+ CH")
testParsing(t, "+ AS")
// asn
testParsing(t, "+ AS1")
testParsing(t, "+ AS12")
testParsing(t, "+ AS123")
testParsing(t, "+ AS1234")
testParsing(t, "+ AS12345")
// protocol and ports
testParsing(t, "+ * TCP/1-1024")
testParsing(t, "+ * */DNS")
testParsing(t, "+ * ICMP")
testParsing(t, "+ * 127")
testParsing(t, "+ * UDP/1234")
testParsing(t, "+ * TCP/HTTP")
testParsing(t, "+ * TCP/80-443")
}
func testParsing(t *testing.T, value string) {
ep, err := parseEndpoint(value)
if err != nil {
t.Error(err)
return
}
if value != ep.String() {
t.Errorf(`stringified endpoint mismatch: original was "%s", parsed is "%s"`, value, ep.String())
}
}
func testDomainParsing(t *testing.T, value string, matchType uint8, matchValue string) {
testParsing(t, value)
epGeneric, err := parseTypeDomain(strings.Fields(value))
if err != nil {
t.Error(err)
return
}
ep := epGeneric.(*EndpointDomain)
if ep.MatchType != matchType {
t.Errorf(`error parsing domain endpoint "%s": match type should be %d, was %d`, value, matchType, ep.MatchType)
}
if ep.Domain != matchValue {
t.Errorf(`error parsing domain endpoint "%s": match domain value should be %s, was %s`, value, matchValue, ep.Domain)
}
}

View File

@@ -0,0 +1,93 @@
package endpoints
import (
"fmt"
"strings"
"github.com/safing/portmaster/intel"
)
// Endpoints is a list of permitted or denied endpoints.
type Endpoints []Endpoint
// EPResult represents the result of a check against an EndpointPermission
type EPResult uint8
// Endpoint matching return values
const (
NoMatch EPResult = iota
Undeterminable
Denied
Permitted
)
// ParseEndpoints parses a list of endpoints and returns a list of Endpoints for matching.
func ParseEndpoints(entries []string) (Endpoints, error) {
var firstErr error
var errCnt int
endpoints := make(Endpoints, 0, len(entries))
entriesLoop:
for _, entry := range entries {
ep, err := parseEndpoint(entry)
if err != nil {
errCnt++
if firstErr == nil {
firstErr = err
}
continue entriesLoop
}
endpoints = append(endpoints, ep)
}
if firstErr != nil {
if errCnt > 0 {
return endpoints, fmt.Errorf("encountered %d errors, first was: %s", errCnt, firstErr)
}
return endpoints, firstErr
}
return endpoints, nil
}
// IsSet returns whether the Endpoints object is "set".
func (e Endpoints) IsSet() bool {
return len(e) > 0
}
// Match checks whether the given entity matches any of the endpoint definitions in the list.
func (e Endpoints) Match(entity *intel.Entity) (result EPResult, reason string) {
for _, entry := range e {
if entry != nil {
if result, reason = entry.Matches(entity); result != NoMatch {
return
}
}
}
return NoMatch, ""
}
func (e Endpoints) String() string {
s := make([]string, 0, len(e))
for _, entry := range e {
s = append(s, entry.String())
}
return fmt.Sprintf("[%s]", strings.Join(s, ", "))
}
func (epr EPResult) String() string {
switch epr {
case NoMatch:
return "No Match"
case Undeterminable:
return "Undeterminable"
case Denied:
return "Denied"
case Permitted:
return "Permitted"
default:
return "Unknown"
}
}

View File

@@ -0,0 +1,353 @@
package endpoints
import (
"net"
"runtime"
"testing"
"github.com/safing/portmaster/core/pmtesting"
"github.com/safing/portmaster/intel"
)
func TestMain(m *testing.M) {
pmtesting.TestMain(m)
}
func testEndpointMatch(t *testing.T, ep Endpoint, entity *intel.Entity, expectedResult EPResult) {
result, _ := ep.Matches(entity)
if result != expectedResult {
t.Errorf(
"line %d: unexpected result for endpoint %s and entity %+v: result=%s, expected=%s",
getLineNumberOfCaller(1),
ep,
entity,
result,
expectedResult,
)
}
}
func TestEndpointMatching(t *testing.T) {
// ANY
ep, err := parseEndpoint("+ *")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
// DOMAIN
// wildcard domains
ep, err = parseEndpoint("+ *example.com")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc-example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc-example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
ep, err = parseEndpoint("+ *.example.com")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc-example.com.",
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc-example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), NoMatch)
ep, err = parseEndpoint("+ .example.com")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc-example.com.",
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc-example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), NoMatch)
ep, err = parseEndpoint("+ example.*")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), NoMatch)
ep, err = parseEndpoint("+ *.exampl*")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "abc.example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
ep, err = parseEndpoint("+ *.com.")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.org.",
}).Init(), NoMatch)
// protocol
ep, err = parseEndpoint("+ example.com UDP")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 17,
Port: 443,
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), Undeterminable)
// ports
ep, err = parseEndpoint("+ example.com 17/442-444")
if err != nil {
t.Fatal(err)
}
entity := (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 17,
Port: 441,
}).Init()
testEndpointMatch(t, ep, entity, NoMatch)
entity.Port = 442
testEndpointMatch(t, ep, entity, Permitted)
entity.Port = 443
testEndpointMatch(t, ep, entity, Permitted)
entity.Port = 444
testEndpointMatch(t, ep, entity, Permitted)
entity.Port = 445
testEndpointMatch(t, ep, entity, NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), Undeterminable)
// IP
ep, err = parseEndpoint("+ 10.2.3.4")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "",
IP: net.ParseIP("10.2.3.4"),
Protocol: 6,
Port: 443,
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.4"),
Protocol: 17,
Port: 443,
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "",
IP: net.ParseIP("10.2.3.3"),
Protocol: 6,
Port: 443,
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
IP: net.ParseIP("10.2.3.5"),
Protocol: 17,
Port: 443,
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
Domain: "example.com.",
}).Init(), Undeterminable)
// IP Range
ep, err = parseEndpoint("+ 10.2.3.0/24")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
IP: net.ParseIP("10.2.2.4"),
}).Init(), NoMatch)
testEndpointMatch(t, ep, (&intel.Entity{
IP: net.ParseIP("10.2.3.4"),
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
IP: net.ParseIP("10.2.4.4"),
}).Init(), NoMatch)
// ASN
ep, err = parseEndpoint("+ AS13335")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
IP: net.ParseIP("1.1.1.1"),
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
IP: net.ParseIP("8.8.8.8"),
}).Init(), NoMatch)
// Country
ep, err = parseEndpoint("+ AT")
if err != nil {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
IP: net.ParseIP("194.232.104.1"), // orf.at
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
IP: net.ParseIP("151.101.1.164"), // nytimes.com
}).Init(), NoMatch)
// Lists
// TODO: write test for lists matcher
}
func getLineNumberOfCaller(levels int) int {
_, _, line, _ := runtime.Caller(levels + 1) //nolint:dogsled
return line
}

View File

@@ -1,187 +0,0 @@
package profile
import (
"net"
"testing"
)
func testEndpointDomainMatch(t *testing.T, ep *EndpointPermission, domain string, expectedResult EPResult) {
var result EPResult
result, _ = ep.MatchesDomain(domain)
if result != expectedResult {
t.Errorf(
"line %d: unexpected result for endpoint domain match %s: result=%s, expected=%s",
getLineNumberOfCaller(1),
domain,
result,
expectedResult,
)
}
}
func testEndpointIPMatch(t *testing.T, ep *EndpointPermission, domain string, ip net.IP, protocol uint8, port uint16, expectedResult EPResult) {
var result EPResult
result, _ = ep.MatchesIP(domain, ip, protocol, port, nil)
if result != expectedResult {
t.Errorf(
"line %d: unexpected result for endpoint %s/%s/%d/%d: result=%s, expected=%s",
getLineNumberOfCaller(1),
domain,
ip,
protocol,
port,
result,
expectedResult,
)
}
}
func TestEndpointMatching(t *testing.T) {
ep := &EndpointPermission{
Type: EptAny,
Protocol: 0,
StartPort: 0,
EndPort: 0,
Permit: true,
}
// ANY
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
// DOMAIN
// wildcard domains
ep.Type = EptDomain
ep.Value = "*example.com."
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted)
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
testEndpointDomainMatch(t, ep, "abc-example.com.", Permitted)
testEndpointIPMatch(t, ep, "abc-example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
ep.Value = "*.example.com."
testEndpointDomainMatch(t, ep, "example.com.", NoMatch)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted)
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
testEndpointDomainMatch(t, ep, "abc-example.com.", NoMatch)
testEndpointIPMatch(t, ep, "abc-example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
ep.Value = ".example.com."
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted)
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
testEndpointDomainMatch(t, ep, "abc-example.com.", NoMatch)
testEndpointIPMatch(t, ep, "abc-example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
ep.Value = "example.*"
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
testEndpointDomainMatch(t, ep, "abc.example.com.", NoMatch)
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
ep.Value = ".example.*"
testEndpointDomainMatch(t, ep, "example.com.", NoMatch)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
testEndpointDomainMatch(t, ep, "abc.example.com.", NoMatch)
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
ep.Value = "*.exampl*"
testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted)
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
ep.Value = "*.com."
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
// edge case
ep.Value = ""
testEndpointDomainMatch(t, ep, "example.com", NoMatch)
// edge case
ep.Value = "*"
testEndpointDomainMatch(t, ep, "example.com", Permitted)
// edge case
ep.Value = "**"
testEndpointDomainMatch(t, ep, "example.com", Permitted)
// edge case
ep.Value = "***"
testEndpointDomainMatch(t, ep, "example.com", Permitted)
// protocol
ep.Value = "example.com"
ep.Protocol = 17
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
testEndpointDomainMatch(t, ep, "example.com", Undeterminable)
// ports
ep.StartPort = 442
ep.EndPort = 444
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
ep.StartPort = 442
ep.StartPort = 443
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
ep.StartPort = 443
ep.EndPort = 444
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
ep.StartPort = 443
ep.EndPort = 443
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
testEndpointDomainMatch(t, ep, "example.com", Undeterminable)
// IP
ep.Type = EptIPv4
ep.Value = "10.2.3.4"
ep.Protocol = 0
ep.StartPort = 0
ep.EndPort = 0
testEndpointIPMatch(t, ep, "", net.ParseIP("10.2.3.4"), 6, 80, Permitted)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
testEndpointIPMatch(t, ep, "", net.ParseIP("10.2.3.5"), 6, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.5"), 17, 443, NoMatch)
testEndpointDomainMatch(t, ep, "example.com", Undeterminable)
}
func TestEPString(t *testing.T) {
var endpoints Endpoints = []*EndpointPermission{
{
Type: EptDomain,
Value: "example.com",
Protocol: 6,
Permit: true,
},
{
Type: EptIPv4,
Value: "1.1.1.1",
Protocol: 17, // TCP
StartPort: 53, // DNS
EndPort: 53,
Permit: false,
},
{
Type: EptDomain,
Value: "example.org",
Permit: false,
},
}
if endpoints.String() != "[Domain:example.com 6/*, IPv4:1.1.1.1 17/53, Domain:example.org */*]" {
t.Errorf("unexpected result: %s", endpoints.String())
}
var noEndpoints Endpoints = []*EndpointPermission{}
if noEndpoints.String() != "[]" {
t.Errorf("unexpected result: %s", noEndpoints.String())
}
}

54
profile/find.go Normal file
View File

@@ -0,0 +1,54 @@
package profile
import (
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/log"
)
func FindOrCreateLocalProfileByPath(fullPath string) (profile *Profile, new bool, err error) {
// find local profile
it, err := profileDB.Query(
query.New(makeProfileKey(SourceLocal, "")).Where(
query.Where("LinkedPath", query.SameAs, fullPath),
),
)
if err != nil {
return nil, false, err
}
// get first result
r := <-it.Next
// cancel immediately
it.Cancel()
// return new if none was found
if r == nil {
profile = New()
profile.LinkedPath = fullPath
return profile, true, nil
}
// ensure its a profile
profile, err = EnsureProfile(r)
if err != nil {
return nil, false, err
}
// prepare config
err = profile.prepConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// parse config
err = profile.parseConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// mark active
markProfileActive(profile)
// return parsed profile
return profile, false, nil
}

View File

@@ -1,7 +1,5 @@
package profile
import "time"
var (
fingerprintWeights = map[string]int{
"full_path": 2,
@@ -35,6 +33,8 @@ func GetFingerprintWeight(fpType string) (weight int) {
return 0
}
// TODO: move to profile
/*
// AddFingerprint adds the given fingerprint to the profile.
func (profile *Profile) AddFingerprint(fp *Fingerprint) {
if fp.OS == "" {
@@ -46,3 +46,4 @@ func (profile *Profile) AddFingerprint(fp *Fingerprint) {
profile.Fingerprints = append(profile.Fingerprints, fp)
}
*/

View File

@@ -1,130 +0,0 @@
package profile
import (
"errors"
"fmt"
"strings"
"github.com/safing/portmaster/status"
)
// Flags are used to quickly add common attributes to profiles
type Flags map[uint8]uint8
// Profile Flags
const (
// Profile Modes
Prompt uint8 = 0 // Prompt first-seen connections
Blacklist uint8 = 1 // Allow everything not explicitly denied
Whitelist uint8 = 2 // Only allow everything explicitly allowed
// Network Locations
Internet uint8 = 16 // Allow connections to the Internet
LAN uint8 = 17 // Allow connections to the local area network
Localhost uint8 = 18 // Allow connections on the local host
// Specials
Related uint8 = 32 // If and before prompting, allow domains that are related to the program
PeerToPeer uint8 = 33 // Allow program to directly communicate with peers, without resolving DNS first
Service uint8 = 34 // Allow program to accept incoming connections
Independent uint8 = 35 // Ignore profile settings coming from the Community
RequireGate17 uint8 = 36 // Require all connections to go over Gate17
)
var (
// ErrFlagsParseFailed is returned if a an invalid flag is encountered while parsing
ErrFlagsParseFailed = errors.New("profiles: failed to parse flags")
sortedFlags = []uint8{
Prompt,
Blacklist,
Whitelist,
Internet,
LAN,
Localhost,
Related,
PeerToPeer,
Service,
Independent,
RequireGate17,
}
flagIDs = map[string]uint8{
"Prompt": Prompt,
"Blacklist": Blacklist,
"Whitelist": Whitelist,
"Internet": Internet,
"LAN": LAN,
"Localhost": Localhost,
"Related": Related,
"PeerToPeer": PeerToPeer,
"Service": Service,
"Independent": Independent,
"RequireGate17": RequireGate17,
}
flagNames = map[uint8]string{
Prompt: "Prompt",
Blacklist: "Blacklist",
Whitelist: "Whitelist",
Internet: "Internet",
LAN: "LAN",
Localhost: "Localhost",
Related: "Related",
PeerToPeer: "PeerToPeer",
Service: "Service",
Independent: "Independent",
RequireGate17: "RequireGate17",
}
)
// Check checks if a flag is set at all and if it's active in the given security level.
func (flags Flags) Check(flag, level uint8) (active bool, ok bool) {
if flags == nil {
return false, false
}
setting, ok := flags[flag]
if ok {
if setting&level > 0 {
return true, true
}
return false, true
}
return false, false
}
func getLevelMarker(levels, level uint8) string {
if levels&level > 0 {
return "+"
}
return "-"
}
// String return a string representation of Flags
func (flags Flags) String() string {
var markedFlags []string
for _, flag := range sortedFlags {
levels, ok := flags[flag]
if ok {
s := flagNames[flag]
if levels != status.SecurityLevelsAll {
s += getLevelMarker(levels, status.SecurityLevelDynamic)
s += getLevelMarker(levels, status.SecurityLevelSecure)
s += getLevelMarker(levels, status.SecurityLevelFortress)
}
markedFlags = append(markedFlags, s)
}
}
return fmt.Sprintf("[%s]", strings.Join(markedFlags, ", "))
}
// Add adds a flag to the Flags with the given level.
func (flags Flags) Add(flag, levels uint8) {
flags[flag] = levels
}
// Remove removes a flag from the Flags.
func (flags Flags) Remove(flag uint8) {
delete(flags, flag)
}

View File

@@ -1,69 +0,0 @@
package profile
import (
"testing"
"github.com/safing/portmaster/status"
)
func TestProfileFlags(t *testing.T) {
// check if all IDs have a name
for key, entry := range flagIDs {
if _, ok := flagNames[entry]; !ok {
t.Errorf("could not find entry for %s in flagNames", key)
}
}
// check if all names have an ID
for key, entry := range flagNames {
if _, ok := flagIDs[entry]; !ok {
t.Errorf("could not find entry for %d in flagNames", key)
}
}
testFlags := Flags{
Prompt: status.SecurityLevelsAll,
Internet: status.SecurityLevelsDynamicAndSecure,
LAN: status.SecurityLevelsDynamicAndSecure,
Localhost: status.SecurityLevelsAll,
Related: status.SecurityLevelDynamic,
RequireGate17: status.SecurityLevelsSecureAndFortress,
}
if testFlags.String() != "[Prompt, Internet++-, LAN++-, Localhost, Related+--, RequireGate17-++]" {
t.Errorf("unexpected output: %s", testFlags.String())
}
// // check Has
// emptyFlags := ProfileFlags{}
// for flag, name := range flagNames {
// if !sortedFlags.Has(flag) {
// t.Errorf("sortedFlags should have flag %s (%d)", name, flag)
// }
// if emptyFlags.Has(flag) {
// t.Errorf("emptyFlags should not have flag %s (%d)", name, flag)
// }
// }
//
// // check ProfileFlags creation from strings
// var allFlagStrings []string
// for _, flag := range *sortedFlags {
// allFlagStrings = append(allFlagStrings, flagNames[flag])
// }
// newFlags, err := FlagsFromNames(allFlagStrings)
// if err != nil {
// t.Errorf("error while parsing flags: %s", err)
// }
// if newFlags.String() != sortedFlags.String() {
// t.Errorf("parsed flags are not correct (or tests have not been updated to reflect the right number), expected %v, got %v", *sortedFlags, *newFlags)
// }
//
// // check ProfileFlags Stringer
// flagString := newFlags.String()
// check := strings.Join(allFlagStrings, ",")
// if flagString != check {
// t.Errorf("flag string is not correct, expected %s, got %s", check, flagString)
// }
}

View File

@@ -1,6 +1,8 @@
package profile
import (
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
// module dependencies
@@ -8,22 +10,42 @@ import (
)
var (
shutdownSignal = make(chan struct{})
module *modules.Module
)
func init() {
modules.Register("profile", nil, start, stop, "core")
module = modules.Register("profiles", prep, start, nil, "core")
}
func start() error {
err := initSpecialProfiles()
func prep() error {
err := registerConfiguration()
if err != nil {
return err
}
err = registerConfigUpdater()
if err != nil {
return err
}
return initUpdateListener()
}
func stop() error {
close(shutdownSignal)
return nil
}
func start() error {
err := registerValidationDBHook()
if err != nil {
return err
}
err = startProfileUpdateChecker()
if err != nil {
return err
}
err = updateGlobalConfigProfile(module.Ctx, nil)
if err != nil {
log.Warningf("profile: error during loading global profile from configuration: %s", err)
}
return nil
}

306
profile/profile-layered.go Normal file
View File

@@ -0,0 +1,306 @@
package profile
import (
"sync"
"sync/atomic"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/status"
"github.com/tevino/abool"
"github.com/safing/portbase/config"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/profile/endpoints"
)
var (
no = abool.NewBool(false)
)
// LayeredProfile combines multiple Profiles.
type LayeredProfile struct {
lock sync.Mutex
localProfile *Profile
layers []*Profile
revisionCounter uint64
validityFlag *abool.AtomicBool
validityFlagLock sync.Mutex
globalValidityFlag *config.ValidityFlag
securityLevel *uint32
DisableAutoPermit config.BoolOption
BlockScopeLocal config.BoolOption
BlockScopeLAN config.BoolOption
BlockScopeInternet config.BoolOption
BlockP2P config.BoolOption
BlockInbound config.BoolOption
EnforceSPN config.BoolOption
RemoveOutOfScopeDNS config.BoolOption
RemoveBlockedDNS config.BoolOption
}
// NewLayeredProfile returns a new layered profile based on the given local profile.
func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
var securityLevelVal uint32
new := &LayeredProfile{
localProfile: localProfile,
layers: make([]*Profile, 0, len(localProfile.LinkedProfiles)+1),
revisionCounter: 0,
validityFlag: abool.NewBool(true),
globalValidityFlag: config.NewValidityFlag(),
securityLevel: &securityLevelVal,
}
new.DisableAutoPermit = new.wrapSecurityLevelOption(
CfgOptionDisableAutoPermitKey,
cfgOptionDisableAutoPermit,
)
new.BlockScopeLocal = new.wrapSecurityLevelOption(
CfgOptionBlockScopeLocalKey,
cfgOptionBlockScopeLocal,
)
new.BlockScopeLAN = new.wrapSecurityLevelOption(
CfgOptionBlockScopeLANKey,
cfgOptionBlockScopeLAN,
)
new.BlockScopeInternet = new.wrapSecurityLevelOption(
CfgOptionBlockScopeInternetKey,
cfgOptionBlockScopeInternet,
)
new.BlockP2P = new.wrapSecurityLevelOption(
CfgOptionBlockP2PKey,
cfgOptionBlockP2P,
)
new.BlockInbound = new.wrapSecurityLevelOption(
CfgOptionBlockInboundKey,
cfgOptionBlockInbound,
)
new.EnforceSPN = new.wrapSecurityLevelOption(
CfgOptionEnforceSPNKey,
cfgOptionEnforceSPN,
)
new.RemoveOutOfScopeDNS = new.wrapSecurityLevelOption(
CfgOptionRemoveOutOfScopeDNSKey,
cfgOptionRemoveOutOfScopeDNS,
)
new.RemoveBlockedDNS = new.wrapSecurityLevelOption(
CfgOptionRemoveBlockedDNSKey,
cfgOptionRemoveBlockedDNS,
)
// TODO: load referenced profiles
// FUTURE: load forced company profile
new.layers = append(new.layers, localProfile)
// FUTURE: load company profile
// FUTURE: load community profile
new.updateCaches()
return new
}
func (lp *LayeredProfile) getValidityFlag() *abool.AtomicBool {
lp.validityFlagLock.Lock()
defer lp.validityFlagLock.Unlock()
return lp.validityFlag
}
// Update checks for updated profiles and replaces any outdated profiles.
func (lp *LayeredProfile) Update() (revisionCounter uint64) {
lp.lock.Lock()
defer lp.lock.Unlock()
var changed bool
for i, layer := range lp.layers {
if layer.oudated.IsSet() {
changed = true
// update layer
newLayer, err := GetProfile(layer.Source, layer.ID)
if err != nil {
log.Errorf("profiles: failed to update profile %s", layer.ScopedID())
} else {
lp.layers[i] = newLayer
}
}
}
if !lp.globalValidityFlag.IsValid() {
changed = true
}
if changed {
// reset validity flag
lp.validityFlagLock.Lock()
lp.validityFlag.SetTo(false)
lp.validityFlag = abool.NewBool(true)
lp.validityFlagLock.Unlock()
// get global config validity flag
lp.globalValidityFlag.Refresh()
// update cached data fields
lp.updateCaches()
// bump revision counter
lp.revisionCounter++
}
return lp.revisionCounter
}
func (lp *LayeredProfile) updateCaches() {
// update security level
var newLevel uint8 = 0
for _, layer := range lp.layers {
if newLevel < layer.SecurityLevel {
newLevel = layer.SecurityLevel
}
}
atomic.StoreUint32(lp.securityLevel, uint32(newLevel))
// TODO: ignore community profiles
}
// SecurityLevel returns the highest security level of all layered profiles.
func (lp *LayeredProfile) SecurityLevel() uint8 {
return uint8(atomic.LoadUint32(lp.securityLevel))
}
// DefaultAction returns the active default action ID.
func (lp *LayeredProfile) DefaultAction() uint8 {
for _, layer := range lp.layers {
if layer.defaultAction > 0 {
log.Tracef("profile: default action by layer = %d", layer.defaultAction)
return layer.defaultAction
}
}
cfgLock.RLock()
defer cfgLock.RUnlock()
log.Tracef("profile: default action from global = %d", cfgDefaultAction)
return cfgDefaultAction
}
// MatchEndpoint checks if the given endpoint matches an entry in any of the profiles.
func (lp *LayeredProfile) MatchEndpoint(entity *intel.Entity) (result endpoints.EPResult, reason string) {
for _, layer := range lp.layers {
if layer.endpoints.IsSet() {
result, reason = layer.endpoints.Match(entity)
if result != endpoints.NoMatch {
return
}
}
}
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfgEndpoints.Match(entity)
}
// MatchServiceEndpoint checks if the given endpoint of an inbound connection matches an entry in any of the profiles.
func (lp *LayeredProfile) MatchServiceEndpoint(entity *intel.Entity) (result endpoints.EPResult, reason string) {
entity.EnableReverseResolving()
for _, layer := range lp.layers {
if layer.serviceEndpoints.IsSet() {
result, reason = layer.serviceEndpoints.Match(entity)
if result != endpoints.NoMatch {
return
}
}
}
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfgServiceEndpoints.Match(entity)
}
// AddEndpoint adds an endpoint to the local endpoint list, saves the local profile and reloads the configuration.
func (lp *LayeredProfile) AddEndpoint(newEntry string) {
lp.localProfile.AddEndpoint(newEntry)
}
// AddServiceEndpoint adds a service endpoint to the local endpoint list, saves the local profile and reloads the configuration.
func (lp *LayeredProfile) AddServiceEndpoint(newEntry string) {
lp.localProfile.AddServiceEndpoint(newEntry)
}
func (lp *LayeredProfile) wrapSecurityLevelOption(configKey string, globalConfig config.IntOption) config.BoolOption {
activeAtLevels := lp.wrapIntOption(configKey, globalConfig)
return func() bool {
return uint8(activeAtLevels())&max(
lp.SecurityLevel(), // layered profile security level
status.ActiveSecurityLevel(), // global security level
) > 0
}
}
func (lp *LayeredProfile) wrapIntOption(configKey string, globalConfig config.IntOption) config.IntOption {
valid := no
var value int64
return func() int64 {
if !valid.IsSet() {
valid = lp.getValidityFlag()
found := false
layerLoop:
for _, layer := range lp.layers {
layerValue, ok := layer.configPerspective.GetAsInt(configKey)
if ok {
found = true
value = layerValue
break layerLoop
}
}
if !found {
value = globalConfig()
}
}
return value
}
}
/*
For later:
func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config.StringOption) config.StringOption {
valid := no
var value string
return func() string {
if !valid.IsSet() {
valid = lp.getValidityFlag()
found := false
layerLoop:
for _, layer := range lp.layers {
layerValue, ok := layer.configPerspective.GetAsString(configKey)
if ok {
found = true
value = layerValue
break layerLoop
}
}
if !found {
value = globalConfig()
}
}
return value
}
}
*/
func max(a, b uint8) uint8 {
if a > b {
return a
}
return b
}

View File

@@ -1,78 +1,180 @@
package profile
import (
"errors"
"fmt"
"sync"
"time"
"github.com/safing/portbase/log"
"github.com/tevino/abool"
uuid "github.com/satori/go.uuid"
"github.com/safing/portbase/config"
"github.com/safing/portbase/database/record"
"github.com/safing/portmaster/status"
"github.com/safing/portmaster/profile/endpoints"
)
var (
lastUsedUpdateThreshold = 1 * time.Hour
)
// Profile Sources
const (
SourceLocal string = "local"
SourceCommunity string = "community"
SourceEnterprise string = "enterprise"
SourceGlobal string = "global"
)
// Default Action IDs
const (
DefaultActionNotSet uint8 = 0
DefaultActionBlock uint8 = 1
DefaultActionAsk uint8 = 2
DefaultActionPermit uint8 = 3
)
// Profile is used to predefine a security profile for applications.
type Profile struct {
type Profile struct { //nolint:maligned // not worth the effort
record.Base
sync.Mutex
// Profile Metadata
ID string
// Identity
ID string
Source string
// App Information
Name string
Description string
Homepage string
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for a database path or "e:" for the encoded data.
Icon string
// User Profile Only
LinkedPath string
StampProfileID string
StampProfileAssigned int64
// References - local profiles only
// LinkedPath is a filesystem path to the executable this profile was created for.
LinkedPath string
// LinkedProfiles is a list of other profiles
LinkedProfiles []string
// Fingerprints
Fingerprints []*Fingerprint
// TODO: Fingerprints []*Fingerprint
// Configuration
// The mininum security level to apply to connections made with this profile
SecurityLevel uint8
Flags Flags
Endpoints Endpoints
ServiceEndpoints Endpoints
SecurityLevel uint8
Config map[string]interface{}
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process must be found
// Framework *Framework `json:",omitempty bson:",omitempty"`
// Interpreted Data
configPerspective *config.Perspective
dataParsed bool
defaultAction uint8
endpoints endpoints.Endpoints
serviceEndpoints endpoints.Endpoints
// When this Profile was approximately last used (for performance reasons not every single usage is saved)
Created int64
// Lifecycle Management
oudated *abool.AtomicBool
// Framework
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process/actor must be found
// TODO: Framework *Framework
// When this Profile was approximately last used.
// For performance reasons not every single usage is saved.
ApproxLastUsed int64
Created int64
internalSave bool
}
func (profile *Profile) prepConfig() (err error) {
// prepare configuration
profile.configPerspective, err = config.NewPerspective(profile.Config)
profile.oudated = abool.New()
return
}
func (profile *Profile) parseConfig() error {
if profile.configPerspective == nil {
return errors.New("config not prepared")
}
// check if already parsed
if profile.dataParsed {
return nil
}
profile.dataParsed = true
var err error
var lastErr error
action, ok := profile.configPerspective.GetAsString(CfgOptionBlockInboundKey)
if ok {
switch action {
case "permit":
profile.defaultAction = DefaultActionPermit
case "ask":
profile.defaultAction = DefaultActionAsk
case "block":
profile.defaultAction = DefaultActionBlock
default:
lastErr = fmt.Errorf(`default action "%s" invalid`, action)
}
}
list, ok := profile.configPerspective.GetAsStringArray(CfgOptionEndpointsKey)
if ok {
profile.endpoints, err = endpoints.ParseEndpoints(list)
if err != nil {
lastErr = err
}
}
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionServiceEndpointsKey)
if ok {
profile.serviceEndpoints, err = endpoints.ParseEndpoints(list)
if err != nil {
lastErr = err
}
}
return lastErr
}
// New returns a new Profile.
func New() *Profile {
return &Profile{
profile := &Profile{
ID: uuid.NewV4().String(),
Source: SourceLocal,
Created: time.Now().Unix(),
Config: make(map[string]interface{}),
}
// create placeholders
_ = profile.prepConfig()
_ = profile.parseConfig()
return profile
}
// MakeProfileKey creates the correct key for a profile with the given namespace and ID.
func MakeProfileKey(namespace, id string) string {
return fmt.Sprintf("core:profiles/%s/%s", namespace, id)
// ScopedID returns the scoped ID (Source + ID) of the profile.
func (profile *Profile) ScopedID() string {
return makeScopedID(profile.Source, profile.ID)
}
// Save saves the profile to the database
func (profile *Profile) Save(namespace string) error {
func (profile *Profile) Save() error {
if profile.ID == "" {
profile.ID = uuid.NewV4().String()
return errors.New("profile: tried to save profile without ID")
}
if profile.Source == "" {
return fmt.Errorf("profile: profile %s does not specify a source", profile.ID)
}
if !profile.KeyIsSet() {
if namespace == "" {
return fmt.Errorf("no key or namespace defined for profile %s", profile.String())
}
profile.SetKey(MakeProfileKey(namespace, profile.ID))
profile.SetKey(makeProfileKey(profile.Source, profile.ID))
}
return profileDB.Put(profile)
@@ -92,27 +194,86 @@ func (profile *Profile) String() string {
return profile.Name
}
// DetailedString returns a more detailed string representation of theProfile.
func (profile *Profile) DetailedString() string {
return fmt.Sprintf("%s(SL=%s Flags=%s Endpoints=%s)", profile.Name, status.FmtSecurityLevel(profile.SecurityLevel), profile.Flags.String(), profile.Endpoints.String())
// AddEndpoint adds an endpoint to the endpoint list, saves the profile and reloads the configuration.
func (profile *Profile) AddEndpoint(newEntry string) {
profile.addEndpointyEntry(CfgOptionEndpointsKey, newEntry)
}
// GetUserProfile loads a profile from the database.
func GetUserProfile(id string) (*Profile, error) {
return getProfile(UserNamespace, id)
// AddServiceEndpoint adds a service endpoint to the endpoint list, saves the profile and reloads the configuration.
func (profile *Profile) AddServiceEndpoint(newEntry string) {
profile.addEndpointyEntry(CfgOptionServiceEndpointsKey, newEntry)
}
// GetStampProfile loads a profile from the database.
func GetStampProfile(id string) (*Profile, error) {
return getProfile(StampNamespace, id)
func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) {
profile.Lock()
// get, update, save endpoints list
endpointList, ok := profile.configPerspective.GetAsStringArray(cfgKey)
if !ok {
endpointList = make([]string, 0, 1)
}
endpointList = append(endpointList, newEntry)
profile.Config[cfgKey] = endpointList
// save without full reload
profile.internalSave = true
profile.Unlock()
err := profile.Save()
if err != nil {
log.Warningf("profile: failed to save profile after adding endpoint: %s", err)
}
// reload manually
err = profile.parseConfig()
if err != nil {
log.Warningf("profile: failed to parse profile config after adding endpoint: %s", err)
}
}
func getProfile(namespace, id string) (*Profile, error) {
r, err := profileDB.Get(MakeProfileKey(namespace, id))
// GetProfile loads a profile from the database.
func GetProfile(source, id string) (*Profile, error) {
return GetProfileByScopedID(makeScopedID(source, id))
}
// GetProfileByScopedID loads a profile from the database using a scoped ID like "local/id" or "community/id".
func GetProfileByScopedID(scopedID string) (*Profile, error) {
// check cache
profile := getActiveProfile(scopedID)
if profile != nil {
return profile, nil
}
// get from database
r, err := profileDB.Get(profilesDBPath + scopedID)
if err != nil {
return nil, err
}
return EnsureProfile(r)
// convert
profile, err = EnsureProfile(r)
if err != nil {
return nil, err
}
// lock for prepping
profile.Lock()
defer profile.Unlock()
// prepare config
err = profile.prepConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// parse config
err = profile.parseConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// mark active
markProfileActive(profile)
return profile, nil
}
// EnsureProfile ensures that the given record is a *Profile, and returns it.

View File

@@ -1,188 +0,0 @@
package profile
import (
"context"
"net"
"sync"
"github.com/safing/portmaster/status"
)
// Set handles Profile chaining.
type Set struct {
sync.Mutex
id string
profiles [4]*Profile
// Application
// Global
// Stamp
// Default
combinedSecurityLevel uint8
independent bool
}
// NewSet returns a new profile set with given the profiles.
func NewSet(ctx context.Context, id string, user, stamp *Profile) *Set {
new := &Set{
id: id,
profiles: [4]*Profile{
user, // Application
nil, // Global
stamp, // Stamp
nil, // Default
},
}
activateProfileSet(ctx, new)
new.Update(status.SecurityLevelFortress)
return new
}
// UserProfile returns the user profile.
func (set *Set) UserProfile() *Profile {
return set.profiles[0]
}
// Update gets the new global and default profile and updates the independence status. It must be called when reusing a profile set for a series of calls.
func (set *Set) Update(securityLevel uint8) {
set.Lock()
specialProfileLock.RLock()
defer specialProfileLock.RUnlock()
// update profiles
set.profiles[1] = globalProfile
set.profiles[3] = fallbackProfile
// update security level
profileSecurityLevel := set.getSecurityLevel()
if profileSecurityLevel > securityLevel {
set.combinedSecurityLevel = profileSecurityLevel
} else {
set.combinedSecurityLevel = securityLevel
}
set.Unlock()
// update independence
if set.CheckFlag(Independent) {
set.Lock()
set.independent = true
set.Unlock()
} else {
set.Lock()
set.independent = false
set.Unlock()
}
}
// SecurityLevel returns the applicable security level for the profile set.
func (set *Set) SecurityLevel() uint8 {
set.Lock()
defer set.Unlock()
return set.combinedSecurityLevel
}
// GetProfileMode returns the active profile mode.
func (set *Set) GetProfileMode() uint8 {
switch {
case set.CheckFlag(Whitelist):
return Whitelist
case set.CheckFlag(Prompt):
return Prompt
case set.CheckFlag(Blacklist):
return Blacklist
default:
return Whitelist
}
}
// CheckFlag returns whether a given flag is set.
func (set *Set) CheckFlag(flag uint8) (active bool) {
set.Lock()
defer set.Unlock()
for i, profile := range set.profiles {
if i == 2 && set.independent {
continue
}
if profile != nil {
active, ok := profile.Flags.Check(flag, set.combinedSecurityLevel)
if ok {
return active
}
}
}
return false
}
// CheckEndpointDomain checks if the given endpoint matches an entry in the corresponding list. This is for outbound communication only.
func (set *Set) CheckEndpointDomain(domain string) (result EPResult, reason string) {
set.Lock()
defer set.Unlock()
for i, profile := range set.profiles {
if i == 2 && set.independent {
continue
}
if profile != nil {
if result, reason = profile.Endpoints.CheckDomain(domain); result != NoMatch {
return
}
}
}
return NoMatch, ""
}
// CheckEndpointIP checks if the given endpoint matches an entry in the corresponding list.
func (set *Set) CheckEndpointIP(domain string, ip net.IP, protocol uint8, port uint16, inbound bool) (result EPResult, reason string) {
set.Lock()
defer set.Unlock()
for i, profile := range set.profiles {
if i == 2 && set.independent {
continue
}
if profile != nil {
if inbound {
if result, reason = profile.ServiceEndpoints.CheckIP(domain, ip, protocol, port, inbound, set.combinedSecurityLevel); result != NoMatch {
return
}
} else {
if result, reason = profile.Endpoints.CheckIP(domain, ip, protocol, port, inbound, set.combinedSecurityLevel); result != NoMatch {
return
}
}
}
}
return NoMatch, ""
}
// getSecurityLevel returns the highest prioritized security level.
func (set *Set) getSecurityLevel() uint8 {
if set == nil {
return 0
}
for i, profile := range set.profiles {
if i == 2 {
// Stamp profiles do not have the SecurityLevel setting
continue
}
if profile != nil {
if profile.SecurityLevel > 0 {
return profile.SecurityLevel
}
}
}
return 0
}

View File

@@ -1,179 +0,0 @@
//nolint:unparam
package profile
import (
"context"
"net"
"runtime"
"testing"
"time"
"github.com/safing/portmaster/status"
)
var (
testUserProfile *Profile
testStampProfile *Profile
)
func init() {
specialProfileLock.Lock()
defer specialProfileLock.Unlock()
globalProfile = makeDefaultGlobalProfile()
fallbackProfile = makeDefaultFallbackProfile()
testUserProfile = &Profile{
ID: "unit-test-user",
Name: "Unit Test User Profile",
SecurityLevel: status.SecurityLevelDynamic,
Flags: map[uint8]uint8{
Independent: status.SecurityLevelFortress,
},
Endpoints: []*EndpointPermission{
{
Type: EptDomain,
Value: "good.bad.example.com.",
Permit: true,
Created: time.Now().Unix(),
},
{
Type: EptDomain,
Value: "*bad.example.com.",
Permit: false,
Created: time.Now().Unix(),
},
{
Type: EptDomain,
Value: "example.com.",
Permit: true,
Created: time.Now().Unix(),
},
{
Type: EptAny,
Permit: true,
Protocol: 6,
StartPort: 22000,
EndPort: 22000,
Created: time.Now().Unix(),
},
},
}
testStampProfile = &Profile{
ID: "unit-test-stamp",
Name: "Unit Test Stamp Profile",
SecurityLevel: status.SecurityLevelFortress,
// Flags: map[uint8]uint8{
// Internet: status.SecurityLevelsAll,
// },
Endpoints: []*EndpointPermission{
{
Type: EptDomain,
Value: "*bad2.example.com.",
Permit: false,
Created: time.Now().Unix(),
},
{
Type: EptAny,
Permit: true,
Protocol: 6,
StartPort: 80,
EndPort: 80,
Created: time.Now().Unix(),
},
},
ServiceEndpoints: []*EndpointPermission{
{
Type: EptAny,
Permit: true,
Protocol: 17,
StartPort: 12345,
EndPort: 12347,
Created: time.Now().Unix(),
},
{ // default deny
Type: EptAny,
Permit: false,
Created: time.Now().Unix(),
},
},
}
}
func testFlag(t *testing.T, set *Set, flag uint8, shouldBeActive bool) {
active := set.CheckFlag(flag)
if active != shouldBeActive {
t.Errorf("unexpected result: flag %s: active=%v, expected=%v", flagNames[flag], active, shouldBeActive)
}
}
func testEndpointDomain(t *testing.T, set *Set, domain string, expectedResult EPResult) {
var result EPResult
result, _ = set.CheckEndpointDomain(domain)
if result != expectedResult {
t.Errorf(
"line %d: unexpected result for endpoint domain %s: result=%s, expected=%s",
getLineNumberOfCaller(1),
domain,
result,
expectedResult,
)
}
}
func testEndpointIP(t *testing.T, set *Set, domain string, ip net.IP, protocol uint8, port uint16, inbound bool, expectedResult EPResult) {
var result EPResult
result, _ = set.CheckEndpointIP(domain, ip, protocol, port, inbound)
if result != expectedResult {
t.Errorf(
"line %d: unexpected result for endpoint %s/%s/%d/%d/%v: result=%s, expected=%s",
getLineNumberOfCaller(1),
domain,
ip,
protocol,
port,
inbound,
result,
expectedResult,
)
}
}
func TestProfileSet(t *testing.T) {
set := NewSet(context.Background(), "[pid]-/path/to/bin", testUserProfile, testStampProfile)
set.Update(status.SecurityLevelDynamic)
testFlag(t, set, Whitelist, false)
// testFlag(t, set, Internet, true)
testEndpointDomain(t, set, "example.com.", Permitted)
testEndpointDomain(t, set, "bad.example.com.", Denied)
testEndpointDomain(t, set, "other.bad.example.com.", Denied)
testEndpointDomain(t, set, "good.bad.example.com.", Permitted)
testEndpointDomain(t, set, "bad2.example.com.", Undeterminable)
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 22000, false, Permitted)
testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 6, 22000, false, Permitted)
testEndpointDomain(t, set, "test.local.", Undeterminable)
testEndpointDomain(t, set, "other.example.com.", Undeterminable)
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 53, false, NoMatch)
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 443, false, NoMatch)
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 12346, false, NoMatch)
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 12345, true, Permitted)
testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 17, 12347, true, Permitted)
set.Update(status.SecurityLevelSecure)
// testFlag(t, set, Internet, true)
set.Update(status.SecurityLevelFortress) // Independent!
testFlag(t, set, Whitelist, true)
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 12345, true, Denied)
testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 17, 12347, true, Denied)
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 80, false, NoMatch)
testEndpointDomain(t, set, "bad2.example.com.", Undeterminable)
}
func getLineNumberOfCaller(levels int) int {
_, _, line, _ := runtime.Caller(levels + 1) //nolint:dogsled
return line
}

View File

@@ -1,69 +0,0 @@
package profile
import (
"sync"
"github.com/safing/portbase/database"
)
var (
globalProfile *Profile
fallbackProfile *Profile
specialProfileLock sync.RWMutex
)
func initSpecialProfiles() (err error) {
specialProfileLock.Lock()
defer specialProfileLock.Unlock()
globalProfile, err = getSpecialProfile("global")
if err != nil {
if err != database.ErrNotFound {
return err
}
globalProfile = makeDefaultGlobalProfile()
_ = globalProfile.Save(SpecialNamespace)
}
fallbackProfile, err = getSpecialProfile("fallback")
if err != nil {
if err != database.ErrNotFound {
return err
}
fallbackProfile = makeDefaultFallbackProfile()
ensureServiceEndpointsDenyAll(fallbackProfile)
_ = fallbackProfile.Save(SpecialNamespace)
}
ensureServiceEndpointsDenyAll(fallbackProfile)
return nil
}
func getSpecialProfile(id string) (*Profile, error) {
return getProfile(SpecialNamespace, id)
}
func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) {
for _, ep := range p.ServiceEndpoints {
if ep != nil {
if ep.Type == EptAny &&
ep.Protocol == 0 &&
ep.StartPort == 0 &&
ep.EndPort == 0 &&
!ep.Permit {
return false
}
}
}
p.ServiceEndpoints = append(p.ServiceEndpoints, &EndpointPermission{
Type: EptAny,
Protocol: 0,
StartPort: 0,
EndPort: 0,
Permit: false,
})
return true
}

View File

@@ -1,89 +0,0 @@
package profile
import (
"strings"
"sync/atomic"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/log"
)
func initUpdateListener() error {
sub, err := profileDB.Subscribe(query.New("core:profiles/"))
if err != nil {
return err
}
go updateListener(sub)
return nil
}
func updateListener(sub *database.Subscription) {
for {
select {
case <-shutdownSignal:
return
case r := <-sub.Feed:
if r.Meta().IsDeleted() {
continue
}
profile, err := EnsureProfile(r)
if err != nil {
log.Errorf("profile: received update for profile, but could not read: %s", err)
continue
}
log.Infof("profile: updated %s", profile.ID)
switch profile.DatabaseKey() {
case "profiles/special/global":
specialProfileLock.Lock()
globalProfile = profile
specialProfileLock.Unlock()
case "profiles/special/fallback":
profile.Lock()
profileChanged := ensureServiceEndpointsDenyAll(profile)
profile.Unlock()
if profileChanged {
_ = profile.Save(SpecialNamespace)
continue
}
specialProfileLock.Lock()
fallbackProfile = profile
specialProfileLock.Unlock()
default:
switch {
case strings.HasPrefix(profile.Key(), MakeProfileKey(UserNamespace, "")):
updateActiveProfile(profile, true /* User Profile */)
case strings.HasPrefix(profile.Key(), MakeProfileKey(StampNamespace, "")):
updateActiveProfile(profile, false /* Stamp Profile */)
}
}
}
}
}
var (
updateVersion uint32
)
// GetUpdateVersion returns the current profiles internal update version
func GetUpdateVersion() uint32 {
return atomic.LoadUint32(&updateVersion)
}
func increaseUpdateVersion() {
// we intentially want to wrap
atomic.AddUint32(&updateVersion, 1)
}

View File

@@ -1,4 +1,4 @@
package intel
package resolver
import (
"crypto/tls"

203
resolver/config.go Normal file
View File

@@ -0,0 +1,203 @@
package resolver
import (
"fmt"
"strings"
"github.com/safing/portbase/config"
"github.com/safing/portmaster/status"
)
var (
defaultNameServers = []string{
// Collection of default DNS Servers
// Default servers should be:
// Anycast:
// - Servers should be reachable from anywhere with reasonable latency.
// - Servers should be near to the user for geo-content to work correctly.
// Private:
// - Servers should not do any or only minimal logging.
// - Available logging data may not be used against the user, ie. unethically.
// Sadly, only a few services come close to fulfilling these requirements.
// For now, we have settled for two bigger and well known services: Cloudflare and Quad9.
// TODO: monitor situation and re-evaluate when new services become available
// TODO: explore other methods of making queries more private
// We encourage everyone who has the technical abilities to set their own preferred servers.
// Default 1: Cloudflare
"dot|1.1.1.1:853|cloudflare-dns.com", // Cloudflare
"dot|1.0.0.1:853|cloudflare-dns.com", // Cloudflare
// Default 2: Quad9
"dot|9.9.9.9:853|dns.quad9.net", // Quad9
"dot|149.112.112.112:853|dns.quad9.net", // Quad9
// Fallback 1: Cloudflare
"dns|1.1.1.1:53", // Cloudflare
"dns|1.0.0.1:53", // Cloudflare
// Fallback 2: Quad9
"dns|9.9.9.9:53", // Quad9
"dns|149.112.112.112:53", // Quad9
// Configuration:
// protocol: dns, dot
// : IP + Port
// parameters
// - `name=name`: human readable name for resolver
// - `verify=domain`: verify domain (dot only)
// - `blockedif=baredns`: how to detect if the dns service blocked something
// - `baredns`: NXDomain result, but without any other record in any section
// Possible future format:
// "dot://9.9.9.9:853?verify=dns.quad9.net&", // Quad9
// "dot|149.112.112.112:853|dns.quad9.net", // Quad9
// "dot://[2620:fe::fe]:853?verify=dns.quad9.net&name=Quad9" // Quad9
// "dot://[2620:fe::9]:853?verify=dns.quad9.net&name=Quad9" // Quad9
}
CfgOptionNameServersKey = "dns/nameservers"
configuredNameServers config.StringArrayOption
CfgOptionNameserverRetryRateKey = "dns/nameserverRetryRate"
nameserverRetryRate config.IntOption
CfgOptionNoMulticastDNSKey = "dns/noMulticastDNS"
noMulticastDNS status.SecurityLevelOption
CfgOptionNoAssignedNameserversKey = "dns/noAssignedNameservers"
noAssignedNameservers status.SecurityLevelOption
CfgOptionNoInsecureProtocolsKey = "dns/noInsecureProtocols"
noInsecureProtocols status.SecurityLevelOption
CfgOptionDontResolveSpecialDomainsKey = "dns/dontResolveSpecialDomains"
dontResolveSpecialDomains status.SecurityLevelOption
CfgOptionDontResolveTestDomainsKey = "dns/dontResolveTestDomains"
dontResolveTestDomains status.SecurityLevelOption
)
func prepConfig() error {
err := config.Register(&config.Option{
Name: "DNS Servers",
Key: CfgOptionNameServersKey,
Description: "DNS Servers to use for resolving DNS requests.",
OptType: config.OptTypeStringArray,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: defaultNameServers,
ValidationRegex: "^(dns|tcp|tls|https)|[a-z0-9\\.|-]+$",
})
if err != nil {
return err
}
configuredNameServers = config.Concurrent.GetAsStringArray(CfgOptionNameServersKey, defaultNameServers)
err = config.Register(&config.Option{
Name: "DNS Server Retry Rate",
Key: CfgOptionNameserverRetryRateKey,
Description: "Rate at which to retry failed DNS Servers, in seconds.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: 600,
})
if err != nil {
return err
}
nameserverRetryRate = config.Concurrent.GetAsInt(CfgOptionNameserverRetryRateKey, 600)
err = config.Register(&config.Option{
Name: "Do not use Multicast DNS",
Key: CfgOptionNoMulticastDNSKey,
Description: "Multicast DNS queries other devices in the local network",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 6,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
noMulticastDNS = status.ConfigIsActiveConcurrent(CfgOptionNoMulticastDNSKey)
err = config.Register(&config.Option{
Name: "Do not use assigned Nameservers",
Key: CfgOptionNoAssignedNameserversKey,
Description: "that were acquired by the network (dhcp) or system",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 4,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
noAssignedNameservers = status.ConfigIsActiveConcurrent(CfgOptionNoAssignedNameserversKey)
err = config.Register(&config.Option{
Name: "Do not resolve insecurely",
Key: CfgOptionNoInsecureProtocolsKey,
Description: "Do not resolve domains with insecure protocols, ie. plain DNS",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 6,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
noInsecureProtocols = status.ConfigIsActiveConcurrent(CfgOptionNoInsecureProtocolsKey)
err = config.Register(&config.Option{
Name: "Do not resolve special domains",
Key: CfgOptionDontResolveSpecialDomainsKey,
Description: fmt.Sprintf("Do not resolve the special top level domains %s", formatScopeList(specialServiceScopes)),
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
dontResolveSpecialDomains = status.ConfigIsActiveConcurrent(CfgOptionDontResolveSpecialDomainsKey)
err = config.Register(&config.Option{
Name: "Do not resolve test domains",
Key: CfgOptionDontResolveTestDomainsKey,
Description: fmt.Sprintf("Do not resolve the special testing top level domains %s", formatScopeList(localTestScopes)),
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 6,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
dontResolveTestDomains = status.ConfigIsActiveConcurrent(CfgOptionDontResolveTestDomainsKey)
return nil
}
func formatScopeList(list []string) string {
formatted := make([]string, 0, len(list))
for _, domain := range list {
formatted = append(formatted, strings.Trim(domain, "."))
}
return strings.Join(formatted, ", ")
}

View File

@@ -1,5 +1,5 @@
/*
Package intel is responsible for fetching intelligence data, including DNS, on remote entities.
package resolver is responsible for fetching intelligence data, including DNS, on remote entities.
DNS Servers
@@ -27,4 +27,4 @@ All other domains are resolved using search scopes and all available resolvers.
*/
package intel
package resolver

View File

@@ -1,4 +1,4 @@
package intel
package resolver
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package intel
package resolver
import "testing"

View File

@@ -1,4 +1,4 @@
package intel
package resolver
import (
"context"
@@ -6,6 +6,7 @@ import (
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/intel"
// module dependencies
_ "github.com/safing/portmaster/core"
@@ -16,10 +17,12 @@ var (
)
func init() {
module = modules.Register("intel", prep, start, nil, "core", "network")
module = modules.Register("resolver", prep, start, nil, "core", "network")
}
func prep() error {
intel.SetReverseResolver(ResolveIPAndValidate)
return prepConfig()
}

6
resolver/main_test.go Normal file
View File

@@ -0,0 +1,6 @@
package resolver
import (
// portmaster tests helper
_ "github.com/safing/portmaster/core/pmtesting"
)

View File

@@ -1,4 +1,4 @@
package intel
package resolver
import (
"context"
@@ -313,7 +313,7 @@ func listenForDNSPackets(conn *net.UDPConn, messages chan *dns.Msg) error {
for {
n, err := conn.Read(buf)
if err != nil {
if module.ShutdownInProgress() {
if module.IsStopping() {
return nil
}
log.Debugf("intel: failed to read packet: %s", err)

View File

@@ -1,4 +1,4 @@
package intel
package resolver
import (
"errors"

View File

@@ -1,4 +1,4 @@
package intel
package resolver
import (
"context"
@@ -158,13 +158,14 @@ func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
dupKey := fmt.Sprintf("%s%s", q.FQDN, q.QType.String())
dupReqLock.Lock()
defer dupReqLock.Unlock()
// get duplicate request waitgroup
wg, requestActive := dupReqMap[dupKey]
// someone else is already on it!
if requestActive {
dupReqLock.Unlock()
// log that we are waiting
log.Tracer(ctx).Tracef("intel: waiting for duplicate query for %s to complete", dupKey)
// wait
@@ -182,6 +183,8 @@ func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
// add to registry
dupReqMap[dupKey] = wg
dupReqLock.Unlock()
// return function to mark request as finished
return func() {
dupReqLock.Lock()
@@ -222,7 +225,7 @@ resolveLoop:
rrCache, err = resolver.Conn.Query(ctx, q)
if err != nil {
// FIXME: check if we are online?
// TODO: check if we are online?
switch {
case errors.Is(err, ErrNotFound):

View File

@@ -1,4 +1,4 @@
package intel
package resolver
// DISABLE TESTING FOR NOW: find a way to have tests with the module system

View File

@@ -1,4 +1,4 @@
package intel
package resolver
import (
"context"
@@ -114,7 +114,7 @@ func domainInScope(dotPrefixedFQDN string, scopeList []string) bool {
}
// GetResolversInScope returns all resolvers that are in scope the resolve the given query and options.
func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) {
func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) { //nolint:gocognit // TODO
resolversLock.RLock()
defer resolversLock.RUnlock()
@@ -226,13 +226,13 @@ func (q *Query) checkCompliance() error {
}
// special TLDs
if doNotResolveSpecialDomains(q.SecurityLevel) &&
if dontResolveSpecialDomains(q.SecurityLevel) &&
domainInScope(q.dotPrefixedFQDN, specialServiceScopes) {
return ErrSpecialDomainsDisabled
}
// testing TLDs
if doNotResolveTestDomains(q.SecurityLevel) &&
if dontResolveTestDomains(q.SecurityLevel) &&
domainInScope(q.dotPrefixedFQDN, localTestScopes) {
return ErrTestDomainsDisabled
}
@@ -245,7 +245,7 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
return errSkip
}
if doNotUseInsecureProtocols(q.SecurityLevel) {
if noInsecureProtocols(q.SecurityLevel) {
switch resolver.ServerType {
case ServerTypeDNS:
return errInsecureProtocol
@@ -260,13 +260,13 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
}
}
if doNotUseAssignedNameservers(q.SecurityLevel) {
if noAssignedNameservers(q.SecurityLevel) {
if resolver.Source == ServerSourceAssigned {
return errAssignedServer
}
}
if doNotUseMulticastDNS(q.SecurityLevel) {
if noMulticastDNS(q.SecurityLevel) {
if resolver.Source == ServerSourceMDNS {
return errMulticastDNS
}

153
resolver/resolver.go Normal file
View File

@@ -0,0 +1,153 @@
package resolver
import (
"context"
"net"
"sync"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/environment"
)
// DNS Resolver Attributes
const (
ServerTypeDNS = "dns"
ServerTypeTCP = "tcp"
ServerTypeDoT = "dot"
ServerTypeDoH = "doh"
ServerSourceConfigured = "config"
ServerSourceAssigned = "dhcp"
ServerSourceMDNS = "mdns"
)
// Resolver holds information about an active resolver.
type Resolver struct {
// Server config url (and ID)
Server string
// Parsed config
ServerType string
ServerAddress string
ServerIP net.IP
ServerIPScope int8
ServerPort uint16
// Special Options
VerifyDomain string
Search []string
SkipFQDN string
Source string
// logic interface
Conn ResolverConn
}
// String returns the URL representation of the resolver.
func (resolver *Resolver) String() string {
return resolver.Server
}
// ResolverConn is an interface to implement different types of query backends.
type ResolverConn interface { //nolint:go-lint // TODO
Query(ctx context.Context, q *Query) (*RRCache, error)
MarkFailed()
LastFail() time.Time
}
// BasicResolverConn implements ResolverConn for standard dns clients.
type BasicResolverConn struct {
sync.Mutex // for lastFail
resolver *Resolver
clientManager *clientManager
lastFail time.Time
}
// MarkFailed marks the resolver as failed.
func (brc *BasicResolverConn) MarkFailed() {
if !environment.Online() {
// don't mark failed if we are offline
return
}
brc.Lock()
defer brc.Unlock()
brc.lastFail = time.Now()
}
// LastFail returns the internal lastfail value while locking the Resolver.
func (brc *BasicResolverConn) LastFail() time.Time {
brc.Lock()
defer brc.Unlock()
return brc.lastFail
}
// Query executes the given query against the resolver.
func (brc *BasicResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) {
// convenience
resolver := brc.resolver
// create query
dnsQuery := new(dns.Msg)
dnsQuery.SetQuestion(q.FQDN, uint16(q.QType))
// start
var reply *dns.Msg
var err error
for i := 0; i < 3; i++ {
// log query time
// qStart := time.Now()
reply, _, err = brc.clientManager.getDNSClient().Exchange(dnsQuery, resolver.ServerAddress)
// log.Tracef("intel: query to %s took %s", resolver.Server, time.Now().Sub(qStart))
// error handling
if err != nil {
log.Tracer(ctx).Tracef("intel: query to %s encountered error: %s", resolver.Server, err)
// TODO: handle special cases
// 1. connect: network is unreachable
// 2. timeout
// hint network environment at failed connection
environment.ReportFailedConnection()
// temporary error
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
log.Tracer(ctx).Tracef("intel: retrying to resolve %s%s with %s, error is temporary", q.FQDN, q.QType, resolver.Server)
continue
}
// permanent error
break
}
// no error
break
}
if err != nil {
return nil, err
// FIXME: mark as failed
}
// hint network environment at successful connection
environment.ReportSuccessfulConnection()
new := &RRCache{
Domain: q.FQDN,
Question: q.QType,
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
Server: resolver.Server,
ServerScope: resolver.ServerIPScope,
}
// TODO: check if reply.Answer is valid
return new, nil
}

View File

@@ -1,4 +1,4 @@
package intel
package resolver
import (
"errors"

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