Files
mesh-catalog/modules/netcheck/cmd/netcheck/ask_test.go
T
jochen c0fa282548 netcheck: ask one named resolver, and list what listens here (hq issue 348)
During the 2026-10-09 outage the control node's resolver stayed active while it no longer listened on
its private address, and nothing on the mesh could ask that address a question or show which sockets
were bound. netcheck_dns takes server (and tcp) to ask one resolver alone and say its response code;
netcheck_listening reads the kernel's socket tables, as ss -lunt does, with the owning program where
the tool's account may see it and a sentence where it may not.
2026-10-09 13:27:39 +02:00

176 lines
5.8 KiB
Go

package main
import (
"encoding/binary"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// respond answers one query: novox.internal A 10.10.0.1 via a compressed name, anything else rcode.
func respond(q []byte, rcode byte) []byte {
end := 12
for q[end] != 0 {
end += int(q[end]) + 1
}
question := q[12 : end+5]
name := strings.ToLower(string(q[13:end]))
r := append([]byte{}, q[:2]...)
r = append(r, 0x81, 0x80|rcode, 0, 1)
answer := rcode == 0 && strings.Contains(name, "novox") && binary.BigEndian.Uint16(q[end+1:]) == 1
if answer {
r = append(r, 0, 1, 0, 0, 0, 0)
} else {
r = append(r, 0, 0, 0, 0, 0, 0)
}
r = append(r, question...)
if answer {
r = append(r, 0xC0, 12, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4, 10, 10, 0, 1)
}
return r
}
func udpResolver(t *testing.T, rcode byte) string {
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { pc.Close() })
go func() {
buf := make([]byte, 512)
for {
n, from, err := pc.ReadFrom(buf)
if err != nil {
return
}
_, _ = pc.WriteTo(respond(buf[:n], rcode), from)
}
}()
return pc.LocalAddr().String()
}
func tcpResolver(t *testing.T) string {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { l.Close() })
go func() {
for {
c, err := l.Accept()
if err != nil {
return
}
var size [2]byte
if _, err := readFull(c, size[:]); err == nil {
q := make([]byte, binary.BigEndian.Uint16(size[:]))
if _, err := readFull(c, q); err == nil {
r := respond(q, 0)
_, _ = c.Write(append(binary.BigEndian.AppendUint16(nil, uint16(len(r))), r...))
}
}
c.Close()
}
}()
return l.Addr().String()
}
// Issue 348: the question that could not be asked during the outage — does the resolver at one address answer?
func TestDNSAsksANamedResolverOverUDPAndTCPAndSaysItsResponseCode(t *testing.T) {
got, err := AskDNS("novox.internal", "A", udpResolver(t, 0), false, time.Second)
if err != nil || got.Rcode != "NOERROR" || got.Protocol != "udp" || len(got.Answers) != 1 || got.Answers[0] != "10.10.0.1" || got.Error != "" {
t.Fatalf("udp: %+v, %v", got, err)
}
got, err = AskDNS("novox.internal", "A", tcpResolver(t), true, time.Second)
if err != nil || got.Rcode != "NOERROR" || got.Protocol != "tcp" || len(got.Answers) != 1 || got.Answers[0] != "10.10.0.1" {
t.Fatalf("tcp: %+v, %v", got, err)
}
got, _ = AskDNS("nope.internal", "A", udpResolver(t, 3), false, time.Second)
if got.Rcode != "NXDOMAIN" || got.Error == "" || len(got.Answers) != 0 {
t.Fatalf("no such name is a result with its code: %+v", got)
}
got, _ = AskDNS("novox.internal", "A", udpResolver(t, 5), false, time.Second)
if got.Rcode != "REFUSED" || got.Error == "" {
t.Fatalf("a refusal: %+v", got)
}
}
func TestDNSSaysARefusedPortAndRefusesANameAsServer(t *testing.T) {
l, _ := net.Listen("tcp", "127.0.0.1:0")
addr := l.Addr().String()
l.Close()
got, err := AskDNS("novox.internal", "A", addr, true, time.Second)
if err != nil || !strings.Contains(got.Error, "connection refused") {
t.Fatalf("nothing listening is a result naming it: %+v, %v", got, err)
}
if _, err := AskDNS("novox.internal", "A", "resolver.example", false, time.Second); err == nil {
t.Fatal("a server given by name was accepted")
}
if _, err := AskDNS("novox.internal", "A", "", true, time.Second); err == nil {
t.Fatal("tcp without a server was accepted")
}
if a, _ := serverAddress("10.10.0.1"); a != "10.10.0.1:53" {
t.Fatalf("default port: %s", a)
}
}
func TestListeningReadsTheKernelsTablesAndSaysWhatItCouldNotSee(t *testing.T) {
root := t.TempDir()
must := func(err error) {
if err != nil {
t.Fatal(err)
}
}
must(os.MkdirAll(filepath.Join(root, "net"), 0o755))
head := " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n"
// 127.0.0.1:53 and 10.10.0.1:53 listening, a connection on 22 (not listening), and an udp 53 bound on loopback only.
must(os.WriteFile(filepath.Join(root, "net", "tcp"), []byte(head+
" 0: 0100007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 111 1 0 100 0 0 10 0\n"+
" 1: 01000A0A:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 112 1 0 100 0 0 10 0\n"+
" 2: 0100007F:0016 0200007F:9C40 01 00000000:00000000 00:00000000 00000000 0 0 113 1 0 100 0 0 10 0\n"), 0o644))
must(os.WriteFile(filepath.Join(root, "net", "udp"), []byte(head+
" 0: 0100007F:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 121 2 0 0\n"), 0o644))
must(os.MkdirAll(filepath.Join(root, "42", "fd"), 0o755))
must(os.WriteFile(filepath.Join(root, "42", "comm"), []byte("dnsmasq\n"), 0o644))
must(os.Symlink("socket:[111]", filepath.Join(root, "42", "fd", "5")))
got := Listening(root, 53)
if got.Count != 3 || len(got.Unread) != 0 {
t.Fatalf("%+v", got)
}
var where []string
for _, s := range got.Sockets {
where = append(where, s.Protocol+" "+s.Address)
}
if strings.Join(where, ",") != "tcp 10.10.0.1,tcp 127.0.0.1,udp 127.0.0.1" {
t.Fatalf("sockets: %v", where)
}
if got.Sockets[1].Process != "42/dnsmasq" || got.Sockets[0].Owner == "" || !strings.Contains(got.Owners, "2 socket(s)") {
t.Fatalf("owners: %+v", got)
}
if missing := Listening(t.TempDir(), 0); len(missing.Unread) != 2 {
t.Fatalf("an unreadable table is said, not left out: %+v", missing)
}
}
func TestListeningOnThisMachineFindsASocketItOpened(t *testing.T) {
if _, err := os.Stat("/proc/net/tcp"); err != nil {
t.Skip("no /proc here")
}
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
port := l.Addr().(*net.TCPAddr).Port
got := Listening("/proc", port)
if got.Count != 1 || got.Sockets[0].Address != "127.0.0.1" {
t.Fatalf("%+v", got)
}
if got.Sockets[0].Process == "" {
t.Fatalf("its own socket's program is not named: %+v", got)
}
}