From 5d438c90b1fceb51d2ed3e6db1a3b1d463e73444 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 Apr 2020 22:21:11 +0200 Subject: [PATCH 01/30] Update README.md --- README.md | 106 ++++++++++++++++-------------------------------------- 1 file changed, 31 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index b8caab10..0e3685b4 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,54 @@ -# Portmaster +# Portmaster Wiki -The Portmaster enables you to protect your data on your device. You are back in charge of your outgoing connections: you choose what data you share and what data stays private. Read more on [docs.safing.io](http://docs.safing.io/). +The Portmaster is a privacy app that at its core simply intercepts _all_ your network connections. Different modules with different privacy features are built on top of it, which can all be enabled or disabled as desired: -## Current Status +![portmaster modules](https://safing.io/assets/img/portmaster/modules.png) -**NOTE: Portmaster is currently in development freeze in order to focus on our upcoming [privacy network](https://safing.io/technology/#gate17) (Codename: Gate17)** +#### ⚠️ Disclaimer -The Portmaster is currently in alpha. Expect dragons. -Supported platforms: +> The Portmaster is still in its early "pre-alpha" development stage. It is functional, but has not yet been tested widely. We are glad if you want to try out the Portmaster right away but please expect bugs and rather technical problems. We'll push updates and fixes as we go. A list of known problems can be found at the bottom of this page. -- linux_amd64 -- windows_amd64 (_soon_) -- darwin_amd64 (_later_) +#### πŸ”„ Automatic Updates -## Using the Alpha Version +We have set up update servers so we can push fixes and improvements as we go. -#### Must-Know Basics +# Modules -The Portmaster is all about protecting your privacy. As soon as it starts, it will start to intercept network connections. If other programs are already running, this may cause them to lose Internet connectivity for a short duration. +## DNS-over-TLS Resolver -The main way to configure the application firewall is by configuring application profiles. For every program that is active on the network the Portmaster automatically creates a profile for it the first it's seen. These profiles are empty at first and only fed by a fallback profile. By changing these profiles in the app, you change what programs are allowed to do. +**Status:** _pre-alpha_ -You can also see what is going on right now. The monitor page in the app lets you see the network like the Portmaster sees it: `Communications` represent a logical connection between a program and a domain. These second level objects group `Links` (physical connections: IP->IP) together for easier handling and viewing. +A DNS resolver that does not only encrypt your queries, but figures out where it makes the most sense to send your queries. Queries for local domains will not be sent to the upstream servers. This means it won't break your or your company's network setup. -The Portmaster consists of three parts: -- The _core_ (ie. the _daemon_) that runs as an administrator and does all the work. (`sudo ./pmctl run core --data=/opt/pm_db`) -- The _app_, a user interface to set preferences, monitor apps and configure application profiles (`sudo ./pmctl run app --data=/opt/pm_db`) -- The _notifier_, a little menu/tray app for quick access and notifications (`sudo ./pmctl run notifier --data=/opt/pm_db`) +**Features/Settings:** -If you want to know more, here are [the docs](http://docs.safing.io/). +- Configure upstream DNS resolvers +- Don't use assigned Nameserver (by DHCP / local network - public WiFi!) +- Don't use Multicast DNS (public WiFi!) -#### Installation +## Privacy Filter -The `pmctl` command will help you get up and running. It will bootstrap your the environment and download additional files it needs. All commands need the `--data` parameter with the database location, as this is where all the data and also the binaries live. +**Status:** _unreleased - pre-alpha scheduled for the next days_ -Just download `pmctl` from the [releases page](https://github.com/safing/portmaster/releases) and put it somewhere comfortable. You may freely choose where you want to put the database - it needs to be the same for all commands. Here we go - run every command in a seperate terminal window: +Think of a pi-hole for your computer. Or an ad-blocker that blocks ads on your whole computer, not only on your browser. With you everywhere you go and every network you visit. -```bash -# Either export the PORTMASTER_DATA environment variable or add -# --data=/opt/pm_db to all commands below. If you use pmctl a -# lot you may move the export line to your ~/.bashrc -export PORTMASTER_DATA=/opt/pm_db +**Features/Settings:** -# start the portmaster: -sudo ./pmctl run core -# this will add some rules to iptables for traffic interception via nfqueue (and will clean up afterwards!) -# already active connections may not be handled correctly, please restart programs for clean behavior +- Select and activate block-lists +- Manually black/whitelist domains + - You can whitelist domains in case something breaks +- CNAME Blocking (block these new nasty "unblockable" ads/trackers - coming soon) +- Block all subdomains of a domain in the block-lists -# then start the app: -./pmctl run app +## Safing Privacy Network (SPN) -# and the notifier: -./pmctl run notifier -``` +**Status:** _unreleased - pre-alpha scheduled for June_ -#### Feedback +Please [visit our Kickstarter campaign](https://www.kickstarter.com/projects/safingio/spn/) to read all about this module. -We'd love to know what you think, drop by on [our forum](https://discourse.safing.community/) and let us know! -If you want to report a bug, please [open an issue on Github](https://github.com/safing/portmaster/issues/new). +# Installation -## Documentation +Installation instructions for your platform as well as known issues can be found at the respective wiki pages: -Documentation _in progress_ can be found here: [docs.safing.io](http://docs.safing.io/) - -## Usage Dependencies - -#### Linux -- libnetfilter_queue - - debian/ubuntu: `sudo apt-get install libnetfilter-queue1` - - fedora: `sudo yum install libnetfilter_queue` - - arch: `sudo pacman -S libnetfilter_queue` -- [Network Manager](https://wiki.gnome.org/Projects/NetworkManager) (_optional_) - -#### Windows -- Windows 7 (with update KB3033929) or up - - [KB3033929](https://docs.microsoft.com/en-us/security-updates/SecurityAdvisories/2015/3033929) (a 2015 security update) is required for correctly verifying the driver signature -- Windows Server 2016 systems must have secure boot disabled. (_clarification needed_) - -## Build Dependencies - -#### Linux -- libnetfilter_queue development files - - debian/ubuntu: `sudo apt-get install libnetfilter-queue-dev` - - fedora: `?` - - arch: `sudo pacman -S libnetfilter_queue` - -## TCP/UDP Ports - -The Portmaster (with Gate17) uses the following ports: -- ` 17` Gate17 port for connecting to Gate17 nodes -- ` 53` DNS server (local only) -- `717` Gate17 entrypoint as the local endpoint for tunneled connections (local only) -- `817` Portmaster API for integration with UI elements and other helpers (local only) - -Learn more about [why we chose these ports](https://docs.safing.io/docs/portmaster/os-integration.html). - -Gate17 nodes additionally uses other common ports like `80` and `443` to provide access in restricted network environments. +- [Linux](https://github.com/safing/portmaster/wiki/Linux) +- [Windows](https://github.com/safing/portmaster/wiki/Windows) From cee50745bb20be4a9398d9dd392237c50988b96b Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 Apr 2020 22:28:57 +0200 Subject: [PATCH 02/30] Remove Wiki from Readme title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e3685b4..2ca6215a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Portmaster Wiki +# Portmaster The Portmaster is a privacy app that at its core simply intercepts _all_ your network connections. Different modules with different privacy features are built on top of it, which can all be enabled or disabled as desired: From 943246c74753ce00c02249f2abfa760145236c03 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Fri, 17 Apr 2020 11:01:26 +0200 Subject: [PATCH 03/30] Add support for firefox canary domain for disabling DoH --- firewall/master.go | 14 ++++++++++++-- intel/entity.go | 1 - profile/config.go | 20 ++++++++++++++++++++ profile/profile-layered.go | 24 +++++++++++++++++++++++- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/firewall/master.go b/firewall/master.go index 9650b625..3dd9f8e8 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -141,9 +141,19 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: } } + // check for bypass protection + result, reason := p.MatchBypassProtection(conn.Entity) + switch result { + case endpoints.Denied: + conn.Block("bypass prevention: " + reason) + return + case endpoints.Permitted: + conn.Accept("bypass prevention: " + reason) + return + case endpoints.NoMatch: + } + // check endpoints list - var result endpoints.EPResult - var reason string if conn.Inbound { result, reason = p.MatchServiceEndpoint(conn.Entity) } else { diff --git a/intel/entity.go b/intel/entity.go index f4412bc3..e667f07d 100644 --- a/intel/entity.go +++ b/intel/entity.go @@ -151,7 +151,6 @@ func (e *Entity) getLocation() { e.fetchLocationOnce.Do(func() { // need IP! if e.IP == nil { - log.Warningf("intel: cannot get location for %s data without IP", e.Domain) return } diff --git a/profile/config.go b/profile/config.go index 9cc341f9..bf0313a0 100644 --- a/profile/config.go +++ b/profile/config.go @@ -53,6 +53,9 @@ var ( CfgOptionRemoveBlockedDNSKey = "filter/removeBlockedDNS" cfgOptionRemoveBlockedDNS config.IntOption // security level option + + CfgOptionBypassProtectionKey = "filter/preventBypassing" + cfgOptionBypassProtection config.IntOption // security level option ) func registerConfiguration() error { @@ -325,5 +328,22 @@ Examples: cfgOptionRemoveBlockedDNS = config.Concurrent.GetAsInt(CfgOptionRemoveBlockedDNSKey, int64(status.SecurityLevelsAll)) cfgIntOptions[CfgOptionRemoveBlockedDNSKey] = cfgOptionRemoveBlockedDNS + err = config.Register(&config.Option{ + Name: "Prevent Bypassing", + Key: CfgOptionBypassProtectionKey, + Description: "Prevent apps from bypassing the privacy filter:\n- Firefox: Disable DNS-over-HTTPs", + OptType: config.OptTypeInt, + ExpertiseLevel: config.ExpertiseLevelUser, + ReleaseLevel: config.ReleaseLevelBeta, + ExternalOptType: "security level", + DefaultValue: status.SecurityLevelsAll, + ValidationRegex: "^(7|6|4|0)", + }) + if err != nil { + return err + } + cfgOptionBypassProtection = config.Concurrent.GetAsInt((CfgOptionBypassProtectionKey), int64(status.SecurityLevelsAll)) + cfgIntOptions[CfgOptionBypassProtectionKey] = cfgOptionBypassProtection + return nil } diff --git a/profile/profile-layered.go b/profile/profile-layered.go index 90f0478b..0fca0bad 100644 --- a/profile/profile-layered.go +++ b/profile/profile-layered.go @@ -1,6 +1,7 @@ package profile import ( + "strings" "sync" "sync/atomic" @@ -43,6 +44,7 @@ type LayeredProfile struct { RemoveOutOfScopeDNS config.BoolOption RemoveBlockedDNS config.BoolOption FilterSubDomains config.BoolOption + PreventBypassing config.BoolOption } // NewLayeredProfile returns a new layered profile based on the given local profile. @@ -98,6 +100,10 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile { CfgOptionFilterSubDomainsKey, cfgOptionFilterSubDomains, ) + new.PreventBypassing = new.wrapSecurityLevelOption( + CfgOptionBypassProtectionKey, + cfgOptionBypassProtection, + ) // TODO: load linked profiles. @@ -224,7 +230,7 @@ func (lp *LayeredProfile) MatchServiceEndpoint(entity *intel.Entity) (result end // MatchFilterLists matches the entity against the set of filter // lists. -func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (result endpoints.EPResult, reason string) { +func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPResult, string) { entity.ResolveSubDomainLists(lp.FilterSubDomains()) lookupMap, hasLists := entity.GetListsMap() @@ -253,6 +259,22 @@ func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (result endpoin return endpoints.NoMatch, "" } +// MatchBypassProtection checks if the entity should be denied or permitted +// based on some bypass protection checks. +func (lp *LayeredProfile) MatchBypassProtection(entity *intel.Entity) (endpoints.EPResult, string) { + if !lp.PreventBypassing() { + return endpoints.NoMatch, "" + } + + // Block firefox canary domain to disable DoH + if strings.ToLower(entity.Domain) == "use-application-dns.net." { + log.Warningf("bypass protection for firefox canary") + return endpoints.Denied, "Firefox canary domain" + } + + return endpoints.NoMatch, "" +} + // 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) From 58ad3eb88b17dd20294e16e13f13d9ebcf22529c Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Fri, 17 Apr 2020 11:04:17 +0200 Subject: [PATCH 04/30] Switch filterlist debug logging to trace --- intel/entity.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/intel/entity.go b/intel/entity.go index e667f07d..290da215 100644 --- a/intel/entity.go +++ b/intel/entity.go @@ -223,11 +223,11 @@ func (e *Entity) getDomainLists() { var domains = []string{domain} if e.resolveSubDomainLists { domains = splitDomain(domain) - log.Debugf("intel: subdomain list resolving is enabled, checking %v", domains) + log.Tracef("intel: subdomain list resolving is enabled, checking %v", domains) } for _, d := range domains { - log.Debugf("intel: loading domain list for %s", d) + log.Tracef("intel: loading domain list for %s", d) list, err := filterlists.LookupDomain(d) if err != nil { log.Errorf("intel: failed to get domain blocklists for %s: %s", d, err) @@ -277,7 +277,7 @@ func (e *Entity) getASNLists() { return } - log.Debugf("intel: loading ASN list for %d", asn) + log.Tracef("intel: loading ASN list for %d", asn) e.loadAsnListOnce.Do(func() { list, err := filterlists.LookupASNString(fmt.Sprintf("%d", asn)) if err != nil { @@ -301,7 +301,7 @@ func (e *Entity) getCountryLists() { return } - log.Debugf("intel: loading country list for %s", country) + log.Tracef("intel: loading country list for %s", country) e.loadCoutryListOnce.Do(func() { list, err := filterlists.LookupCountry(country) if err != nil { @@ -334,7 +334,7 @@ func (e *Entity) getIPLists() { return } - log.Debugf("intel: loading IP list for %s", ip) + log.Tracef("intel: loading IP list for %s", ip) e.loadIPListOnce.Do(func() { list, err := filterlists.LookupIP(ip) From ea3e327c272ed21706a1e8eb6fee76a96ac7c274 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Fri, 17 Apr 2020 11:52:53 +0200 Subject: [PATCH 05/30] Implement review changes --- firewall/bypassing.go | 19 +++++++++++++++++++ firewall/master.go | 25 +++++++++++++++---------- profile/config.go | 14 +++++++------- profile/profile-layered.go | 21 ++------------------- 4 files changed, 43 insertions(+), 36 deletions(-) create mode 100644 firewall/bypassing.go diff --git a/firewall/bypassing.go b/firewall/bypassing.go new file mode 100644 index 00000000..ac3349f5 --- /dev/null +++ b/firewall/bypassing.go @@ -0,0 +1,19 @@ +package firewall + +import ( + "strings" + + "github.com/safing/portmaster/network" + "github.com/safing/portmaster/profile/endpoints" +) + +// PreventBypassing checks if the connection should be denied or permitted +// based on some bypass protection checks. +func PreventBypassing(conn *network.Connection) (endpoints.EPResult, string) { + // Block firefox canary domain to disable DoH + if strings.ToLower(conn.Entity.Domain) == "use-application-dns.net." { + return endpoints.Denied, "blocked canary domain to prevent enabling DNS-over-HTTPs" + } + + return endpoints.NoMatch, "" +} diff --git a/firewall/master.go b/firewall/master.go index 3dd9f8e8..f09ad644 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -141,16 +141,21 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: } } - // check for bypass protection - result, reason := p.MatchBypassProtection(conn.Entity) - switch result { - case endpoints.Denied: - conn.Block("bypass prevention: " + reason) - return - case endpoints.Permitted: - conn.Accept("bypass prevention: " + reason) - return - case endpoints.NoMatch: + var result endpoints.EPResult + var reason string + + if p.PreventBypassing() { + // check for bypass protection + result, reason := PreventBypassing(conn) + switch result { + case endpoints.Denied: + conn.Block("bypass prevention: " + reason) + return + case endpoints.Permitted: + conn.Accept("bypass prevention: " + reason) + return + case endpoints.NoMatch: + } } // check endpoints list diff --git a/profile/config.go b/profile/config.go index bf0313a0..2607646a 100644 --- a/profile/config.go +++ b/profile/config.go @@ -54,8 +54,8 @@ var ( CfgOptionRemoveBlockedDNSKey = "filter/removeBlockedDNS" cfgOptionRemoveBlockedDNS config.IntOption // security level option - CfgOptionBypassProtectionKey = "filter/preventBypassing" - cfgOptionBypassProtection config.IntOption // security level option + CfgOptionPreventBypassingKey = "filter/preventBypassing" + cfgOptionPreventBypassing config.IntOption // security level option ) func registerConfiguration() error { @@ -330,20 +330,20 @@ Examples: err = config.Register(&config.Option{ Name: "Prevent Bypassing", - Key: CfgOptionBypassProtectionKey, - Description: "Prevent apps from bypassing the privacy filter:\n- Firefox: Disable DNS-over-HTTPs", + Key: CfgOptionPreventBypassingKey, + Description: "Prevent apps from bypassing the privacy filter: Firefox by disabling DNS-over-HTTPs", OptType: config.OptTypeInt, ExpertiseLevel: config.ExpertiseLevelUser, ReleaseLevel: config.ReleaseLevelBeta, ExternalOptType: "security level", DefaultValue: status.SecurityLevelsAll, - ValidationRegex: "^(7|6|4|0)", + ValidationRegex: "^(7|6|4)", }) if err != nil { return err } - cfgOptionBypassProtection = config.Concurrent.GetAsInt((CfgOptionBypassProtectionKey), int64(status.SecurityLevelsAll)) - cfgIntOptions[CfgOptionBypassProtectionKey] = cfgOptionBypassProtection + cfgOptionPreventBypassing = config.Concurrent.GetAsInt((CfgOptionPreventBypassingKey), int64(status.SecurityLevelsAll)) + cfgIntOptions[CfgOptionPreventBypassingKey] = cfgOptionPreventBypassing return nil } diff --git a/profile/profile-layered.go b/profile/profile-layered.go index 0fca0bad..f00dbfe7 100644 --- a/profile/profile-layered.go +++ b/profile/profile-layered.go @@ -1,7 +1,6 @@ package profile import ( - "strings" "sync" "sync/atomic" @@ -101,8 +100,8 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile { cfgOptionFilterSubDomains, ) new.PreventBypassing = new.wrapSecurityLevelOption( - CfgOptionBypassProtectionKey, - cfgOptionBypassProtection, + CfgOptionPreventBypassingKey, + cfgOptionPreventBypassing, ) // TODO: load linked profiles. @@ -259,22 +258,6 @@ func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPRe return endpoints.NoMatch, "" } -// MatchBypassProtection checks if the entity should be denied or permitted -// based on some bypass protection checks. -func (lp *LayeredProfile) MatchBypassProtection(entity *intel.Entity) (endpoints.EPResult, string) { - if !lp.PreventBypassing() { - return endpoints.NoMatch, "" - } - - // Block firefox canary domain to disable DoH - if strings.ToLower(entity.Domain) == "use-application-dns.net." { - log.Warningf("bypass protection for firefox canary") - return endpoints.Denied, "Firefox canary domain" - } - - return endpoints.NoMatch, "" -} - // 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) From 10ee7fd7dbd9a1e446c5e1e361a5014920a6c5a9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 17 Apr 2020 21:51:01 +0200 Subject: [PATCH 06/30] Update random->rng module name --- core/core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/core.go b/core/core.go index f15a2709..d70569f0 100644 --- a/core/core.go +++ b/core/core.go @@ -18,7 +18,7 @@ var ( ) func init() { - modules.Register("base", nil, registerDatabases, nil, "database", "config", "random") + modules.Register("base", nil, registerDatabases, nil, "database", "config", "rng") module = modules.Register("core", nil, start, nil, "base", "subsystems", "status", "updates", "api", "notifications", "ui") subsystems.Register( From 033dceab5b6a94802417c6f3126efe620b7f5d11 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 17 Apr 2020 21:52:06 +0200 Subject: [PATCH 07/30] Add support for unidentified/system processes/profiles --- firewall/firewall.go | 1 + firewall/master.go | 2 ++ nameserver/nameserver.go | 1 + network/connection.go | 11 +++++-- network/dns.go | 11 ++++++- process/find.go | 4 +-- process/process.go | 22 ++++++------- process/profile.go | 8 ++--- process/special.go | 67 ++++++++++++++++++++++++++++++++++++++ process/unknown.go | 26 --------------- profile/active.go | 36 +++++++++++++++++++- profile/config-update.go | 4 +-- profile/module.go | 2 ++ profile/profile-layered.go | 7 +++- profile/profile.go | 54 +++++++++++++++++++++--------- profile/special.go | 54 ++++++++++++++++++++++++++++++ 16 files changed, 243 insertions(+), 67 deletions(-) create mode 100644 process/special.go delete mode 100644 process/unknown.go create mode 100644 profile/special.go diff --git a/firewall/firewall.go b/firewall/firewall.go index 530f7437..5ccc8677 100644 --- a/firewall/firewall.go +++ b/firewall/firewall.go @@ -233,6 +233,7 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) { if ps.isMe { // approve conn.Accept("internally approved") + conn.Hidden = true // finish conn.StopFirewallHandler() issueVerdict(conn, pkt, 0, true) diff --git a/firewall/master.go b/firewall/master.go index 9650b625..9cb55bc1 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -50,6 +50,7 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: if conn.Process().Pid == os.Getpid() { log.Infof("filter: granting own connection %s", conn) conn.Verdict = network.VerdictAccept + conn.Hidden = true return } @@ -75,6 +76,7 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: log.Warningf("filter: failed to find load local peer process with PID %d: %s", otherPid, err) } else if otherProcess.Pid == conn.Process().Pid { conn.Accept("connection to self") + conn.Hidden = true return } } diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 7eee0c4d..8f2dbd65 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -199,6 +199,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er } }() + // TODO: this has been obsoleted due to special profiles if conn.Process().Profile() == nil { tracer.Infof("nameserver: failed to find process for request %s, returning NXDOMAIN", conn) returnNXDomain(w, query) diff --git a/network/connection.go b/network/connection.go index d2ada322..3b2c6a84 100644 --- a/network/connection.go +++ b/network/connection.go @@ -41,6 +41,7 @@ type Connection struct { //nolint:maligned // TODO: fix alignment VerdictPermanent bool Inspecting bool Encrypted bool // TODO + Hidden bool pktQueue chan packet.Packet firewallHandler FirewallHandler @@ -58,7 +59,7 @@ func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, ip net.IP, po proc, err := process.GetProcessByEndpoints(ctx, ip, port, dnsAddress, dnsPort, packet.UDP) if err != nil { log.Warningf("network: failed to find process of dns request for %s: %s", fqdn, err) - proc = process.UnknownProcess + proc = process.GetUnidentifiedProcess(ctx) } timestamp := time.Now().Unix() @@ -80,7 +81,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { proc, inbound, err := process.GetProcessByPacket(pkt) if err != nil { log.Warningf("network: failed to find process of packet %s: %s", pkt, err) - proc = process.UnknownProcess + proc = process.GetUnidentifiedProcess(pkt.Ctx()) } var scope string @@ -270,7 +271,11 @@ func (conn *Connection) Save() { // delete deletes a link from the storage and propagates the change. Nothing is locked - both the conns map and the connection itself require locking func (conn *Connection) delete() { - delete(conns, conn.ID) + if conn.ID == "" { + delete(dnsConns, strconv.Itoa(conn.process.Pid)+"/"+conn.Scope) + } else { + delete(conns, conn.ID) + } conn.Meta().Delete() dbController.PushUpdate(conn) diff --git a/network/dns.go b/network/dns.go index 88ca7be3..43c0efcf 100644 --- a/network/dns.go +++ b/network/dns.go @@ -23,7 +23,16 @@ func removeOpenDNSRequest(pid int, fqdn string) { defer openDNSRequestsLock.Unlock() key := strconv.Itoa(pid) + "/" + fqdn - delete(openDNSRequests, key) + _, ok := openDNSRequests[key] + if ok { + delete(openDNSRequests, key) + return + } + + // check if there is an open dns request from an unidentified process + if pid >= 0 { + delete(openDNSRequests, "-1/"+fqdn) + } } // SaveOpenDNSRequest saves a dns request connection that was allowed to proceed. diff --git a/process/find.go b/process/find.go index 997d487b..0e609f52 100644 --- a/process/find.go +++ b/process/find.go @@ -58,7 +58,7 @@ func GetPidByPacket(pkt packet.Packet) (pid int, direction bool, err error) { func GetProcessByPacket(pkt packet.Packet) (process *Process, direction bool, err error) { if !enableProcessDetection() { log.Tracer(pkt.Ctx()).Tracef("process: process detection disabled") - return UnknownProcess, direction, nil + return GetUnidentifiedProcess(pkt.Ctx()), pkt.Info().Direction, nil } log.Tracer(pkt.Ctx()).Tracef("process: getting process and profile by packet") @@ -116,7 +116,7 @@ func GetPidByEndpoints(localIP net.IP, localPort uint16, remoteIP net.IP, remote 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 + return GetUnidentifiedProcess(ctx), nil } log.Tracer(ctx).Tracef("process: getting process and profile by endpoints") diff --git a/process/process.go b/process/process.go index 2ab29383..acdadeb8 100644 --- a/process/process.go +++ b/process/process.go @@ -75,11 +75,11 @@ func (p *Process) String() string { func GetOrFindPrimaryProcess(ctx context.Context, pid int) (*Process, error) { log.Tracer(ctx).Tracef("process: getting primary process for PID %d", pid) - if pid == -1 { - return UnknownProcess, nil + if pid <= -1 { + return GetUnidentifiedProcess(ctx), nil } if pid == 0 { - return OSProcess, nil + return GetSystemProcess(ctx), nil } process, err := loadProcess(ctx, pid) @@ -88,8 +88,8 @@ func GetOrFindPrimaryProcess(ctx context.Context, pid int) (*Process, error) { } for { - if process.ParentPid == 0 { - return OSProcess, nil + if process.ParentPid <= 0 { + return process, nil } parentProcess, err := loadProcess(ctx, process.ParentPid) if err != nil { @@ -121,11 +121,11 @@ func GetOrFindPrimaryProcess(ctx context.Context, pid int) (*Process, error) { func GetOrFindProcess(ctx context.Context, pid int) (*Process, error) { log.Tracer(ctx).Tracef("process: getting process for PID %d", pid) - if pid == -1 { - return UnknownProcess, nil + if pid <= -1 { + return GetUnidentifiedProcess(ctx), nil } if pid == 0 { - return OSProcess, nil + return GetSystemProcess(ctx), nil } p, err := loadProcess(ctx, pid) @@ -184,11 +184,11 @@ func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) { } func loadProcess(ctx context.Context, pid int) (*Process, error) { - if pid == -1 { - return UnknownProcess, nil + if pid <= -1 { + return GetUnidentifiedProcess(ctx), nil } if pid == 0 { - return OSProcess, nil + return GetSystemProcess(ctx), nil } process, ok := GetProcessFromStorage(pid) diff --git a/process/profile.go b/process/profile.go index 388684fa..0f0ad5c6 100644 --- a/process/profile.go +++ b/process/profile.go @@ -15,6 +15,8 @@ func (p *Process) GetProfile(ctx context.Context) error { // only find profiles if not already done. if p.profile != nil { log.Tracer(ctx).Trace("process: profile already loaded") + // mark profile as used + p.profile.MarkUsed() return nil } log.Tracer(ctx).Trace("process: loading profile") @@ -29,10 +31,8 @@ func (p *Process) GetProfile(ctx context.Context) error { localProfile.Name = p.ExecName } - // mark as used and save - if localProfile.MarkUsed() { - _ = localProfile.Save() - } + // mark profile as used + localProfile.MarkUsed() p.LocalProfileKey = localProfile.Key() p.profile = profile.NewLayeredProfile(localProfile) diff --git a/process/special.go b/process/special.go new file mode 100644 index 00000000..8cbf6130 --- /dev/null +++ b/process/special.go @@ -0,0 +1,67 @@ +package process + +import ( + "context" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/profile" +) + +var ( + // unidentifiedProcess is used when a process cannot be found. + unidentifiedProcess = &Process{ + UserID: -1, + UserName: "Unknown", + Pid: -1, + ParentPid: -1, + Name: "Unidentified Processes", + } + + // systemProcess is used to represent the Kernel. + systemProcess = &Process{ + UserID: 0, + UserName: "Kernel", + Pid: 0, + ParentPid: 0, + Name: "Operating System", + } +) + +func GetUnidentifiedProcess(ctx context.Context) *Process { + return getSpecialProcess(ctx, unidentifiedProcess, profile.GetUnidentifiedProfile) +} + +func GetSystemProcess(ctx context.Context) *Process { + return getSpecialProcess(ctx, systemProcess, profile.GetSystemProfile) +} + +func getSpecialProcess(ctx context.Context, p *Process, getProfile func() *profile.Profile) *Process { + p.Lock() + defer p.Unlock() + + if p.FirstSeen == 0 { + p.FirstSeen = time.Now().Unix() + } + + // only find profiles if not already done. + if p.profile != nil { + log.Tracer(ctx).Trace("process: special profile already loaded") + // mark profile as used + p.profile.MarkUsed() + return p + } + log.Tracer(ctx).Trace("process: loading special profile") + + // get profile + localProfile := getProfile() + + // mark profile as used + localProfile.MarkUsed() + + p.LocalProfileKey = localProfile.Key() + p.profile = profile.NewLayeredProfile(localProfile) + + go p.Save() + return p +} diff --git a/process/unknown.go b/process/unknown.go deleted file mode 100644 index 4f6cde12..00000000 --- a/process/unknown.go +++ /dev/null @@ -1,26 +0,0 @@ -package process - -var ( - // UnknownProcess is used when a process cannot be found. - UnknownProcess = &Process{ - UserID: -1, - UserName: "Unknown", - Pid: -1, - ParentPid: -1, - Name: "Unknown Processes", - } - - // OSProcess is used to represent the Kernel. - OSProcess = &Process{ - UserID: 0, - UserName: "Kernel", - Pid: 0, - ParentPid: 0, - Name: "Operating System", - } -) - -func init() { - UnknownProcess.Save() - OSProcess.Save() -} diff --git a/profile/active.go b/profile/active.go index ff8f3d3e..3d45a727 100644 --- a/profile/active.go +++ b/profile/active.go @@ -1,7 +1,14 @@ package profile import ( + "context" "sync" + "time" +) + +const ( + activeProfileCleanerTickDuration = 10 * time.Minute + activeProfileCleanerThreshold = 1 * time.Hour ) var ( @@ -38,7 +45,34 @@ func markActiveProfileAsOutdated(scopedID string) { profile, ok := activeProfiles[scopedID] if ok { - profile.oudated.Set() + profile.outdated.Set() delete(activeProfiles, scopedID) } } + +func cleanActiveProfiles(ctx context.Context) error { //nolint:param // need to conform to interface + for { + select { + case <-time.After(activeProfileCleanerTickDuration): + + threshold := time.Now().Add(-activeProfileCleanerThreshold) + + activeProfilesLock.Lock() + for id, profile := range activeProfiles { + // get last used + profile.Lock() + lastUsed := profile.lastUsed + profile.Unlock() + // remove if not used for a while + if lastUsed.Before(threshold) { + profile.outdated.Set() + delete(activeProfiles, id) + } + } + activeProfilesLock.Unlock() + + case <-ctx.Done(): + return nil + } + } +} diff --git a/profile/config-update.go b/profile/config-update.go index 15e62b8e..b4c3f5e4 100644 --- a/profile/config-update.go +++ b/profile/config-update.go @@ -70,8 +70,8 @@ func updateGlobalConfigProfile(ctx context.Context, data interface{}) error { // build global profile for reference profile := &Profile{ - ID: "config", - Source: SourceGlobal, + ID: "global-config", + Source: SourceSpecial, Name: "Global Configuration", Config: make(map[string]interface{}), internalSave: true, diff --git a/profile/module.go b/profile/module.go index 615eddcf..8d962a8a 100644 --- a/profile/module.go +++ b/profile/module.go @@ -42,6 +42,8 @@ func start() error { return err } + module.StartServiceWorker("clean active profiles", 0, cleanActiveProfiles) + err = updateGlobalConfigProfile(module.Ctx, nil) if err != nil { log.Warningf("profile: error during loading global profile from configuration: %s", err) diff --git a/profile/profile-layered.go b/profile/profile-layered.go index 90f0478b..6bf3ea2e 100644 --- a/profile/profile-layered.go +++ b/profile/profile-layered.go @@ -123,7 +123,7 @@ func (lp *LayeredProfile) Update() (revisionCounter uint64) { var changed bool for i, layer := range lp.layers { - if layer.oudated.IsSet() { + if layer.outdated.IsSet() { changed = true // update layer newLayer, err := GetProfile(layer.Source, layer.ID) @@ -170,6 +170,11 @@ func (lp *LayeredProfile) updateCaches() { // TODO: ignore community profiles } +// MarkUsed marks the localProfile as used. +func (lp *LayeredProfile) MarkUsed() { + lp.localProfile.MarkUsed() +} + // SecurityLevel returns the highest security level of all layered profiles. func (lp *LayeredProfile) SecurityLevel() uint8 { return uint8(atomic.LoadUint32(lp.securityLevel)) diff --git a/profile/profile.go b/profile/profile.go index 46e3b447..70f7c162 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -19,15 +19,15 @@ import ( ) var ( - lastUsedUpdateThreshold = 1 * time.Hour + lastUsedUpdateThreshold = 24 * time.Hour ) // Profile Sources const ( - SourceLocal string = "local" + SourceLocal string = "local" // local, editable + SourceSpecial string = "special" // specials (read-only) SourceCommunity string = "community" SourceEnterprise string = "enterprise" - SourceGlobal string = "global" ) // Default Action IDs @@ -77,7 +77,8 @@ type Profile struct { //nolint:maligned // not worth the effort filterListIDs []string // Lifecycle Management - oudated *abool.AtomicBool + outdated *abool.AtomicBool + lastUsed time.Time // Framework // If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process/actor must be found @@ -94,7 +95,7 @@ type Profile struct { //nolint:maligned // not worth the effort func (profile *Profile) prepConfig() (err error) { // prepare configuration profile.configPerspective, err = config.NewPerspective(profile.Config) - profile.oudated = abool.New() + profile.outdated = abool.New() return } @@ -156,10 +157,11 @@ func (profile *Profile) parseConfig() error { // New returns a new Profile. func New() *Profile { profile := &Profile{ - ID: uuid.NewV4().String(), - Source: SourceLocal, - Created: time.Now().Unix(), - Config: make(map[string]interface{}), + ID: uuid.NewV4().String(), + Source: SourceLocal, + Created: time.Now().Unix(), + Config: make(map[string]interface{}), + internalSave: true, } // create placeholders @@ -190,13 +192,26 @@ func (profile *Profile) Save() error { return profileDB.Put(profile) } -// MarkUsed marks the profile as used, eventually. -func (profile *Profile) MarkUsed() (updated bool) { +// MarkUsed marks the profile as used and saves it when it has changed. +func (profile *Profile) MarkUsed() { + profile.Lock() + // lastUsed + profile.lastUsed = time.Now() + + // ApproxLastUsed + save := false if time.Now().Add(-lastUsedUpdateThreshold).Unix() > profile.ApproxLastUsed { profile.ApproxLastUsed = time.Now().Unix() - return true + save = true + } + profile.Unlock() + + if save { + err := profile.Save() + if err != nil { + log.Warningf("profiles: failed to save profile %s after marking as used: %s", profile.ScopedID(), err) + } } - return false } // String returns a string representation of the Profile. @@ -224,8 +239,6 @@ func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) { endpointList = append(endpointList, newEntry) profile.Config[cfgKey] = endpointList - // save without full reload - profile.internalSave = true profile.Unlock() err := profile.Save() if err != nil { @@ -233,10 +246,13 @@ func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) { } // reload manually + profile.Lock() + profile.dataParsed = false err = profile.parseConfig() if err != nil { log.Warningf("profile: failed to parse profile config after adding endpoint: %s", err) } + profile.Unlock() } // GetProfile loads a profile from the database. @@ -249,6 +265,7 @@ func GetProfileByScopedID(scopedID string) (*Profile, error) { // check cache profile := getActiveProfile(scopedID) if profile != nil { + profile.MarkUsed() return profile, nil } @@ -266,7 +283,6 @@ func GetProfileByScopedID(scopedID string) (*Profile, error) { // lock for prepping profile.Lock() - defer profile.Unlock() // prepare config err = profile.prepConfig() @@ -280,7 +296,13 @@ func GetProfileByScopedID(scopedID string) (*Profile, error) { log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err) } + // mark as internal + profile.internalSave = true + + profile.Unlock() + // mark active + profile.MarkUsed() markProfileActive(profile) return profile, nil diff --git a/profile/special.go b/profile/special.go new file mode 100644 index 00000000..13993337 --- /dev/null +++ b/profile/special.go @@ -0,0 +1,54 @@ +package profile + +import ( + "github.com/safing/portbase/log" +) + +const ( + unidentifiedProfileID = "_unidentified" + systemProfileID = "_system" +) + +func GetUnidentifiedProfile() *Profile { + // get profile + profile, err := GetProfile(SourceLocal, unidentifiedProfileID) + if err == nil { + return profile + } + + // create if not available (or error) + profile = New() + profile.Name = "Unidentified Processes" + profile.Source = SourceLocal + profile.ID = unidentifiedProfileID + + // save to db + err = profile.Save() + if err != nil { + log.Warningf("profiles: failed to save %s: %s", profile.ScopedID(), err) + } + + return profile +} + +func GetSystemProfile() *Profile { + // get profile + profile, err := GetProfile(SourceLocal, systemProfileID) + if err == nil { + return profile + } + + // create if not available (or error) + profile = New() + profile.Name = "Operating System" + profile.Source = SourceLocal + profile.ID = systemProfileID + + // save to db + err = profile.Save() + if err != nil { + log.Warningf("profiles: failed to save %s: %s", profile.ScopedID(), err) + } + + return profile +} From a33808685c3c3ea7433b5239031b132cf6beb554 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 20 Apr 2020 13:57:07 +0200 Subject: [PATCH 08/30] Implement review suggestions --- firewall/firewall.go | 2 +- firewall/master.go | 4 ++-- network/connection.go | 2 +- network/dns.go | 14 ++++++++------ process/database.go | 15 +++++++-------- process/find.go | 4 ++-- process/iphelper/get.go | 26 +++++++++++++++----------- process/proc/gather.go | 8 ++++---- process/proc/get.go | 10 +++++++--- process/proc/processfinder.go | 2 +- process/proc/sockets.go | 6 +++--- process/process.go | 19 ++++++++++--------- process/special.go | 35 ++++++++++++++++++++++++++--------- profile/active.go | 2 +- profile/special.go | 2 ++ 15 files changed, 90 insertions(+), 61 deletions(-) diff --git a/firewall/firewall.go b/firewall/firewall.go index 5ccc8677..923e87f2 100644 --- a/firewall/firewall.go +++ b/firewall/firewall.go @@ -233,7 +233,7 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) { if ps.isMe { // approve conn.Accept("internally approved") - conn.Hidden = true + conn.Internal = true // finish conn.StopFirewallHandler() issueVerdict(conn, pkt, 0, true) diff --git a/firewall/master.go b/firewall/master.go index 9cb55bc1..15f6edb8 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -50,7 +50,7 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: if conn.Process().Pid == os.Getpid() { log.Infof("filter: granting own connection %s", conn) conn.Verdict = network.VerdictAccept - conn.Hidden = true + conn.Internal = true return } @@ -76,7 +76,7 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: log.Warningf("filter: failed to find load local peer process with PID %d: %s", otherPid, err) } else if otherProcess.Pid == conn.Process().Pid { conn.Accept("connection to self") - conn.Hidden = true + conn.Internal = true return } } diff --git a/network/connection.go b/network/connection.go index 3b2c6a84..88b36e14 100644 --- a/network/connection.go +++ b/network/connection.go @@ -41,7 +41,7 @@ type Connection struct { //nolint:maligned // TODO: fix alignment VerdictPermanent bool Inspecting bool Encrypted bool // TODO - Hidden bool + Internal bool // Portmaster internal connections are marked in order to easily filter these out in the UI pktQueue chan packet.Packet firewallHandler FirewallHandler diff --git a/network/dns.go b/network/dns.go index 43c0efcf..b33fa99f 100644 --- a/network/dns.go +++ b/network/dns.go @@ -5,6 +5,8 @@ import ( "strconv" "sync" "time" + + "github.com/safing/portmaster/process" ) var ( @@ -16,6 +18,9 @@ var ( // duration after which DNS requests without a following connection are logged openDNSRequestLimit = 3 * time.Second + + // scope prefix + unidentifiedProcessScopePrefix = strconv.Itoa(process.UnidentifiedProcessID) + "/" ) func removeOpenDNSRequest(pid int, fqdn string) { @@ -26,12 +31,9 @@ func removeOpenDNSRequest(pid int, fqdn string) { _, ok := openDNSRequests[key] if ok { delete(openDNSRequests, key) - return - } - - // check if there is an open dns request from an unidentified process - if pid >= 0 { - delete(openDNSRequests, "-1/"+fqdn) + } else if pid != process.UnidentifiedProcessID { + // check if there is an open dns request from an unidentified process + delete(openDNSRequests, unidentifiedProcessScopePrefix+fqdn) } } diff --git a/process/database.go b/process/database.go index b4ce09b3..1ce5295d 100644 --- a/process/database.go +++ b/process/database.go @@ -53,16 +53,13 @@ func (p *Process) Save() { p.Lock() defer p.Unlock() + p.UpdateMeta() + if !p.KeyIsSet() { + // set key p.SetKey(fmt.Sprintf("%s/%d", processDatabaseNamespace, p.Pid)) - p.CreateMeta() - } - processesLock.RLock() - _, ok := processes[p.Pid] - processesLock.RUnlock() - - if !ok { + // save processesLock.Lock() processes[p.Pid] = p processesLock.Unlock() @@ -113,7 +110,9 @@ func CleanProcessStorage(activePIDs map[int]struct{}) { _, active := activePIDs[p.Pid] switch { - case p.Pid <= 0: + case p.Pid == UnidentifiedProcessID: + // internal + case p.Pid == SystemProcessID: // internal case active: // process in system process table or recently seen on the network diff --git a/process/find.go b/process/find.go index 0e609f52..30f93f2d 100644 --- a/process/find.go +++ b/process/find.go @@ -49,7 +49,7 @@ func GetPidByPacket(pkt packet.Packet) (pid int, direction bool, err error) { case pkt.Info().Protocol == packet.UDP && pkt.Info().Version == packet.IPv6: return getUDP6PacketInfo(localIP, localPort, remoteIP, remotePort, pkt.IsInbound()) default: - return -1, false, errors.New("unsupported protocol for finding process") + return UnidentifiedProcessID, false, errors.New("unsupported protocol for finding process") } } @@ -107,7 +107,7 @@ func GetPidByEndpoints(localIP net.IP, localPort uint16, remoteIP net.IP, remote case protocol == packet.UDP && ipVersion == packet.IPv6: return getUDP6PacketInfo(localIP, localPort, remoteIP, remotePort, false) default: - return -1, false, errors.New("unsupported protocol for finding process") + return UnidentifiedProcessID, false, errors.New("unsupported protocol for finding process") } } diff --git a/process/iphelper/get.go b/process/iphelper/get.go index 99c0f821..6487ea06 100644 --- a/process/iphelper/get.go +++ b/process/iphelper/get.go @@ -9,6 +9,10 @@ import ( "time" ) +const ( + unidentifiedProcessID = -1 +) + var ( tcp4Connections []*ConnectionEntry tcp4Listeners []*ConnectionEntry @@ -55,7 +59,7 @@ func GetTCP4PacketInfo(localIP net.IP, localPort uint16, remoteIP net.IP, remote } lock.Unlock() if err != nil { - return -1, pktDirection, err + return unidentifiedProcessID, pktDirection, err } // search @@ -67,7 +71,7 @@ func GetTCP4PacketInfo(localIP net.IP, localPort uint16, remoteIP net.IP, remote time.Sleep(waitTime) } - return -1, pktDirection, nil + return unidentifiedProcessID, pktDirection, nil } // GetTCP6PacketInfo returns the pid of the given IPv6/TCP connection. @@ -91,7 +95,7 @@ func GetTCP6PacketInfo(localIP net.IP, localPort uint16, remoteIP net.IP, remote } lock.Unlock() if err != nil { - return -1, pktDirection, err + return unidentifiedProcessID, pktDirection, err } // search @@ -103,7 +107,7 @@ func GetTCP6PacketInfo(localIP net.IP, localPort uint16, remoteIP net.IP, remote time.Sleep(waitTime) } - return -1, pktDirection, nil + return unidentifiedProcessID, pktDirection, nil } // GetUDP4PacketInfo returns the pid of the given IPv4/UDP connection. @@ -127,7 +131,7 @@ func GetUDP4PacketInfo(localIP net.IP, localPort uint16, remoteIP net.IP, remote } lock.Unlock() if err != nil { - return -1, pktDirection, err + return unidentifiedProcessID, pktDirection, err } // search @@ -139,7 +143,7 @@ func GetUDP4PacketInfo(localIP net.IP, localPort uint16, remoteIP net.IP, remote time.Sleep(waitTime) } - return -1, pktDirection, nil + return unidentifiedProcessID, pktDirection, nil } // GetUDP6PacketInfo returns the pid of the given IPv6/UDP connection. @@ -163,7 +167,7 @@ func GetUDP6PacketInfo(localIP net.IP, localPort uint16, remoteIP net.IP, remote } lock.Unlock() if err != nil { - return -1, pktDirection, err + return unidentifiedProcessID, pktDirection, err } // search @@ -175,7 +179,7 @@ func GetUDP6PacketInfo(localIP net.IP, localPort uint16, remoteIP net.IP, remote time.Sleep(waitTime) } - return -1, pktDirection, nil + return unidentifiedProcessID, pktDirection, nil } func search(connections, listeners []*ConnectionEntry, localIP, remoteIP net.IP, localPort, remotePort uint16, pktDirection bool) (pid int, direction bool) { //nolint:unparam // TODO: use direction, it may not be used because results caused problems, investigate. @@ -204,7 +208,7 @@ func search(connections, listeners []*ConnectionEntry, localIP, remoteIP net.IP, } } - return -1, pktDirection + return unidentifiedProcessID, pktDirection } func searchConnections(list []*ConnectionEntry, localIP, remoteIP net.IP, localPort, remotePort uint16) (pid int) { @@ -218,7 +222,7 @@ func searchConnections(list []*ConnectionEntry, localIP, remoteIP net.IP, localP } } - return -1 + return unidentifiedProcessID } func searchListeners(list []*ConnectionEntry, localIP net.IP, localPort uint16) (pid int) { @@ -231,7 +235,7 @@ func searchListeners(list []*ConnectionEntry, localIP net.IP, localPort uint16) } } - return -1 + return unidentifiedProcessID } // GetActiveConnectionIDs returns all currently active connection IDs. diff --git a/process/proc/gather.go b/process/proc/gather.go index 436b0bb2..1413b3c9 100644 --- a/process/proc/gather.go +++ b/process/proc/gather.go @@ -33,7 +33,7 @@ func GetPidOfConnection(localIP net.IP, localPort uint16, protocol uint8) (pid i } } if !ok { - return -1, NoSocket + return unidentifiedProcessID, NoSocket } } @@ -45,7 +45,7 @@ func GetPidOfConnection(localIP net.IP, localPort uint16, protocol uint8) (pid i pid, ok = GetPidOfInode(uid, inode) } if !ok { - return -1, NoProcess + return unidentifiedProcessID, NoProcess } return @@ -64,7 +64,7 @@ func GetPidOfIncomingConnection(localIP net.IP, localPort uint16, protocol uint8 } if !ok { - return -1, NoSocket + return unidentifiedProcessID, NoSocket } } @@ -76,7 +76,7 @@ func GetPidOfIncomingConnection(localIP net.IP, localPort uint16, protocol uint8 pid, ok = GetPidOfInode(uid, inode) } if !ok { - return -1, NoProcess + return unidentifiedProcessID, NoProcess } return diff --git a/process/proc/get.go b/process/proc/get.go index dec27e23..52974b3e 100644 --- a/process/proc/get.go +++ b/process/proc/get.go @@ -7,6 +7,10 @@ import ( "net" ) +const ( + unidentifiedProcessID = -1 +) + // GetTCP4PacketInfo searches the network state tables for a TCP4 connection func GetTCP4PacketInfo(localIP net.IP, localPort uint16, remoteIP net.IP, remotePort uint16, pktDirection bool) (pid int, direction bool, err error) { return search(TCP4, localIP, localPort, pktDirection) @@ -52,11 +56,11 @@ func search(protocol uint8, localIP net.IP, localPort uint16, pktDirection bool) switch status { case NoSocket: - return -1, direction, errors.New("could not find socket") + return unidentifiedProcessID, direction, errors.New("could not find socket") case NoProcess: - return -1, direction, errors.New("could not find PID") + return unidentifiedProcessID, direction, errors.New("could not find PID") default: - return -1, direction, nil + return unidentifiedProcessID, direction, nil } } diff --git a/process/proc/processfinder.go b/process/proc/processfinder.go index 5ee1bb4b..5e6ed7cc 100644 --- a/process/proc/processfinder.go +++ b/process/proc/processfinder.go @@ -77,7 +77,7 @@ func GetPidOfInode(uid, inode int) (int, bool) { //nolint:gocognit // TODO } } - return -1, false + return unidentifiedProcessID, false } func findSocketFromPid(pid, inode int) bool { diff --git a/process/proc/sockets.go b/process/proc/sockets.go index c82b078c..bcdd91d4 100644 --- a/process/proc/sockets.go +++ b/process/proc/sockets.go @@ -100,7 +100,7 @@ func getConnectionSocket(localIP net.IP, localPort uint16, protocol uint8) (int, socketData, err := os.Open(procFile) if err != nil { log.Warningf("process/proc: could not read %s: %s", procFile, err) - return -1, -1, false + return unidentifiedProcessID, unidentifiedProcessID, false } defer socketData.Close() @@ -146,7 +146,7 @@ func getConnectionSocket(localIP net.IP, localPort uint16, protocol uint8) (int, } - return -1, -1, false + return unidentifiedProcessID, unidentifiedProcessID, false } @@ -187,7 +187,7 @@ func getListeningSocket(localIP net.IP, localPort uint16, protocol uint8) (uid, return data[0], data[1], true } - return -1, -1, false + return unidentifiedProcessID, unidentifiedProcessID, false } func procDelimiter(c rune) bool { diff --git a/process/process.go b/process/process.go index acdadeb8..8ef1ad73 100644 --- a/process/process.go +++ b/process/process.go @@ -75,10 +75,10 @@ func (p *Process) String() string { func GetOrFindPrimaryProcess(ctx context.Context, pid int) (*Process, error) { log.Tracer(ctx).Tracef("process: getting primary process for PID %d", pid) - if pid <= -1 { + switch pid { + case UnidentifiedProcessID: return GetUnidentifiedProcess(ctx), nil - } - if pid == 0 { + case SystemProcessID: return GetSystemProcess(ctx), nil } @@ -121,10 +121,10 @@ func GetOrFindPrimaryProcess(ctx context.Context, pid int) (*Process, error) { func GetOrFindProcess(ctx context.Context, pid int) (*Process, error) { log.Tracer(ctx).Tracef("process: getting process for PID %d", pid) - if pid <= -1 { + switch pid { + case UnidentifiedProcessID: return GetUnidentifiedProcess(ctx), nil - } - if pid == 0 { + case SystemProcessID: return GetSystemProcess(ctx), nil } @@ -184,10 +184,11 @@ func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) { } func loadProcess(ctx context.Context, pid int) (*Process, error) { - if pid <= -1 { + + switch pid { + case UnidentifiedProcessID: return GetUnidentifiedProcess(ctx), nil - } - if pid == 0 { + case SystemProcessID: return GetSystemProcess(ctx), nil } diff --git a/process/special.go b/process/special.go index 8cbf6130..277d337b 100644 --- a/process/special.go +++ b/process/special.go @@ -8,35 +8,52 @@ import ( "github.com/safing/portmaster/profile" ) +// Special Process IDs +const ( + UnidentifiedProcessID = -1 + SystemProcessID = 0 +) + var ( // unidentifiedProcess is used when a process cannot be found. unidentifiedProcess = &Process{ - UserID: -1, + UserID: UnidentifiedProcessID, UserName: "Unknown", - Pid: -1, - ParentPid: -1, + Pid: UnidentifiedProcessID, + ParentPid: UnidentifiedProcessID, Name: "Unidentified Processes", } // systemProcess is used to represent the Kernel. systemProcess = &Process{ - UserID: 0, + UserID: SystemProcessID, UserName: "Kernel", - Pid: 0, - ParentPid: 0, + Pid: SystemProcessID, + ParentPid: SystemProcessID, Name: "Operating System", } ) +// GetUnidentifiedProcess returns the special process assigned to unidentified processes. func GetUnidentifiedProcess(ctx context.Context) *Process { - return getSpecialProcess(ctx, unidentifiedProcess, profile.GetUnidentifiedProfile) + return getSpecialProcess(ctx, UnidentifiedProcessID, unidentifiedProcess, profile.GetUnidentifiedProfile) } +// GetSystemProcess returns the special process used for the Kernel. func GetSystemProcess(ctx context.Context) *Process { - return getSpecialProcess(ctx, systemProcess, profile.GetSystemProfile) + return getSpecialProcess(ctx, SystemProcessID, systemProcess, profile.GetSystemProfile) } -func getSpecialProcess(ctx context.Context, p *Process, getProfile func() *profile.Profile) *Process { +func getSpecialProcess(ctx context.Context, pid int, template *Process, getProfile func() *profile.Profile) *Process { + // check storage + p, ok := GetProcessFromStorage(pid) + if ok { + return p + } + + // assign template + p = template + p.Lock() defer p.Unlock() diff --git a/profile/active.go b/profile/active.go index 3d45a727..ff6a71c8 100644 --- a/profile/active.go +++ b/profile/active.go @@ -50,7 +50,7 @@ func markActiveProfileAsOutdated(scopedID string) { } } -func cleanActiveProfiles(ctx context.Context) error { //nolint:param // need to conform to interface +func cleanActiveProfiles(ctx context.Context) error { for { select { case <-time.After(activeProfileCleanerTickDuration): diff --git a/profile/special.go b/profile/special.go index 13993337..6bce01d7 100644 --- a/profile/special.go +++ b/profile/special.go @@ -9,6 +9,7 @@ const ( systemProfileID = "_system" ) +// GetUnidentifiedProfile returns the special profile assigned to unidentified processes. func GetUnidentifiedProfile() *Profile { // get profile profile, err := GetProfile(SourceLocal, unidentifiedProfileID) @@ -31,6 +32,7 @@ func GetUnidentifiedProfile() *Profile { return profile } +// GetSystemProfile returns the special profile used for the Kernel. func GetSystemProfile() *Profile { // get profile profile, err := GetProfile(SourceLocal, systemProfileID) From 92d41961e041aadea896f7c829c25a524484fc2f Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 20 Apr 2020 13:57:40 +0200 Subject: [PATCH 09/30] Fix and improve network database ops --- network/clean.go | 3 +-- network/connection.go | 46 ++++++++++++++++++------------------------- network/database.go | 10 +++++----- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/network/clean.go b/network/clean.go index 2c8cff02..ec51b611 100644 --- a/network/clean.go +++ b/network/clean.go @@ -57,8 +57,7 @@ func cleanConnections() (activePIDs map[int]struct{}) { // Step 2: mark end activePIDs[conn.process.Pid] = struct{}{} conn.Ended = now - // "save" - dbController.PushUpdate(conn) + conn.Save() } case conn.Ended < deleteOlderThan: // Step 3: delete diff --git a/network/connection.go b/network/connection.go index 88b36e14..b9bef333 100644 --- a/network/connection.go +++ b/network/connection.go @@ -230,39 +230,31 @@ func (conn *Connection) SaveWhenFinished() { // Save saves the connection in the storage and propagates the change through the database system. func (conn *Connection) Save() { - if conn.ID == "" { + conn.UpdateMeta() - // dns request - if !conn.KeyIsSet() { + if !conn.KeyIsSet() { + if conn.ID == "" { + // dns request + + // set key conn.SetKey(fmt.Sprintf("network:tree/%d/%s", conn.process.Pid, conn.Scope)) - conn.UpdateMeta() - } - // save to internal state - // check if it already exists - mapKey := strconv.Itoa(conn.process.Pid) + "/" + conn.Scope - dnsConnsLock.Lock() - _, ok := dnsConns[mapKey] - if !ok { + mapKey := strconv.Itoa(conn.process.Pid) + "/" + conn.Scope + + // save + dnsConnsLock.Lock() dnsConns[mapKey] = conn - } - dnsConnsLock.Unlock() + dnsConnsLock.Unlock() + } else { + // network connection - } else { - - // connection - if !conn.KeyIsSet() { + // set key conn.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", conn.process.Pid, conn.Scope, conn.ID)) - conn.UpdateMeta() - } - // save to internal state - // check if it already exists - connsLock.Lock() - _, ok := conns[conn.ID] - if !ok { - conns[conn.ID] = conn - } - connsLock.Unlock() + // save + connsLock.Lock() + conns[conn.ID] = conn + connsLock.Unlock() + } } // notify database controller diff --git a/network/database.go b/network/database.go index d7dca398..073dcbc0 100644 --- a/network/database.go +++ b/network/database.go @@ -77,7 +77,7 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) { if slashes <= 1 { // processes for _, proc := range process.All() { - if strings.HasPrefix(proc.DatabaseKey(), q.DatabaseKeyPrefix()) { + if q.Matches(proc) { it.Next <- proc } } @@ -86,9 +86,9 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) { if slashes <= 2 { // dns scopes only dnsConnsLock.RLock() - for _, dnsConns := range dnsConns { - if strings.HasPrefix(dnsConns.DatabaseKey(), q.DatabaseKeyPrefix()) { - it.Next <- dnsConns + for _, dnsConn := range dnsConns { + if q.Matches(dnsConn) { + it.Next <- dnsConn } } dnsConnsLock.RUnlock() @@ -98,7 +98,7 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) { // connections connsLock.RLock() for _, conn := range conns { - if strings.HasPrefix(conn.DatabaseKey(), q.DatabaseKeyPrefix()) { + if q.Matches(conn) { it.Next <- conn } } From bffe4a9eaff9ef01978c630f31f7c5472b2213d8 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Fri, 17 Apr 2020 15:55:52 +0200 Subject: [PATCH 10/30] Add CNAME blocking support --- intel/entity.go | 63 ++++++++++++-- nameserver/nameserver.go | 124 +++++++++++++++++++-------- nameserver/only/nameserver.go | 76 ++++++++++------ network/connection.go | 26 +++--- profile/config.go | 20 +++++ profile/endpoints/endpoint-domain.go | 88 ++++++++++++++----- profile/profile-layered.go | 6 ++ resolver/ipinfo.go | 123 ++++++++++++++++++++++---- resolver/ipinfo_test.go | 55 ++++++++---- 9 files changed, 445 insertions(+), 136 deletions(-) diff --git a/intel/entity.go b/intel/entity.go index 290da215..39a3765c 100644 --- a/intel/entity.go +++ b/intel/entity.go @@ -32,14 +32,32 @@ type Entity struct { asnListLoaded bool reverseResolveEnabled bool resolveSubDomainLists bool + checkCNAMEs bool + // Protocol is the protcol number used by the connection. Protocol uint8 - Port uint16 - Domain string - IP net.IP - Country string - ASN uint + // Port is the destination port of the connection + Port uint16 + + // Domain is the target domain of the connection. + Domain string + + // CNAME is a list of domain names that have been + // resolved for Domain. + CNAME []string + + // IP is the IP address of the connection. If domain is + // set, IP has been resolved by following all CNAMEs. + IP net.IP + + // Country holds the country the IP address (ASN) is + // located in. + Country string + + // ASN holds the autonomous system number of the IP. + ASN uint + location *geoip.Location Lists []string @@ -79,6 +97,7 @@ func (e *Entity) ResetLists() { e.countryListLoaded = false e.asnListLoaded = false e.resolveSubDomainLists = false + e.checkCNAMEs = false e.loadDomainListOnce = sync.Once{} e.loadIPListOnce = sync.Once{} e.loadCoutryListOnce = sync.Once{} @@ -94,6 +113,21 @@ func (e *Entity) ResolveSubDomainLists(enabled bool) { e.resolveSubDomainLists = enabled } +// EnableCNAMECheck enalbes or disables list lookups for +// entity CNAMEs. +func (e *Entity) EnableCNAMECheck(enabled bool) { + if e.domainListLoaded { + log.Warningf("intel/filterlists: tried to change CNAME resolving for %s but lists are already fetched", e.Domain) + } + e.checkCNAMEs = enabled +} + +// CNAMECheckEnabled returns true if the entities CNAMEs should +// also be checked. +func (e *Entity) CNAMECheckEnabled() bool { + return e.checkCNAMEs +} + // Domain and IP // EnableReverseResolving enables reverse resolving the domain from the IP on demand. @@ -220,10 +254,23 @@ func (e *Entity) getDomainLists() { } e.loadDomainListOnce.Do(func() { - var domains = []string{domain} + var domainsToInspect = []string{domain} + + if e.checkCNAMEs { + log.Tracef("intel: CNAME filtering enabled, checking %v too", e.CNAME) + domainsToInspect = append(domainsToInspect, e.CNAME...) + } + + var domains []string if e.resolveSubDomainLists { - domains = splitDomain(domain) - log.Tracef("intel: subdomain list resolving is enabled, checking %v", domains) + for _, domain := range domainsToInspect { + subdomains := splitDomain(domain) + domains = append(domains, subdomains...) + + log.Tracef("intel: subdomain list resolving is enabled: %s => %v", domains, subdomains) + } + } else { + domains = domainsToInspect } for _, d := range domains { diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 8f2dbd65..f02151fe 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -2,9 +2,11 @@ package nameserver import ( "context" + "fmt" "net" "strings" + "github.com/safing/portbase/database" "github.com/safing/portbase/modules/subsystems" "github.com/safing/portbase/log" @@ -14,6 +16,7 @@ import ( "github.com/safing/portmaster/netenv" "github.com/safing/portmaster/network" "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/profile/endpoints" "github.com/safing/portmaster/resolver" "github.com/miekg/dns" @@ -87,9 +90,11 @@ func stop() error { return nil } -func returnNXDomain(w dns.ResponseWriter, query *dns.Msg) { +func returnNXDomain(w dns.ResponseWriter, query *dns.Msg, reason string) { m := new(dns.Msg) m.SetRcode(query, dns.RcodeNameError) + rr, _ := dns.NewRR("portmaster.block.reason. 0 IN TXT " + fmt.Sprintf("%q", reason)) + m.Extra = []dns.RR{rr} _ = w.WriteMsg(m) } @@ -126,7 +131,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er if question.Qclass != dns.ClassINET { // we only serve IN records, return nxdomain log.Warningf("nameserver: only IN record requests are supported but received Qclass %d, returning NXDOMAIN", question.Qclass) - returnNXDomain(w, query) + returnNXDomain(w, query, "wrong type") return nil } @@ -166,7 +171,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // check if valid domain name if !netutils.IsValidFqdn(q.FQDN) { log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", q.FQDN) - returnNXDomain(w, query) + returnNXDomain(w, query, "invalid domain") return nil } @@ -177,7 +182,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // TODO: if there are 3 request for the same domain/type in a row, delete all caches of that domain // get connection - conn := network.NewConnectionFromDNSRequest(ctx, q.FQDN, remoteAddr.IP, uint16(remoteAddr.Port)) + conn := network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, remoteAddr.IP, uint16(remoteAddr.Port)) // once we decided on the connection we might need to save it to the database // so we defer that check right now. @@ -202,7 +207,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // TODO: this has been obsoleted due to special profiles if conn.Process().Profile() == nil { tracer.Infof("nameserver: failed to find process for request %s, returning NXDOMAIN", conn) - returnNXDomain(w, query) + returnNXDomain(w, query, "unknown process") // NOTE(ppacher): saving unknown process connection might end up in a lot of // processes. Consider disabling that via config. conn.Failed("Unknown process") @@ -218,7 +223,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // log.Tracef("nameserver: domain %s has lms score of %f", fqdn, lms) if lms < 10 { tracer.Warningf("nameserver: possible data tunnel by %s: %s has lms score of %f, returning nxdomain", conn.Process(), q.FQDN, lms) - returnNXDomain(w, query) + returnNXDomain(w, query, "lms") conn.Block("Possible data tunnel") return nil } @@ -229,7 +234,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er switch conn.Verdict { case network.VerdictBlock: tracer.Infof("nameserver: %s blocked, returning nxdomain", conn) - returnNXDomain(w, query) + returnNXDomain(w, query, conn.Reason) return nil case network.VerdictDrop, network.VerdictFailed: tracer.Infof("nameserver: %s dropped, not replying", conn) @@ -241,7 +246,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er if err != nil { // TODO: analyze nxdomain requests, malware could be trying DGA-domains tracer.Warningf("nameserver: %s requested %s%s: %s", conn.Process(), q.FQDN, q.QType, err) - returnNXDomain(w, query) + returnNXDomain(w, query, conn.Reason) conn.Failed("failed to resolve: " + err.Error()) return nil } @@ -251,41 +256,92 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // TODO: FilterDNSResponse also sets a connection verdict if rrCache == nil { tracer.Infof("nameserver: %s implicitly denied by filtering the dns response, returning nxdomain", conn) - returnNXDomain(w, query) + returnNXDomain(w, query, conn.Reason) conn.Block("DNS response filtered") return nil } // save IP addresses to IPInfo + cnames := make(map[string]string) + ips := make(map[string]struct{}) + for _, rr := range append(rrCache.Answer, rrCache.Extra...) { switch v := rr.(type) { + case *dns.CNAME: + cnames[v.Hdr.Name] = v.Target + case *dns.A: - ipInfo, err := resolver.GetIPInfo(v.A.String()) - if err != nil { - ipInfo = &resolver.IPInfo{ - IP: v.A.String(), - Domains: []string{q.FQDN}, - } - _ = ipInfo.Save() - } else { - added := ipInfo.AddDomain(q.FQDN) - if added { - _ = ipInfo.Save() - } - } + ips[v.A.String()] = struct{}{} + case *dns.AAAA: - ipInfo, err := resolver.GetIPInfo(v.AAAA.String()) - if err != nil { - ipInfo = &resolver.IPInfo{ - IP: v.AAAA.String(), - Domains: []string{q.FQDN}, - } - _ = ipInfo.Save() - } else { - added := ipInfo.AddDomain(q.FQDN) - if added { - _ = ipInfo.Save() - } + ips[v.AAAA.String()] = struct{}{} + } + } + + for ip := range ips { + record := resolver.ResolvedDomain{ + Domain: q.FQDN, + } + + // resolve all CNAMEs in the correct order. + var domain = q.FQDN + for { + nextDomain, isCNAME := cnames[domain] + if !isCNAME { + break + } + + record.CNAMEs = append(record.CNAMEs, nextDomain) + domain = nextDomain + } + + // update the entity to include the cnames + conn.Entity.CNAME = record.CNAMEs + + // get the existing IP info or create a new one + var save bool + info, err := resolver.GetIPInfo(ip) + if err != nil { + if err != database.ErrNotFound { + log.Errorf("nameserver: failed to search for IP info record: %s", err) + } + + info = &resolver.IPInfo{ + IP: ip, + } + save = true + } + + // and the new resolved domain record and save + if new := info.AddDomain(record); new { + save = true + } + if save { + if err := info.Save(); err != nil { + log.Errorf("nameserver: failed to save IP info record: %s", err) + } + } + } + + // if we have CNAMEs and the profile is configured to filter them + // we need to re-check the lists and endpoints here + if conn.Process().Profile().FilterCNAMEs() { + conn.Entity.ResetLists() + conn.Entity.EnableCNAMECheck(true) + + result, reason := conn.Process().Profile().MatchEndpoint(conn.Entity) + if result == endpoints.Denied { + conn.Block("endpoint in blocklist: " + reason) + returnNXDomain(w, query, conn.Reason) + return nil + } + + if result == endpoints.NoMatch { + result, reason = conn.Process().Profile().MatchFilterLists(conn.Entity) + if result == endpoints.Denied { + conn.Block("endpoint in filterlists: " + reason) + returnNXDomain(w, query, conn.Reason) + return nil } } } diff --git a/nameserver/only/nameserver.go b/nameserver/only/nameserver.go index feaa5ca5..6d5cb5cb 100644 --- a/nameserver/only/nameserver.go +++ b/nameserver/only/nameserver.go @@ -5,6 +5,7 @@ import ( "net" "strings" + "github.com/safing/portbase/database" "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portmaster/netenv" @@ -164,35 +165,60 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er } // save IP addresses to IPInfo + cnames := make(map[string]string) + ips := make(map[string]struct{}) + for _, rr := range append(rrCache.Answer, rrCache.Extra...) { switch v := rr.(type) { + case *dns.CNAME: + cnames[v.Hdr.Name] = v.Target + case *dns.A: - ipInfo, err := resolver.GetIPInfo(v.A.String()) - if err != nil { - ipInfo = &resolver.IPInfo{ - IP: v.A.String(), - Domains: []string{q.FQDN}, - } - _ = ipInfo.Save() - } else { - added := ipInfo.AddDomain(q.FQDN) - if added { - _ = ipInfo.Save() - } - } + ips[v.A.String()] = struct{}{} + case *dns.AAAA: - ipInfo, err := resolver.GetIPInfo(v.AAAA.String()) - if err != nil { - ipInfo = &resolver.IPInfo{ - IP: v.AAAA.String(), - Domains: []string{q.FQDN}, - } - _ = ipInfo.Save() - } else { - added := ipInfo.AddDomain(q.FQDN) - if added { - _ = ipInfo.Save() - } + ips[v.AAAA.String()] = struct{}{} + } + } + + for ip := range ips { + record := resolver.ResolvedDomain{ + Domain: q.FQDN, + } + + // resolve all CNAMEs in the correct order. + var domain = q.FQDN + for { + nextDomain, isCNAME := cnames[domain] + if !isCNAME { + break + } + + record.CNAMEs = append(record.CNAMEs, nextDomain) + domain = nextDomain + } + + // get the existing IP info or create a new one + var save bool + info, err := resolver.GetIPInfo(ip) + if err != nil { + if err != database.ErrNotFound { + log.Errorf("nameserver: failed to search for IP info record: %s", err) + } + + info = &resolver.IPInfo{ + IP: ip, + } + save = true + } + + // and the new resolved domain record and save + if new := info.AddDomain(record); new { + save = true + } + if save { + if err := info.Save(); err != nil { + log.Errorf("nameserver: failed to save IP info record: %s", err) } } } diff --git a/network/connection.go b/network/connection.go index b9bef333..e0af4c01 100644 --- a/network/connection.go +++ b/network/connection.go @@ -54,9 +54,9 @@ type Connection struct { //nolint:maligned // TODO: fix alignment } // NewConnectionFromDNSRequest returns a new connection based on the given dns request. -func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, ip net.IP, port uint16) *Connection { +func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []string, localIP net.IP, localPort uint16) *Connection { // get Process - proc, err := process.GetProcessByEndpoints(ctx, ip, port, dnsAddress, dnsPort, packet.UDP) + proc, err := process.GetProcessByEndpoints(ctx, localIP, localPort, dnsAddress, dnsPort, packet.UDP) if err != nil { log.Warningf("network: failed to find process of dns request for %s: %s", fqdn, err) proc = process.GetUnidentifiedProcess(ctx) @@ -67,7 +67,8 @@ func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, ip net.IP, po Scope: fqdn, Entity: (&intel.Entity{ Domain: fqdn, - }).Init(), + CNAME: cnames, + }), process: proc, Started: timestamp, Ended: timestamp, @@ -104,7 +105,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { IP: pkt.Info().Src, Protocol: uint8(pkt.Info().Protocol), Port: pkt.Info().SrcPort, - }).Init() + }) } else { @@ -113,18 +114,21 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { IP: pkt.Info().Dst, Protocol: uint8(pkt.Info().Protocol), Port: pkt.Info().DstPort, - }).Init() + }) // check if we can find a domain for that IP ipinfo, err := resolver.GetIPInfo(pkt.Info().Dst.String()) if err == nil { + lastResolvedDomain := ipinfo.ResolvedDomains.MostRecentDomain() + if lastResolvedDomain != nil { + scope = lastResolvedDomain.Domain + entity.Domain = lastResolvedDomain.Domain + entity.CNAME = lastResolvedDomain.CNAMEs + removeOpenDNSRequest(proc.Pid, lastResolvedDomain.Domain) + } + } - // outbound to domain - scope = ipinfo.Domains[0] - entity.Domain = scope - removeOpenDNSRequest(proc.Pid, scope) - - } else { + if scope == "" { // outbound direct (possibly P2P) connection switch netutils.ClassifyIP(pkt.Info().Dst) { diff --git a/profile/config.go b/profile/config.go index 2607646a..1890afa6 100644 --- a/profile/config.go +++ b/profile/config.go @@ -30,6 +30,9 @@ var ( CfgOptionFilterSubDomainsKey = "filter/includeSubdomains" cfgOptionFilterSubDomains config.IntOption // security level option + CfgOptionFilterCNAMEKey = "filter/includeCNAMEs" + cfgOptionFilterCNAME config.IntOption // security level option + CfgOptionBlockScopeLocalKey = "filter/blockLocal" cfgOptionBlockScopeLocal config.IntOption // security level option @@ -180,6 +183,23 @@ Examples: cfgOptionFilterLists = config.Concurrent.GetAsStringArray(CfgOptionFilterListKey, []string{}) cfgStringArrayOptions[CfgOptionFilterListKey] = cfgOptionFilterLists + // Include CNAMEs + err = config.Register(&config.Option{ + Name: "Filter CNAMEs", + Key: CfgOptionFilterCNAMEKey, + Description: "Also filter requests where a CNAME would be blocked", + OptType: config.OptTypeInt, + ExternalOptType: "security level", + DefaultValue: status.SecurityLevelsAll, + ValidationRegex: "^(7|6|4)$", + }) + if err != nil { + return err + } + cfgOptionFilterCNAME = config.Concurrent.GetAsInt(CfgOptionFilterCNAMEKey, int64(status.SecurityLevelsAll)) + cfgIntOptions[CfgOptionFilterCNAMEKey] = cfgOptionFilterCNAME + + // Include subdomains err = config.Register(&config.Option{ Name: "Filter SubDomains", Key: CfgOptionFilterSubDomainsKey, diff --git a/profile/endpoints/endpoint-domain.go b/profile/endpoints/endpoint-domain.go index 225a4b5d..3350f053 100644 --- a/profile/endpoints/endpoint-domain.go +++ b/profile/endpoints/endpoint-domain.go @@ -31,35 +31,77 @@ type EndpointDomain struct { Reason string } +func (ep *EndpointDomain) check(entity *intel.Entity, domain string) (EPResult, string) { + switch ep.MatchType { + case domainMatchTypeExact: + if domain == ep.Domain { + return ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypeZone: + if domain == ep.Domain { + return ep.matchesPPP(entity), ep.Reason + } + if strings.HasSuffix(domain, ep.DomainZone) { + return ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypeSuffix: + if strings.HasSuffix(domain, ep.Domain) { + return ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypePrefix: + if strings.HasPrefix(domain, ep.Domain) { + return ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypeContains: + if strings.Contains(domain, ep.Domain) { + return ep.matchesPPP(entity), ep.Reason + } + } + return NoMatch, "" +} + // 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 + result, reason = ep.check(entity, entity.Domain) + if result != NoMatch { + return + } + + if entity.CNAMECheckEnabled() { + for _, domain := range entity.CNAME { + switch ep.MatchType { + case domainMatchTypeExact: + if domain == ep.Domain { + result, reason = ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypeZone: + if domain == ep.Domain { + result, reason = ep.matchesPPP(entity), ep.Reason + } + if strings.HasSuffix(domain, ep.DomainZone) { + result, reason = ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypeSuffix: + if strings.HasSuffix(domain, ep.Domain) { + result, reason = ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypePrefix: + if strings.HasPrefix(domain, ep.Domain) { + result, reason = ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypeContains: + if strings.Contains(domain, ep.Domain) { + result, reason = ep.matchesPPP(entity), ep.Reason + } + } + + if result == Denied { + return result, reason + } } } diff --git a/profile/profile-layered.go b/profile/profile-layered.go index dd0af165..4ba2210b 100644 --- a/profile/profile-layered.go +++ b/profile/profile-layered.go @@ -43,6 +43,7 @@ type LayeredProfile struct { RemoveOutOfScopeDNS config.BoolOption RemoveBlockedDNS config.BoolOption FilterSubDomains config.BoolOption + FilterCNAMEs config.BoolOption PreventBypassing config.BoolOption } @@ -99,6 +100,10 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile { CfgOptionFilterSubDomainsKey, cfgOptionFilterSubDomains, ) + new.FilterCNAMEs = new.wrapSecurityLevelOption( + CfgOptionFilterCNAMEKey, + cfgOptionFilterCNAME, + ) new.PreventBypassing = new.wrapSecurityLevelOption( CfgOptionPreventBypassingKey, cfgOptionPreventBypassing, @@ -236,6 +241,7 @@ func (lp *LayeredProfile) MatchServiceEndpoint(entity *intel.Entity) (result end // lists. func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPResult, string) { entity.ResolveSubDomainLists(lp.FilterSubDomains()) + entity.EnableCNAMECheck(lp.FilterCNAMEs()) lookupMap, hasLists := entity.GetListsMap() if !hasLists { diff --git a/resolver/ipinfo.go b/resolver/ipinfo.go index 760b25f3..03c0b59b 100644 --- a/resolver/ipinfo.go +++ b/resolver/ipinfo.go @@ -16,13 +16,92 @@ var ( }) ) +// ResolvedDomain holds a Domain name and a list of +// CNAMES that have been resolved. +type ResolvedDomain struct { + // Domain is the domain as requested by the application. + Domain string + + // CNAMEs is a list of CNAMEs that have been resolved for + // Domain. + CNAMEs []string +} + +// String returns a string representation of ResolvedDomain including +// the CNAME chain. It implements fmt.Stringer +func (resolved *ResolvedDomain) String() string { + ret := resolved.Domain + cnames := "" + + if len(resolved.CNAMEs) > 0 { + cnames = " (-> " + strings.Join(resolved.CNAMEs, "->") + ")" + } + + return ret + cnames +} + +// ResolvedDomains is a helper type for operating on a slice +// of ResolvedDomain +type ResolvedDomains []ResolvedDomain + +// String returns a string representation of all domains joined +// to a single string. +func (rds ResolvedDomains) String() string { + var domains []string + for _, n := range rds { + domains = append(domains, n.String()) + } + return strings.Join(domains, " or ") +} + +// MostRecentDomain returns the most recent domain. +func (rds ResolvedDomains) MostRecentDomain() *ResolvedDomain { + if len(rds) == 0 { + return nil + } + // TODO(ppacher): we could also do that by using ResolvedAt() + mostRecent := rds[len(rds)-1] + return &mostRecent +} + // IPInfo represents various information about an IP. type IPInfo struct { record.Base sync.Mutex - IP string - Domains []string + // IP holds the acutal IP address. + IP string + + // Domains holds a list of domains that have been + // resolved to IP. This field is deprecated and should + // be removed. + // DEPRECATED: remove with alpha. + Domains []string `json:"Domains,omitempty"` + + // ResolvedDomain is a slice of domains that + // have been requested by various applications + // and have been resolved to IP. + ResolvedDomains ResolvedDomains +} + +// AddDomain adds a new resolved domain to ipi. +func (ipi *IPInfo) AddDomain(resolved ResolvedDomain) bool { + for idx, d := range ipi.ResolvedDomains { + if d.Domain == resolved.Domain { + if utils.StringSliceEqual(d.CNAMEs, resolved.CNAMEs) { + return false + } + + // we have a different CNAME chain now, remove the previous + // entry and add it at the end. + ipi.ResolvedDomains = append(ipi.ResolvedDomains[:idx], ipi.ResolvedDomains[idx+1:]...) + ipi.ResolvedDomains = append(ipi.ResolvedDomains, resolved) + return true + } + } + + ipi.ResolvedDomains = append(ipi.ResolvedDomains, resolved) + return true } func makeIPInfoKey(ip string) string { @@ -46,6 +125,19 @@ func GetIPInfo(ip string) (*IPInfo, error) { if err != nil { return nil, err } + + // Legacy support, + // DEPRECATED: remove with alpha + if len(new.Domains) > 0 && len(new.ResolvedDomains) == 0 { + for _, d := range new.Domains { + new.ResolvedDomains = append(new.ResolvedDomains, ResolvedDomain{ + Domain: d, + // rest is empty... + }) + } + new.Domains = nil // clean up so we remove it from the database + } + return new, nil } @@ -57,17 +149,6 @@ func GetIPInfo(ip string) (*IPInfo, error) { return new, nil } -// AddDomain adds a domain to the list and reports back if it was added, or was already present. -func (ipi *IPInfo) AddDomain(domain string) (added bool) { - ipi.Lock() - defer ipi.Unlock() - if !utils.StringInSlice(ipi.Domains, domain) { - ipi.Domains = append([]string{domain}, ipi.Domains...) - return true - } - return false -} - // Save saves the IPInfo record to the database. func (ipi *IPInfo) Save() error { ipi.Lock() @@ -75,17 +156,21 @@ func (ipi *IPInfo) Save() error { ipi.SetKey(makeIPInfoKey(ipi.IP)) } ipi.Unlock() - return ipInfoDatabase.Put(ipi) -} -// FmtDomains returns a string consisting of the domains that have seen to use this IP, joined by " or " -func (ipi *IPInfo) FmtDomains() string { - return strings.Join(ipi.Domains, " or ") + // Legacy support + // Ensure we don't write new Domain fields into the + // database. + // DEPRECATED: remove with alpha + if len(ipi.Domains) > 0 { + ipi.Domains = nil + } + + return ipInfoDatabase.Put(ipi) } // FmtDomains returns a string consisting of the domains that have seen to use this IP, joined by " or " func (ipi *IPInfo) String() string { ipi.Lock() defer ipi.Unlock() - return fmt.Sprintf(" Date: Fri, 17 Apr 2020 17:02:04 +0200 Subject: [PATCH 11/30] Add support to detect upstream DNS resolver blocking --- nameserver/nameserver.go | 94 +++++++++++++++------------- profile/endpoints/endpoint-domain.go | 27 +------- resolver/block_detection.go | 61 ++++++++++++++++++ resolver/config.go | 22 ++++--- resolver/resolve.go | 15 +++++ resolver/resolver.go | 35 ++++++++++- resolver/resolvers.go | 25 ++++++-- 7 files changed, 194 insertions(+), 85 deletions(-) create mode 100644 resolver/block_detection.go diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index f02151fe..013fc749 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -246,8 +246,14 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er if err != nil { // TODO: analyze nxdomain requests, malware could be trying DGA-domains tracer.Warningf("nameserver: %s requested %s%s: %s", conn.Process(), q.FQDN, q.QType, err) + + if _, ok := err.(*resolver.BlockedUpstreamError); ok { + conn.Block(err.Error()) + } else { + conn.Failed("failed to resolve: " + err.Error()) + } + returnNXDomain(w, query, conn.Reason) - conn.Failed("failed to resolve: " + err.Error()) return nil } @@ -261,6 +267,51 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er return nil } + updateIPsAndCNAMEs(q, rrCache, conn) + + // if we have CNAMEs and the profile is configured to filter them + // we need to re-check the lists and endpoints here + if conn.Process().Profile().FilterCNAMEs() { + conn.Entity.ResetLists() + conn.Entity.EnableCNAMECheck(true) + + result, reason := conn.Process().Profile().MatchEndpoint(conn.Entity) + if result == endpoints.Denied { + conn.Block("endpoint in blocklist: " + reason) + returnNXDomain(w, query, conn.Reason) + return nil + } + + if result == endpoints.NoMatch { + result, reason = conn.Process().Profile().MatchFilterLists(conn.Entity) + if result == endpoints.Denied { + conn.Block("endpoint in filterlists: " + reason) + returnNXDomain(w, query, conn.Reason) + return nil + } + } + } + + // reply to query + m := new(dns.Msg) + m.SetReply(query) + m.Answer = rrCache.Answer + m.Ns = rrCache.Ns + m.Extra = rrCache.Extra + + if err := w.WriteMsg(m); err != nil { + log.Warningf("nameserver: failed to return response %s%s to %s: %s", q.FQDN, q.QType, conn.Process(), err) + } else { + tracer.Debugf("nameserver: returning response %s%s to %s", q.FQDN, q.QType, conn.Process()) + } + + // save dns request as open + network.SaveOpenDNSRequest(conn) + + return nil +} + +func updateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *network.Connection) { // save IP addresses to IPInfo cnames := make(map[string]string) ips := make(map[string]struct{}) @@ -322,45 +373,4 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er } } } - - // if we have CNAMEs and the profile is configured to filter them - // we need to re-check the lists and endpoints here - if conn.Process().Profile().FilterCNAMEs() { - conn.Entity.ResetLists() - conn.Entity.EnableCNAMECheck(true) - - result, reason := conn.Process().Profile().MatchEndpoint(conn.Entity) - if result == endpoints.Denied { - conn.Block("endpoint in blocklist: " + reason) - returnNXDomain(w, query, conn.Reason) - return nil - } - - if result == endpoints.NoMatch { - result, reason = conn.Process().Profile().MatchFilterLists(conn.Entity) - if result == endpoints.Denied { - conn.Block("endpoint in filterlists: " + reason) - returnNXDomain(w, query, conn.Reason) - return nil - } - } - } - - // reply to query - m := new(dns.Msg) - m.SetReply(query) - m.Answer = rrCache.Answer - m.Ns = rrCache.Ns - m.Extra = rrCache.Extra - - if err := w.WriteMsg(m); err != nil { - log.Warningf("nameserver: failed to return response %s%s to %s: %s", q.FQDN, q.QType, conn.Process(), err) - } else { - tracer.Debugf("nameserver: returning response %s%s to %s", q.FQDN, q.QType, conn.Process()) - } - - // save dns request as open - network.SaveOpenDNSRequest(conn) - - return nil } diff --git a/profile/endpoints/endpoint-domain.go b/profile/endpoints/endpoint-domain.go index 3350f053..2691ba13 100644 --- a/profile/endpoints/endpoint-domain.go +++ b/profile/endpoints/endpoint-domain.go @@ -73,32 +73,7 @@ func (ep *EndpointDomain) Matches(entity *intel.Entity) (result EPResult, reason if entity.CNAMECheckEnabled() { for _, domain := range entity.CNAME { - switch ep.MatchType { - case domainMatchTypeExact: - if domain == ep.Domain { - result, reason = ep.matchesPPP(entity), ep.Reason - } - case domainMatchTypeZone: - if domain == ep.Domain { - result, reason = ep.matchesPPP(entity), ep.Reason - } - if strings.HasSuffix(domain, ep.DomainZone) { - result, reason = ep.matchesPPP(entity), ep.Reason - } - case domainMatchTypeSuffix: - if strings.HasSuffix(domain, ep.Domain) { - result, reason = ep.matchesPPP(entity), ep.Reason - } - case domainMatchTypePrefix: - if strings.HasPrefix(domain, ep.Domain) { - result, reason = ep.matchesPPP(entity), ep.Reason - } - case domainMatchTypeContains: - if strings.Contains(domain, ep.Domain) { - result, reason = ep.matchesPPP(entity), ep.Reason - } - } - + result, reason = ep.check(entity, domain) if result == Denied { return result, reason } diff --git a/resolver/block_detection.go b/resolver/block_detection.go new file mode 100644 index 00000000..8a4005cd --- /dev/null +++ b/resolver/block_detection.go @@ -0,0 +1,61 @@ +package resolver + +import ( + "net" + + "github.com/miekg/dns" +) + +// Supported upstream block detections +const ( + BlockDetectionRefused = "refused" + BlockDetectionZeroIP = "zeroip" + BlockDetectionEmptyAnswer = "empty" + BlockDetectionDisabled = "disabled" +) + +func isBlockedUpstream(resolver *Resolver, answer *dns.Msg) bool { + if resolver.UpstreamBlockDetection == BlockDetectionDisabled { + return false + } + + switch resolver.UpstreamBlockDetection { + case BlockDetectionRefused: + return answer.Rcode == dns.RcodeRefused + case BlockDetectionZeroIP: + if answer.Rcode != dns.RcodeSuccess { + return false + } + var ips []net.IP + for _, rr := range answer.Answer { + switch v := rr.(type) { + case *dns.A: + ips = append(ips, v.A) + case *dns.AAAA: + ips = append(ips, v.AAAA) + } + } + + if len(ips) == 0 { + return false // we expected an empty IP + } + + for _, ip := range ips { + if ip.To4() != nil { + if !ip.Equal(net.IPv4zero) { + return false + } + } else { + if !ip.To16().Equal(net.IPv6zero) { + return false + } + } + } + + return true + case BlockDetectionEmptyAnswer: + return answer.Rcode == dns.RcodeNameError && len(answer.Ns) == 0 && len(answer.Answer) == 0 && len(answer.Extra) == 0 + } + + return false +} diff --git a/resolver/config.go b/resolver/config.go index d4c4828d..ac9567b0 100644 --- a/resolver/config.go +++ b/resolver/config.go @@ -29,28 +29,30 @@ var ( // We encourage everyone who has the technical abilities to set their own preferred servers. // Default 1: Cloudflare - "dot://1.1.1.1:853?verify=cloudflare-dns.com", // Cloudflare - "dot://1.0.0.1:853?verify=cloudflare-dns.com", // Cloudflare + "dot://1.1.1.1:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip", // Cloudflare + "dot://1.0.0.1:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip", // Cloudflare // Default 2: Quad9 - "dot://9.9.9.9:853?verify=dns.quad9.net", // Quad9 - "dot://149.112.112.112:853?verify=dns.quad9.net", // Quad9 + "dot://9.9.9.9:853?verify=dns.quad9.net&name=Quad9&blockedif=empty", // Quad9 + "dot://149.112.112.112:853?verify=dns.quad9.net&name=Quad9&blockedif=empty", // Quad9 // Fallback 1: Cloudflare - "dns://1.1.1.1:53", // Cloudflare - "dns://1.0.0.1:53", // Cloudflare + "dns://1.1.1.1:53?name=Cloudflare&blockedif=zeroip", // Cloudflare + "dns://1.0.0.1:53?name=Cloudflare&blockedif=zeroip", // Cloudflare // Fallback 2: Quad9 - "dns://9.9.9.9:53", // Quad9 - "dns://149.112.112.112:53", // Quad9 + "dns://9.9.9.9:53?name=Quad9&blockedif=empty", // Quad9 + "dns://149.112.112.112:53?name=Quad9&blockedif=empty", // Quad9 // supported parameters // - `verify=domain`: verify domain (dot only) // future parameters: // // - `name=name`: human readable name for resolver - // - `blockedif=baredns`: how to detect if the dns service blocked something - // - `baredns`: NXDomain result, but without any other record in any section + // - `blockedif=empty`: how to detect if the dns service blocked something + // - `empty`: NXDomain result, but without any other record in any section + // - `refused`: Request was refused + // - `zeroip`: Answer only contains zeroip } CfgOptionNameServersKey = "dns/nameservers" diff --git a/resolver/resolve.go b/resolver/resolve.go index 2c7f532a..f13d07c2 100644 --- a/resolver/resolve.go +++ b/resolver/resolve.go @@ -37,6 +37,21 @@ var ( ErrNoCompliance = fmt.Errorf("%w: no compliant resolvers for this query", ErrBlocked) ) +// BlockedUpstreamError is returned when a DNS request +// has been blocked by the upstream server. +type BlockedUpstreamError struct { + ResolverName string +} + +func (blocked *BlockedUpstreamError) Error() string { + return fmt.Sprintf("Endpoint blocked by upstream DNS resolver %s", blocked.ResolverName) +} + +// Unwrap implements errors.Unwrapper +func (blocked *BlockedUpstreamError) Unwrap() error { + return ErrBlocked +} + // Query describes a dns query. type Query struct { FQDN string diff --git a/resolver/resolver.go b/resolver/resolver.go index 0201b0c5..65155fab 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -28,6 +28,19 @@ type Resolver struct { // Server config url (and ID) Server string + // Name is the name of the resolver as passed via + // ?name=. + Name string + + // UpstreamBlockDetection defines the detection type + // to identifier upstream DNS query blocking. + // Valid values are: + // - zeroip + // - empty + // - refused (default) + // - disabled + UpstreamBlockDetection string + // Parsed config ServerType string ServerAddress string @@ -46,9 +59,25 @@ type Resolver struct { Conn ResolverConn } +// IsBlockedUpstream returns true if the request has been blocked +// upstream. +func (resolver *Resolver) IsBlockedUpstream(answer *dns.Msg) bool { + return isBlockedUpstream(resolver, answer) +} + +// GetName returns the name of the server. If no name +// is configured the server address is returned. +func (resolver *Resolver) GetName() string { + if resolver.Name != "" { + return resolver.Name + } + + return resolver.Server +} + // String returns the URL representation of the resolver. func (resolver *Resolver) String() string { - return resolver.Server + return resolver.GetName() } // ResolverConn is an interface to implement different types of query backends. @@ -126,6 +155,10 @@ func (brc *BasicResolverConn) Query(ctx context.Context, q *Query) (*RRCache, er break } + if resolver.IsBlockedUpstream(reply) { + return nil, &BlockedUpstreamError{resolver.GetName()} + } + // no error break } diff --git a/resolver/resolvers.go b/resolver/resolvers.go index b4451f11..00ad0d0e 100644 --- a/resolver/resolvers.go +++ b/resolver/resolvers.go @@ -107,13 +107,26 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) { return nil, false, fmt.Errorf("DOT must have a verify query parameter set") } + blockType := query.Get("blockedif") + if blockType == "" { + blockType = BlockDetectionRefused + } + + switch blockType { + case BlockDetectionDisabled, BlockDetectionEmptyAnswer, BlockDetectionRefused, BlockDetectionZeroIP: + default: + return nil, false, fmt.Errorf("invalid value for upstream block detection (blockedif=)") + } + new := &Resolver{ - Server: resolverURL, - ServerType: u.Scheme, - ServerAddress: u.Host, - ServerIPScope: scope, - Source: source, - VerifyDomain: verifyDomain, + Server: resolverURL, + ServerType: u.Scheme, + ServerAddress: u.Host, + ServerIPScope: scope, + Source: source, + VerifyDomain: verifyDomain, + Name: query.Get("name"), + UpstreamBlockDetection: blockType, } newConn := &BasicResolverConn{ From 42ccb3e39a5469bfdf7c8be81dca1c65c2eda082 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Mon, 20 Apr 2020 08:25:34 +0200 Subject: [PATCH 12/30] Small changes based on review comments --- intel/entity.go | 11 +++++++++++ nameserver/nameserver.go | 3 ++- profile/config.go | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/intel/entity.go b/intel/entity.go index 39a3765c..f01b53cb 100644 --- a/intel/entity.go +++ b/intel/entity.go @@ -273,6 +273,8 @@ func (e *Entity) getDomainLists() { domains = domainsToInspect } + domains = makeDistinct(domains) + for _, d := range domains { log.Tracef("intel: loading domain list for %s", d) list, err := filterlists.LookupDomain(d) @@ -441,3 +443,12 @@ func buildLookupMap(l []string) filterlists.LookupMap { return m } + +func makeDistinct(slice []string) []string { + lm := buildLookupMap(slice) + result := make([]string, 0, len(lm)) + for key := range lm { + result = append(result, key) + } + return result +} diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 013fc749..a30d933c 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -2,6 +2,7 @@ package nameserver import ( "context" + "errors" "fmt" "net" "strings" @@ -247,7 +248,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // TODO: analyze nxdomain requests, malware could be trying DGA-domains tracer.Warningf("nameserver: %s requested %s%s: %s", conn.Process(), q.FQDN, q.QType, err) - if _, ok := err.(*resolver.BlockedUpstreamError); ok { + if errors.Is(err, &resolver.BlockedUpstreamError{}) { conn.Block(err.Error()) } else { conn.Failed("failed to resolve: " + err.Error()) diff --git a/profile/config.go b/profile/config.go index 1890afa6..8842530e 100644 --- a/profile/config.go +++ b/profile/config.go @@ -192,6 +192,7 @@ Examples: ExternalOptType: "security level", DefaultValue: status.SecurityLevelsAll, ValidationRegex: "^(7|6|4)$", + ExpertiseLevel: config.ExpertiseLevelExpert, }) if err != nil { return err From eeb358425df918cd665337e5fadaa8c84c9430b2 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Mon, 20 Apr 2020 11:36:34 +0200 Subject: [PATCH 13/30] Refactor entity list handling --- intel/entity.go | 183 ++++++++++++++++++++++------ intel/filterlists/lookup_map.go | 25 ---- intel/filterlists/lookup_test.go | 92 -------------- intel/lists.go | 40 ------ profile/endpoints/endpoint-lists.go | 16 ++- profile/endpoints/endpoint.go | 2 +- profile/profile-layered.go | 26 ++-- 7 files changed, 167 insertions(+), 217 deletions(-) delete mode 100644 intel/filterlists/lookup_map.go delete mode 100644 intel/filterlists/lookup_test.go delete mode 100644 intel/lists.go diff --git a/intel/entity.go b/intel/entity.go index f01b53cb..e46ac041 100644 --- a/intel/entity.go +++ b/intel/entity.go @@ -16,6 +16,43 @@ import ( "golang.org/x/net/publicsuffix" ) +// ListMatch represents an entity that has been +// matched against filterlists. +type ListMatch struct { + Entity string + ActiveLists []string + InactiveLists []string +} + +func (lm *ListMatch) String() string { + inactive := "" + if len(lm.InactiveLists) > 0 { + inactive = " and in deactivated lists " + strings.Join(lm.InactiveLists, ", ") + } + return fmt.Sprintf( + "%s in activated lists %s%s", + lm.Entity, + strings.Join(lm.ActiveLists, ","), + inactive, + ) +} + +// ListBlockReason is a list of list matches. +type ListBlockReason []ListMatch + +func (br ListBlockReason) String() string { + if len(br) == 0 { + return "" + } + + matches := make([]string, len(br)) + for idx, lm := range br { + matches[idx] = lm.String() + } + + return strings.Join(matches, " and ") +} + // Entity describes a remote endpoint in many different ways. // It embeddes a sync.Mutex but none of the endpoints own // functions performs locking. The caller MUST ENSURE @@ -60,8 +97,18 @@ type Entity struct { location *geoip.Location - Lists []string - ListsMap filterlists.LookupMap + // BlockedByLists holds list source IDs that + // are used to block the entity. + BlockedByLists []string + + // BlockedEntities holds a list of entities that + // have been blocked. Values can be used as a key + // for the ListOccurences map. + BlockedEntities []string + + // ListOccurences is a map that matches an entity (Domain, IPs, ASN, Country, Sub-domain) + // to a list of sources where the entity has been observed in. + ListOccurences map[string][]string // we only load each data above at most once fetchLocationOnce sync.Once @@ -90,8 +137,11 @@ func (e *Entity) ResetLists() { // TODO(ppacher): our actual goal is to reset the domain // list right now so we could be more efficient by keeping // the other lists around. - e.Lists = nil - e.ListsMap = nil + + // FIXME + //e.Lists = nil + //e.ListsMap = nil + e.ListOccurences = nil e.domainListLoaded = false e.ipListLoaded = false e.countryListLoaded = false @@ -238,9 +288,19 @@ func (e *Entity) getLists() { e.getCountryLists() } -func (e *Entity) mergeList(list []string) { - e.Lists = mergeStringList(e.Lists, list) - e.ListsMap = buildLookupMap(e.Lists) +func (e *Entity) mergeList(key string, list []string) { + if len(list) == 0 { + return + } + + if e.ListOccurences == nil { + e.ListOccurences = make(map[string][]string) + } + + e.ListOccurences[key] = mergeStringList(e.ListOccurences[key], list) + + //e.Lists = mergeStringList(e.Lists, list) + //e.ListsMap = buildLookupMap(e.Lists) } func (e *Entity) getDomainLists() { @@ -284,7 +344,7 @@ func (e *Entity) getDomainLists() { return } - e.mergeList(list) + e.mergeList(d, list) } e.domainListLoaded = true }) @@ -328,7 +388,8 @@ func (e *Entity) getASNLists() { log.Tracef("intel: loading ASN list for %d", asn) e.loadAsnListOnce.Do(func() { - list, err := filterlists.LookupASNString(fmt.Sprintf("%d", asn)) + asnStr := fmt.Sprintf("%d", asn) + list, err := filterlists.LookupASNString(asnStr) if err != nil { log.Errorf("intel: failed to get ASN blocklist for %d: %s", asn, err) e.loadAsnListOnce = sync.Once{} @@ -336,7 +397,7 @@ func (e *Entity) getASNLists() { } e.asnListLoaded = true - e.mergeList(list) + e.mergeList(asnStr, list) }) } @@ -360,7 +421,7 @@ func (e *Entity) getCountryLists() { } e.countryListLoaded = true - e.mergeList(list) + e.mergeList(country, list) }) } @@ -393,28 +454,71 @@ func (e *Entity) getIPLists() { return } e.ipListLoaded = true - e.mergeList(list) + e.mergeList(ip.String(), list) }) } -// GetLists returns the filter list identifiers the entity matched and whether this data is set. -func (e *Entity) GetLists() ([]string, bool) { +// LoadLists searches all filterlists for all occurences of +// this entity. +func (e *Entity) LoadLists() bool { e.getLists() - if e.Lists == nil { - return nil, false + if e.ListOccurences == nil { + return false } - return e.Lists, true + return true } -// GetListsMap is like GetLists but returns a lookup map for list IDs. -func (e *Entity) GetListsMap() (filterlists.LookupMap, bool) { - e.getLists() +// MatchLists matches the entities lists against a slice +// of source IDs and updates various entity properties +// like BlockedByLists, ListOccurences and BlockedEntitites. +func (e *Entity) MatchLists(lists []string) bool { + e.BlockedByLists = nil + e.BlockedEntities = nil - if e.ListsMap == nil { - return nil, false + lm := makeMap(lists) + for key, keyLists := range e.ListOccurences { + for _, keyListID := range keyLists { + if _, ok := lm[keyListID]; ok { + e.BlockedByLists = append(e.BlockedByLists, keyListID) + e.BlockedEntities = append(e.BlockedEntities, key) + } + } } - return e.ListsMap, true + + makeDistinct(e.BlockedByLists) + + return len(e.BlockedByLists) > 0 +} + +// ListBlockReason returns the block reason for this entity. +func (e *Entity) ListBlockReason() ListBlockReason { + blockedBy := make([]ListMatch, len(e.BlockedEntities)) + + lm := makeMap(e.BlockedByLists) + + for idx, blockedEntity := range e.BlockedEntities { + if entityLists, ok := e.ListOccurences[blockedEntity]; ok { + var activeLists []string + var inactiveLists []string + + for _, l := range entityLists { + if _, ok := lm[l]; ok { + activeLists = append(activeLists, l) + } else { + inactiveLists = append(inactiveLists, l) + } + } + + blockedBy[idx] = ListMatch{ + Entity: blockedEntity, + ActiveLists: activeLists, + InactiveLists: inactiveLists, + } + } + } + + return blockedBy } func mergeStringList(a, b []string) []string { @@ -434,21 +538,26 @@ func mergeStringList(a, b []string) []string { return res } -func buildLookupMap(l []string) filterlists.LookupMap { - m := make(filterlists.LookupMap, len(l)) - - for _, s := range l { - m[s] = struct{}{} - } - - return m -} - func makeDistinct(slice []string) []string { - lm := buildLookupMap(slice) - result := make([]string, 0, len(lm)) - for key := range lm { - result = append(result, key) + m := make(map[string]struct{}, len(slice)) + var result []string + + for _, v := range slice { + if _, ok := m[v]; ok { + continue + } + + m[v] = struct{}{} + result = append(result, v) } + return result } + +func makeMap(slice []string) map[string]struct{} { + lm := make(map[string]struct{}) + for _, v := range slice { + lm[v] = struct{}{} + } + return lm +} diff --git a/intel/filterlists/lookup_map.go b/intel/filterlists/lookup_map.go deleted file mode 100644 index 5a161c91..00000000 --- a/intel/filterlists/lookup_map.go +++ /dev/null @@ -1,25 +0,0 @@ -package filterlists - -import "strings" - -// LookupMap is a helper type for matching a list of endpoint sources -// against a map. -type LookupMap map[string]struct{} - -// Match checks if a source in `list` is part of lm. -// Matches are joined to string and returned. -// If nothing is found, an empty string is returned. -func (lm LookupMap) Match(list []string) string { - matches := make([]string, 0, len(list)) - for _, l := range list { - if _, ok := lm[l]; ok { - matches = append(matches, l) - } - } - - if len(matches) == 0 { - return "" - } - - return strings.Join(matches, ", ") -} diff --git a/intel/filterlists/lookup_test.go b/intel/filterlists/lookup_test.go deleted file mode 100644 index 0a2e9220..00000000 --- a/intel/filterlists/lookup_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package filterlists - -/* - -func TestLookupASN(t *testing.T) { - lists, err := LookupASNString("123") - assert.NoError(t, err) - assert.Equal(t, []string{"TEST"}, lists) - - lists, err = LookupASNString("does-not-exist") - assert.NoError(t, err) - assert.Empty(t, lists) - - defer testMarkNotLoaded()() - lists, err = LookupASNString("123") - assert.NoError(t, err) - assert.Empty(t, lists) -} - -func TestLookupCountry(t *testing.T) { - lists, err := LookupCountry("AT") - assert.NoError(t, err) - assert.Equal(t, []string{"TEST"}, lists) - - lists, err = LookupCountry("does-not-exist") - assert.NoError(t, err) - assert.Empty(t, lists) - - defer testMarkNotLoaded()() - lists, err = LookupCountry("AT") - assert.NoError(t, err) - assert.Empty(t, lists) -} - -func TestLookupIP(t *testing.T) { - lists, err := LookupIP(net.IP{1, 1, 1, 1}) - assert.NoError(t, err) - assert.Equal(t, []string{"TEST"}, lists) - - lists, err = LookupIP(net.IP{127, 0, 0, 1}) - assert.NoError(t, err) - assert.Empty(t, lists) - - defer testMarkNotLoaded()() - lists, err = LookupIP(net.IP{1, 1, 1, 1}) - assert.NoError(t, err) - assert.Empty(t, lists) -} - -func TestLookupDomain(t *testing.T) { - lists, err := LookupDomain("example.com") - assert.NoError(t, err) - assert.Equal(t, []string{"TEST"}, lists) - - lists, err = LookupDomain("does-not-exist") - assert.NoError(t, err) - assert.Empty(t, lists) - - defer testMarkNotLoaded()() - lists, err = LookupDomain("example.com") - assert.NoError(t, err) - assert.Empty(t, lists) -} - -// testMarkNotLoaded ensures that functions believe -// filterlists are not yet loaded. It returns a -// func that restores the previous state. -func testMarkNotLoaded() func() { - if isLoaded() { - filterListsLoaded = make(chan struct{}) - return func() { - close(filterListsLoaded) - } - } - - return func() {} -} - -// testMarkLoaded is like testMarkNotLoaded but ensures -// isLoaded() return true. It returns a function to restore -// the previous state. -func testMarkLoaded() func() { - if !isLoaded() { - close(filterListsLoaded) - return func() { - filterListsLoaded = make(chan struct{}) - } - } - - return func() {} -} -*/ diff --git a/intel/lists.go b/intel/lists.go deleted file mode 100644 index 08ca0925..00000000 --- a/intel/lists.go +++ /dev/null @@ -1,40 +0,0 @@ -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 -} diff --git a/profile/endpoints/endpoint-lists.go b/profile/endpoints/endpoint-lists.go index fe151347..b11da060 100644 --- a/profile/endpoints/endpoint-lists.go +++ b/profile/endpoints/endpoint-lists.go @@ -10,21 +10,19 @@ import ( type EndpointLists struct { EndpointBase - ListSet *intel.ListSet + ListSet []string 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 + entity.LoadLists() + + if entity.MatchLists(ep.ListSet) { + return ep.matchesPPP(entity), entity.ListBlockReason().String() } + return NoMatch, "" } @@ -36,7 +34,7 @@ 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), + ListSet: lists, Lists: "L:" + strings.Join(lists, ","), Reason: "matched lists " + strings.Join(lists, ","), } diff --git a/profile/endpoints/endpoint.go b/profile/endpoints/endpoint.go index 687834cd..b634b911 100644 --- a/profile/endpoints/endpoint.go +++ b/profile/endpoints/endpoint.go @@ -11,7 +11,7 @@ import ( // Endpoint describes an Endpoint Matcher type Endpoint interface { - Matches(entity *intel.Entity) (result EPResult, reason string) + Matches(entity *intel.Entity) (EPResult, string) String() string } diff --git a/profile/profile-layered.go b/profile/profile-layered.go index 4ba2210b..edb8a8a2 100644 --- a/profile/profile-layered.go +++ b/profile/profile-layered.go @@ -243,27 +243,27 @@ func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPRe entity.ResolveSubDomainLists(lp.FilterSubDomains()) entity.EnableCNAMECheck(lp.FilterCNAMEs()) - lookupMap, hasLists := entity.GetListsMap() - if !hasLists { - return endpoints.NoMatch, "" - } - for _, layer := range lp.layers { - if reason := lookupMap.Match(layer.filterListIDs); reason != "" { - return endpoints.Denied, reason - } - - // only check the first layer that has filter list - // IDs defined. + // search for the first layer that has filterListIDs set if len(layer.filterListIDs) > 0 { + entity.LoadLists() + + if entity.MatchLists(layer.filterListIDs) { + return endpoints.Denied, entity.ListBlockReason().String() + } + return endpoints.NoMatch, "" } } cfgLock.RLock() defer cfgLock.RUnlock() - if reason := lookupMap.Match(cfgFilterLists); reason != "" { - return endpoints.Denied, reason + if len(cfgFilterLists) > 0 { + entity.LoadLists() + + if entity.MatchLists(cfgFilterLists) { + return endpoints.Denied, entity.ListBlockReason().String() + } } return endpoints.NoMatch, "" From 8c5526a69b74f19daeb78deb05c07c51aadf3696 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Mon, 20 Apr 2020 17:19:48 +0200 Subject: [PATCH 14/30] Add support for verdict and decision reason context --- firewall/inspection/inspection.go | 4 +- firewall/master.go | 12 ++--- intel/block_reason.go | 51 +++++++++++++++++++ intel/entity.go | 37 -------------- nameserver/nameserver.go | 4 +- network/connection.go | 72 ++++++++++++++++++--------- profile/endpoints/endpoint-any.go | 4 +- profile/endpoints/endpoint-asn.go | 21 ++++---- profile/endpoints/endpoint-country.go | 13 ++--- profile/endpoints/endpoint-domain.go | 30 +++++------ profile/endpoints/endpoint-ip.go | 15 +++--- profile/endpoints/endpoint-iprange.go | 14 +++--- profile/endpoints/endpoint-lists.go | 16 +++--- profile/endpoints/endpoint.go | 31 +++++++++++- profile/endpoints/endpoints.go | 10 +++- profile/endpoints/reason.go | 34 +++++++++++++ profile/profile-layered.go | 26 +++++----- 17 files changed, 246 insertions(+), 148 deletions(-) create mode 100644 intel/block_reason.go create mode 100644 profile/endpoints/reason.go diff --git a/firewall/inspection/inspection.go b/firewall/inspection/inspection.go index 55629b19..7dc59494 100644 --- a/firewall/inspection/inspection.go +++ b/firewall/inspection/inspection.go @@ -85,11 +85,11 @@ func RunInspectors(conn *network.Connection, pkt packet.Packet) (network.Verdict verdict = network.VerdictDrop continueInspection = true case BLOCK_CONN: - conn.SetVerdict(network.VerdictBlock) + conn.SetVerdict(network.VerdictBlock, "", nil) verdict = conn.Verdict activeInspectors[key] = true case DROP_CONN: - conn.SetVerdict(network.VerdictDrop) + conn.SetVerdict(network.VerdictDrop, "", nil) verdict = conn.Verdict activeInspectors[key] = true case STOP_INSPECTING: diff --git a/firewall/master.go b/firewall/master.go index 86196e36..cafd11f2 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -143,9 +143,6 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: } } - var result endpoints.EPResult - var reason string - if p.PreventBypassing() { // check for bypass protection result, reason := PreventBypassing(conn) @@ -160,6 +157,9 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: } } + var result endpoints.EPResult + var reason endpoints.Reason + // check endpoints list if conn.Inbound { result, reason = p.MatchServiceEndpoint(conn.Entity) @@ -168,10 +168,10 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: } switch result { case endpoints.Denied: - conn.Deny("endpoint is blacklisted: " + reason) // Block Outbound / Drop Inbound + conn.DenyWithContext(reason.String(), reason.Context()) return case endpoints.Permitted: - conn.Accept("endpoint is whitelisted: " + reason) + conn.AcceptWithContext(reason.String(), reason.Context()) return } // continuing with result == NoMatch @@ -180,7 +180,7 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: result, reason = p.MatchFilterLists(conn.Entity) switch result { case endpoints.Denied: - conn.Deny("endpoint in filterlists: " + reason) + conn.DenyWithContext(reason.String(), reason.Context()) return case endpoints.NoMatch: // nothing to do diff --git a/intel/block_reason.go b/intel/block_reason.go new file mode 100644 index 00000000..5a14c6cd --- /dev/null +++ b/intel/block_reason.go @@ -0,0 +1,51 @@ +package intel + +import ( + "fmt" + "strings" +) + +// ListMatch represents an entity that has been +// matched against filterlists. +type ListMatch struct { + Entity string + ActiveLists []string + InactiveLists []string +} + +func (lm *ListMatch) String() string { + inactive := "" + if len(lm.InactiveLists) > 0 { + inactive = " and in deactivated lists " + strings.Join(lm.InactiveLists, ", ") + } + return fmt.Sprintf( + "%s in activated lists %s%s", + lm.Entity, + strings.Join(lm.ActiveLists, ","), + inactive, + ) +} + +// ListBlockReason is a list of list matches. +type ListBlockReason []ListMatch + +func (br ListBlockReason) String() string { + if len(br) == 0 { + return "" + } + + matches := make([]string, len(br)) + for idx, lm := range br { + matches[idx] = lm.String() + } + + return strings.Join(matches, " and ") +} + +// Context returns br wrapped into a map. It implements +// the endpoints.Reason interface. +func (br ListBlockReason) Context() interface{} { + return map[string]interface{}{ + "filterlists": br, + } +} diff --git a/intel/entity.go b/intel/entity.go index e46ac041..8d2e21fb 100644 --- a/intel/entity.go +++ b/intel/entity.go @@ -16,43 +16,6 @@ import ( "golang.org/x/net/publicsuffix" ) -// ListMatch represents an entity that has been -// matched against filterlists. -type ListMatch struct { - Entity string - ActiveLists []string - InactiveLists []string -} - -func (lm *ListMatch) String() string { - inactive := "" - if len(lm.InactiveLists) > 0 { - inactive = " and in deactivated lists " + strings.Join(lm.InactiveLists, ", ") - } - return fmt.Sprintf( - "%s in activated lists %s%s", - lm.Entity, - strings.Join(lm.ActiveLists, ","), - inactive, - ) -} - -// ListBlockReason is a list of list matches. -type ListBlockReason []ListMatch - -func (br ListBlockReason) String() string { - if len(br) == 0 { - return "" - } - - matches := make([]string, len(br)) - for idx, lm := range br { - matches[idx] = lm.String() - } - - return strings.Join(matches, " and ") -} - // Entity describes a remote endpoint in many different ways. // It embeddes a sync.Mutex but none of the endpoints own // functions performs locking. The caller MUST ENSURE diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index a30d933c..9b3d8016 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -278,7 +278,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er result, reason := conn.Process().Profile().MatchEndpoint(conn.Entity) if result == endpoints.Denied { - conn.Block("endpoint in blocklist: " + reason) + conn.BlockWithContext(reason.String(), reason.Context()) returnNXDomain(w, query, conn.Reason) return nil } @@ -286,7 +286,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er if result == endpoints.NoMatch { result, reason = conn.Process().Profile().MatchFilterLists(conn.Entity) if result == endpoints.Denied { - conn.Block("endpoint in filterlists: " + reason) + conn.BlockWithContext(reason.String(), reason.Context()) returnNXDomain(w, query, conn.Reason) return nil } diff --git a/network/connection.go b/network/connection.go index e0af4c01..bbc088dc 100644 --- a/network/connection.go +++ b/network/connection.go @@ -31,9 +31,10 @@ type Connection struct { //nolint:maligned // TODO: fix alignment Entity *intel.Entity // needs locking, instance is never shared process *process.Process - Verdict Verdict - Reason string - ReasonID string // format source[:id[:id]] // TODO + Verdict Verdict + Reason string + ReasonContext interface{} + ReasonID string // format source[:id[:id]] // TODO Started int64 Ended int64 @@ -164,59 +165,82 @@ func GetConnection(id string) (*Connection, bool) { return conn, ok } -// Accept accepts the connection. -func (conn *Connection) Accept(reason string) { - if conn.SetVerdict(VerdictAccept) { - conn.Reason = reason +// AcceptWithContext accepts the connection. +func (conn *Connection) AcceptWithContext(reason string, ctx interface{}) { + if conn.SetVerdict(VerdictAccept, reason, ctx) { log.Infof("filter: granting connection %s, %s", conn, conn.Reason) } else { log.Warningf("filter: tried to accept %s, but current verdict is %s", conn, conn.Verdict) } } -// Block blocks the connection. -func (conn *Connection) Block(reason string) { - if conn.SetVerdict(VerdictBlock) { - conn.Reason = reason +// Accept is like AcceptWithContext but only accepts a reason. +func (conn *Connection) Accept(reason string) { + conn.AcceptWithContext(reason, nil) +} + +// BlockWithContext blocks the connection. +func (conn *Connection) BlockWithContext(reason string, ctx interface{}) { + if conn.SetVerdict(VerdictBlock, reason, ctx) { log.Infof("filter: blocking connection %s, %s", conn, conn.Reason) } else { log.Warningf("filter: tried to block %s, but current verdict is %s", conn, conn.Verdict) } } -// Drop drops the connection. -func (conn *Connection) Drop(reason string) { - if conn.SetVerdict(VerdictDrop) { - conn.Reason = reason +// Block is like BlockWithContext but does only accepts a reason. +func (conn *Connection) Block(reason string) { + conn.BlockWithContext(reason, nil) +} + +// DropWithContext drops the connection. +func (conn *Connection) DropWithContext(reason string, ctx interface{}) { + if conn.SetVerdict(VerdictDrop, reason, ctx) { log.Infof("filter: dropping connection %s, %s", conn, conn.Reason) } else { log.Warningf("filter: tried to drop %s, but current verdict is %s", conn, conn.Verdict) } } -// Deny blocks or drops the link depending on the connection direction. -func (conn *Connection) Deny(reason string) { +// Drop is like DropWithContext but does only accepts a reason. +func (conn *Connection) Drop(reason string) { + conn.DropWithContext(reason, nil) +} + +// DenyWithContext blocks or drops the link depending on the connection direction. +func (conn *Connection) DenyWithContext(reason string, ctx interface{}) { if conn.Inbound { - conn.Drop(reason) + conn.DropWithContext(reason, ctx) } else { - conn.Block(reason) + conn.BlockWithContext(reason, ctx) } } -// Failed marks the connection with VerdictFailed and stores the reason. -func (conn *Connection) Failed(reason string) { - if conn.SetVerdict(VerdictFailed) { - conn.Reason = reason +// Deny is like DenyWithContext but only accepts a reason. +func (conn *Connection) Deny(reason string) { + conn.DenyWithContext(reason, nil) +} + +// FailedWithContext marks the connection with VerdictFailed and stores the reason. +func (conn *Connection) FailedWithContext(reason string, ctx interface{}) { + if conn.SetVerdict(VerdictFailed, reason, ctx) { log.Infof("filter: dropping connection %s because of an internal error: %s", conn, reason) } else { log.Warningf("filter: tried to drop %s due to error but current verdict is %s", conn, conn.Verdict) } } +// Failed is like FailedWithContext but only accepts a string. +func (conn *Connection) Failed(reason string) { + conn.FailedWithContext(reason, nil) +} + // SetVerdict sets a new verdict for the connection, making sure it does not interfere with previous verdicts. -func (conn *Connection) SetVerdict(newVerdict Verdict) (ok bool) { +func (conn *Connection) SetVerdict(newVerdict Verdict, reason string, ctx interface{}) (ok bool) { if newVerdict >= conn.Verdict { conn.Verdict = newVerdict + conn.Reason = reason + conn.ReasonContext = ctx return true } return false diff --git a/profile/endpoints/endpoint-any.go b/profile/endpoints/endpoint-any.go index 34fd65b4..8e8deb98 100644 --- a/profile/endpoints/endpoint-any.go +++ b/profile/endpoints/endpoint-any.go @@ -8,8 +8,8 @@ type EndpointAny struct { } // 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) Matches(entity *intel.Entity) (EPResult, Reason) { + return ep.match(ep, entity, "*", "matches") } func (ep *EndpointAny) String() string { diff --git a/profile/endpoints/endpoint-asn.go b/profile/endpoints/endpoint-asn.go index dee94f2d..6713d199 100644 --- a/profile/endpoints/endpoint-asn.go +++ b/profile/endpoints/endpoint-asn.go @@ -16,24 +16,22 @@ var ( type EndpointASN struct { EndpointBase - ASN uint - Reason string + ASN uint } // 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, "" - } - +func (ep *EndpointASN) Matches(entity *intel.Entity) (EPResult, Reason) { asn, ok := entity.GetASN() if !ok { - return Undeterminable, "" + return Undeterminable, nil } + if asn == ep.ASN { - return ep.matchesPPP(entity), ep.Reason + asnStr := strconv.Itoa(int(ep.ASN)) + return ep.match(ep, entity, asnStr, "IP is part of AS") } - return NoMatch, "" + + return NoMatch, nil } func (ep *EndpointASN) String() string { @@ -48,8 +46,7 @@ func parseTypeASN(fields []string) (Endpoint, error) { } ep := &EndpointASN{ - ASN: uint(asn), - Reason: "IP is part of AS" + strconv.FormatInt(int64(asn), 10), + ASN: uint(asn), } return ep.parsePPP(ep, fields) } diff --git a/profile/endpoints/endpoint-country.go b/profile/endpoints/endpoint-country.go index 63d21bfc..85449cf5 100644 --- a/profile/endpoints/endpoint-country.go +++ b/profile/endpoints/endpoint-country.go @@ -19,19 +19,16 @@ type EndpointCountry struct { } // 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, "" - } - +func (ep *EndpointCountry) Matches(entity *intel.Entity) (EPResult, Reason) { country, ok := entity.GetCountry() if !ok { - return Undeterminable, "" + return Undeterminable, nil } + if country == ep.Country { - return ep.matchesPPP(entity), "IP is located in " + country + return ep.match(ep, entity, country, "IP is located in") } - return NoMatch, "" + return NoMatch, nil } func (ep *EndpointCountry) String() string { diff --git a/profile/endpoints/endpoint-domain.go b/profile/endpoints/endpoint-domain.go index 2691ba13..fbd0dcf9 100644 --- a/profile/endpoints/endpoint-domain.go +++ b/profile/endpoints/endpoint-domain.go @@ -28,47 +28,48 @@ type EndpointDomain struct { Domain string DomainZone string MatchType uint8 - Reason string } -func (ep *EndpointDomain) check(entity *intel.Entity, domain string) (EPResult, string) { +func (ep *EndpointDomain) check(entity *intel.Entity, domain string) (EPResult, Reason) { + result, reason := ep.match(ep, entity, ep.Domain, "domain matches") + switch ep.MatchType { case domainMatchTypeExact: if domain == ep.Domain { - return ep.matchesPPP(entity), ep.Reason + return result, reason } case domainMatchTypeZone: if domain == ep.Domain { - return ep.matchesPPP(entity), ep.Reason + return result, reason } if strings.HasSuffix(domain, ep.DomainZone) { - return ep.matchesPPP(entity), ep.Reason + return result, reason } case domainMatchTypeSuffix: if strings.HasSuffix(domain, ep.Domain) { - return ep.matchesPPP(entity), ep.Reason + return result, reason } case domainMatchTypePrefix: if strings.HasPrefix(domain, ep.Domain) { - return ep.matchesPPP(entity), ep.Reason + return result, reason } case domainMatchTypeContains: if strings.Contains(domain, ep.Domain) { - return ep.matchesPPP(entity), ep.Reason + return result, reason } } - return NoMatch, "" + return NoMatch, nil } // Matches checks whether the given entity matches this endpoint definition. -func (ep *EndpointDomain) Matches(entity *intel.Entity) (result EPResult, reason string) { +func (ep *EndpointDomain) Matches(entity *intel.Entity) (EPResult, Reason) { if entity.Domain == "" { - return NoMatch, "" + return NoMatch, nil } - result, reason = ep.check(entity, entity.Domain) + result, reason := ep.check(entity, entity.Domain) if result != NoMatch { - return + return result, reason } if entity.CNAMECheckEnabled() { @@ -80,7 +81,7 @@ func (ep *EndpointDomain) Matches(entity *intel.Entity) (result EPResult, reason } } - return NoMatch, "" + return NoMatch, nil } func (ep *EndpointDomain) String() string { @@ -93,7 +94,6 @@ func parseTypeDomain(fields []string) (Endpoint, error) { if domainRegex.MatchString(domain) || altDomainRegex.MatchString(domain) { ep := &EndpointDomain{ OriginalValue: domain, - Reason: "domain matches " + domain, } // fix domain ending diff --git a/profile/endpoints/endpoint-ip.go b/profile/endpoints/endpoint-ip.go index 6fa742c0..43ea47f7 100644 --- a/profile/endpoints/endpoint-ip.go +++ b/profile/endpoints/endpoint-ip.go @@ -10,19 +10,19 @@ import ( type EndpointIP struct { EndpointBase - IP net.IP - Reason string + IP net.IP } // Matches checks whether the given entity matches this endpoint definition. -func (ep *EndpointIP) Matches(entity *intel.Entity) (result EPResult, reason string) { +func (ep *EndpointIP) Matches(entity *intel.Entity) (EPResult, Reason) { if entity.IP == nil { - return Undeterminable, "" + return Undeterminable, nil } + if ep.IP.Equal(entity.IP) { - return ep.matchesPPP(entity), ep.Reason + return ep.match(ep, entity, ep.IP.String(), "IP matches") } - return NoMatch, "" + return NoMatch, nil } func (ep *EndpointIP) String() string { @@ -33,8 +33,7 @@ func parseTypeIP(fields []string) (Endpoint, error) { ip := net.ParseIP(fields[1]) if ip != nil { ep := &EndpointIP{ - IP: ip, - Reason: "IP is " + ip.String(), + IP: ip, } return ep.parsePPP(ep, fields) } diff --git a/profile/endpoints/endpoint-iprange.go b/profile/endpoints/endpoint-iprange.go index 16afa4bb..bc0d22fe 100644 --- a/profile/endpoints/endpoint-iprange.go +++ b/profile/endpoints/endpoint-iprange.go @@ -10,19 +10,18 @@ import ( type EndpointIPRange struct { EndpointBase - Net *net.IPNet - Reason string + Net *net.IPNet } // Matches checks whether the given entity matches this endpoint definition. -func (ep *EndpointIPRange) Matches(entity *intel.Entity) (result EPResult, reason string) { +func (ep *EndpointIPRange) Matches(entity *intel.Entity) (EPResult, Reason) { if entity.IP == nil { - return Undeterminable, "" + return Undeterminable, nil } if ep.Net.Contains(entity.IP) { - return ep.matchesPPP(entity), ep.Reason + return ep.match(ep, entity, ep.Net.String(), "IP is in") } - return NoMatch, "" + return NoMatch, nil } func (ep *EndpointIPRange) String() string { @@ -33,8 +32,7 @@ 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(), + Net: net, } return ep.parsePPP(ep, fields) } diff --git a/profile/endpoints/endpoint-lists.go b/profile/endpoints/endpoint-lists.go index b11da060..27ec8b00 100644 --- a/profile/endpoints/endpoint-lists.go +++ b/profile/endpoints/endpoint-lists.go @@ -12,18 +12,19 @@ type EndpointLists struct { ListSet []string 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) { - entity.LoadLists() - - if entity.MatchLists(ep.ListSet) { - return ep.matchesPPP(entity), entity.ListBlockReason().String() +func (ep *EndpointLists) Matches(entity *intel.Entity) (EPResult, Reason) { + if !entity.LoadLists() { + return Undeterminable, nil } - return NoMatch, "" + if entity.MatchLists(ep.ListSet) { + return ep.match(ep, entity, ep.Lists, "filterlist contains", "filterlist", entity.ListBlockReason()) + } + + return NoMatch, nil } func (ep *EndpointLists) String() string { @@ -36,7 +37,6 @@ func parseTypeList(fields []string) (Endpoint, error) { ep := &EndpointLists{ ListSet: lists, Lists: "L:" + strings.Join(lists, ","), - Reason: "matched lists " + strings.Join(lists, ","), } return ep.parsePPP(ep, fields) } diff --git a/profile/endpoints/endpoint.go b/profile/endpoints/endpoint.go index b634b911..43741c40 100644 --- a/profile/endpoints/endpoint.go +++ b/profile/endpoints/endpoint.go @@ -11,7 +11,7 @@ import ( // Endpoint describes an Endpoint Matcher type Endpoint interface { - Matches(entity *intel.Entity) (EPResult, string) + Matches(entity *intel.Entity) (EPResult, Reason) String() string } @@ -24,6 +24,35 @@ type EndpointBase struct { //nolint:maligned // TODO Permitted bool } +func (ep *EndpointBase) match(s fmt.Stringer, entity *intel.Entity, value, desc string, keval ...interface{}) (EPResult, Reason) { + result := ep.matchesPPP(entity) + if result == Undeterminable || result == NoMatch { + return result, nil + } + + return result, ep.makeReason(s, value, desc) +} + +func (ep *EndpointBase) makeReason(s fmt.Stringer, value, desc string, keyval ...interface{}) Reason { + r := &reason{ + description: desc, + Filter: ep.renderPPP(s.String()), + Permitted: ep.Permitted, + Value: value, + } + + r.Extra = make(map[string]interface{}) + + for idx := 0; idx < int(len(keyval)/2); idx += 2 { + key := keyval[idx] + val := keyval[idx+1] + + r.Extra[key.(string)] = val + } + + return r +} + func (ep *EndpointBase) matchesPPP(entity *intel.Entity) (result EPResult) { // only check if protocol is defined if ep.Protocol > 0 { diff --git a/profile/endpoints/endpoints.go b/profile/endpoints/endpoints.go index 7a49a0c8..d8af9423 100644 --- a/profile/endpoints/endpoints.go +++ b/profile/endpoints/endpoints.go @@ -21,6 +21,12 @@ const ( Permitted ) +// IsDecision returns true if result represents a decision +// and false if result is NoMatch or Undeterminable. +func IsDecision(result EPResult) bool { + return result == Denied || result == Permitted +} + // ParseEndpoints parses a list of endpoints and returns a list of Endpoints for matching. func ParseEndpoints(entries []string) (Endpoints, error) { var firstErr error @@ -57,7 +63,7 @@ func (e Endpoints) IsSet() bool { } // 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) { +func (e Endpoints) Match(entity *intel.Entity) (result EPResult, reason Reason) { for _, entry := range e { if entry != nil { if result, reason = entry.Matches(entity); result != NoMatch { @@ -66,7 +72,7 @@ func (e Endpoints) Match(entity *intel.Entity) (result EPResult, reason string) } } - return NoMatch, "" + return NoMatch, nil } func (e Endpoints) String() string { diff --git a/profile/endpoints/reason.go b/profile/endpoints/reason.go new file mode 100644 index 00000000..d137c3b3 --- /dev/null +++ b/profile/endpoints/reason.go @@ -0,0 +1,34 @@ +package endpoints + +// Reason describes the reason why an endpoint has been +// permitted or blocked. +type Reason interface { + // String should return a human readable string + // describing the decision reason. + String() string + + // Context returns the context that was used + // for the decision. + Context() interface{} +} + +type reason struct { + description string + Filter string + Value string + Permitted bool + Extra map[string]interface{} +} + +func (r *reason) String() string { + prefix := "endpoint in blocklist: " + if r.Permitted { + prefix = "endpoint in whitelist: " + } + + return prefix + r.description + " " + r.Value +} + +func (r *reason) Context() interface{} { + return r +} diff --git a/profile/profile-layered.go b/profile/profile-layered.go index edb8a8a2..45311662 100644 --- a/profile/profile-layered.go +++ b/profile/profile-layered.go @@ -204,12 +204,12 @@ func (lp *LayeredProfile) DefaultAction() uint8 { } // 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) { +func (lp *LayeredProfile) MatchEndpoint(entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) { for _, layer := range lp.layers { if layer.endpoints.IsSet() { - result, reason = layer.endpoints.Match(entity) - if result != endpoints.NoMatch { - return + result, reason := layer.endpoints.Match(entity) + if endpoints.IsDecision(result) { + return result, reason } } } @@ -220,14 +220,14 @@ func (lp *LayeredProfile) MatchEndpoint(entity *intel.Entity) (result endpoints. } // 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) { +func (lp *LayeredProfile) MatchServiceEndpoint(entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) { entity.EnableReverseResolving() for _, layer := range lp.layers { if layer.serviceEndpoints.IsSet() { - result, reason = layer.serviceEndpoints.Match(entity) - if result != endpoints.NoMatch { - return + result, reason := layer.serviceEndpoints.Match(entity) + if endpoints.IsDecision(result) { + return result, reason } } } @@ -239,7 +239,7 @@ func (lp *LayeredProfile) MatchServiceEndpoint(entity *intel.Entity) (result end // MatchFilterLists matches the entity against the set of filter // lists. -func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPResult, string) { +func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) { entity.ResolveSubDomainLists(lp.FilterSubDomains()) entity.EnableCNAMECheck(lp.FilterCNAMEs()) @@ -249,10 +249,10 @@ func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPRe entity.LoadLists() if entity.MatchLists(layer.filterListIDs) { - return endpoints.Denied, entity.ListBlockReason().String() + return endpoints.Denied, entity.ListBlockReason() } - return endpoints.NoMatch, "" + return endpoints.NoMatch, nil } } @@ -262,11 +262,11 @@ func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPRe entity.LoadLists() if entity.MatchLists(cfgFilterLists) { - return endpoints.Denied, entity.ListBlockReason().String() + return endpoints.Denied, entity.ListBlockReason() } } - return endpoints.NoMatch, "" + return endpoints.NoMatch, nil } // AddEndpoint adds an endpoint to the local endpoint list, saves the local profile and reloads the configuration. From f2e41a0d32d2f9319cf82945c6ba732ba40d10e6 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 09:55:49 +0200 Subject: [PATCH 15/30] Refactoring of FilterDNSResponse and DecideOnConnection --- firewall/dns.go | 230 +++++++++++++++++++++ firewall/master.go | 367 +++++++++++++++------------------ intel/block_reason.go | 52 ++++- intel/entity.go | 16 +- nameserver/nameserver.go | 132 +++--------- profile/endpoints/endpoint.go | 6 +- profile/endpoints/endpoints.go | 2 +- resolver/ipinfo.go | 6 +- 8 files changed, 480 insertions(+), 331 deletions(-) create mode 100644 firewall/dns.go diff --git a/firewall/dns.go b/firewall/dns.go new file mode 100644 index 00000000..d6421e2e --- /dev/null +++ b/firewall/dns.go @@ -0,0 +1,230 @@ +package firewall + +import ( + "net" + "os" + "strings" + + "github.com/miekg/dns" + "github.com/safing/portbase/database" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/network" + "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/resolver" +) + +func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ([]dns.RR, []string, int) { + goodEntries := make([]dns.RR, 0, len(entries)) + filteredRecords := make([]string, 0, len(entries)) + + // keeps track of the number of valid and allowed + // A and AAAA records. + var allowedAddressRecords int + + for _, rr := range entries { + // get IP and classification + var ip net.IP + switch v := rr.(type) { + case *dns.A: + ip = v.A + case *dns.AAAA: + ip = v.AAAA + default: + // add non A/AAAA entries + goodEntries = append(goodEntries, rr) + continue + } + classification := netutils.ClassifyIP(ip) + + if p.RemoveOutOfScopeDNS() { + switch { + case classification == netutils.HostLocal: + // No DNS should return localhost addresses + filteredRecords = append(filteredRecords, rr.String()) + continue + case scope == netutils.Global && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): + // No global DNS should return LAN addresses + filteredRecords = append(filteredRecords, rr.String()) + continue + } + } + + if p.RemoveBlockedDNS() { + // filter by flags + switch { + case p.BlockScopeInternet() && classification == netutils.Global: + filteredRecords = append(filteredRecords, rr.String()) + continue + case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): + filteredRecords = append(filteredRecords, rr.String()) + continue + case p.BlockScopeLocal() && classification == netutils.HostLocal: + filteredRecords = append(filteredRecords, rr.String()) + continue + } + + // TODO: filter by endpoint list (IP only) + } + + // if survived, add to good entries + allowedAddressRecords++ + goodEntries = append(goodEntries, rr) + } + + return goodEntries, filteredRecords, allowedAddressRecords +} + +func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *resolver.RRCache { + // do not modify own queries + if conn.Process().Pid == os.Getpid() { + return rrCache + } + + // get profile + p := conn.Process().Profile() + if p == nil { + conn.Block("no profile") + return nil + } + + // check if DNS response filtering is completely turned off + if !p.RemoveOutOfScopeDNS() && !p.RemoveBlockedDNS() { + return rrCache + } + + // duplicate entry + rrCache = rrCache.ShallowCopy() + rrCache.FilteredEntries = make([]string, 0) + + var filteredRecords []string + var validIPs int + + rrCache.Answer, filteredRecords, validIPs = filterDNSSection(rrCache.Answer, p, rrCache.ServerScope) + rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...) + + // FIXME(ppacher): should we consider valid IPs from the extra section? + rrCache.Extra, filteredRecords, _ = filterDNSSection(rrCache.Extra, p, rrCache.ServerScope) + rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...) + + if len(rrCache.FilteredEntries) > 0 { + rrCache.Filtered = true + if validIPs == 0 { + conn.Block("no addresses returned for this domain are permitted") + return nil + } + + log.Infof("filter: filtered DNS replies for %s: %s", conn, strings.Join(rrCache.FilteredEntries, ", ")) + } + + return rrCache +} + +// DecideOnResolvedDNS filters a dns response according to the application profile and settings. +func DecideOnResolvedDNS(conn *network.Connection, q *resolver.Query, rrCache *resolver.RRCache) *resolver.RRCache { + updatedRR := filterDNSResponse(conn, rrCache) + if updatedRR == nil { + return nil + } + + updateIPsAndCNAMEs(q, rrCache, conn) + + if mayBlockCNAMEs(conn) { + return nil + } + + // TODO: Gate17 integration + // tunnelInfo, err := AssignTunnelIP(fqdn) + + return updatedRR +} + +func mayBlockCNAMEs(conn *network.Connection) bool { + // if we have CNAMEs and the profile is configured to filter them + // we need to re-check the lists and endpoints here + if conn.Process().Profile().FilterCNAMEs() { + conn.Entity.ResetLists() + conn.Entity.EnableCNAMECheck(true) + + result, reason := conn.Process().Profile().MatchEndpoint(conn.Entity) + if result == endpoints.Denied { + conn.BlockWithContext(reason.String(), reason.Context()) + return true + } + + if result == endpoints.NoMatch { + result, reason = conn.Process().Profile().MatchFilterLists(conn.Entity) + if result == endpoints.Denied { + conn.BlockWithContext(reason.String(), reason.Context()) + return true + } + } + } + + return false +} + +func updateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *network.Connection) { + // save IP addresses to IPInfo + cnames := make(map[string]string) + ips := make(map[string]struct{}) + + for _, rr := range append(rrCache.Answer, rrCache.Extra...) { + switch v := rr.(type) { + case *dns.CNAME: + cnames[v.Hdr.Name] = v.Target + + case *dns.A: + ips[v.A.String()] = struct{}{} + + case *dns.AAAA: + ips[v.AAAA.String()] = struct{}{} + } + } + + for ip := range ips { + record := resolver.ResolvedDomain{ + Domain: q.FQDN, + } + + // resolve all CNAMEs in the correct order. + var domain = q.FQDN + for { + nextDomain, isCNAME := cnames[domain] + if !isCNAME { + break + } + + record.CNAMEs = append(record.CNAMEs, nextDomain) + domain = nextDomain + } + + // update the entity to include the cnames + conn.Entity.CNAME = record.CNAMEs + + // get the existing IP info or create a new one + var save bool + info, err := resolver.GetIPInfo(ip) + if err != nil { + if err != database.ErrNotFound { + log.Errorf("nameserver: failed to search for IP info record: %s", err) + } + + info = &resolver.IPInfo{ + IP: ip, + } + save = true + } + + // and the new resolved domain record and save + if new := info.AddDomain(record); new { + save = true + } + if save { + if err := info.Save(); err != nil { + log.Errorf("nameserver: failed to save IP info record: %s", err) + } + } + } +} diff --git a/firewall/master.go b/firewall/master.go index cafd11f2..06a0f5f1 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -2,7 +2,6 @@ package firewall import ( "fmt" - "net" "os" "path/filepath" "strings" @@ -14,10 +13,8 @@ import ( "github.com/safing/portmaster/process" "github.com/safing/portmaster/profile" "github.com/safing/portmaster/profile/endpoints" - "github.com/safing/portmaster/resolver" "github.com/agext/levenshtein" - "github.com/miekg/dns" ) // Call order: @@ -26,7 +23,7 @@ import ( // 1. DecideOnConnection // is called when a DNS query is made, may set verdict to Undeterminable to permit a DNS reply. // is called with a nil packet. -// 2. FilterDNSResponse +// 2. DecideOnResolvedDNS // is called to (possibly) filter out A/AAAA records that the filter would deny later. // // Network Connection: @@ -35,7 +32,7 @@ import ( // DecideOnConnection makes a decision about a connection. // When called, the connection and profile is already locked. -func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit,gocyclo // TODO +func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { // update profiles and check if communication needs reevaluation if conn.UpdateAndCheck() { log.Infof("filter: re-evaluating verdict on %s", conn) @@ -46,14 +43,47 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: } } + var deciders = []func(*network.Connection, packet.Packet) bool{ + checkPortmasterConnection, + checkSelfCommunication, + checkProfileExists, + checkConnectionType, + checkConnectionScope, + checkEndpointLists, + checkBypassPrevention, + checkFilterLists, + checkInbound, + checkDefaultPermit, + checkAutoPermitRelated, + checkDefaultAction, + } + + for _, decider := range deciders { + if decider(conn, pkt) { + return + } + } + + // DefaultAction == DefaultActionBlock + conn.Deny("endpoint is not whitelisted (default=block)") +} + +// checkPortmasterConnection allows all connection that originate from +// portmaster itself. +func checkPortmasterConnection(conn *network.Connection, _ packet.Packet) bool { // grant self if conn.Process().Pid == os.Getpid() { log.Infof("filter: granting own connection %s", conn) conn.Verdict = network.VerdictAccept conn.Internal = true - return + return true } + return false +} + +// checkSelfCommunication checks if the process is communicating with itself. +func checkSelfCommunication(conn *network.Connection, pkt packet.Packet) bool { // check if process is communicating with itself if pkt != nil { // TODO: evaluate the case where different IPs in the 127/8 net are used. @@ -77,89 +107,30 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: } else if otherProcess.Pid == conn.Process().Pid { conn.Accept("connection to self") conn.Internal = true - return + return true } } } } - // get profile - p := conn.Process().Profile() - if p == nil { - conn.Block("no profile") - return - } - - // check conn type - switch conn.Scope { - case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid: - if p.BlockInbound() { - if conn.Scope == network.IncomingHost { - conn.Block("inbound connections blocked") - } else { - conn.Drop("inbound connections blocked") - } - return - } - case network.PeerLAN, network.PeerInternet, network.PeerInvalid: - // Important: PeerHost is and should be missing! - if p.BlockP2P() { - conn.Block("direct connections (P2P) blocked") - return - } - } - - // check scopes - if conn.Entity.IP != nil { - classification := netutils.ClassifyIP(conn.Entity.IP) - - switch classification { - case netutils.Global, netutils.GlobalMulticast: - if p.BlockScopeInternet() { - conn.Deny("Internet access blocked") // Block Outbound / Drop Inbound - return - } - case netutils.SiteLocal, netutils.LinkLocal, netutils.LocalMulticast: - if p.BlockScopeLAN() { - conn.Block("LAN access blocked") // Block Outbound / Drop Inbound - return - } - case netutils.HostLocal: - if p.BlockScopeLocal() { - conn.Block("Localhost access blocked") // Block Outbound / Drop Inbound - return - } - default: // netutils.Invalid - conn.Deny("invalid IP") // Block Outbound / Drop Inbound - return - } - } else if conn.Entity.Domain != "" { - // DNS Query - // DNS is expected to resolve to LAN or Internet addresses - // TODO: handle domains mapped to localhost - if p.BlockScopeInternet() && p.BlockScopeLAN() { - conn.Block("Internet and LAN access blocked") - return - } - } - - if p.PreventBypassing() { - // check for bypass protection - result, reason := PreventBypassing(conn) - switch result { - case endpoints.Denied: - conn.Block("bypass prevention: " + reason) - return - case endpoints.Permitted: - conn.Accept("bypass prevention: " + reason) - return - case endpoints.NoMatch: - } + return false +} + +func checkProfileExists(conn *network.Connection, _ packet.Packet) bool { + if conn.Process().Profile() == nil { + conn.Block("unknown process or profile") + return true } + return false +} +func checkEndpointLists(conn *network.Connection, _ packet.Packet) bool { var result endpoints.EPResult var reason endpoints.Reason + // there must always be a profile. + p := conn.Process().Profile() + // check endpoints list if conn.Inbound { result, reason = p.MatchServiceEndpoint(conn.Entity) @@ -169,167 +140,151 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: switch result { case endpoints.Denied: conn.DenyWithContext(reason.String(), reason.Context()) - return + return true case endpoints.Permitted: conn.AcceptWithContext(reason.String(), reason.Context()) - return + return true } - // continuing with result == NoMatch + return false +} + +func checkConnectionType(conn *network.Connection, _ packet.Packet) bool { + p := conn.Process().Profile() + + // check conn type + switch conn.Scope { + case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid: + if p.BlockInbound() { + if conn.Scope == network.IncomingHost { + conn.Block("inbound connections blocked") + } else { + conn.Drop("inbound connections blocked") + } + return true + } + case network.PeerLAN, network.PeerInternet, network.PeerInvalid: + // Important: PeerHost is and should be missing! + if p.BlockP2P() { + conn.Block("direct connections (P2P) blocked") + return true + } + } + + return false +} + +func checkConnectionScope(conn *network.Connection, _ packet.Packet) bool { + p := conn.Process().Profile() + + // check scopes + if conn.Entity.IP != nil { + classification := netutils.ClassifyIP(conn.Entity.IP) + + switch classification { + case netutils.Global, netutils.GlobalMulticast: + if p.BlockScopeInternet() { + conn.Deny("Internet access blocked") // Block Outbound / Drop Inbound + return true + } + case netutils.SiteLocal, netutils.LinkLocal, netutils.LocalMulticast: + if p.BlockScopeLAN() { + conn.Block("LAN access blocked") // Block Outbound / Drop Inbound + return true + } + case netutils.HostLocal: + if p.BlockScopeLocal() { + conn.Block("Localhost access blocked") // Block Outbound / Drop Inbound + return true + } + default: // netutils.Invalid + conn.Deny("invalid IP") // Block Outbound / Drop Inbound + return true + } + } else if conn.Entity.Domain != "" { + // DNS Query + // DNS is expected to resolve to LAN or Internet addresses + // TODO: handle domains mapped to localhost + if p.BlockScopeInternet() && p.BlockScopeLAN() { + conn.Block("Internet and LAN access blocked") + return true + } + } + return false +} + +func checkBypassPrevention(conn *network.Connection, _ packet.Packet) bool { + if conn.Process().Profile().PreventBypassing() { + // check for bypass protection + result, reason := PreventBypassing(conn) + switch result { + case endpoints.Denied: + conn.Block("bypass prevention: " + reason) + return true + case endpoints.Permitted: + conn.Accept("bypass prevention: " + reason) + return true + case endpoints.NoMatch: + } + } + return false +} + +func checkFilterLists(conn *network.Connection, _ packet.Packet) bool { // apply privacy filter lists - result, reason = p.MatchFilterLists(conn.Entity) + p := conn.Process().Profile() + + result, reason := p.MatchFilterLists(conn.Entity) switch result { case endpoints.Denied: conn.DenyWithContext(reason.String(), reason.Context()) - return + return true case endpoints.NoMatch: // nothing to do default: log.Debugf("filter: filter lists returned unsupported verdict: %s", result) } + return false +} +func checkInbound(conn *network.Connection, _ packet.Packet) bool { // implicit default=block for inbound if conn.Inbound { conn.Drop("endpoint is not whitelisted (incoming is always default=block)") - return + return true } + return false +} +func checkDefaultPermit(conn *network.Connection, _ packet.Packet) bool { // check default action + p := conn.Process().Profile() if p.DefaultAction() == profile.DefaultActionPermit { conn.Accept("endpoint is not blacklisted (default=permit)") - return + return true } + return false +} - // check relation +func checkAutoPermitRelated(conn *network.Connection, _ packet.Packet) bool { + p := conn.Process().Profile() if !p.DisableAutoPermit() { related, reason := checkRelation(conn) if related { conn.Accept(reason) - return + return true } } - - // prompt - if p.DefaultAction() == profile.DefaultActionAsk { - prompt(conn, pkt) - return - } - - // DefaultAction == DefaultActionBlock - conn.Deny("endpoint is not whitelisted (default=block)") + return false } -// FilterDNSResponse filters a dns response according to the application profile and settings. -func FilterDNSResponse(conn *network.Connection, q *resolver.Query, rrCache *resolver.RRCache) *resolver.RRCache { //nolint:gocognit // TODO - // do not modify own queries - if conn.Process().Pid == os.Getpid() { - return rrCache - } - - // get profile +func checkDefaultAction(conn *network.Connection, pkt packet.Packet) bool { p := conn.Process().Profile() - if p == nil { - conn.Block("no profile") - return nil + if p.DefaultAction() == profile.DefaultActionAsk { + prompt(conn, pkt) + return true } - - // check if DNS response filtering is completely turned off - if !p.RemoveOutOfScopeDNS() && !p.RemoveBlockedDNS() { - return rrCache - } - - // duplicate entry - rrCache = rrCache.ShallowCopy() - rrCache.FilteredEntries = make([]string, 0) - - // change information - var addressesRemoved int - var addressesOk int - - // loop vars - var classification int8 - var ip net.IP - - // filter function - filterEntries := func(entries []dns.RR) (goodEntries []dns.RR) { - goodEntries = make([]dns.RR, 0, len(entries)) - - for _, rr := range entries { - - // get IP and classification - switch v := rr.(type) { - case *dns.A: - ip = v.A - case *dns.AAAA: - ip = v.AAAA - default: - // add non A/AAAA entries - goodEntries = append(goodEntries, rr) - continue - } - classification = netutils.ClassifyIP(ip) - - if p.RemoveOutOfScopeDNS() { - switch { - case classification == netutils.HostLocal: - // No DNS should return localhost addresses - addressesRemoved++ - rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) - continue - case rrCache.ServerScope == netutils.Global && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): - // No global DNS should return LAN addresses - addressesRemoved++ - rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) - continue - } - } - - if p.RemoveBlockedDNS() { - // filter by flags - switch { - case p.BlockScopeInternet() && classification == netutils.Global: - addressesRemoved++ - rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) - continue - case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): - addressesRemoved++ - rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) - continue - case p.BlockScopeLocal() && classification == netutils.HostLocal: - addressesRemoved++ - rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) - continue - } - - // TODO: filter by endpoint list (IP only) - } - - // if survived, add to good entries - addressesOk++ - goodEntries = append(goodEntries, rr) - } - return - } - - rrCache.Answer = filterEntries(rrCache.Answer) - rrCache.Extra = filterEntries(rrCache.Extra) - - if addressesRemoved > 0 { - rrCache.Filtered = true - if addressesOk == 0 { - conn.Block("no addresses returned for this domain are permitted") - return nil - } - } - - if rrCache.Filtered { - log.Infof("filter: filtered DNS replies for %s: %s", conn, strings.Join(rrCache.FilteredEntries, ", ")) - } - - // TODO: Gate17 integration - // tunnelInfo, err := AssignTunnelIP(fqdn) - - return rrCache + return false } // 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. diff --git a/intel/block_reason.go b/intel/block_reason.go index 5a14c6cd..040d6c01 100644 --- a/intel/block_reason.go +++ b/intel/block_reason.go @@ -1,8 +1,12 @@ package intel import ( + "encoding/json" "fmt" "strings" + + "github.com/miekg/dns" + "github.com/safing/portbase/log" ) // ListMatch represents an entity that has been @@ -45,7 +49,49 @@ func (br ListBlockReason) String() string { // Context returns br wrapped into a map. It implements // the endpoints.Reason interface. func (br ListBlockReason) Context() interface{} { - return map[string]interface{}{ - "filterlists": br, - } + return br +} + +// MarshalJSON marshals the list block reason into a map +// prefixed with filterlists. +func (br ListBlockReason) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + // we convert to []ListMatch to avoid recursing + // here. + "filterlists": []ListMatch(br), + }) +} + +// ToRRs returns a set of dns TXT records that describe the +// block reason. +func (br ListBlockReason) ToRRs() []dns.RR { + rrs := make([]dns.RR, 0, len(br)) + + for _, lm := range br { + blockedBy, err := dns.NewRR(fmt.Sprintf( + "%s-blockedBy. 0 IN TXT %q", + strings.TrimRight(lm.Entity, "."), + strings.Join(lm.ActiveLists, ","), + )) + if err == nil { + rrs = append(rrs, blockedBy) + } else { + log.Errorf("intel: failed to create TXT RR for block reason: %s", err) + } + + if len(lm.InactiveLists) > 0 { + wouldBeBlockedBy, err := dns.NewRR(fmt.Sprintf( + "%s-wouldBeBlockedBy. 0 IN TXT %q", + strings.TrimRight(lm.Entity, "."), + strings.Join(lm.ActiveLists, ","), + )) + if err == nil { + rrs = append(rrs, wouldBeBlockedBy) + } else { + log.Errorf("intel: failed to create TXT RR for block reason: %s", err) + } + } + } + + return rrs } diff --git a/intel/entity.go b/intel/entity.go index 8d2e21fb..af96343d 100644 --- a/intel/entity.go +++ b/intel/entity.go @@ -101,10 +101,10 @@ func (e *Entity) ResetLists() { // list right now so we could be more efficient by keeping // the other lists around. - // FIXME - //e.Lists = nil - //e.ListsMap = nil + e.BlockedByLists = nil + e.BlockedEntities = nil e.ListOccurences = nil + e.domainListLoaded = false e.ipListLoaded = false e.countryListLoaded = false @@ -421,15 +421,12 @@ func (e *Entity) getIPLists() { }) } -// LoadLists searches all filterlists for all occurences of +// LoadLists searches all filterlists for all occurrences of // this entity. func (e *Entity) LoadLists() bool { e.getLists() - if e.ListOccurences == nil { - return false - } - return true + return e.ListOccurences != nil } // MatchLists matches the entities lists against a slice @@ -450,6 +447,7 @@ func (e *Entity) MatchLists(lists []string) bool { } makeDistinct(e.BlockedByLists) + makeDistinct(e.BlockedEntities) return len(e.BlockedByLists) > 0 } @@ -503,7 +501,7 @@ func mergeStringList(a, b []string) []string { func makeDistinct(slice []string) []string { m := make(map[string]struct{}, len(slice)) - var result []string + result := make([]string, 0, len(slice)) for _, v := range slice { if _, ok := m[v]; ok { diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 9b3d8016..03d71701 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -7,7 +7,6 @@ import ( "net" "strings" - "github.com/safing/portbase/database" "github.com/safing/portbase/modules/subsystems" "github.com/safing/portbase/log" @@ -17,7 +16,6 @@ import ( "github.com/safing/portmaster/netenv" "github.com/safing/portmaster/network" "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/profile/endpoints" "github.com/safing/portmaster/resolver" "github.com/miekg/dns" @@ -91,12 +89,27 @@ func stop() error { return nil } -func returnNXDomain(w dns.ResponseWriter, query *dns.Msg, reason string) { +func returnNXDomain(w dns.ResponseWriter, query *dns.Msg, reason string, reasonContext interface{}) { m := new(dns.Msg) m.SetRcode(query, dns.RcodeNameError) - rr, _ := dns.NewRR("portmaster.block.reason. 0 IN TXT " + fmt.Sprintf("%q", reason)) + rr, _ := dns.NewRR("portmaster.block-reason. 0 IN TXT " + fmt.Sprintf("%q", reason)) m.Extra = []dns.RR{rr} - _ = w.WriteMsg(m) + + if reasonContext != nil { + if v, ok := reasonContext.(interface { + ToRRs() []dns.RR + }); ok { + m.Extra = append(m.Extra, v.ToRRs()...) + } else if v, ok := reasonContext.(interface { + ToRR() dns.RR + }); ok { + m.Extra = append(m.Extra, v.ToRR()) + } + } + + if err := w.WriteMsg(m); err != nil { + log.Errorf("nameserver: failed to send response: %s", err) + } } func returnServerFailure(w dns.ResponseWriter, query *dns.Msg) { @@ -132,7 +145,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er if question.Qclass != dns.ClassINET { // we only serve IN records, return nxdomain log.Warningf("nameserver: only IN record requests are supported but received Qclass %d, returning NXDOMAIN", question.Qclass) - returnNXDomain(w, query, "wrong type") + returnNXDomain(w, query, "wrong type", nil) return nil } @@ -172,7 +185,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // check if valid domain name if !netutils.IsValidFqdn(q.FQDN) { log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", q.FQDN) - returnNXDomain(w, query, "invalid domain") + returnNXDomain(w, query, "invalid domain", nil) return nil } @@ -208,10 +221,10 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // TODO: this has been obsoleted due to special profiles if conn.Process().Profile() == nil { tracer.Infof("nameserver: failed to find process for request %s, returning NXDOMAIN", conn) - returnNXDomain(w, query, "unknown process") // NOTE(ppacher): saving unknown process connection might end up in a lot of // processes. Consider disabling that via config. conn.Failed("Unknown process") + returnNXDomain(w, query, "unknown process", conn.ReasonContext) return nil } @@ -224,8 +237,8 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // log.Tracef("nameserver: domain %s has lms score of %f", fqdn, lms) if lms < 10 { tracer.Warningf("nameserver: possible data tunnel by %s: %s has lms score of %f, returning nxdomain", conn.Process(), q.FQDN, lms) - returnNXDomain(w, query, "lms") conn.Block("Possible data tunnel") + returnNXDomain(w, query, "lms", conn.ReasonContext) return nil } @@ -235,7 +248,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er switch conn.Verdict { case network.VerdictBlock: tracer.Infof("nameserver: %s blocked, returning nxdomain", conn) - returnNXDomain(w, query, conn.Reason) + returnNXDomain(w, query, conn.Reason, conn.ReasonContext) return nil case network.VerdictDrop, network.VerdictFailed: tracer.Infof("nameserver: %s dropped, not replying", conn) @@ -254,45 +267,16 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er conn.Failed("failed to resolve: " + err.Error()) } - returnNXDomain(w, query, conn.Reason) + returnNXDomain(w, query, conn.Reason, conn.ReasonContext) return nil } - // filter DNS response - rrCache = firewall.FilterDNSResponse(conn, q, rrCache) - // TODO: FilterDNSResponse also sets a connection verdict + rrCache = firewall.DecideOnResolvedDNS(conn, q, rrCache) if rrCache == nil { - tracer.Infof("nameserver: %s implicitly denied by filtering the dns response, returning nxdomain", conn) - returnNXDomain(w, query, conn.Reason) - conn.Block("DNS response filtered") + returnNXDomain(w, query, conn.Reason, conn.ReasonContext) return nil } - updateIPsAndCNAMEs(q, rrCache, conn) - - // if we have CNAMEs and the profile is configured to filter them - // we need to re-check the lists and endpoints here - if conn.Process().Profile().FilterCNAMEs() { - conn.Entity.ResetLists() - conn.Entity.EnableCNAMECheck(true) - - result, reason := conn.Process().Profile().MatchEndpoint(conn.Entity) - if result == endpoints.Denied { - conn.BlockWithContext(reason.String(), reason.Context()) - returnNXDomain(w, query, conn.Reason) - return nil - } - - if result == endpoints.NoMatch { - result, reason = conn.Process().Profile().MatchFilterLists(conn.Entity) - if result == endpoints.Denied { - conn.BlockWithContext(reason.String(), reason.Context()) - returnNXDomain(w, query, conn.Reason) - return nil - } - } - } - // reply to query m := new(dns.Msg) m.SetReply(query) @@ -311,67 +295,3 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er return nil } - -func updateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *network.Connection) { - // save IP addresses to IPInfo - cnames := make(map[string]string) - ips := make(map[string]struct{}) - - for _, rr := range append(rrCache.Answer, rrCache.Extra...) { - switch v := rr.(type) { - case *dns.CNAME: - cnames[v.Hdr.Name] = v.Target - - case *dns.A: - ips[v.A.String()] = struct{}{} - - case *dns.AAAA: - ips[v.AAAA.String()] = struct{}{} - } - } - - for ip := range ips { - record := resolver.ResolvedDomain{ - Domain: q.FQDN, - } - - // resolve all CNAMEs in the correct order. - var domain = q.FQDN - for { - nextDomain, isCNAME := cnames[domain] - if !isCNAME { - break - } - - record.CNAMEs = append(record.CNAMEs, nextDomain) - domain = nextDomain - } - - // update the entity to include the cnames - conn.Entity.CNAME = record.CNAMEs - - // get the existing IP info or create a new one - var save bool - info, err := resolver.GetIPInfo(ip) - if err != nil { - if err != database.ErrNotFound { - log.Errorf("nameserver: failed to search for IP info record: %s", err) - } - - info = &resolver.IPInfo{ - IP: ip, - } - save = true - } - - // and the new resolved domain record and save - if new := info.AddDomain(record); new { - save = true - } - if save { - if err := info.Save(); err != nil { - log.Errorf("nameserver: failed to save IP info record: %s", err) - } - } - } -} diff --git a/profile/endpoints/endpoint.go b/profile/endpoints/endpoint.go index 43741c40..76847ac7 100644 --- a/profile/endpoints/endpoint.go +++ b/profile/endpoints/endpoint.go @@ -24,13 +24,13 @@ type EndpointBase struct { //nolint:maligned // TODO Permitted bool } -func (ep *EndpointBase) match(s fmt.Stringer, entity *intel.Entity, value, desc string, keval ...interface{}) (EPResult, Reason) { +func (ep *EndpointBase) match(s fmt.Stringer, entity *intel.Entity, value, desc string, keyval ...interface{}) (EPResult, Reason) { result := ep.matchesPPP(entity) if result == Undeterminable || result == NoMatch { return result, nil } - return result, ep.makeReason(s, value, desc) + return result, ep.makeReason(s, value, desc, keyval...) } func (ep *EndpointBase) makeReason(s fmt.Stringer, value, desc string, keyval ...interface{}) Reason { @@ -43,7 +43,7 @@ func (ep *EndpointBase) makeReason(s fmt.Stringer, value, desc string, keyval .. r.Extra = make(map[string]interface{}) - for idx := 0; idx < int(len(keyval)/2); idx += 2 { + for idx := 0; idx < len(keyval)/2; idx += 2 { key := keyval[idx] val := keyval[idx+1] diff --git a/profile/endpoints/endpoints.go b/profile/endpoints/endpoints.go index d8af9423..f74edd24 100644 --- a/profile/endpoints/endpoints.go +++ b/profile/endpoints/endpoints.go @@ -24,7 +24,7 @@ const ( // IsDecision returns true if result represents a decision // and false if result is NoMatch or Undeterminable. func IsDecision(result EPResult) bool { - return result == Denied || result == Permitted + return result == Denied || result == Permitted || result == Undeterminable } // ParseEndpoints parses a list of endpoints and returns a list of Endpoints for matching. diff --git a/resolver/ipinfo.go b/resolver/ipinfo.go index 03c0b59b..0ecf9766 100644 --- a/resolver/ipinfo.go +++ b/resolver/ipinfo.go @@ -47,9 +47,9 @@ type ResolvedDomains []ResolvedDomain // String returns a string representation of all domains joined // to a single string. func (rds ResolvedDomains) String() string { - var domains []string - for _, n := range rds { - domains = append(domains, n.String()) + domains := make([]string, len(rds)) + for idx, n := range rds { + domains[idx] = n.String() } return strings.Join(domains, " or ") } From a07c36159be8d058c0f85eec2375d546ae01d3d7 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 09:59:36 +0200 Subject: [PATCH 16/30] Minor bug fix --- firewall/dns.go | 2 +- intel/block_reason.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firewall/dns.go b/firewall/dns.go index d6421e2e..d9a868cb 100644 --- a/firewall/dns.go +++ b/firewall/dns.go @@ -104,7 +104,7 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *res rrCache.Answer, filteredRecords, validIPs = filterDNSSection(rrCache.Answer, p, rrCache.ServerScope) rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...) - // FIXME(ppacher): should we consider valid IPs from the extra section? + // we don't count the valid IPs in the extra section rrCache.Extra, filteredRecords, _ = filterDNSSection(rrCache.Extra, p, rrCache.ServerScope) rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...) diff --git a/intel/block_reason.go b/intel/block_reason.go index 040d6c01..09b89db2 100644 --- a/intel/block_reason.go +++ b/intel/block_reason.go @@ -83,7 +83,7 @@ func (br ListBlockReason) ToRRs() []dns.RR { wouldBeBlockedBy, err := dns.NewRR(fmt.Sprintf( "%s-wouldBeBlockedBy. 0 IN TXT %q", strings.TrimRight(lm.Entity, "."), - strings.Join(lm.ActiveLists, ","), + strings.Join(lm.InactiveLists, ","), )) if err == nil { rrs = append(rrs, wouldBeBlockedBy) From b7cf838e482e7699a2574e7a26f96fbf25199267 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 10:52:14 +0200 Subject: [PATCH 17/30] Added first github templates --- .github/ISSUE_TEMPLATE/bug-report.md | 53 ++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature.md | 15 ++++++++ .github/ISSUE_TEMPLATE/support.md | 16 +++++++++ 3 files changed, 84 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/feature.md create mode 100644 .github/ISSUE_TEMPLATE/support.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000..d9d40cae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,53 @@ +--- +name: Bug Report +about: Report a bug encountered while using the Portmaster +labels: bug + +--- + + + +**Checklist**: + +- [ ] I'm using the official portmaster release (i.e no custom builds) +- [ ] I searched for similar/existing issues first. +- [ ] My issue is not mentioned in the Known Issues section of my [OS](https://github.com/safing/portmaster/wiki) + +**What happened**: + +**What you expected to happen**: + +**How to reproduce it (as minimally and precisely as possible)**: + +**Anything else we need to know?**: + +**Environment**: + +Portmaster Version: + +
+ `portmaster-control --version` + +
+ +Operating System: +- [ ] Windows 7 +- [ ] Windows 8/8.1 +- [ ] Windows 10 +- [ ] Linux + - Please provide the output of `cat /etc/os-release` + +If applicable you can provide related sections from the log files (Typically at `/opt/portmaster/logs`) and ensure to **remove sensitive or otherwise private information**. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..87f04413 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,15 @@ +--- +name: Feature Request +about: Suggest an enhancement or feature to the Portmaster +labels: feature + +--- + + +**What would you like to be added**: + +**Why is this needed**: diff --git a/.github/ISSUE_TEMPLATE/support.md b/.github/ISSUE_TEMPLATE/support.md new file mode 100644 index 00000000..277cdaeb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support.md @@ -0,0 +1,16 @@ +--- +name: Support Request +about: Support request or question relating to the Portmaster +labels: question + +--- + + \ No newline at end of file From 8d6da848e1958a910908117bac9e3135b0f952e1 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 11:37:07 +0200 Subject: [PATCH 18/30] Update bug-report.md Change E-Mail, location of log files and how version data can be retrieved --- .github/ISSUE_TEMPLATE/bug-report.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index d9d40cae..b7586b2c 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -6,10 +6,8 @@ labels: bug --- **Checklist**: @@ -39,7 +37,7 @@ For security related reports, please disclose it privately to noc@safing.io. Portmaster Version:
- `portmaster-control --version` + Versions from the `About` page in Portmaster's UI
@@ -50,4 +48,6 @@ Operating System: - [ ] Linux - Please provide the output of `cat /etc/os-release` -If applicable you can provide related sections from the log files (Typically at `/opt/portmaster/logs`) and ensure to **remove sensitive or otherwise private information**. \ No newline at end of file +If applicable you can provide related sections from the log files and ensure to **remove sensitive or otherwise private information**. + - Linux: `/var/lib/portmaster/logs` + - Windows: `%PROGRAMDATA%\Portmaster\ΔΌogs` From f83809f868e667706d33ee84e5b5acae04ec4b4a Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 11:37:50 +0200 Subject: [PATCH 19/30] Update support.md --- .github/ISSUE_TEMPLATE/support.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/support.md b/.github/ISSUE_TEMPLATE/support.md index 277cdaeb..bca346ed 100644 --- a/.github/ISSUE_TEMPLATE/support.md +++ b/.github/ISSUE_TEMPLATE/support.md @@ -12,5 +12,5 @@ GitHub is not the right place for support requests. If you're looking for help, please post your question or search our sub-reddit at https://www.reddit.com/r/safing -If the matter is security related, please disclose it privately to noc@safing.io ---> \ No newline at end of file +If the matter is security related, please disclose it privately to support@safing.io +--> From 7e06f9b746f2c6cae8ebc1b859ebad34718da86b Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 11:38:03 +0200 Subject: [PATCH 20/30] Update bug-report.md --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index b7586b2c..b6462b80 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -15,7 +15,7 @@ a UI issue please report it at https://github.com/safing/portmaster-ui/issues/ne Thank you! -For security related reports, please disclose it privately to security@safing.io. +For security related reports, please disclose it privately to support@safing.io. --> **Checklist**: From e1c75d758a01efc1cacbf2d5b1e6eba1d2a69aa5 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 11:58:45 +0200 Subject: [PATCH 21/30] Create config.yaml --- .github/ISSUE_TEMPLATE/config.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yaml diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 00000000..dd76319d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,14 @@ + +# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser +blank_issues_enabled: false # default: true +contact_links: +- name: Community Support 🀷 + url: https://www.reddit.com/r/safing + about: Please ask questions and support requests here +- name: Security related issues πŸ” + about: | + Please report security related bugs and issues + privately to security@safing.io. +- name: Code of Conduct πŸ“ + url: https://github.com/safing/portmaster/blob/develop/CODE_OF_CONDUCT.md + about: Be nice to other community members ❀ From a6cbe6d0c99efdd74817009361735cfdfdca7f44 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 12:00:00 +0200 Subject: [PATCH 22/30] Rename config.yaml to config.yml --- .github/ISSUE_TEMPLATE/{config.yaml => config.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{config.yaml => config.yml} (100%) diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yaml rename to .github/ISSUE_TEMPLATE/config.yml From 98350ff62d6d7a9f5a505ac7adc47e1e09c5cf6b Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 12:04:51 +0200 Subject: [PATCH 23/30] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index dd76319d..ce7fb014 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,6 +6,7 @@ contact_links: url: https://www.reddit.com/r/safing about: Please ask questions and support requests here - name: Security related issues πŸ” + url: mailto:support@safing.io about: | Please report security related bugs and issues privately to security@safing.io. From fb47b076ddc06eaf6c1d69096b791603883fc321 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 12:10:30 +0200 Subject: [PATCH 24/30] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ce7fb014..860ac71e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,12 +4,7 @@ blank_issues_enabled: false # default: true contact_links: - name: Community Support 🀷 url: https://www.reddit.com/r/safing - about: Please ask questions and support requests here -- name: Security related issues πŸ” - url: mailto:support@safing.io - about: | - Please report security related bugs and issues - privately to security@safing.io. + about: Please ask any questions you have here - name: Code of Conduct πŸ“ url: https://github.com/safing/portmaster/blob/develop/CODE_OF_CONDUCT.md about: Be nice to other community members ❀ From 72a477ab5754129be8e72d8f7e2ff4c3e0379a35 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 12:16:42 +0200 Subject: [PATCH 25/30] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 860ac71e..788fb3b3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,10 +1,10 @@ # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser -blank_issues_enabled: false # default: true +blank_issues_enabled: true # default: true contact_links: -- name: Community Support 🀷 +- name: Support Requests & Community 🀷 url: https://www.reddit.com/r/safing - about: Please ask any questions you have here + about: Ask for support and any questions you might have on reddit. - name: Code of Conduct πŸ“ url: https://github.com/safing/portmaster/blob/develop/CODE_OF_CONDUCT.md about: Be nice to other community members ❀ From 670b69528dbcdadbe472a10a0c4e4d4236988621 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 12:16:52 +0200 Subject: [PATCH 26/30] Delete support.md --- .github/ISSUE_TEMPLATE/support.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/support.md diff --git a/.github/ISSUE_TEMPLATE/support.md b/.github/ISSUE_TEMPLATE/support.md deleted file mode 100644 index bca346ed..00000000 --- a/.github/ISSUE_TEMPLATE/support.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Support Request -about: Support request or question relating to the Portmaster -labels: question - ---- - - From 0d087e6f3383ba6ce0c98942ba147466ac0fe380 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 12:25:03 +0200 Subject: [PATCH 27/30] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 788fb3b3..bee1074d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,6 +2,9 @@ # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser blank_issues_enabled: true # default: true contact_links: +- name: User Interface Issues πŸŽ› + url: https://github.com/safing/portmaster-ui/issues/new/choose + about: Report any issues or feature requests for the Portmaster UI here - name: Support Requests & Community 🀷 url: https://www.reddit.com/r/safing about: Ask for support and any questions you might have on reddit. From ddaeab57dddcc0990a96db4fc1fc2cd78dd7e860 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 12:25:50 +0200 Subject: [PATCH 28/30] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index bee1074d..ddc730f1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,7 +2,7 @@ # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser blank_issues_enabled: true # default: true contact_links: -- name: User Interface Issues πŸŽ› +- name: User Interface Issues πŸ–₯ url: https://github.com/safing/portmaster-ui/issues/new/choose about: Report any issues or feature requests for the Portmaster UI here - name: Support Requests & Community 🀷 From 8b2f23f4bf280bab3b2b16feb8a74eb24d8fa45b Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 21 Apr 2020 12:27:06 +0200 Subject: [PATCH 29/30] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ddc730f1..4ab1dc2b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,7 +2,7 @@ # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser blank_issues_enabled: true # default: true contact_links: -- name: User Interface Issues πŸ–₯ +- name: User Interface Issues πŸ’» url: https://github.com/safing/portmaster-ui/issues/new/choose about: Report any issues or feature requests for the Portmaster UI here - name: Support Requests & Community 🀷 From f78dd18869b920004276f08b5bc071c4a5c24468 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 13:01:24 +0200 Subject: [PATCH 30/30] Bump version to 0.4.1 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index b45f5ddf..520e8a2d 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,6 @@ import ( ) func main() { - info.Set("Portmaster", "0.4.0", "AGPLv3", true) + info.Set("Portmaster", "0.4.1", "AGPLv3", true) os.Exit(run.Run()) }