diff --git a/firewall/master.go b/firewall/master.go index 0dbe21e9..fee5adba 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -616,21 +616,34 @@ matchLoop: } func checkCustomFilterList(_ context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool { - // block domains that are in the custom list + // block if the domain name appears in the custom filter list if conn.Entity.Domain != "" { if customlists.LookupDomain(conn.Entity.Domain) { - // FIXME: add proper messages - log.Debugf("Blocked %s", conn.Entity.Domain) - conn.Block("Domains appiers in the custom user list", profile.CfgOptionRemoveBlockedDNSKey) + conn.Block("Domains appears in the custom user list", customlists.CfgOptionCustomListBlockingKey) return true } } + // block if ip addresses appears in the custom filter list if conn.Entity.IP != nil { - if customlists.LookupIPv4(&conn.Entity.IP) { - // FIXME: add proper messages - log.Debugf("Blocked %s", conn.Entity.IP) - conn.Block("IP appiers in the custom user list", profile.CfgOptionBlockScopeInternetKey) + if customlists.LookupIP(&conn.Entity.IP) { + conn.Block("IP appears in the custom filter list", customlists.CfgOptionCustomListBlockingKey) + return true + } + } + + // block autonomous system by its number if it appears in the custom filter list + if conn.Entity.ASN != 0 { + if customlists.LookupASN(conn.Entity.ASN) { + conn.Block("ASN appears in the custom filter list", customlists.CfgOptionCustomListBlockingKey) + return true + } + } + + // block if the country appears in the custom filter list + if conn.Entity.Country != "" { + if customlists.LookupCountry(conn.Entity.Country) { + conn.Block("Country appears in the custom filter list", customlists.CfgOptionCustomListBlockingKey) return true } } diff --git a/intel/customlists/config.go b/intel/customlists/config.go new file mode 100644 index 00000000..f011827a --- /dev/null +++ b/intel/customlists/config.go @@ -0,0 +1,39 @@ +package customlists + +import "github.com/safing/portbase/config" + +var ( + // CfgOptionCustomListBlockingKey is the config key for the listen address.. + CfgOptionCustomListBlockingKey = "filter/customListBlocking" + cfgOptionCustomListBlockingOrder = 37 + cfgOptionCustomListCategoryAnnotation = "Filter Lists" +) + +var ( + getFilePath func() string +) + +func registerConfig() error { + // register a setting for the file path in the ui + err := config.Register(&config.Option{ + Name: "Custom Filter List", + 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", + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + ReleaseLevel: config.ReleaseLevelStable, + DefaultValue: "", + RequiresRestart: false, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: cfgOptionCustomListBlockingOrder, + config.CategoryAnnotation: cfgOptionCustomListCategoryAnnotation, + }, + }) + if err != nil { + return err + } + + getFilePath = config.GetAsString(CfgOptionCustomListBlockingKey, "") + + return nil +} diff --git a/intel/customlists/lists.go b/intel/customlists/lists.go new file mode 100644 index 00000000..c9d0f410 --- /dev/null +++ b/intel/customlists/lists.go @@ -0,0 +1,88 @@ +package customlists + +import ( + "bufio" + "net" + "os" + "strconv" + "strings" + + "github.com/miekg/dns" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/network/netutils" +) + +var ( + countryCodesFilterList map[string]struct{} + ipAddressesFilterList map[string]struct{} + autonomousSystemsFilterList map[uint]struct{} + domainsFilterList map[string]struct{} +) + +func parseFile(filePath string) error { + // open the file if possible + file, err := os.Open(filePath) + if err != nil { + log.Warningf("Custom filter: failed to parse file: \"%s\"", filePath) + return err + } + defer file.Close() + + // initialize maps to hold data from the file + countryCodesFilterList = make(map[string]struct{}) + ipAddressesFilterList = make(map[string]struct{}) + autonomousSystemsFilterList = make(map[uint]struct{}) + domainsFilterList = make(map[string]struct{}) + + // read filter file line by line + scanner := bufio.NewScanner(file) + // the scanner will error out if the line is greater than 64K, in this case it is enough + for scanner.Scan() { + parseLine(scanner.Text()) + } + + // check for scanner error + if err := scanner.Err(); err != nil { + return err + } + + log.Infof("Custom filter: list loaded successful: %s", filePath) + + return nil +} + +func parseLine(line string) { + // ignore empty lines and comment lines + if len(line) == 0 || line[0] == '#' { + return + } + + // everything after the first field will be ignored + field := strings.Fields(line)[0] + + // check if it'a a country code + if isCountryCode(field) { + countryCodesFilterList[field] = struct{}{} + } + + // try to parse IP address + ip := net.ParseIP(field) + if ip != nil { + ipAddressesFilterList[ip.String()] = struct{}{} + } + + // check if it's a Autonomous system (example AS123) + if isAutonomousSystem(field) { + asNumber, err := strconv.ParseUint(field[2:], 10, 32) + if err != nil { + return + } + autonomousSystemsFilterList[uint(asNumber)] = struct{}{} + } + + // check if it's a domain + domain := dns.Fqdn(field) + if netutils.IsValidFqdn(domain) { + domainsFilterList[domain] = struct{}{} + } +} diff --git a/intel/customlists/module.go b/intel/customlists/module.go index 129c01c8..2d5feba6 100644 --- a/intel/customlists/module.go +++ b/intel/customlists/module.go @@ -1,112 +1,110 @@ package customlists import ( - "bufio" + "context" "net" "os" "regexp" - "strings" + "time" - "github.com/miekg/dns" - "github.com/safing/portbase/log" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/network/netutils" ) var module *modules.Module +const configChangeEvent = "config change" + // Helper variables for parsing the input file var ( - countryCodes = map[string]struct{}{"AF": {}, "AX": {}, "AL": {}, "DZ": {}, "AS": {}, "AD": {}, "AO": {}, "AI": {}, "AQ": {}, "AG": {}, "AR": {}, "AM": {}, "AW": {}, "AU": {}, "AT": {}, "AZ": {}, "BH": {}, "BS": {}, "BD": {}, "BB": {}, "BY": {}, "BE": {}, "BZ": {}, "BJ": {}, "BM": {}, "BT": {}, "BO": {}, "BQ": {}, "BA": {}, "BW": {}, "BV": {}, "BR": {}, "IO": {}, "BN": {}, "BG": {}, "BF": {}, "BI": {}, "KH": {}, "CM": {}, "CA": {}, "CV": {}, "KY": {}, "CF": {}, "TD": {}, "CL": {}, "CN": {}, "CX": {}, "CC": {}, "CO": {}, "KM": {}, "CG": {}, "CD": {}, "CK": {}, "CR": {}, "CI": {}, "HR": {}, "CU": {}, "CW": {}, "CY": {}, "CZ": {}, "DK": {}, "DJ": {}, "DM": {}, "DO": {}, "EC": {}, "EG": {}, "SV": {}, "GQ": {}, "ER": {}, "EE": {}, "ET": {}, "FK": {}, "FO": {}, "FJ": {}, "FI": {}, "FR": {}, "GF": {}, "PF": {}, "TF": {}, "GA": {}, "GM": {}, "GE": {}, "DE": {}, "GH": {}, "GI": {}, "GR": {}, "GL": {}, "GD": {}, "GP": {}, "GU": {}, "GT": {}, "GG": {}, "GN": {}, "GW": {}, "GY": {}, "HT": {}, "HM": {}, "VA": {}, "HN": {}, "HK": {}, "HU": {}, "IS": {}, "IN": {}, "ID": {}, "IR": {}, "IQ": {}, "IE": {}, "IM": {}, "IL": {}, "IT": {}, "JM": {}, "JP": {}, "JE": {}, "JO": {}, "KZ": {}, "KE": {}, "KI": {}, "KP": {}, "KR": {}, "KW": {}, "KG": {}, "LA": {}, "LV": {}, "LB": {}, "LS": {}, "LR": {}, "LY": {}, "LI": {}, "LT": {}, "LU": {}, "MO": {}, "MK": {}, "MG": {}, "MW": {}, "MY": {}, "MV": {}, "ML": {}, "MT": {}, "MH": {}, "MQ": {}, "MR": {}, "MU": {}, "YT": {}, "MX": {}, "FM": {}, "MD": {}, "MC": {}, "MN": {}, "ME": {}, "MS": {}, "MA": {}, "MZ": {}, "MM": {}, "NA": {}, "NR": {}, "NP": {}, "NL": {}, "NC": {}, "NZ": {}, "NI": {}, "NE": {}, "NG": {}, "NU": {}, "NF": {}, "MP": {}, "NO": {}, "OM": {}, "PK": {}, "PW": {}, "PS": {}, "PA": {}, "PG": {}, "PY": {}, "PE": {}, "PH": {}, "PN": {}, "PL": {}, "PT": {}, "PR": {}, "QA": {}, "RE": {}, "RO": {}, "RU": {}, "RW": {}, "BL": {}, "SH": {}, "KN": {}, "LC": {}, "MF": {}, "PM": {}, "VC": {}, "WS": {}, "SM": {}, "ST": {}, "SA": {}, "SN": {}, "RS": {}, "SC": {}, "SL": {}, "SG": {}, "SX": {}, "SK": {}, "SI": {}, "SB": {}, "SO": {}, "ZA": {}, "GS": {}, "SS": {}, "ES": {}, "LK": {}, "SD": {}, "SR": {}, "SJ": {}, "SZ": {}, "SE": {}, "CH": {}, "SY": {}, "TW": {}, "TJ": {}, "TZ": {}, "TH": {}, "TL": {}, "TG": {}, "TK": {}, "TO": {}, "TT": {}, "TN": {}, "TR": {}, "TM": {}, "TC": {}, "TV": {}, "UG": {}, "UA": {}, "AE": {}, "GB": {}, "US": {}, "UM": {}, "UY": {}, "UZ": {}, "VU": {}, "VE": {}, "VN": {}, "VG": {}, "VI": {}, "WF": {}, "EH": {}, "YE": {}, "ZM": {}, "ZW": {}} + isCountryCode = regexp.MustCompile("^[A-Z]{2}$").MatchString isAutonomousSystem = regexp.MustCompile(`^AS[0-9]+$`).MatchString ) var ( - filteredCountryCodes map[string]struct{} - filteredIPAddresses map[string]struct{} - filteredAutonomousSystems map[string]struct{} - filteredDomains map[string]struct{} + filterListFilePath string + filterListFileModifiedTime time.Time ) func init() { - module = modules.Register("customlists", prep, nil, nil, "base") + module = modules.Register("customlists", prep, start, nil, "base") } func prep() error { - filteredCountryCodes = make(map[string]struct{}) - filteredIPAddresses = make(map[string]struct{}) - filteredAutonomousSystems = make(map[string]struct{}) - filteredDomains = make(map[string]struct{}) - - file, err := os.Open("/home/vladimir/Dev/Safing/filterlists/custom.txt") + // register the config in the ui + err := registerConfig() if err != nil { return err } - defer file.Close() - // read filter file line by line - scanner := bufio.NewScanner(file) - // the scanner will error out if the line is greater than 64K, in this case it is enough - for scanner.Scan() { - parseFilterLine(scanner.Text()) - } - - if err := scanner.Err(); err != nil { + // register to hook to update after config change. + if err := module.RegisterEventHook( + module.Name, + configChangeEvent, + "update custom filter list", + func(ctx context.Context, obj interface{}) error { + _ = checkAndUpdateFilterList() + return nil + }, + ); err != nil { return err } - log.Criticalf("filteredCountryCodes: %v", filteredCountryCodes) - log.Criticalf("filteredIPAddresses: %v", filteredIPAddresses) - log.Criticalf("filteredAutonomousSystems: %v", filteredAutonomousSystems) - log.Criticalf("filteredDomains: %v", filteredDomains) - return nil } -func parseFilterLine(line string) { - // ignore empty lines and comment lines - if len(line) == 0 || line[0] == '#' { - return - } +func start() error { + // 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 { + _ = checkAndUpdateFilterList() + return nil + }).Repeat(10 * time.Minute) - fields := strings.Fields(line) - - // everything after the first field will be ignored - firstField := fields[0] - - // check if it'a a country code - if _, ok := countryCodes[firstField]; ok { - filteredCountryCodes[firstField] = struct{}{} - } - - // try to parse IP address - ip := net.ParseIP(firstField) - if ip != nil { - filteredIPAddresses[ip.String()] = struct{}{} - } - - // check if it's a Autonomous system (example AS123) - if isAutonomousSystem(firstField) { - filteredAutonomousSystems[firstField] = struct{}{} - } - - // check if it's a domain - potentialDomain := dns.Fqdn(firstField) - if netutils.IsValidFqdn(potentialDomain) { - filteredDomains[potentialDomain] = struct{}{} - } + // parse the file for the first time at start + _ = parseFile(getFilePath()) + return nil } -// LookupIPv4 checks if the IP is in a custom filter list -func LookupIPv4(ip *net.IP) bool { - log.Debugf("Checking ip %s", ip.String()) - _, ok := filteredIPAddresses[ip.String()] +func checkAndUpdateFilterList() error { + // get path and try to get its info + filePath := getFilePath() + fileInfo, err := os.Stat(filePath) + if err != nil { + return 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 nil + } + filterListFileModifiedTime = modifiedTime + filterListFilePath = filePath + } + return nil +} + +// LookupIP checks if the IP address is in a custom filter list +func LookupIP(ip *net.IP) bool { + _, ok := ipAddressesFilterList[ip.String()] return ok } // LookupDomain checks if the Domain is in a custom filter list func LookupDomain(domain string) bool { - log.Debugf("Checking domain %s", domain) - _, ok := filteredDomains[domain] + _, ok := domainsFilterList[domain] + return ok +} + +// LookupASN checks if the Autonomous system number is in a custom filter list +func LookupASN(number uint) bool { + _, ok := autonomousSystemsFilterList[number] + return ok +} + +// LookupCountry checks if the country code is in a custom filter list +func LookupCountry(countryCode string) bool { + _, ok := countryCodesFilterList[countryCode] return ok }