From 86a449a619f8eae715f00248dc9441919b45c8a1 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 15 Apr 2020 09:59:59 +0200 Subject: [PATCH] Add support to filter sub-domains as well --- firewall/master.go | 6 +-- intel/entity.go | 79 ++++++++++++++++++++++++++++++++++---- netenv/online-status.go | 12 +++--- profile/config.go | 18 +++++++++ profile/profile-layered.go | 7 ++++ 5 files changed, 107 insertions(+), 15 deletions(-) diff --git a/firewall/master.go b/firewall/master.go index ddf09a37..9650b625 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -41,9 +41,9 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: log.Infof("filter: re-evaluating verdict on %s", conn) conn.Verdict = network.VerdictUndecided - //if conn.Entity != nil { - //conn.Entity.ResetLists() - //} + if conn.Entity != nil { + conn.Entity.ResetLists() + } } // grant self diff --git a/intel/entity.go b/intel/entity.go index f84b9d6d..f4412bc3 100644 --- a/intel/entity.go +++ b/intel/entity.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "sort" + "strings" "sync" "github.com/safing/portbase/log" @@ -12,6 +13,7 @@ import ( "github.com/safing/portmaster/intel/geoip" "github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/status" + "golang.org/x/net/publicsuffix" ) // Entity describes a remote endpoint in many different ways. @@ -29,6 +31,7 @@ type Entity struct { countryListLoaded bool asnListLoaded bool reverseResolveEnabled bool + resolveSubDomainLists bool Protocol uint8 Port uint16 @@ -63,6 +66,34 @@ func (e *Entity) FetchData() { e.getLists() } +// ResetLists resets the current list data and forces +// all list sources to be re-acquired when calling GetLists(). +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 + e.domainListLoaded = false + e.ipListLoaded = false + e.countryListLoaded = false + e.asnListLoaded = false + e.resolveSubDomainLists = false + e.loadDomainListOnce = sync.Once{} + e.loadIPListOnce = sync.Once{} + e.loadCoutryListOnce = sync.Once{} + e.loadAsnListOnce = sync.Once{} +} + +// ResolveSubDomainLists enables or disables list lookups for +// sub-domains. +func (e *Entity) ResolveSubDomainLists(enabled bool) { + if e.domainListLoaded { + log.Warningf("intel/filterlists: tried to change sub-domain resolving for %s but lists are already fetched", e.Domain) + } + e.resolveSubDomainLists = enabled +} + // Domain and IP // EnableReverseResolving enables reverse resolving the domain from the IP on demand. @@ -190,19 +221,53 @@ func (e *Entity) getDomainLists() { } e.loadDomainListOnce.Do(func() { - log.Debugf("intel: loading domain list for %s", domain) - list, err := filterlists.LookupDomain(domain) - if err != nil { - log.Errorf("intel: failed to get domain blocklists for %s: %s", domain, err) - e.loadDomainListOnce = sync.Once{} - return + var domains = []string{domain} + if e.resolveSubDomainLists { + domains = splitDomain(domain) + log.Debugf("intel: subdomain list resolving is enabled, checking %v", domains) } + for _, d := range domains { + log.Debugf("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) + e.loadDomainListOnce = sync.Once{} + return + } + + e.mergeList(list) + } e.domainListLoaded = true - e.mergeList(list) }) } +func splitDomain(domain string) []string { + domain = strings.Trim(domain, ".") + suffix, _ := publicsuffix.PublicSuffix(domain) + if suffix == domain { + return []string{domain} + } + + domainWithoutSuffix := domain[:len(domain)-len(suffix)] + domainWithoutSuffix = strings.Trim(domainWithoutSuffix, ".") + + splitted := strings.FieldsFunc(domainWithoutSuffix, func(r rune) bool { + return r == '.' + }) + + domains := make([]string, 0, len(splitted)) + for idx := range splitted { + + d := strings.Join(splitted[idx:], ".") + "." + suffix + if d[len(d)-1] != '.' { + d += "." + } + domains = append(domains, d) + } + return domains +} + func (e *Entity) getASNLists() { if e.asnListLoaded { return diff --git a/netenv/online-status.go b/netenv/online-status.go index 1cd6964a..b7542b3b 100644 --- a/netenv/online-status.go +++ b/netenv/online-status.go @@ -201,11 +201,13 @@ func triggerOnlineStatusInvestigation() { func monitorOnlineStatus(ctx context.Context) error { for { - timeout := time.Minute - if GetOnlineStatus() != StatusOnline { - timeout = time.Second - log.Debugf("checking online status again in %s because current status is %s", timeout, GetOnlineStatus()) - } + timeout := 5 * time.Minute + /* + if GetOnlineStatus() != StatusOnline { + timeout = time.Second + log.Debugf("checking online status again in %s because current status is %s", timeout, GetOnlineStatus()) + } + */ // wait for trigger select { case <-ctx.Done(): diff --git a/profile/config.go b/profile/config.go index 941af738..58bd744b 100644 --- a/profile/config.go +++ b/profile/config.go @@ -27,6 +27,9 @@ var ( CfgOptionFilterListKey = "filter/lists" cfgOptionFilterLists config.StringArrayOption + CfgOptionFilterSubDomainsKey = "filter/includeSubdomains" + cfgOptionFilterSubDomains config.IntOption // security level option + CfgOptionBlockScopeLocalKey = "filter/blockLocal" cfgOptionBlockScopeLocal config.IntOption // security level option @@ -155,6 +158,21 @@ Examples: cfgOptionFilterLists = config.Concurrent.GetAsStringArray(CfgOptionFilterListKey, []string{}) cfgStringArrayOptions[CfgOptionFilterListKey] = cfgOptionFilterLists + err = config.Register(&config.Option{ + Name: "Filter SubDomains", + Key: CfgOptionFilterSubDomainsKey, + Description: "Also filter sub-domains if a parent domain is blocked by a filter list", + OptType: config.OptTypeInt, + ExternalOptType: "security level", + DefaultValue: status.SecurityLevelOff, + ValidationRegex: "^(0|4|6|7)$", + }) + if err != nil { + return err + } + cfgOptionFilterSubDomains = config.Concurrent.GetAsInt(CfgOptionFilterSubDomainsKey, int64(status.SecurityLevelOff)) + cfgIntOptions[CfgOptionFilterSubDomainsKey] = cfgOptionFilterSubDomains + // Block Scope Local err = config.Register(&config.Option{ Name: "Block Scope Local", diff --git a/profile/profile-layered.go b/profile/profile-layered.go index 7cd6f4e4..90f0478b 100644 --- a/profile/profile-layered.go +++ b/profile/profile-layered.go @@ -42,6 +42,7 @@ type LayeredProfile struct { EnforceSPN config.BoolOption RemoveOutOfScopeDNS config.BoolOption RemoveBlockedDNS config.BoolOption + FilterSubDomains config.BoolOption } // NewLayeredProfile returns a new layered profile based on the given local profile. @@ -93,6 +94,10 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile { CfgOptionRemoveBlockedDNSKey, cfgOptionRemoveBlockedDNS, ) + new.FilterSubDomains = new.wrapSecurityLevelOption( + CfgOptionFilterSubDomainsKey, + cfgOptionFilterSubDomains, + ) // TODO: load linked profiles. @@ -220,6 +225,8 @@ 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) { + entity.ResolveSubDomainLists(lp.FilterSubDomains()) + lookupMap, hasLists := entity.GetListsMap() if !hasLists { return endpoints.NoMatch, ""