Files
mesh-catalog/modules/netcheck/cmd/netcheck/ask.go
T
jochen 0f6986aead 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 15:04:45 +02:00

253 lines
7.1 KiB
Go

package main
// Asking one named resolver directly (hq issue 348). netcheck_dns without a server asks the machine's
// resolver the way its programs do — resolv.conf, hosts, search domains, several servers in turn — which
// answers "can this machine resolve?" and hides which resolver answered. During the 2026-10-09 outage the
// question was "does the resolver at 10.10.0.1 answer?", and nothing on the mesh could ask it. Asked with a
// server, the question goes to that address and port alone, over UDP or TCP, and the answer says its
// response code: a refusal, no such name, and no answer at all are three different findings.
//
// The wire format is written here rather than taken from a library: a question and the records of five
// types are all it needs, and the module stays without a dependency.
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
)
var dnsTypeCodes = map[string]uint16{"A": 1, "CNAME": 5, "MX": 15, "TXT": 16, "AAAA": 28}
var rcodeWords = map[int]string{0: "NOERROR", 1: "FORMERR", 2: "SERVFAIL", 3: "NXDOMAIN", 4: "NOTIMP", 5: "REFUSED"}
// serverAddress is a server as asked — an address, with a port or without (53) — as host:port.
func serverAddress(server string) (string, error) {
server = strings.TrimSpace(server)
if host, port, err := net.SplitHostPort(server); err == nil {
p, err := strconv.Atoi(port)
if err != nil || p < 1 || p > 65535 {
return "", fmt.Errorf("server %q: the port is not one (1-65535)", server)
}
if net.ParseIP(host) == nil {
return "", fmt.Errorf("server %q: an address, not a name — a resolver is asked by the address it listens on", server)
}
return net.JoinHostPort(host, port), nil
}
if net.ParseIP(server) == nil {
return "", fmt.Errorf("server %q: an address, not a name — a resolver is asked by the address it listens on", server)
}
return net.JoinHostPort(server, "53"), nil
}
// askServer asks one resolver one question and reads its answer.
func askServer(server, name, kind string, overTCP bool, timeout time.Duration) (rcode int, answers []string, err error) {
addr, err := serverAddress(server)
if err != nil {
return 0, nil, err
}
query, id, err := buildQuery(name, dnsTypeCodes[kind])
if err != nil {
return 0, nil, err
}
network := "udp"
if overTCP {
network = "tcp"
}
conn, err := net.DialTimeout(network, addr, timeout)
if err != nil {
return 0, nil, err
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(timeout))
var reply []byte
if overTCP {
framed := binary.BigEndian.AppendUint16(nil, uint16(len(query)))
if _, err := conn.Write(append(framed, query...)); err != nil {
return 0, nil, err
}
var size [2]byte
if _, err := readFull(conn, size[:]); err != nil {
return 0, nil, err
}
reply = make([]byte, binary.BigEndian.Uint16(size[:]))
if _, err := readFull(conn, reply); err != nil {
return 0, nil, err
}
} else {
if _, err := conn.Write(query); err != nil {
return 0, nil, err
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
return 0, nil, err
}
reply = buf[:n]
}
return parseReply(reply, id, dnsTypeCodes[kind])
}
func readFull(conn net.Conn, b []byte) (int, error) {
got := 0
for got < len(b) {
n, err := conn.Read(b[got:])
got += n
if err != nil {
return got, err
}
}
return got, nil
}
// buildQuery is one question, recursion desired, under a random id.
func buildQuery(name string, qtype uint16) ([]byte, uint16, error) {
var idb [2]byte
_, _ = rand.Read(idb[:])
id := binary.BigEndian.Uint16(idb[:])
msg := binary.BigEndian.AppendUint16(nil, id)
msg = append(msg, 0x01, 0x00) // recursion desired
msg = append(msg, 0, 1, 0, 0, 0, 0, 0, 0) // one question
wire, err := encodeName(name)
if err != nil {
return nil, 0, err
}
msg = append(msg, wire...)
msg = binary.BigEndian.AppendUint16(msg, qtype)
msg = binary.BigEndian.AppendUint16(msg, 1) // IN
return msg, id, nil
}
func encodeName(name string) ([]byte, error) {
name = strings.TrimSuffix(strings.TrimSpace(name), ".")
var out []byte
if name != "" {
for _, label := range strings.Split(name, ".") {
if label == "" || len(label) > 63 {
return nil, fmt.Errorf("name %q has an empty or too long label", name)
}
out = append(out, byte(len(label)))
out = append(out, label...)
}
}
return append(out, 0), nil
}
var errShort = errors.New("the answer is cut short")
// readName reads a possibly compressed name at off, and the offset after it.
func readName(msg []byte, off int) (string, int, error) {
var labels []string
next, jumped := -1, 0
for {
if off >= len(msg) {
return "", 0, errShort
}
l := int(msg[off])
switch {
case l == 0:
if next < 0 {
next = off + 1
}
return strings.Join(labels, "."), next, nil
case l&0xC0 == 0xC0:
if off+1 >= len(msg) {
return "", 0, errShort
}
if next < 0 {
next = off + 2
}
if jumped++; jumped > 32 {
return "", 0, errors.New("the answer's names point in a loop")
}
off = int(binary.BigEndian.Uint16(msg[off:]) & 0x3FFF)
default:
if off+1+l > len(msg) {
return "", 0, errShort
}
labels = append(labels, string(msg[off+1:off+1+l]))
off += 1 + l
}
}
}
// parseReply reads the response code and the answers of the asked type.
func parseReply(msg []byte, id, qtype uint16) (int, []string, error) {
if len(msg) < 12 {
return 0, nil, errShort
}
if binary.BigEndian.Uint16(msg) != id {
return 0, nil, errors.New("the answer is to another question (its id differs)")
}
if msg[2]&0x80 == 0 {
return 0, nil, errors.New("the answer is not marked as one")
}
rcode := int(msg[3] & 0x0F)
qd, an := int(binary.BigEndian.Uint16(msg[4:])), int(binary.BigEndian.Uint16(msg[6:]))
off := 12
for i := 0; i < qd; i++ {
_, n, err := readName(msg, off)
if err != nil {
return rcode, nil, err
}
off = n + 4
}
answers := []string{}
for i := 0; i < an; i++ {
_, n, err := readName(msg, off)
if err != nil {
return rcode, answers, err
}
off = n
if off+10 > len(msg) {
return rcode, answers, errShort
}
rtype := binary.BigEndian.Uint16(msg[off:])
size := int(binary.BigEndian.Uint16(msg[off+8:]))
data := off + 10
if data+size > len(msg) {
return rcode, answers, errShort
}
rdata := msg[data : data+size]
off = data + size
if rtype != qtype {
continue // a CNAME on the way to an address, say
}
switch rtype {
case 1, 28:
answers = append(answers, net.IP(rdata).String())
case 5:
s, _, err := readName(msg, data)
if err != nil {
return rcode, answers, err
}
answers = append(answers, s)
case 15:
if size < 3 {
return rcode, answers, errShort
}
s, _, err := readName(msg, data+2)
if err != nil {
return rcode, answers, err
}
answers = append(answers, fmt.Sprintf("%d %s", binary.BigEndian.Uint16(rdata), s))
case 16:
var parts []string
for p := 0; p < len(rdata); {
l := int(rdata[p])
if p+1+l > len(rdata) {
return rcode, answers, errShort
}
parts = append(parts, string(rdata[p+1:p+1+l]))
p += 1 + l
}
answers = append(answers, strings.Join(parts, ""))
}
}
return rcode, answers, nil
}