From ecffc2d5c1a6190d04dc41295170b8fc25fa4f5b Mon Sep 17 00:00:00 2001 From: adam Date: Sun, 11 Oct 2026 18:35:06 +0100 Subject: [PATCH] Let jarld recognize this host's addresses under systemd The unit's RestrictAddressFamilies lacked AF_NETLINK, which listing the host's addresses needs, so the name server checks didn't know this server's own addresses: the Name servers tab queried them like another server's, and the delegation check didn't sign them with the view key. Allow AF_NETLINK, and fall back to /proc/net without it. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 ++++ deploy/systemd/jarl.service | 4 +- internal/nscheck/localaddr_test.go | 46 ++++++++++++++++++++ internal/nscheck/nscheck.go | 68 ++++++++++++++++++++++++++---- 4 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 internal/nscheck/localaddr_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e24c79..f22e4d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,15 @@ All notable changes to Jarl are documented here. The format follows ### Fixed +- Under the shipped systemd unit, jarld couldn't list the host's + addresses (it needs a netlink socket, which `RestrictAddressFamilies` + didn't allow), so the name server checks didn't recognize this server's + own addresses. The Name servers tab then queried them like another + server's, and the delegation check didn't sign those queries with the + view's key: with views, both got the view matching the server's own + address (usually internal) and reported a wrong serial. The unit now + allows `AF_NETLINK`, and without it jarld reads the addresses from + /proc. - Statistics: after a reconfig that added, removed or recreated views (creating the first view, for instance), a minute could count everything since named started: millions of queries in an hour. A per-view counter diff --git a/deploy/systemd/jarl.service b/deploy/systemd/jarl.service index c09f398..ad8fc01 100644 --- a/deploy/systemd/jarl.service +++ b/deploy/systemd/jarl.service @@ -55,7 +55,9 @@ RestrictRealtime=yes RestrictSUIDSGID=yes LockPersonality=yes MemoryDenyWriteExecute=yes -RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +# AF_NETLINK lets jarld list this host's addresses, so its name server +# checks know which addresses are its own. +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK SystemCallArchitectures=native # @system-service includes @chown, which jarld needs to give files to group bind. SystemCallFilter=@system-service diff --git a/internal/nscheck/localaddr_test.go b/internal/nscheck/localaddr_test.go new file mode 100644 index 0000000..a15e4e0 --- /dev/null +++ b/internal/nscheck/localaddr_test.go @@ -0,0 +1,46 @@ +package nscheck + +import ( + "net/netip" + "os" + "path/filepath" + "slices" + "testing" +) + +// Without netlink (a sandboxed jarld), the host's addresses come from /proc. +func TestProcAddrs(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "if_inet6"), []byte( + "00000000000000000000000000000001 01 80 10 80 lo\n"+ + "2a0280137f3d00100000000000000005 02 40 00 80 eth0\n"), 0o644) + os.WriteFile(filepath.Join(dir, "fib_trie"), []byte(`Main: + +-- 0.0.0.0/0 3 0 5 + |-- 0.0.0.0 + /0 universe UNICAST + +-- 10.0.10.0/24 2 0 2 + |-- 10.0.10.0 + /24 link UNICAST + |-- 10.0.10.5 + /32 host LOCAL + |-- 10.0.10.255 + /32 link BROADCAST +Local: + +-- 0.0.0.0/0 3 0 5 + |-- 10.0.10.5 + /32 host LOCAL +`), 0o644) + got := procAddrs(dir) + for _, want := range []string{"::1", "2a02:8013:7f3d:10::5", "10.0.10.5"} { + if !slices.Contains(got, netip.MustParseAddr(want)) { + t.Errorf("%s missing from %v", want, got) + } + } + if len(got) != 3 { + t.Errorf("got %v (10.0.10.5 once, no broadcast or network addresses)", got) + } + // And the real ones on this machine: at least loopback. + if len(procAddrs("/proc/net")) == 0 { + t.Log("no addresses in /proc/net here") + } +} diff --git a/internal/nscheck/nscheck.go b/internal/nscheck/nscheck.go index 60c8ecc..89dcd57 100644 --- a/internal/nscheck/nscheck.go +++ b/internal/nscheck/nscheck.go @@ -15,10 +15,13 @@ package nscheck import ( "context" + "encoding/hex" "errors" "fmt" "net" "net/netip" + "os" + "path/filepath" "sort" "strconv" "strings" @@ -257,20 +260,67 @@ func IsLocalAddr(a netip.Addr) bool { if a.IsLoopback() { return true } - addrs, err := net.InterfaceAddrs() - if err != nil { - return false - } - for _, ia := range addrs { - if n, ok := ia.(*net.IPNet); ok { - if b, ok := netip.AddrFromSlice(n.IP); ok && b.Unmap() == a.Unmap() { - return true - } + for _, b := range LocalAddrs() { + if b == a.Unmap() { + return true } } return false } +// LocalAddrs returns this host's interface addresses. Listing them needs a +// netlink socket; where that is not allowed (a systemd unit without +// AF_NETLINK in RestrictAddressFamilies), they are read from /proc. +func LocalAddrs() []netip.Addr { + var out []netip.Addr + if addrs, err := net.InterfaceAddrs(); err == nil { + for _, ia := range addrs { + if n, ok := ia.(*net.IPNet); ok { + if b, ok := netip.AddrFromSlice(n.IP); ok { + out = append(out, b.Unmap()) + } + } + } + return out + } + return procAddrs("/proc/net") +} + +// procAddrs reads the host's addresses from /proc/net: IPv6 from +// if_inet6, IPv4 from the local (/32 host LOCAL) entries of fib_trie. +func procAddrs(dir string) []netip.Addr { + var out []netip.Addr + if b, err := os.ReadFile(filepath.Join(dir, "if_inet6")); err == nil { + for _, line := range strings.Split(string(b), "\n") { + f := strings.Fields(line) + if len(f) == 0 || len(f[0]) != 32 { + continue + } + raw, err := hex.DecodeString(f[0]) + if err != nil { + continue + } + if a, ok := netip.AddrFromSlice(raw); ok { + out = append(out, a) + } + } + } + if b, err := os.ReadFile(filepath.Join(dir, "fib_trie")); err == nil { + seen := map[netip.Addr]bool{} + var last netip.Addr + for _, line := range strings.Split(string(b), "\n") { + t := strings.TrimSpace(line) + if strings.HasPrefix(t, "|-- ") { + last, _ = netip.ParseAddr(strings.TrimPrefix(t, "|-- ")) + } else if strings.HasPrefix(t, "/32 host LOCAL") && last.IsValid() && !seen[last] { + seen[last] = true + out = append(out, last) + } + } + } + return out +} + // SystemResolver returns the first nameserver in /etc/resolv.conf as // host:port. func SystemResolver() (string, error) {