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 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-11 18:35:06 +01:00
1 parent 9d9ae37367
commit 485139161d
4 files changed
+116 -9

No files matched your search

+9
View File
@@ -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
+3 -1
View File
@@ -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
+46
View File
@@ -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")
}
}
+58 -8
View File
@@ -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() {
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) {