feature/conn25: rewrite A records for connector domains

When we are mapping a dns response, if it is a connector domain, change
the source IP addresses for our magic IP addresses. This will allow the
tailscaled to DNAT the traffic for the domain to the connector.

Updates tailscale/corp#34258
Signed-off-by: Fran Bull <fran@tailscale.com>
This commit is contained in:
Fran Bull
2026-03-03 10:35:12 -08:00
committed by franbull
parent 54606a0a89
commit 51a117f494
2 changed files with 557 additions and 72 deletions
+149 -26
View File
@@ -17,6 +17,7 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"slices" "slices"
"strings"
"sync" "sync"
"go4.org/netipx" "go4.org/netipx"
@@ -55,6 +56,12 @@ func jsonDecode(target any, rc io.ReadCloser) error {
return err return err
} }
func normalizeDNSName(name string) (dnsname.FQDN, error) {
// note that appconnector does this same thing, tsdns has its own custom lower casing
// it might be good to unify in a function in dnsname package.
return dnsname.ToFQDN(strings.ToLower(name))
}
func init() { func init() {
feature.Register(featureName) feature.Register(featureName)
ipnext.RegisterExtension(featureName, func(logf logger.Logf, sb ipnext.SafeBackend) (ipnext.Extension, error) { ipnext.RegisterExtension(featureName, func(logf logger.Logf, sb ipnext.SafeBackend) (ipnext.Extension, error) {
@@ -438,7 +445,7 @@ func configFromNodeView(n tailcfg.NodeView) (config, error) {
for _, app := range apps { for _, app := range apps {
selfMatchesTags := slices.ContainsFunc(app.Connectors, selfTags.Contains) selfMatchesTags := slices.ContainsFunc(app.Connectors, selfTags.Contains)
for _, d := range app.Domains { for _, d := range app.Domains {
fqdn, err := dnsname.ToFQDN(d) fqdn, err := normalizeDNSName(d)
if err != nil { if err != nil {
return config{}, err return config{}, err
} }
@@ -641,16 +648,81 @@ func (e *extension) sendAddressAssignment(ctx context.Context, as addrs) error {
return makePeerAPIReq(ctx, client, urlBase, as) return makePeerAPIReq(ctx, client, urlBase, as)
} }
type dnsResponseRewrite struct {
domain dnsname.FQDN
dst netip.Addr
}
func makeServFail(logf logger.Logf, h dnsmessage.Header, q dnsmessage.Question) []byte {
h.Response = true
h.Authoritative = true
h.RCode = dnsmessage.RCodeServerFailure
b := dnsmessage.NewBuilder(nil, h)
err := b.StartQuestions()
if err != nil {
logf("error making servfail: %v", err)
return []byte{}
}
err = b.Question(q)
if err != nil {
logf("error making servfail: %v", err)
return []byte{}
}
bs, err := b.Finish()
if err != nil {
// If there's an error here there's a bug somewhere directly above.
// _possibly_ some kind of question that was parseable but not encodable?,
// otherwise we could panic.
logf("error making servfail: %v", err)
}
return bs
}
func (c *client) mapDNSResponse(buf []byte) []byte { func (c *client) mapDNSResponse(buf []byte) []byte {
var p dnsmessage.Parser var p dnsmessage.Parser
if _, err := p.Start(buf); err != nil { hdr, err := p.Start(buf)
if err != nil {
c.logf("error parsing dns response: %v", err) c.logf("error parsing dns response: %v", err)
return buf return buf
} }
if err := p.SkipAllQuestions(); err != nil { questions, err := p.AllQuestions()
if err != nil {
c.logf("error parsing dns response: %v", err) c.logf("error parsing dns response: %v", err)
return buf return buf
} }
// Any message we are interested in has one question (RFC 9619)
if len(questions) != 1 {
return buf
}
question := questions[0]
// The other Class types are not commonly used and supporting them hasn't been considered.
if question.Class != dnsmessage.ClassINET {
return buf
}
queriedDomain, err := normalizeDNSName(question.Name.String())
if err != nil {
return buf
}
if !c.isConnectorDomain(queriedDomain) {
return buf
}
// Now we know this is a dns response we think we should rewrite, we're going to provide our response which
// currently means we will:
// * write the questions through as they are
// * not send through the additional section
// * provide our answers, or no answers if we don't handle those answers (possibly in the future we should write through answers for eg TypeTXT)
var answers []dnsResponseRewrite
if question.Type != dnsmessage.TypeA {
// we plan to support TypeAAAA soon (2026-03-11)
c.logf("mapping dns response for connector domain, unsupported type: %v", question.Type)
newBuf, err := c.rewriteDNSResponse(hdr, questions, answers)
if err != nil {
c.logf("error writing empty response for unsupported type: %v", err)
return makeServFail(c.logf, hdr, question)
}
return newBuf
}
for { for {
h, err := p.AnswerHeader() h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone { if err == dnsmessage.ErrSectionDone {
@@ -658,57 +730,108 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
} }
if err != nil { if err != nil {
c.logf("error parsing dns response: %v", err) c.logf("error parsing dns response: %v", err)
return buf return makeServFail(c.logf, hdr, question)
} }
// other classes are unsupported, and we checked the question was for ClassINET already
if h.Class != dnsmessage.ClassINET { if h.Class != dnsmessage.ClassINET {
c.logf("unexpected class for connector domain dns response: %v %v", queriedDomain, h.Class)
if err := p.SkipAnswer(); err != nil { if err := p.SkipAnswer(); err != nil {
c.logf("error parsing dns response: %v", err) c.logf("error parsing dns response: %v", err)
return buf return makeServFail(c.logf, hdr, question)
} }
continue continue
} }
switch h.Type { switch h.Type {
case dnsmessage.TypeCNAME:
// An A record was asked for, and the answer is a CNAME, this answer will tell us which domain it's a CNAME for
// and a subsequent answer should tell us what the target domains address is (or possibly another CNAME). Drop
// this for now (2026-03-11) but in the near future we should collapse the CNAME chain and map to the ultimate
// destination address (see eg appc/{appconnector,observe}.go).
c.logf("not yet implemented CNAME answer: %v", queriedDomain)
if err := p.SkipAnswer(); err != nil {
c.logf("error parsing dns response: %v", err)
return makeServFail(c.logf, hdr, question)
}
case dnsmessage.TypeA: case dnsmessage.TypeA:
domain, err := dnsname.ToFQDN(h.Name.String()) domain, err := normalizeDNSName(h.Name.String())
if err != nil { if err != nil {
c.logf("bad dnsname: %v", err) c.logf("bad dnsname: %v", err)
return buf return makeServFail(c.logf, hdr, question)
} }
if !c.isConnectorDomain(domain) { // answers should be for the domain that was queried
if domain != queriedDomain {
c.logf("unexpected domain for connector domain dns response: %v %v", queriedDomain, domain)
if err := p.SkipAnswer(); err != nil { if err := p.SkipAnswer(); err != nil {
c.logf("error parsing dns response: %v", err) c.logf("error parsing dns response: %v", err)
return buf return makeServFail(c.logf, hdr, question)
} }
continue continue
} }
r, err := p.AResource() r, err := p.AResource()
if err != nil { if err != nil {
c.logf("error parsing dns response: %v", err) c.logf("error parsing dns response: %v", err)
return buf return makeServFail(c.logf, hdr, question)
}
addrs, err := c.reserveAddresses(domain, netip.AddrFrom4(r.A))
if err != nil {
c.logf("error assigning connector addresses: %v", err)
return buf
}
if !addrs.isValid() {
c.logf("assigned connector addresses unexpectedly empty: %v", err)
return buf
} }
answers = append(answers, dnsResponseRewrite{domain: domain, dst: netip.AddrFrom4(r.A)})
default: default:
// we already checked the question was for a supported type, this answer is unexpected
c.logf("unexpected type for connector domain dns response: %v %v", queriedDomain, h.Type)
if err := p.SkipAnswer(); err != nil { if err := p.SkipAnswer(); err != nil {
c.logf("error parsing dns response: %v", err) c.logf("error parsing dns response: %v", err)
return buf return makeServFail(c.logf, hdr, question)
} }
continue
} }
} }
newBuf, err := c.rewriteDNSResponse(hdr, questions, answers)
if err != nil {
c.logf("error rewriting dns response: %v", err)
return makeServFail(c.logf, hdr, question)
}
return newBuf
}
// TODO(fran) 2026-01-21 return a dns response with addresses func (c *client) rewriteDNSResponse(hdr dnsmessage.Header, questions []dnsmessage.Question, answers []dnsResponseRewrite) ([]byte, error) {
// swapped out for the magic IPs to make conn25 work. // We are currently (2026-03-10) only doing this for AResource records, we know that if we are here
return buf // with non-empty answers, the type was AResource.
b := dnsmessage.NewBuilder(nil, hdr)
b.EnableCompression()
if err := b.StartQuestions(); err != nil {
return nil, err
}
for _, q := range questions {
if err := b.Question(q); err != nil {
return nil, err
}
}
if err := b.StartAnswers(); err != nil {
return nil, err
}
// make an answer for each rewrite
for _, rw := range answers {
as, err := c.reserveAddresses(rw.domain, rw.dst)
if err != nil {
return nil, err
}
if !as.isValid() {
return nil, errors.New("connector addresses empty")
}
name, err := dnsmessage.NewName(rw.domain.WithTrailingDot())
if err != nil {
return nil, err
}
// only handling TypeA right now
rhdr := dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 0}
if err := b.AResource(rhdr, dnsmessage.AResource{A: as.magic.As4()}); err != nil {
return nil, err
}
}
// We do _not_ include the additional section in our rewrite. (We don't want to include
// eg DNSSEC info, or other extra info like related records).
out, err := b.Finish()
if err != nil {
return nil, err
}
return out, nil
} }
type connector struct { type connector struct {
+408 -46
View File
@@ -8,7 +8,6 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"reflect"
"testing" "testing"
"time" "time"
@@ -547,61 +546,139 @@ func rangeFrom(from, to string) netipx.IPRange {
) )
} }
func TestMapDNSResponse(t *testing.T) { func makeDNSResponse(t *testing.T, domain string, addrs []*dnsmessage.AResource) []byte {
makeDNSResponse := func(domain string, addrs []dnsmessage.AResource) []byte { t.Helper()
b := dnsmessage.NewBuilder(nil, name := dnsmessage.MustNewName(domain)
dnsmessage.Header{ questions := []dnsmessage.Question{
ID: 1, {
Response: true, Name: name,
Authoritative: true,
RCode: dnsmessage.RCodeSuccess,
})
b.EnableCompression()
if err := b.StartQuestions(); err != nil {
t.Fatal(err)
}
if err := b.Question(dnsmessage.Question{
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeA, Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET, Class: dnsmessage.ClassINET,
}); err != nil { },
t.Fatal(err) }
var answers []dnsmessage.Resource
for _, addr := range addrs {
ans := dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: name,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
Body: addr,
} }
answers = append(answers, ans)
}
additional := []dnsmessage.Resource{
{
Header: dnsmessage.ResourceHeader{
Name: name,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
Body: &dnsmessage.AResource{A: [4]byte{9, 9, 9, 9}},
},
}
return makeDNSResponseForSections(t, questions, answers, additional)
}
if err := b.StartAnswers(); err != nil { func makeV6DNSResponse(t *testing.T, domain string, addrs []*dnsmessage.AAAAResource) []byte {
t.Fatal(err) t.Helper()
name := dnsmessage.MustNewName(domain)
questions := []dnsmessage.Question{
{
Name: name,
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
},
}
var answers []dnsmessage.Resource
for _, addr := range addrs {
ans := dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: name,
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
},
Body: addr,
} }
answers = append(answers, ans)
}
return makeDNSResponseForSections(t, questions, answers, nil)
}
for _, addr := range addrs { func makeDNSResponseForSections(t *testing.T, questions []dnsmessage.Question, answers []dnsmessage.Resource, additional []dnsmessage.Resource) []byte {
b.AResource( t.Helper()
dnsmessage.ResourceHeader{ b := dnsmessage.NewBuilder(nil,
Name: dnsmessage.MustNewName(domain), dnsmessage.Header{
Type: dnsmessage.TypeA, ID: 1,
Class: dnsmessage.ClassINET, Response: true,
}, Authoritative: true,
addr, RCode: dnsmessage.RCodeSuccess,
) })
} b.EnableCompression()
outbs, err := b.Finish() if err := b.StartQuestions(); err != nil {
if err != nil { t.Fatal(err)
t.Fatal(err)
}
return outbs
} }
for _, q := range questions {
if err := b.Question(q); err != nil {
t.Fatal(err)
}
}
if err := b.StartAnswers(); err != nil {
t.Fatal(err)
}
for _, ans := range answers {
switch ans.Header.Type {
case dnsmessage.TypeA:
body, ok := (ans.Body).(*dnsmessage.AResource)
if !ok {
t.Fatalf("unexpected answer type, update test")
}
b.AResource(ans.Header, *body)
case dnsmessage.TypeAAAA:
body, ok := (ans.Body).(*dnsmessage.AAAAResource)
if !ok {
t.Fatalf("unexpected answer type, update test")
}
b.AAAAResource(ans.Header, *body)
default:
t.Fatalf("unhandled answer type, update test: %v", ans.Header.Type)
}
}
if err := b.StartAdditionals(); err != nil {
t.Fatal(err)
}
for _, add := range additional {
body, ok := (add.Body).(*dnsmessage.AResource)
if !ok {
t.Fatalf("unexpected additional type, update test")
}
b.AResource(add.Header, *body)
}
outbs, err := b.Finish()
if err != nil {
t.Fatal(err)
}
return outbs
}
func TestMapDNSResponseAssignsAddrs(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
domain string domain string
addrs []dnsmessage.AResource addrs []*dnsmessage.AResource
wantByMagicIP map[netip.Addr]addrs wantByMagicIP map[netip.Addr]addrs
}{ }{
{ {
name: "one-ip-matches", name: "one-ip-matches",
domain: "example.com.", domain: "example.com.",
addrs: []dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}}, addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
// these are 'expected' because they are the beginning of the provided pools // these are 'expected' because they are the beginning of the provided pools
wantByMagicIP: map[netip.Addr]addrs{ wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("100.64.0.0"): { netip.MustParseAddr("100.64.0.0"): {
@@ -616,7 +693,7 @@ func TestMapDNSResponse(t *testing.T) {
{ {
name: "multiple-ip-matches", name: "multiple-ip-matches",
domain: "example.com.", domain: "example.com.",
addrs: []dnsmessage.AResource{ addrs: []*dnsmessage.AResource{
{A: [4]byte{1, 0, 0, 0}}, {A: [4]byte{1, 0, 0, 0}},
{A: [4]byte{2, 0, 0, 0}}, {A: [4]byte{2, 0, 0, 0}},
}, },
@@ -640,14 +717,14 @@ func TestMapDNSResponse(t *testing.T) {
{ {
name: "no-domain-match", name: "no-domain-match",
domain: "x.example.com.", domain: "x.example.com.",
addrs: []dnsmessage.AResource{ addrs: []*dnsmessage.AResource{
{A: [4]byte{1, 0, 0, 0}}, {A: [4]byte{1, 0, 0, 0}},
{A: [4]byte{2, 0, 0, 0}}, {A: [4]byte{2, 0, 0, 0}},
}, },
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
dnsResp := makeDNSResponse(tt.domain, tt.addrs) dnsResp := makeDNSResponse(t, tt.domain, tt.addrs)
sn := makeSelfNode(t, appctype.Conn25Attr{ sn := makeSelfNode(t, appctype.Conn25Attr{
Name: "app1", Name: "app1",
Connectors: []string{"tag:woo"}, Connectors: []string{"tag:woo"},
@@ -658,10 +735,7 @@ func TestMapDNSResponse(t *testing.T) {
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
c.reconfig(sn) c.reconfig(sn)
bs := c.mapDNSResponse(dnsResp) c.mapDNSResponse(dnsResp)
if !reflect.DeepEqual(dnsResp, bs) {
t.Fatal("shouldn't be changing the bytes (yet)")
}
if diff := cmp.Diff(tt.wantByMagicIP, c.client.assignments.byMagicIP, cmpopts.EquateComparable(addrs{}, netip.Addr{})); diff != "" { if diff := cmp.Diff(tt.wantByMagicIP, c.client.assignments.byMagicIP, cmpopts.EquateComparable(addrs{}, netip.Addr{})); diff != "" {
t.Errorf("byMagicIP diff (-want, +got):\n%s", diff) t.Errorf("byMagicIP diff (-want, +got):\n%s", diff)
} }
@@ -821,3 +895,291 @@ func TestEnqueueAddress(t *testing.T) {
t.Fatal("timed out waiting for connector to receive request") t.Fatal("timed out waiting for connector to receive request")
} }
} }
func parseResponse(t *testing.T, buf []byte) ([]dnsmessage.Resource, []dnsmessage.Resource) {
t.Helper()
var p dnsmessage.Parser
header, err := p.Start(buf)
if err != nil {
t.Fatalf("parsing DNS response: %v", err)
}
if header.RCode != dnsmessage.RCodeSuccess {
t.Fatalf("RCode want: %v, got: %v", dnsmessage.RCodeSuccess, header.RCode)
}
if err := p.SkipAllQuestions(); err != nil {
t.Fatalf("skipping questions: %v", err)
}
answers, err := p.AllAnswers()
if err != nil {
t.Fatalf("reading answers: %v", err)
}
if err := p.SkipAllAuthorities(); err != nil {
t.Fatalf("skipping questions: %v", err)
}
additionals, err := p.AllAdditionals()
if err != nil {
t.Fatalf("reading additionals: %v", err)
}
return answers, additionals
}
func TestMapDNSResponseRewritesResponses(t *testing.T) {
configuredDomain := "example.com"
domainName := configuredDomain + "."
dnsMessageName := dnsmessage.MustNewName(domainName)
sn := makeSelfNode(t, appctype.Conn25Attr{
Name: "app1",
Connectors: []string{"tag:connector"},
Domains: []string{configuredDomain},
MagicIPPool: []netipx.IPRange{rangeFrom("0", "10")},
TransitIPPool: []netipx.IPRange{rangeFrom("40", "50")},
}, []string{})
compareToRecords := func(t *testing.T, resources []dnsmessage.Resource, want []netip.Addr) {
t.Helper()
var got []netip.Addr
for _, r := range resources {
if b, ok := r.Body.(*dnsmessage.AResource); ok {
got = append(got, netip.AddrFrom4(b.A))
} else if b, ok := r.Body.(*dnsmessage.AAAAResource); ok {
got = append(got, netip.AddrFrom16(b.AAAA))
}
}
if diff := cmp.Diff(want, got, cmpopts.EquateComparable(netip.Addr{})); diff != "" {
t.Fatalf("A/AAAA records mismatch (-want +got):\n%s", diff)
}
}
assertParsesToAnswers := func(want []netip.Addr) func(t *testing.T, bs []byte) {
return func(t *testing.T, bs []byte) {
t.Helper()
answers, _ := parseResponse(t, bs)
compareToRecords(t, answers, want)
}
}
assertParsesToAdditionals := func(want []netip.Addr) func(t *testing.T, bs []byte) {
return func(t *testing.T, bs []byte) {
t.Helper()
_, additionals := parseResponse(t, bs)
compareToRecords(t, additionals, want)
}
}
assertBytes := func(want []byte) func(t *testing.T, bs []byte) {
return func(t *testing.T, bs []byte) {
t.Helper()
if diff := cmp.Diff(want, bs); diff != "" {
t.Fatalf("bytes mismatch (-want +got):\n%s", diff)
}
}
}
assertServFail := func(t *testing.T, bs []byte) {
var p dnsmessage.Parser
header, err := p.Start(bs)
if err != nil {
t.Fatalf("parsing DNS response: %v", err)
}
if header.RCode != dnsmessage.RCodeServerFailure {
t.Fatalf("RCode want: %v, got: %v", dnsmessage.RCodeServerFailure, header.RCode)
}
}
ipv6ResponseUnhandledDomain := makeV6DNSResponse(t, "tailscale.com.", []*dnsmessage.AAAAResource{
{AAAA: netip.MustParseAddr("2606:4700::6812:1a78").As16()},
{AAAA: netip.MustParseAddr("2606:4700::6812:1b78").As16()},
})
ipv4ResponseUnhandledDomain := makeDNSResponse(t, "tailscale.com.", []*dnsmessage.AResource{
{A: netip.MustParseAddr("1.2.3.4").As4()},
{A: netip.MustParseAddr("5.6.7.8").As4()},
})
nonINETQuestionResp := makeDNSResponseForSections(t, []dnsmessage.Question{
{
Name: dnsMessageName,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassCHAOS,
},
}, nil, nil)
for _, tt := range []struct {
name string
toMap []byte
assertFx func(*testing.T, []byte)
}{
{
name: "unparseable",
toMap: []byte{1, 2, 3, 4},
assertFx: assertBytes([]byte{1, 2, 3, 4}),
},
{
name: "maps-multi-typea-answers",
toMap: makeDNSResponse(t, domainName, []*dnsmessage.AResource{
{A: netip.MustParseAddr("1.2.3.4").As4()},
{A: netip.MustParseAddr("5.6.7.8").As4()},
}),
assertFx: assertParsesToAnswers(
[]netip.Addr{
netip.MustParseAddr("100.64.0.0"),
netip.MustParseAddr("100.64.0.1"),
},
),
},
{
name: "ipv6-no-answers",
toMap: makeV6DNSResponse(t, domainName, []*dnsmessage.AAAAResource{
{AAAA: netip.MustParseAddr("2606:4700::6812:1a78").As16()},
{AAAA: netip.MustParseAddr("2606:4700::6812:1b78").As16()},
}),
assertFx: assertParsesToAnswers(nil),
},
{
name: "not-our-domain",
toMap: ipv4ResponseUnhandledDomain,
assertFx: assertBytes(ipv4ResponseUnhandledDomain),
},
{
name: "ipv6-not-our-domain",
toMap: ipv6ResponseUnhandledDomain,
assertFx: assertBytes(ipv6ResponseUnhandledDomain),
},
{
name: "case-insensitive",
toMap: makeDNSResponse(t, "eXample.com.", []*dnsmessage.AResource{
{A: netip.MustParseAddr("1.2.3.4").As4()},
{A: netip.MustParseAddr("5.6.7.8").As4()},
}),
assertFx: assertParsesToAnswers(
[]netip.Addr{
netip.MustParseAddr("100.64.0.0"),
netip.MustParseAddr("100.64.0.1"),
},
),
},
{
name: "unhandled-keeps-additional-section",
toMap: makeDNSResponse(t, "tailscale.com.", []*dnsmessage.AResource{
{A: netip.MustParseAddr("1.2.3.4").As4()},
{A: netip.MustParseAddr("5.6.7.8").As4()},
}),
assertFx: assertParsesToAdditionals(
// additionals are added in makeDNSResponse
[]netip.Addr{
netip.MustParseAddr("9.9.9.9"),
},
),
},
{
name: "handled-strips-additional-section",
toMap: makeDNSResponse(t, domainName, []*dnsmessage.AResource{
{A: netip.MustParseAddr("1.2.3.4").As4()},
{A: netip.MustParseAddr("5.6.7.8").As4()},
}),
assertFx: assertParsesToAdditionals(nil),
},
{
name: "servfail-when-we-should-handle-but-cant",
// produced by
// makeDNSResponse(t, domainName, []*dnsmessage.AResource{{A: netip.MustParseAddr("1.2.3.4").As4()}})
// and then taking 17 bytes off the end. So that the parsing of it breaks after we have decided we should handle it.
// Frozen like this so that it doesn't depend on the implementation of dnsmessage.
toMap: []byte{0, 1, 132, 0, 0, 1, 0, 1, 0, 0, 0, 1, 7, 101, 120, 97, 109, 112, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, 192, 12, 0, 1, 0, 1, 0, 0, 0, 0, 0, 4, 1, 2, 3},
assertFx: assertServFail,
},
{
name: "not-inet-question",
toMap: nonINETQuestionResp,
assertFx: assertBytes(nonINETQuestionResp),
},
{
name: "not-inet-answer",
toMap: makeDNSResponseForSections(t,
[]dnsmessage.Question{
{
Name: dnsMessageName,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
},
[]dnsmessage.Resource{
{
Header: dnsmessage.ResourceHeader{
Name: dnsMessageName,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassCHAOS,
},
Body: &dnsmessage.AResource{A: netip.MustParseAddr("1.2.3.4").As4()},
},
},
nil,
),
assertFx: assertParsesToAnswers(nil),
},
{
name: "answer-domain-mismatch",
toMap: makeDNSResponseForSections(t,
[]dnsmessage.Question{
{
Name: dnsMessageName,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
},
[]dnsmessage.Resource{
{
Header: dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("tailscale.com."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
Body: &dnsmessage.AResource{A: netip.MustParseAddr("1.2.3.4").As4()},
},
},
nil,
),
assertFx: assertParsesToAnswers(nil),
},
{
name: "answer-type-mismatch",
toMap: makeDNSResponseForSections(t,
[]dnsmessage.Question{
{
Name: dnsMessageName,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
},
[]dnsmessage.Resource{
{
Header: dnsmessage.ResourceHeader{
Name: dnsMessageName,
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
},
Body: &dnsmessage.AAAAResource{AAAA: netip.MustParseAddr("1.2.3.4").As16()},
},
{
Header: dnsmessage.ResourceHeader{
Name: dnsMessageName,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
Body: &dnsmessage.AResource{A: netip.MustParseAddr("5.6.7.8").As4()},
},
},
nil,
),
assertFx: assertParsesToAnswers([]netip.Addr{netip.MustParseAddr("100.64.0.0")}),
},
} {
t.Run(tt.name, func(t *testing.T) {
c := newConn25(logger.Discard)
if err := c.reconfig(sn); err != nil {
t.Fatal(err)
}
bs := c.mapDNSResponse(tt.toMap)
tt.assertFx(t, bs)
})
}
}