Resolve hexadecimal notation for IPv6 addresses

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)
This commit is contained in:
Brian Cunnie
2025-06-22 05:35:17 -07:00
parent 08ca8e4e85
commit d5d02f5999
3 changed files with 87 additions and 36 deletions

View File

@@ -136,22 +136,6 @@ var _ = Describe("flags", func() {
Eventually(digSession, 1).Should(Exit(0)) Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 8-8-8-8\.sslip\.io\. briancunnie\.gmail\.com\.`)) 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() { It("doesn't resolve public IPv6 addresses", func() {
digArgs := "@localhost aaaa 2600--.sslip.io -p " + strconv.Itoa(port) digArgs := "@localhost aaaa 2600--.sslip.io -p " + strconv.Itoa(port)
digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) digCmd := exec.Command("dig", strings.Split(digArgs, " ")...)
@@ -160,6 +144,22 @@ var _ = Describe("flags", func() {
Eventually(digSession, 1).Should(Exit(0)) Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 2600--\.sslip\.io\. briancunnie\.gmail\.com\.`)) 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() { It("resolves private IPv4 addresses", func() {
digArgs := "@localhost 192-168-0-1.sslip.io -p " + strconv.Itoa(port) digArgs := "@localhost 192-168-0-1.sslip.io -p " + strconv.Itoa(port)
digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) digCmd := exec.Command("dig", strings.Split(digArgs, " ")...)
@@ -176,6 +176,22 @@ var _ = Describe("flags", func() {
Eventually(digSession, 1).Should(Exit(0)) Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`fc00--\.sslip\.io\. \? fc00::`)) 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() { When("-delegates is set", func() {
BeforeEach(func() { BeforeEach(func() {

View File

@@ -87,6 +87,7 @@ var (
ipv4REHex = regexp.MustCompile(`(^|\.)([[:xdigit:]]{8})($|\.)`) // no dash separators, only dots ipv4REHex = regexp.MustCompile(`(^|\.)([[:xdigit:]]{8})($|\.)`) // no dash separators, only dots
// https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses // 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))($|[.-])`) 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\.$`) ipv4ReverseRE = regexp.MustCompile(`^(.*)\.in-addr\.arpa\.$`)
ipv6ReverseRE = regexp.MustCompile(`^(([[:xdigit:]]\.){32})ip6\.arpa\.`) ipv6ReverseRE = regexp.MustCompile(`^(([[:xdigit:]]\.){32})ip6\.arpa\.`)
dns01ChallengeRE = regexp.MustCompile(`(?i)_acme-challenge\.`) // (?i) → non-capturing case insensitive dns01ChallengeRE = regexp.MustCompile(`(?i)_acme-challenge\.`) // (?i) → non-capturing case insensitive
@@ -815,10 +816,7 @@ func NameToAAAA(fqdnString string, allowPublicIPs bool) []dnsmessage.AAAAResourc
if domain, ok := Customizations[strings.ToLower(fqdnString)]; ok && len(domain.AAAA) > 0 { if domain, ok := Customizations[strings.ToLower(fqdnString)]; ok && len(domain.AAAA) > 0 {
return domain.AAAA return domain.AAAA
} }
if !ipv6RE.Match(fqdn) { if ipv6RE.Match(fqdn) {
return []dnsmessage.AAAAResource{}
}
ipv6RE.Longest() ipv6RE.Longest()
match := string(ipv6RE.FindSubmatch(fqdn)[2]) match := string(ipv6RE.FindSubmatch(fqdn)[2])
match = strings.Replace(match, "-", ":", -1) match = strings.Replace(match, "-", ":", -1)
@@ -836,6 +834,29 @@ func NameToAAAA(fqdnString string, allowPublicIPs bool) []dnsmessage.AAAAResourc
AAAAR.AAAA[i] = ipv16address[i] AAAAR.AAAA[i] = ipv16address[i]
} }
return []dnsmessage.AAAAResource{AAAAR} return []dnsmessage.AAAAResource{AAAAR}
}
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]}},
}
}
return []dnsmessage.AAAAResource{}
} }
// CNAMEResource returns the CNAME via Customizations, otherwise nil // CNAMEResource returns the CNAME via Customizations, otherwise nil

View File

@@ -351,8 +351,8 @@ var _ = Describe("Xip", func() {
DescribeTable("when it succeeds", DescribeTable("when it succeeds",
func(fqdn string, expectedAAAA dnsmessage.AAAAResource) { func(fqdn string, expectedAAAA dnsmessage.AAAAResource) {
ipv6Answers := xip.NameToAAAA(fqdn, true) ipv6Answers := xip.NameToAAAA(fqdn, true)
Expect(len(ipv6Answers)).To(Equal(1))
Expect(ipv6Answers[0]).To(Equal(expectedAAAA)) Expect(ipv6Answers[0]).To(Equal(expectedAAAA))
Expect(len(ipv6Answers)).To(Equal(1))
}, },
// dashes only // 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}}), 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", "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, 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("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", DescribeTable("when it does not match an IP address",
func(fqdn string) { func(fqdn string) {
@@ -374,6 +384,10 @@ var _ = Describe("Xip", func() {
Entry("www", "www.sslip.io"), Entry("www", "www.sslip.io"),
Entry("a 1 without double-dash", "-1"), Entry("a 1 without double-dash", "-1"),
Entry("too big", "--g"), 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() { When("using randomly generated IPv6 addresses (fuzz testing)", func() {
It("should succeed every time", func() { It("should succeed every time", func() {