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.
253 lines
7.1 KiB
Go
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
|
|
}
|