mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-05 08:54:09 -04:00
fix(ipv6): replace bad regex with custom IPv6 extract function
- Fix HTTP IPv6 fetching invalid extraction result - Affects IPv6 comparison for allinkl, dnsomatic, google, he and noip
This commit is contained in:
52
pkg/ipextract/ipextract.go
Normal file
52
pkg/ipextract/ipextract.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package ipextract
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IPv4 extracts all valid IPv4 addresses from a given
|
||||
// text string. Each IPv4 address must be separated by a character
|
||||
// not part of the IPv4 alphabet (0123456789.).
|
||||
// Performance-wise, this extraction is at least x3 times faster
|
||||
// than using a regular expression.
|
||||
func IPv4(text string) (addresses []netip.Addr) {
|
||||
const ipv4Alphabet = "0123456789."
|
||||
return extract(text, ipv4Alphabet)
|
||||
}
|
||||
|
||||
// IPv6 extracts all valid IPv6 addresses from a given
|
||||
// text string. Each IPv6 address must be separated by a character
|
||||
// not part of the IPv6 alphabet (0123456789abcdefABCDEF:).
|
||||
// Performance-wise, this extraction is at least x3 times faster
|
||||
// than using a regular expression.
|
||||
func IPv6(text string) (addresses []netip.Addr) {
|
||||
const ipv6Alphabet = "0123456789abcdefABCDEF:"
|
||||
return extract(text, ipv6Alphabet)
|
||||
}
|
||||
|
||||
func extract(text string, alphabet string) (addresses []netip.Addr) {
|
||||
var start, end int
|
||||
for {
|
||||
for i := start; i < len(text); i++ {
|
||||
r := rune(text[i])
|
||||
if !strings.ContainsRune(alphabet, r) {
|
||||
break
|
||||
}
|
||||
end++
|
||||
}
|
||||
|
||||
possibleIPString := text[start:end]
|
||||
ipAddress, err := netip.ParseAddr(possibleIPString)
|
||||
if err == nil { // Valid IP address found
|
||||
addresses = append(addresses, ipAddress)
|
||||
}
|
||||
|
||||
if end == len(text) {
|
||||
return addresses
|
||||
}
|
||||
|
||||
start = end + 1 // + 1 to skip non alphabet match character
|
||||
end = start
|
||||
}
|
||||
}
|
||||
137
pkg/ipextract/ipextract_test.go
Normal file
137
pkg/ipextract/ipextract_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package ipextract
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_IPv4(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
text string
|
||||
extracted []netip.Addr
|
||||
}{
|
||||
"empty": {},
|
||||
"one_ipv4": {
|
||||
text: "1.2.3.4",
|
||||
extracted: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
|
||||
},
|
||||
"two_ipv4": {
|
||||
text: " 1.2.3.4 x.x.2.2 5.6.7.8.9 10.11.12.13",
|
||||
extracted: []netip.Addr{
|
||||
netip.MustParseAddr("1.2.3.4"),
|
||||
netip.MustParseAddr("10.11.12.13"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
extracted := IPv4(testCase.text)
|
||||
|
||||
assert.Equal(t, testCase.extracted, extracted)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IPv6(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
text string
|
||||
extracted []netip.Addr
|
||||
}{
|
||||
"empty": {},
|
||||
"ipv6_compact": {
|
||||
text: "::1",
|
||||
extracted: []netip.Addr{netip.MustParseAddr("::1")},
|
||||
},
|
||||
"two_ipv6_compact": {
|
||||
text: ":1 ::1 ::0 ",
|
||||
extracted: []netip.Addr{
|
||||
netip.MustParseAddr("::1"),
|
||||
netip.MustParseAddr("::0"),
|
||||
},
|
||||
},
|
||||
"ipv6_A": {
|
||||
text: "2408:8256:480:3162::cef",
|
||||
extracted: []netip.Addr{netip.MustParseAddr("2408:8256:480:3162::cef")},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
extracted := IPv6(testCase.text)
|
||||
|
||||
assert.Equal(t, testCase.extracted, extracted)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Fuzz_IPv6(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, ipv6A, ipv6B, ipv6C []byte,
|
||||
garbageA, garbageB, garbageC string) {
|
||||
var arrayA [16]byte
|
||||
if len(ipv6A) > 0 {
|
||||
copy(arrayA[:], ipv6A)
|
||||
}
|
||||
|
||||
var arrayB [16]byte
|
||||
if len(ipv6B) > 0 {
|
||||
copy(arrayB[:], ipv6B)
|
||||
}
|
||||
|
||||
var arrayC [16]byte
|
||||
if len(ipv6C) > 0 {
|
||||
copy(arrayC[:], ipv6C)
|
||||
}
|
||||
|
||||
text := garbageA +
|
||||
netip.AddrFrom16(arrayA).String() +
|
||||
garbageB +
|
||||
netip.AddrFrom16(arrayB).String() +
|
||||
garbageC +
|
||||
netip.AddrFrom16(arrayC).String() +
|
||||
garbageA
|
||||
|
||||
_ = IPv6(text)
|
||||
})
|
||||
}
|
||||
|
||||
func Benchmark_IPv6(b *testing.B) {
|
||||
source := rand.NewSource(time.Now().UnixNano())
|
||||
generator := rand.New(source) //nolint:gosec
|
||||
|
||||
text := "garbage " +
|
||||
generateIPv6(generator) +
|
||||
"::99999" +
|
||||
generateIPv6(generator) +
|
||||
"1.2.3.4" +
|
||||
generateIPv6(generator) +
|
||||
" fac00"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = IPv6(text)
|
||||
}
|
||||
}
|
||||
|
||||
func generateIPv6(generator *rand.Rand) string {
|
||||
ipv6Bytes := make([]byte, 16)
|
||||
_, err := generator.Read(ipv6Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return netip.AddrFrom16([16]byte(ipv6Bytes)).String()
|
||||
}
|
||||
Reference in New Issue
Block a user