diff --git a/intel/entity.go b/intel/entity.go new file mode 100644 index 00000000..20bfe7f6 --- /dev/null +++ b/intel/entity.go @@ -0,0 +1,201 @@ +package intel + +import ( + "context" + "net" + "sync" + + "github.com/tevino/abool" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/intel/geoip" + "github.com/safing/portmaster/status" +) + +// Entity describes a remote endpoint in many different ways. +type Entity struct { + sync.Mutex + + Domain string + IP net.IP + Protocol uint8 + Port uint16 + doReverseResolve bool + reverseResolveDone *abool.AtomicBool + + Country string + ASN uint + location *geoip.Location + locationFetched *abool.AtomicBool + + Lists []string + listsFetched *abool.AtomicBool +} + +// Init initializes the internal state and returns the entity. +func (e *Entity) Init() *Entity { + e.reverseResolveDone = abool.New() + e.locationFetched = abool.New() + e.listsFetched = abool.New() + return e +} + +// FetchData fetches additional information, meant to be called before persisting an entity record. +func (e *Entity) FetchData() { + e.getLocation() + e.getLists() +} + +// Domain and IP + +// EnableReverseResolving enables reverse resolving the domain from the IP on demand. +func (e *Entity) EnableReverseResolving() { + e.Lock() + defer e.Lock() + + e.doReverseResolve = true +} + +func (e *Entity) reverseResolve() { + // only get once + if !e.reverseResolveDone.IsSet() { + e.Lock() + defer e.Unlock() + + // check for concurrent request + if e.reverseResolveDone.IsSet() { + return + } + defer e.reverseResolveDone.Set() + + // check if we should resolve + if !e.doReverseResolve { + return + } + + // need IP! + if e.IP == nil { + return + } + + // reverse resolve + if reverseResolver == nil { + return + } + // TODO: security level + domain, err := reverseResolver(context.TODO(), e.IP.String(), status.SecurityLevelDynamic) + if err != nil { + log.Warningf("intel: failed to resolve IP %s: %s", e.IP, err) + return + } + e.Domain = domain + } +} + +// GetDomain returns the domain and whether it is set. +func (e *Entity) GetDomain() (string, bool) { + e.reverseResolve() + + if e.Domain == "" { + return "", false + } + return e.Domain, true +} + +// GetIP returns the IP and whether it is set. +func (e *Entity) GetIP() (net.IP, bool) { + if e.IP == nil { + return nil, false + } + return e.IP, true +} + +// Location + +func (e *Entity) getLocation() { + // only get once + if !e.locationFetched.IsSet() { + e.Lock() + defer e.Unlock() + + // check for concurrent request + if e.locationFetched.IsSet() { + return + } + defer e.locationFetched.Set() + + // need IP! + if e.IP == nil { + log.Warningf("intel: cannot get location for %s data without IP", e.Domain) + return + } + + // get location data + loc, err := geoip.GetLocation(e.IP) + if err != nil { + log.Warningf("intel: failed to get location data for %s: %s", e.IP, err) + return + } + e.location = loc + e.Country = loc.Country.ISOCode + e.ASN = loc.AutonomousSystemNumber + } +} + +// GetLocation returns the raw location data and whether it is set. +func (e *Entity) GetLocation() (*geoip.Location, bool) { + e.getLocation() + + if e.location == nil { + return nil, false + } + return e.location, true +} + +// GetCountry returns the two letter ISO country code and whether it is set. +func (e *Entity) GetCountry() (string, bool) { + e.getLocation() + + if e.Country == "" { + return "", false + } + return e.Country, true +} + +// GetASN returns the AS number and whether it is set. +func (e *Entity) GetASN() (uint, bool) { + e.getLocation() + + if e.ASN == 0 { + return 0, false + } + return e.ASN, true +} + +// Lists + +func (e *Entity) getLists() { + // only get once + if !e.listsFetched.IsSet() { + e.Lock() + defer e.Unlock() + + // check for concurrent request + if e.listsFetched.IsSet() { + return + } + defer e.listsFetched.Set() + + // TODO: fetch lists + } +} + +// GetLists returns the filter list identifiers the entity matched and whether this data is set. +func (e *Entity) GetLists() ([]string, bool) { + e.getLists() + + if e.Lists == nil { + return nil, false + } + return e.Lists, true +} diff --git a/intel/intel.go b/intel/intel.go deleted file mode 100644 index 6103d328..00000000 --- a/intel/intel.go +++ /dev/null @@ -1,75 +0,0 @@ -package intel - -import ( - "context" - "fmt" - "sync" - - "github.com/safing/portbase/database" - "github.com/safing/portbase/database/record" - "github.com/safing/portbase/log" -) - -var ( - intelDatabase = database.NewInterface(&database.Options{ - AlwaysSetRelativateExpiry: 2592000, // 30 days - }) -) - -// Intel holds intelligence data for a domain. -type Intel struct { - record.Base - sync.Mutex - - Domain string -} - -func makeIntelKey(domain string) string { - return fmt.Sprintf("cache:intel/domain/%s", domain) -} - -// GetIntelFromDB gets an Intel record from the database. -func GetIntelFromDB(domain string) (*Intel, error) { - key := makeIntelKey(domain) - - r, err := intelDatabase.Get(key) - if err != nil { - return nil, err - } - - // unwrap - if r.IsWrapped() { - // only allocate a new struct, if we need it - new := &Intel{} - err = record.Unwrap(r, new) - if err != nil { - return nil, err - } - return new, nil - } - - // or adjust type - new, ok := r.(*Intel) - if !ok { - return nil, fmt.Errorf("record not of type *Intel, but %T", r) - } - return new, nil -} - -// Save saves the Intel record to the database. -func (intel *Intel) Save() error { - intel.SetKey(makeIntelKey(intel.Domain)) - return intelDatabase.PutNew(intel) -} - -// GetIntel fetches intelligence data for the given domain. -func GetIntel(ctx context.Context, q *Query) (*Intel, error) { - // sanity check - if q == nil || !q.check() { - return nil, ErrInvalid - } - - log.Tracer(ctx).Trace("intel: getting intel") - // TODO - return &Intel{Domain: q.FQDN}, nil -} diff --git a/intel/lists.go b/intel/lists.go new file mode 100644 index 00000000..08ca0925 --- /dev/null +++ b/intel/lists.go @@ -0,0 +1,40 @@ +package intel + +// ListSet holds a set of list IDs. +type ListSet struct { + match []string +} + +// NewListSet returns a new ListSet with the given list IDs. +func NewListSet(lists []string) *ListSet { + // TODO: validate lists + return &ListSet{ + match: lists, + } +} + +// Matches returns whether there is a match in the given list IDs. +func (ls *ListSet) Matches(lists []string) (matches bool) { + for _, list := range lists { + for _, entry := range ls.match { + if entry == list { + return true + } + } + } + + return false +} + +// MatchSet returns the matching list IDs. +func (ls *ListSet) MatchSet(lists []string) (matched []string) { + for _, list := range lists { + for _, entry := range ls.match { + if entry == list { + matched = append(matched, list) + } + } + } + + return +} diff --git a/intel/main_test.go b/intel/main_test.go deleted file mode 100644 index c3c4b4da..00000000 --- a/intel/main_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package intel - -import ( - "os" - "testing" - - "github.com/safing/portmaster/core" -) - -func TestMain(m *testing.M) { - // setup - tmpDir, err := core.InitForTesting() - if err != nil { - panic(err) - } - - // setup package - err = prep() - if err != nil { - panic(err) - } - loadResolvers() - - // run tests - rv := m.Run() - - // teardown - core.StopTesting() - _ = os.RemoveAll(tmpDir) - - // exit with test run return value - os.Exit(rv) -} diff --git a/intel/resolver.go b/intel/resolver.go index f475b7f5..ec9ca8a4 100644 --- a/intel/resolver.go +++ b/intel/resolver.go @@ -2,152 +2,15 @@ package intel import ( "context" - "net" - "sync" - "time" - - "github.com/miekg/dns" - "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/environment" ) -// DNS Resolver Attributes -const ( - ServerTypeDNS = "dns" - ServerTypeTCP = "tcp" - ServerTypeDoT = "dot" - ServerTypeDoH = "doh" - - ServerSourceConfigured = "config" - ServerSourceAssigned = "dhcp" - ServerSourceMDNS = "mdns" +var ( + reverseResolver func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error) ) -// Resolver holds information about an active resolver. -type Resolver struct { - // Server config url (and ID) - Server string - - // Parsed config - ServerType string - ServerAddress string - ServerIP net.IP - ServerIPScope int8 - ServerPort uint16 - - // Special Options - VerifyDomain string - Search []string - SkipFQDN string - - Source string - - // logic interface - Conn ResolverConn -} - -// String returns the URL representation of the resolver. -func (resolver *Resolver) String() string { - return resolver.Server -} - -// ResolverConn is an interface to implement different types of query backends. -type ResolverConn interface { - Query(ctx context.Context, q *Query) (*RRCache, error) - MarkFailed() - LastFail() time.Time -} - -// BasicResolverConn implements ResolverConn for standard dns clients. -type BasicResolverConn struct { - sync.Mutex // for lastFail - - resolver *Resolver - clientManager *clientManager - lastFail time.Time -} - -// MarkFailed marks the resolver as failed. -func (brc *BasicResolverConn) MarkFailed() { - if !environment.Online() { - // don't mark failed if we are offline - return +// SetReverseResolver allows the resolver module to register a function to allow reverse resolving IPs to domains. +func SetReverseResolver(fn func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error)) { + if reverseResolver == nil { + reverseResolver = fn } - - brc.Lock() - defer brc.Unlock() - brc.lastFail = time.Now() -} - -// LastFail returns the internal lastfail value while locking the Resolver. -func (brc *BasicResolverConn) LastFail() time.Time { - brc.Lock() - defer brc.Unlock() - return brc.lastFail -} - -// Query executes the given query against the resolver. -func (brc *BasicResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) { - // convenience - resolver := brc.resolver - - // create query - dnsQuery := new(dns.Msg) - dnsQuery.SetQuestion(q.FQDN, uint16(q.QType)) - - // start - var reply *dns.Msg - var err error - for i := 0; i < 3; i++ { - - // log query time - // qStart := time.Now() - reply, _, err = brc.clientManager.getDNSClient().Exchange(dnsQuery, resolver.ServerAddress) - // log.Tracef("intel: query to %s took %s", resolver.Server, time.Now().Sub(qStart)) - - // error handling - if err != nil { - log.Tracer(ctx).Tracef("intel: query to %s encountered error: %s", resolver.Server, err) - - // TODO: handle special cases - // 1. connect: network is unreachable - // 2. timeout - - // hint network environment at failed connection - environment.ReportFailedConnection() - - // temporary error - if nerr, ok := err.(net.Error); ok && nerr.Timeout() { - log.Tracer(ctx).Tracef("intel: retrying to resolve %s%s with %s, error is temporary", q.FQDN, q.QType, resolver.Server) - continue - } - - // permanent error - break - } - - // no error - break - } - - if err != nil { - return nil, err - // FIXME: mark as failed - } - - // hint network environment at successful connection - environment.ReportSuccessfulConnection() - - new := &RRCache{ - Domain: q.FQDN, - Question: q.QType, - Answer: reply.Answer, - Ns: reply.Ns, - Extra: reply.Extra, - Server: resolver.Server, - ServerScope: resolver.ServerIPScope, - } - - // TODO: check if reply.Answer is valid - return new, nil }