Jarl works out which views are reachable from the internet (match-clients
ending in a catch-all, not private-looking addresses: an internal view of a
global IPv6 prefix is still yours; a server without views: open allow-query
and a non-private listen address), with a per-view override (schema 9), and
checks public ones: rate limiting, open resolver, minimal-any, cache
snooping, open NOTIFY and transfers, version and host name, management
interfaces. Fixes are staged like any change, including rate limiting from
ISC's example in log-only mode with the secondaries exempt, then enforcing.
named logs what rate limiting limits (or would, in log-only mode, where its
counters don't move) as query-errors; Jarl routes those lines to its log
and counts them (rrl breakdown, ratelimited top lists, Prometheus
jarl_ratelimit_logged_total), shown on the new Exposure page.
A change that newly opens a public view up must be confirmed (accept_risk,
--accept-risk, a checkbox on the Changes page; 409 confirm_risk
otherwise). Findings can be acknowledged with a reason and stay visible as
info. GET /exposure, POST /exposure/fix|ack|unack, jarl exposure,
docs/exposure.md.
Also fixes query statistics skipping TSIG-signed queries ("IP#port/key
NAME" in named's log).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
4.6 KiB
Go
87 lines
4.6 KiB
Go
package stats
|
|
|
|
import (
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseResponse(t *testing.T) {
|
|
loc := time.FixedZone("test", 3600)
|
|
for _, tc := range []struct {
|
|
line string
|
|
ok bool
|
|
client, view, name, typ, rc string
|
|
}{
|
|
// Real lines from named 9.20.24 (with and without views).
|
|
{`07-Oct-2026 19:46:22.184 client @0x71358b8ff000 127.0.0.1#53937 (www.example.test): view internal: response: www.example.test IN A NOERROR 1 0 1 +E(0)K (127.0.0.1)`,
|
|
true, "127.0.0.1", "internal", "www.example.test", "A", "NOERROR"},
|
|
{`07-Oct-2026 19:46:22.195 client @0x71358b8ff000 10.10.0.37#51709 (nope.example.test): response: Nope.Example.Test IN AAAA NXDOMAIN 0 1 1 +E(0)K (127.0.0.1)`,
|
|
true, "10.10.0.37", "", "nope.example.test", "AAAA", "NXDOMAIN"},
|
|
{`07-Oct-2026 19:46:22.217 client @0x71358b8ff000 2001:db8::7#55006 (other.zone): view external net: response: other.zone. IN MX REFUSED 0 0 1 +E(0)K (2001:db8::1)`,
|
|
true, "2001:db8::7", "external net", "other.zone", "MX", "REFUSED"},
|
|
{`07-Oct-2026 19:46:22.217 client @0x71358b8ff000 ::ffff:10.0.0.1#55006 (.): response: . IN NS NOERROR 13 0 27 + (10.0.0.2)`,
|
|
true, "10.0.0.1", "", ".", "NS", "NOERROR"},
|
|
// Query lines and other messages are not responses.
|
|
{`07-Oct-2026 19:46:22.184 client @0x71358b8ff000 127.0.0.1#53937 (www.example.test): view internal: query: www.example.test IN A +E(0)K (127.0.0.1)`, false, "", "", "", "", ""},
|
|
{`07-Oct-2026 19:46:20.000 logging channel 'jarl_responses' file '/var/log/named/jarl-responses.log': rolled`, false, "", "", "", "", ""},
|
|
{``, false, "", "", "", "", ""},
|
|
} {
|
|
ev, ok := ParseResponse(tc.line, loc)
|
|
if ok != tc.ok {
|
|
t.Errorf("%q: ok = %v", tc.line, ok)
|
|
continue
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
if ev.Client.String() != tc.client || ev.View != tc.view || ev.Name != tc.name || ev.Type != tc.typ || ev.RCode != tc.rc {
|
|
t.Errorf("%q: got %+v", tc.line, ev)
|
|
}
|
|
if want := time.Date(2026, 10, 7, 19, 46, 22, 0, loc); ev.Time.Truncate(time.Second) != want {
|
|
t.Errorf("%q: time %v, want %v", tc.line, ev.Time, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseRateLimit(t *testing.T) {
|
|
loc := time.UTC
|
|
for _, tc := range []struct {
|
|
line string
|
|
want RateLimit
|
|
views bool
|
|
}{
|
|
{line: "12-Oct-2026 09:56:35.996 client @0x70a247adf000 127.0.0.2#59296 (www.rrl.test): would rate limit slip response to 127.0.0.0/24 for www.rrl.test IN A (0ce833a4)",
|
|
want: RateLimit{Would: true, Action: "slip", Prefix: "127.0.0.0/24", Name: "www.rrl.test", Type: "A"}},
|
|
{line: "12-Oct-2026 09:56:38.303 client @0x7eee390df000 127.0.0.2#35687 (www.rrl.test): rate limit drop response to 127.0.0.0/24 for www.rrl.test IN A (0ce833a4)",
|
|
want: RateLimit{Action: "drop", Prefix: "127.0.0.0/24", Name: "www.rrl.test", Type: "A"}},
|
|
{line: "12-Oct-2026 09:56:38.303 client @0x7eee390df000 2001:db8::7#35687 (Adbraun.lol): view external: rate limit drop response to 2001:db8::/56 for adbraun.lol IN ANY (0ce833a4)",
|
|
want: RateLimit{View: "external", Action: "drop", Prefix: "2001:db8::/56", Name: "adbraun.lol", Type: "ANY"}},
|
|
} {
|
|
rl, ok := ParseRateLimit(tc.line, loc)
|
|
rl.Time, rl.Client = time.Time{}, netip.Addr{}
|
|
if !ok || rl != tc.want {
|
|
t.Errorf("%s:\n got %+v %v\nwant %+v", tc.line, rl, ok, tc.want)
|
|
}
|
|
}
|
|
if _, ok := ParseRateLimit("12-Oct-2026 09:56:35.996 rate-limit: info: would limit responses to 127.0.0.0/24 for www.rrl.test IN A (0ce833a4)", loc); ok {
|
|
t.Error("the start-of-limiting line counted as a response")
|
|
}
|
|
}
|
|
|
|
// Signed queries have "/key NAME" after the client's port.
|
|
func TestParseSigned(t *testing.T) {
|
|
loc := time.UTC
|
|
ev, ok := ParseResponse("12-Oct-2026 10:11:06.490 client @0x7a22f754b000 127.0.0.1#34745/key jarl-view-external (ns1.adbraun.test): view external: response: ns1.adbraun.test IN A NOERROR 1 0 2 +E(0)SK (127.0.0.1)", loc)
|
|
if !ok || ev.View != "external" || ev.Name != "ns1.adbraun.test" || ev.Port != "34745" {
|
|
t.Errorf("signed response = %+v %v", ev, ok)
|
|
}
|
|
rl, ok := ParseRateLimit("12-Oct-2026 10:11:06.599 client @0x7a22f754b000 127.0.0.1#34856/key jarl-view-external (ns1.adbraun.test): view external: would rate limit slip response to 127.0.0.0/24 for ns1.adbraun.test IN A (e777e288)", loc)
|
|
if !ok || rl.View != "external" || !rl.Would || rl.Action != "slip" || rl.Prefix != "127.0.0.0/24" {
|
|
t.Errorf("signed rate limit = %+v %v", rl, ok)
|
|
}
|
|
if _, ok := ParseRewrite("09-Oct-2026 23:49:13.089 client @0x7bd607320000 127.0.0.1#56905/key k1 (x.ads.example): view internal: rpz QNAME NXDOMAIN rewrite x.ads.example/A/IN via x.ads.example.rules.jarl.invalid", loc); !ok {
|
|
t.Error("signed rewrite not parsed")
|
|
}
|
|
}
|