safing-portmaster/service/intel/customlists/module.go

251 lines
5.6 KiB
Go

package customlists
import (
"errors"
"net"
"os"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"golang.org/x/net/publicsuffix"
"github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/config"
"github.com/safing/portmaster/service/mgr"
)
type CustomList struct {
mgr *mgr.Manager
instance instance
updateFilterListWorkerMgr *mgr.WorkerMgr
states *mgr.StateMgr
}
func (cl *CustomList) Manager() *mgr.Manager {
return cl.mgr
}
func (cl *CustomList) States() *mgr.StateMgr {
return cl.states
}
func (cl *CustomList) Start() error {
return start()
}
func (cl *CustomList) Stop() error {
return nil
}
// Helper variables for parsing the input file.
var (
isCountryCode = regexp.MustCompile("^[A-Z]{2}$").MatchString
isAutonomousSystem = regexp.MustCompile(`^AS[0-9]+$`).MatchString
)
var (
filterListFilePath string
filterListFileModifiedTime time.Time
filterListLock sync.RWMutex
// ErrNotConfigured is returned when updating the custom filter list, but it
// is not configured.
ErrNotConfigured = errors.New("custom filter list not configured")
)
func prep() error {
initFilterLists()
// Register the config in the ui.
err := registerConfig()
if err != nil {
return err
}
// Register api endpoint for updating the filter list.
if err := api.RegisterEndpoint(api.Endpoint{
Path: "customlists/update",
Write: api.PermitUser,
ActionFunc: func(ar *api.Request) (msg string, err error) {
errCheck := checkAndUpdateFilterList(nil)
if errCheck != nil {
return "", errCheck
}
return "Custom filter list loaded successfully.", nil
},
Name: "Update custom filter list",
Description: "Reload the filter list from the configured file.",
}); err != nil {
return err
}
return nil
}
func start() error {
// Register to hook to update after config change.
module.instance.Config().EventConfigChange.AddCallback(
"update custom filter list",
func(wc *mgr.WorkerCtx, _ struct{}) (bool, error) {
err := checkAndUpdateFilterList(wc)
if !errors.Is(err, ErrNotConfigured) {
return false, err
}
return false, nil
},
)
// Create parser task and enqueue for execution. "checkAndUpdateFilterList" will schedule the next execution.
module.updateFilterListWorkerMgr.Delay(20 * time.Second).Repeat(1 * time.Minute)
return nil
}
func checkAndUpdateFilterList(_ *mgr.WorkerCtx) error {
filterListLock.Lock()
defer filterListLock.Unlock()
// Get path and return error if empty
filePath := getFilePath()
if filePath == "" {
return ErrNotConfigured
}
// Try to get file info
modifiedTime := time.Now()
if fileInfo, err := os.Stat(filePath); err == nil {
modifiedTime = fileInfo.ModTime()
}
// Check if file path has changed or if modified time has changed
if filterListFilePath != filePath || !filterListFileModifiedTime.Equal(modifiedTime) {
err := parseFile(filePath)
if err != nil {
return err
}
filterListFileModifiedTime = modifiedTime
filterListFilePath = filePath
}
return nil
}
// LookupIP checks if the IP address is in a custom filter list.
func LookupIP(ip net.IP) bool {
filterListLock.RLock()
defer filterListLock.RUnlock()
_, ok := ipAddressesFilterList[ip.String()]
return ok
}
// LookupDomain checks if the Domain is in a custom filter list.
func LookupDomain(fullDomain string, filterSubdomains bool) (bool, string) {
filterListLock.RLock()
defer filterListLock.RUnlock()
if filterSubdomains {
// Check if domain is in the list and all its subdomains.
listOfDomains := splitDomain(fullDomain)
for _, domain := range listOfDomains {
_, ok := domainsFilterList[domain]
if ok {
return true, domain
}
}
} else {
// Check only if the domain is in the list
_, ok := domainsFilterList[fullDomain]
return ok, fullDomain
}
return false, ""
}
// LookupASN checks if the Autonomous system number is in a custom filter list.
func LookupASN(number uint) bool {
filterListLock.RLock()
defer filterListLock.RUnlock()
_, ok := autonomousSystemsFilterList[number]
return ok
}
// LookupCountry checks if the country code is in a custom filter list.
func LookupCountry(countryCode string) bool {
filterListLock.RLock()
defer filterListLock.RUnlock()
_, ok := countryCodesFilterList[countryCode]
return ok
}
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
}
var (
module *CustomList
shimLoaded atomic.Bool
)
// New returns a new CustomList module.
func New(instance instance) (*CustomList, error) {
if !shimLoaded.CompareAndSwap(false, true) {
return nil, errors.New("only one instance allowed")
}
m := mgr.New("CustomList")
module = &CustomList{
mgr: m,
instance: instance,
states: mgr.NewStateMgr(m),
updateFilterListWorkerMgr: m.NewWorkerMgr(
"update custom filter list",
func(ctx *mgr.WorkerCtx) error {
err := checkAndUpdateFilterList(ctx)
if !errors.Is(err, ErrNotConfigured) {
return err
}
return nil
},
nil,
),
}
if err := prep(); err != nil {
return nil, err
}
return module, nil
}
type instance interface {
Config() *config.Config
}