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 - funlen
- whitespace - whitespace
- wsl - 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 language: go
go:
- 1.x
os: os:
- linux - linux
- windows - windows

143
Gopkg.lock generated
View File

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

View File

@@ -25,79 +25,3 @@
# unused-packages = true # unused-packages = true
ignored = ["github.com/safing/portbase/*"] 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 ( var (
CfgDevModeKey = "core/devMode"
defaultDevMode bool defaultDevMode bool
) )
@@ -24,7 +25,7 @@ func logFlagOverrides() {
func registerConfig() error { func registerConfig() error {
err := config.Register(&config.Option{ err := config.Register(&config.Option{
Name: "Development Mode", 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.", Description: "In Development Mode security restrictions are lifted/softened to enable easier access to Portmaster for debugging and testing purposes.",
OptType: config.OptTypeBool, OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelDeveloper, ExpertiseLevel: config.ExpertiseLevelDeveloper,

View File

@@ -3,14 +3,28 @@ package core
import ( import (
"fmt" "fmt"
"github.com/safing/portbase/modules/subsystems"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
) )
var (
module *modules.Module
)
func init() { 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 { if err := startPlatformSpecific(); err != nil {
return fmt.Errorf("failed to start plattform-specific components: %s", err) return fmt.Errorf("failed to start plattform-specific components: %s", err)
} }

View File

@@ -3,15 +3,22 @@ package core
import ( import (
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
// database module
_ "github.com/safing/portbase/database/dbmodule"
// module dependencies // module dependencies
_ "github.com/safing/portbase/database/storage/bbolt" _ "github.com/safing/portbase/database/storage/bbolt"
) )
var (
DefaultDatabaseStorageType = "bbolt"
)
func registerDatabases() error { func registerDatabases() error {
_, err := database.Register(&database.Database{ _, err := database.Register(&database.Database{
Name: "core", Name: "core",
Description: "Holds core data, such as settings and profiles", Description: "Holds core data, such as settings and profiles",
StorageType: "bbolt", StorageType: DefaultDatabaseStorageType,
PrimaryAPI: "", PrimaryAPI: "",
}) })
if err != nil { if err != nil {
@@ -21,7 +28,7 @@ func registerDatabases() error {
_, err = database.Register(&database.Database{ _, err = database.Register(&database.Database{
Name: "cache", Name: "cache",
Description: "Cached data, such as Intelligence and DNS Records", Description: "Cached data, such as Intelligence and DNS Records",
StorageType: "bbolt", StorageType: DefaultDatabaseStorageType,
PrimaryAPI: "", PrimaryAPI: "",
}) })
if err != nil { if err != nil {
@@ -31,7 +38,7 @@ func registerDatabases() error {
// _, err = database.Register(&database.Database{ // _, err = database.Register(&database.Database{
// Name: "history", // Name: "history",
// Description: "Historic event data", // Description: "Historic event data",
// StorageType: "bbolt", // StorageType: DefaultDatabaseStorageType,
// PrimaryAPI: "", // PrimaryAPI: "",
// }) // })
// if err != nil { // 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" "strconv"
"strings" "strings"
"github.com/safing/portbase/utils" "github.com/safing/portbase/api"
"github.com/safing/portmaster/core/structure" "github.com/safing/portbase/dataroot"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process" "github.com/safing/portmaster/process"
"github.com/safing/portbase/api"
) )
var ( var (
@@ -28,7 +25,7 @@ var (
) )
func prepAPIAuth() error { func prepAPIAuth() error {
dataRoot = structure.Root() dataRoot = dataroot.Root()
return api.SetAuthenticator(apiAuthenticator) return api.SetAuthenticator(apiAuthenticator)
} }

View File

@@ -2,14 +2,16 @@ package firewall
import ( import (
"github.com/safing/portbase/config" "github.com/safing/portbase/config"
"github.com/safing/portmaster/status"
) )
var ( var (
permanentVerdicts config.BoolOption CfgOptionEnableFilterKey = "filter/enable"
filterDNSByScope status.SecurityLevelOption
filterDNSByProfile status.SecurityLevelOption CfgOptionPermanentVerdictsKey = "filter/permanentVerdicts"
promptTimeout config.IntOption permanentVerdicts config.BoolOption
CfgOptionPromptTimeoutKey = "filter/promptTimeout"
promptTimeout config.IntOption
devMode config.BoolOption devMode config.BoolOption
apiListenAddress config.StringOption apiListenAddress config.StringOption
@@ -18,7 +20,7 @@ var (
func registerConfig() error { func registerConfig() error {
err := config.Register(&config.Option{ err := config.Register(&config.Option{
Name: "Permanent Verdicts", 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.", 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, OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelExpert, ExpertiseLevel: config.ExpertiseLevelExpert,
@@ -28,43 +30,11 @@ func registerConfig() error {
if err != nil { if err != nil {
return err return err
} }
permanentVerdicts = config.Concurrent.GetAsBool("firewall/permanentVerdicts", true) permanentVerdicts = config.Concurrent.GetAsBool(CfgOptionPermanentVerdictsKey, 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")
err = config.Register(&config.Option{ err = config.Register(&config.Option{
Name: "Timeout for prompt notifications", 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.", 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, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelUser, ExpertiseLevel: config.ExpertiseLevelUser,
@@ -74,9 +44,9 @@ func registerConfig() error {
if err != nil { if err != nil {
return err 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", "") apiListenAddress = config.GetAsString("api/listenAddress", "")
return nil return nil

View File

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

View File

@@ -7,6 +7,9 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/safing/portbase/config"
"github.com/safing/portbase/modules/subsystems"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
"github.com/safing/portmaster/firewall/inspection" "github.com/safing/portmaster/firewall/inspection"
@@ -41,11 +44,26 @@ var (
) )
func init() { 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) { func prep() (err error) {
err = registerConfig() err = registerConfig()
if err != nil { if err != nil {
return err return err
@@ -188,22 +206,18 @@ func handlePacket(pkt packet.Packet) {
// associate packet to link and handle // associate packet to link and handle
link, created := network.GetOrCreateLinkByPacket(pkt) link, created := network.GetOrCreateLinkByPacket(pkt)
defer func() {
go link.SaveIfNeeded()
}()
if created { if created {
link.SetFirewallHandler(initialHandler) link.SetFirewallHandler(initialHandler)
link.HandlePacket(pkt)
return
} }
if link.FirewallHandlerIsSet() {
link.HandlePacket(pkt) link.HandlePacket(pkt)
return
}
issueVerdict(pkt, link, 0, true)
} }
func initialHandler(pkt packet.Packet, link *network.Link) { func initialHandler(pkt packet.Packet, link *network.Link) {
defer func() {
go link.SaveIfNeeded()
}()
log.Tracer(pkt.Ctx()).Trace("firewall: [initial handler]") log.Tracer(pkt.Ctx()).Trace("firewall: [initial handler]")
// check for internal firewall bypass // check for internal firewall bypass
@@ -217,9 +231,6 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
} else { } else {
comm.AddLink(link) comm.AddLink(link)
} }
defer func() {
go comm.SaveIfNeeded()
}()
// approve // approve
link.Accept("internally approved") link.Accept("internally approved")
@@ -249,9 +260,6 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
return return
} }
} }
defer func() {
go comm.SaveIfNeeded()
}()
// add new Link to Communication (and save both) // add new Link to Communication (and save both)
comm.AddLink(link) comm.AddLink(link)
@@ -267,7 +275,8 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
log.Tracer(pkt.Ctx()).Trace("firewall: starting decision process") 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) DecideOnLink(comm, link, pkt)
// TODO: link this to real status // TODO: link this to real status
@@ -380,7 +389,7 @@ func issueVerdict(pkt packet.Packet, link *network.Link, verdict network.Verdict
func run() { func run() {
for { for {
select { select {
case <-modules.ShuttingDown(): case <-module.Stopping():
return return
case pkt := <-interception.Packets: case pkt := <-interception.Packets:
handlePacket(pkt) handlePacket(pkt)
@@ -391,7 +400,7 @@ func run() {
func statLogger() { func statLogger() {
for { for {
select { select {
case <-modules.ShuttingDown(): case <-module.Stopping():
return return
case <-time.After(10 * time.Second): 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)) 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" "fmt"
"net" "net"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/miekg/dns"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network" "github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process" "github.com/safing/portmaster/process"
"github.com/safing/portmaster/profile" "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/agext/levenshtein"
"github.com/miekg/dns"
) )
// Call order: // Call order:
@@ -30,11 +31,10 @@ import (
// 4. DecideOnLink // 4. DecideOnLink
// is called when when the first packet of a link arrives only if communication has verdict UNDECIDED or CANTSAY // 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. // DecideOnCommunicationBeforeDNS makes a decision about a communication before the dns query is resolved and intel is gathered.
func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) { func DecideOnCommunicationBeforeDNS(comm *network.Communication) {
// update profiles and check if communication needs reevaluation
// check if communication needs reevaluation if comm.UpdateAndCheck() {
if comm.NeedsReevaluation() {
log.Infof("firewall: re-evaluating verdict on %s", comm) log.Infof("firewall: re-evaluating verdict on %s", comm)
comm.ResetVerdict() comm.ResetVerdict()
} }
@@ -51,116 +51,74 @@ func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string)
return return
} }
// get and check profile set // get profile
profileSet := comm.Process().ProfileSet() p := comm.Process().Profile()
if profileSet == nil {
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
comm.Deny("no Profile Set")
return
}
profileSet.Update(status.ActiveSecurityLevel())
// check for any network access // 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) log.Infof("firewall: denying communication %s, accessing Internet or LAN not permitted", comm)
comm.Deny("accessing Internet or LAN not permitted") comm.Deny("accessing Internet or LAN not permitted")
return return
} }
// continueing with access to either Internet or LAN
// check endpoint list // check endpoint list
result, reason := profileSet.CheckEndpointDomain(fqdn) // FIXME: comm.Entity.Lock()
result, reason := p.MatchEndpoint(comm.Entity)
// FIXME: comm.Entity.Unlock()
switch result { switch result {
case profile.NoMatch: case endpoints.Undeterminable:
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:
comm.UpdateVerdict(network.VerdictUndeterminable) comm.UpdateVerdict(network.VerdictUndeterminable)
case profile.Denied: return
log.Infof("firewall: denying communication %s, endpoint is blacklisted: %s", comm, reason) case endpoints.Denied:
comm.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason)) log.Infof("firewall: denying communication %s, domain is blacklisted: %s", comm, reason)
case profile.Permitted: comm.Deny(fmt.Sprintf("domain is blacklisted: %s", reason))
log.Infof("firewall: permitting communication %s, endpoint is whitelisted: %s", comm, reason) return
comm.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason)) case endpoints.Permitted:
} log.Infof("firewall: permitting communication %s, domain is whitelisted: %s", comm, reason)
} comm.Accept(fmt.Sprintf("domain 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 return
} }
// continueing with result == NoMatch
// grant self - should not get here // check default action
if comm.Process().Pid == os.Getpid() { if p.DefaultAction() == profile.DefaultActionPermit {
log.Infof("firewall: granting own communication %s", comm) log.Infof("firewall: permitting communication %s, domain is not blacklisted (default=permit)", comm)
comm.Accept("") comm.Accept("domain is not blacklisted (default=permit)")
return 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 // check relation
if profileSet.CheckFlag(profile.Related) { if !p.DisableAutoPermit() {
if checkRelation(comm, fqdn) { if checkRelation(comm) {
return return
} }
} }
// prompt // 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. // FilterDNSResponse filters a dns response according to the application profile and settings.
//nolint:gocognit // FIXME func FilterDNSResponse(comm *network.Communication, q *resolver.Query, rrCache *resolver.RRCache) *resolver.RRCache { //nolint:gocognit // TODO
func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *intel.RRCache) *intel.RRCache {
// do not modify own queries - this should not happen anyway // do not modify own queries - this should not happen anyway
if comm.Process().Pid == os.Getpid() { if comm.Process().Pid == os.Getpid() {
return rrCache return rrCache
} }
// check if there is a profile // get profile
profileSet := comm.Process().ProfileSet() p := comm.Process().Profile()
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)
// check if DNS response filtering is completely turned off // check if DNS response filtering is completely turned off
if !filterByScope && !filterByProfile { if !p.RemoveOutOfScopeDNS() && !p.RemoveBlockedDNS() {
return rrCache return rrCache
} }
@@ -175,7 +133,6 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int
// loop vars // loop vars
var classification int8 var classification int8
var ip net.IP var ip net.IP
var result profile.EPResult
// filter function // filter function
filterEntries := func(entries []dns.RR) (goodEntries []dns.RR) { 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) classification = netutils.ClassifyIP(ip)
if filterByScope { if p.RemoveOutOfScopeDNS() {
switch { switch {
case classification == netutils.HostLocal: case classification == netutils.HostLocal:
// No DNS should return localhost addresses // 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 // filter by flags
switch { switch {
case !profileSet.CheckFlag(profile.Internet) && classification == netutils.Global: case p.BlockScopeInternet() && classification == netutils.Global:
addressesRemoved++ addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue continue
case !profileSet.CheckFlag(profile.LAN) && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
addressesRemoved++ addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue continue
case !profileSet.CheckFlag(profile.Localhost) && classification == netutils.HostLocal: case p.BlockScopeLocal() && classification == netutils.HostLocal:
addressesRemoved++ addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue continue
} }
// filter by endpoints // TODO: filter by endpoint list (IP only)
result, _ = profileSet.CheckEndpointIP(q.FQDN, ip, 0, 0, false)
if result == profile.Denied {
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
}
} }
// if survived, add to good entries // 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. // DecideOnCommunication makes a decision about a communication with its first packet.
func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) { func DecideOnCommunication(comm *network.Communication) {
// update profiles and check if communication needs reevaluation
// check if communication needs reevaluation, if it's not with a domain if comm.UpdateAndCheck() {
if comm.NeedsReevaluation() {
log.Infof("firewall: re-evaluating verdict on %s", comm) log.Infof("firewall: re-evaluating verdict on %s", comm)
comm.ResetVerdict() comm.ResetVerdict()
// if communicating with a domain entity, re-evaluate with Before/AfterIntel // if communicating with a domain entity, re-evaluate with BeforeDNS
if strings.HasSuffix(comm.Domain, ".") { if strings.HasSuffix(comm.Scope, ".") {
DecideOnCommunicationBeforeIntel(comm, comm.Domain) DecideOnCommunicationBeforeDNS(comm)
DecideOnCommunicationAfterIntel(comm, comm.Domain, nil)
} }
} }
@@ -293,29 +242,24 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
return return
} }
// check if there is a profile // get profile
profileSet := comm.Process().ProfileSet() p := comm.Process().Profile()
if profileSet == nil {
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
comm.Deny("no Profile Set")
return
}
profileSet.Update(status.ActiveSecurityLevel())
// check comm type // check comm type
switch comm.Domain { switch comm.Scope {
case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid: 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) 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") comm.Block("not a service")
} else { } else {
comm.Deny("not a service") comm.Deny("not a service")
} }
return return
} }
case network.PeerLAN, network.PeerInternet, network.PeerInvalid: // Important: PeerHost is and should be missing! case network.PeerLAN, network.PeerInternet, network.PeerInvalid:
if !profileSet.CheckFlag(profile.PeerToPeer) { // 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) 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") comm.Deny("peer to peer comms (to an IP) not allowed")
return return
@@ -323,21 +267,21 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
} }
// check network scope // check network scope
switch comm.Domain { switch comm.Scope {
case network.IncomingHost: case network.IncomingHost:
if !profileSet.CheckFlag(profile.Localhost) { if p.BlockScopeLocal() {
log.Infof("firewall: denying communication %s, serving localhost not allowed", comm) log.Infof("firewall: denying communication %s, serving localhost not allowed", comm)
comm.Block("serving localhost not allowed") comm.Block("serving localhost not allowed")
return return
} }
case network.IncomingLAN: case network.IncomingLAN:
if !profileSet.CheckFlag(profile.LAN) { if p.BlockScopeLAN() {
log.Infof("firewall: denying communication %s, serving LAN not allowed", comm) log.Infof("firewall: denying communication %s, serving LAN not allowed", comm)
comm.Deny("serving LAN not allowed") comm.Deny("serving LAN not allowed")
return return
} }
case network.IncomingInternet: case network.IncomingInternet:
if !profileSet.CheckFlag(profile.Internet) { if p.BlockScopeInternet() {
log.Infof("firewall: denying communication %s, serving Internet not allowed", comm) log.Infof("firewall: denying communication %s, serving Internet not allowed", comm)
comm.Deny("serving Internet not allowed") comm.Deny("serving Internet not allowed")
return return
@@ -347,19 +291,19 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
comm.Drop("invalid IP address") comm.Drop("invalid IP address")
return return
case network.PeerHost: case network.PeerHost:
if !profileSet.CheckFlag(profile.Localhost) { if p.BlockScopeLocal() {
log.Infof("firewall: denying communication %s, accessing localhost not allowed", comm) log.Infof("firewall: denying communication %s, accessing localhost not allowed", comm)
comm.Block("accessing localhost not allowed") comm.Block("accessing localhost not allowed")
return return
} }
case network.PeerLAN: case network.PeerLAN:
if !profileSet.CheckFlag(profile.LAN) { if p.BlockScopeLAN() {
log.Infof("firewall: denying communication %s, accessing the LAN not allowed", comm) log.Infof("firewall: denying communication %s, accessing the LAN not allowed", comm)
comm.Deny("accessing the LAN not allowed") comm.Deny("accessing the LAN not allowed")
return return
} }
case network.PeerInternet: case network.PeerInternet:
if !profileSet.CheckFlag(profile.Internet) { if p.BlockScopeInternet() {
log.Infof("firewall: denying communication %s, accessing the Internet not allowed", comm) log.Infof("firewall: denying communication %s, accessing the Internet not allowed", comm)
comm.Deny("accessing the Internet not allowed") comm.Deny("accessing the Internet not allowed")
return return
@@ -384,7 +328,7 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa
return 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) { if comm.Process().Pid >= 0 && pkt.Info().Src.Equal(pkt.Info().Dst) {
// get PID // get PID
otherPid, _, err := process.GetPidByEndpoints( otherPid, _, err := process.GetPidByEndpoints(
@@ -424,86 +368,80 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa
return return
} }
// check if there is a profile // get profile
profileSet := comm.Process().ProfileSet() p := comm.Process().Profile()
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
// check endpoints list // 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 { switch result {
case profile.Denied: case endpoints.Denied:
log.Infof("firewall: denying link %s, endpoint is blacklisted: %s", link, reason) log.Infof("firewall: denying link %s, endpoint is blacklisted: %s", link, reason)
link.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason)) link.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason))
return return
case profile.Permitted: case endpoints.Permitted:
log.Infof("firewall: permitting link %s, endpoint is whitelisted: %s", link, reason) log.Infof("firewall: permitting link %s, endpoint is whitelisted: %s", link, reason)
link.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason)) link.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
return return
} }
// continueing with result == NoMatch
// TODO: Stamp integration // implicit default=block for incoming
if comm.Direction {
switch profileSet.GetProfileMode() { log.Infof("firewall: denying link %s: endpoint is not whitelisted (incoming is always default=block)", link)
case profile.Whitelist: link.Deny("endpoint is not whitelisted (incoming is always default=block)")
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")
return 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 // check relation
if fqdn != "" && profileSet.CheckFlag(profile.Related) { if !p.DisableAutoPermit() {
if checkRelation(comm, fqdn) { if checkRelation(comm) {
return return
} }
} }
// prompt // prompt
prompt(comm, link, pkt, fqdn) if p.DefaultAction() == profile.DefaultActionAsk {
} prompt(comm, link, pkt)
func checkRelation(comm *network.Communication, fqdn string) (related bool) {
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
return 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 // only look at the last two path segments
if len(pathElements) > 2 { if len(pathElements) > 2 {
pathElements = pathElements[len(pathElements)-2:] pathElements = pathElements[len(pathElements)-2:]
} }
domainElements := strings.Split(fqdn, ".") domainElements := strings.Split(comm.Entity.Domain, ".")
var domainElement string var domainElement string
var processElement string var processElement string
@@ -517,11 +455,6 @@ matchLoop:
break 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 { if levenshtein.Match(domainElement, comm.Process().Name, nil) > 0.5 {
related = true related = true
processElement = comm.Process().Name processElement = comm.Process().Name

View File

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

View File

@@ -1,15 +1,15 @@
package firewall package firewall
import ( import (
"context"
"fmt" "fmt"
"time" "time"
"github.com/safing/portmaster/profile/endpoints"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/notifications" "github.com/safing/portbase/notifications"
"github.com/safing/portmaster/network" "github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/profile"
) )
const ( const (
@@ -30,14 +30,14 @@ var (
) )
//nolint:gocognit // FIXME //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 nTTL := time.Duration(promptTimeout()) * time.Second
// first check if there is an existing notification for this. // first check if there is an existing notification for this.
// build notification ID // build notification ID
var nID string var nID string
switch { switch {
case comm.Direction, fqdn == "": // connection to/from IP case comm.Direction, comm.Entity.Domain == "": // connection to/from IP
if pkt == nil { if pkt == nil {
log.Error("firewall: could not prompt for incoming/direct connection: missing pkt") log.Error("firewall: could not prompt for incoming/direct connection: missing pkt")
if link != nil { if link != nil {
@@ -47,9 +47,9 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
} }
return 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 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) n := notifications.Get(nID)
saveResponse := true saveResponse := true
@@ -70,7 +70,7 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
// add message and actions // add message and actions
switch { switch {
case comm.Direction: // incoming 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{ n.AvailableActions = []*notifications.Action{
{ {
ID: permitServingIP, ID: permitServingIP,
@@ -81,8 +81,8 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
Text: "Deny", Text: "Deny",
}, },
} }
case fqdn == "": // direct connection case comm.Entity.Domain == "": // 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()) 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{ n.AvailableActions = []*notifications.Action{
{ {
ID: permitIP, ID: permitIP,
@@ -94,10 +94,10 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
}, },
} }
default: // connection to domain default: // connection to domain
if pkt != nil { if link != 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()) 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 { } 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{ n.AvailableActions = []*notifications.Action{
{ {
@@ -141,62 +141,57 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
return return
} }
new := &profile.EndpointPermission{ // get profile
Type: profile.EptDomain, p := comm.Process().Profile()
Value: comm.Domain,
Permit: false,
Created: time.Now().Unix(),
}
// permission type var ep endpoints.Endpoint
switch promptResponse { switch promptResponse {
case permitDomainAll, denyDomainAll: case permitDomainAll:
new.Value = "." + new.Value ep = &endpoints.EndpointDomain{
case permitIP, permitServingIP, denyIP, denyServingIP: EndpointBase: endpoints.EndpointBase{Permitted: true},
if pkt == nil { Domain: "." + comm.Entity.Domain,
log.Warningf("firewall: received invalid prompt response: %s for %s", promptResponse, comm.Domain)
return
} }
if pkt.Info().Version == packet.IPv4 { case permitDomainDistinct:
new.Type = profile.EptIPv4 ep = &endpoints.EndpointDomain{
} else { EndpointBase: endpoints.EndpointBase{Permitted: true},
new.Type = profile.EptIPv6 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 { switch promptResponse {
case permitServingIP, denyServingIP: case permitServingIP, denyServingIP:
userProfile.ServiceEndpoints = append(userProfile.ServiceEndpoints, new) p.AddServiceEndpoint(ep.String())
default: 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(): case <-n.Expired():
if link != nil { if link != nil {
link.Accept("no response to prompt") link.Deny("no response to prompt")
} else { } 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 { if err != nil {
return nil, err return nil, err
} }
return record, nil return record, nil
} }

View File

@@ -3,16 +3,9 @@ package geoip
import ( import (
"net" "net"
"testing" "testing"
"github.com/safing/portmaster/updates"
) )
func TestLocationLookup(t *testing.T) { func TestLocationLookup(t *testing.T) {
err := updates.InitForTesting()
if err != nil {
t.Fatal(err)
}
ip1 := net.ParseIP("81.2.69.142") ip1 := net.ParseIP("81.2.69.142")
loc1, err := GetLocation(ip1) loc1, err := GetLocation(ip1)
if err != nil { if err != nil {
@@ -39,7 +32,21 @@ func TestLocationLookup(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) 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) dist1 := loc1.EstimateNetworkProximity(loc2)
dist2 := loc2.EstimateNetworkProximity(loc3) 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", ip2, ip3, dist2)
t.Logf("proximity %s <> %s: %d", ip1, ip3, dist3) t.Logf("proximity %s <> %s: %d", ip1, ip3, dist3)
t.Logf("proximity %s <> %s: %d", ip1, ip4, dist4) t.Logf("proximity %s <> %s: %d", ip1, ip4, dist4)
} }

View File

@@ -2,7 +2,6 @@ package geoip
import ( import (
"context" "context"
"fmt"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
) )
@@ -12,15 +11,10 @@ var (
) )
func init() { func init() {
module = modules.Register("geoip", nil, start, nil, "updates") module = modules.Register("geoip", prep, nil, nil, "core")
} }
func start() error { func prep() error {
err := prepDatabaseForUse()
if err != nil {
return fmt.Errorf("goeip: failed to load databases: %s", err)
}
return module.RegisterEventHook( return module.RegisterEventHook(
"updates", "updates",
"resource update", "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 ( import (
"context" "context"
"net"
"sync"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/environment"
) )
// DNS Resolver Attributes var (
const ( reverseResolver func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error)
ServerTypeDNS = "dns"
ServerTypeTCP = "tcp"
ServerTypeDoT = "dot"
ServerTypeDoH = "doh"
ServerSourceConfigured = "config"
ServerSourceAssigned = "dhcp"
ServerSourceMDNS = "mdns"
) )
// Resolver holds information about an active resolver. // SetReverseResolver allows the resolver module to register a function to allow reverse resolving IPs to domains.
type Resolver struct { func SetReverseResolver(fn func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error)) {
// Server config url (and ID) if reverseResolver == nil {
Server string reverseResolver = fn
// 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
} }
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" "github.com/safing/portbase/run"
// include packages here // include packages here
_ "github.com/safing/portbase/modules/subsystems"
_ "github.com/safing/portmaster/core" _ "github.com/safing/portmaster/core"
_ "github.com/safing/portmaster/firewall" _ "github.com/safing/portmaster/firewall"
_ "github.com/safing/portmaster/nameserver" _ "github.com/safing/portmaster/nameserver"
@@ -14,13 +15,6 @@ import (
) )
func main() { 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) info.Set("Portmaster", "0.3.9", "AGPLv3", true)
os.Exit(run.Run()) os.Exit(run.Run())
} }

View File

@@ -5,18 +5,18 @@ import (
"net" "net"
"strings" "strings"
"github.com/safing/portmaster/network/environment" "github.com/safing/portbase/modules/subsystems"
"github.com/miekg/dns"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
"github.com/safing/portmaster/detection/dga" "github.com/safing/portmaster/detection/dga"
"github.com/safing/portmaster/firewall" "github.com/safing/portmaster/firewall"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network" "github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/environment"
"github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/resolver"
"github.com/miekg/dns"
) )
var ( var (
@@ -30,10 +30,18 @@ var (
) )
func init() { 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") localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1")
if err != nil { if err != nil {
return err return err
@@ -45,6 +53,7 @@ func initLocalhostRRs() error {
} }
localhostRRs = []dns.RR{localhostIPv4, localhostIPv6} localhostRRs = []dns.RR{localhostIPv4, localhostIPv6}
return nil return nil
} }
@@ -56,7 +65,7 @@ func start() error {
err := dnsServer.ListenAndServe() err := dnsServer.ListenAndServe()
if err != nil { if err != nil {
// check if we are shutting down // check if we are shutting down
if module.ShutdownInProgress() { if module.IsStopping() {
return nil return nil
} }
// is something blocking our port? // 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. // only process first question, that's how everyone does it.
question := query.Question[0] question := query.Question[0]
q := &intel.Query{ q := &resolver.Query{
FQDN: question.Name, FQDN: question.Name,
QType: dns.Type(question.Qtype), 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 // save security level to query
q.SecurityLevel = comm.Process().ProfileSet().SecurityLevel() q.SecurityLevel = comm.Process().Profile().SecurityLevel()
// check for possible DNS tunneling / data transmission // check for possible DNS tunneling / data transmission
// TODO: improve this // 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 // check profile before we even get intel and rr
firewall.DecideOnCommunicationBeforeIntel(comm, q.FQDN) firewall.DecideOnCommunicationBeforeDNS(comm)
comm.Lock() comm.Lock()
comm.SaveWhenFinished() comm.SaveWhenFinished()
comm.Unlock() comm.Unlock()
@@ -200,8 +209,8 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
return nil return nil
} }
// get intel and RRs // resolve
rrCache, err := intel.Resolve(ctx, q) rrCache, err := resolver.Resolve(ctx, q)
if err != nil { if err != nil {
// TODO: analyze nxdomain requests, malware could be trying DGA-domains // 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) 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 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 // filter DNS response
rrCache = firewall.FilterDNSResponse(comm, q, rrCache) rrCache = firewall.FilterDNSResponse(comm, q, rrCache)
if rrCache == nil { 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...) { for _, rr := range append(rrCache.Answer, rrCache.Extra...) {
switch v := rr.(type) { switch v := rr.(type) {
case *dns.A: case *dns.A:
ipInfo, err := intel.GetIPInfo(v.A.String()) ipInfo, err := resolver.GetIPInfo(v.A.String())
if err != nil { if err != nil {
ipInfo = &intel.IPInfo{ ipInfo = &resolver.IPInfo{
IP: v.A.String(), IP: v.A.String(),
Domains: []string{q.FQDN}, Domains: []string{q.FQDN},
} }
@@ -260,9 +244,9 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
} }
} }
case *dns.AAAA: case *dns.AAAA:
ipInfo, err := intel.GetIPInfo(v.AAAA.String()) ipInfo, err := resolver.GetIPInfo(v.AAAA.String())
if err != nil { if err != nil {
ipInfo = &intel.IPInfo{ ipInfo = &resolver.IPInfo{
IP: v.AAAA.String(), IP: v.AAAA.String(),
Domains: []string{q.FQDN}, Domains: []string{q.FQDN},
} }

View File

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

View File

@@ -8,48 +8,63 @@ import (
"sync" "sync"
"time" "time"
"github.com/safing/portmaster/resolver"
"github.com/safing/portbase/database/record" "github.com/safing/portbase/database/record"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portmaster/intel" "github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process" "github.com/safing/portmaster/process"
"github.com/safing/portmaster/profile"
) )
// Communication describes a logical connection between a process and a domain. // Communication describes a logical connection between a process and a domain.
//nolint:maligned // TODO: fix alignment //nolint:maligned // TODO: fix alignment
type Communication struct { type Communication struct {
record.Base record.Base
sync.Mutex lock sync.Mutex
Domain string Scope string
Entity *intel.Entity
Direction bool Direction bool
Intel *intel.Intel
process *process.Process Verdict Verdict
Verdict Verdict Reason string
Reason string ReasonID string // format source[:id[:id]]
Inspect bool Inspect bool
process *process.Process
profileRevisionCounter uint64
FirstLinkEstablished int64 FirstLinkEstablished int64
LastLinkEstablished 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. // Process returns the process that owns the connection.
func (comm *Communication) Process() *process.Process { func (comm *Communication) Process() *process.Process {
comm.Lock() comm.lock.Lock()
defer comm.Unlock() defer comm.lock.Unlock()
return comm.process return comm.process
} }
// ResetVerdict resets the verdict to VerdictUndecided. // ResetVerdict resets the verdict to VerdictUndecided.
func (comm *Communication) ResetVerdict() { func (comm *Communication) ResetVerdict() {
comm.Lock() comm.lock.Lock()
defer comm.Unlock() defer comm.lock.Unlock()
comm.Verdict = VerdictUndecided comm.Verdict = VerdictUndecided
comm.Reason = "" comm.Reason = ""
@@ -58,8 +73,8 @@ func (comm *Communication) ResetVerdict() {
// GetVerdict returns the current verdict. // GetVerdict returns the current verdict.
func (comm *Communication) GetVerdict() Verdict { func (comm *Communication) GetVerdict() Verdict {
comm.Lock() comm.lock.Lock()
defer comm.Unlock() defer comm.lock.Unlock()
return comm.Verdict 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. // UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts.
func (comm *Communication) UpdateVerdict(newVerdict Verdict) { func (comm *Communication) UpdateVerdict(newVerdict Verdict) {
comm.Lock() comm.lock.Lock()
defer comm.Unlock() defer comm.lock.Unlock()
if newVerdict > comm.Verdict { if newVerdict > comm.Verdict {
comm.Verdict = newVerdict comm.Verdict = newVerdict
@@ -108,8 +123,8 @@ func (comm *Communication) SetReason(reason string) {
return return
} }
comm.Lock() comm.lock.Lock()
defer comm.Unlock() defer comm.lock.Unlock()
comm.Reason = reason comm.Reason = reason
comm.saveWhenFinished = true comm.saveWhenFinished = true
} }
@@ -120,8 +135,8 @@ func (comm *Communication) AddReason(reason string) {
return return
} }
comm.Lock() comm.lock.Lock()
defer comm.Unlock() defer comm.lock.Unlock()
if comm.Reason != "" { if comm.Reason != "" {
comm.Reason += " | " comm.Reason += " | "
@@ -129,21 +144,18 @@ func (comm *Communication) AddReason(reason string) {
comm.Reason += reason comm.Reason += reason
} }
// NeedsReevaluation returns whether the decision on this communication should be re-evaluated. // UpdateAndCheck updates profiles and checks whether a reevaluation is needed.
func (comm *Communication) NeedsReevaluation() bool { func (comm *Communication) UpdateAndCheck() (needsReevaluation bool) {
comm.Lock() revCnt := comm.Process().Profile().Update()
defer comm.Unlock()
oldVersion := comm.profileUpdateVersion comm.lock.Lock()
comm.profileUpdateVersion = profile.GetUpdateVersion() defer comm.lock.Unlock()
if comm.profileRevisionCounter != revCnt {
comm.profileRevisionCounter = revCnt
needsReevaluation = true
}
if oldVersion == 0 { return
return false
}
if oldVersion != comm.profileUpdateVersion {
return true
}
return false
} }
// GetCommunicationByFirstPacket returns the matching communication from the internal storage. // GetCommunicationByFirstPacket returns the matching communication from the internal storage.
@@ -153,25 +165,26 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
var domain string var scope string
// Incoming // Incoming
if direction { if direction {
switch netutils.ClassifyIP(pkt.Info().Src) { switch netutils.ClassifyIP(pkt.Info().Src) {
case netutils.HostLocal: case netutils.HostLocal:
domain = IncomingHost scope = IncomingHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = IncomingLAN scope = IncomingLAN
case netutils.Global, netutils.GlobalMulticast: case netutils.Global, netutils.GlobalMulticast:
domain = IncomingInternet scope = IncomingInternet
case netutils.Invalid: case netutils.Invalid:
domain = IncomingInvalid scope = IncomingInvalid
} }
communication, ok := GetCommunication(proc.Pid, domain) communication, ok := GetCommunication(proc.Pid, scope)
if !ok { if !ok {
communication = &Communication{ communication = &Communication{
Domain: domain, Scope: scope,
Entity: (&intel.Entity{}).Init(),
Direction: Inbound, Direction: Inbound,
process: proc, process: proc,
Inspect: true, Inspect: true,
@@ -184,7 +197,7 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
} }
// get domain // get domain
ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP()) ipinfo, err := resolver.GetIPInfo(pkt.FmtRemoteIP())
// PeerToPeer // PeerToPeer
if err != nil { if err != nil {
@@ -192,19 +205,20 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
switch netutils.ClassifyIP(pkt.Info().Dst) { switch netutils.ClassifyIP(pkt.Info().Dst) {
case netutils.HostLocal: case netutils.HostLocal:
domain = PeerHost scope = PeerHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = PeerLAN scope = PeerLAN
case netutils.Global, netutils.GlobalMulticast: case netutils.Global, netutils.GlobalMulticast:
domain = PeerInternet scope = PeerInternet
case netutils.Invalid: case netutils.Invalid:
domain = PeerInvalid scope = PeerInvalid
} }
communication, ok := GetCommunication(proc.Pid, domain) communication, ok := GetCommunication(proc.Pid, scope)
if !ok { if !ok {
communication = &Communication{ communication = &Communication{
Domain: domain, Scope: scope,
Entity: (&intel.Entity{}).Init(),
Direction: Outbound, Direction: Outbound,
process: proc, process: proc,
Inspect: true, Inspect: true,
@@ -221,7 +235,10 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
communication, ok := GetCommunication(proc.Pid, ipinfo.Domains[0]) communication, ok := GetCommunication(proc.Pid, ipinfo.Domains[0])
if !ok { if !ok {
communication = &Communication{ communication = &Communication{
Domain: ipinfo.Domains[0], Scope: ipinfo.Domains[0],
Entity: (&intel.Entity{
Domain: ipinfo.Domains[0],
}).Init(),
Direction: Outbound, Direction: Outbound,
process: proc, process: proc,
Inspect: true, Inspect: true,
@@ -251,7 +268,10 @@ func GetCommunicationByDNSRequest(ctx context.Context, ip net.IP, port uint16, f
communication, ok := GetCommunication(proc.Pid, fqdn) communication, ok := GetCommunication(proc.Pid, fqdn)
if !ok { if !ok {
communication = &Communication{ communication = &Communication{
Domain: fqdn, Scope: fqdn,
Entity: (&intel.Entity{
Domain: fqdn,
}).Init(),
process: proc, process: proc,
Inspect: true, Inspect: true,
saveWhenFinished: true, saveWhenFinished: true,
@@ -271,7 +291,7 @@ func GetCommunication(pid int, domain string) (comm *Communication, ok bool) {
} }
func (comm *Communication) makeKey() string { 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. // 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. // SaveIfNeeded saves the Connection if it is marked for saving when finished.
func (comm *Communication) SaveIfNeeded() { func (comm *Communication) SaveIfNeeded() {
comm.Lock() comm.lock.Lock()
save := comm.saveWhenFinished save := comm.saveWhenFinished
if save { if save {
comm.saveWhenFinished = false comm.saveWhenFinished = false
} }
comm.Unlock() comm.lock.Unlock()
if save { if save {
err := comm.save() err := comm.save()
@@ -299,14 +319,14 @@ func (comm *Communication) SaveIfNeeded() {
// Save saves the Connection object in the storage and propagates the change. // Save saves the Connection object in the storage and propagates the change.
func (comm *Communication) save() error { func (comm *Communication) save() error {
// update comm // update comm
comm.Lock() comm.lock.Lock()
if comm.process == nil { if comm.process == nil {
comm.Unlock() comm.lock.Unlock()
return errors.New("cannot save connection without process") return errors.New("cannot save connection without process")
} }
if !comm.KeyIsSet() { 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() comm.UpdateMeta()
} }
if comm.Meta().Deleted > 0 { if comm.Meta().Deleted > 0 {
@@ -315,7 +335,7 @@ func (comm *Communication) save() error {
} }
key := comm.makeKey() key := comm.makeKey()
comm.saveWhenFinished = false comm.saveWhenFinished = false
comm.Unlock() comm.lock.Unlock()
// save comm // save comm
commsLock.RLock() commsLock.RLock()
@@ -336,8 +356,8 @@ func (comm *Communication) save() error {
func (comm *Communication) Delete() { func (comm *Communication) Delete() {
commsLock.Lock() commsLock.Lock()
defer commsLock.Unlock() defer commsLock.Unlock()
comm.Lock() comm.lock.Lock()
defer comm.Unlock() defer comm.lock.Unlock()
delete(comms, comm.makeKey()) delete(comms, comm.makeKey())
@@ -347,16 +367,18 @@ func (comm *Communication) Delete() {
// AddLink applies the Communication to the Link and sets timestamps. // AddLink applies the Communication to the Link and sets timestamps.
func (comm *Communication) AddLink(link *Link) { func (comm *Communication) AddLink(link *Link) {
comm.lock.Lock()
defer comm.lock.Unlock()
// apply comm to link // apply comm to link
link.Lock() link.lock.Lock()
link.comm = comm link.comm = comm
link.Verdict = comm.Verdict link.Verdict = comm.Verdict
link.Inspect = comm.Inspect link.Inspect = comm.Inspect
// FIXME: use new copy methods
link.Entity.Domain = comm.Entity.Domain
link.saveWhenFinished = true link.saveWhenFinished = true
link.Unlock() link.lock.Unlock()
// update comm LastLinkEstablished
comm.Lock()
// check if we should save // check if we should save
if comm.LastLinkEstablished < time.Now().Add(-3*time.Second).Unix() { if comm.LastLinkEstablished < time.Now().Add(-3*time.Second).Unix() {
@@ -368,8 +390,6 @@ func (comm *Communication) AddLink(link *Link) {
if comm.FirstLinkEstablished == 0 { if comm.FirstLinkEstablished == 0 {
comm.FirstLinkEstablished = comm.LastLinkEstablished comm.FirstLinkEstablished = comm.LastLinkEstablished
} }
comm.Unlock()
} }
// String returns a string representation of Communication. // String returns a string representation of Communication.
@@ -377,7 +397,7 @@ func (comm *Communication) String() string {
comm.Lock() comm.Lock()
defer comm.Unlock() defer comm.Unlock()
switch comm.Domain { switch comm.Scope {
case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid: case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
if comm.process == nil { if comm.process == nil {
return "? <- *" return "? <- *"
@@ -390,8 +410,8 @@ func (comm *Communication) String() string {
return fmt.Sprintf("%s -> *", comm.process.String()) return fmt.Sprintf("%s -> *", comm.process.String())
default: default:
if comm.process == nil { 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" "sync"
"time" "time"
"github.com/safing/portmaster/intel"
"github.com/safing/portbase/database/record" "github.com/safing/portbase/database/record"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
@@ -16,21 +18,22 @@ import (
type FirewallHandler func(pkt packet.Packet, link *Link) type FirewallHandler func(pkt packet.Packet, link *Link)
// Link describes a distinct physical connection (e.g. TCP connection) - like an instance - of a Connection. // Link describes a distinct physical connection (e.g. TCP connection) - like an instance - of a Connection.
//nolint:maligned // TODO: fix alignment type Link struct { //nolint:maligned // TODO: fix alignment
type Link struct {
record.Base record.Base
sync.Mutex lock sync.Mutex
ID string ID string
Entity *intel.Entity
Direction bool
Verdict Verdict Verdict Verdict
Reason string Reason string
ReasonID string // format source[:id[:id]]
Tunneled bool Tunneled bool
VerdictPermanent bool VerdictPermanent bool
Inspect bool Inspect bool
Started int64 Started int64
Ended int64 Ended int64
RemoteAddress string
pktQueue chan packet.Packet pktQueue chan packet.Packet
firewallHandler FirewallHandler firewallHandler FirewallHandler
@@ -41,70 +44,82 @@ type Link struct {
saveWhenFinished bool 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 // Communication returns the Communication the Link is part of
func (link *Link) Communication() *Communication { func (link *Link) Communication() *Communication {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
return link.comm return link.comm
} }
// GetVerdict returns the current verdict. // GetVerdict returns the current verdict.
func (link *Link) GetVerdict() Verdict { func (link *Link) GetVerdict() Verdict {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
return link.Verdict return link.Verdict
} }
// FirewallHandlerIsSet returns whether a firewall handler is set or not // FirewallHandlerIsSet returns whether a firewall handler is set or not
func (link *Link) FirewallHandlerIsSet() bool { func (link *Link) FirewallHandlerIsSet() bool {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
return link.firewallHandler != nil return link.firewallHandler != nil
} }
// SetFirewallHandler sets the firewall handler for this link // SetFirewallHandler sets the firewall handler for this link
func (link *Link) SetFirewallHandler(handler FirewallHandler) { func (link *Link) SetFirewallHandler(handler FirewallHandler) {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
if link.firewallHandler == nil { if link.firewallHandler == nil {
link.firewallHandler = handler
link.pktQueue = make(chan packet.Packet, 1000) link.pktQueue = make(chan packet.Packet, 1000)
// start handling // start handling
module.StartWorker("", func(ctx context.Context) error { module.StartWorker("packet handler", func(ctx context.Context) error {
link.packetHandler() link.packetHandler()
return nil return nil
}) })
return
} }
link.firewallHandler = handler link.firewallHandler = handler
} }
// StopFirewallHandler unsets the firewall handler // StopFirewallHandler unsets the firewall handler
func (link *Link) StopFirewallHandler() { func (link *Link) StopFirewallHandler() {
link.Lock() link.lock.Lock()
link.firewallHandler = nil link.firewallHandler = nil
link.Unlock() link.lock.Unlock()
link.pktQueue <- nil link.pktQueue <- nil
} }
// HandlePacket queues packet of Link for handling // HandlePacket queues packet of Link for handling
func (link *Link) HandlePacket(pkt packet.Packet) { func (link *Link) HandlePacket(pkt packet.Packet) {
link.Lock() // get handler
defer link.Unlock() link.lock.Lock()
handler := link.firewallHandler
link.lock.Unlock()
if link.firewallHandler != nil { // send to queue
if handler != nil {
link.pktQueue <- pkt link.pktQueue <- pkt
return return
} }
// no handler!
log.Warningf("network: link %s does not have a firewallHandler, dropping packet", link) log.Warningf("network: link %s does not have a firewallHandler, dropping packet", link)
err := pkt.Drop() err := pkt.Drop()
if err != nil { if err != nil {
log.Warningf("network: failed to drop packet %s: %s", pkt, err) 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. // Deny blocks or drops the link depending on the connection direction and adds the given reason.
func (link *Link) Deny(reason string) { func (link *Link) Deny(reason string) {
if link.comm != nil && link.comm.Direction { if link.Direction {
link.Drop(reason) link.Drop(reason)
} else { } else {
link.Block(reason) 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 // UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts
func (link *Link) UpdateVerdict(newVerdict Verdict) { func (link *Link) UpdateVerdict(newVerdict Verdict) {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
if newVerdict > link.Verdict { if newVerdict > link.Verdict {
link.Verdict = newVerdict link.Verdict = newVerdict
@@ -166,8 +181,8 @@ func (link *Link) AddReason(reason string) {
return return
} }
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
if link.Reason != "" { if link.Reason != "" {
link.Reason += " | " link.Reason += " | "
@@ -185,9 +200,9 @@ func (link *Link) packetHandler() {
return return
} }
// get handler // get handler
link.Lock() link.lock.Lock()
handler := link.firewallHandler handler := link.firewallHandler
link.Unlock() link.lock.Unlock()
// execute handler or verdict // execute handler or verdict
if handler != nil { if handler != nil {
handler(pkt, link) handler(pkt, link)
@@ -201,8 +216,8 @@ func (link *Link) packetHandler() {
// ApplyVerdict appies the link verdict to a packet. // ApplyVerdict appies the link verdict to a packet.
func (link *Link) ApplyVerdict(pkt packet.Packet) { func (link *Link) ApplyVerdict(pkt packet.Packet) {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
var err error var err error
@@ -251,12 +266,12 @@ func (link *Link) SaveWhenFinished() {
// SaveIfNeeded saves the Link if it is marked for saving when finished. // SaveIfNeeded saves the Link if it is marked for saving when finished.
func (link *Link) SaveIfNeeded() { func (link *Link) SaveIfNeeded() {
link.Lock() link.lock.Lock()
save := link.saveWhenFinished save := link.saveWhenFinished
if save { if save {
link.saveWhenFinished = false link.saveWhenFinished = false
} }
link.Unlock() link.lock.Unlock()
if save { if save {
link.saveAndLog() link.saveAndLog()
@@ -274,18 +289,18 @@ func (link *Link) saveAndLog() {
// save saves the link object in the storage and propagates the change. // save saves the link object in the storage and propagates the change.
func (link *Link) save() error { func (link *Link) save() error {
// update link // update link
link.Lock() link.lock.Lock()
if link.comm == nil { if link.comm == nil {
link.Unlock() link.lock.Unlock()
return errors.New("cannot save link without comms") return errors.New("cannot save link without comms")
} }
if !link.KeyIsSet() { 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.UpdateMeta()
} }
link.saveWhenFinished = false link.saveWhenFinished = false
link.Unlock() link.lock.Unlock()
// save link // save link
linksLock.RLock() linksLock.RLock()
@@ -306,8 +321,8 @@ func (link *Link) save() error {
func (link *Link) Delete() { func (link *Link) Delete() {
linksLock.Lock() linksLock.Lock()
defer linksLock.Unlock() defer linksLock.Unlock()
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
delete(links, link.ID) delete(links, link.ID)
@@ -339,10 +354,15 @@ func GetOrCreateLinkByPacket(pkt packet.Packet) (*Link, bool) {
// CreateLinkFromPacket creates a new Link based on Packet. // CreateLinkFromPacket creates a new Link based on Packet.
func CreateLinkFromPacket(pkt packet.Packet) *Link { func CreateLinkFromPacket(pkt packet.Packet) *Link {
link := &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, Verdict: VerdictUndecided,
Started: time.Now().Unix(), Started: time.Now().Unix(),
RemoteAddress: pkt.FmtRemoteAddress(),
saveWhenFinished: true, saveWhenFinished: true,
} }
return link return link
@@ -350,59 +370,59 @@ func CreateLinkFromPacket(pkt packet.Packet) *Link {
// GetActiveInspectors returns the list of active inspectors. // GetActiveInspectors returns the list of active inspectors.
func (link *Link) GetActiveInspectors() []bool { func (link *Link) GetActiveInspectors() []bool {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
return link.activeInspectors return link.activeInspectors
} }
// SetActiveInspectors sets the list of active inspectors. // SetActiveInspectors sets the list of active inspectors.
func (link *Link) SetActiveInspectors(new []bool) { func (link *Link) SetActiveInspectors(new []bool) {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
link.activeInspectors = new link.activeInspectors = new
} }
// GetInspectorData returns the list of inspector data. // GetInspectorData returns the list of inspector data.
func (link *Link) GetInspectorData() map[uint8]interface{} { func (link *Link) GetInspectorData() map[uint8]interface{} {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
return link.inspectorData return link.inspectorData
} }
// SetInspectorData set the list of inspector data. // SetInspectorData set the list of inspector data.
func (link *Link) SetInspectorData(new map[uint8]interface{}) { func (link *Link) SetInspectorData(new map[uint8]interface{}) {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
link.inspectorData = new link.inspectorData = new
} }
// String returns a string representation of Link. // String returns a string representation of Link.
func (link *Link) String() string { func (link *Link) String() string {
link.Lock() link.lock.Lock()
defer link.Unlock() defer link.lock.Unlock()
if link.comm == nil { if link.comm == nil {
return fmt.Sprintf("? <-> %s", link.RemoteAddress) return fmt.Sprintf("? <-> %s", link.Entity.IP.String())
} }
switch link.comm.Domain { switch link.comm.Scope {
case "I": case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
if link.comm.process == nil { 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())
case "D": case PeerHost, PeerLAN, PeerInternet, PeerInvalid:
if link.comm.process == nil { 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: default:
if link.comm.process == nil { 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 package reference
import "strconv" import (
"strconv"
"strings"
)
var ( var (
protocolNames = map[uint8]string{ protocolNames = map[uint8]string{
@@ -9,20 +12,20 @@ var (
6: "TCP", 6: "TCP",
17: "UDP", 17: "UDP",
27: "RDP", 27: "RDP",
58: "ICMPv6", 58: "ICMP6",
33: "DCCP", 33: "DCCP",
136: "UDPLite", 136: "UDP-LITE",
} }
protocolNumbers = map[string]uint8{ protocolNumbers = map[string]uint8{
"ICMP": 1, "ICMP": 1,
"IGMP": 2, "IGMP": 2,
"TCP": 6, "TCP": 6,
"UDP": 17, "UDP": 17,
"RDP": 27, "RDP": 27,
"DCCP": 33, "DCCP": 33,
"ICMPv6": 58, "ICMP6": 58,
"UDPLite": 136, "UDP-LITE": 136,
} }
) )
@@ -37,7 +40,7 @@ func GetProtocolName(protocol uint8) (name string) {
// GetProtocolNumber returns the number of a IP protocol name. // GetProtocolNumber returns the number of a IP protocol name.
func GetProtocolNumber(protocol string) (number uint8, ok bool) { func GetProtocolNumber(protocol string) (number uint8, ok bool) {
number, ok = protocolNumbers[protocol] number, ok = protocolNumbers[strings.ToUpper(protocol)]
if ok { if ok {
return number, true return number, true
} }

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package network
import ( import (
"time" "time"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process" "github.com/safing/portmaster/process"
@@ -43,11 +44,12 @@ func GetUnknownCommunication(pkt packet.Packet) (*Communication, error) {
return getOrCreateUnknownCommunication(pkt, PeerInvalid) return getOrCreateUnknownCommunication(pkt, PeerInvalid)
} }
func getOrCreateUnknownCommunication(pkt packet.Packet, connClass string) (*Communication, error) { func getOrCreateUnknownCommunication(pkt packet.Packet, connScope string) (*Communication, error) {
connection, ok := GetCommunication(process.UnknownProcess.Pid, connClass) connection, ok := GetCommunication(process.UnknownProcess.Pid, connScope)
if !ok { if !ok {
connection = &Communication{ connection = &Communication{
Domain: connClass, Scope: connScope,
Entity: (&intel.Entity{}).Init(),
Direction: pkt.IsInbound(), Direction: pkt.IsInbound(),
Verdict: VerdictDrop, Verdict: VerdictDrop,
Reason: ReasonUnknownProcess, 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/database"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portmaster/profile"
"github.com/tevino/abool" "github.com/tevino/abool"
) )
@@ -90,11 +89,7 @@ func (p *Process) Delete() {
go dbController.PushUpdate(p) go dbController.PushUpdate(p)
} }
// deactivate profile // TODO: maybe mark the assigned profiles as no longer needed?
// TODO: check if there is another process using the same profile set
if p.profileSet != nil {
profile.DeactivateProfileSet(p.profileSet)
}
} }
// CleanProcessStorage cleans the storage from old processes. // 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. // GetProcessByPacket returns the process that owns the given packet.
func GetProcessByPacket(pkt packet.Packet) (process *Process, direction bool, err error) { 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") log.Tracer(pkt.Ctx()).Tracef("process: getting process and profile by packet")
var pid int var pid int
@@ -75,10 +80,9 @@ func GetProcessByPacket(pkt packet.Packet) (process *Process, direction bool, er
return nil, direction, err return nil, direction, err
} }
err = process.FindProfiles(pkt.Ctx()) err = process.GetProfile(pkt.Ctx())
if err != nil { if err != nil {
log.Tracer(pkt.Ctx()).Errorf("process: 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)
log.Errorf("failed to find profiles for process %s: %s", process, err)
} }
return process, direction, nil 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. // 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) { 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") log.Tracer(ctx).Tracef("process: getting process and profile by endpoints")
var pid int var pid int
@@ -129,10 +138,9 @@ func GetProcessByEndpoints(ctx context.Context, localIP net.IP, localPort uint16
return nil, err return nil, err
} }
err = process.FindProfiles(ctx) err = process.GetProfile(ctx)
if err != nil { if err != nil {
log.Tracer(ctx).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)
log.Errorf("process: failed to find profiles for process %s: %s", process, err)
} }
return process, nil 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. // 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() pidsByUserLock.Lock()
defer pidsByUserLock.Unlock() defer pidsByUserLock.Unlock()

View File

@@ -40,10 +40,10 @@ type Process struct {
// ExecOwner ... // ExecOwner ...
// ExecSignature ... // ExecSignature ...
UserProfileKey string LocalProfileKey string
profileSet *profile.Set profile *profile.LayeredProfile
Name string Name string
Icon 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. // 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 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. 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. // Profile returns the assigned layered profile.
func (p *Process) ProfileSet() *profile.Set { func (p *Process) Profile() *profile.LayeredProfile {
p.Lock() p.Lock()
defer p.Unlock() defer p.Unlock()
return p.profileSet return p.profile
} }
// Strings returns a string representation of process. // 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()) { func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) {
dupReqLock.Lock() dupReqLock.Lock()
defer dupReqLock.Unlock()
// get duplicate request waitgroup // get duplicate request waitgroup
wg, requestActive := dupReqMap[pid] wg, requestActive := dupReqMap[pid]
// someone else is already on it! // someone else is already on it!
if requestActive { if requestActive {
dupReqLock.Unlock()
// log that we are waiting // log that we are waiting
log.Tracer(ctx).Tracef("intel: waiting for duplicate request for PID %d to complete", pid) log.Tracer(ctx).Tracef("intel: waiting for duplicate request for PID %d to complete", pid)
// wait // wait
@@ -232,6 +233,8 @@ func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) {
// add to registry // add to registry
dupReqMap[pid] = wg dupReqMap[pid] = wg
dupReqLock.Unlock()
// return function to mark request as finished // return function to mark request as finished
return func() { return func() {
dupReqLock.Lock() 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 package profile
import ( import (
"context"
"sync" "sync"
"github.com/safing/portbase/log"
) )
var ( var (
activeProfileSets = make(map[string]*Set) // TODO: periodically clean up inactive profiles
activeProfileSetsLock sync.RWMutex activeProfiles = make(map[string]*Profile)
activeProfilesLock sync.RWMutex
) )
func activateProfileSet(ctx context.Context, set *Set) { // getActiveProfile returns a cached copy of an active profile and nil if it isn't found.
activeProfileSetsLock.Lock() func getActiveProfile(scopedID string) *Profile {
defer activeProfileSetsLock.Unlock() activeProfilesLock.Lock()
set.Lock() defer activeProfilesLock.Unlock()
defer set.Unlock()
activeProfileSets[set.id] = set
log.Tracer(ctx).Tracef("profile: activated profile set %s", set.id)
}
// DeactivateProfileSet marks a profile set as not active. profile, ok := activeProfiles[scopedID]
func DeactivateProfileSet(set *Set) { if ok {
activeProfileSetsLock.Lock() return profile
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()
} }
if profilesUpdated { return nil
increaseUpdateVersion() }
// 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 package profile
import ( import (
"context"
"errors"
"strings"
"github.com/safing/portbase/database" "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 // Database paths:
// core:profiles/special/default // core:profiles/<scope>/<id>
// /global // cache:profiles/index/<identifier>/<value>
// core:profiles/stamp/12334-1235-1234-5123-1234
// core:profiles/identifier/base64
// Namespaces
const ( const (
UserNamespace = "user" profilesDBPath = "core:profiles/"
StampNamespace = "stamp"
SpecialNamespace = "special"
) )
var ( var (
profileDB = database.NewInterface(nil) 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 package profile
import "time"
var ( var (
fingerprintWeights = map[string]int{ fingerprintWeights = map[string]int{
"full_path": 2, "full_path": 2,
@@ -35,6 +33,8 @@ func GetFingerprintWeight(fpType string) (weight int) {
return 0 return 0
} }
// TODO: move to profile
/*
// AddFingerprint adds the given fingerprint to the profile. // AddFingerprint adds the given fingerprint to the profile.
func (profile *Profile) AddFingerprint(fp *Fingerprint) { func (profile *Profile) AddFingerprint(fp *Fingerprint) {
if fp.OS == "" { if fp.OS == "" {
@@ -46,3 +46,4 @@ func (profile *Profile) AddFingerprint(fp *Fingerprint) {
profile.Fingerprints = append(profile.Fingerprints, fp) 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 package profile
import ( import (
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
// module dependencies // module dependencies
@@ -8,22 +10,42 @@ import (
) )
var ( var (
shutdownSignal = make(chan struct{}) module *modules.Module
) )
func init() { func init() {
modules.Register("profile", nil, start, stop, "core") module = modules.Register("profiles", prep, start, nil, "core")
} }
func start() error { func prep() error {
err := initSpecialProfiles() err := registerConfiguration()
if err != nil {
return err
}
err = registerConfigUpdater()
if err != nil { if err != nil {
return err return err
} }
return initUpdateListener()
}
func stop() error { return nil
close(shutdownSignal) }
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 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 package profile
import ( import (
"errors"
"fmt" "fmt"
"sync" "sync"
"time" "time"
"github.com/safing/portbase/log"
"github.com/tevino/abool"
uuid "github.com/satori/go.uuid" uuid "github.com/satori/go.uuid"
"github.com/safing/portbase/config"
"github.com/safing/portbase/database/record" "github.com/safing/portbase/database/record"
"github.com/safing/portmaster/status" "github.com/safing/portmaster/profile/endpoints"
) )
var ( var (
lastUsedUpdateThreshold = 1 * time.Hour 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. // Profile is used to predefine a security profile for applications.
type Profile struct { type Profile struct { //nolint:maligned // not worth the effort
record.Base record.Base
sync.Mutex sync.Mutex
// Profile Metadata // Identity
ID string ID string
Source string
// App Information
Name string Name string
Description string Description string
Homepage 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 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 Icon string
// User Profile Only // References - local profiles only
LinkedPath string // LinkedPath is a filesystem path to the executable this profile was created for.
StampProfileID string LinkedPath string
StampProfileAssigned int64 // LinkedProfiles is a list of other profiles
LinkedProfiles []string
// Fingerprints // Fingerprints
Fingerprints []*Fingerprint // TODO: Fingerprints []*Fingerprint
// Configuration
// The mininum security level to apply to connections made with this profile // The mininum security level to apply to connections made with this profile
SecurityLevel uint8 SecurityLevel uint8
Flags Flags Config map[string]interface{}
Endpoints Endpoints
ServiceEndpoints Endpoints
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process must be found // Interpreted Data
// Framework *Framework `json:",omitempty bson:",omitempty"` 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) // Lifecycle Management
Created int64 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 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. // New returns a new Profile.
func New() *Profile { func New() *Profile {
return &Profile{ profile := &Profile{
ID: uuid.NewV4().String(),
Source: SourceLocal,
Created: time.Now().Unix(), 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. // ScopedID returns the scoped ID (Source + ID) of the profile.
func MakeProfileKey(namespace, id string) string { func (profile *Profile) ScopedID() string {
return fmt.Sprintf("core:profiles/%s/%s", namespace, id) return makeScopedID(profile.Source, profile.ID)
} }
// Save saves the profile to the database // Save saves the profile to the database
func (profile *Profile) Save(namespace string) error { func (profile *Profile) Save() error {
if profile.ID == "" { 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 !profile.KeyIsSet() {
if namespace == "" { profile.SetKey(makeProfileKey(profile.Source, profile.ID))
return fmt.Errorf("no key or namespace defined for profile %s", profile.String())
}
profile.SetKey(MakeProfileKey(namespace, profile.ID))
} }
return profileDB.Put(profile) return profileDB.Put(profile)
@@ -92,27 +194,86 @@ func (profile *Profile) String() string {
return profile.Name return profile.Name
} }
// DetailedString returns a more detailed string representation of theProfile. // AddEndpoint adds an endpoint to the endpoint list, saves the profile and reloads the configuration.
func (profile *Profile) DetailedString() string { func (profile *Profile) AddEndpoint(newEntry string) {
return fmt.Sprintf("%s(SL=%s Flags=%s Endpoints=%s)", profile.Name, status.FmtSecurityLevel(profile.SecurityLevel), profile.Flags.String(), profile.Endpoints.String()) profile.addEndpointyEntry(CfgOptionEndpointsKey, newEntry)
} }
// GetUserProfile loads a profile from the database. // AddServiceEndpoint adds a service endpoint to the endpoint list, saves the profile and reloads the configuration.
func GetUserProfile(id string) (*Profile, error) { func (profile *Profile) AddServiceEndpoint(newEntry string) {
return getProfile(UserNamespace, id) profile.addEndpointyEntry(CfgOptionServiceEndpointsKey, newEntry)
} }
// GetStampProfile loads a profile from the database. func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) {
func GetStampProfile(id string) (*Profile, error) { profile.Lock()
return getProfile(StampNamespace, id) // 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) { // GetProfile loads a profile from the database.
r, err := profileDB.Get(MakeProfileKey(namespace, id)) 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 { if err != nil {
return nil, err 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. // 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 ( import (
"crypto/tls" "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 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 ( import (
"fmt" "fmt"

View File

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

View File

@@ -1,4 +1,4 @@
package intel package resolver
import ( import (
"context" "context"
@@ -6,6 +6,7 @@ import (
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
"github.com/safing/portmaster/intel"
// module dependencies // module dependencies
_ "github.com/safing/portmaster/core" _ "github.com/safing/portmaster/core"
@@ -16,10 +17,12 @@ var (
) )
func init() { func init() {
module = modules.Register("intel", prep, start, nil, "core", "network") module = modules.Register("resolver", prep, start, nil, "core", "network")
} }
func prep() error { func prep() error {
intel.SetReverseResolver(ResolveIPAndValidate)
return prepConfig() 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 ( import (
"context" "context"
@@ -313,7 +313,7 @@ func listenForDNSPackets(conn *net.UDPConn, messages chan *dns.Msg) error {
for { for {
n, err := conn.Read(buf) n, err := conn.Read(buf)
if err != nil { if err != nil {
if module.ShutdownInProgress() { if module.IsStopping() {
return nil return nil
} }
log.Debugf("intel: failed to read packet: %s", err) log.Debugf("intel: failed to read packet: %s", err)

View File

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

View File

@@ -1,4 +1,4 @@
package intel package resolver
import ( import (
"context" "context"
@@ -158,13 +158,14 @@ func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
dupKey := fmt.Sprintf("%s%s", q.FQDN, q.QType.String()) dupKey := fmt.Sprintf("%s%s", q.FQDN, q.QType.String())
dupReqLock.Lock() dupReqLock.Lock()
defer dupReqLock.Unlock()
// get duplicate request waitgroup // get duplicate request waitgroup
wg, requestActive := dupReqMap[dupKey] wg, requestActive := dupReqMap[dupKey]
// someone else is already on it! // someone else is already on it!
if requestActive { if requestActive {
dupReqLock.Unlock()
// log that we are waiting // log that we are waiting
log.Tracer(ctx).Tracef("intel: waiting for duplicate query for %s to complete", dupKey) log.Tracer(ctx).Tracef("intel: waiting for duplicate query for %s to complete", dupKey)
// wait // wait
@@ -182,6 +183,8 @@ func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
// add to registry // add to registry
dupReqMap[dupKey] = wg dupReqMap[dupKey] = wg
dupReqLock.Unlock()
// return function to mark request as finished // return function to mark request as finished
return func() { return func() {
dupReqLock.Lock() dupReqLock.Lock()
@@ -222,7 +225,7 @@ resolveLoop:
rrCache, err = resolver.Conn.Query(ctx, q) rrCache, err = resolver.Conn.Query(ctx, q)
if err != nil { if err != nil {
// FIXME: check if we are online? // TODO: check if we are online?
switch { switch {
case errors.Is(err, ErrNotFound): 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 // 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 ( import (
"context" "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. // 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() resolversLock.RLock()
defer resolversLock.RUnlock() defer resolversLock.RUnlock()
@@ -226,13 +226,13 @@ func (q *Query) checkCompliance() error {
} }
// special TLDs // special TLDs
if doNotResolveSpecialDomains(q.SecurityLevel) && if dontResolveSpecialDomains(q.SecurityLevel) &&
domainInScope(q.dotPrefixedFQDN, specialServiceScopes) { domainInScope(q.dotPrefixedFQDN, specialServiceScopes) {
return ErrSpecialDomainsDisabled return ErrSpecialDomainsDisabled
} }
// testing TLDs // testing TLDs
if doNotResolveTestDomains(q.SecurityLevel) && if dontResolveTestDomains(q.SecurityLevel) &&
domainInScope(q.dotPrefixedFQDN, localTestScopes) { domainInScope(q.dotPrefixedFQDN, localTestScopes) {
return ErrTestDomainsDisabled return ErrTestDomainsDisabled
} }
@@ -245,7 +245,7 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
return errSkip return errSkip
} }
if doNotUseInsecureProtocols(q.SecurityLevel) { if noInsecureProtocols(q.SecurityLevel) {
switch resolver.ServerType { switch resolver.ServerType {
case ServerTypeDNS: case ServerTypeDNS:
return errInsecureProtocol 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 { if resolver.Source == ServerSourceAssigned {
return errAssignedServer return errAssignedServer
} }
} }
if doNotUseMulticastDNS(q.SecurityLevel) { if noMulticastDNS(q.SecurityLevel) {
if resolver.Source == ServerSourceMDNS { if resolver.Source == ServerSourceMDNS {
return errMulticastDNS 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 ( import (
"errors" "errors"

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