Merge pull request #22 from safing/feature/config-consolidation
Config consolidation
This commit is contained in:
@@ -7,4 +7,15 @@ linters:
|
||||
- funlen
|
||||
- whitespace
|
||||
- wsl
|
||||
- godox
|
||||
- gomnd
|
||||
|
||||
linters-settings:
|
||||
godox:
|
||||
# report any comments starting with keywords, this is useful for TODO or FIXME comments that
|
||||
# might be left in the code accidentally and should be resolved before merging
|
||||
keywords:
|
||||
- FIXME
|
||||
# gocognit:
|
||||
# min-complexity: 50
|
||||
# gocyclo:
|
||||
# min-complexity: 50
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.x
|
||||
|
||||
os:
|
||||
- linux
|
||||
- windows
|
||||
|
||||
143
Gopkg.lock
generated
143
Gopkg.lock
generated
@@ -2,137 +2,118 @@
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e92f5581902c345eb4ceffdcd4a854fb8f73cf436d47d837d1ec98ef1fe0a214"
|
||||
digest = "1:f82b8ac36058904227087141017bb82f4b0fc58272990a4cdae3e2d6d222644e"
|
||||
name = "github.com/StackExchange/wmi"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "5d049714c4a64225c3c79a7cf7d02f7fb5b96338"
|
||||
version = "1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:90afd0cfdffcc3df7855160ee2954cbca286e23c4eb9cb3d075536c9e4e1b04f"
|
||||
digest = "1:3c753679736345f50125ae993e0a2614da126859921ea7faeecda6d217501ce2"
|
||||
name = "github.com/agext/levenshtein"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "0ded9c86537917af2ff89bc9c78de6bd58477894"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e097b70eeb55d10325f576ec2f97221d8ae18a3569cb0bc404863bde944788b8"
|
||||
name = "github.com/cloudflare/cfssl"
|
||||
packages = [
|
||||
"crypto/pkcs7",
|
||||
"errors",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "768cd563887febaad559b511aaa5964823ccb4ab"
|
||||
version = "1.3.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2.1"
|
||||
digest = "1:1a2c0956b86c464f8ee91a46e0947dac7251f1d6fb7d3ad89cb9b376273ece82"
|
||||
digest = "1:3fc5d0d9cb474736e8e6c2f2292e0763b5132c6e7d8cbedf7bde404a470c8c3b"
|
||||
name = "github.com/cookieo9/resources-go"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "d27c04069d0d5dfe11c202dacbf745ae8d1ab181"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3b4fcce8036672adc21c09d8e288f7b7590e76a468fae5a13346366e778e5ff8"
|
||||
digest = "1:166e24c91c2732657d2f791d3ee3897e7d85ece7cbb62ad991250e6b51fc1d4c"
|
||||
name = "github.com/coreos/go-iptables"
|
||||
packages = ["iptables"]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "78b5fff24e6df8886ef8eca9411f683a884349a5"
|
||||
version = "v0.4.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:440028f55cb322d8cb5b9d5ebec298a00b7d74690a658fe6b1c0c0b44341bfae"
|
||||
digest = "1:b6581f9180e0f2d5549280d71819ab951db9d511478c87daca95669589d505c0"
|
||||
name = "github.com/go-ole/go-ole"
|
||||
packages = [
|
||||
".",
|
||||
"oleutil",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "97b6244175ae18ea6eef668034fd6565847501c9"
|
||||
version = "v1.2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b3d20bcdedab2050e6bc58e52f4fdc46f710b4c74e1a1ecee262ebec1aee7b6e"
|
||||
digest = "1:cc1255e2fef3819bfab3540277001e602892dd431ef9ab5499bcdbc425923d64"
|
||||
name = "github.com/godbus/dbus"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "2ff6f7ffd60f0f2410b3105864bdd12c7894f844"
|
||||
version = "v5.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0d40612c9acf7173aa742ee335bf344524add9c8d7e818992b610f3982350101"
|
||||
digest = "1:e85e59c4152d8576341daf54f40d96c404c264e04941a4a36b97a0f427eb9e5e"
|
||||
name = "github.com/google/gopacket"
|
||||
packages = [
|
||||
".",
|
||||
"layers",
|
||||
"tcpassembly",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "6d3e2615da4ed2ed2a349918fe74e7e6d03482fa"
|
||||
version = "v1.1.17"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c3388642e07731a240e14f4bc7207df59cfcc009447c657b9de87fec072d07e3"
|
||||
digest = "1:20dc576ad8f98fe64777c62f090a9b37dd67c62b23fe42b429c2c41936aa8a9c"
|
||||
name = "github.com/google/renameio"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "f0e32980c006571efd537032e5f9cd8c1a92819e"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:88e0b0baeb9072f0a4afbcf12dda615fc8be001d1802357538591155998da21b"
|
||||
name = "github.com/hashicorp/go-version"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ac23dc3fea5d1a983c43f6a0f6e2c13f0195d8bd"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f433da6fbabb0bd1bd730fd340782015c644123d02eb78c20c69ce03352daad1"
|
||||
digest = "1:e71cc6b377264002aec0d9c235087e51ad7a3c1fb341bb4baa84709308b94fe8"
|
||||
name = "github.com/kardianos/osext"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "2bc1f35cddc0cc527b4bc3dce8578fc2a6c11384"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:66def2f80d5127123c81ffa876242c12d3b7b06188f6645d79b3cc4acba637b3"
|
||||
digest = "1:0b6694f306890ddbb69c96a16776510bd24e07436fae3f9b0a4e5b650f1e6fb7"
|
||||
name = "github.com/miekg/dns"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "b13675009d59c97f3721247d9efa8914e1866a5b"
|
||||
version = "v1.1.15"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8186cc518f5dcea736d7725e284632937971fdf29fa2953fdfa53f631e21a799"
|
||||
digest = "1:3819cd861b7abd7d12dc1ea52ecb998ad1171826a76ecf0aefa09545781091f9"
|
||||
name = "github.com/oschwald/maxminddb-golang"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "2905694a1b00c5574f1418a7dbf8a22a7d247559"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:274f67cb6fed9588ea2521ecdac05a6d62a8c51c074c1fccc6a49a40ba80e925"
|
||||
digest = "1:7f569d906bdd20d906b606415b7d794f798f91a62fcfb6a4daa6d50690fb7a3f"
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:36ff5e7a8aa3e7d01b9ca22d441484d9c43247b6e34e379c404d17a2e0098091"
|
||||
digest = "1:8bf42eb2ded52ed2678b0716dbfbf30628765bc12b13222c4d5669ba4c1310e4"
|
||||
name = "github.com/shirou/gopsutil"
|
||||
packages = [
|
||||
"cpu",
|
||||
@@ -141,7 +122,7 @@
|
||||
"net",
|
||||
"process",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "4c8b404ee5c53b04b04f34b1744a26bf5d2910de"
|
||||
version = "v2.19.6"
|
||||
|
||||
@@ -150,79 +131,74 @@
|
||||
digest = "1:99c6a6dab47067c9b898e8c8b13d130c6ab4ffbcc4b7cc6236c2cd0b1e344f5b"
|
||||
name = "github.com/shirou/w32"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e096613fb7cf34743d49af87d197663cfccd61876e2219853005a57baedfa562"
|
||||
digest = "1:0c63b3c7ad6d825a898f28cb854252a3b29d37700c68a117a977263f5ec94efe"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "f2b07da1e2c38d5f12845a4f607e2e1018cbb1f5"
|
||||
version = "v0.0.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2"
|
||||
digest = "1:cbaf13cdbfef0e4734ed8a7504f57fe893d471d62a35b982bf6fb3f036449a66"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
|
||||
version = "v1.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:93d6687fc19da8a35c7352d72117a6acd2072dfb7e9bfd65646227bf2a913b2a"
|
||||
digest = "1:86e6712cfd4070a2120c03fcec41cfcbbc51813504a74e28d74479edfaf669ee"
|
||||
name = "github.com/tevino/abool"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "9b9efcf221b50905aab9bbabd3daed56dc10f339"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:2a9f7ca80c3035b7e68a927db100922d4a266a31cea2568d9a5185a90b469279"
|
||||
digest = "1:21097653bd7914de1262f2429e277933507442f892815a791ce1c0dbf0a8dc20"
|
||||
name = "github.com/umahmood/haversine"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "808ab04add26660fd241ddb7973886c6dd6669e8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:bf3f59dec5aa79945bd4b45c0c649c99d03609565acdd6a087f4cd6e9e5b5544"
|
||||
digest = "1:086760278d762dbb0e9a26e09b57f04c89178c86467d8d94fae47d64c222f328"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"chacha20poly1305",
|
||||
"curve25519",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"ocsp",
|
||||
"poly1305",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "4def268fd1a49955bfb3dda92fe3db4f924f2285"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:caffb9a4f8c756941de4b3eb577abd167e7fd4b570f2078c05ceb8835a1514cb"
|
||||
digest = "1:31cd6e3c114e17c5f0c9e8b0bcaa3025ab3c221ce36323c7ce1acaa753d0d0aa"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
"icmp",
|
||||
"idna",
|
||||
"internal/iana",
|
||||
"internal/socket",
|
||||
"ipv4",
|
||||
"ipv6",
|
||||
"publicsuffix",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "da137c7871d730100384dbcf36e6f8fa493aef5b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:84945c0665ea5fc3ccbd067c35890a7d28e369131ac411b8a820b40115245c19"
|
||||
digest = "1:2579a16d8afda9c9a475808c13324f5e572852e8927905ffa15bb14e71baba4f"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"cpu",
|
||||
"unix",
|
||||
"windows",
|
||||
"windows/registry",
|
||||
@@ -231,15 +207,39 @@
|
||||
"windows/svc/eventlog",
|
||||
"windows/svc/mgr",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
pruneopts = ""
|
||||
revision = "04f50cda93cbb67f2afa353c52f342100e80e625"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:740b51a55815493a8d0f2b1e0d0ae48fe48953bf7eaf3fcc4198823bf67768c0"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/language",
|
||||
"internal/language/compact",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
|
||||
version = "v0.3.2"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/agext/levenshtein",
|
||||
"github.com/cloudflare/cfssl/crypto/pkcs7",
|
||||
"github.com/cookieo9/resources-go",
|
||||
"github.com/coreos/go-iptables/iptables",
|
||||
"github.com/godbus/dbus",
|
||||
@@ -247,7 +247,6 @@
|
||||
"github.com/google/gopacket/layers",
|
||||
"github.com/google/gopacket/tcpassembly",
|
||||
"github.com/google/renameio",
|
||||
"github.com/hashicorp/go-version",
|
||||
"github.com/miekg/dns",
|
||||
"github.com/oschwald/maxminddb-golang",
|
||||
"github.com/satori/go.uuid",
|
||||
@@ -255,11 +254,9 @@
|
||||
"github.com/spf13/cobra",
|
||||
"github.com/tevino/abool",
|
||||
"github.com/umahmood/haversine",
|
||||
"golang.org/x/crypto/chacha20poly1305",
|
||||
"golang.org/x/crypto/curve25519",
|
||||
"golang.org/x/crypto/ocsp",
|
||||
"golang.org/x/net/icmp",
|
||||
"golang.org/x/net/ipv4",
|
||||
"golang.org/x/net/publicsuffix",
|
||||
"golang.org/x/sys/windows",
|
||||
"golang.org/x/sys/windows/svc",
|
||||
"golang.org/x/sys/windows/svc/debug",
|
||||
|
||||
76
Gopkg.toml
76
Gopkg.toml
@@ -25,79 +25,3 @@
|
||||
# unused-packages = true
|
||||
|
||||
ignored = ["github.com/safing/portbase/*"]
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/agext/levenshtein"
|
||||
version = "1.2.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/cloudflare/cfssl"
|
||||
version = "1.3.3"
|
||||
|
||||
[[constraint]]
|
||||
branch = "v2.1"
|
||||
name = "github.com/cookieo9/resources-go"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coreos/go-iptables"
|
||||
version = "0.4.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/godbus/dbus"
|
||||
version = "5.0.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/google/gopacket"
|
||||
version = "1.1.17"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/google/renameio"
|
||||
version = "0.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/miekg/dns"
|
||||
version = "1.1.15"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/oschwald/maxminddb-golang"
|
||||
version = "1.3.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/shirou/gopsutil"
|
||||
version = "2.19.6"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/cobra"
|
||||
version = "0.0.5"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/tevino/abool"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/umahmood/haversine"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/hashicorp/go-version"
|
||||
version = "1.2.0"
|
||||
|
||||
64
core/base.go
64
core/base.go
@@ -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
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
CfgDevModeKey = "core/devMode"
|
||||
defaultDevMode bool
|
||||
)
|
||||
|
||||
@@ -24,7 +25,7 @@ func logFlagOverrides() {
|
||||
func registerConfig() error {
|
||||
err := config.Register(&config.Option{
|
||||
Name: "Development Mode",
|
||||
Key: "core/devMode",
|
||||
Key: CfgDevModeKey,
|
||||
Description: "In Development Mode security restrictions are lifted/softened to enable easier access to Portmaster for debugging and testing purposes.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||
|
||||
18
core/core.go
18
core/core.go
@@ -3,14 +3,28 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
|
||||
"github.com/safing/portbase/modules"
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register("core", nil, startCore, nil, "base", "database", "config", "api", "random")
|
||||
module = modules.Register("core", nil, start, nil, "database", "config", "api", "random", "notifications", "subsystems", "ui", "updates", "status")
|
||||
subsystems.Register(
|
||||
"core",
|
||||
"Core",
|
||||
"Base Structure and System Integration",
|
||||
module,
|
||||
"config:core/",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func startCore() error {
|
||||
func start() error {
|
||||
if err := startPlatformSpecific(); err != nil {
|
||||
return fmt.Errorf("failed to start plattform-specific components: %s", err)
|
||||
}
|
||||
|
||||
@@ -3,15 +3,22 @@ package core
|
||||
import (
|
||||
"github.com/safing/portbase/database"
|
||||
|
||||
// database module
|
||||
_ "github.com/safing/portbase/database/dbmodule"
|
||||
|
||||
// module dependencies
|
||||
_ "github.com/safing/portbase/database/storage/bbolt"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultDatabaseStorageType = "bbolt"
|
||||
)
|
||||
|
||||
func registerDatabases() error {
|
||||
_, err := database.Register(&database.Database{
|
||||
Name: "core",
|
||||
Description: "Holds core data, such as settings and profiles",
|
||||
StorageType: "bbolt",
|
||||
StorageType: DefaultDatabaseStorageType,
|
||||
PrimaryAPI: "",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -21,7 +28,7 @@ func registerDatabases() error {
|
||||
_, err = database.Register(&database.Database{
|
||||
Name: "cache",
|
||||
Description: "Cached data, such as Intelligence and DNS Records",
|
||||
StorageType: "bbolt",
|
||||
StorageType: DefaultDatabaseStorageType,
|
||||
PrimaryAPI: "",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -31,7 +38,7 @@ func registerDatabases() error {
|
||||
// _, err = database.Register(&database.Database{
|
||||
// Name: "history",
|
||||
// Description: "Historic event data",
|
||||
// StorageType: "bbolt",
|
||||
// StorageType: DefaultDatabaseStorageType,
|
||||
// PrimaryAPI: "",
|
||||
// })
|
||||
// if err != nil {
|
||||
|
||||
65
core/global.go
Normal file
65
core/global.go
Normal 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
81
core/pmtesting/testing.go
Normal 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 ===")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -9,15 +9,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portmaster/core/structure"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/log"
|
||||
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portmaster/network/packet"
|
||||
"github.com/safing/portmaster/process"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -28,7 +25,7 @@ var (
|
||||
)
|
||||
|
||||
func prepAPIAuth() error {
|
||||
dataRoot = structure.Root()
|
||||
dataRoot = dataroot.Root()
|
||||
return api.SetAuthenticator(apiAuthenticator)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,16 @@ package firewall
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portmaster/status"
|
||||
)
|
||||
|
||||
var (
|
||||
permanentVerdicts config.BoolOption
|
||||
filterDNSByScope status.SecurityLevelOption
|
||||
filterDNSByProfile status.SecurityLevelOption
|
||||
promptTimeout config.IntOption
|
||||
CfgOptionEnableFilterKey = "filter/enable"
|
||||
|
||||
CfgOptionPermanentVerdictsKey = "filter/permanentVerdicts"
|
||||
permanentVerdicts config.BoolOption
|
||||
|
||||
CfgOptionPromptTimeoutKey = "filter/promptTimeout"
|
||||
promptTimeout config.IntOption
|
||||
|
||||
devMode config.BoolOption
|
||||
apiListenAddress config.StringOption
|
||||
@@ -18,7 +20,7 @@ var (
|
||||
func registerConfig() error {
|
||||
err := config.Register(&config.Option{
|
||||
Name: "Permanent Verdicts",
|
||||
Key: "firewall/permanentVerdicts",
|
||||
Key: CfgOptionPermanentVerdictsKey,
|
||||
Description: "With permanent verdicts, control of a connection is fully handed back to the OS after the initial decision. This brings a great performance increase, but makes it impossible to change the decision of a link later on.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
@@ -28,43 +30,11 @@ func registerConfig() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
permanentVerdicts = config.Concurrent.GetAsBool("firewall/permanentVerdicts", true)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Filter DNS Responses by Server Scope",
|
||||
Key: "firewall/filterDNSByScope",
|
||||
Description: "This option will filter out DNS answers that are outside of the scope of the server. A server on the public Internet may not respond with a private LAN address.",
|
||||
OptType: config.OptTypeInt,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelBeta,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: 7,
|
||||
ValidationRegex: "^(7|6|4)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filterDNSByScope = status.ConfigIsActiveConcurrent("firewall/filterDNSByScope")
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Filter DNS Responses by Application Profile",
|
||||
Key: "firewall/filterDNSByProfile",
|
||||
Description: "This option will filter out DNS answers that an application would not be allowed to connect, based on its profile.",
|
||||
OptType: config.OptTypeInt,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelBeta,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: 7,
|
||||
ValidationRegex: "^(7|6|4)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filterDNSByProfile = status.ConfigIsActiveConcurrent("firewall/filterDNSByProfile")
|
||||
permanentVerdicts = config.Concurrent.GetAsBool(CfgOptionPermanentVerdictsKey, true)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Timeout for prompt notifications",
|
||||
Key: "firewall/promptTimeout",
|
||||
Key: CfgOptionPromptTimeoutKey,
|
||||
Description: "Amount of time how long Portmaster will wait for a response when prompting about a connection via a notification. In seconds.",
|
||||
OptType: config.OptTypeInt,
|
||||
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||
@@ -74,9 +44,9 @@ func registerConfig() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
promptTimeout = config.Concurrent.GetAsInt("firewall/promptTimeout", 30)
|
||||
promptTimeout = config.Concurrent.GetAsInt(CfgOptionPromptTimeoutKey, 60)
|
||||
|
||||
devMode = config.Concurrent.GetAsBool("firewall/permanentVerdicts", false)
|
||||
devMode = config.Concurrent.GetAsBool("core/devMode", false)
|
||||
apiListenAddress = config.GetAsString("api/listenAddress", "")
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
"github.com/safing/portmaster/network/environment"
|
||||
"github.com/safing/portmaster/resolver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
intel.SetLocalAddrFactory(PermittedAddr)
|
||||
resolver.SetLocalAddrFactory(PermittedAddr)
|
||||
environment.SetLocalAddrFactory(PermittedAddr)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/firewall/inspection"
|
||||
@@ -41,11 +44,26 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("firewall", prep, start, stop, "core", "network", "nameserver", "profile", "updates")
|
||||
module = modules.Register("firewall", prep, start, stop, "core", "network", "resolver", "intel", "processes")
|
||||
subsystems.Register(
|
||||
"filter",
|
||||
"Privacy Filter",
|
||||
"DNS and Network Filter",
|
||||
module,
|
||||
"config:filter/",
|
||||
&config.Option{
|
||||
Name: "Enable Privacy Filter",
|
||||
Key: CfgOptionEnableFilterKey,
|
||||
Description: "Enable the Privacy Filter Subsystem to filter DNS queries and network requests.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||
ReleaseLevel: config.ReleaseLevelBeta,
|
||||
DefaultValue: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func prep() (err error) {
|
||||
|
||||
err = registerConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -188,22 +206,18 @@ func handlePacket(pkt packet.Packet) {
|
||||
|
||||
// associate packet to link and handle
|
||||
link, created := network.GetOrCreateLinkByPacket(pkt)
|
||||
defer func() {
|
||||
go link.SaveIfNeeded()
|
||||
}()
|
||||
if created {
|
||||
link.SetFirewallHandler(initialHandler)
|
||||
link.HandlePacket(pkt)
|
||||
return
|
||||
}
|
||||
if link.FirewallHandlerIsSet() {
|
||||
link.HandlePacket(pkt)
|
||||
return
|
||||
}
|
||||
issueVerdict(pkt, link, 0, true)
|
||||
|
||||
link.HandlePacket(pkt)
|
||||
}
|
||||
|
||||
func initialHandler(pkt packet.Packet, link *network.Link) {
|
||||
defer func() {
|
||||
go link.SaveIfNeeded()
|
||||
}()
|
||||
|
||||
log.Tracer(pkt.Ctx()).Trace("firewall: [initial handler]")
|
||||
|
||||
// check for internal firewall bypass
|
||||
@@ -217,9 +231,6 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
|
||||
} else {
|
||||
comm.AddLink(link)
|
||||
}
|
||||
defer func() {
|
||||
go comm.SaveIfNeeded()
|
||||
}()
|
||||
|
||||
// approve
|
||||
link.Accept("internally approved")
|
||||
@@ -249,9 +260,6 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
|
||||
return
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
go comm.SaveIfNeeded()
|
||||
}()
|
||||
|
||||
// add new Link to Communication (and save both)
|
||||
comm.AddLink(link)
|
||||
@@ -267,7 +275,8 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
|
||||
|
||||
log.Tracer(pkt.Ctx()).Trace("firewall: starting decision process")
|
||||
|
||||
DecideOnCommunication(comm, pkt)
|
||||
// TODO: filter lists may have IPs in the future!
|
||||
DecideOnCommunication(comm)
|
||||
DecideOnLink(comm, link, pkt)
|
||||
|
||||
// TODO: link this to real status
|
||||
@@ -380,7 +389,7 @@ func issueVerdict(pkt packet.Packet, link *network.Link, verdict network.Verdict
|
||||
func run() {
|
||||
for {
|
||||
select {
|
||||
case <-modules.ShuttingDown():
|
||||
case <-module.Stopping():
|
||||
return
|
||||
case pkt := <-interception.Packets:
|
||||
handlePacket(pkt)
|
||||
@@ -391,7 +400,7 @@ func run() {
|
||||
func statLogger() {
|
||||
for {
|
||||
select {
|
||||
case <-modules.ShuttingDown():
|
||||
case <-module.Stopping():
|
||||
return
|
||||
case <-time.After(10 * time.Second):
|
||||
log.Tracef("firewall: packets accepted %d, blocked %d, dropped %d", atomic.LoadUint64(packetsAccepted), atomic.LoadUint64(packetsBlocked), atomic.LoadUint64(packetsDropped))
|
||||
|
||||
@@ -4,19 +4,20 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/intel"
|
||||
"github.com/safing/portmaster/network"
|
||||
"github.com/safing/portmaster/network/netutils"
|
||||
"github.com/safing/portmaster/network/packet"
|
||||
"github.com/safing/portmaster/process"
|
||||
"github.com/safing/portmaster/profile"
|
||||
"github.com/safing/portmaster/status"
|
||||
"github.com/safing/portmaster/profile/endpoints"
|
||||
"github.com/safing/portmaster/resolver"
|
||||
|
||||
"github.com/agext/levenshtein"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Call order:
|
||||
@@ -30,11 +31,10 @@ import (
|
||||
// 4. DecideOnLink
|
||||
// is called when when the first packet of a link arrives only if communication has verdict UNDECIDED or CANTSAY
|
||||
|
||||
// DecideOnCommunicationBeforeIntel makes a decision about a communication before the dns query is resolved and intel is gathered.
|
||||
func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) {
|
||||
|
||||
// check if communication needs reevaluation
|
||||
if comm.NeedsReevaluation() {
|
||||
// DecideOnCommunicationBeforeDNS makes a decision about a communication before the dns query is resolved and intel is gathered.
|
||||
func DecideOnCommunicationBeforeDNS(comm *network.Communication) {
|
||||
// update profiles and check if communication needs reevaluation
|
||||
if comm.UpdateAndCheck() {
|
||||
log.Infof("firewall: re-evaluating verdict on %s", comm)
|
||||
comm.ResetVerdict()
|
||||
}
|
||||
@@ -51,116 +51,74 @@ func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string)
|
||||
return
|
||||
}
|
||||
|
||||
// get and check profile set
|
||||
profileSet := comm.Process().ProfileSet()
|
||||
if profileSet == nil {
|
||||
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
|
||||
comm.Deny("no Profile Set")
|
||||
return
|
||||
}
|
||||
profileSet.Update(status.ActiveSecurityLevel())
|
||||
// get profile
|
||||
p := comm.Process().Profile()
|
||||
|
||||
// check for any network access
|
||||
if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LAN) {
|
||||
if p.BlockScopeInternet() && p.BlockScopeLAN() {
|
||||
log.Infof("firewall: denying communication %s, accessing Internet or LAN not permitted", comm)
|
||||
comm.Deny("accessing Internet or LAN not permitted")
|
||||
return
|
||||
}
|
||||
// continueing with access to either Internet or LAN
|
||||
|
||||
// check endpoint list
|
||||
result, reason := profileSet.CheckEndpointDomain(fqdn)
|
||||
// FIXME: comm.Entity.Lock()
|
||||
result, reason := p.MatchEndpoint(comm.Entity)
|
||||
// FIXME: comm.Entity.Unlock()
|
||||
switch result {
|
||||
case profile.NoMatch:
|
||||
comm.UpdateVerdict(network.VerdictUndecided)
|
||||
if profileSet.GetProfileMode() == profile.Whitelist {
|
||||
log.Infof("firewall: denying communication %s, domain is not whitelisted", comm)
|
||||
comm.Deny("domain is not whitelisted")
|
||||
}
|
||||
case profile.Undeterminable:
|
||||
case endpoints.Undeterminable:
|
||||
comm.UpdateVerdict(network.VerdictUndeterminable)
|
||||
case profile.Denied:
|
||||
log.Infof("firewall: denying communication %s, endpoint is blacklisted: %s", comm, reason)
|
||||
comm.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason))
|
||||
case profile.Permitted:
|
||||
log.Infof("firewall: permitting communication %s, endpoint is whitelisted: %s", comm, reason)
|
||||
comm.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
|
||||
}
|
||||
}
|
||||
|
||||
// DecideOnCommunicationAfterIntel makes a decision about a communication after the dns query is resolved and intel is gathered.
|
||||
func DecideOnCommunicationAfterIntel(comm *network.Communication, fqdn string, rrCache *intel.RRCache) {
|
||||
// rrCache may be nil, when function is called for re-evaluation by DecideOnCommunication
|
||||
|
||||
// check if need to run
|
||||
if comm.GetVerdict() != network.VerdictUndecided {
|
||||
return
|
||||
case endpoints.Denied:
|
||||
log.Infof("firewall: denying communication %s, domain is blacklisted: %s", comm, reason)
|
||||
comm.Deny(fmt.Sprintf("domain is blacklisted: %s", reason))
|
||||
return
|
||||
case endpoints.Permitted:
|
||||
log.Infof("firewall: permitting communication %s, domain is whitelisted: %s", comm, reason)
|
||||
comm.Accept(fmt.Sprintf("domain is whitelisted: %s", reason))
|
||||
return
|
||||
}
|
||||
// continueing with result == NoMatch
|
||||
|
||||
// grant self - should not get here
|
||||
if comm.Process().Pid == os.Getpid() {
|
||||
log.Infof("firewall: granting own communication %s", comm)
|
||||
comm.Accept("")
|
||||
// check default action
|
||||
if p.DefaultAction() == profile.DefaultActionPermit {
|
||||
log.Infof("firewall: permitting communication %s, domain is not blacklisted (default=permit)", comm)
|
||||
comm.Accept("domain is not blacklisted (default=permit)")
|
||||
return
|
||||
}
|
||||
|
||||
// check if there is a profile
|
||||
profileSet := comm.Process().ProfileSet()
|
||||
if profileSet == nil {
|
||||
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
|
||||
comm.Deny("no Profile Set")
|
||||
return
|
||||
}
|
||||
profileSet.Update(status.ActiveSecurityLevel())
|
||||
|
||||
// TODO: Stamp integration
|
||||
|
||||
switch profileSet.GetProfileMode() {
|
||||
case profile.Whitelist:
|
||||
log.Infof("firewall: denying communication %s, domain is not whitelisted", comm)
|
||||
comm.Deny("domain is not whitelisted")
|
||||
return
|
||||
case profile.Blacklist:
|
||||
log.Infof("firewall: permitting communication %s, domain is not blacklisted", comm)
|
||||
comm.Accept("domain is not blacklisted")
|
||||
return
|
||||
}
|
||||
|
||||
// ProfileMode == Prompt
|
||||
|
||||
// check relation
|
||||
if profileSet.CheckFlag(profile.Related) {
|
||||
if checkRelation(comm, fqdn) {
|
||||
if !p.DisableAutoPermit() {
|
||||
if checkRelation(comm) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// prompt
|
||||
prompt(comm, nil, nil, fqdn)
|
||||
if p.DefaultAction() == profile.DefaultActionAsk {
|
||||
prompt(comm, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// DefaultAction == DefaultActionBlock
|
||||
log.Infof("firewall: denying communication %s, domain is not whitelisted (default=block)", comm)
|
||||
comm.Deny("domain is not whitelisted (default=block)")
|
||||
return
|
||||
}
|
||||
|
||||
// FilterDNSResponse filters a dns response according to the application profile and settings.
|
||||
//nolint:gocognit // FIXME
|
||||
func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *intel.RRCache) *intel.RRCache {
|
||||
func FilterDNSResponse(comm *network.Communication, q *resolver.Query, rrCache *resolver.RRCache) *resolver.RRCache { //nolint:gocognit // TODO
|
||||
// do not modify own queries - this should not happen anyway
|
||||
if comm.Process().Pid == os.Getpid() {
|
||||
return rrCache
|
||||
}
|
||||
|
||||
// check if there is a profile
|
||||
profileSet := comm.Process().ProfileSet()
|
||||
if profileSet == nil {
|
||||
log.Infof("firewall: blocking dns query of communication %s, no Profile Set", comm)
|
||||
return nil
|
||||
}
|
||||
profileSet.Update(status.ActiveSecurityLevel())
|
||||
|
||||
// save config for consistency during function call
|
||||
secLevel := profileSet.SecurityLevel()
|
||||
filterByScope := filterDNSByScope(secLevel)
|
||||
filterByProfile := filterDNSByProfile(secLevel)
|
||||
// get profile
|
||||
p := comm.Process().Profile()
|
||||
|
||||
// check if DNS response filtering is completely turned off
|
||||
if !filterByScope && !filterByProfile {
|
||||
if !p.RemoveOutOfScopeDNS() && !p.RemoveBlockedDNS() {
|
||||
return rrCache
|
||||
}
|
||||
|
||||
@@ -175,7 +133,6 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int
|
||||
// loop vars
|
||||
var classification int8
|
||||
var ip net.IP
|
||||
var result profile.EPResult
|
||||
|
||||
// filter function
|
||||
filterEntries := func(entries []dns.RR) (goodEntries []dns.RR) {
|
||||
@@ -196,7 +153,7 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int
|
||||
}
|
||||
classification = netutils.ClassifyIP(ip)
|
||||
|
||||
if filterByScope {
|
||||
if p.RemoveOutOfScopeDNS() {
|
||||
switch {
|
||||
case classification == netutils.HostLocal:
|
||||
// No DNS should return localhost addresses
|
||||
@@ -211,30 +168,24 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int
|
||||
}
|
||||
}
|
||||
|
||||
if filterByProfile {
|
||||
if p.RemoveBlockedDNS() {
|
||||
// filter by flags
|
||||
switch {
|
||||
case !profileSet.CheckFlag(profile.Internet) && classification == netutils.Global:
|
||||
case p.BlockScopeInternet() && classification == netutils.Global:
|
||||
addressesRemoved++
|
||||
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
|
||||
continue
|
||||
case !profileSet.CheckFlag(profile.LAN) && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
|
||||
case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
|
||||
addressesRemoved++
|
||||
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
|
||||
continue
|
||||
case !profileSet.CheckFlag(profile.Localhost) && classification == netutils.HostLocal:
|
||||
case p.BlockScopeLocal() && classification == netutils.HostLocal:
|
||||
addressesRemoved++
|
||||
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
|
||||
continue
|
||||
}
|
||||
|
||||
// filter by endpoints
|
||||
result, _ = profileSet.CheckEndpointIP(q.FQDN, ip, 0, 0, false)
|
||||
if result == profile.Denied {
|
||||
addressesRemoved++
|
||||
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
|
||||
continue
|
||||
}
|
||||
// TODO: filter by endpoint list (IP only)
|
||||
}
|
||||
|
||||
// if survived, add to good entries
|
||||
@@ -267,17 +218,15 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int
|
||||
}
|
||||
|
||||
// DecideOnCommunication makes a decision about a communication with its first packet.
|
||||
func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
|
||||
|
||||
// check if communication needs reevaluation, if it's not with a domain
|
||||
if comm.NeedsReevaluation() {
|
||||
func DecideOnCommunication(comm *network.Communication) {
|
||||
// update profiles and check if communication needs reevaluation
|
||||
if comm.UpdateAndCheck() {
|
||||
log.Infof("firewall: re-evaluating verdict on %s", comm)
|
||||
comm.ResetVerdict()
|
||||
|
||||
// if communicating with a domain entity, re-evaluate with Before/AfterIntel
|
||||
if strings.HasSuffix(comm.Domain, ".") {
|
||||
DecideOnCommunicationBeforeIntel(comm, comm.Domain)
|
||||
DecideOnCommunicationAfterIntel(comm, comm.Domain, nil)
|
||||
// if communicating with a domain entity, re-evaluate with BeforeDNS
|
||||
if strings.HasSuffix(comm.Scope, ".") {
|
||||
DecideOnCommunicationBeforeDNS(comm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,29 +242,24 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
|
||||
return
|
||||
}
|
||||
|
||||
// check if there is a profile
|
||||
profileSet := comm.Process().ProfileSet()
|
||||
if profileSet == nil {
|
||||
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
|
||||
comm.Deny("no Profile Set")
|
||||
return
|
||||
}
|
||||
profileSet.Update(status.ActiveSecurityLevel())
|
||||
// get profile
|
||||
p := comm.Process().Profile()
|
||||
|
||||
// check comm type
|
||||
switch comm.Domain {
|
||||
switch comm.Scope {
|
||||
case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid:
|
||||
if !profileSet.CheckFlag(profile.Service) {
|
||||
if p.BlockInbound() {
|
||||
log.Infof("firewall: denying communication %s, not a service", comm)
|
||||
if comm.Domain == network.IncomingHost {
|
||||
if comm.Scope == network.IncomingHost {
|
||||
comm.Block("not a service")
|
||||
} else {
|
||||
comm.Deny("not a service")
|
||||
}
|
||||
return
|
||||
}
|
||||
case network.PeerLAN, network.PeerInternet, network.PeerInvalid: // Important: PeerHost is and should be missing!
|
||||
if !profileSet.CheckFlag(profile.PeerToPeer) {
|
||||
case network.PeerLAN, network.PeerInternet, network.PeerInvalid:
|
||||
// Important: PeerHost is and should be missing!
|
||||
if p.BlockP2P() {
|
||||
log.Infof("firewall: denying communication %s, peer to peer comms (to an IP) not allowed", comm)
|
||||
comm.Deny("peer to peer comms (to an IP) not allowed")
|
||||
return
|
||||
@@ -323,21 +267,21 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
|
||||
}
|
||||
|
||||
// check network scope
|
||||
switch comm.Domain {
|
||||
switch comm.Scope {
|
||||
case network.IncomingHost:
|
||||
if !profileSet.CheckFlag(profile.Localhost) {
|
||||
if p.BlockScopeLocal() {
|
||||
log.Infof("firewall: denying communication %s, serving localhost not allowed", comm)
|
||||
comm.Block("serving localhost not allowed")
|
||||
return
|
||||
}
|
||||
case network.IncomingLAN:
|
||||
if !profileSet.CheckFlag(profile.LAN) {
|
||||
if p.BlockScopeLAN() {
|
||||
log.Infof("firewall: denying communication %s, serving LAN not allowed", comm)
|
||||
comm.Deny("serving LAN not allowed")
|
||||
return
|
||||
}
|
||||
case network.IncomingInternet:
|
||||
if !profileSet.CheckFlag(profile.Internet) {
|
||||
if p.BlockScopeInternet() {
|
||||
log.Infof("firewall: denying communication %s, serving Internet not allowed", comm)
|
||||
comm.Deny("serving Internet not allowed")
|
||||
return
|
||||
@@ -347,19 +291,19 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
|
||||
comm.Drop("invalid IP address")
|
||||
return
|
||||
case network.PeerHost:
|
||||
if !profileSet.CheckFlag(profile.Localhost) {
|
||||
if p.BlockScopeLocal() {
|
||||
log.Infof("firewall: denying communication %s, accessing localhost not allowed", comm)
|
||||
comm.Block("accessing localhost not allowed")
|
||||
return
|
||||
}
|
||||
case network.PeerLAN:
|
||||
if !profileSet.CheckFlag(profile.LAN) {
|
||||
if p.BlockScopeLAN() {
|
||||
log.Infof("firewall: denying communication %s, accessing the LAN not allowed", comm)
|
||||
comm.Deny("accessing the LAN not allowed")
|
||||
return
|
||||
}
|
||||
case network.PeerInternet:
|
||||
if !profileSet.CheckFlag(profile.Internet) {
|
||||
if p.BlockScopeInternet() {
|
||||
log.Infof("firewall: denying communication %s, accessing the Internet not allowed", comm)
|
||||
comm.Deny("accessing the Internet not allowed")
|
||||
return
|
||||
@@ -384,7 +328,7 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa
|
||||
return
|
||||
}
|
||||
|
||||
// check if communicating with self
|
||||
// check if process is communicating with itself
|
||||
if comm.Process().Pid >= 0 && pkt.Info().Src.Equal(pkt.Info().Dst) {
|
||||
// get PID
|
||||
otherPid, _, err := process.GetPidByEndpoints(
|
||||
@@ -424,86 +368,80 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa
|
||||
return
|
||||
}
|
||||
|
||||
// check if there is a profile
|
||||
profileSet := comm.Process().ProfileSet()
|
||||
if profileSet == nil {
|
||||
log.Infof("firewall: no Profile Set, denying %s", link)
|
||||
link.Deny("no Profile Set")
|
||||
return
|
||||
}
|
||||
profileSet.Update(status.ActiveSecurityLevel())
|
||||
|
||||
// get domain
|
||||
var fqdn string
|
||||
if strings.HasSuffix(comm.Domain, ".") {
|
||||
fqdn = comm.Domain
|
||||
}
|
||||
|
||||
// remoteIP
|
||||
var remoteIP net.IP
|
||||
if comm.Direction {
|
||||
remoteIP = pkt.Info().Src
|
||||
} else {
|
||||
remoteIP = pkt.Info().Dst
|
||||
}
|
||||
|
||||
// protocol and destination port
|
||||
protocol := uint8(pkt.Info().Protocol)
|
||||
dstPort := pkt.Info().DstPort
|
||||
// get profile
|
||||
p := comm.Process().Profile()
|
||||
|
||||
// check endpoints list
|
||||
result, reason := profileSet.CheckEndpointIP(fqdn, remoteIP, protocol, dstPort, comm.Direction)
|
||||
var result endpoints.EPResult
|
||||
var reason string
|
||||
// FIXME: link.Entity.Lock()
|
||||
if comm.Direction {
|
||||
result, reason = p.MatchServiceEndpoint(link.Entity)
|
||||
} else {
|
||||
result, reason = p.MatchEndpoint(link.Entity)
|
||||
}
|
||||
// FIXME: link.Entity.Unlock()
|
||||
switch result {
|
||||
case profile.Denied:
|
||||
case endpoints.Denied:
|
||||
log.Infof("firewall: denying link %s, endpoint is blacklisted: %s", link, reason)
|
||||
link.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason))
|
||||
return
|
||||
case profile.Permitted:
|
||||
case endpoints.Permitted:
|
||||
log.Infof("firewall: permitting link %s, endpoint is whitelisted: %s", link, reason)
|
||||
link.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
|
||||
return
|
||||
}
|
||||
// continueing with result == NoMatch
|
||||
|
||||
// TODO: Stamp integration
|
||||
|
||||
switch profileSet.GetProfileMode() {
|
||||
case profile.Whitelist:
|
||||
log.Infof("firewall: denying link %s: endpoint is not whitelisted", link)
|
||||
link.Deny("endpoint is not whitelisted")
|
||||
return
|
||||
case profile.Blacklist:
|
||||
log.Infof("firewall: permitting link %s: endpoint is not blacklisted", link)
|
||||
link.Accept("endpoint is not blacklisted")
|
||||
// implicit default=block for incoming
|
||||
if comm.Direction {
|
||||
log.Infof("firewall: denying link %s: endpoint is not whitelisted (incoming is always default=block)", link)
|
||||
link.Deny("endpoint is not whitelisted (incoming is always default=block)")
|
||||
return
|
||||
}
|
||||
|
||||
// ProfileMode == Prompt
|
||||
// check default action
|
||||
if p.DefaultAction() == profile.DefaultActionPermit {
|
||||
log.Infof("firewall: permitting link %s: endpoint is not blacklisted (default=permit)", link)
|
||||
link.Accept("endpoint is not blacklisted (default=permit)")
|
||||
return
|
||||
}
|
||||
|
||||
// check relation
|
||||
if fqdn != "" && profileSet.CheckFlag(profile.Related) {
|
||||
if checkRelation(comm, fqdn) {
|
||||
if !p.DisableAutoPermit() {
|
||||
if checkRelation(comm) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// prompt
|
||||
prompt(comm, link, pkt, fqdn)
|
||||
}
|
||||
|
||||
func checkRelation(comm *network.Communication, fqdn string) (related bool) {
|
||||
profileSet := comm.Process().ProfileSet()
|
||||
if profileSet == nil {
|
||||
if p.DefaultAction() == profile.DefaultActionAsk {
|
||||
prompt(comm, link, pkt)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: add #AI
|
||||
// DefaultAction == DefaultActionBlock
|
||||
log.Infof("firewall: denying link %s: endpoint is not whitelisted (default=block)", link)
|
||||
link.Deny("endpoint is not whitelisted (default=block)")
|
||||
return
|
||||
}
|
||||
|
||||
pathElements := strings.Split(comm.Process().Path, "/") // FIXME: path separator
|
||||
// checkRelation tries to find a relation between a process and a communication. This is for better out of the box experience and is _not_ meant to thwart intentional malware.
|
||||
func checkRelation(comm *network.Communication) (related bool) {
|
||||
if comm.Entity.Domain != "" {
|
||||
return false
|
||||
}
|
||||
// don't check for unknown processes
|
||||
if comm.Process().Pid < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
pathElements := strings.Split(comm.Process().Path, string(filepath.Separator))
|
||||
// only look at the last two path segments
|
||||
if len(pathElements) > 2 {
|
||||
pathElements = pathElements[len(pathElements)-2:]
|
||||
}
|
||||
domainElements := strings.Split(fqdn, ".")
|
||||
domainElements := strings.Split(comm.Entity.Domain, ".")
|
||||
|
||||
var domainElement string
|
||||
var processElement string
|
||||
@@ -517,11 +455,6 @@ matchLoop:
|
||||
break matchLoop
|
||||
}
|
||||
}
|
||||
if levenshtein.Match(domainElement, profileSet.UserProfile().Name, nil) > 0.5 {
|
||||
related = true
|
||||
processElement = profileSet.UserProfile().Name
|
||||
break matchLoop
|
||||
}
|
||||
if levenshtein.Match(domainElement, comm.Process().Name, nil) > 0.5 {
|
||||
related = true
|
||||
processElement = comm.Process().Name
|
||||
|
||||
@@ -71,8 +71,12 @@ func GetPermittedPort() uint16 {
|
||||
|
||||
func portsInUseCleaner() {
|
||||
for {
|
||||
time.Sleep(cleanerTickDuration)
|
||||
cleanPortsInUse()
|
||||
select {
|
||||
case <-module.Stopping():
|
||||
return
|
||||
case <-time.After(cleanerTickDuration):
|
||||
cleanPortsInUse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/profile/endpoints"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portmaster/network"
|
||||
"github.com/safing/portmaster/network/packet"
|
||||
"github.com/safing/portmaster/profile"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,14 +30,14 @@ var (
|
||||
)
|
||||
|
||||
//nolint:gocognit // FIXME
|
||||
func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, fqdn string) {
|
||||
func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet) {
|
||||
nTTL := time.Duration(promptTimeout()) * time.Second
|
||||
|
||||
// first check if there is an existing notification for this.
|
||||
// build notification ID
|
||||
var nID string
|
||||
switch {
|
||||
case comm.Direction, fqdn == "": // connection to/from IP
|
||||
case comm.Direction, comm.Entity.Domain == "": // connection to/from IP
|
||||
if pkt == nil {
|
||||
log.Error("firewall: could not prompt for incoming/direct connection: missing pkt")
|
||||
if link != nil {
|
||||
@@ -47,9 +47,9 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
|
||||
}
|
||||
return
|
||||
}
|
||||
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s", comm.Process().Pid, comm.Domain, pkt.Info().RemoteIP())
|
||||
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s", comm.Process().Pid, comm.Scope, pkt.Info().RemoteIP())
|
||||
default: // connection to domain
|
||||
nID = fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Domain)
|
||||
nID = fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Scope)
|
||||
}
|
||||
n := notifications.Get(nID)
|
||||
saveResponse := true
|
||||
@@ -70,7 +70,7 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
|
||||
// add message and actions
|
||||
switch {
|
||||
case comm.Direction: // incoming
|
||||
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().LocalPort())
|
||||
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", comm.Process(), link.Entity.IP.String(), link.Entity.Protocol, link.Entity.Port)
|
||||
n.AvailableActions = []*notifications.Action{
|
||||
{
|
||||
ID: permitServingIP,
|
||||
@@ -81,8 +81,8 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
|
||||
Text: "Deny",
|
||||
},
|
||||
}
|
||||
case fqdn == "": // direct connection
|
||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort())
|
||||
case comm.Entity.Domain == "": // direct connection
|
||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", comm.Process(), link.Entity.IP.String(), link.Entity.Protocol, link.Entity.Port)
|
||||
n.AvailableActions = []*notifications.Action{
|
||||
{
|
||||
ID: permitIP,
|
||||
@@ -94,10 +94,10 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
|
||||
},
|
||||
}
|
||||
default: // connection to domain
|
||||
if pkt != nil {
|
||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Domain, pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort())
|
||||
if link != nil {
|
||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Entity.Domain, link.Entity.IP.String(), link.Entity.Protocol, link.Entity.Port)
|
||||
} else {
|
||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Domain)
|
||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Entity.Domain)
|
||||
}
|
||||
n.AvailableActions = []*notifications.Action{
|
||||
{
|
||||
@@ -141,62 +141,57 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet,
|
||||
return
|
||||
}
|
||||
|
||||
new := &profile.EndpointPermission{
|
||||
Type: profile.EptDomain,
|
||||
Value: comm.Domain,
|
||||
Permit: false,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
// get profile
|
||||
p := comm.Process().Profile()
|
||||
|
||||
// permission type
|
||||
var ep endpoints.Endpoint
|
||||
switch promptResponse {
|
||||
case permitDomainAll, denyDomainAll:
|
||||
new.Value = "." + new.Value
|
||||
case permitIP, permitServingIP, denyIP, denyServingIP:
|
||||
if pkt == nil {
|
||||
log.Warningf("firewall: received invalid prompt response: %s for %s", promptResponse, comm.Domain)
|
||||
return
|
||||
case permitDomainAll:
|
||||
ep = &endpoints.EndpointDomain{
|
||||
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
||||
Domain: "." + comm.Entity.Domain,
|
||||
}
|
||||
if pkt.Info().Version == packet.IPv4 {
|
||||
new.Type = profile.EptIPv4
|
||||
} else {
|
||||
new.Type = profile.EptIPv6
|
||||
case permitDomainDistinct:
|
||||
ep = &endpoints.EndpointDomain{
|
||||
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
||||
Domain: comm.Entity.Domain,
|
||||
}
|
||||
new.Value = pkt.Info().RemoteIP().String()
|
||||
case denyDomainAll:
|
||||
ep = &endpoints.EndpointDomain{
|
||||
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
||||
Domain: "." + comm.Entity.Domain,
|
||||
}
|
||||
case denyDomainDistinct:
|
||||
ep = &endpoints.EndpointDomain{
|
||||
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
||||
Domain: comm.Entity.Domain,
|
||||
}
|
||||
case permitIP, permitServingIP:
|
||||
ep = &endpoints.EndpointIP{
|
||||
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
||||
IP: comm.Entity.IP,
|
||||
}
|
||||
case denyIP, denyServingIP:
|
||||
ep = &endpoints.EndpointIP{
|
||||
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
||||
IP: comm.Entity.IP,
|
||||
}
|
||||
default:
|
||||
log.Warningf("filter: unknown prompt response: %s", promptResponse)
|
||||
}
|
||||
|
||||
// permission verdict
|
||||
switch promptResponse {
|
||||
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
|
||||
new.Permit = false
|
||||
}
|
||||
|
||||
// get user profile
|
||||
profileSet := comm.Process().ProfileSet()
|
||||
profileSet.Lock()
|
||||
defer profileSet.Unlock()
|
||||
userProfile := profileSet.UserProfile()
|
||||
userProfile.Lock()
|
||||
defer userProfile.Unlock()
|
||||
|
||||
// add to correct list
|
||||
switch promptResponse {
|
||||
case permitServingIP, denyServingIP:
|
||||
userProfile.ServiceEndpoints = append(userProfile.ServiceEndpoints, new)
|
||||
p.AddServiceEndpoint(ep.String())
|
||||
default:
|
||||
userProfile.Endpoints = append(userProfile.Endpoints, new)
|
||||
p.AddEndpoint(ep.String())
|
||||
}
|
||||
|
||||
// save!
|
||||
module.StartMicroTask(&mtSaveProfile, func(ctx context.Context) error {
|
||||
return userProfile.Save("")
|
||||
})
|
||||
|
||||
case <-n.Expired():
|
||||
if link != nil {
|
||||
link.Accept("no response to prompt")
|
||||
link.Deny("no response to prompt")
|
||||
} else {
|
||||
comm.Accept("no response to prompt")
|
||||
comm.Deny("no response to prompt")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
155
intel/config.go
155
intel/config.go
@@ -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
201
intel/entity.go
Normal 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
|
||||
}
|
||||
@@ -41,5 +41,6 @@ func GetLocation(ip net.IP) (record *Location, err error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
@@ -3,16 +3,9 @@ package geoip
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/updates"
|
||||
)
|
||||
|
||||
func TestLocationLookup(t *testing.T) {
|
||||
err := updates.InitForTesting()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ip1 := net.ParseIP("81.2.69.142")
|
||||
loc1, err := GetLocation(ip1)
|
||||
if err != nil {
|
||||
@@ -39,7 +32,21 @@ func TestLocationLookup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%v", loc1)
|
||||
t.Logf("%v", loc4)
|
||||
|
||||
ip5 := net.ParseIP("194.232.1.1")
|
||||
loc5, err := GetLocation(ip5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%v", loc5)
|
||||
|
||||
ip6 := net.ParseIP("151.101.1.164")
|
||||
loc6, err := GetLocation(ip6)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%v", loc6)
|
||||
|
||||
dist1 := loc1.EstimateNetworkProximity(loc2)
|
||||
dist2 := loc2.EstimateNetworkProximity(loc3)
|
||||
@@ -50,5 +57,4 @@ func TestLocationLookup(t *testing.T) {
|
||||
t.Logf("proximity %s <> %s: %d", ip2, ip3, dist2)
|
||||
t.Logf("proximity %s <> %s: %d", ip1, ip3, dist3)
|
||||
t.Logf("proximity %s <> %s: %d", ip1, ip4, dist4)
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/modules"
|
||||
)
|
||||
@@ -12,15 +11,10 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("geoip", nil, start, nil, "updates")
|
||||
module = modules.Register("geoip", prep, nil, nil, "core")
|
||||
}
|
||||
|
||||
func start() error {
|
||||
err := prepDatabaseForUse()
|
||||
if err != nil {
|
||||
return fmt.Errorf("goeip: failed to load databases: %s", err)
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
return module.RegisterEventHook(
|
||||
"updates",
|
||||
"resource update",
|
||||
11
intel/geoip/module_test.go
Normal file
11
intel/geoip/module_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/core/pmtesting"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pmtesting.TestMain(m)
|
||||
}
|
||||
@@ -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
40
intel/lists.go
Normal 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
|
||||
}
|
||||
@@ -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
9
intel/module.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package intel
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register("intel", nil, nil, nil, "geoip")
|
||||
}
|
||||
@@ -2,152 +2,15 @@ package intel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/network/environment"
|
||||
)
|
||||
|
||||
// DNS Resolver Attributes
|
||||
const (
|
||||
ServerTypeDNS = "dns"
|
||||
ServerTypeTCP = "tcp"
|
||||
ServerTypeDoT = "dot"
|
||||
ServerTypeDoH = "doh"
|
||||
|
||||
ServerSourceConfigured = "config"
|
||||
ServerSourceAssigned = "dhcp"
|
||||
ServerSourceMDNS = "mdns"
|
||||
var (
|
||||
reverseResolver func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error)
|
||||
)
|
||||
|
||||
// Resolver holds information about an active resolver.
|
||||
type Resolver struct {
|
||||
// Server config url (and ID)
|
||||
Server string
|
||||
|
||||
// Parsed config
|
||||
ServerType string
|
||||
ServerAddress string
|
||||
ServerIP net.IP
|
||||
ServerIPScope int8
|
||||
ServerPort uint16
|
||||
|
||||
// Special Options
|
||||
VerifyDomain string
|
||||
Search []string
|
||||
SkipFQDN string
|
||||
|
||||
Source string
|
||||
|
||||
// logic interface
|
||||
Conn ResolverConn
|
||||
}
|
||||
|
||||
// String returns the URL representation of the resolver.
|
||||
func (resolver *Resolver) String() string {
|
||||
return resolver.Server
|
||||
}
|
||||
|
||||
// ResolverConn is an interface to implement different types of query backends.
|
||||
type ResolverConn interface {
|
||||
Query(ctx context.Context, q *Query) (*RRCache, error)
|
||||
MarkFailed()
|
||||
LastFail() time.Time
|
||||
}
|
||||
|
||||
// BasicResolverConn implements ResolverConn for standard dns clients.
|
||||
type BasicResolverConn struct {
|
||||
sync.Mutex // for lastFail
|
||||
|
||||
resolver *Resolver
|
||||
clientManager *clientManager
|
||||
lastFail time.Time
|
||||
}
|
||||
|
||||
// MarkFailed marks the resolver as failed.
|
||||
func (brc *BasicResolverConn) MarkFailed() {
|
||||
if !environment.Online() {
|
||||
// don't mark failed if we are offline
|
||||
return
|
||||
// SetReverseResolver allows the resolver module to register a function to allow reverse resolving IPs to domains.
|
||||
func SetReverseResolver(fn func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error)) {
|
||||
if reverseResolver == nil {
|
||||
reverseResolver = fn
|
||||
}
|
||||
|
||||
brc.Lock()
|
||||
defer brc.Unlock()
|
||||
brc.lastFail = time.Now()
|
||||
}
|
||||
|
||||
// LastFail returns the internal lastfail value while locking the Resolver.
|
||||
func (brc *BasicResolverConn) LastFail() time.Time {
|
||||
brc.Lock()
|
||||
defer brc.Unlock()
|
||||
return brc.lastFail
|
||||
}
|
||||
|
||||
// Query executes the given query against the resolver.
|
||||
func (brc *BasicResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) {
|
||||
// convenience
|
||||
resolver := brc.resolver
|
||||
|
||||
// create query
|
||||
dnsQuery := new(dns.Msg)
|
||||
dnsQuery.SetQuestion(q.FQDN, uint16(q.QType))
|
||||
|
||||
// start
|
||||
var reply *dns.Msg
|
||||
var err error
|
||||
for i := 0; i < 3; i++ {
|
||||
|
||||
// log query time
|
||||
// qStart := time.Now()
|
||||
reply, _, err = brc.clientManager.getDNSClient().Exchange(dnsQuery, resolver.ServerAddress)
|
||||
// log.Tracef("intel: query to %s took %s", resolver.Server, time.Now().Sub(qStart))
|
||||
|
||||
// error handling
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Tracef("intel: query to %s encountered error: %s", resolver.Server, err)
|
||||
|
||||
// TODO: handle special cases
|
||||
// 1. connect: network is unreachable
|
||||
// 2. timeout
|
||||
|
||||
// hint network environment at failed connection
|
||||
environment.ReportFailedConnection()
|
||||
|
||||
// temporary error
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
log.Tracer(ctx).Tracef("intel: retrying to resolve %s%s with %s, error is temporary", q.FQDN, q.QType, resolver.Server)
|
||||
continue
|
||||
}
|
||||
|
||||
// permanent error
|
||||
break
|
||||
}
|
||||
|
||||
// no error
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// FIXME: mark as failed
|
||||
}
|
||||
|
||||
// hint network environment at successful connection
|
||||
environment.ReportSuccessfulConnection()
|
||||
|
||||
new := &RRCache{
|
||||
Domain: q.FQDN,
|
||||
Question: q.QType,
|
||||
Answer: reply.Answer,
|
||||
Ns: reply.Ns,
|
||||
Extra: reply.Extra,
|
||||
Server: resolver.Server,
|
||||
ServerScope: resolver.ServerIPScope,
|
||||
}
|
||||
|
||||
// TODO: check if reply.Answer is valid
|
||||
return new, nil
|
||||
}
|
||||
|
||||
8
main.go
8
main.go
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/safing/portbase/run"
|
||||
|
||||
// include packages here
|
||||
_ "github.com/safing/portbase/modules/subsystems"
|
||||
_ "github.com/safing/portmaster/core"
|
||||
_ "github.com/safing/portmaster/firewall"
|
||||
_ "github.com/safing/portmaster/nameserver"
|
||||
@@ -14,13 +15,6 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
/*go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
fmt.Fprintln(os.Stderr, "===== TAKING TOO LONG FOR SHUTDOWN - PRINTING STACK TRACES =====")
|
||||
_ = pprof.Lookup("goroutine").WriteTo(os.Stderr, 2)
|
||||
os.Exit(1)
|
||||
}()*/
|
||||
|
||||
info.Set("Portmaster", "0.3.9", "AGPLv3", true)
|
||||
os.Exit(run.Run())
|
||||
}
|
||||
|
||||
@@ -5,18 +5,18 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/network/environment"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
|
||||
"github.com/safing/portmaster/detection/dga"
|
||||
"github.com/safing/portmaster/firewall"
|
||||
"github.com/safing/portmaster/intel"
|
||||
"github.com/safing/portmaster/network"
|
||||
"github.com/safing/portmaster/network/environment"
|
||||
"github.com/safing/portmaster/network/netutils"
|
||||
"github.com/safing/portmaster/resolver"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,10 +30,18 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("nameserver", initLocalhostRRs, start, stop, "core", "intel", "network")
|
||||
module = modules.Register("nameserver", prep, start, stop, "core", "resolver", "network")
|
||||
subsystems.Register(
|
||||
"dns",
|
||||
"Secure DNS",
|
||||
"DNS resolver with scoping and DNS-over-TLS",
|
||||
module,
|
||||
"config:dns/",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func initLocalhostRRs() error {
|
||||
func prep() error {
|
||||
localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -45,6 +53,7 @@ func initLocalhostRRs() error {
|
||||
}
|
||||
|
||||
localhostRRs = []dns.RR{localhostIPv4, localhostIPv6}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,7 +65,7 @@ func start() error {
|
||||
err := dnsServer.ListenAndServe()
|
||||
if err != nil {
|
||||
// check if we are shutting down
|
||||
if module.ShutdownInProgress() {
|
||||
if module.IsStopping() {
|
||||
return nil
|
||||
}
|
||||
// is something blocking our port?
|
||||
@@ -108,7 +117,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
|
||||
// only process first question, that's how everyone does it.
|
||||
question := query.Question[0]
|
||||
q := &intel.Query{
|
||||
q := &resolver.Query{
|
||||
FQDN: question.Name,
|
||||
QType: dns.Type(question.Qtype),
|
||||
}
|
||||
@@ -176,7 +185,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
}()
|
||||
|
||||
// save security level to query
|
||||
q.SecurityLevel = comm.Process().ProfileSet().SecurityLevel()
|
||||
q.SecurityLevel = comm.Process().Profile().SecurityLevel()
|
||||
|
||||
// check for possible DNS tunneling / data transmission
|
||||
// TODO: improve this
|
||||
@@ -189,7 +198,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
}
|
||||
|
||||
// check profile before we even get intel and rr
|
||||
firewall.DecideOnCommunicationBeforeIntel(comm, q.FQDN)
|
||||
firewall.DecideOnCommunicationBeforeDNS(comm)
|
||||
comm.Lock()
|
||||
comm.SaveWhenFinished()
|
||||
comm.Unlock()
|
||||
@@ -200,8 +209,8 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// get intel and RRs
|
||||
rrCache, err := intel.Resolve(ctx, q)
|
||||
// resolve
|
||||
rrCache, err := resolver.Resolve(ctx, q)
|
||||
if err != nil {
|
||||
// TODO: analyze nxdomain requests, malware could be trying DGA-domains
|
||||
tracer.Warningf("nameserver: %s requested %s%s: %s", comm.Process(), q.FQDN, q.QType, err)
|
||||
@@ -209,31 +218,6 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// get current intel
|
||||
comm.Lock()
|
||||
domainIntel := comm.Intel
|
||||
comm.Unlock()
|
||||
if domainIntel == nil {
|
||||
// fetch intel
|
||||
domainIntel, err = intel.GetIntel(ctx, q)
|
||||
if err != nil {
|
||||
tracer.Warningf("nameserver: failed to get intel for %s%s: %s", q.FQDN, q.QType, err)
|
||||
returnNXDomain(w, query)
|
||||
}
|
||||
comm.Lock()
|
||||
comm.Intel = domainIntel
|
||||
comm.Unlock()
|
||||
}
|
||||
|
||||
// check with intel
|
||||
firewall.DecideOnCommunicationAfterIntel(comm, q.FQDN, rrCache)
|
||||
switch comm.GetVerdict() {
|
||||
case network.VerdictUndecided, network.VerdictBlock, network.VerdictDrop:
|
||||
tracer.Infof("nameserver: %s denied after intel, returning nxdomain", comm)
|
||||
returnNXDomain(w, query)
|
||||
return nil
|
||||
}
|
||||
|
||||
// filter DNS response
|
||||
rrCache = firewall.FilterDNSResponse(comm, q, rrCache)
|
||||
if rrCache == nil {
|
||||
@@ -246,9 +230,9 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
for _, rr := range append(rrCache.Answer, rrCache.Extra...) {
|
||||
switch v := rr.(type) {
|
||||
case *dns.A:
|
||||
ipInfo, err := intel.GetIPInfo(v.A.String())
|
||||
ipInfo, err := resolver.GetIPInfo(v.A.String())
|
||||
if err != nil {
|
||||
ipInfo = &intel.IPInfo{
|
||||
ipInfo = &resolver.IPInfo{
|
||||
IP: v.A.String(),
|
||||
Domains: []string{q.FQDN},
|
||||
}
|
||||
@@ -260,9 +244,9 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
}
|
||||
}
|
||||
case *dns.AAAA:
|
||||
ipInfo, err := intel.GetIPInfo(v.AAAA.String())
|
||||
ipInfo, err := resolver.GetIPInfo(v.AAAA.String())
|
||||
if err != nil {
|
||||
ipInfo = &intel.IPInfo{
|
||||
ipInfo = &resolver.IPInfo{
|
||||
IP: v.AAAA.String(),
|
||||
Domains: []string{q.FQDN},
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ func cleanLinks() (activeComms map[string]struct{}) {
|
||||
for key, link := range links {
|
||||
|
||||
// delete dead links
|
||||
link.Lock()
|
||||
link.lock.Lock()
|
||||
deleteThis := link.Ended > 0 && link.Ended < deleteOlderThan
|
||||
link.Unlock()
|
||||
link.lock.Unlock()
|
||||
if deleteThis {
|
||||
log.Tracef("network.clean: deleted %s (ended at %d)", link.DatabaseKey(), link.Ended)
|
||||
go link.Delete()
|
||||
@@ -51,9 +51,9 @@ func cleanLinks() (activeComms map[string]struct{}) {
|
||||
|
||||
// not yet deleted, so its still a valid link regarding link count
|
||||
comm := link.Communication()
|
||||
comm.Lock()
|
||||
comm.lock.Lock()
|
||||
markActive(activeComms, comm.DatabaseKey())
|
||||
comm.Unlock()
|
||||
comm.lock.Unlock()
|
||||
|
||||
// check if link is dead
|
||||
found = false
|
||||
@@ -66,9 +66,9 @@ func cleanLinks() (activeComms map[string]struct{}) {
|
||||
|
||||
if !found {
|
||||
// mark end time
|
||||
link.Lock()
|
||||
link.lock.Lock()
|
||||
link.Ended = now
|
||||
link.Unlock()
|
||||
link.lock.Unlock()
|
||||
log.Tracef("network.clean: marked %s as ended", link.DatabaseKey())
|
||||
// save
|
||||
linkToSave := link
|
||||
@@ -95,9 +95,9 @@ func cleanComms(activeLinks map[string]struct{}) (activeComms map[string]struct{
|
||||
_, hasLinks := activeLinks[comm.DatabaseKey()]
|
||||
|
||||
// comm created
|
||||
comm.Lock()
|
||||
comm.lock.Lock()
|
||||
created := comm.Meta().Created
|
||||
comm.Unlock()
|
||||
comm.lock.Unlock()
|
||||
|
||||
if !hasLinks && created < threshold {
|
||||
log.Tracef("network.clean: deleted %s", comm.DatabaseKey())
|
||||
|
||||
@@ -8,48 +8,63 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/resolver"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/intel"
|
||||
"github.com/safing/portmaster/network/netutils"
|
||||
"github.com/safing/portmaster/network/packet"
|
||||
"github.com/safing/portmaster/process"
|
||||
"github.com/safing/portmaster/profile"
|
||||
)
|
||||
|
||||
// Communication describes a logical connection between a process and a domain.
|
||||
//nolint:maligned // TODO: fix alignment
|
||||
type Communication struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
lock sync.Mutex
|
||||
|
||||
Domain string
|
||||
Scope string
|
||||
Entity *intel.Entity
|
||||
Direction bool
|
||||
Intel *intel.Intel
|
||||
process *process.Process
|
||||
Verdict Verdict
|
||||
Reason string
|
||||
Inspect bool
|
||||
|
||||
Verdict Verdict
|
||||
Reason string
|
||||
ReasonID string // format source[:id[:id]]
|
||||
Inspect bool
|
||||
process *process.Process
|
||||
profileRevisionCounter uint64
|
||||
|
||||
FirstLinkEstablished int64
|
||||
LastLinkEstablished int64
|
||||
|
||||
profileUpdateVersion uint32
|
||||
saveWhenFinished bool
|
||||
saveWhenFinished bool
|
||||
}
|
||||
|
||||
// Lock locks the communication and the communication's Entity.
|
||||
func (comm *Communication) Lock() {
|
||||
comm.lock.Lock()
|
||||
comm.Entity.Lock()
|
||||
}
|
||||
|
||||
// Lock unlocks the communication and the communication's Entity.
|
||||
func (comm *Communication) Unlock() {
|
||||
comm.Entity.Unlock()
|
||||
comm.lock.Unlock()
|
||||
}
|
||||
|
||||
// Process returns the process that owns the connection.
|
||||
func (comm *Communication) Process() *process.Process {
|
||||
comm.Lock()
|
||||
defer comm.Unlock()
|
||||
comm.lock.Lock()
|
||||
defer comm.lock.Unlock()
|
||||
|
||||
return comm.process
|
||||
}
|
||||
|
||||
// ResetVerdict resets the verdict to VerdictUndecided.
|
||||
func (comm *Communication) ResetVerdict() {
|
||||
comm.Lock()
|
||||
defer comm.Unlock()
|
||||
comm.lock.Lock()
|
||||
defer comm.lock.Unlock()
|
||||
|
||||
comm.Verdict = VerdictUndecided
|
||||
comm.Reason = ""
|
||||
@@ -58,8 +73,8 @@ func (comm *Communication) ResetVerdict() {
|
||||
|
||||
// GetVerdict returns the current verdict.
|
||||
func (comm *Communication) GetVerdict() Verdict {
|
||||
comm.Lock()
|
||||
defer comm.Unlock()
|
||||
comm.lock.Lock()
|
||||
defer comm.lock.Unlock()
|
||||
|
||||
return comm.Verdict
|
||||
}
|
||||
@@ -93,8 +108,8 @@ func (comm *Communication) Drop(reason string) {
|
||||
|
||||
// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts.
|
||||
func (comm *Communication) UpdateVerdict(newVerdict Verdict) {
|
||||
comm.Lock()
|
||||
defer comm.Unlock()
|
||||
comm.lock.Lock()
|
||||
defer comm.lock.Unlock()
|
||||
|
||||
if newVerdict > comm.Verdict {
|
||||
comm.Verdict = newVerdict
|
||||
@@ -108,8 +123,8 @@ func (comm *Communication) SetReason(reason string) {
|
||||
return
|
||||
}
|
||||
|
||||
comm.Lock()
|
||||
defer comm.Unlock()
|
||||
comm.lock.Lock()
|
||||
defer comm.lock.Unlock()
|
||||
comm.Reason = reason
|
||||
comm.saveWhenFinished = true
|
||||
}
|
||||
@@ -120,8 +135,8 @@ func (comm *Communication) AddReason(reason string) {
|
||||
return
|
||||
}
|
||||
|
||||
comm.Lock()
|
||||
defer comm.Unlock()
|
||||
comm.lock.Lock()
|
||||
defer comm.lock.Unlock()
|
||||
|
||||
if comm.Reason != "" {
|
||||
comm.Reason += " | "
|
||||
@@ -129,21 +144,18 @@ func (comm *Communication) AddReason(reason string) {
|
||||
comm.Reason += reason
|
||||
}
|
||||
|
||||
// NeedsReevaluation returns whether the decision on this communication should be re-evaluated.
|
||||
func (comm *Communication) NeedsReevaluation() bool {
|
||||
comm.Lock()
|
||||
defer comm.Unlock()
|
||||
// UpdateAndCheck updates profiles and checks whether a reevaluation is needed.
|
||||
func (comm *Communication) UpdateAndCheck() (needsReevaluation bool) {
|
||||
revCnt := comm.Process().Profile().Update()
|
||||
|
||||
oldVersion := comm.profileUpdateVersion
|
||||
comm.profileUpdateVersion = profile.GetUpdateVersion()
|
||||
comm.lock.Lock()
|
||||
defer comm.lock.Unlock()
|
||||
if comm.profileRevisionCounter != revCnt {
|
||||
comm.profileRevisionCounter = revCnt
|
||||
needsReevaluation = true
|
||||
}
|
||||
|
||||
if oldVersion == 0 {
|
||||
return false
|
||||
}
|
||||
if oldVersion != comm.profileUpdateVersion {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
// GetCommunicationByFirstPacket returns the matching communication from the internal storage.
|
||||
@@ -153,25 +165,26 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var domain string
|
||||
var scope string
|
||||
|
||||
// Incoming
|
||||
if direction {
|
||||
switch netutils.ClassifyIP(pkt.Info().Src) {
|
||||
case netutils.HostLocal:
|
||||
domain = IncomingHost
|
||||
scope = IncomingHost
|
||||
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
|
||||
domain = IncomingLAN
|
||||
scope = IncomingLAN
|
||||
case netutils.Global, netutils.GlobalMulticast:
|
||||
domain = IncomingInternet
|
||||
scope = IncomingInternet
|
||||
case netutils.Invalid:
|
||||
domain = IncomingInvalid
|
||||
scope = IncomingInvalid
|
||||
}
|
||||
|
||||
communication, ok := GetCommunication(proc.Pid, domain)
|
||||
communication, ok := GetCommunication(proc.Pid, scope)
|
||||
if !ok {
|
||||
communication = &Communication{
|
||||
Domain: domain,
|
||||
Scope: scope,
|
||||
Entity: (&intel.Entity{}).Init(),
|
||||
Direction: Inbound,
|
||||
process: proc,
|
||||
Inspect: true,
|
||||
@@ -184,7 +197,7 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
|
||||
}
|
||||
|
||||
// get domain
|
||||
ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP())
|
||||
ipinfo, err := resolver.GetIPInfo(pkt.FmtRemoteIP())
|
||||
|
||||
// PeerToPeer
|
||||
if err != nil {
|
||||
@@ -192,19 +205,20 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
|
||||
|
||||
switch netutils.ClassifyIP(pkt.Info().Dst) {
|
||||
case netutils.HostLocal:
|
||||
domain = PeerHost
|
||||
scope = PeerHost
|
||||
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
|
||||
domain = PeerLAN
|
||||
scope = PeerLAN
|
||||
case netutils.Global, netutils.GlobalMulticast:
|
||||
domain = PeerInternet
|
||||
scope = PeerInternet
|
||||
case netutils.Invalid:
|
||||
domain = PeerInvalid
|
||||
scope = PeerInvalid
|
||||
}
|
||||
|
||||
communication, ok := GetCommunication(proc.Pid, domain)
|
||||
communication, ok := GetCommunication(proc.Pid, scope)
|
||||
if !ok {
|
||||
communication = &Communication{
|
||||
Domain: domain,
|
||||
Scope: scope,
|
||||
Entity: (&intel.Entity{}).Init(),
|
||||
Direction: Outbound,
|
||||
process: proc,
|
||||
Inspect: true,
|
||||
@@ -221,7 +235,10 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
|
||||
communication, ok := GetCommunication(proc.Pid, ipinfo.Domains[0])
|
||||
if !ok {
|
||||
communication = &Communication{
|
||||
Domain: ipinfo.Domains[0],
|
||||
Scope: ipinfo.Domains[0],
|
||||
Entity: (&intel.Entity{
|
||||
Domain: ipinfo.Domains[0],
|
||||
}).Init(),
|
||||
Direction: Outbound,
|
||||
process: proc,
|
||||
Inspect: true,
|
||||
@@ -251,7 +268,10 @@ func GetCommunicationByDNSRequest(ctx context.Context, ip net.IP, port uint16, f
|
||||
communication, ok := GetCommunication(proc.Pid, fqdn)
|
||||
if !ok {
|
||||
communication = &Communication{
|
||||
Domain: fqdn,
|
||||
Scope: fqdn,
|
||||
Entity: (&intel.Entity{
|
||||
Domain: fqdn,
|
||||
}).Init(),
|
||||
process: proc,
|
||||
Inspect: true,
|
||||
saveWhenFinished: true,
|
||||
@@ -271,7 +291,7 @@ func GetCommunication(pid int, domain string) (comm *Communication, ok bool) {
|
||||
}
|
||||
|
||||
func (comm *Communication) makeKey() string {
|
||||
return fmt.Sprintf("%d/%s", comm.process.Pid, comm.Domain)
|
||||
return fmt.Sprintf("%d/%s", comm.process.Pid, comm.Scope)
|
||||
}
|
||||
|
||||
// SaveWhenFinished marks the Connection for saving after all current actions are finished.
|
||||
@@ -281,12 +301,12 @@ func (comm *Communication) SaveWhenFinished() {
|
||||
|
||||
// SaveIfNeeded saves the Connection if it is marked for saving when finished.
|
||||
func (comm *Communication) SaveIfNeeded() {
|
||||
comm.Lock()
|
||||
comm.lock.Lock()
|
||||
save := comm.saveWhenFinished
|
||||
if save {
|
||||
comm.saveWhenFinished = false
|
||||
}
|
||||
comm.Unlock()
|
||||
comm.lock.Unlock()
|
||||
|
||||
if save {
|
||||
err := comm.save()
|
||||
@@ -299,14 +319,14 @@ func (comm *Communication) SaveIfNeeded() {
|
||||
// Save saves the Connection object in the storage and propagates the change.
|
||||
func (comm *Communication) save() error {
|
||||
// update comm
|
||||
comm.Lock()
|
||||
comm.lock.Lock()
|
||||
if comm.process == nil {
|
||||
comm.Unlock()
|
||||
comm.lock.Unlock()
|
||||
return errors.New("cannot save connection without process")
|
||||
}
|
||||
|
||||
if !comm.KeyIsSet() {
|
||||
comm.SetKey(fmt.Sprintf("network:tree/%d/%s", comm.process.Pid, comm.Domain))
|
||||
comm.SetKey(fmt.Sprintf("network:tree/%d/%s", comm.process.Pid, comm.Scope))
|
||||
comm.UpdateMeta()
|
||||
}
|
||||
if comm.Meta().Deleted > 0 {
|
||||
@@ -315,7 +335,7 @@ func (comm *Communication) save() error {
|
||||
}
|
||||
key := comm.makeKey()
|
||||
comm.saveWhenFinished = false
|
||||
comm.Unlock()
|
||||
comm.lock.Unlock()
|
||||
|
||||
// save comm
|
||||
commsLock.RLock()
|
||||
@@ -336,8 +356,8 @@ func (comm *Communication) save() error {
|
||||
func (comm *Communication) Delete() {
|
||||
commsLock.Lock()
|
||||
defer commsLock.Unlock()
|
||||
comm.Lock()
|
||||
defer comm.Unlock()
|
||||
comm.lock.Lock()
|
||||
defer comm.lock.Unlock()
|
||||
|
||||
delete(comms, comm.makeKey())
|
||||
|
||||
@@ -347,16 +367,18 @@ func (comm *Communication) Delete() {
|
||||
|
||||
// AddLink applies the Communication to the Link and sets timestamps.
|
||||
func (comm *Communication) AddLink(link *Link) {
|
||||
comm.lock.Lock()
|
||||
defer comm.lock.Unlock()
|
||||
|
||||
// apply comm to link
|
||||
link.Lock()
|
||||
link.lock.Lock()
|
||||
link.comm = comm
|
||||
link.Verdict = comm.Verdict
|
||||
link.Inspect = comm.Inspect
|
||||
// FIXME: use new copy methods
|
||||
link.Entity.Domain = comm.Entity.Domain
|
||||
link.saveWhenFinished = true
|
||||
link.Unlock()
|
||||
|
||||
// update comm LastLinkEstablished
|
||||
comm.Lock()
|
||||
link.lock.Unlock()
|
||||
|
||||
// check if we should save
|
||||
if comm.LastLinkEstablished < time.Now().Add(-3*time.Second).Unix() {
|
||||
@@ -368,8 +390,6 @@ func (comm *Communication) AddLink(link *Link) {
|
||||
if comm.FirstLinkEstablished == 0 {
|
||||
comm.FirstLinkEstablished = comm.LastLinkEstablished
|
||||
}
|
||||
|
||||
comm.Unlock()
|
||||
}
|
||||
|
||||
// String returns a string representation of Communication.
|
||||
@@ -377,7 +397,7 @@ func (comm *Communication) String() string {
|
||||
comm.Lock()
|
||||
defer comm.Unlock()
|
||||
|
||||
switch comm.Domain {
|
||||
switch comm.Scope {
|
||||
case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
|
||||
if comm.process == nil {
|
||||
return "? <- *"
|
||||
@@ -390,8 +410,8 @@ func (comm *Communication) String() string {
|
||||
return fmt.Sprintf("%s -> *", comm.process.String())
|
||||
default:
|
||||
if comm.process == nil {
|
||||
return fmt.Sprintf("? -> %s", comm.Domain)
|
||||
return fmt.Sprintf("? -> %s", comm.Scope)
|
||||
}
|
||||
return fmt.Sprintf("%s -> %s", comm.process.String(), comm.Domain)
|
||||
return fmt.Sprintf("%s -> %s", comm.process.String(), comm.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
144
network/link.go
144
network/link.go
@@ -7,6 +7,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/network/packet"
|
||||
@@ -16,21 +18,22 @@ import (
|
||||
type FirewallHandler func(pkt packet.Packet, link *Link)
|
||||
|
||||
// Link describes a distinct physical connection (e.g. TCP connection) - like an instance - of a Connection.
|
||||
//nolint:maligned // TODO: fix alignment
|
||||
type Link struct {
|
||||
type Link struct { //nolint:maligned // TODO: fix alignment
|
||||
record.Base
|
||||
sync.Mutex
|
||||
lock sync.Mutex
|
||||
|
||||
ID string
|
||||
ID string
|
||||
Entity *intel.Entity
|
||||
Direction bool
|
||||
|
||||
Verdict Verdict
|
||||
Reason string
|
||||
ReasonID string // format source[:id[:id]]
|
||||
Tunneled bool
|
||||
VerdictPermanent bool
|
||||
Inspect bool
|
||||
Started int64
|
||||
Ended int64
|
||||
RemoteAddress string
|
||||
|
||||
pktQueue chan packet.Packet
|
||||
firewallHandler FirewallHandler
|
||||
@@ -41,70 +44,82 @@ type Link struct {
|
||||
saveWhenFinished bool
|
||||
}
|
||||
|
||||
// Lock locks the link and the link's Entity.
|
||||
func (link *Link) Lock() {
|
||||
link.lock.Lock()
|
||||
link.Entity.Lock()
|
||||
}
|
||||
|
||||
// Lock unlocks the link and the link's Entity.
|
||||
func (link *Link) Unlock() {
|
||||
link.Entity.Unlock()
|
||||
link.lock.Unlock()
|
||||
}
|
||||
|
||||
// Communication returns the Communication the Link is part of
|
||||
func (link *Link) Communication() *Communication {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
return link.comm
|
||||
}
|
||||
|
||||
// GetVerdict returns the current verdict.
|
||||
func (link *Link) GetVerdict() Verdict {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
return link.Verdict
|
||||
}
|
||||
|
||||
// FirewallHandlerIsSet returns whether a firewall handler is set or not
|
||||
func (link *Link) FirewallHandlerIsSet() bool {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
return link.firewallHandler != nil
|
||||
}
|
||||
|
||||
// SetFirewallHandler sets the firewall handler for this link
|
||||
func (link *Link) SetFirewallHandler(handler FirewallHandler) {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
if link.firewallHandler == nil {
|
||||
link.firewallHandler = handler
|
||||
link.pktQueue = make(chan packet.Packet, 1000)
|
||||
|
||||
// start handling
|
||||
module.StartWorker("", func(ctx context.Context) error {
|
||||
module.StartWorker("packet handler", func(ctx context.Context) error {
|
||||
link.packetHandler()
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
link.firewallHandler = handler
|
||||
}
|
||||
|
||||
// StopFirewallHandler unsets the firewall handler
|
||||
func (link *Link) StopFirewallHandler() {
|
||||
link.Lock()
|
||||
link.lock.Lock()
|
||||
link.firewallHandler = nil
|
||||
link.Unlock()
|
||||
link.lock.Unlock()
|
||||
link.pktQueue <- nil
|
||||
}
|
||||
|
||||
// HandlePacket queues packet of Link for handling
|
||||
func (link *Link) HandlePacket(pkt packet.Packet) {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
// get handler
|
||||
link.lock.Lock()
|
||||
handler := link.firewallHandler
|
||||
link.lock.Unlock()
|
||||
|
||||
if link.firewallHandler != nil {
|
||||
// send to queue
|
||||
if handler != nil {
|
||||
link.pktQueue <- pkt
|
||||
return
|
||||
}
|
||||
|
||||
// no handler!
|
||||
log.Warningf("network: link %s does not have a firewallHandler, dropping packet", link)
|
||||
|
||||
err := pkt.Drop()
|
||||
if err != nil {
|
||||
log.Warningf("network: failed to drop packet %s: %s", pkt, err)
|
||||
@@ -119,7 +134,7 @@ func (link *Link) Accept(reason string) {
|
||||
|
||||
// Deny blocks or drops the link depending on the connection direction and adds the given reason.
|
||||
func (link *Link) Deny(reason string) {
|
||||
if link.comm != nil && link.comm.Direction {
|
||||
if link.Direction {
|
||||
link.Drop(reason)
|
||||
} else {
|
||||
link.Block(reason)
|
||||
@@ -151,8 +166,8 @@ func (link *Link) RerouteToTunnel(reason string) {
|
||||
|
||||
// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts
|
||||
func (link *Link) UpdateVerdict(newVerdict Verdict) {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
if newVerdict > link.Verdict {
|
||||
link.Verdict = newVerdict
|
||||
@@ -166,8 +181,8 @@ func (link *Link) AddReason(reason string) {
|
||||
return
|
||||
}
|
||||
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
if link.Reason != "" {
|
||||
link.Reason += " | "
|
||||
@@ -185,9 +200,9 @@ func (link *Link) packetHandler() {
|
||||
return
|
||||
}
|
||||
// get handler
|
||||
link.Lock()
|
||||
link.lock.Lock()
|
||||
handler := link.firewallHandler
|
||||
link.Unlock()
|
||||
link.lock.Unlock()
|
||||
// execute handler or verdict
|
||||
if handler != nil {
|
||||
handler(pkt, link)
|
||||
@@ -201,8 +216,8 @@ func (link *Link) packetHandler() {
|
||||
|
||||
// ApplyVerdict appies the link verdict to a packet.
|
||||
func (link *Link) ApplyVerdict(pkt packet.Packet) {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
var err error
|
||||
|
||||
@@ -251,12 +266,12 @@ func (link *Link) SaveWhenFinished() {
|
||||
|
||||
// SaveIfNeeded saves the Link if it is marked for saving when finished.
|
||||
func (link *Link) SaveIfNeeded() {
|
||||
link.Lock()
|
||||
link.lock.Lock()
|
||||
save := link.saveWhenFinished
|
||||
if save {
|
||||
link.saveWhenFinished = false
|
||||
}
|
||||
link.Unlock()
|
||||
link.lock.Unlock()
|
||||
|
||||
if save {
|
||||
link.saveAndLog()
|
||||
@@ -274,18 +289,18 @@ func (link *Link) saveAndLog() {
|
||||
// save saves the link object in the storage and propagates the change.
|
||||
func (link *Link) save() error {
|
||||
// update link
|
||||
link.Lock()
|
||||
link.lock.Lock()
|
||||
if link.comm == nil {
|
||||
link.Unlock()
|
||||
link.lock.Unlock()
|
||||
return errors.New("cannot save link without comms")
|
||||
}
|
||||
|
||||
if !link.KeyIsSet() {
|
||||
link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.comm.Process().Pid, link.comm.Domain, link.ID))
|
||||
link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.comm.Process().Pid, link.comm.Scope, link.ID))
|
||||
link.UpdateMeta()
|
||||
}
|
||||
link.saveWhenFinished = false
|
||||
link.Unlock()
|
||||
link.lock.Unlock()
|
||||
|
||||
// save link
|
||||
linksLock.RLock()
|
||||
@@ -306,8 +321,8 @@ func (link *Link) save() error {
|
||||
func (link *Link) Delete() {
|
||||
linksLock.Lock()
|
||||
defer linksLock.Unlock()
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
delete(links, link.ID)
|
||||
|
||||
@@ -339,10 +354,15 @@ func GetOrCreateLinkByPacket(pkt packet.Packet) (*Link, bool) {
|
||||
// CreateLinkFromPacket creates a new Link based on Packet.
|
||||
func CreateLinkFromPacket(pkt packet.Packet) *Link {
|
||||
link := &Link{
|
||||
ID: pkt.GetLinkID(),
|
||||
ID: pkt.GetLinkID(),
|
||||
Entity: (&intel.Entity{
|
||||
IP: pkt.Info().RemoteIP(),
|
||||
Protocol: uint8(pkt.Info().Protocol),
|
||||
Port: pkt.Info().RemotePort(),
|
||||
}).Init(),
|
||||
Direction: pkt.IsInbound(),
|
||||
Verdict: VerdictUndecided,
|
||||
Started: time.Now().Unix(),
|
||||
RemoteAddress: pkt.FmtRemoteAddress(),
|
||||
saveWhenFinished: true,
|
||||
}
|
||||
return link
|
||||
@@ -350,59 +370,59 @@ func CreateLinkFromPacket(pkt packet.Packet) *Link {
|
||||
|
||||
// GetActiveInspectors returns the list of active inspectors.
|
||||
func (link *Link) GetActiveInspectors() []bool {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
return link.activeInspectors
|
||||
}
|
||||
|
||||
// SetActiveInspectors sets the list of active inspectors.
|
||||
func (link *Link) SetActiveInspectors(new []bool) {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
link.activeInspectors = new
|
||||
}
|
||||
|
||||
// GetInspectorData returns the list of inspector data.
|
||||
func (link *Link) GetInspectorData() map[uint8]interface{} {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
return link.inspectorData
|
||||
}
|
||||
|
||||
// SetInspectorData set the list of inspector data.
|
||||
func (link *Link) SetInspectorData(new map[uint8]interface{}) {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
link.inspectorData = new
|
||||
}
|
||||
|
||||
// String returns a string representation of Link.
|
||||
func (link *Link) String() string {
|
||||
link.Lock()
|
||||
defer link.Unlock()
|
||||
link.lock.Lock()
|
||||
defer link.lock.Unlock()
|
||||
|
||||
if link.comm == nil {
|
||||
return fmt.Sprintf("? <-> %s", link.RemoteAddress)
|
||||
return fmt.Sprintf("? <-> %s", link.Entity.IP.String())
|
||||
}
|
||||
switch link.comm.Domain {
|
||||
case "I":
|
||||
switch link.comm.Scope {
|
||||
case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
|
||||
if link.comm.process == nil {
|
||||
return fmt.Sprintf("? <- %s", link.RemoteAddress)
|
||||
return fmt.Sprintf("? <- %s", link.Entity.IP.String())
|
||||
}
|
||||
return fmt.Sprintf("%s <- %s", link.comm.process.String(), link.RemoteAddress)
|
||||
case "D":
|
||||
return fmt.Sprintf("%s <- %s", link.comm.process.String(), link.Entity.IP.String())
|
||||
case PeerHost, PeerLAN, PeerInternet, PeerInvalid:
|
||||
if link.comm.process == nil {
|
||||
return fmt.Sprintf("? -> %s", link.RemoteAddress)
|
||||
return fmt.Sprintf("? -> %s", link.Entity.IP.String())
|
||||
}
|
||||
return fmt.Sprintf("%s -> %s", link.comm.process.String(), link.RemoteAddress)
|
||||
return fmt.Sprintf("%s -> %s", link.comm.process.String(), link.Entity.IP.String())
|
||||
default:
|
||||
if link.comm.process == nil {
|
||||
return fmt.Sprintf("? -> %s (%s)", link.comm.Domain, link.RemoteAddress)
|
||||
return fmt.Sprintf("? -> %s (%s)", link.comm.Scope, link.Entity.IP.String())
|
||||
}
|
||||
return fmt.Sprintf("%s to %s (%s)", link.comm.process.String(), link.comm.Domain, link.RemoteAddress)
|
||||
return fmt.Sprintf("%s to %s (%s)", link.comm.process.String(), link.comm.Scope, link.Entity.IP.String())
|
||||
}
|
||||
}
|
||||
|
||||
78
network/reference/ports.go
Normal file
78
network/reference/ports.go
Normal 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
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package reference
|
||||
|
||||
import "strconv"
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
protocolNames = map[uint8]string{
|
||||
@@ -9,20 +12,20 @@ var (
|
||||
6: "TCP",
|
||||
17: "UDP",
|
||||
27: "RDP",
|
||||
58: "ICMPv6",
|
||||
58: "ICMP6",
|
||||
33: "DCCP",
|
||||
136: "UDPLite",
|
||||
136: "UDP-LITE",
|
||||
}
|
||||
|
||||
protocolNumbers = map[string]uint8{
|
||||
"ICMP": 1,
|
||||
"IGMP": 2,
|
||||
"TCP": 6,
|
||||
"UDP": 17,
|
||||
"RDP": 27,
|
||||
"DCCP": 33,
|
||||
"ICMPv6": 58,
|
||||
"UDPLite": 136,
|
||||
"ICMP": 1,
|
||||
"IGMP": 2,
|
||||
"TCP": 6,
|
||||
"UDP": 17,
|
||||
"RDP": 27,
|
||||
"DCCP": 33,
|
||||
"ICMP6": 58,
|
||||
"UDP-LITE": 136,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -37,7 +40,7 @@ func GetProtocolName(protocol uint8) (name string) {
|
||||
|
||||
// GetProtocolNumber returns the number of a IP protocol name.
|
||||
func GetProtocolNumber(protocol string) (number uint8, ok bool) {
|
||||
number, ok = protocolNumbers[protocol]
|
||||
number, ok = protocolNumbers[strings.ToUpper(protocol)]
|
||||
if ok {
|
||||
return number, true
|
||||
}
|
||||
|
||||
@@ -5,36 +5,38 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
"github.com/safing/portmaster/network/netutils"
|
||||
"github.com/safing/portmaster/network/packet"
|
||||
"github.com/safing/portmaster/process"
|
||||
)
|
||||
|
||||
// GetOwnComm returns the communication for the given packet, that originates from
|
||||
// GetOwnComm returns the communication for the given packet, that originates from the Portmaster itself.
|
||||
func GetOwnComm(pkt packet.Packet) (*Communication, error) {
|
||||
var domain string
|
||||
var scope string
|
||||
|
||||
// Incoming
|
||||
if pkt.IsInbound() {
|
||||
switch netutils.ClassifyIP(pkt.Info().RemoteIP()) {
|
||||
case netutils.HostLocal:
|
||||
domain = IncomingHost
|
||||
scope = IncomingHost
|
||||
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
|
||||
domain = IncomingLAN
|
||||
scope = IncomingLAN
|
||||
case netutils.Global, netutils.GlobalMulticast:
|
||||
domain = IncomingInternet
|
||||
scope = IncomingInternet
|
||||
case netutils.Invalid:
|
||||
domain = IncomingInvalid
|
||||
scope = IncomingInvalid
|
||||
}
|
||||
|
||||
communication, ok := GetCommunication(os.Getpid(), domain)
|
||||
communication, ok := GetCommunication(os.Getpid(), scope)
|
||||
if !ok {
|
||||
proc, err := process.GetOrFindProcess(pkt.Ctx(), os.Getpid())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get own process")
|
||||
}
|
||||
communication = &Communication{
|
||||
Domain: domain,
|
||||
Scope: scope,
|
||||
Entity: (&intel.Entity{}).Init(),
|
||||
Direction: Inbound,
|
||||
process: proc,
|
||||
Inspect: true,
|
||||
@@ -48,23 +50,24 @@ func GetOwnComm(pkt packet.Packet) (*Communication, error) {
|
||||
// PeerToPeer
|
||||
switch netutils.ClassifyIP(pkt.Info().RemoteIP()) {
|
||||
case netutils.HostLocal:
|
||||
domain = PeerHost
|
||||
scope = PeerHost
|
||||
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
|
||||
domain = PeerLAN
|
||||
scope = PeerLAN
|
||||
case netutils.Global, netutils.GlobalMulticast:
|
||||
domain = PeerInternet
|
||||
scope = PeerInternet
|
||||
case netutils.Invalid:
|
||||
domain = PeerInvalid
|
||||
scope = PeerInvalid
|
||||
}
|
||||
|
||||
communication, ok := GetCommunication(os.Getpid(), domain)
|
||||
communication, ok := GetCommunication(os.Getpid(), scope)
|
||||
if !ok {
|
||||
proc, err := process.GetOrFindProcess(pkt.Ctx(), os.Getpid())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get own process")
|
||||
}
|
||||
communication = &Communication{
|
||||
Domain: domain,
|
||||
Scope: scope,
|
||||
Entity: (&intel.Entity{}).Init(),
|
||||
Direction: Outbound,
|
||||
process: proc,
|
||||
Inspect: true,
|
||||
|
||||
@@ -42,7 +42,7 @@ const (
|
||||
Outbound = false
|
||||
)
|
||||
|
||||
// Non-Domain Connections
|
||||
// Non-Domain Scopes
|
||||
const (
|
||||
IncomingHost = "IH"
|
||||
IncomingLAN = "IL"
|
||||
|
||||
@@ -3,6 +3,7 @@ package network
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
"github.com/safing/portmaster/network/netutils"
|
||||
"github.com/safing/portmaster/network/packet"
|
||||
"github.com/safing/portmaster/process"
|
||||
@@ -43,11 +44,12 @@ func GetUnknownCommunication(pkt packet.Packet) (*Communication, error) {
|
||||
return getOrCreateUnknownCommunication(pkt, PeerInvalid)
|
||||
}
|
||||
|
||||
func getOrCreateUnknownCommunication(pkt packet.Packet, connClass string) (*Communication, error) {
|
||||
connection, ok := GetCommunication(process.UnknownProcess.Pid, connClass)
|
||||
func getOrCreateUnknownCommunication(pkt packet.Packet, connScope string) (*Communication, error) {
|
||||
connection, ok := GetCommunication(process.UnknownProcess.Pid, connScope)
|
||||
if !ok {
|
||||
connection = &Communication{
|
||||
Domain: connClass,
|
||||
Scope: connScope,
|
||||
Entity: (&intel.Entity{}).Init(),
|
||||
Direction: pkt.IsInbound(),
|
||||
Verdict: VerdictDrop,
|
||||
Reason: ReasonUnknownProcess,
|
||||
|
||||
29
process/config.go
Normal file
29
process/config.go
Normal 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
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/profile"
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
@@ -90,11 +89,7 @@ func (p *Process) Delete() {
|
||||
go dbController.PushUpdate(p)
|
||||
}
|
||||
|
||||
// deactivate profile
|
||||
// TODO: check if there is another process using the same profile set
|
||||
if p.profileSet != nil {
|
||||
profile.DeactivateProfileSet(p.profileSet)
|
||||
}
|
||||
// TODO: maybe mark the assigned profiles as no longer needed?
|
||||
}
|
||||
|
||||
// CleanProcessStorage cleans the storage from old processes.
|
||||
|
||||
@@ -56,6 +56,11 @@ func GetPidByPacket(pkt packet.Packet) (pid int, direction bool, err error) {
|
||||
|
||||
// GetProcessByPacket returns the process that owns the given packet.
|
||||
func GetProcessByPacket(pkt packet.Packet) (process *Process, direction bool, err error) {
|
||||
if !enableProcessDetection() {
|
||||
log.Tracer(pkt.Ctx()).Tracef("process: process detection disabled")
|
||||
return UnknownProcess, direction, nil
|
||||
}
|
||||
|
||||
log.Tracer(pkt.Ctx()).Tracef("process: getting process and profile by packet")
|
||||
|
||||
var pid int
|
||||
@@ -75,10 +80,9 @@ func GetProcessByPacket(pkt packet.Packet) (process *Process, direction bool, er
|
||||
return nil, direction, err
|
||||
}
|
||||
|
||||
err = process.FindProfiles(pkt.Ctx())
|
||||
err = process.GetProfile(pkt.Ctx())
|
||||
if err != nil {
|
||||
log.Tracer(pkt.Ctx()).Errorf("process: failed to find profiles for process %s: %s", process, err)
|
||||
log.Errorf("failed to find profiles for process %s: %s", process, err)
|
||||
log.Tracer(pkt.Ctx()).Errorf("process: failed to get profile for process %s: %s", process, err)
|
||||
}
|
||||
|
||||
return process, direction, nil
|
||||
@@ -110,6 +114,11 @@ func GetPidByEndpoints(localIP net.IP, localPort uint16, remoteIP net.IP, remote
|
||||
|
||||
// GetProcessByEndpoints returns the process that owns the described link.
|
||||
func GetProcessByEndpoints(ctx context.Context, localIP net.IP, localPort uint16, remoteIP net.IP, remotePort uint16, protocol packet.IPProtocol) (process *Process, err error) {
|
||||
if !enableProcessDetection() {
|
||||
log.Tracer(ctx).Tracef("process: process detection disabled")
|
||||
return UnknownProcess, nil
|
||||
}
|
||||
|
||||
log.Tracer(ctx).Tracef("process: getting process and profile by endpoints")
|
||||
|
||||
var pid int
|
||||
@@ -129,10 +138,9 @@ func GetProcessByEndpoints(ctx context.Context, localIP net.IP, localPort uint16
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = process.FindProfiles(ctx)
|
||||
err = process.GetProfile(ctx)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Errorf("process: failed to find profiles for process %s: %s", process, err)
|
||||
log.Errorf("process: failed to find profiles for process %s: %s", process, err)
|
||||
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
|
||||
}
|
||||
|
||||
return process, nil
|
||||
|
||||
@@ -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
17
process/module.go
Normal 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()
|
||||
}
|
||||
@@ -17,7 +17,7 @@ var (
|
||||
)
|
||||
|
||||
// GetPidOfInode returns the pid of the given uid and socket inode.
|
||||
func GetPidOfInode(uid, inode int) (int, bool) {
|
||||
func GetPidOfInode(uid, inode int) (int, bool) { //nolint:gocognit // TODO
|
||||
pidsByUserLock.Lock()
|
||||
defer pidsByUserLock.Unlock()
|
||||
|
||||
|
||||
@@ -40,10 +40,10 @@ type Process struct {
|
||||
// ExecOwner ...
|
||||
// ExecSignature ...
|
||||
|
||||
UserProfileKey string
|
||||
profileSet *profile.Set
|
||||
Name string
|
||||
Icon string
|
||||
LocalProfileKey string
|
||||
profile *profile.LayeredProfile
|
||||
Name string
|
||||
Icon string
|
||||
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for database cache path or "c:"/"a:" for a the icon key to fetch it from a company / authoritative node and cache it in its own cache.
|
||||
|
||||
FirstCommEstablished int64
|
||||
@@ -53,12 +53,12 @@ type Process struct {
|
||||
Error string // If this is set, the process is invalid. This is used to cache failing or inexistent processes.
|
||||
}
|
||||
|
||||
// ProfileSet returns the assigned profile set.
|
||||
func (p *Process) ProfileSet() *profile.Set {
|
||||
// Profile returns the assigned layered profile.
|
||||
func (p *Process) Profile() *profile.LayeredProfile {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
return p.profileSet
|
||||
return p.profile
|
||||
}
|
||||
|
||||
// Strings returns a string representation of process.
|
||||
@@ -208,13 +208,14 @@ func GetOrFindProcess(ctx context.Context, pid int) (*Process, error) {
|
||||
|
||||
func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) {
|
||||
dupReqLock.Lock()
|
||||
defer dupReqLock.Unlock()
|
||||
|
||||
// get duplicate request waitgroup
|
||||
wg, requestActive := dupReqMap[pid]
|
||||
|
||||
// someone else is already on it!
|
||||
if requestActive {
|
||||
dupReqLock.Unlock()
|
||||
|
||||
// log that we are waiting
|
||||
log.Tracer(ctx).Tracef("intel: waiting for duplicate request for PID %d to complete", pid)
|
||||
// wait
|
||||
@@ -232,6 +233,8 @@ func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) {
|
||||
// add to registry
|
||||
dupReqMap[pid] = wg
|
||||
|
||||
dupReqLock.Unlock()
|
||||
|
||||
// return function to mark request as finished
|
||||
return func() {
|
||||
dupReqLock.Lock()
|
||||
|
||||
42
process/profile.go
Normal file
42
process/profile.go
Normal 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
|
||||
}
|
||||
@@ -1,76 +1,44 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
var (
|
||||
activeProfileSets = make(map[string]*Set)
|
||||
activeProfileSetsLock sync.RWMutex
|
||||
// TODO: periodically clean up inactive profiles
|
||||
activeProfiles = make(map[string]*Profile)
|
||||
activeProfilesLock sync.RWMutex
|
||||
)
|
||||
|
||||
func activateProfileSet(ctx context.Context, set *Set) {
|
||||
activeProfileSetsLock.Lock()
|
||||
defer activeProfileSetsLock.Unlock()
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
activeProfileSets[set.id] = set
|
||||
log.Tracer(ctx).Tracef("profile: activated profile set %s", set.id)
|
||||
}
|
||||
// getActiveProfile returns a cached copy of an active profile and nil if it isn't found.
|
||||
func getActiveProfile(scopedID string) *Profile {
|
||||
activeProfilesLock.Lock()
|
||||
defer activeProfilesLock.Unlock()
|
||||
|
||||
// DeactivateProfileSet marks a profile set as not active.
|
||||
func DeactivateProfileSet(set *Set) {
|
||||
activeProfileSetsLock.Lock()
|
||||
defer activeProfileSetsLock.Unlock()
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
delete(activeProfileSets, set.id)
|
||||
log.Tracef("profile: deactivated profile set %s", set.id)
|
||||
}
|
||||
|
||||
func updateActiveProfile(profile *Profile, userProfile bool) {
|
||||
activeProfileSetsLock.RLock()
|
||||
defer activeProfileSetsLock.RUnlock()
|
||||
|
||||
var activeProfile *Profile
|
||||
var profilesUpdated bool
|
||||
|
||||
// iterate all active profile sets
|
||||
for _, activeSet := range activeProfileSets {
|
||||
activeSet.Lock()
|
||||
|
||||
if userProfile {
|
||||
activeProfile = activeSet.profiles[0]
|
||||
} else {
|
||||
activeProfile = activeSet.profiles[2]
|
||||
}
|
||||
|
||||
// check if profile exists (for stamp profiles)
|
||||
if activeProfile != nil {
|
||||
activeProfile.Lock()
|
||||
|
||||
// check if the stamp profile has the same ID
|
||||
if activeProfile.ID == profile.ID {
|
||||
if userProfile {
|
||||
activeSet.profiles[0] = profile
|
||||
log.Infof("profile: updated active user profile %s (%s)", profile.ID, profile.LinkedPath)
|
||||
} else {
|
||||
activeSet.profiles[2] = profile
|
||||
log.Infof("profile: updated active stamp profile %s", profile.ID)
|
||||
}
|
||||
profilesUpdated = true
|
||||
}
|
||||
|
||||
activeProfile.Unlock()
|
||||
}
|
||||
|
||||
activeSet.Unlock()
|
||||
profile, ok := activeProfiles[scopedID]
|
||||
if ok {
|
||||
return profile
|
||||
}
|
||||
|
||||
if profilesUpdated {
|
||||
increaseUpdateVersion()
|
||||
return nil
|
||||
}
|
||||
|
||||
// markProfileActive registers a profile as active.
|
||||
func markProfileActive(profile *Profile) {
|
||||
activeProfilesLock.Lock()
|
||||
defer activeProfilesLock.Unlock()
|
||||
|
||||
activeProfiles[profile.ScopedID()] = profile
|
||||
}
|
||||
|
||||
// markActiveProfileAsOutdated marks an active profile as outdated, so that it will be refetched from the database.
|
||||
func markActiveProfileAsOutdated(scopedID string) {
|
||||
activeProfilesLock.Lock()
|
||||
defer activeProfilesLock.Unlock()
|
||||
|
||||
profile, ok := activeProfiles[scopedID]
|
||||
if ok {
|
||||
profile.oudated.Set()
|
||||
delete(activeProfiles, scopedID)
|
||||
}
|
||||
}
|
||||
|
||||
94
profile/config-update.go
Normal file
94
profile/config-update.go
Normal 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
270
profile/config.go
Normal 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
|
||||
}
|
||||
@@ -1,22 +1,105 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/database/record"
|
||||
)
|
||||
|
||||
// core:profiles/user/12345-1234-125-1234-1235
|
||||
// core:profiles/special/default
|
||||
// /global
|
||||
// core:profiles/stamp/12334-1235-1234-5123-1234
|
||||
// core:profiles/identifier/base64
|
||||
// Database paths:
|
||||
// core:profiles/<scope>/<id>
|
||||
// cache:profiles/index/<identifier>/<value>
|
||||
|
||||
// Namespaces
|
||||
const (
|
||||
UserNamespace = "user"
|
||||
StampNamespace = "stamp"
|
||||
SpecialNamespace = "special"
|
||||
profilesDBPath = "core:profiles/"
|
||||
)
|
||||
|
||||
var (
|
||||
profileDB = database.NewInterface(nil)
|
||||
)
|
||||
|
||||
func makeScopedID(source, id string) string {
|
||||
return source + "/" + id
|
||||
}
|
||||
|
||||
func makeProfileKey(source, id string) string {
|
||||
return profilesDBPath + source + "/" + id
|
||||
}
|
||||
|
||||
func registerValidationDBHook() (err error) {
|
||||
_, err = database.RegisterHook(query.New(profilesDBPath), &databaseHook{})
|
||||
return
|
||||
}
|
||||
|
||||
func startProfileUpdateChecker() error {
|
||||
profilesSub, err := profileDB.Subscribe(query.New(profilesDBPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
module.StartServiceWorker("update active profiles", 0, func(ctx context.Context) (err error) {
|
||||
feedSelect:
|
||||
for {
|
||||
select {
|
||||
case r := <-profilesSub.Feed:
|
||||
// check if nil
|
||||
if r == nil {
|
||||
return errors.New("subscription canceled")
|
||||
}
|
||||
|
||||
// check if internal save
|
||||
if !r.IsWrapped() {
|
||||
profile, ok := r.(*Profile)
|
||||
if ok && profile.internalSave {
|
||||
continue feedSelect
|
||||
}
|
||||
}
|
||||
|
||||
// mark as outdated
|
||||
markActiveProfileAsOutdated(strings.TrimPrefix(r.Key(), profilesDBPath))
|
||||
case <-ctx.Done():
|
||||
return profilesSub.Cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type databaseHook struct {
|
||||
database.HookBase
|
||||
}
|
||||
|
||||
// UsesPrePut implements the Hook interface and returns false.
|
||||
func (h *databaseHook) UsesPrePut() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PrePut implements the Hook interface.
|
||||
func (h *databaseHook) PrePut(r record.Record) (record.Record, error) {
|
||||
// convert
|
||||
profile, err := EnsureProfile(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// prepare config
|
||||
err = profile.prepConfig()
|
||||
if err != nil {
|
||||
// error here, warning when loading
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse config
|
||||
err = profile.parseConfig()
|
||||
if err != nil {
|
||||
// error here, warning when loading
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
25
profile/endpoints/endpoint-any.go
Normal file
25
profile/endpoints/endpoint-any.go
Normal 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
|
||||
}
|
||||
58
profile/endpoints/endpoint-asn.go
Normal file
58
profile/endpoints/endpoint-asn.go
Normal 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
|
||||
}
|
||||
50
profile/endpoints/endpoint-country.go
Normal file
50
profile/endpoints/endpoint-country.go
Normal 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
|
||||
}
|
||||
123
profile/endpoints/endpoint-domain.go
Normal file
123
profile/endpoints/endpoint-domain.go
Normal 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
|
||||
}
|
||||
42
profile/endpoints/endpoint-ip.go
Normal file
42
profile/endpoints/endpoint-ip.go
Normal 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
|
||||
}
|
||||
42
profile/endpoints/endpoint-iprange.go
Normal file
42
profile/endpoints/endpoint-iprange.go
Normal 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
|
||||
}
|
||||
46
profile/endpoints/endpoint-lists.go
Normal file
46
profile/endpoints/endpoint-lists.go
Normal 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
|
||||
}
|
||||
211
profile/endpoints/endpoint.go
Normal file
211
profile/endpoints/endpoint.go
Normal 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)
|
||||
}
|
||||
83
profile/endpoints/endpoint_test.go
Normal file
83
profile/endpoints/endpoint_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
93
profile/endpoints/endpoints.go
Normal file
93
profile/endpoints/endpoints.go
Normal 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"
|
||||
}
|
||||
}
|
||||
353
profile/endpoints/endpoints_test.go
Normal file
353
profile/endpoints/endpoints_test.go
Normal 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
|
||||
}
|
||||
@@ -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
54
profile/find.go
Normal 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
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package profile
|
||||
|
||||
import "time"
|
||||
|
||||
var (
|
||||
fingerprintWeights = map[string]int{
|
||||
"full_path": 2,
|
||||
@@ -35,6 +33,8 @@ func GetFingerprintWeight(fpType string) (weight int) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// TODO: move to profile
|
||||
/*
|
||||
// AddFingerprint adds the given fingerprint to the profile.
|
||||
func (profile *Profile) AddFingerprint(fp *Fingerprint) {
|
||||
if fp.OS == "" {
|
||||
@@ -46,3 +46,4 @@ func (profile *Profile) AddFingerprint(fp *Fingerprint) {
|
||||
|
||||
profile.Fingerprints = append(profile.Fingerprints, fp)
|
||||
}
|
||||
*/
|
||||
130
profile/flags.go
130
profile/flags.go
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/log"
|
||||
|
||||
"github.com/safing/portbase/modules"
|
||||
|
||||
// module dependencies
|
||||
@@ -8,22 +10,42 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
shutdownSignal = make(chan struct{})
|
||||
module *modules.Module
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register("profile", nil, start, stop, "core")
|
||||
module = modules.Register("profiles", prep, start, nil, "core")
|
||||
}
|
||||
|
||||
func start() error {
|
||||
err := initSpecialProfiles()
|
||||
func prep() error {
|
||||
err := registerConfiguration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = registerConfigUpdater()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return initUpdateListener()
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
close(shutdownSignal)
|
||||
return nil
|
||||
}
|
||||
|
||||
func start() error {
|
||||
err := registerValidationDBHook()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = startProfileUpdateChecker()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateGlobalConfigProfile(module.Ctx, nil)
|
||||
if err != nil {
|
||||
log.Warningf("profile: error during loading global profile from configuration: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
306
profile/profile-layered.go
Normal file
306
profile/profile-layered.go
Normal 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
|
||||
}
|
||||
@@ -1,78 +1,180 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portmaster/status"
|
||||
"github.com/safing/portmaster/profile/endpoints"
|
||||
)
|
||||
|
||||
var (
|
||||
lastUsedUpdateThreshold = 1 * time.Hour
|
||||
)
|
||||
|
||||
// Profile Sources
|
||||
const (
|
||||
SourceLocal string = "local"
|
||||
SourceCommunity string = "community"
|
||||
SourceEnterprise string = "enterprise"
|
||||
SourceGlobal string = "global"
|
||||
)
|
||||
|
||||
// Default Action IDs
|
||||
const (
|
||||
DefaultActionNotSet uint8 = 0
|
||||
DefaultActionBlock uint8 = 1
|
||||
DefaultActionAsk uint8 = 2
|
||||
DefaultActionPermit uint8 = 3
|
||||
)
|
||||
|
||||
// Profile is used to predefine a security profile for applications.
|
||||
type Profile struct {
|
||||
type Profile struct { //nolint:maligned // not worth the effort
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
// Profile Metadata
|
||||
ID string
|
||||
// Identity
|
||||
ID string
|
||||
Source string
|
||||
|
||||
// App Information
|
||||
Name string
|
||||
Description string
|
||||
Homepage string
|
||||
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for a database path or "e:" for the encoded data.
|
||||
Icon string
|
||||
|
||||
// User Profile Only
|
||||
LinkedPath string
|
||||
StampProfileID string
|
||||
StampProfileAssigned int64
|
||||
// References - local profiles only
|
||||
// LinkedPath is a filesystem path to the executable this profile was created for.
|
||||
LinkedPath string
|
||||
// LinkedProfiles is a list of other profiles
|
||||
LinkedProfiles []string
|
||||
|
||||
// Fingerprints
|
||||
Fingerprints []*Fingerprint
|
||||
// TODO: Fingerprints []*Fingerprint
|
||||
|
||||
// Configuration
|
||||
// The mininum security level to apply to connections made with this profile
|
||||
SecurityLevel uint8
|
||||
Flags Flags
|
||||
Endpoints Endpoints
|
||||
ServiceEndpoints Endpoints
|
||||
SecurityLevel uint8
|
||||
Config map[string]interface{}
|
||||
|
||||
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process must be found
|
||||
// Framework *Framework `json:",omitempty bson:",omitempty"`
|
||||
// Interpreted Data
|
||||
configPerspective *config.Perspective
|
||||
dataParsed bool
|
||||
defaultAction uint8
|
||||
endpoints endpoints.Endpoints
|
||||
serviceEndpoints endpoints.Endpoints
|
||||
|
||||
// When this Profile was approximately last used (for performance reasons not every single usage is saved)
|
||||
Created int64
|
||||
// Lifecycle Management
|
||||
oudated *abool.AtomicBool
|
||||
|
||||
// Framework
|
||||
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process/actor must be found
|
||||
// TODO: Framework *Framework
|
||||
|
||||
// When this Profile was approximately last used.
|
||||
// For performance reasons not every single usage is saved.
|
||||
ApproxLastUsed int64
|
||||
Created int64
|
||||
|
||||
internalSave bool
|
||||
}
|
||||
|
||||
func (profile *Profile) prepConfig() (err error) {
|
||||
// prepare configuration
|
||||
profile.configPerspective, err = config.NewPerspective(profile.Config)
|
||||
profile.oudated = abool.New()
|
||||
return
|
||||
}
|
||||
|
||||
func (profile *Profile) parseConfig() error {
|
||||
if profile.configPerspective == nil {
|
||||
return errors.New("config not prepared")
|
||||
}
|
||||
|
||||
// check if already parsed
|
||||
if profile.dataParsed {
|
||||
return nil
|
||||
}
|
||||
profile.dataParsed = true
|
||||
|
||||
var err error
|
||||
var lastErr error
|
||||
|
||||
action, ok := profile.configPerspective.GetAsString(CfgOptionBlockInboundKey)
|
||||
if ok {
|
||||
switch action {
|
||||
case "permit":
|
||||
profile.defaultAction = DefaultActionPermit
|
||||
case "ask":
|
||||
profile.defaultAction = DefaultActionAsk
|
||||
case "block":
|
||||
profile.defaultAction = DefaultActionBlock
|
||||
default:
|
||||
lastErr = fmt.Errorf(`default action "%s" invalid`, action)
|
||||
}
|
||||
}
|
||||
|
||||
list, ok := profile.configPerspective.GetAsStringArray(CfgOptionEndpointsKey)
|
||||
if ok {
|
||||
profile.endpoints, err = endpoints.ParseEndpoints(list)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionServiceEndpointsKey)
|
||||
if ok {
|
||||
profile.serviceEndpoints, err = endpoints.ParseEndpoints(list)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// New returns a new Profile.
|
||||
func New() *Profile {
|
||||
return &Profile{
|
||||
profile := &Profile{
|
||||
ID: uuid.NewV4().String(),
|
||||
Source: SourceLocal,
|
||||
Created: time.Now().Unix(),
|
||||
Config: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// create placeholders
|
||||
_ = profile.prepConfig()
|
||||
_ = profile.parseConfig()
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
// MakeProfileKey creates the correct key for a profile with the given namespace and ID.
|
||||
func MakeProfileKey(namespace, id string) string {
|
||||
return fmt.Sprintf("core:profiles/%s/%s", namespace, id)
|
||||
// ScopedID returns the scoped ID (Source + ID) of the profile.
|
||||
func (profile *Profile) ScopedID() string {
|
||||
return makeScopedID(profile.Source, profile.ID)
|
||||
}
|
||||
|
||||
// Save saves the profile to the database
|
||||
func (profile *Profile) Save(namespace string) error {
|
||||
func (profile *Profile) Save() error {
|
||||
if profile.ID == "" {
|
||||
profile.ID = uuid.NewV4().String()
|
||||
return errors.New("profile: tried to save profile without ID")
|
||||
}
|
||||
if profile.Source == "" {
|
||||
return fmt.Errorf("profile: profile %s does not specify a source", profile.ID)
|
||||
}
|
||||
|
||||
if !profile.KeyIsSet() {
|
||||
if namespace == "" {
|
||||
return fmt.Errorf("no key or namespace defined for profile %s", profile.String())
|
||||
}
|
||||
profile.SetKey(MakeProfileKey(namespace, profile.ID))
|
||||
profile.SetKey(makeProfileKey(profile.Source, profile.ID))
|
||||
}
|
||||
|
||||
return profileDB.Put(profile)
|
||||
@@ -92,27 +194,86 @@ func (profile *Profile) String() string {
|
||||
return profile.Name
|
||||
}
|
||||
|
||||
// DetailedString returns a more detailed string representation of theProfile.
|
||||
func (profile *Profile) DetailedString() string {
|
||||
return fmt.Sprintf("%s(SL=%s Flags=%s Endpoints=%s)", profile.Name, status.FmtSecurityLevel(profile.SecurityLevel), profile.Flags.String(), profile.Endpoints.String())
|
||||
// AddEndpoint adds an endpoint to the endpoint list, saves the profile and reloads the configuration.
|
||||
func (profile *Profile) AddEndpoint(newEntry string) {
|
||||
profile.addEndpointyEntry(CfgOptionEndpointsKey, newEntry)
|
||||
}
|
||||
|
||||
// GetUserProfile loads a profile from the database.
|
||||
func GetUserProfile(id string) (*Profile, error) {
|
||||
return getProfile(UserNamespace, id)
|
||||
// AddServiceEndpoint adds a service endpoint to the endpoint list, saves the profile and reloads the configuration.
|
||||
func (profile *Profile) AddServiceEndpoint(newEntry string) {
|
||||
profile.addEndpointyEntry(CfgOptionServiceEndpointsKey, newEntry)
|
||||
}
|
||||
|
||||
// GetStampProfile loads a profile from the database.
|
||||
func GetStampProfile(id string) (*Profile, error) {
|
||||
return getProfile(StampNamespace, id)
|
||||
func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) {
|
||||
profile.Lock()
|
||||
// get, update, save endpoints list
|
||||
endpointList, ok := profile.configPerspective.GetAsStringArray(cfgKey)
|
||||
if !ok {
|
||||
endpointList = make([]string, 0, 1)
|
||||
}
|
||||
endpointList = append(endpointList, newEntry)
|
||||
profile.Config[cfgKey] = endpointList
|
||||
|
||||
// save without full reload
|
||||
profile.internalSave = true
|
||||
profile.Unlock()
|
||||
err := profile.Save()
|
||||
if err != nil {
|
||||
log.Warningf("profile: failed to save profile after adding endpoint: %s", err)
|
||||
}
|
||||
|
||||
// reload manually
|
||||
err = profile.parseConfig()
|
||||
if err != nil {
|
||||
log.Warningf("profile: failed to parse profile config after adding endpoint: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getProfile(namespace, id string) (*Profile, error) {
|
||||
r, err := profileDB.Get(MakeProfileKey(namespace, id))
|
||||
// GetProfile loads a profile from the database.
|
||||
func GetProfile(source, id string) (*Profile, error) {
|
||||
return GetProfileByScopedID(makeScopedID(source, id))
|
||||
}
|
||||
|
||||
// GetProfileByScopedID loads a profile from the database using a scoped ID like "local/id" or "community/id".
|
||||
func GetProfileByScopedID(scopedID string) (*Profile, error) {
|
||||
// check cache
|
||||
profile := getActiveProfile(scopedID)
|
||||
if profile != nil {
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// get from database
|
||||
r, err := profileDB.Get(profilesDBPath + scopedID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return EnsureProfile(r)
|
||||
|
||||
// convert
|
||||
profile, err = EnsureProfile(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// lock for prepping
|
||||
profile.Lock()
|
||||
defer profile.Unlock()
|
||||
|
||||
// prepare config
|
||||
err = profile.prepConfig()
|
||||
if err != nil {
|
||||
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
||||
}
|
||||
|
||||
// parse config
|
||||
err = profile.parseConfig()
|
||||
if err != nil {
|
||||
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
||||
}
|
||||
|
||||
// mark active
|
||||
markProfileActive(profile)
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// EnsureProfile ensures that the given record is a *Profile, and returns it.
|
||||
|
||||
188
profile/set.go
188
profile/set.go
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
203
resolver/config.go
Normal file
203
resolver/config.go
Normal 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, ", ")
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Package intel is responsible for fetching intelligence data, including DNS, on remote entities.
|
||||
package resolver is responsible for fetching intelligence data, including DNS, on remote entities.
|
||||
|
||||
DNS Servers
|
||||
|
||||
@@ -27,4 +27,4 @@ All other domains are resolved using search scopes and all available resolvers.
|
||||
|
||||
|
||||
*/
|
||||
package intel
|
||||
package resolver
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/intel"
|
||||
|
||||
// module dependencies
|
||||
_ "github.com/safing/portmaster/core"
|
||||
@@ -16,10 +17,12 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("intel", prep, start, nil, "core", "network")
|
||||
module = modules.Register("resolver", prep, start, nil, "core", "network")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
intel.SetReverseResolver(ResolveIPAndValidate)
|
||||
|
||||
return prepConfig()
|
||||
}
|
||||
|
||||
6
resolver/main_test.go
Normal file
6
resolver/main_test.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
// portmaster tests helper
|
||||
_ "github.com/safing/portmaster/core/pmtesting"
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -313,7 +313,7 @@ func listenForDNSPackets(conn *net.UDPConn, messages chan *dns.Msg) error {
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
if module.ShutdownInProgress() {
|
||||
if module.IsStopping() {
|
||||
return nil
|
||||
}
|
||||
log.Debugf("intel: failed to read packet: %s", err)
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -158,13 +158,14 @@ func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
|
||||
dupKey := fmt.Sprintf("%s%s", q.FQDN, q.QType.String())
|
||||
|
||||
dupReqLock.Lock()
|
||||
defer dupReqLock.Unlock()
|
||||
|
||||
// get duplicate request waitgroup
|
||||
wg, requestActive := dupReqMap[dupKey]
|
||||
|
||||
// someone else is already on it!
|
||||
if requestActive {
|
||||
dupReqLock.Unlock()
|
||||
|
||||
// log that we are waiting
|
||||
log.Tracer(ctx).Tracef("intel: waiting for duplicate query for %s to complete", dupKey)
|
||||
// wait
|
||||
@@ -182,6 +183,8 @@ func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
|
||||
// add to registry
|
||||
dupReqMap[dupKey] = wg
|
||||
|
||||
dupReqLock.Unlock()
|
||||
|
||||
// return function to mark request as finished
|
||||
return func() {
|
||||
dupReqLock.Lock()
|
||||
@@ -222,7 +225,7 @@ resolveLoop:
|
||||
rrCache, err = resolver.Conn.Query(ctx, q)
|
||||
if err != nil {
|
||||
|
||||
// FIXME: check if we are online?
|
||||
// TODO: check if we are online?
|
||||
|
||||
switch {
|
||||
case errors.Is(err, ErrNotFound):
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
// DISABLE TESTING FOR NOW: find a way to have tests with the module system
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -114,7 +114,7 @@ func domainInScope(dotPrefixedFQDN string, scopeList []string) bool {
|
||||
}
|
||||
|
||||
// GetResolversInScope returns all resolvers that are in scope the resolve the given query and options.
|
||||
func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) {
|
||||
func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) { //nolint:gocognit // TODO
|
||||
resolversLock.RLock()
|
||||
defer resolversLock.RUnlock()
|
||||
|
||||
@@ -226,13 +226,13 @@ func (q *Query) checkCompliance() error {
|
||||
}
|
||||
|
||||
// special TLDs
|
||||
if doNotResolveSpecialDomains(q.SecurityLevel) &&
|
||||
if dontResolveSpecialDomains(q.SecurityLevel) &&
|
||||
domainInScope(q.dotPrefixedFQDN, specialServiceScopes) {
|
||||
return ErrSpecialDomainsDisabled
|
||||
}
|
||||
|
||||
// testing TLDs
|
||||
if doNotResolveTestDomains(q.SecurityLevel) &&
|
||||
if dontResolveTestDomains(q.SecurityLevel) &&
|
||||
domainInScope(q.dotPrefixedFQDN, localTestScopes) {
|
||||
return ErrTestDomainsDisabled
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
|
||||
return errSkip
|
||||
}
|
||||
|
||||
if doNotUseInsecureProtocols(q.SecurityLevel) {
|
||||
if noInsecureProtocols(q.SecurityLevel) {
|
||||
switch resolver.ServerType {
|
||||
case ServerTypeDNS:
|
||||
return errInsecureProtocol
|
||||
@@ -260,13 +260,13 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
|
||||
}
|
||||
}
|
||||
|
||||
if doNotUseAssignedNameservers(q.SecurityLevel) {
|
||||
if noAssignedNameservers(q.SecurityLevel) {
|
||||
if resolver.Source == ServerSourceAssigned {
|
||||
return errAssignedServer
|
||||
}
|
||||
}
|
||||
|
||||
if doNotUseMulticastDNS(q.SecurityLevel) {
|
||||
if noMulticastDNS(q.SecurityLevel) {
|
||||
if resolver.Source == ServerSourceMDNS {
|
||||
return errMulticastDNS
|
||||
}
|
||||
153
resolver/resolver.go
Normal file
153
resolver/resolver.go
Normal 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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package intel
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user