package stats import ( "net/netip" "regexp" "strings" "time" ) // Event is one response named logged. type Event struct { Time time.Time Client netip.Addr Port string // the client's port: with Client and Name, it ties an RPZ rewrite to its response View string // "" without views Name string // lowercase, without the final dot ("." for the root) Type string // e.g. A, AAAA, PTR RCode string // e.g. NOERROR, NXDOMAIN } // Rewrite is a response policy (RPZ) rewrite named logged: blocking. type Rewrite struct { Time time.Time Client netip.Addr Port string View string Name string // the query name, normalized like Event.Name Action string // NXDOMAIN, NODATA, PASSTHRU, DROP, ... Via string // the policy zone entry: OWNER.ZONE } // responseRe matches named's response log lines (BIND 9.20, print-time): // // 07-Oct-2026 19:46:22.195 client @0x7135... 10.10.0.37#51709 (nas.adbraun.lol): view internal: response: nas.adbraun.lol IN A NOERROR 1 0 1 +E(0)K (10.10.0.2) // // The "view NAME: " part is missing when no views are configured. var responseRe = regexp.MustCompile(`^(\d{2}-[A-Za-z]{3}-\d{4} \d{2}:\d{2}:\d{2}\.\d{3}) client @0x[0-9a-fA-F]+ (\S+)#(\d+) \(.*?\): (?:view (.+?): )?response: (\S+) (\S+) (\S+) ([A-Z0-9]+) `) // rewriteRe matches named's RPZ rewrite lines (category rpz), logged just // before the response line of the same query: // // 09-Oct-2026 23:49:13.698 client @0x7135... 10.10.0.37#54468 (ads.example): view internal: rpz QNAME NXDOMAIN rewrite ads.example/A/IN via ads.example.hagezi.blocklist.jarl.invalid var rewriteRe = regexp.MustCompile(`^(\d{2}-[A-Za-z]{3}-\d{4} \d{2}:\d{2}:\d{2}\.\d{3}) client @0x[0-9a-fA-F]+ (\S+)#(\d+) \((.*?)\): (?:view (.+?): )?rpz \S+ (\S+) rewrite \S+ via (\S+)`) const logTimeLayout = "02-Jan-2006 15:04:05.000" // ParseResponse parses a response log line. ok is false for other lines // (named also logs to the channel when it starts and rotates files). func ParseResponse(line string, loc *time.Location) (ev Event, ok bool) { m := responseRe.FindStringSubmatch(line) if m == nil { return ev, false } t, err := time.ParseInLocation(logTimeLayout, m[1], loc) if err != nil { return ev, false } client, err := netip.ParseAddr(m[2]) if err != nil { return ev, false } return Event{ Time: t, Client: client.Unmap(), Port: m[3], View: m[4], Name: normalizeName(m[5]), Type: m[7], RCode: m[8], }, true } // ParseRewrite parses an RPZ rewrite log line. func ParseRewrite(line string, loc *time.Location) (rw Rewrite, ok bool) { m := rewriteRe.FindStringSubmatch(line) if m == nil { return rw, false } t, err := time.ParseInLocation(logTimeLayout, m[1], loc) if err != nil { return rw, false } client, err := netip.ParseAddr(m[2]) if err != nil { return rw, false } return Rewrite{Time: t, Client: client.Unmap(), Port: m[3], Name: normalizeName(m[4]), View: m[5], Action: m[6], Via: strings.ToLower(m[7])}, true } func normalizeName(n string) string { n = strings.ToLower(n) if n == "." { return n } return strings.TrimSuffix(n, ".") }