From d5d02f5999d3d945d7c7c6b7baba72da8e324798 Mon Sep 17 00:00:00 2001 From: Brian Cunnie Date: Sun, 22 Jun 2025 05:35:17 -0700 Subject: [PATCH] Resolve hexadecimal notation for IPv6 addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e.g. `00000000000000000000000000000001.nip.io` → ::1 This is to bring parity with IPv4's hexadecimal notation, though IPv6's hexadecimal notation is so clunky that I doubt it'll ever be used. - The hexadecimal-notated IPv6 must be exactly 32 hexadecimal characters, no separators. - Any hexadecimal notation _must_ be bookended by dots or by the beginning or end of the string (www.0000000000000000000000000000001.sslip.io or 00000000000000000000000000000001.sslip.io). No dashes. - If a normal IP notation and a hex notation are in the same hostname, then the normal IP notation takes precedence. This preserves existing behavior for sslip.io users, e.g. (00000000000000000000000000000001.2600--.nip.io resolves to 2600::, not ::1) --- integration_flags_test.go | 48 ++++++++++++++++++++----------- xip/xip.go | 59 ++++++++++++++++++++++++++------------- xip/xip_test.go | 16 ++++++++++- 3 files changed, 87 insertions(+), 36 deletions(-) diff --git a/integration_flags_test.go b/integration_flags_test.go index 846b7b3..f0be1dc 100644 --- a/integration_flags_test.go +++ b/integration_flags_test.go @@ -136,22 +136,6 @@ var _ = Describe("flags", func() { Eventually(digSession, 1).Should(Exit(0)) Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 8-8-8-8\.sslip\.io\. briancunnie\.gmail\.com\.`)) }) - It("doesn't resolve public IPv4 addresses (hexadecimal)", func() { - digArgs := "@localhost 08080808.sslip.io -p " + strconv.Itoa(port) - digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) - digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) - Expect(err).ToNot(HaveOccurred()) - Eventually(digSession, 1).Should(Exit(0)) - Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 08080808\.sslip\.io\. briancunnie\.gmail\.com\.`)) - }) - It("resolves private IPv4 addresses (hexadecimal)", func() { - digArgs := "@localhost 7f000001.sslip.io -p " + strconv.Itoa(port) - digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) - digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) - Expect(err).ToNot(HaveOccurred()) - Eventually(digSession, 1).Should(Exit(0)) - Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA 7f000001.sslip.io. \? 127.0.0.1`)) - }) It("doesn't resolve public IPv6 addresses", func() { digArgs := "@localhost aaaa 2600--.sslip.io -p " + strconv.Itoa(port) digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) @@ -160,6 +144,22 @@ var _ = Describe("flags", func() { Eventually(digSession, 1).Should(Exit(0)) Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 2600--\.sslip\.io\. briancunnie\.gmail\.com\.`)) }) + It("doesn't resolve public IPv4 addresses (hexadecimal)", func() { + digArgs := "@localhost 08080808.nip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 08080808\.nip\.io\. briancunnie\.gmail\.com\.`)) + }) + It("doesn't resolve public IPv6 addresses (hexadecimal)", func() { + digArgs := "@localhost aaaa 26010646010069f0042c6ab3cdd9e562.nip.io -p " + strconv.Itoa(port) // my laptop's IPv6 address + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 26010646010069f0042c6ab3cdd9e562\.nip\.io\. briancunnie\.gmail\.com\.`)) + }) It("resolves private IPv4 addresses", func() { digArgs := "@localhost 192-168-0-1.sslip.io -p " + strconv.Itoa(port) digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) @@ -176,6 +176,22 @@ var _ = Describe("flags", func() { Eventually(digSession, 1).Should(Exit(0)) Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`fc00--\.sslip\.io\. \? fc00::`)) }) + It("resolves private IPv4 addresses (hexadecimal)", func() { + digArgs := "@localhost 7f000001.nip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA 7f000001.nip.io. \? 127.0.0.1`)) + }) + It("resolves private IPv6 addresses (hexadecimal)", func() { + digArgs := "@localhost aaaa 00000000000000000000000000000001.nip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeAAAA 00000000000000000000000000000001.nip.io. \? ::1`)) + }) }) When("-delegates is set", func() { BeforeEach(func() { diff --git a/xip/xip.go b/xip/xip.go index a232f24..19f0239 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -87,6 +87,7 @@ var ( ipv4REHex = regexp.MustCompile(`(^|\.)([[:xdigit:]]{8})($|\.)`) // no dash separators, only dots // https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses ipv6RE = regexp.MustCompile(`(^|[.-])(([[:xdigit:]]{1,4}-){7}[[:xdigit:]]{1,4}|([[:xdigit:]]{1,4}-){1,7}-|([[:xdigit:]]{1,4}-){1,6}-[[:xdigit:]]{1,4}|([[:xdigit:]]{1,4}-){1,5}(-[[:xdigit:]]{1,4}){1,2}|([[:xdigit:]]{1,4}-){1,4}(-[[:xdigit:]]{1,4}){1,3}|([[:xdigit:]]{1,4}-){1,3}(-[[:xdigit:]]{1,4}){1,4}|([[:xdigit:]]{1,4}-){1,2}(-[[:xdigit:]]{1,4}){1,5}|[[:xdigit:]]{1,4}-((-[[:xdigit:]]{1,4}){1,6})|-((-[[:xdigit:]]{1,4}){1,7}|-)|fe80-(-[[:xdigit:]]{0,4}){0,4}%[\da-zA-Z]+|--(ffff(-0{1,4})?-)?((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d)|([[:xdigit:]]{1,4}-){1,4}-((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d))($|[.-])`) + ipv6REHex = regexp.MustCompile(`(^|\.)([[:xdigit:]]{32})($|\.)`) // no dash separators, only dots ipv4ReverseRE = regexp.MustCompile(`^(.*)\.in-addr\.arpa\.$`) ipv6ReverseRE = regexp.MustCompile(`^(([[:xdigit:]]\.){32})ip6\.arpa\.`) dns01ChallengeRE = regexp.MustCompile(`(?i)_acme-challenge\.`) // (?i) → non-capturing case insensitive @@ -815,27 +816,47 @@ func NameToAAAA(fqdnString string, allowPublicIPs bool) []dnsmessage.AAAAResourc if domain, ok := Customizations[strings.ToLower(fqdnString)]; ok && len(domain.AAAA) > 0 { return domain.AAAA } - if !ipv6RE.Match(fqdn) { - return []dnsmessage.AAAAResource{} + if ipv6RE.Match(fqdn) { + ipv6RE.Longest() + match := string(ipv6RE.FindSubmatch(fqdn)[2]) + match = strings.Replace(match, "-", ":", -1) + ipv16address := net.ParseIP(match).To16() + if ipv16address == nil { + // We shouldn't reach here because `match` should always be valid, but we're not optimists + log.Printf("----> Should be valid AAAA but isn't: %s\n", fqdn) // TODO: delete this + return []dnsmessage.AAAAResource{} + } + if (!allowPublicIPs) && IsPublic(ipv16address) { + return []dnsmessage.AAAAResource{} + } + AAAAR := dnsmessage.AAAAResource{} + for i := range ipv16address { + AAAAR.AAAA[i] = ipv16address[i] + } + return []dnsmessage.AAAAResource{AAAAR} } - - ipv6RE.Longest() - match := string(ipv6RE.FindSubmatch(fqdn)[2]) - match = strings.Replace(match, "-", ":", -1) - ipv16address := net.ParseIP(match).To16() - if ipv16address == nil { - // We shouldn't reach here because `match` should always be valid, but we're not optimists - log.Printf("----> Should be valid AAAA but isn't: %s\n", fqdn) // TODO: delete this - return []dnsmessage.AAAAResource{} + if match := ipv6REHex.FindSubmatch(fqdn); match != nil { + hexes := match[2] // strip out leading & trailing "." by using only the 2nd capture group + ipBytes := make([]byte, 16) + _, err := hex.Decode(ipBytes, []byte(hexes)) + if err != nil || len(ipBytes) != 16 { + return []dnsmessage.AAAAResource{} + } + ipv6address := net.IP(ipBytes) + if ipv6address == nil { + return []dnsmessage.AAAAResource{} + } + if (!allowPublicIPs) && IsPublic(ipv6address) { + return []dnsmessage.AAAAResource{} + } + return []dnsmessage.AAAAResource{ + {AAAA: [16]byte{ipv6address[0], ipv6address[1], ipv6address[2], ipv6address[3], + ipv6address[4], ipv6address[5], ipv6address[6], ipv6address[7], + ipv6address[8], ipv6address[9], ipv6address[10], ipv6address[11], + ipv6address[12], ipv6address[13], ipv6address[14], ipv6address[15]}}, + } } - if (!allowPublicIPs) && IsPublic(ipv16address) { - return []dnsmessage.AAAAResource{} - } - AAAAR := dnsmessage.AAAAResource{} - for i := range ipv16address { - AAAAR.AAAA[i] = ipv16address[i] - } - return []dnsmessage.AAAAResource{AAAAR} + return []dnsmessage.AAAAResource{} } // CNAMEResource returns the CNAME via Customizations, otherwise nil diff --git a/xip/xip_test.go b/xip/xip_test.go index d6193cb..0fdb489 100644 --- a/xip/xip_test.go +++ b/xip/xip_test.go @@ -351,8 +351,8 @@ var _ = Describe("Xip", func() { DescribeTable("when it succeeds", func(fqdn string, expectedAAAA dnsmessage.AAAAResource) { ipv6Answers := xip.NameToAAAA(fqdn, true) - Expect(len(ipv6Answers)).To(Equal(1)) Expect(ipv6Answers[0]).To(Equal(expectedAAAA)) + Expect(len(ipv6Answers)).To(Equal(1)) }, // dashes only Entry("loopback", "--1", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), @@ -362,6 +362,16 @@ var _ = Describe("Xip", func() { Entry("Browsing the logs", "2006-41d0-2-e01e--56dB-3598.sSLIP.io.", dnsmessage.AAAAResource{AAAA: [16]byte{32, 6, 65, 208, 0, 2, 224, 30, 0, 0, 0, 0, 86, 219, 53, 152}}), Entry("Browsing the logs", "1-2-3--4-5-6.sSLIP.io.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 4, 0, 5, 0, 6}}), Entry("Browsing the logs", "1--2-3-4-5-6.sSLIP.io.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 1, 0, 0, 0, 0, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6}}), + Entry("Hexadecimal #0", "filer.00000000000000000000000000000001.sslip.io", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #1, TLD", "00000000000000000000000000000001", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #1, TLD #2", "00000000000000000000000000000001.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #1, TLD #3", ".00000000000000000000000000000001.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #1, TLD #4", "www.00000000000000000000000000000001.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #2, mixed case", "89abcdef0000000089ABCDEF00000000.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{137, 171, 205, 239, 0, 0, 0, 0, 137, 171, 205, 239, 0, 0, 0, 0}}), + Entry("Hexadecimal #3, different numbers", "www.0123456789abcdef0123456789abcdef.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{1, 35, 69, 103, 137, 171, 205, 239, 1, 35, 69, 103, 137, 171, 205, 239}}), + Entry("Hexadecimal #3, different numbers #2", "www.00000000000000000000000000000001.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #4, dashes trump hex", "www.2600--.00000000000000000000000000000001.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}), + Entry("Hexadecimal #4, dashes trump hex #2", "www.00000000000000000000000000000001.--2.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}}), ) DescribeTable("when it does not match an IP address", func(fqdn string) { @@ -374,6 +384,10 @@ var _ = Describe("Xip", func() { Entry("www", "www.sslip.io"), Entry("a 1 without double-dash", "-1"), Entry("too big", "--g"), + Entry("Hexadecimal with too many digits (33 instead of 32)", "www.0123456789abcdef0123456789abcdef0.com"), + Entry("Hexadecimal with too few digits (31 instead of 32)", "www.0123456789abcdef0123456789abcde.com"), + Entry("Hexadecimal with a dash instead of a .", "www-0123456789abcdef0123456789abcdef.com"), + Entry("Hexadecimal with a dash instead of a . #2", "www.0123456789abcdef0123456789abcdef-com"), ) When("using randomly generated IPv6 addresses (fuzz testing)", func() { It("should succeed every time", func() {