A VPN client rewrote the laptop's resolver file and every mesh name failed while each module read healthy: nothing asked the machine. The engine now looks every 30 s at the resolver file the uplink holder declared (naming the program that rewrote it), the names through each listed resolver (NXDOMAIN for a mesh name's IPv6 address is a finding, issue 262), the tunnel's handshake with the hub, the bus and the default route; a part is unhealthy on its second failing look, and the statement carries it.
156 lines
4.0 KiB
Go
156 lines
4.0 KiB
Go
package network
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The record types a look asks for.
|
|
const (
|
|
TypeA uint16 = 1
|
|
TypeAAAA uint16 = 28
|
|
)
|
|
|
|
// The answers a resolver gives that a look tells apart.
|
|
const (
|
|
RcodeOK = 0
|
|
RcodeServFail = 2
|
|
RcodeNXDomain = 3
|
|
RcodeRefused = 5
|
|
)
|
|
|
|
// Answer is what one resolver said to one question: its code, how many records of the type asked it
|
|
// gave, and how long it took. A question with no answer at all is an error, never an Answer.
|
|
type Answer struct {
|
|
Rcode int
|
|
Records int
|
|
Took time.Duration
|
|
}
|
|
|
|
// Ask asks one resolver one question over UDP, and reads its code and how many records of the type
|
|
// asked it answered with. **It tells "no such name" from "no record of that type"** — the C library's
|
|
// lookup does not, and that difference is issue 262: an Alpine container failed a mesh name because a
|
|
// resolver said NXDOMAIN for its IPv6 address where it should have said there was none.
|
|
func Ask(ctx context.Context, server, name string, qtype uint16, timeout time.Duration) (Answer, error) {
|
|
start := time.Now()
|
|
query, id, err := question(name, qtype)
|
|
if err != nil {
|
|
return Answer{}, err
|
|
}
|
|
if net.ParseIP(server) != nil {
|
|
server = net.JoinHostPort(server, "53")
|
|
}
|
|
d := net.Dialer{Timeout: timeout}
|
|
conn, err := d.DialContext(ctx, "udp", server)
|
|
if err != nil {
|
|
return Answer{}, err
|
|
}
|
|
defer conn.Close()
|
|
_ = conn.SetDeadline(start.Add(timeout))
|
|
if _, err := conn.Write(query); err != nil {
|
|
return Answer{}, err
|
|
}
|
|
buf := make([]byte, 1500)
|
|
for {
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
var ne net.Error
|
|
if errors.As(err, &ne) && ne.Timeout() {
|
|
return Answer{}, fmt.Errorf("no answer within %s", timeout)
|
|
}
|
|
return Answer{}, err
|
|
}
|
|
a, ours, err := parse(buf[:n], id, qtype)
|
|
if !ours {
|
|
continue // a late answer to somebody else's question on this port
|
|
}
|
|
if err != nil {
|
|
return Answer{}, err
|
|
}
|
|
a.Took = time.Since(start)
|
|
return a, nil
|
|
}
|
|
}
|
|
|
|
// question is one query: a header asking for recursion, and the name and type.
|
|
func question(name string, qtype uint16) ([]byte, uint16, error) {
|
|
var idb [2]byte
|
|
if _, err := rand.Read(idb[:]); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
id := binary.BigEndian.Uint16(idb[:])
|
|
msg := make([]byte, 12, 64)
|
|
binary.BigEndian.PutUint16(msg[0:], id)
|
|
msg[2] = 0x01 // recursion desired
|
|
binary.BigEndian.PutUint16(msg[4:], 1)
|
|
for _, label := range strings.Split(strings.TrimSuffix(name, "."), ".") {
|
|
if label == "" || len(label) > 63 {
|
|
return nil, 0, fmt.Errorf("%q is not a name that can be asked", name)
|
|
}
|
|
msg = append(msg, byte(len(label)))
|
|
msg = append(msg, label...)
|
|
}
|
|
msg = append(msg, 0, byte(qtype>>8), byte(qtype), 0, 1)
|
|
return msg, id, nil
|
|
}
|
|
|
|
// parse reads an answer: false when it is not the answer to this question.
|
|
func parse(msg []byte, id, qtype uint16) (Answer, bool, error) {
|
|
if len(msg) < 12 || binary.BigEndian.Uint16(msg[0:]) != id || msg[2]&0x80 == 0 {
|
|
return Answer{}, false, nil
|
|
}
|
|
a := Answer{Rcode: int(msg[3] & 0x0f)}
|
|
qd, an := int(binary.BigEndian.Uint16(msg[4:])), int(binary.BigEndian.Uint16(msg[6:]))
|
|
at := 12
|
|
for i := 0; i < qd; i++ {
|
|
var err error
|
|
if at, err = skipName(msg, at); err != nil {
|
|
return a, true, err
|
|
}
|
|
at += 4
|
|
}
|
|
for i := 0; i < an; i++ {
|
|
var err error
|
|
if at, err = skipName(msg, at); err != nil {
|
|
return a, true, err
|
|
}
|
|
if at+10 > len(msg) {
|
|
return a, true, errors.New("the answer is cut short")
|
|
}
|
|
typ := binary.BigEndian.Uint16(msg[at:])
|
|
length := int(binary.BigEndian.Uint16(msg[at+8:]))
|
|
at += 10 + length
|
|
if at > len(msg) {
|
|
return a, true, errors.New("the answer is cut short")
|
|
}
|
|
if typ == qtype {
|
|
a.Records++
|
|
}
|
|
}
|
|
return a, true, nil
|
|
}
|
|
|
|
// skipName steps over a name, compressed or not.
|
|
func skipName(msg []byte, at int) (int, error) {
|
|
for {
|
|
if at >= len(msg) {
|
|
return 0, errors.New("the answer is cut short")
|
|
}
|
|
l := int(msg[at])
|
|
switch {
|
|
case l == 0:
|
|
return at + 1, nil
|
|
case l&0xc0 == 0xc0:
|
|
return at + 2, nil
|
|
default:
|
|
at += 1 + l
|
|
}
|
|
}
|
|
}
|