diff --git a/dns.go b/dns.go index 5a4f682a..df896090 100644 --- a/dns.go +++ b/dns.go @@ -14,6 +14,11 @@ const ( ByteSize = 8 ) +const ( + ipv4AddressLength = 32 + ipv6AddressLength = 128 +) + // generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`. // This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS // server (listening in 100.100.100.100 udp/53) should be used for. @@ -37,22 +42,25 @@ const ( func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN { fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes)) for _, ipPrefix := range ipPrefixes { - var generateDnsRoot func(netaddr.IPPrefix) []dnsname.FQDN + var generateDNSRoot func(netaddr.IPPrefix) []dnsname.FQDN switch ipPrefix.IP().BitLen() { - case 32: - generateDnsRoot = generateIPv4DNSRootDomain + case ipv4AddressLength: + generateDNSRoot = generateIPv4DNSRootDomain + + case ipv6AddressLength: + generateDNSRoot = generateIPv6DNSRootDomain default: panic(fmt.Sprintf("unsupported IP version with address length %d", ipPrefix.IP().BitLen())) } - fqdns = append(fqdns, generateDnsRoot(ipPrefix)...) + fqdns = append(fqdns, generateDNSRoot(ipPrefix)...) } return fqdns } -func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) (fqdns []dnsname.FQDN) { +func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN { // Conversion to the std lib net.IPnet, a bit easier to operate netRange := ipPrefix.IPNet() maskBits, _ := netRange.Mask.Size() @@ -76,6 +84,7 @@ func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) (fqdns []dnsname.FQDN) rdnsSlice = append(rdnsSlice, "in-addr.arpa.") rdnsBase := strings.Join(rdnsSlice, ".") + fqdns := make([]dnsname.FQDN, 0, max-min+1) for i := min; i <= max; i++ { fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.%s", i, rdnsBase)) if err != nil { @@ -84,7 +93,55 @@ func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) (fqdns []dnsname.FQDN) fqdns = append(fqdns, fqdn) } - return + return fqdns +} + +func generateIPv6DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN { + const nibbleLen = 4 + + maskBits, _ := ipPrefix.IPNet().Mask.Size() + expanded := ipPrefix.IP().StringExpanded() + nibbleStr := strings.Map(func(r rune) rune { + if r == ':' { + return -1 + } + + return r + }, expanded) + + // TODO?: that does not look the most efficient implementation, + // but the inputs are not so long as to cause problems, + // and from what I can see, the generateMagicDNSRootDomains + // function is called only once over the lifetime of a server process. + prefixConstantParts := []string{} + for i := 0; i < maskBits/nibbleLen; i++ { + prefixConstantParts = append([]string{string(nibbleStr[i])}, prefixConstantParts...) + } + + makeDomain := func(variablePrefix ...string) (dnsname.FQDN, error) { + prefix := strings.Join(append(variablePrefix, prefixConstantParts...), ".") + + return dnsname.ToFQDN(fmt.Sprintf("%s.ip6.arpa", prefix)) + } + + var fqdns []dnsname.FQDN + if maskBits%4 == 0 { + dom, _ := makeDomain() + fqdns = append(fqdns, dom) + } else { + domCount := 1 << (maskBits % nibbleLen) + fqdns = make([]dnsname.FQDN, 0, domCount) + for i := 0; i < domCount; i++ { + varNibble := fmt.Sprintf("%x", i) + dom, err := makeDomain(varNibble) + if err != nil { + continue + } + fqdns = append(fqdns, dom) + } + } + + return fqdns } func getMapResponseDNSConfig( diff --git a/dns_test.go b/dns_test.go index 8c0da638..f8f7fb96 100644 --- a/dns_test.go +++ b/dns_test.go @@ -10,8 +10,10 @@ import ( ) func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { - prefix := netaddr.MustParseIPPrefix("100.64.0.0/10") - domains := generateMagicDNSRootDomains(prefix) + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("100.64.0.0/10"), + } + domains := generateMagicDNSRootDomains(prefixes) found := false for _, domain := range domains { @@ -45,8 +47,10 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { } func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { - prefix := netaddr.MustParseIPPrefix("172.16.0.0/16") - domains := generateMagicDNSRootDomains(prefix) + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("172.16.0.0/16"), + } + domains := generateMagicDNSRootDomains(prefixes) found := false for _, domain := range domains { @@ -69,6 +73,40 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { c.Assert(found, check.Equals, true) } +// Happens when netmask is a multiple of 4 bits (sounds likely). +func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) { + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48"), + } + domains := generateMagicDNSRootDomains(prefixes) + + c.Assert(len(domains), check.Equals, 1) + c.Assert(domains[0].WithTrailingDot(), check.Equals, "0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") +} + +func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) { + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/50"), + } + domains := generateMagicDNSRootDomains(prefixes) + + yieldsRoot := func(dom string) bool { + for _, candidate := range domains { + if candidate.WithTrailingDot() == dom { + return true + } + } + + return false + } + + c.Assert(len(domains), check.Equals, 4) + c.Assert(yieldsRoot("0.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) + c.Assert(yieldsRoot("1.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) + c.Assert(yieldsRoot("2.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) + c.Assert(yieldsRoot("3.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) +} + func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { namespaceShared1, err := app.CreateNamespace("shared1") c.Assert(err, check.IsNil)