mirror of
https://github.com/tailscale/tailscale.git
synced 2026-11-09 02:53:09 -05:00
response when DNS request goes to PeerAPI DoH To support feature/conn25 and other extensions or consumers needing to know which PeerAPI node handled a DNS request, e.g. for app connectors or exit nodes, and needing extra data from the server that can be transmitted in the headers. DNS responses from other upstream resolvers do not get any metadata populated, even if those servers are DoH. Add a new Response type in net/dns/resolver that includes the raw response bytes and the optional metadata, and replace the bare bytes with it in some places. Pass this new type to to net/dns.ResponseMapper instead of the bare bytes to the extensions can see the metadata. Updates tailscale/corp#46169 Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
332 lines
9.2 KiB
Go
332 lines
9.2 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package tsnet
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
dns "golang.org/x/net/dns/dnsmessage"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/types/dnstype"
|
|
)
|
|
|
|
// TestSplitDNSToTailnetResolverUDP resolves a split-DNS name over UDP from a
|
|
// node in userspace networking mode, where the upstream resolver is another
|
|
// tailnet node. Neither node has a tun device, so the upstream is reachable
|
|
// only through netstack. See tailscale/tailscale#20314.
|
|
func TestSplitDNSToTailnetResolverUDP(t *testing.T) {
|
|
lt := setupTwoClientTest(t, false) // netstack on both sides; no tun.
|
|
|
|
const domain = "e2e.split.example.com."
|
|
const suffix = "split.example.com."
|
|
const upstreamPort = 53
|
|
wantAddr := netip.MustParseAddr("100.101.102.103")
|
|
|
|
// This listener lives inside netstack on s1's tailnet address, so s2 can
|
|
// only reach it over the tailnet.
|
|
pc, err := lt.s1.ListenPacket("udp", net.JoinHostPort(lt.s1ip4.String(), fmt.Sprint(upstreamPort)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer pc.Close()
|
|
go serveOneAnswerDNS(pc, wantAddr)
|
|
|
|
// Point split DNS for suffix at s1's tailnet address.
|
|
upstream := netip.AddrPortFrom(lt.s1ip4, upstreamPort)
|
|
if !lt.control.AddRawMapResponse(lt.s2.lb.NodeKey(), &tailcfg.MapResponse{
|
|
DNSConfig: &tailcfg.DNSConfig{
|
|
Proxied: true,
|
|
Routes: map[string][]*dnstype.Resolver{
|
|
suffix: {{Addr: upstream.String()}},
|
|
},
|
|
},
|
|
}) {
|
|
t.Fatal("AddRawMapResponse failed")
|
|
}
|
|
|
|
res := lt.s2.Sys().DNSManager.Get().Resolver()
|
|
if res == nil {
|
|
t.Fatal("s2 has no resolver")
|
|
}
|
|
query := mustDNSQuery(t, domain)
|
|
from := netip.MustParseAddrPort("127.0.0.1:12345")
|
|
|
|
// Until the split-DNS route lands the resolver answers immediately with
|
|
// an empty answer section, so wait it out before timing anything below.
|
|
if err := tstest.WaitFor(60*time.Second, func() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
resp, err := res.Query(ctx, query, "udp", from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := firstAAnswer(resp.Bs); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("waiting for split-DNS route to %v to work: %v", upstream, err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
start := time.Now()
|
|
resp, err := res.Query(ctx, query, "udp", from)
|
|
elapsed := time.Since(start)
|
|
if err != nil {
|
|
t.Fatalf("Query: %v", err)
|
|
}
|
|
got, err := firstAAnswer(resp.Bs)
|
|
if err != nil {
|
|
t.Fatalf("parsing response: %v", err)
|
|
}
|
|
if got != wantAddr {
|
|
t.Errorf("answer = %v, want %v", got, wantAddr)
|
|
}
|
|
t.Logf("split-DNS query to a tailnet resolver over netstack UDP took %v", elapsed)
|
|
|
|
// An answer is also correct if UDP blackholed and the TCP fallback
|
|
// supplied it, just late: that fallback can't fire before udpRaceTimeout
|
|
// (2s, net/dns/resolver), so bound the time as well as the bytes.
|
|
if elapsed >= 2*time.Second {
|
|
t.Errorf("query took %v (>= 2s): answered by the TCP fallback, not UDP", elapsed)
|
|
}
|
|
}
|
|
|
|
// TestRefusingUpstreamAnswersPromptly exercises the whole daemon DNS path,
|
|
// from control pushing a resolver through dns.Manager to the forwarder. The
|
|
// upstream answers REFUSED over UDP and accepts TCP without ever answering.
|
|
// The client must get the REFUSED, not a stall that ends in no answer at all.
|
|
//
|
|
// See tailscale/tailscale#20826.
|
|
func TestRefusingUpstreamAnswersPromptly(t *testing.T) {
|
|
ctx := t.Context()
|
|
controlURL, control := startControl(t)
|
|
s1, _, _ := startServer(t, ctx, controlURL, "s1")
|
|
|
|
// A host-loopback upstream, so this is the ordinary "system upstream" case
|
|
// rather than a tailnet address reached over netstack.
|
|
upstream := listenRefusingUpstream(t)
|
|
|
|
if !control.AddRawMapResponse(s1.lb.NodeKey(), &tailcfg.MapResponse{
|
|
DNSConfig: &tailcfg.DNSConfig{
|
|
Proxied: true,
|
|
Resolvers: []*dnstype.Resolver{{Addr: upstream.String()}},
|
|
},
|
|
}) {
|
|
t.Fatal("AddRawMapResponse failed")
|
|
}
|
|
|
|
mgr := s1.Sys().DNSManager.Get()
|
|
query := mustDNSQuery(t, "refused.example.com.")
|
|
from := netip.MustParseAddrPort("127.0.0.1:12345")
|
|
|
|
// Wait for the pushed resolver to take effect. Until it lands there is no
|
|
// upstream at all, and the forwarder synthesizes its own SERVFAIL.
|
|
if err := tstest.WaitFor(60*time.Second, func() error {
|
|
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
|
defer cancel()
|
|
resp, err := mgr.Query(ctx, query, "udp", from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if got := rcodeOf(t, resp); got != dns.RCodeRefused {
|
|
return fmt.Errorf("rcode = %v, want %v", got, dns.RCodeRefused)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("waiting for the REFUSED upstream to take effect: %v", err)
|
|
}
|
|
|
|
for i := range 3 {
|
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
start := time.Now()
|
|
resp, err := mgr.Query(ctx, query, "udp", from)
|
|
elapsed := time.Since(start)
|
|
cancel()
|
|
if err != nil {
|
|
t.Fatalf("query %d: %v", i, err)
|
|
}
|
|
if got := rcodeOf(t, resp); got != dns.RCodeRefused {
|
|
t.Errorf("query %d: rcode = %v, want %v", i, got, dns.RCodeRefused)
|
|
}
|
|
// Logged only. A stall returns an error, which the check above catches.
|
|
t.Logf("query %d: %v", i, elapsed)
|
|
}
|
|
}
|
|
|
|
// listenRefusingUpstream starts a DNS upstream on host loopback that answers
|
|
// REFUSED over UDP and accepts TCP connections without ever answering them. It
|
|
// returns the address, and stops both listeners when the test ends.
|
|
func listenRefusingUpstream(t *testing.T) netip.AddrPort {
|
|
t.Helper()
|
|
const tries = 25
|
|
var (
|
|
tcpLn net.Listener
|
|
pc net.PacketConn
|
|
)
|
|
// Listen on TCP first, then bind UDP to the same port, retrying on a clash.
|
|
for range tries {
|
|
var err error
|
|
tcpLn, err = net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pc, err = net.ListenPacket("udp", tcpLn.Addr().String())
|
|
if err == nil {
|
|
break
|
|
}
|
|
tcpLn.Close()
|
|
tcpLn = nil
|
|
}
|
|
if tcpLn == nil {
|
|
t.Skipf("failed to listen on the same port for TCP and UDP after %d tries", tries)
|
|
}
|
|
|
|
// Hold accepted conns open until the test ends, so the forwarder's TCP
|
|
// retry, if it makes one, connects and then waits rather than failing fast.
|
|
var (
|
|
heldMu sync.Mutex
|
|
held []net.Conn
|
|
)
|
|
t.Cleanup(func() {
|
|
tcpLn.Close()
|
|
pc.Close()
|
|
heldMu.Lock()
|
|
defer heldMu.Unlock()
|
|
for _, c := range held {
|
|
c.Close()
|
|
}
|
|
})
|
|
go serveRefusedDNS(pc)
|
|
go func() {
|
|
for {
|
|
c, err := tcpLn.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
heldMu.Lock()
|
|
held = append(held, c)
|
|
heldMu.Unlock()
|
|
}
|
|
}()
|
|
return netip.MustParseAddrPort(pc.LocalAddr().String())
|
|
}
|
|
|
|
// serveDNS answers every query on pc with whatever respond builds from the
|
|
// query's header and question, until pc is closed. Queries that don't parse
|
|
// and responses that don't build are dropped.
|
|
func serveDNS(pc net.PacketConn, respond func(hdr dns.Header, q dns.Question) ([]byte, error)) {
|
|
buf := make([]byte, 1500)
|
|
for {
|
|
n, from, err := pc.ReadFrom(buf)
|
|
if err != nil {
|
|
return // listener closed
|
|
}
|
|
var p dns.Parser
|
|
hdr, err := p.Start(buf[:n])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
q, err := p.Question()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
resp, err := respond(hdr, q)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
pc.WriteTo(resp, from)
|
|
}
|
|
}
|
|
|
|
// serveRefusedDNS answers every query on pc with REFUSED, until pc is closed.
|
|
func serveRefusedDNS(pc net.PacketConn) {
|
|
serveDNS(pc, func(hdr dns.Header, q dns.Question) ([]byte, error) {
|
|
hdr.Response = true
|
|
hdr.RCode = dns.RCodeRefused
|
|
b := dns.NewBuilder(nil, hdr)
|
|
b.StartQuestions()
|
|
b.Question(q)
|
|
return b.Finish()
|
|
})
|
|
}
|
|
|
|
// rcodeOf returns the response code in resp.
|
|
func rcodeOf(tb testing.TB, resp []byte) dns.RCode {
|
|
tb.Helper()
|
|
var p dns.Parser
|
|
h, err := p.Start(resp)
|
|
if err != nil {
|
|
tb.Fatalf("parsing response: %v", err)
|
|
}
|
|
return h.RCode
|
|
}
|
|
|
|
// serveOneAnswerDNS answers every A query on pc with addr, until pc is closed.
|
|
func serveOneAnswerDNS(pc net.PacketConn, addr netip.Addr) {
|
|
serveDNS(pc, func(hdr dns.Header, q dns.Question) ([]byte, error) {
|
|
b := dns.NewBuilder(nil, dns.Header{ID: hdr.ID, Response: true})
|
|
b.StartQuestions()
|
|
b.Question(q)
|
|
b.StartAnswers()
|
|
b.AResource(dns.ResourceHeader{
|
|
Name: q.Name,
|
|
Class: dns.ClassINET,
|
|
TTL: 300,
|
|
}, dns.AResource{A: addr.As4()})
|
|
return b.Finish()
|
|
})
|
|
}
|
|
|
|
// mustDNSQuery builds an A query for domain.
|
|
func mustDNSQuery(tb testing.TB, domain string) []byte {
|
|
tb.Helper()
|
|
b := dns.NewBuilder(nil, dns.Header{RecursionDesired: true})
|
|
b.StartQuestions()
|
|
if err := b.Question(dns.Question{
|
|
Name: dns.MustNewName(domain),
|
|
Type: dns.TypeA,
|
|
Class: dns.ClassINET,
|
|
}); err != nil {
|
|
tb.Fatal(err)
|
|
}
|
|
query, err := b.Finish()
|
|
if err != nil {
|
|
tb.Fatal(err)
|
|
}
|
|
return query
|
|
}
|
|
|
|
// firstAAnswer returns the address in resp's first answer record, or an error
|
|
// if resp doesn't parse or has no A answer.
|
|
func firstAAnswer(resp []byte) (netip.Addr, error) {
|
|
var p dns.Parser
|
|
if _, err := p.Start(resp); err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
if err := p.SkipAllQuestions(); err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
h, err := p.AnswerHeader()
|
|
if err != nil {
|
|
return netip.Addr{}, fmt.Errorf("no answer section: %w", err)
|
|
}
|
|
if h.Type != dns.TypeA {
|
|
return netip.Addr{}, fmt.Errorf("answer type = %v, want A", h.Type)
|
|
}
|
|
r, err := p.AResource()
|
|
if err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
return netip.AddrFrom4(r.A), nil
|
|
}
|