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.
176 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|