Add DNS filtering with dnsmasq address=/ directives for wildcard blocking
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).
This commit is contained in:
89
internal/dnsfilter/parser.go
Normal file
89
internal/dnsfilter/parser.go
Normal file
@@ -0,0 +1,89 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user