Adds Pi-hole-style DNS filtering using dnsmasq's native address=/ directives, which block domains and all subdomains. Users subscribe to blocklists by URL or upload files, with suggested lists from Hagezi, Steven Black, and OISD. Background runner refreshes lists on a configurable interval (default 24h).
90 lines
2.2 KiB
Go
90 lines
2.2 KiB
Go
package dnsfilter
|
|
|
|
import (
|
|
"bufio"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// ParseLine extracts a domain from a single blocklist line.
|
|
// Handles hosts format (0.0.0.0 domain), dnsmasq format (address=/domain/...),
|
|
// and plain domain lists. Returns empty string and false for unparseable lines.
|
|
func ParseLine(line string) (string, bool) {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || line[0] == '#' || line[0] == '!' {
|
|
return "", false
|
|
}
|
|
|
|
// dnsmasq format: address=/domain/0.0.0.0 or server=/domain/
|
|
if strings.HasPrefix(line, "address=/") || strings.HasPrefix(line, "server=/") {
|
|
parts := strings.SplitN(line, "/", 4)
|
|
if len(parts) >= 3 && parts[1] != "" {
|
|
return validateDomain(parts[1])
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// Hosts format: 0.0.0.0 domain or 127.0.0.1 domain
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 2 {
|
|
ip := fields[0]
|
|
if ip == "0.0.0.0" || ip == "127.0.0.1" || ip == "::1" || ip == "::0" {
|
|
domain := fields[1]
|
|
// Strip inline comments
|
|
if i := strings.IndexByte(domain, '#'); i >= 0 {
|
|
domain = domain[:i]
|
|
}
|
|
return validateDomain(domain)
|
|
}
|
|
}
|
|
|
|
// Plain domain format: one domain per line
|
|
if len(fields) == 1 {
|
|
return validateDomain(fields[0])
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// validateDomain checks that a string looks like a valid domain for blocking.
|
|
func validateDomain(d string) (string, bool) {
|
|
d = strings.ToLower(strings.TrimSpace(d))
|
|
if d == "" || !strings.Contains(d, ".") {
|
|
return "", false
|
|
}
|
|
|
|
// Reject known non-domains
|
|
switch d {
|
|
case "localhost", "localhost.localdomain", "local", "broadcasthost",
|
|
"ip6-localhost", "ip6-loopback", "ip6-localnet",
|
|
"ip6-mcastprefix", "ip6-allnodes", "ip6-allrouters":
|
|
return "", false
|
|
}
|
|
|
|
// Reject IP addresses
|
|
if net.ParseIP(d) != nil {
|
|
return "", false
|
|
}
|
|
|
|
return d, true
|
|
}
|
|
|
|
// ParseFile reads a cached blocklist file and returns all unique domains.
|
|
func ParseFile(path string) (map[string]struct{}, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
domains := make(map[string]struct{})
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
if domain, ok := ParseLine(scanner.Text()); ok {
|
|
domains[domain] = struct{}{}
|
|
}
|
|
}
|
|
return domains, scanner.Err()
|
|
}
|