Files
tailscale/ipn/ipnlocal/peerapi_test.go
T
leoca ec1e07c737 ipn/ipnlocal: don't panic on an over-long name in the peerAPI DNS debug mode
dnsQueryForName builds the query for GET /dns-query?q=<name>, the peerAPI's
interactive debug mode. The name comes from the peer's query string and the
only thing done to it is appending a trailing dot, but the query is built
with dnsmessage.MustNewName, which panics as soon as the name is longer than
255 bytes.

The panic happens before the query reaches the resolver, so the nameAllowed
filter never gets a say and only sourceAllowed has to be true: any of the
user's own untagged devices, and any peer an extension hook lets through,
such as a client using this node as an exit node with DNS proxying allowed.
http.Server recovers it, so tailscaled survives, but the peer's connection is
dropped instead of answered and the node logs a panic trace per request.

Just under the limit the name was mishandled too: a 255-byte name is accepted
by NewName but rejected by Question when it packs the name, and that error
was dropped, so Finish returned a well-formed query with no question in it
which was then handed to the resolver.

Have dnsQueryForName use NewName, check the error from Question, and return
the error from Finish. handleDNSQuery turns a name it cannot build a query
for into the 400 it already uses for the other malformed-request cases.

Fixes #21307

Change-Id: I7c1a4b7e4f9a1d2c3b5e8f0a6d4c2b9e1f3a7d50
Signed-off-by: leoca <leo.camus23@gmail.com>
2026-09-15 17:53:19 -07:00

