mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
Add intel package with filter list mgmt stubs
This commit is contained in:
parent
55033404d4
commit
34f2beb8d4
5 changed files with 247 additions and 251 deletions
201
intel/entity.go
Normal file
201
intel/entity.go
Normal 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
|
||||
}
|
|
@ -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
40
intel/lists.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue