mirror of
https://github.com/safing/portmaster
synced 2025-09-04 11:39:29 +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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
"github.com/safing/portmaster/network/environment"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNS Resolver Attributes
|
var (
|
||||||
const (
|
reverseResolver func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error)
|
||||||
ServerTypeDNS = "dns"
|
|
||||||
ServerTypeTCP = "tcp"
|
|
||||||
ServerTypeDoT = "dot"
|
|
||||||
ServerTypeDoH = "doh"
|
|
||||||
|
|
||||||
ServerSourceConfigured = "config"
|
|
||||||
ServerSourceAssigned = "dhcp"
|
|
||||||
ServerSourceMDNS = "mdns"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Resolver holds information about an active resolver.
|
// SetReverseResolver allows the resolver module to register a function to allow reverse resolving IPs to domains.
|
||||||
type Resolver struct {
|
func SetReverseResolver(fn func(ctx context.Context, ip string, securityLevel uint8) (domain string, err error)) {
|
||||||
// Server config url (and ID)
|
if reverseResolver == nil {
|
||||||
Server string
|
reverseResolver = fn
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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