mirror of
https://github.com/cunnie/sslip.io.git
synced 2025-12-24 12:12:48 +08:00
🐞 IPv4 regex correctly identifies IPv4 addresses.
My regex was broken, and it didn't correctly identify the following hostnames lookups (real, actual hostname lookups): - funprdmongo30-03.10.1.4.133.nip.io. - olvm-engine-01.132.145.157.105.nip.io. - wt32-ETh01-03.172.26.131.29.NIp.IO. The problem? The leading zero. `funprdmongo30-03.10.1.4.133.nip.io.` should've resolved to `10.1.4.133`, but the regex incorrectly matched it as `03.10.1.4`, which isn't a valid IPv4 address according to Golang's net library, so the hostname DNS resolution was incorrectly treated as not having an A record. With this commit, the regex is now fixed, though I must admit I have a certain trepidation tinkering with the regex at the very core of sslip.io. - refactored a portion of the `NameToA()` function to another function, `String2IPv4()`. `NameToA()` is now more readable. - added simple fuzz test, but it wouldn't have discovered the problem. - added a helper method for the fuzz test, `RandomIPv4String()`, and bumped `math/rand` to `math/rand/v2` along the way.
This commit is contained in:
@@ -2,8 +2,10 @@ package testhelper
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RandomIPv6Address is used for fuzz testing
|
||||
@@ -32,11 +34,25 @@ func RandomIPv6Address() net.IP {
|
||||
func Random8ByteString() string {
|
||||
var randomString []byte
|
||||
// 71 == ascii 'G', +32 (103) == ascii 'g'
|
||||
randomString = append(randomString, byte(71+32*rand.Intn(2)+rand.Intn(20)))
|
||||
randomString = append(randomString, byte(71+32*rand.IntN(2)+rand.IntN(20)))
|
||||
for range 6 {
|
||||
// 65 == ascii 'A', +32 (96) == ascii 'a', there are 26 letters in the alphabet. Mix upper case, too.
|
||||
randomString = append(randomString, byte(65+32*rand.Intn(2)+rand.Intn(26)))
|
||||
randomString = append(randomString, byte(65+32*rand.IntN(2)+rand.IntN(26)))
|
||||
}
|
||||
randomString = append(randomString, byte(71+32*rand.Intn(2)+rand.Intn(20)))
|
||||
randomString = append(randomString, byte(71+32*rand.IntN(2)+rand.IntN(20)))
|
||||
return string(randomString)
|
||||
}
|
||||
|
||||
// returns a random IPv4 address, sometimes with dots, sometimes with dashes
|
||||
func RandomIPv4String() string {
|
||||
separator := "."
|
||||
if rand.IntN(2) == 1 {
|
||||
separator = "-"
|
||||
}
|
||||
ipString := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
ipString += strconv.Itoa(int(rand.IntN(256))) + separator
|
||||
}
|
||||
ipString = strings.TrimSuffix(ipString, separator)
|
||||
return ipString
|
||||
}
|
||||
|
||||
57
xip/xip.go
57
xip/xip.go
@@ -84,8 +84,8 @@ type DomainCustomizations map[string]DomainCustomization
|
||||
// Some of these are global because they are, in essence, constants which
|
||||
// I don't want to waste time recreating with every function call.
|
||||
var (
|
||||
ipv4REDots = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d))($|[.-])`)
|
||||
ipv4REDashes = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?\d)?\d)-){3}(25[0-5]|(2[0-4]|1?\d)?\d))($|[.-])`)
|
||||
ipv4REDots = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1\d|[1-9])?\d)\.){3}(25[0-5]|(2[0-4]|1\d|[1-9])?\d))($|[.-])`)
|
||||
ipv4REDashes = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1\d|[1-9])?\d)\-){3}(25[0-5]|(2[0-4]|1\d|[1-9])?\d))($|[.-])`)
|
||||
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))($|[.-])`)
|
||||
@@ -781,56 +781,53 @@ func buildNSRecords(b *dnsmessage.Builder, name dnsmessage.Name, nameServers []d
|
||||
// possibly more if it's a customized record (e.g. the addresses of "ns.sslip.io.")
|
||||
// if "allowPublicIPs" is false, and the IP address is public, it'll return an empty array
|
||||
func NameToA(fqdnString string, allowPublicIPs bool) []dnsmessage.AResource {
|
||||
fqdn := []byte(fqdnString)
|
||||
// is it a customized A record? If so, return early
|
||||
if domain, ok := Customizations[strings.ToLower(fqdnString)]; ok && len(domain.A) > 0 {
|
||||
return domain.A
|
||||
}
|
||||
ipv4 := String2IPv4(fqdnString)
|
||||
if ipv4 == nil {
|
||||
return []dnsmessage.AResource{}
|
||||
}
|
||||
if (!allowPublicIPs) && IsPublic(ipv4) {
|
||||
return []dnsmessage.AResource{}
|
||||
}
|
||||
return []dnsmessage.AResource{
|
||||
{A: [4]byte{ipv4[12], ipv4[13], ipv4[14], ipv4[15]}},
|
||||
}
|
||||
}
|
||||
|
||||
func String2IPv4(fqdn string) net.IP {
|
||||
for _, ipv4RE := range []*regexp.Regexp{ipv4REDashes, ipv4REDots} {
|
||||
if ipv4RE.Match(fqdn) {
|
||||
match := string(ipv4RE.FindSubmatch(fqdn)[2])
|
||||
if ipv4RE.MatchString(fqdn) {
|
||||
match := string(ipv4RE.FindStringSubmatch(fqdn)[2])
|
||||
match = strings.Replace(match, "-", ".", -1)
|
||||
ipv4address := net.ParseIP(match).To4()
|
||||
ipv4address := net.ParseIP(match)
|
||||
// We shouldn't reach here because `match` should always be valid, but we're not optimists
|
||||
if ipv4address == nil {
|
||||
// e.g. "ubuntu20.04.235.249.181-notify.sslip.io." <- the leading zero is the problem
|
||||
// funprdmongo30-03.10.1.4.133.nip.io.
|
||||
// olvm-engine-01.132.145.157.105.nip.io.
|
||||
// wt32-ETh01-03.172.26.131.29.NIp.IO.
|
||||
log.Printf("----> Should be valid A but isn't: %s\n", fqdn) // TODO: delete this
|
||||
return []dnsmessage.AResource{}
|
||||
}
|
||||
if (!allowPublicIPs) && IsPublic(ipv4address) {
|
||||
return []dnsmessage.AResource{}
|
||||
}
|
||||
return []dnsmessage.AResource{
|
||||
{A: [4]byte{ipv4address[0], ipv4address[1], ipv4address[2], ipv4address[3]}},
|
||||
fmt.Printf("----> match: %s, Should be valid A but isn't: %s\n", match, fqdn) // TODO: delete this
|
||||
} else {
|
||||
return ipv4address
|
||||
}
|
||||
}
|
||||
}
|
||||
if match := ipv4REHex.FindSubmatch(fqdn); match != nil {
|
||||
if match := ipv4REHex.FindStringSubmatch(fqdn); match != nil {
|
||||
hexes := match[2] // strip out leading & trailing "." by using only the 2nd capture group, e.g. "7f000001"
|
||||
ipBytes := make([]byte, 4)
|
||||
_, err := hex.Decode(ipBytes, []byte(hexes))
|
||||
if err != nil || len(ipBytes) != 4 {
|
||||
return []dnsmessage.AResource{}
|
||||
return nil
|
||||
}
|
||||
ipv4address := net.IPv4(ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3])
|
||||
if ipv4address == nil {
|
||||
return []dnsmessage.AResource{}
|
||||
return nil
|
||||
}
|
||||
ipv4address = ipv4address.To4()
|
||||
if ipv4address == nil {
|
||||
return []dnsmessage.AResource{}
|
||||
}
|
||||
if (!allowPublicIPs) && IsPublic(ipv4address) {
|
||||
return []dnsmessage.AResource{}
|
||||
}
|
||||
return []dnsmessage.AResource{
|
||||
{A: [4]byte{ipv4address[0], ipv4address[1], ipv4address[2], ipv4address[3]}},
|
||||
return nil
|
||||
}
|
||||
return ipv4address
|
||||
}
|
||||
return []dnsmessage.AResource{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NameToAAAA returns an []AAAAResource that matched the hostname; it returns an array of zero-or-one records
|
||||
|
||||
@@ -609,4 +609,24 @@ var _ = Describe("Xip", func() {
|
||||
Entry("Private internets", net.ParseIP("fc00::"), false),
|
||||
)
|
||||
})
|
||||
Describe("String2IPv4()", func() {
|
||||
DescribeTable("when determining if a name is IPv4",
|
||||
func(fqdn string, expectedPublic net.IP) {
|
||||
Expect(xip.String2IPv4(fqdn)).To(Equal(expectedPublic))
|
||||
},
|
||||
Entry("ubuntu20.04.52.0.56.137-notify.sslip.io.", "ubuntu20.04.52.0.56.137.181-notify.sslip.io.", net.ParseIP("52.0.56.137")),
|
||||
Entry("funprdmongo30-03.10.1.4.133.nip.io.", "funprdmongo30-03.10.1.4.133.nip.io.", net.ParseIP("10.1.4.133")),
|
||||
Entry("olvm-engine-01.132.145.157.105.nip.io.", "olvm-engine-01.132.145.157.105.nip.io.", net.ParseIP("132.145.157.105")),
|
||||
Entry("wt32-ETh01-03.172.26.131.29.NIp.IO.", "wt32-ETh01-03.172.26.131.29.NIp.IO.", net.ParseIP("172.26.131.29")),
|
||||
Entry("prepend-zeros.01.23.45.67.89.", "prepend-zeros.01.23.45.67.89.", net.ParseIP("23.45.67.89")),
|
||||
Entry("moar-zeros.23.45.67.09.", "prepend-zeros.23.45.67.09.", nil),
|
||||
)
|
||||
When("we're fuzz testing", func() {
|
||||
for i := 0; i < 1000; i++ {
|
||||
It("correctly identifies the IPv4 address", func() {
|
||||
Expect(xip.String2IPv4(testhelper.RandomIPv4String())).ToNot(BeNil())
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user