Custom filter list:

subdomain and cname cheks
Automatic realod when settings is changed
periodicly check for file changes
This commit is contained in:
Vladimir Stoilov 2022-07-26 17:18:58 +02:00 committed by Daniel
parent 35697989e5
commit 62c100714a
5 changed files with 126 additions and 16 deletions

View file

@ -616,14 +616,24 @@ matchLoop:
} }
func checkCustomFilterList(_ context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool { func checkCustomFilterList(_ context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
// block if the domain name appears in the custom filter list // block if the domain name appears in the custom filter list (check for subdomains if enabled)
if conn.Entity.Domain != "" { if conn.Entity.Domain != "" {
if customlists.LookupDomain(conn.Entity.Domain) { if customlists.LookupDomain(conn.Entity.Domain, p.FilterSubDomains()) {
conn.Block("Domains appears in the custom user list", customlists.CfgOptionCustomListBlockingKey) conn.Block("Domains appears in the custom user list", customlists.CfgOptionCustomListBlockingKey)
return true return true
} }
} }
// block if any of the CNAME appears in the custom filter list (check for subdomains if enabled)
if len(conn.Entity.CNAME) > 0 && p.FilterCNAMEs() {
for _, cname := range conn.Entity.CNAME {
if customlists.LookupDomain(cname, p.FilterSubDomains()) {
conn.Block("CNAME appears in the custom user list", customlists.CfgOptionCustomListBlockingKey)
return true
}
}
}
// block if ip addresses appears in the custom filter list // block if ip addresses appears in the custom filter list
if conn.Entity.IP != nil { if conn.Entity.IP != nil {
if customlists.LookupIP(&conn.Entity.IP) { if customlists.LookupIP(&conn.Entity.IP) {

View file

@ -1,11 +1,15 @@
package customlists package customlists
import "github.com/safing/portbase/config" import (
"strings"
"github.com/safing/portbase/config"
)
var ( var (
// CfgOptionCustomListBlockingKey is the config key for the listen address.. // CfgOptionCustomListBlockingKey is the config key for the listen address..
CfgOptionCustomListBlockingKey = "filter/customListBlocking" CfgOptionCustomListBlockingKey = "filter/customListBlocking"
cfgOptionCustomListBlockingOrder = 37 cfgOptionCustomListBlockingOrder = 35
cfgOptionCustomListCategoryAnnotation = "Filter Lists" cfgOptionCustomListCategoryAnnotation = "Filter Lists"
) )
@ -14,11 +18,35 @@ var (
) )
func registerConfig() error { func registerConfig() error {
help := strings.ReplaceAll(`Put all domains, Ip addresses, country codes and autonomous system that you want to block in a file in which each entry is on a new line.
Lines that start with a '#' symbol are ignored.
Everything after the first space/tab is ignored.
Example:
"""
# Domains:
example.com
google.com
# IP addresses
1.2.3.4
4.3.2.1
# Countries
AU
BG
# Autonomous Systems
AS123
"""
> * All the records are stored in RAM, careful with large block lists.
> * Hosts files are not supported.`, "\"", "`")
// register a setting for the file path in the ui // register a setting for the file path in the ui
err := config.Register(&config.Option{ err := config.Register(&config.Option{
Name: "Custom Filter List", Name: "Custom Filter List",
Key: CfgOptionCustomListBlockingKey, Key: CfgOptionCustomListBlockingKey,
Description: "Path to the file that contains a list of Domain, IP addresses, country codes and autonomous systems that you want to block", Description: "Path to the file that contains a list of Domain, IP addresses, country codes and autonomous systems that you want to block",
Help: help,
OptType: config.OptTypeString, OptType: config.OptTypeString,
ExpertiseLevel: config.ExpertiseLevelExpert, ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable, ReleaseLevel: config.ReleaseLevelStable,

View file

@ -9,6 +9,7 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/notifications"
"github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/netutils"
) )
@ -19,11 +20,18 @@ var (
domainsFilterList map[string]struct{} domainsFilterList map[string]struct{}
) )
const numberOfZeroIPsUntilWarning = 100
func parseFile(filePath string) error { func parseFile(filePath string) error {
// ignore empty file path
if filePath == "" {
return nil
}
// open the file if possible // open the file if possible
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
log.Warningf("Custom filter: failed to parse file: \"%s\"", filePath) log.Warningf("intel/customlists: failed to parse file: \"%s\"", filePath)
return err return err
} }
defer file.Close() defer file.Close()
@ -34,11 +42,13 @@ func parseFile(filePath string) error {
autonomousSystemsFilterList = make(map[uint]struct{}) autonomousSystemsFilterList = make(map[uint]struct{})
domainsFilterList = make(map[string]struct{}) domainsFilterList = make(map[string]struct{})
var numberOfZeroIPs uint64
// read filter file line by line // read filter file line by line
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
// the scanner will error out if the line is greater than 64K, in this case it is enough // the scanner will error out if the line is greater than 64K, in this case it is enough
for scanner.Scan() { for scanner.Scan() {
parseLine(scanner.Text()) parseLine(scanner.Text(), &numberOfZeroIPs)
} }
// check for scanner error // check for scanner error
@ -46,12 +56,17 @@ func parseFile(filePath string) error {
return err return err
} }
log.Infof("Custom filter: list loaded successful: %s", filePath) if numberOfZeroIPs >= numberOfZeroIPsUntilWarning {
log.Warning("intel/customlists: Too many zero IP addresses.")
notifications.NotifyWarn("too_many_zero_ips", "Too many zero IP addresses. Check your custom filter list.", "Hosts file format is not spported.")
}
log.Infof("intel/customlists: list loaded successful: %s", filePath)
return nil return nil
} }
func parseLine(line string) { func parseLine(line string, numberOfZeroIPs *uint64) {
// ignore empty lines and comment lines // ignore empty lines and comment lines
if len(line) == 0 || line[0] == '#' { if len(line) == 0 || line[0] == '#' {
return return
@ -69,6 +84,13 @@ func parseLine(line string) {
ip := net.ParseIP(field) ip := net.ParseIP(field)
if ip != nil { if ip != nil {
ipAddressesFilterList[ip.String()] = struct{}{} ipAddressesFilterList[ip.String()] = struct{}{}
// check if its zero ip
for i := 0; i < len(ip); i++ {
if ip[i] != 0 {
*numberOfZeroIPs++
}
}
} }
// check if it's a Autonomous system (example AS123) // check if it's a Autonomous system (example AS123)

View file

@ -5,14 +5,20 @@ import (
"net" "net"
"os" "os"
"regexp" "regexp"
"strings"
"sync"
"time" "time"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
"golang.org/x/net/publicsuffix"
) )
var module *modules.Module var module *modules.Module
const configChangeEvent = "config change" const (
configModuleName = "config"
configChangeEvent = "config change"
)
// Helper variables for parsing the input file // Helper variables for parsing the input file
var ( var (
@ -23,6 +29,8 @@ var (
var ( var (
filterListFilePath string filterListFilePath string
filterListFileModifiedTime time.Time filterListFileModifiedTime time.Time
parseLock sync.RWMutex
) )
func init() { func init() {
@ -38,7 +46,7 @@ func prep() error {
// register to hook to update after config change. // register to hook to update after config change.
if err := module.RegisterEventHook( if err := module.RegisterEventHook(
module.Name, configModuleName,
configChangeEvent, configChangeEvent,
"update custom filter list", "update custom filter list",
func(ctx context.Context, obj interface{}) error { func(ctx context.Context, obj interface{}) error {
@ -54,17 +62,20 @@ func prep() error {
func start() error { func start() error {
// register timer to run every periodically and check for file updates // register timer to run every periodically and check for file updates
module.NewTask("Custom filter list file update check", func(context.Context, *modules.Task) error { module.NewTask("intel/customlists list file update check", func(context.Context, *modules.Task) error {
_ = checkAndUpdateFilterList() _ = checkAndUpdateFilterList()
return nil return nil
}).Repeat(10 * time.Minute) }).Repeat(10 * time.Minute)
// parse the file for the first time at start // parse the file at startup
_ = parseFile(getFilePath()) _ = parseFile(getFilePath())
return nil return nil
} }
func checkAndUpdateFilterList() error { func checkAndUpdateFilterList() error {
parseLock.Lock()
defer parseLock.Unlock()
// get path and try to get its info // get path and try to get its info
filePath := getFilePath() filePath := getFilePath()
fileInfo, err := os.Stat(filePath) fileInfo, err := os.Stat(filePath)
@ -92,9 +103,20 @@ func LookupIP(ip *net.IP) bool {
} }
// LookupDomain checks if the Domain is in a custom filter list // LookupDomain checks if the Domain is in a custom filter list
func LookupDomain(domain string) bool { func LookupDomain(fullDomain string, filterSubdomains bool) bool {
_, ok := domainsFilterList[domain] if filterSubdomains {
return ok listOfDomains := splitDomain(fullDomain)
for _, domain := range listOfDomains {
_, ok := domainsFilterList[domain]
if ok {
return true
}
}
} else {
_, ok := domainsFilterList[fullDomain]
return ok
}
return false
} }
// LookupASN checks if the Autonomous system number is in a custom filter list // LookupASN checks if the Autonomous system number is in a custom filter list
@ -108,3 +130,29 @@ func LookupCountry(countryCode string) bool {
_, ok := countryCodesFilterList[countryCode] _, ok := countryCodesFilterList[countryCode]
return ok 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
}

View file

@ -64,9 +64,11 @@ var (
cfgOptionFilterLists config.StringArrayOption cfgOptionFilterLists config.StringArrayOption
cfgOptionFilterListsOrder = 34 cfgOptionFilterListsOrder = 34
// Setting "Custom Filter List" at order 35
CfgOptionFilterSubDomainsKey = "filter/includeSubdomains" CfgOptionFilterSubDomainsKey = "filter/includeSubdomains"
cfgOptionFilterSubDomains config.IntOption // security level option cfgOptionFilterSubDomains config.IntOption // security level option
cfgOptionFilterSubDomainsOrder = 35 cfgOptionFilterSubDomainsOrder = 36
// DNS Filtering. // DNS Filtering.