794 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/netip"
"slices"
"strings"
"testing"
"go4.org/netipx"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc"
"tailscale.com/appc/appctest"
"tailscale.com/health"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
"tailscale.com/tailcfg"
"tailscale.com/tailcfg/nodecap"
"tailscale.com/tsd"
"tailscale.com/tstest"
"tailscale.com/types/appctype"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/util/eventbus/eventbustest"
"tailscale.com/util/must"
"tailscale.com/util/set"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
type peerAPITestEnv struct {
ph *peerAPIHandler
rr *httptest.ResponseRecorder
logBuf tstest.MemLogger
}
type check func(*testing.T, *peerAPITestEnv)
func checks(vv ...check) []check { return vv }
func httpStatus(wantStatus int) check {
return func(t *testing.T, e *peerAPITestEnv) {
if res := e.rr.Result(); res.StatusCode != wantStatus {
t.Errorf("HTTP response code = %v; want %v", res.Status, wantStatus)
}
}
}
func bodyContains(sub string) check {
return func(t *testing.T, e *peerAPITestEnv) {
if body := e.rr.Body.String(); !strings.Contains(body, sub) {
t.Errorf("HTTP response body does not contain %q; got: %s", sub, body)
}
}
}
func bodyNotContains(sub string) check {
return func(t *testing.T, e *peerAPITestEnv) {
if body := e.rr.Body.String(); strings.Contains(body, sub) {
t.Errorf("HTTP response body unexpectedly contains %q; got: %s", sub, body)
}
}
}
func TestHandlePeerAPI(t *testing.T) {
tests := []struct {
name string
isSelf bool // the peer sending the request is owned by us
debugCap bool // self node has debug capability
reqs []*http.Request
checks []check
}{
{
name: "not_peer_api",
isSelf: true,
reqs: []*http.Request{httptest.NewRequest("GET", "/", nil)},
checks: checks(
httpStatus(200),
bodyContains("This is my Tailscale device."),
bodyContains("You are the owner of this node."),
),
},
{
name: "not_peer_api_not_owner",
isSelf: false,
reqs: []*http.Request{httptest.NewRequest("GET", "/", nil)},
checks: checks(
httpStatus(200),
bodyContains("This is my Tailscale device."),
bodyNotContains("You are the owner of this node."),
),
},
{
name: "goroutines/deny-self-no-cap",
isSelf: true,
debugCap: false,
reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
checks: checks(httpStatus(403)),
},
{
name: "goroutines/deny-nonself",
isSelf: false,
debugCap: true,
reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
checks: checks(httpStatus(403)),
},
{
name: "goroutines/accept-self",
isSelf: true,
debugCap: true,
reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
checks: checks(
httpStatus(200),
bodyContains("ServeHTTP"),
),
},
{
name: "host-val/bad-ip",
isSelf: true,
debugCap: true,
reqs: []*http.Request{httptest.NewRequest("GET", "http://12.23.45.66:1234/v0/env", nil)},
checks: checks(
httpStatus(403),
),
},
{
name: "host-val/no-port",
isSelf: true,
debugCap: true,
reqs: []*http.Request{httptest.NewRequest("GET", "http://100.100.100.101/v0/env", nil)},
checks: checks(
httpStatus(403),
),
},
{
name: "host-val/peer",
isSelf: true,
debugCap: true,
reqs: []*http.Request{httptest.NewRequest("GET", "http://peer/v0/env", nil)},
checks: checks(
httpStatus(200),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
selfNode := &tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.100.100.101/32"),
},
}
if tt.debugCap {
selfNode.CapMap = tailcfg.NodeCapMap{nodecap.Debug: nil}
}
var e peerAPITestEnv
lb := newTestLocalBackend(t)
lb.logf = e.logBuf.Logf
lb.clock = &tstest.Clock{}
lb.currentNode().SetNetMap(&netmap.NetworkMap{SelfNode: selfNode.View()})
e.ph = &peerAPIHandler{
isSelf: tt.isSelf,
selfNode: selfNode.View(),
peerNode: (&tailcfg.Node{
ComputedName: "some-peer-name",
}).View(),
ps: &peerAPIServer{
b: lb,
},
}
for _, req := range tt.reqs {
e.rr = httptest.NewRecorder()
if req.Host == "example.com" {
req.Host = "100.100.100.101:12345"
}
e.ph.ServeHTTP(e.rr, req)
}
for _, f := range tt.checks {
f(t, &e)
}
})
}
}
func TestPeerAPIIsAddressValid(t *testing.T) {
selfNode := &tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.0.1/32"),
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
},
}
tests := []struct {
name string
masqV4 string // SelfNodeV4MasqAddrForThisPeer, if non-empty
masqV6 string // SelfNodeV6MasqAddrForThisPeer, if non-empty
addr string
want bool
}{
{"no_masq_native_v4", "", "", "100.64.0.1", true},
{"no_masq_native_v6", "", "", "fd7a:115c:a1e0::1", true},
{"no_masq_other_addr", "", "", "100.64.0.9", false},
{"masq_v4_masq_addr", "100.99.1.1", "", "100.99.1.1", true},
{"masq_v4_native_v4", "100.99.1.1", "", "100.64.0.1", false},
{"masq_v4_native_v6", "100.99.1.1", "", "fd7a:115c:a1e0::1", true},
{"masq_v6_masq_addr", "", "fd7a:115c:a1e0::99", "fd7a:115c:a1e0::99", true},
{"masq_v6_native_v6", "", "fd7a:115c:a1e0::99", "fd7a:115c:a1e0::1", false},
{"masq_v6_native_v4", "", "fd7a:115c:a1e0::99", "100.64.0.1", true},
{"masq_both_native_v4", "100.99.1.1", "fd7a:115c:a1e0::99", "100.64.0.1", false},
{"masq_both_masq_v4", "100.99.1.1", "fd7a:115c:a1e0::99", "100.99.1.1", true},
{"masq_both_masq_v6", "100.99.1.1", "fd7a:115c:a1e0::99", "fd7a:115c:a1e0::99", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
peerNode := &tailcfg.Node{}
if tt.masqV4 != "" {
peerNode.SelfNodeV4MasqAddrForThisPeer = new(netip.MustParseAddr(tt.masqV4))
}
if tt.masqV6 != "" {
peerNode.SelfNodeV6MasqAddrForThisPeer = new(netip.MustParseAddr(tt.masqV6))
}
h := &peerAPIHandler{
selfNode: selfNode.View(),
peerNode: peerNode.View(),
}
if got := h.isAddressValid(netip.MustParseAddr(tt.addr)); got != tt.want {
t.Errorf("isAddressValid(%v) = %v; want %v", tt.addr, got, tt.want)
}
})
}
}
func TestIsPeerAPIDNSAllowed(t *testing.T) {
// This test can not be run in parallel because it modifies
// HookReplyToDNSQueries and exitNodeDNSFilterForTest.
r := must.Get(http.NewRequest("POST", "http://peerapi:1234/dns-query", nil))
originalHooks := HookReplyToDNSQueries
defer func() { HookReplyToDNSQueries = originalHooks }()
sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
ht := health.NewTracker(sys.Bus.Get())
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
sys.Set(pm.Store())
sys.Set(eng)
b := newTestLocalBackendWithSys(t, sys)
b.pm = pm
if b.OfferingExitNode() {
t.Error("unexpectedly offering exit node")
return
}
addrSubtests := []struct {
name string
addr netip.AddrPort
}{
{
name: "v4",
addr: netip.MustParseAddrPort("100.150.151.152:12345"),
},
{
name: "v6",
addr: netip.MustParseAddrPort("[fe70::1]:12345"),
},
}
tests := []struct {
name string
registerExtension bool // add an extra handler in HookReplyToDNSQueries
// Only used when registerExtension is true
extensionUseNameChecker bool
extensionAllowSource bool
extensionApprovedNames set.Set[string]
isSelf bool
noOfferExitNode bool
noPacketFilter bool
denyPacketFilter bool
wantSourceAllowed bool
wantNamesAllowed map[string]bool
}{
{
name: "self",
isSelf: true,
noOfferExitNode: true,
wantSourceAllowed: true,
wantNamesAllowed: map[string]bool{
"is-self.example.com": true,
"ts.net": false,
},
},
{
name: "no-exit-node",
noOfferExitNode: true,
wantSourceAllowed: false,
},
{
name: "exit-node-no-packet-filter",
noPacketFilter: true,
wantSourceAllowed: false,
},
{
name: "exit-node-deny-packet-filter",
denyPacketFilter: true,
wantSourceAllowed: false,
},
{
name: "exit-node-allow-packet-filter",
wantSourceAllowed: true,
wantNamesAllowed: map[string]bool{
"exit-node.example.com": true,
"ts.net": false,
},
},
{
name: "extension-deny",
registerExtension: true,
noOfferExitNode: true,
wantSourceAllowed: false,
},
{
name: "extension-with-exit-node",
registerExtension: true,
wantSourceAllowed: true,
wantNamesAllowed: map[string]bool{
"exit-node.example.com": true,
"ts.net": false,
},
},
{
name: "extension-without-name-filter",
registerExtension: true,
extensionAllowSource: true,
noOfferExitNode: true,
wantSourceAllowed: true,
wantNamesAllowed: map[string]bool{
"exit-node.example.com": true,
"ts.net": false,
},
},
{
name: "extension-with-name-filter",
registerExtension: true,
extensionAllowSource: true,
extensionUseNameChecker: true,
extensionApprovedNames: set.Of("extension.example.com", "blocked.extension.example.com"),
noOfferExitNode: true,
wantSourceAllowed: true,
wantNamesAllowed: map[string]bool{
"extension.example.com": true,
"blocked.extension.example.com": false,
"exit-node.example.com": false,
"ts.net": false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if len(tt.extensionApprovedNames) > 0 && !tt.extensionUseNameChecker {
t.Error("malformed test: extension has approved names but is not using name checker")
}
h := peerAPIHandler{
ps: &peerAPIServer{
b: b,
},
selfNode: (&tailcfg.Node{}).View(),
peerNode: (&tailcfg.Node{}).View(),
isSelf: tt.isSelf,
}
var advertiseRoutes []netip.Prefix
if !tt.noOfferExitNode {
advertiseRoutes = []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
}
}
if err := h.ps.b.pm.SetPrefs((&ipn.Prefs{
AdvertiseRoutes: advertiseRoutes,
}).View(), ipn.NetworkProfile{}); err != nil {
t.Errorf("SetPrefs: %v", err)
return
}
if h.ps.b.OfferingExitNode() != !tt.noOfferExitNode {
t.Errorf("unexpected: offering exit node = %v, want %v", h.ps.b.OfferingExitNode(), !tt.noOfferExitNode)
return
}
var f *filter.Filter
if !tt.noPacketFilter {
if tt.denyPacketFilter {
f = filter.NewAllowNone(logger.Discard, new(netipx.IPSet))
} else {
f = filter.NewAllowAllForTest(logger.Discard)
}
}
h.ps.b.setFilter(f)
var lastExtensionNameCheck string
if tt.registerExtension {
HookReplyToDNSQueries = slices.Clone(originalHooks)
defer func() { HookReplyToDNSQueries = originalHooks }()
extensionNameChecker := func(name string) bool {
lastExtensionNameCheck = name
return tt.extensionApprovedNames.Contains(name)
}
HookReplyToDNSQueries.Add(func(handler PeerAPIHandler, request *http.Request) (sourceAllowed bool, nameAllowed DNSNameFilter) {
if handler != &h {
t.Error("unexpected handler")
}
if request != r {
t.Error("unexpected request")
}
if tt.extensionUseNameChecker {
return tt.extensionAllowSource, extensionNameChecker
}
return tt.extensionAllowSource, nil
})
}
var lastNameCheck string
exitNodeDNSFilterForTest = func(name string) bool {
lastNameCheck = name
allow, found := tt.wantNamesAllowed[name]
if !found {
t.Errorf("unexpected name %q caught by filter", name)
}
return allow
}
defer func() { exitNodeDNSFilterForTest = nil }()
for _, tt2 := range addrSubtests {
t.Run(tt2.name, func(t *testing.T) {
h.remoteAddr = tt2.addr
sourceAllowed, nameChecker := h.isPeerAPIDNSAllowed(r)
if sourceAllowed != tt.wantSourceAllowed {
t.Errorf("sourceAllowed = %v, want %v", sourceAllowed, tt.wantSourceAllowed)
}
if !sourceAllowed {
if nameChecker != nil {
t.Errorf("nameChecker != nil when source not allowed, want nil")
}
return
}
if nameChecker == nil {
t.Errorf("nameChecker = nil when source allowed, want not-nil")
return
}
for name, want := range tt.wantNamesAllowed {
got := nameChecker(name)
if got != want {
t.Errorf("nameChecker(%q) = %v, want %v", name, got, want)
}
if lastNameCheck != name {
t.Error("lastNameCheck did not update as expected")
}
if tt.extensionUseNameChecker && lastExtensionNameCheck != name {
// Only require the extension to be consulted if the
// exitNodeDNSFilterForTest filter would have
// allowed it.
if want {
t.Error("extensionUseNameChecker did not update as expected")
}
}
}
})
}
})
}
}
func TestPeerAPIPrettyReplyCNAME(t *testing.T) {
r := must.Get(http.NewRequest("POST", "http://peerapi:1234/dns-query", nil))
for _, shouldStore := range []bool{false, true} {
h := peerAPIHandler{
remoteAddr: netip.MustParseAddrPort("100.150.151.152:12345"),
selfNode: (&tailcfg.Node{}).View(),
peerNode: (&tailcfg.Node{}).View(),
}
sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
ht := health.NewTracker(sys.Bus.Get())
reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
a := appc.NewAppConnector(appc.Config{
Logf: t.Logf,
EventBus: sys.Bus.Get(),
HasStoredRoutes: shouldStore,
})
t.Cleanup(a.Close)
sys.Set(pm.Store())
sys.Set(eng)
b := newTestLocalBackendWithSys(t, sys)
b.pm = pm
b.appConnector = a // configure as an app connector just to enable the API.
h.ps = &peerAPIServer{b: b}
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
b.CNAMEResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("www.example.com."),
Type: dnsmessage.TypeCNAME,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.CNAMEResource{
CNAME: dnsmessage.MustNewName("example.com."),
},
)
b.AResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AResource{
A: [4]byte{192, 0, 0, 8},
},
)
}}
f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f)
if allowed, _ := h.isPeerAPIDNSAllowed(r); !allowed {
t.Errorf("unexpectedly deny; wanted to be a DNS server")
}
w := httptest.NewRecorder()
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %v", w.Code)
}
var addrs []string
json.NewDecoder(w.Body).Decode(&addrs)
if len(addrs) == 0 {
t.Fatalf("no addresses returned")
}
for _, addr := range addrs {
netip.MustParseAddr(addr)
}
}
}
// TestPeerAPIDNSQueryLongName checks that a peer allowed to use the peerAPI
// DNS proxy cannot take the handler down with an over-long name in the
// interactive ‘q’ debug mode. The name is used verbatim to build the
// query, so anything that does not fit in a DNS message has to be rejected
// rather than asserted.
func TestPeerAPIDNSQueryLongName(t *testing.T) {
r := must.Get(http.NewRequest("POST", "http://peerapi:1234/dns-query", nil))
h := peerAPIHandler{
remoteAddr: netip.MustParseAddrPort("100.150.151.152:12345"),
selfNode: (&tailcfg.Node{}).View(),
peerNode: (&tailcfg.Node{}).View(),
}
sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
ht := health.NewTracker(sys.Bus.Get())
reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
a := appc.NewAppConnector(appc.Config{
Logf: t.Logf,
EventBus: sys.Bus.Get(),
})
t.Cleanup(a.Close)
sys.Set(pm.Store())
sys.Set(eng)
b := newTestLocalBackendWithSys(t, sys)
b.pm = pm
b.appConnector = a // configure as an app connector just to enable the API.
h.ps = &peerAPIServer{b: b}
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {}}
h.ps.b.setFilter(filter.NewAllowAllForTest(logger.Discard))
if allowed, _ := h.isPeerAPIDNSAllowed(r); !allowed {
t.Fatal("unexpectedly denied; wanted to be a DNS server")
}
w := httptest.NewRecorder()
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q="+strings.Repeat("a", 255), nil))
if w.Code != http.StatusBadRequest {
t.Errorf("status = %v, want %v", w.Code, http.StatusBadRequest)
}
}
func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
r := must.Get(http.NewRequest("POST", "http://peerapi:1234/dns-query", nil))
for _, shouldStore := range []bool{false, true} {
h := peerAPIHandler{
remoteAddr: netip.MustParseAddrPort("100.150.151.152:12345"),
selfNode: (&tailcfg.Node{}).View(),
peerNode: (&tailcfg.Node{}).View(),
}
sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
bw := eventbustest.NewWatcher(t, sys.Bus.Get())
rc := &appctest.RouteCollector{}
ht := health.NewTracker(sys.Bus.Get())
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
a := appc.NewAppConnector(appc.Config{
Logf: t.Logf,
EventBus: sys.Bus.Get(),
RouteAdvertiser: rc,
HasStoredRoutes: shouldStore,
})
t.Cleanup(a.Close)
sys.Set(pm.Store())
sys.Set(eng)
b := newTestLocalBackendWithSys(t, sys)
b.pm = pm
b.appConnector = a
h.ps = &peerAPIServer{b: b}
h.ps.b.appConnector.UpdateDomains([]string{"example.com"})
a.Wait(t.Context())
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
b.AResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AResource{
A: [4]byte{192, 0, 0, 8},
},
)
}}
f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f)
if !h.ps.b.OfferingAppConnector() {
t.Fatal("expecting to be offering app connector")
}
if allowed, _ := h.isPeerAPIDNSAllowed(r); !allowed {
t.Errorf("unexpectedly deny; wanted to be a DNS server")
}
w := httptest.NewRecorder()
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=example.com.", nil))
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %v", w.Code)
}
a.Wait(t.Context())
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
}
if err := eventbustest.Expect(bw,
eqUpdate(appctype.RouteUpdate{Advertise: mustPrefix("192.0.0.8/32")}),
); err != nil {
t.Error(err)
}
}
}
func TestPeerAPIReplyToDNSQueriesAreObservedWithCNAMEFlattening(t *testing.T) {
r := must.Get(http.NewRequest("POST", "http://peerapi:1234/dns-query", nil))
for _, shouldStore := range []bool{false, true} {
ctx := context.Background()
h := peerAPIHandler{
remoteAddr: netip.MustParseAddrPort("100.150.151.152:12345"),
selfNode: (&tailcfg.Node{}).View(),
peerNode: (&tailcfg.Node{}).View(),
}
sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
bw := eventbustest.NewWatcher(t, sys.Bus.Get())
ht := health.NewTracker(sys.Bus.Get())
reg := new(usermetric.Registry)
rc := &appctest.RouteCollector{}
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
a := appc.NewAppConnector(appc.Config{
Logf: t.Logf,
EventBus: sys.Bus.Get(),
RouteAdvertiser: rc,
HasStoredRoutes: shouldStore,
})
t.Cleanup(a.Close)
sys.Set(pm.Store())
sys.Set(eng)
b := newTestLocalBackendWithSys(t, sys)
b.pm = pm
b.appConnector = a
h.ps = &peerAPIServer{b: b}
h.ps.b.appConnector.UpdateDomains([]string{"www.example.com"})
a.Wait(ctx)
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
b.CNAMEResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("www.example.com."),
Type: dnsmessage.TypeCNAME,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.CNAMEResource{
CNAME: dnsmessage.MustNewName("example.com."),
},
)
b.AResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AResource{
A: [4]byte{192, 0, 0, 8},
},
)
}}
f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f)
if !h.ps.b.OfferingAppConnector() {
t.Fatal("expecting to be offering app connector")
}
if allowed, _ := h.isPeerAPIDNSAllowed(r); !allowed {
t.Errorf("unexpectedly deny; wanted to be a DNS server")
}
w := httptest.NewRecorder()
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %v", w.Code)
}
a.Wait(ctx)
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
}
if err := eventbustest.Expect(bw,
eqUpdate(appctype.RouteUpdate{Advertise: mustPrefix("192.0.0.8/32")}),
); err != nil {
t.Error(err)
}
}
}
type fakeResolver struct {
build func(*dnsmessage.Builder)
}
func (f *fakeResolver) HandlePeerDNSQuery(ctx context.Context, q []byte, from netip.AddrPort, allowName func(name string) bool) (res []byte, err error) {
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
b.EnableCompression()
b.StartAnswers()
f.build(&b)
return b.Finish()
}