mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-27 08:46:49 +09:00
Exact appends and deletes; values go 10m after last use or on Close. Limits per node, one lock for provider writes. Waits for every authoritative NS and holds a new value until the CA cache has expired. Updates #2137
155 lines
4.0 KiB
Go
155 lines
4.0 KiB
Go
package dns
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
mdns "github.com/miekg/dns"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// fakeNameserver answers authoritatively for testZone: ns1 and ns2 are
|
|
// the zone's nameservers, and TXT answers come from txt.
|
|
type fakeNameserver struct {
|
|
mu sync.Mutex
|
|
txt map[string][]string
|
|
// hideFor makes the first n TXT queries miss, like a nameserver that
|
|
// has not caught up yet.
|
|
hideFor atomic.Int32
|
|
}
|
|
|
|
func (f *fakeNameserver) set(values ...string) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
f.txt[testName] = values
|
|
}
|
|
|
|
func (f *fakeNameserver) ServeDNS(w mdns.ResponseWriter, r *mdns.Msg) {
|
|
m := new(mdns.Msg)
|
|
m.SetReply(r)
|
|
m.Authoritative = true
|
|
|
|
q := r.Question[0]
|
|
hdr := mdns.RR_Header{Name: q.Name, Class: mdns.ClassINET, Ttl: 60, Rrtype: q.Qtype}
|
|
|
|
switch q.Qtype {
|
|
case mdns.TypeNS:
|
|
m.Answer = []mdns.RR{
|
|
&mdns.NS{Hdr: hdr, Ns: "ns1." + testZone},
|
|
&mdns.NS{Hdr: hdr, Ns: "ns2." + testZone},
|
|
}
|
|
case mdns.TypeA:
|
|
switch q.Name {
|
|
case "ns1." + testZone:
|
|
m.Answer = []mdns.RR{&mdns.A{Hdr: hdr, A: net.IPv4(127, 0, 0, 1)}}
|
|
case "ns2." + testZone:
|
|
m.Answer = []mdns.RR{&mdns.A{Hdr: hdr, A: net.IPv4(127, 0, 0, 2)}}
|
|
}
|
|
case mdns.TypeTXT:
|
|
if f.hideFor.Add(-1) >= 0 {
|
|
break
|
|
}
|
|
|
|
f.mu.Lock()
|
|
for _, v := range f.txt[q.Name] {
|
|
m.Answer = append(m.Answer, &mdns.TXT{Hdr: hdr, Txt: []string{v}})
|
|
}
|
|
f.mu.Unlock()
|
|
}
|
|
|
|
if len(m.Answer) == 0 {
|
|
m.Ns = []mdns.RR{&mdns.SOA{
|
|
Hdr: mdns.RR_Header{Name: testZone, Rrtype: mdns.TypeSOA, Class: mdns.ClassINET, Ttl: 60},
|
|
Ns: "ns1." + testZone,
|
|
Mbox: "hostmaster." + testZone,
|
|
Serial: 1,
|
|
Refresh: 60, Retry: 60, Expire: 60, Minttl: 60,
|
|
}}
|
|
}
|
|
|
|
_ = w.WriteMsg(m)
|
|
}
|
|
|
|
// startNameservers runs ns1 on 127.0.0.1 and ns2 on 127.0.0.2, both on the
|
|
// same port, and returns a checker using ns1 as its recursive resolver.
|
|
func startNameservers(t *testing.T) (txtChecker, *fakeNameserver, *fakeNameserver) {
|
|
t.Helper()
|
|
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("needs 127.0.0.2, which only Linux routes by default")
|
|
}
|
|
|
|
ns1 := &fakeNameserver{txt: map[string][]string{}}
|
|
ns2 := &fakeNameserver{txt: map[string][]string{}}
|
|
|
|
var lc net.ListenConfig
|
|
|
|
pc1, err := lc.ListenPacket(t.Context(), "udp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
_, port, err := net.SplitHostPort(pc1.LocalAddr().String())
|
|
require.NoError(t, err)
|
|
|
|
pc2, err := lc.ListenPacket(t.Context(), "udp", net.JoinHostPort("127.0.0.2", port))
|
|
require.NoError(t, err)
|
|
|
|
for pc, h := range map[net.PacketConn]mdns.Handler{pc1: ns1, pc2: ns2} {
|
|
srv := &mdns.Server{PacketConn: pc, Handler: h}
|
|
|
|
go func() { _ = srv.ActivateAndServe() }()
|
|
|
|
t.Cleanup(func() { _ = srv.Shutdown() })
|
|
}
|
|
|
|
c := newTXTChecker([]string{net.JoinHostPort("127.0.0.1", port)}, 5*time.Second)
|
|
c.interval = 10 * time.Millisecond
|
|
c.port = port
|
|
|
|
return c, ns1, ns2
|
|
}
|
|
|
|
func TestTXTCheckerAllNameservers(t *testing.T) {
|
|
c, ns1, ns2 := startNameservers(t)
|
|
ctx := context.Background()
|
|
|
|
ns1.set("other", "value")
|
|
ns2.set("value")
|
|
require.NoError(t, c.wait(ctx, testName, "value"))
|
|
|
|
// ns2 never gets the value: waiting for ns1 alone would let the CA
|
|
// hit ns2 and fail the challenge.
|
|
ns1.set("value-2")
|
|
|
|
c.timeout = 200 * time.Millisecond
|
|
|
|
err := c.wait(ctx, testName, "value-2")
|
|
require.ErrorIs(t, err, ErrNotPropagated)
|
|
require.ErrorContains(t, err, "ns2."+testZone)
|
|
assert.NotContains(t, err.Error(), "ns1.")
|
|
}
|
|
|
|
func TestTXTCheckerWaitsForLaggingNameserver(t *testing.T) {
|
|
c, ns1, ns2 := startNameservers(t)
|
|
|
|
ns1.set("value")
|
|
ns2.set("value")
|
|
ns2.hideFor.Store(3)
|
|
|
|
require.NoError(t, c.wait(context.Background(), testName, "value"))
|
|
assert.Negative(t, ns2.hideFor.Load(), "ns2 was asked again after it missed")
|
|
}
|
|
|
|
func TestTXTCheckerErrorNamesResolvers(t *testing.T) {
|
|
c := newTXTChecker([]string{"1.1.1.1:53", "8.8.8.8:53"}, time.Second)
|
|
err := c.via(&net.DNSError{Err: "no such host", Name: "ts.example.com.", Server: "127.0.0.53:53"})
|
|
|
|
assert.EqualError(t, err, "lookup ts.example.com. on 1.1.1.1:53, 8.8.8.8:53: no such host")
|
|
}
|