Add intel package with filter list mgmt stubs

This commit is contained in:
Daniel 2020-03-20 23:01:40 +01:00
parent 55033404d4
commit 34f2beb8d4
5 changed files with 247 additions and 251 deletions

201
intel/entity.go Normal file
View file

@ -0,0 +1,201 @@
package intel
import (
"context"
"net"
"sync"
"github.com/tevino/abool"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/intel/geoip"
"github.com/safing/portmaster/status"
)
// Entity describes a remote endpoint in many different ways.
type Entity struct {
sync.Mutex
Domain string
IP net.IP
Protocol uint8
Port uint16
doReverseResolve bool
reverseResolveDone *abool.AtomicBool
Country string
ASN uint
location *geoip.Location
locationFetched *abool.AtomicBool
Lists []string
listsFetched *abool.AtomicBool
}
// Init initializes the internal state and returns the entity.
func (e *Entity) Init() *Entity {
e.reverseResolveDone = abool.New()
e.locationFetched = abool.New()
e.listsFetched = abool.New()
return e
}
// FetchData fetches additional information, meant to be called before persisting an entity record.
func (e *Entity) FetchData() {
e.getLocation()
e.getLists()
}
// Domain and IP
// EnableReverseResolving enables reverse resolving the domain from the IP on demand.
func (e *Entity) EnableReverseResolving() {
e.Lock()
defer e.Lock()
e.doReverseResolve = true
}
func (e *Entity) reverseResolve() {
// only get once
if !e.reverseResolveDone.IsSet() {
e.Lock()
defer e.Unlock()
// check for concurrent request
if e.reverseResolveDone.IsSet() {
return
}
defer e.reverseResolveDone.Set()
// check if we should resolve
if !e.doReverseResolve {
return
}
// need IP!
if e.IP == nil {
return
}
// reverse resolve
if reverseResolver == nil {
return
}
// TODO: security level
domain, err := reverseResolver(context.TODO(), e.IP.String(), status.SecurityLevelDynamic)
if err != nil {
log.Warningf("intel: failed to resolve IP %s: %s", e.IP, err)
return
}
e.Domain = domain
}
}
// GetDomain returns the domain and whether it is set.
func (e *Entity) GetDomain() (string, bool) {
e.reverseResolve()
if e.Domain == "" {
return "", false
}
return e.Domain, true
}
// GetIP returns the IP and whether it is set.
func (e *Entity) GetIP() (net.IP, bool) {
if e.IP == nil {
return nil, false
}
return e.IP, true
}
// Location
func (e *Entity) getLocation() {
// only get once
if !e.locationFetched.IsSet() {
e.Lock()
defer e.Unlock()
// check for concurrent request
if e.locationFetched.IsSet() {
return
}
defer e.locationFetched.Set()
// need IP!
if e.IP == nil {
log.Warningf("intel: cannot get location for %s data without IP", e.Domain)
return
}
// get location data
loc, err := geoip.GetLocation(e.IP)
if err != nil {
log.Warningf("intel: failed to get location data for %s: %s", e.IP, err)
return
}
e.location = loc
e.Country = loc.Country.ISOCode
e.ASN = loc.AutonomousSystemNumber
}
}
// GetLocation returns the raw location data and whether it is set.
func (e *Entity) GetLocation() (*geoip.Location, bool) {
e.getLocation()
if e.location == nil {
return nil, false
}
return e.location, true
}
// GetCountry returns the two letter ISO country code and whether it is set.
func (e *Entity) GetCountry() (string, bool) {
e.getLocation()
if e.Country == "" {
return "", false
}
return e.Country, true
}
// GetASN returns the AS number and whether it is set.
func (e *Entity) GetASN() (uint, bool) {
e.getLocation()
if e.ASN == 0 {
return 0, false
}
return e.ASN, true
}
// Lists
func (e *Entity) getLists() {
// only get once
if !e.listsFetched.IsSet() {
e.Lock()
defer e.Unlock()
// check for concurrent request
if e.listsFetched.IsSet() {
return
}
defer e.listsFetched.Set()
// TODO: fetch lists
}
}
// GetLists returns the filter list identifiers the entity matched and whether this data is set.
func (e *Entity) GetLists() ([]string, bool) {
e.getLists()
if e.Lists == nil {
return nil, false
}
return e.Lists, true
}

View file

@ -1,75 +0,0 @@
package intel
import (
"context"
"fmt"
"sync"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
)
var (
intelDatabase = database.NewInterface(&database.Options{
AlwaysSetRelativateExpiry: 2592000, // 30 days
})
)
// Intel holds intelligence data for a domain.
type Intel struct {
record.Base
sync.Mutex
Domain string
}
func makeIntelKey(domain string) string {
return fmt.Sprintf("cache:intel/domain/%s", domain)
}
// GetIntelFromDB gets an Intel record from the database.
func GetIntelFromDB(domain string) (*Intel, error) {
key := makeIntelKey(domain)
r, err := intelDatabase.Get(key)
if err != nil {
return nil, err
}
// unwrap
if r.IsWrapped() {
// only allocate a new struct, if we need it
new := &Intel{}
err = record.Unwrap(r, new)
if err != nil {
return nil, err
}
return new, nil
}
// or adjust type
new, ok := r.(*Intel)
if !ok {
return nil, fmt.Errorf("record not of type *Intel, but %T", r)
}
return new, nil
}
// Save saves the Intel record to the database.
func (intel *Intel) Save() error {
intel.SetKey(makeIntelKey(intel.Domain))
return intelDatabase.PutNew(intel)
}
// GetIntel fetches intelligence data for the given domain.
func GetIntel(ctx context.Context, q *Query) (*Intel, error) {
// sanity check
if q == nil || !q.check() {
return nil, ErrInvalid
}
log.Tracer(ctx).Trace("intel: getting intel")
// TODO
return &Intel{Domain: q.FQDN}, nil
}

40
intel/lists.go Normal file
View file

@ -0,0 +1,40 @@
package intel
// ListSet holds a set of list IDs.
type ListSet struct {
match []string
}
// NewListSet returns a new ListSet with the given list IDs.
func NewListSet(lists []string) *ListSet {
// TODO: validate lists
return &ListSet{
match: lists,
}
}
// Matches returns whether there is a match in the given list IDs.
func (ls *ListSet) Matches(lists []string) (matches bool) {
for _, list := range lists {
for _, entry := range ls.match {
if entry == list {
return true
}
}
}
return false
}
// MatchSet returns the matching list IDs.
func (ls *ListSet) MatchSet(lists []string) (matched []string) {
for _, list := range lists {
for _, entry := range ls.match {
if entry == list {
matched = append(matched, list)
}
}
}
return
}

View file

@ -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)
}

View file

@ -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
}