mirror of
https://github.com/safing/portmaster
synced 2025-09-01 10:09:11 +00:00
443 lines
9.7 KiB
Go
443 lines
9.7 KiB
Go
package intel
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/safing/portbase/log"
|
|
"github.com/safing/portmaster/intel/filterlists"
|
|
"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.
|
|
// It embeddes a sync.Mutex but none of the endpoints own
|
|
// functions performs locking. The caller MUST ENSURE
|
|
// proper locking and synchronization when accessing
|
|
// any properties of Entity.
|
|
type Entity struct {
|
|
sync.Mutex
|
|
|
|
// lists exist for most entity information and
|
|
// we need to know which one we loaded
|
|
domainListLoaded bool
|
|
ipListLoaded bool
|
|
countryListLoaded bool
|
|
asnListLoaded bool
|
|
reverseResolveEnabled bool
|
|
resolveSubDomainLists bool
|
|
checkCNAMEs bool
|
|
|
|
// Protocol is the protcol number used by the connection.
|
|
Protocol uint8
|
|
|
|
// 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
|
|
ListsMap filterlists.LookupMap
|
|
|
|
// we only load each data above at most once
|
|
fetchLocationOnce sync.Once
|
|
reverseResolveOnce sync.Once
|
|
loadDomainListOnce sync.Once
|
|
loadIPListOnce sync.Once
|
|
loadCoutryListOnce sync.Once
|
|
loadAsnListOnce sync.Once
|
|
}
|
|
|
|
// Init initializes the internal state and returns the entity.
|
|
func (e *Entity) Init() *Entity {
|
|
// for backwards compatibility, remove that one
|
|
return e
|
|
}
|
|
|
|
// FetchData fetches additional information, meant to be called before persisting an entity record.
|
|
func (e *Entity) FetchData() {
|
|
e.getLocation()
|
|
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.checkCNAMEs = 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
|
|
}
|
|
|
|
// 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.
|
|
func (e *Entity) EnableReverseResolving() {
|
|
e.reverseResolveEnabled = true
|
|
}
|
|
|
|
func (e *Entity) reverseResolve() {
|
|
e.reverseResolveOnce.Do(func() {
|
|
// check if we should resolve
|
|
if !e.reverseResolveEnabled {
|
|
return
|
|
}
|
|
|
|
// need IP!
|
|
if e.IP == nil {
|
|
return
|
|
}
|
|
|
|
// reverse resolve
|
|
if reverseResolver == nil {
|
|
return
|
|
}
|
|
// TODO: security level
|
|
domain, err := reverseResolver(context.TODO(), e.IP.String(), status.SecurityLevelNormal)
|
|
if err != nil {
|
|
log.Warningf("intel: failed to resolve IP %s: %s", e.IP, err)
|
|
return
|
|
}
|
|
e.Domain = domain
|
|
})
|
|
}
|
|
|
|
// GetDomain returns the domain and whether it is set.
|
|
func (e *Entity) GetDomain() (string, bool) {
|
|
e.reverseResolve()
|
|
|
|
if e.Domain == "" {
|
|
return "", false
|
|
}
|
|
return e.Domain, true
|
|
}
|
|
|
|
// GetIP returns the IP and whether it is set.
|
|
func (e *Entity) GetIP() (net.IP, bool) {
|
|
if e.IP == nil {
|
|
return nil, false
|
|
}
|
|
return e.IP, true
|
|
}
|
|
|
|
// Location
|
|
|
|
func (e *Entity) getLocation() {
|
|
e.fetchLocationOnce.Do(func() {
|
|
// need IP!
|
|
if e.IP == nil {
|
|
return
|
|
}
|
|
|
|
// get location data
|
|
loc, err := geoip.GetLocation(e.IP)
|
|
if err != nil {
|
|
log.Warningf("intel: failed to get location data for %s: %s", e.IP, err)
|
|
return
|
|
}
|
|
e.location = loc
|
|
e.Country = loc.Country.ISOCode
|
|
e.ASN = loc.AutonomousSystemNumber
|
|
})
|
|
}
|
|
|
|
// GetLocation returns the raw location data and whether it is set.
|
|
func (e *Entity) GetLocation() (*geoip.Location, bool) {
|
|
e.getLocation()
|
|
|
|
if e.location == nil {
|
|
return nil, false
|
|
}
|
|
return e.location, true
|
|
}
|
|
|
|
// GetCountry returns the two letter ISO country code and whether it is set.
|
|
func (e *Entity) GetCountry() (string, bool) {
|
|
e.getLocation()
|
|
|
|
if e.Country == "" {
|
|
return "", false
|
|
}
|
|
return e.Country, true
|
|
}
|
|
|
|
// GetASN returns the AS number and whether it is set.
|
|
func (e *Entity) GetASN() (uint, bool) {
|
|
e.getLocation()
|
|
|
|
if e.ASN == 0 {
|
|
return 0, false
|
|
}
|
|
return e.ASN, true
|
|
}
|
|
|
|
// Lists
|
|
func (e *Entity) getLists() {
|
|
e.getDomainLists()
|
|
e.getASNLists()
|
|
e.getIPLists()
|
|
e.getCountryLists()
|
|
}
|
|
|
|
func (e *Entity) mergeList(list []string) {
|
|
e.Lists = mergeStringList(e.Lists, list)
|
|
e.ListsMap = buildLookupMap(e.Lists)
|
|
}
|
|
|
|
func (e *Entity) getDomainLists() {
|
|
if e.domainListLoaded {
|
|
return
|
|
}
|
|
|
|
domain, ok := e.GetDomain()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
e.loadDomainListOnce.Do(func() {
|
|
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 {
|
|
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 {
|
|
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)
|
|
e.loadDomainListOnce = sync.Once{}
|
|
return
|
|
}
|
|
|
|
e.mergeList(list)
|
|
}
|
|
e.domainListLoaded = true
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
asn, ok := e.GetASN()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
log.Tracef("intel: loading ASN list for %d", asn)
|
|
e.loadAsnListOnce.Do(func() {
|
|
list, err := filterlists.LookupASNString(fmt.Sprintf("%d", asn))
|
|
if err != nil {
|
|
log.Errorf("intel: failed to get ASN blocklist for %d: %s", asn, err)
|
|
e.loadAsnListOnce = sync.Once{}
|
|
return
|
|
}
|
|
|
|
e.asnListLoaded = true
|
|
e.mergeList(list)
|
|
})
|
|
}
|
|
|
|
func (e *Entity) getCountryLists() {
|
|
if e.countryListLoaded {
|
|
return
|
|
}
|
|
|
|
country, ok := e.GetCountry()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
log.Tracef("intel: loading country list for %s", country)
|
|
e.loadCoutryListOnce.Do(func() {
|
|
list, err := filterlists.LookupCountry(country)
|
|
if err != nil {
|
|
log.Errorf("intel: failed to load country blocklist for %s: %s", country, err)
|
|
e.loadCoutryListOnce = sync.Once{}
|
|
return
|
|
}
|
|
|
|
e.countryListLoaded = true
|
|
e.mergeList(list)
|
|
})
|
|
}
|
|
|
|
func (e *Entity) getIPLists() {
|
|
if e.ipListLoaded {
|
|
return
|
|
}
|
|
|
|
ip, ok := e.GetIP()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if ip == nil {
|
|
return
|
|
}
|
|
|
|
// only load lists for IP addresses that are classified as global.
|
|
if netutils.ClassifyIP(ip) != netutils.Global {
|
|
return
|
|
}
|
|
|
|
log.Tracef("intel: loading IP list for %s", ip)
|
|
e.loadIPListOnce.Do(func() {
|
|
list, err := filterlists.LookupIP(ip)
|
|
|
|
if err != nil {
|
|
log.Errorf("intel: failed to get IP blocklist for %s: %s", ip.String(), err)
|
|
e.loadIPListOnce = sync.Once{}
|
|
return
|
|
}
|
|
e.ipListLoaded = true
|
|
e.mergeList(list)
|
|
})
|
|
}
|
|
|
|
// GetLists returns the filter list identifiers the entity matched and whether this data is set.
|
|
func (e *Entity) GetLists() ([]string, bool) {
|
|
e.getLists()
|
|
|
|
if e.Lists == nil {
|
|
return nil, false
|
|
}
|
|
return e.Lists, true
|
|
}
|
|
|
|
// GetListsMap is like GetLists but returns a lookup map for list IDs.
|
|
func (e *Entity) GetListsMap() (filterlists.LookupMap, bool) {
|
|
e.getLists()
|
|
|
|
if e.ListsMap == nil {
|
|
return nil, false
|
|
}
|
|
return e.ListsMap, true
|
|
}
|
|
|
|
func mergeStringList(a, b []string) []string {
|
|
listMap := make(map[string]struct{})
|
|
for _, s := range a {
|
|
listMap[s] = struct{}{}
|
|
}
|
|
for _, s := range b {
|
|
listMap[s] = struct{}{}
|
|
}
|
|
|
|
res := make([]string, 0, len(listMap))
|
|
for s := range listMap {
|
|
res = append(res, s)
|
|
}
|
|
sort.Strings(res)
|
|
return res
|
|
}
|
|
|
|
func buildLookupMap(l []string) filterlists.LookupMap {
|
|
m := make(filterlists.LookupMap, len(l))
|
|
|
|
for _, s := range l {
|
|
m[s] = struct{}{}
|
|
}
|
|
|
|
return m
|
|
}
|