diff --git a/.githooks/pre-commit/01-docgen b/.githooks/pre-commit/01-docgen index c51148f..21604dc 100755 --- a/.githooks/pre-commit/01-docgen +++ b/.githooks/pre-commit/01-docgen @@ -58,7 +58,7 @@ for f in $(find . -type f -iname "README.adoc"); do # * https://docbook.org/ # * LaTex # * Allows for *very* extensive domain-specific ligature/representation (very common in mathematic/scientific literature) - # * But nigh unreadable by human eyes unless you've rather familiar with it + # * But nigh unreadable by human eyes unless you're rather familiar with it # * Parsing/rendering support about on-par with DocBook # * https://www.latex-project.org/ # diff --git a/go.mod b/go.mod index 603a222..c4007af 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,18 @@ module r00t2.io/goutils -go 1.25.0 +go 1.26 require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/coreos/go-systemd/v22 v22.7.0 github.com/davecgh/go-spew v1.1.1 + github.com/deckarep/golang-set/v2 v2.9.0 github.com/google/uuid v1.6.0 github.com/olekukonko/tablewriter v1.1.4 - github.com/shirou/gopsutil/v4 v4.26.5 + github.com/shirou/gopsutil/v4 v4.26.6 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/sys v0.46.0 - r00t2.io/sysutils v1.16.2 + golang.org/x/sys v0.47.0 + r00t2.io/sysutils v1.16.5 ) require ( @@ -22,12 +23,12 @@ require ( github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/djherbis/times v1.6.0 // indirect - github.com/ebitengine/purego v0.10.1 // indirect + github.com/ebitengine/purego v0.10.2 // indirect github.com/fatih/color v1.19.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/huandu/xstrings v1.5.0 // indirect - github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect + github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect @@ -42,6 +43,7 @@ require ( github.com/tklauser/go-sysconf v0.4.0 // indirect github.com/tklauser/numcpus v0.12.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/sync v0.21.0 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/sync v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 52e2f44..bebec67 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4M5yOCKI= +github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= @@ -26,6 +28,8 @@ github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= +github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -51,6 +55,8 @@ github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6 github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15 h1:YkjVPl/YH5XlJ+/NiwzJtPYXXKRcyjmEUhsDci6YK3c= +github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= @@ -81,6 +87,8 @@ github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cI github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM= github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= +github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -97,6 +105,8 @@ github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqo github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= @@ -105,10 +115,14 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -119,7 +133,11 @@ golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= r00t2.io/sysutils v1.16.2 h1:wI01UwZ/bXn/lzBiCpqDmzZCOWiK87kz04SB4xRw+W0= r00t2.io/sysutils v1.16.2/go.mod h1:iXK+ALOwIdRKjAJIE5USlkZ669SVDHBNNuYhunsznH8= +r00t2.io/sysutils v1.16.5 h1:0v/HWTLOl8UN8B6koXM3e+TWN1N3eAoUM+xGK+itwq0= +r00t2.io/sysutils v1.16.5/go.mod h1:GAVZuZeLWiFgzfVVL/6oHgtGDm+h/tT5z11DwPS+fSU= diff --git a/netx/NOTES.adoc b/netx/NOTES.adoc new file mode 100644 index 0000000..0a36f44 --- /dev/null +++ b/netx/NOTES.adoc @@ -0,0 +1,206 @@ += `netx` Notes +Brent Saner +Last rendered {localdatetime} +:doctype: book +:docinfo: shared +:data-uri: +:imagesdir: images +:sectlinks: +:sectnums: +:sectnumlevels: 7 +:toc: preamble +:toc2: left +:idprefix: +:toclevels: 7 +:source-highlighter: rouge +:docinfo: shared + +[id="4in6"] +== IPv4 Addresses as IPv6 +There are, effectively, two RFC-specified ways of representing an IPv4 address in IPv6 notation/address space/context. + +Per https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5[RFC 4291 § 2.5.5^]: + +* _IPv4-Compatible IPv6 Address_ (https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1[RFC 4291 § 2.5.5.1^]) -- `::/96` +** e.g. `::192.0.2.10` +* _IPv4-Mapped IPv6 Address_ (https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2[RFC 4291 § 2.5.5.2^]) -- `::ffff:0:0/96` +** e.g. `::ffff:192.0.2.10` + +The former is considered deprecated, whereas the latter is considered the modern proper representation. +Some naïve or legacy software, however, still use the former. + +An IPv6 address is *128 bits* (hence a prefix length/mask of `/128` representing a complete address), or *16 bytes* (`128 / 8`). + +An IPv4 address is *32 bits* (hence a prefix length/mask of `/32` representing a complete address), or *4 bytes* (`32 / 8`). + +This is why both the "IPv4-compatible" and "IPv4-mapped" IPv6 addresses are within a reserved prefix length of 96 bits (`/96`) - it leaves a remaining +*32 bits* (`128 - 96`) for a complete IPv4 address. + +[IMPORTANT] +==== +The IPv4-Compatible and IPv4-Mapped prefixes *are not* intended for routing! + +They are used for *syntactic/contextual* use. + +For routing/actual IPv4 <=> IPv6 translation, see https://datatracker.ietf.org/doc/html/rfc6052[RFC 6052^] and https://datatracker.ietf.org/doc/html/rfc8215[RFC 8215^]. + +This library may add supporting functions for these _translation_ prefixes at a later time. +==== + +[id="4in6_addr"] +=== Addresses +While https://pkg.go.dev/net/netip#ParseAddr[`net/netip.ParseAddr`^] (or https://pkg.go.dev/net/netip#MustParseAddr[`net/netip.MustParseAddr`^]) will handle +*parsing* the "IPv4-Compatible" format just fine, it will treat it *as* an IPv6 address -- not an "IPv6-wrapped/IPv6-mapped IPv4" address. + +[%collapsible] +.For example... +==== +[source,go] +---- +package main + +import ( + "fmt" + "net/netip" +) + +func main() { + fmt.Printf( + "::192.0.2.10 -> %s\n", + netip.MustParseAddr("::192.0.2.10").String(), + ) +} +---- + +will print: + +[source,text] +---- +::192.0.2.10 -> ::c000:20a +---- +==== + +For addresses, this is an easy enough fix: + +. Take the IPv6 as a byte slice, `b` (`b = ip4in6.AsSlice()`) +. Slice out the last 4 bytes as `b2` (`b2 = b[12:16]`) +. Parse `b2` as an IPv4 address (`ip4 = netip.FromSlice(b2)`) + +For the visual learners, the original address (`::192.0.2.10` or, more properly, `::c000:20a`) can be represented in multiple ways. + +[%collapsible] +.For instance... +==== +* A "canonical"/shorted IPv6 address: `::c000:20a` +* A fully "exploded" IPv6 address: `0000:0000:0000:0000:0000:0000:c000:020a` +* Hex bytes: +** `00:00:00:00:00:00:00:00:00:00:00:00:C0:00:02:0A` +** `0x000000000000000000000000C000020A` +** Via `xxd(1)`: ++ +[source,text] +---- +# xxd -c4 -g 1 +00000000: 00 00 00 00 .... +00000004: 00 00 00 00 .... +00000008: 00 00 00 00 .... +0000000c: c0 00 02 0a .... +---- +** Via `hexdump(1)`: ++ +[source,text] +---- +# hexdump -v -e '"%08_ax: " 4/1 "0x%02X ""\n"' +00000000: 0x00 0x00 0x00 0x00 +00000004: 0x00 0x00 0x00 0x00 +00000008: 0x00 0x00 0x00 0x00 +0000000c: 0xC0 0x00 0x02 0x0A +---- +* A Go byte slice: +** Either as explicit `byte`: ++ +[source,go] +---- +[]byte{ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x02, 0x0A, +} +---- +** Or as `uint8` (a `byte` is, quite literally, https://go.dev/ref/spec#Numeric_types[just a type alias for `uint8`^]): ++ +[source,go] +---- +[]byte{ + uint8(0), uint8(0), uint8(0), uint8(0), + uint8(0), uint8(0), uint8(0), uint8(0), + uint8(0), uint8(0), uint8(0), uint8(0), + uint8(192), uint8(0), uint8(2), uint8(10), +} +---- +==== + +When seeing it represented as a slice of `uint8`, some things should start to click mentally looking at the last +four bytes. + +An IPv4 address can then be derived by taking the last four bytes (`[]byte{...}[12:16]`): + +* `[]byte{0xC0, 0x00, 0x02, 0x0A}` +* `[]byte(uint8(192), uint8(0), uint8(2), uint8(10))` + +As you may have guessed by now, this is the byte format of the IPv4 address `192.0.2.10`. + +The `netx.UnmapAddr()` function handles this cleanly. + +''' + +If you instead prefer to think in _prefixing/masking_, the last four bytes are *32 bits* (`8 * 4`) -- the size of an IPv4 address. + +The "compatible" mapping is '::/96', and an IPv6 address is *128 bits* (*16 bytes*, `128 / 8`) total -- `128 - 96 == 32 bits`, or *4 bytes*. + +[id="4in6_pfx"] +=== Prefixes +Prefixes are a little more tricky. + +Prefixing for IPv4-Compatible/IPv4-Mapped IPv6 addresses works a bit oddly because generally the "4-in-6" addresses are +meant to be purely representative - a way to represent IPv4 addresses in IPv6 implementations. + +But there are cases where one needs a *prefix* represented. +This leads to the awkward case where an IPv4 prefix length (e.g. `/24`) needs to be represented as an IPv6 prefix length (e.g. `/120`). + +While this module (`netx`) does offer helper functions to calculate this (`netx.IP4MapPfx6to4()`, `netx.IP4MapPfx4to6()`), +the `netx.UnmapPfx()` function is offered as a convenience function that handles the translation from an IPv6 representation +of an IPv4 prefix into a fully "native" IPv4 prefix. + +[NOTE] +==== +`netx.IP4MapPfx4to6(len4 uint8)` simply takes the prefix length `len4`, and (if `len4 <= 32`) adds `96`. + +`netx.IP4MapPfx6to4(len6 uint8)` simply takes the prefix length `len6`, and (if `len6 >= 96`) subtracts `96`. +==== + +This is achieved by: + +. Taking the address from the prefix (`ip4in6 = pfx4in6.Addr()`) +.. And <<4in6_addr, converting to native IPv4>> (`ip4 = netx.UnmapAddr(ip4in6)`) +. Taking the IPv6 prefix length (`len6 = pfx4in6.Bits()`) +.. And converting it to an IPv4 prefix length (`len4 = netx.IP4MapPfx6to4(uint8(len6))`) +. Then re-assembling (`pfx4 = netip.PrefixFrom(ip4, int(len4))`) + + +[id="misc"] +== Miscellaneous + +[id="misc_unspec"] +=== Unspecified Address +There are two "unspecified" addresses for each family: + +* IPv4 +** `0.0.0.0/0` +** `0.0.0.0/32` +* IPv6 +** `::/0` +** `::/128` + +For each family, the `/0` prefix length diff --git a/netx/consts.go b/netx/consts.go index 4f5c113..46a7707 100644 --- a/netx/consts.go +++ b/netx/consts.go @@ -1,10 +1,52 @@ package netx import ( - `net/netip` + "net/netip" + + "go4.org/netipx" +) + +const ( + ip4in6Legacy96 string = "::" + ip4in6Modern96 string = "::ffff:" ) var ( - ip4In6Legacy netip.Prefix = netip.MustParsePrefix("::/96") - ip4In6Modern netip.Prefix = netip.MustParsePrefix("::ffff:0:0/96") + // IPv4 documentation prefixes (RFC 5737) + ipDoc4pfxs []netip.Prefix = []netip.Prefix{ + netip.MustParsePrefix("192.0.2.0/24"), + netip.MustParsePrefix("198.51.100.0/24"), + netip.MustParsePrefix("203.0.113.0/24"), + } + ipDoc4 *netipx.IPSet = MustIPSetFromNative(nil, ipDoc4pfxs, nil) + // IPv6 documentation prefixes (RFC 3849, RFC 9637) + ipDoc6pfxs []netip.Prefix = []netip.Prefix{ + netip.MustParsePrefix("2001:db8::/32"), + netip.MustParsePrefix("3fff::/20"), + } + ipDoc6 *netipx.IPSet = MustIPSetFromNative(nil, ipDoc6pfxs, nil) + // combined documentation prefixes + ipDoc *netipx.IPSet = MustIPSetCombined( + []*netipx.IPSet{ + ipDoc4, + ipDoc6, + }, + ) + + // CGNAT/LSN (RFC 6598) + cgnat4 netip.Prefix = netip.MustParsePrefix("100.64.0.0/10") + + // IPv4-Compatible IPv6 Address, RFC 4291 § 2.5.5.1 + ip4in6Legacy netip.Prefix = netip.MustParsePrefix(ip4in6Legacy96 + "/96") + // IPv4-Mapped IPv6 Address, RFC 4291 § 2.5.5.2 + ip4in6Modern netip.Prefix = netip.MustParsePrefix(ip4in6Modern96 + "0:0/96") + // both + ip4in6 *netipx.IPSet = MustIPSetFromNative( + nil, + []netip.Prefix{ + ip4in6Legacy, + ip4in6Modern, + }, + nil, + ) ) diff --git a/netx/consts_nix.go b/netx/consts_nix.go index f2b0dfb..84f9ef0 100644 --- a/netx/consts_nix.go +++ b/netx/consts_nix.go @@ -3,7 +3,7 @@ package netx import ( - `golang.org/x/sys/unix` + "golang.org/x/sys/unix" ) const ( diff --git a/netx/consts_windows.go b/netx/consts_windows.go index caa93b7..b822c45 100644 --- a/netx/consts_windows.go +++ b/netx/consts_windows.go @@ -3,7 +3,7 @@ package netx import ( - `golang.org/x/sys/windows` + "golang.org/x/sys/windows" ) const ( diff --git a/netx/doc.go b/netx/doc.go index 9178273..b7fffcd 100644 --- a/netx/doc.go +++ b/netx/doc.go @@ -1,4 +1,128 @@ /* Package netx includes extensions to the stdlib [net] module. + +It provides many functions that should be significantly more helpful for those working with networking a significant amount. + +Some functions may be "missing". This is by design; the corresponding functionality should be available in stdlib. + +For example, while there is an [IsAddr4Compat] function, there is no "IsAddr4Mapped". This is because this can be accomplished via the following: + + var ip netip.Addr + + // ... + + ip.Is4In6() + + // ... + +If you are looking for specific functionality in this library but are unable to find it, +this is because the functionality already exists in (in the author's opinion) a reasonably convenient +methodology via one of the following: + + - [net] + - [net/netip] + - [go4.org/netipx] + +# IP Family/Versioning + +There are different ways of expressing an IP address/network's version (e.g. IPv4, IPv6). + +Some libraries expect an integer representation (e.g. 4, 6). + +Some libraries expect a system/OS-constant instead (e.g. unix.AF_INET, unix.AF_INET6, windows.AF_INET, windows.AF_INET6). + +The following functions are offered to assist with this translation: + + - [FamilyToVer] + - [GetAddrFamily] + - [GetIpFamily] + - [IpVerStr] + - [VerToFamily] + +The following constants are offered to make cross-platform development more fluid (these are set according to the build target): + + - [AFUnspec] + - [AFInet] + - [AFInet6] + +# IP Sets + +This library provides convenience functions for getting a [netipx.IPSet] in single-entry functions: + + - [IPSetCombined] + - [IPSetFrom] + - [IPSetFromNative] + - [MustIPSetCombined] + - [MustIPSetFrom] + - [MustIPSetFromNative] + +# Canonical String Representation + +Go's stdlib [net] and [net/netip] don't always properly display or handle the proper string formatting per RFC conventions. + +The following functions assist with this: + + - [AddrRfc] + - [IpRfc] + - [IpRfcStr] + - [IpStripRfcStr] + - [IsBracketedIp6] + - [IsPrefixNet] + +# Netmask and CIDR Conversions + +An IPv4 network size can be represented by both a netmask (e.g. "255.255.255.0") or a CIDR prefix length (e.g. "/24"), +as well as a numeric bitmask form (which is different from the prefix length). + +This module offers functions that assist with easy translation between these formats: + + - [Cidr4ToIPMask] + - [Cidr4ToMask] + - [Cidr4ToStr] + - [IPMask4ToCidr] + - [IPMask4ToMask] + - [IPMask4ToStr] + - [Mask4StrToCidr] + - [Mask4StrToIPMask] + - [Mask4StrToMask] + - [Mask4ToCidr] + - [Mask4ToIPMask] + - [Mask4ToStr] + +# IPv4 Addresses as IPv6 + +There are two canonical ways of representing an IPv4 address as an IPv6 address, per RFC 4291 ([section 2.5.5]): + + - "IPv4-Compatible IPv6 Addresses" (RFC [4291 § 2.5.5.1]) — ::/96 + - "IPv4-Mapped IPv6 Addresses" (RFC [4291 § 2.5.5.2]) — ::ffff:0:0/96 + +The former (::/96) is considered deprecated, and there is only parsing support for it in Go stdlib. + +There are a number of functions tailored for use with both of these formats in this module: + + - [Addr4in6Compat] + - [Addr4in6Mapped] + - [IP4MapPfx4to6] + - [IP4MapPfx6to4] + - [IsAddr4Compat] + - [Pfx4in6Compat] + - [Pfx4in6Mapped] + - [UnmapAddr] + - [UnmapPfx] + +For more technical/detailed information, refer to the ./NOTES.adoc file in this module's +git repository, specifically the "IPv4 Addresses as IPv6" section. + +# Miscellaneous Functions + +Other functions that may be of use: + + - [IsPublic] — Note that this does not guarantee that it is a *valid* public IP address (especially in the case of IPv6), + simply that it isn't in a non-WAN-routable prefix reserved by current standards. + - [HasSubnet] + +[section 2.5.5]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5 +[4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 */ package netx diff --git a/netx/errors.go b/netx/errors.go index 1971633..05372c9 100644 --- a/netx/errors.go +++ b/netx/errors.go @@ -1,7 +1,7 @@ package netx import ( - `errors` + "errors" ) var ( diff --git a/netx/funcs.go b/netx/funcs.go index 2c7216c..5762563 100644 --- a/netx/funcs.go +++ b/netx/funcs.go @@ -1,15 +1,110 @@ package netx import ( - `math/bits` - `net` - `net/netip` - `strconv` - `strings` + "fmt" + "math/bits" + "net" + "net/netip" + "strconv" + "strings" - `go4.org/netipx` + "go4.org/netipx" ) +/* +Addr4in6Compat takes IP address `addr` and, if an IPv4 address, returns it +as an "IPv4-Compatible IPv6 Address" (RFC [4291 § 2.5.5.1]). + +If `addr` is already an IPv6 address and NOT in the '::/96' or '::0:0/96' prefix, +`addr` will be returned as `compatAddr`. + +If `addr` is an IPv6 address and is an IPv4-Mapped prefix ('::0:0/96'), +then `compatAddr` will be an IPv4-Compatible address conversion of `addr`. + +If `addr` is an IPv6 address and IS in the '::/96' prefix, +`addr` will be returned as `compatAddr`. + +If `addr` is invalid, `compatAddr` will be the [netip.Addr] returned by [netip.IPv6Unspecified]. + +Note that this is the DEPRECATED format for IPv4-addresses-as-IPv6! +The modern, proper equivalent is "IPv4-Mapped IPv6 Address" (RFC [4291 § 2.5.5.2]), +which can be performed via [Addr4in6Mapped] instead. + + - If you need an IPv6 address format for an IPv4 address and don't know + if you need this function or not, you want [Addr4in6Mapped] instead. + - If you THINK you want this function, you want [Addr4in6Mapped] instead. + - If you ABSOLUTELY KNOW you need this function, you're probably dealing with + some very old IPv6 implementations. + But you at least know you need this function. + +[4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 +*/ +func Addr4in6Compat(addr netip.Addr) (compatAddr netip.Addr) { + + if !addr.IsValid() { + compatAddr = netip.IPv6Unspecified() + return + } + + compatAddr = addr + + if addr.Is6() { + if addr.Is4In6() { + compatAddr = Addr4in6Compat(UnmapAddr(addr)) + } + return + } + + if !addr.Is6() { + compatAddr = netip.MustParseAddr(fmt.Sprintf("%s%s", ip4in6Legacy96, addr.String())) + } + + return +} + +/* +Addr4in6Mapped takes IP address `addr` and, if an IPv4 address, returns it +as an "IPv4-Mapped IPv6 Address" (RFC [4291 § 2.5.5.2]). + +You should almost assuredly be using this function instead of [Addr4in6Compat]. + +If `addr` is already an IPv6 address and NOT in the '::/96' or '::0:0/96' prefix, +`addr` will be returned as `mappedAddr`. + +If `addr` is an IPv6 address and is an IPv4-Compatible prefix ('::/96'), +then `mappedAddr` will be an IPv4-Mapped address conversion of `addr`. + +If `addr` is an IPv6 address and IS in the '::ffff:0:0/96' prefix, +`addr` will be returned as `mappedAddr`. + +If `addr` is invalid, `mappedAddr` will be the [netip.Addr] returned by [netip.IPv6Unspecified]. + +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 +*/ +func Addr4in6Mapped(addr netip.Addr) (mappedAddr netip.Addr) { + + if !addr.IsValid() { + mappedAddr = netip.IPv6Unspecified() + return + } + + mappedAddr = addr + + if addr.Is6() { + if !IsAddr4Compat(addr) { + return + } + mappedAddr = UnmapAddr(addr) + } + + if !mappedAddr.Is6() { + mappedAddr = netip.MustParseAddr(fmt.Sprintf("%s%s", ip4in6Modern96, mappedAddr.String())) + } + + return +} + /* AddrRfc returns an RFC-friendly string from an IP address ([net/netip.Addr]). @@ -17,6 +112,15 @@ If addr is an IPv4 address, it will simply be the string representation (e.g. "2 If addr is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]"). +IPv4-Compatible IPv6 addresses will be represented as e.g. "[::203.0.113.1]". + +IPv4-Mapped IPv6 addresses will be represented as e.g. "[::ffff:203.0.113.1]". + +Note that if addr is an IPv6 unspecifified address derived as an "IPv4-Compatible", +then `rfcStr` will always be the IPv6 unspecified address (::), NOT an IPv4-Compatible +unspecified (::0.0.0.0). Refer to the documentation for [IsAddr4Compat] as to why +this happens. + If the version can't be determined, rfcStr will be an empty string. */ func AddrRfc(addr netip.Addr) (rfcStr string) { @@ -24,7 +128,15 @@ func AddrRfc(addr netip.Addr) (rfcStr string) { if addr.Is4() { rfcStr = addr.String() } else if addr.Is6() { - rfcStr = "[" + addr.String() + "]" + switch { + case IsAddr4Compat(addr): + rfcStr = fmt.Sprintf("%s%s", ip4in6Legacy96, UnmapAddr(addr).String()) + // addr.Is4In6() addrs format properly + default: + rfcStr = addr.String() + } + + rfcStr = "[" + rfcStr + "]" } return @@ -34,11 +146,6 @@ func AddrRfc(addr netip.Addr) (rfcStr string) { Cidr4ToIPMask takes an IPv4 CIDR/bit size/prefix length and returns the [net.IPMask]. It's (essentially) the inverse of [net.IPMask.Size]. -See also: - - * [Cidr4ToMask] - * [Cidr4ToStr] - Inverse of [IPMask4ToCidr]. */ func Cidr4ToIPMask(cidr uint8) (ipMask net.IPMask, err error) { @@ -56,11 +163,6 @@ func Cidr4ToIPMask(cidr uint8) (ipMask net.IPMask, err error) { /* Cidr4ToMask takes an IPv4 CIDR/bit size/prefix length and returns the netmask *in bitmask form*. -See also: - - * [Cidr4ToIPMask] - * [Cidr4ToStr] - Inverse of [Mask4ToCidr]. */ func Cidr4ToMask(cidr uint8) (mask uint32, err error) { @@ -80,11 +182,6 @@ func Cidr4ToMask(cidr uint8) (mask uint32, err error) { /* Cidr4ToStr is a convenience wrapper around [IPMask4ToStr]([Cidr4ToMask](cidr)). -See also: - - * [Cidr4ToIPMask] - * [Cidr4ToMask] - Inverse of [Mask4StrToCidr]. */ func Cidr4ToStr(cidr uint8) (maskStr string, err error) { @@ -128,8 +225,6 @@ func FamilyToVer(family uint16) (ipVer int) { /* GetAddrFamily returns the network family of a [net/netip.Addr]. -See also [GetIpFamily]. - Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6). (See [FamilyToVer] to get the associated higher-level value.) @@ -160,8 +255,6 @@ GetIpFamily returns the network family of a [net.IP]. Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6). (See [FamilyToVer] to get the associated higher-level value.) -See also [GetAddrFamily]. - If ip is not a "valid" IP address or the version can't be determined, family will be [golang.org/x/sys/unix.AF_UNSPEC] or [golang.org/x/sys/windows.AF_UNSPEC] depending on platform (usually 0x00/0). */ @@ -180,36 +273,171 @@ func GetIpFamily(ip net.IP) (family uint16) { } /* -IpRfc returns an RFC-friendly string from an IP address ([net.IP]). +HasSubnet returns true if prefix `sub` is within prefix `parent`. -If ip is an IPv4 address, it will simmply be the string representation (e.g. "203.0.113.1"). +If `incl` is true, HasSubnet will return true if `sub` and `parent` are the same network. -If ip is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]"). +If `incl` is false, HasSubnet will return false if `sub` and `parent` are the same network. +This can be further determined by doing the following: -If the version can't be determined, rfcStr will be an empty string. + var sub netip.Prefix + var parent netip.Prefix -See also [IpRfcStr] for providing an IP address as a string. + // ... + + if !netx.HasSubnet(parent, sub) && parent.Compare(sub) != 0 { // .Compare() returns 0 if the prefixes are the same. + // ... + } + + // ... + +It is expected that both prefixes are valid; HasSubnet will always return false if `parent` or `sub` are invalid. + +HasSub will return false if `parent` and `sub` are different IP address families, but will +properly handle IPv4-Mapped/IPv4-Compatible IPv6 prefixes when compared with IPv6 prefixes. + +Host bits may be set; it will not affect the comparison. */ -func IpRfc(ip net.IP) (rfcStr string) { +func HasSubnet(parent, sub netip.Prefix, incl bool) (hasSub bool) { - if ip.To4() != nil { - rfcStr = ip.To4().String() - } else if ip.To16() != nil { - rfcStr = "[" + ip.To16().String() + "]" + // rule out unpredictable comparison + if !parent.IsValid() || !sub.IsValid() { + return + } + + // different families + if parent.Addr().Is4() != sub.Addr().Is4() { + return + } + + // parent is smaller network size than sub + if parent.Bits() > sub.Bits() { + return + } + + // parent does not contain network (or host) addr of sub + // (returns true even for parent's network addr, etc.) + if !parent.Contains(sub.Addr()) { + return + } + + // parent and sub are same canon (masked) network + // (.Compare() compares host bits as well, hence the .Masked()) + if parent.Masked().Compare(sub.Masked()) == 0 { + hasSub = incl + return + } + + hasSub = true + + return +} + +/* +IP4MapPfx4to6 converts a native IPv4 prefix length (<= 32) to an +IPv6 prefix length for "IPv4-Compatible" (RFC [4291 § 2.5.5.1]) +and "IPv4-Mapped" (RFC [4291 § 2.5.5.2]) addressing. + +If `len4` is > 32, then `len6` will be equal to `len4`. +If this is not desired, ensure that you are gating calls to IP4MapPfx4to6 +with [netip.Addr.Is4] == true on the prefix in question (it will return false +for IPv6 addresses, IPv4-Mapped IPv6 addresses, AND IPv4-Compatible IPv6 addresses): + + var pfx netip.Prefix + + // ... + + ipv6BitLen = pfx.Bits() + if pfx.Addr().Is4() { + ipv6BitLen = netx.IP4MapPfx4to6(ipv6BitLen) + } + + // ... + +[4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 +*/ +func IP4MapPfx4to6(len4 uint8) (len6 uint8) { + + len6 = len4 + if len4 <= 32 { + len6 = 96 + len4 } return } /* -IpRfcStr implements [IpRfc]/[AddrRfc] for string representations of an IP address s. +IP4MapPfx6to4 converts a native IPv6 prefix length (>= 96) to an +IPv4 prefix length from "IPv4-Compatible" (RFC [4291 § 2.5.5.1]) +and "IPv4-Mapped" (RFC [4291 § 2.5.5.2]) addressing. -If s is an IPv6 address already in the bracketed RFC format, -then rfcStr will be equal to s. +If `len6` is < 96, then `len4` will be equal to `len6`. +If this is not desired, ensure that you are gating calls to IP4MapPfx6to4 +with [netip.Addr.Is4In6] == true on the prefix in question: -If s is not a string representation of an IP address, rfcStr will be empty. + var pfx netip.Prefix -See [IpStripRfcStr] for the inverse (removing any brackets from s if present). + // ... + + ipv4BitLen = pfx.Bits() + if pfx.Addr().Is4In6() { + ipv4BitLen = IP4MapPfx6to4(ipv4BitLen) + } + if ipv4BitLen > 32 { + panic("lol oops") + } + + // ... + +[4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 +*/ +func IP4MapPfx6to4(len6 uint8) (len4 uint8) { + + len4 = len6 + if len6 >= 96 { + len4 = len6 - 96 + } + + return +} + +/* +IpRfc returns an RFC-friendly string from an IP address ([net.IP]). + +If ip is an IPv4 address, it will simmply be the string representation (e.g. "203.0.113.1"). + +If ip is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]"). + +IPv4-Compatible IPv6 addresses will be represented as e.g. "[::203.0.113.1]". + +IPv4-Mapped IPv6 addresses will be represented as e.g. "[::ffff:203.0.113.1]". + +If the version can't be determined, `rfcStr` will be an empty string. +*/ +func IpRfc(ip net.IP) (rfcStr string) { + + var err error + var a netip.Addr + + if a, err = netip.ParseAddr(ip.String()); err != nil { + return + } + rfcStr = AddrRfc(a) + + return +} + +/* +IpRfcStr implements [IpRfc]/[AddrRfc] for string representations of an IP address `s`. + +If `s` is an IPv6 address already in the bracketed RFC format, +then `rfcStr` will be equal to `s`. + +If `s` is not a string representation of an IP address, `rfcStr` will be empty. + +See [IpStripRfcStr] for the inverse (removing any brackets from `s` if present). */ func IpRfcStr(s string) (rfcStr string) { @@ -232,9 +460,9 @@ func IpRfcStr(s string) (rfcStr string) { } /* -IpStripRfcStr returns IP address string s without any brackets. +IpStripRfcStr returns IP address string `s` without any brackets. -If s is not a valid IP address, stripStr will be empty. +If `s` is not a valid IP address, `stripStr` will be empty. */ func IpStripRfcStr(s string) (stripStr string) { @@ -254,11 +482,6 @@ func IpStripRfcStr(s string) (stripStr string) { /* IPMask4ToCidr returns a CIDR prefix size/bit size/bit length from a [net.IPMask]. -See also: - - * [IPMask4ToMask] - * [IPMask4ToStr] - Inverse of [Cidr4ToIPMask]. */ func IPMask4ToCidr(ipMask net.IPMask) (cidr uint8, err error) { @@ -285,11 +508,6 @@ func IPMask4ToCidr(ipMask net.IPMask) (cidr uint8, err error) { /* IPMask4ToMask returns the mask *in bitmask form* from a [net.IPMask]. -See also: - - * [IPMask4ToCidr] - * [IPMask4ToStr] - Inverse of [Mask4ToIPMask]. */ func IPMask4ToMask(ipMask net.IPMask) (mask uint32, err error) { @@ -310,11 +528,6 @@ func IPMask4ToMask(ipMask net.IPMask) (mask uint32, err error) { /* IPMask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a [net.IPMask]. -See also: - - * [IPMask4ToCidr] - * [IPMask4ToMask] - Inverse of [Mask4StrToIPMask]. */ func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) { @@ -339,24 +552,120 @@ func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) { } /* -IpVerStr provides the IP family of IP address/network string s. +IPSetCombined combines multiple [go4.org/netipx.IPSet] sets into a single set. -s may be one of the following formats/syntaxes: +The original `sets` will only be read and be untouched otherwise; `combined` is a new set. - * 203.0.113.0 - * 203.0.113.1 - * 203.0.113.0/24 - * 203.0.113.1/24 - * 2001:db8:: - * 2001:db8::1 - * 2001:db8::/32 - * 2001:db8::1/32 - * [2001:db8::] - * [2001:db8::1] +Nil sets in `sets` will be silently skipped. +*/ +func IPSetCombined(sets []*netipx.IPSet) (combined *netipx.IPSet, err error) { + var idx int + var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + + for idx = range sets { + if sets[idx] == nil { + continue + } + ipsb.AddSet(sets[idx]) + } + + if combined, err = ipsb.IPSet(); err != nil { + return + } + + return +} + +/* +IPSetFrom skips some round-tripping and returns a [go4.org/netipx.IPSet] from a given +slice of strings. + +`addrs` must be individual address strings but may be nil. See [net/netip.ParseAddr] for conventions. + +`pfxs` must be prefix strings but may be nil. See [net/netip.ParsePrefix] for conventions. + +`ranges` must be range strings but may be nil. See [go4.org/netipx.ParseIPRange] for conventions. + +IPSetFrom will return immediately on first error, otherwise `ipset` will contain all provided addresses/prefixes/ranges. +*/ +func IPSetFrom(addrs, pfxs, ranges []string) (ipset *netipx.IPSet, err error) { + + var idx int + var a netip.Addr + var p netip.Prefix + var r netipx.IPRange + var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + + for idx = range addrs { + if a, err = netip.ParseAddr(addrs[idx]); err != nil { + return + } + ipsb.Add(a) + } + for idx = range pfxs { + if p, err = netip.ParsePrefix(pfxs[idx]); err != nil { + return + } + ipsb.AddPrefix(p) + } + for idx = range ranges { + if r, err = netipx.ParseIPRange(ranges[idx]); err != nil { + return + } + ipsb.AddRange(r) + } + + if ipset, err = ipsb.IPSet(); err != nil { + return + } + + return +} + +/* +IPSetFromNative is like [IPSetFrom] but takes native types instead of strings. +*/ +func IPSetFromNative(addrs []netip.Addr, pfxs []netip.Prefix, ranges []netipx.IPRange) (ipset *netipx.IPSet, err error) { + + var idx int + var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + + for idx = range addrs { + ipsb.Add(addrs[idx]) + } + for idx = range pfxs { + ipsb.AddPrefix(pfxs[idx]) + } + for idx = range ranges { + ipsb.AddRange(ranges[idx]) + } + + if ipset, err = ipsb.IPSet(); err != nil { + return + } + + return +} + +/* +IpVerStr provides the IP family of IP address/network string `s`. + +`s` may be one of the following formats/syntaxes: + + - 203.0.113.0 + - 203.0.113.1 + - 203.0.113.0/24 + - 203.0.113.1/24 + - 2001:db8:: + - 2001:db8::1 + - 2001:db8::/32 + - 2001:db8::1/32 + - [2001:db8::] + - [2001:db8::1] Unlike [GetAddrFamily]/[GetIpFamily], this returns a more "friendly" -version - if s is not valid syntax, ipVer will be int(0), +version - if `s` is not valid syntax, `ipVer` will be int(0), otherwise ipVer will be int(4) for family IPv4 and int(6) for family IPv6. (See [VerToFamily] to get the associated system/lower-level value.) */ @@ -382,7 +691,47 @@ func IpVerStr(s string) (ipVer int) { } /* -IsBracketedIp6 returns a boolean indicating if s is a valid bracket-enclosed IPv6 in string format +IsAddr4Compat returns true if `addr` is in the "IPv4-Compatible IPv6 Address" prefix. + +Note that if `addr` is from an IPv4-Compatible and was the unspecified address +(::0.0.0.0), isCompat will always return false by design. +This is because ::0.0.0.0 is, as a fully exploded IPv6 address: + + 0000:0000:0000:0000:0000:0000:0000:0000 + +Or, in other words, + + [16]byte{ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + +which is the *exact same* bytes as the native IPv6 "unspecified" address (::). + +Because the IPv4-Compatible prefix itself is ::/96 (and no prefix information +is available from a netip.Addr), this function errs on the side of caution +and treats `addr` as the IPv6 unspecified address instead of the +IPv4-Compatible unspecified. + +This "quirk" is not present in IPv4-Mapped addresses, as they use an explicit non-zero prefix. +(There are many reasons why IPv4-Compatible addressing was legacy/deprecated the moment it was +canonized in RFC; this is one of them.) +*/ +func IsAddr4Compat(addr netip.Addr) (isCompat bool) { + + if addr.Is6() && addr.IsUnspecified() { + return + } + + isCompat = ip4in6Legacy.Contains(addr) + + return +} + +/* +IsBracketedIp6 returns a boolean indicating if `s` is a valid bracket-enclosed IPv6 in string format (e.g. "[2001:db8::1]"). It will return false for *non-bracketed* IPv6 addresses (e.g. "2001:db8::1"), IPv4 addresses, @@ -412,12 +761,12 @@ func IsBracketedIp6(s string) (isBrktdIp bool) { } /* -IsIpAddr returns a boolean indicating if s is an IP address (either IPv4 or IPv6) in string format. +IsIpAddr returns a boolean indicating if `s` is an IP address (either IPv4 or IPv6) in string format. For IPv6, it will return true for both of these formats: - * 2001:db8::1 - * [2001:db8::1] + - 2001:db8::1 + - [2001:db8::1] [IsBracketedIp6] can be used to narrow down which form. */ @@ -436,7 +785,7 @@ func IsIpAddr(s string) (isIp bool) { } /* -IsPrefixNet returns true if s is a (valid) IP address or network (either IPv4 or IPv6) in: +IsPrefixNet returns true if `s` is a (valid) IP address or network (either IPv4 or IPv6) in: / @@ -456,30 +805,36 @@ func IsPrefixNet(s string) (isNet bool) { } /* -IsPublic returns true if ALL of the following conditions are *true* for ip: +IsPublic returns true if ALL of the following conditions are *true* for `ip`: - * [net/netip.Addr.IsGlobalUnicast] - * [net/netip.Addr.IsValid] - * (IPv4) Is not in 192.0.2.0/24, 198.51.100.0/24, or 203.0.113.0/24 ([RFC 5737]) - * (IPv6) Is not in 2001:db8::/32 or 3fff::/20 ([RFC 3849], [RFC 9637]) + - [net/netip.Addr.IsGlobalUnicast] + - [net/netip.Addr.IsValid] + - (IPv4) Is not in 192.0.2.0/24, 198.51.100.0/24, or 203.0.113.0/24 ([RFC 5737]) + - (IPv4) Is not in 100.64.0.0/10 ([RFC 6598]) + - (IPv6) Is not in 2001:db8::/32 or 3fff::/20 ([RFC 3849], [RFC 9637]) AND ALL of the following conditions are *false* for ip: - * [net/netip.Addr.IsLinkLocalMulticast] - * [net/netip.Addr.IsInterfaceLocalMulticast] - * [net/netip.Addr.IsLinkLocalUnicast] - * [net/netip.Addr.IsMulticast] - * [net/netip.Addr.IsLoopback] - * [net/netip.Addr.IsPrivate] - * [net/netip.Addr.IsUnspecified] + - [net/netip.Addr.IsLinkLocalMulticast] + - [net/netip.Addr.IsInterfaceLocalMulticast] + - [net/netip.Addr.IsLinkLocalUnicast] + - [net/netip.Addr.IsMulticast] + - [net/netip.Addr.IsLoopback] + - [net/netip.Addr.IsPrivate] + - [net/netip.Addr.IsUnspecified] -4-in-6 addresses (::0:0/96, "IPv4-Compatible IPv6 Address" [RFC 4291 § 2.5.5.1] (Legacy/Obsolete); -::ffff:0:0/96, "IPv4-Mapped IPv6" [RFC 4291 § 2.5.5.2]) will be internally -"unwrapped" back to native IPv4 before performing conditional checks. +For 4-in-6 addresses: + + - ::0:0/96 (RFC [4291 § 2.5.5.1], "IPv4-Compatible IPv6 Address") (Deprecated) + - ::ffff:0:0/96 (RFC [4291 § 2.5.5.2], "IPv4-Mapped IPv6") + +they will be internally "unwrapped"/unmapped (via [UnmapAddr]) to native IPv4 first +before performing conditional checks. [RFC 5737]: https://datatracker.ietf.org/doc/html/rfc5737 -[RFC 4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 -[RFC 4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 +[RFC 6598]: https://datatracker.ietf.org/doc/html/rfc6598 +[4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 [RFC 3849]: https://datatracker.ietf.org/doc/html/rfc3849 [RFC 9637]: https://datatracker.ietf.org/doc/html/rfc9637 */ @@ -493,7 +848,8 @@ func IsPublic(ip netip.Addr) (isPub bool) { if addr, err = netip.ParseAddr(ip.String()); err != nil { return } - addr = addr.Unmap() + // Unmap to properly determine routedness. + addr = UnmapAddr(addr) // Short-circuit on invalid first to avoid any possible logic oddness. if !addr.IsValid() { @@ -509,8 +865,8 @@ func IsPublic(ip netip.Addr) (isPub bool) { addr.IsPrivate(), addr.IsUnspecified(), // these are in the FALSE to keep syntax pattern but it's cleaner to doc as if it's a ! in TRUE. - ip4In6Legacy.Contains(ip), - ip4In6Modern.Contains(ip), + ipDoc.Contains(addr), + cgnat4.Contains(addr), } { if v { return @@ -534,11 +890,6 @@ func IsPublic(ip netip.Addr) (isPub bool) { /* Mask4ToCidr converts an IPv4 netmask *in bitmask form* to a CIDR prefix size/bit size/bit length. -See also: - - * [Mask4ToIPMask] - * [Mask4ToStr] - Inverse of [Cidr4ToMask]. */ func Mask4ToCidr(mask uint32) (cidr uint8, err error) { @@ -551,11 +902,6 @@ func Mask4ToCidr(mask uint32) (cidr uint8, err error) { /* Mask4ToIPMask returns mask *in bitmask form* as a [net.IPMask]. -See also: - - * [Mask4ToCidr] - * [Mask4ToStr] - Inverse of [IPMask4ToMask]. */ func Mask4ToIPMask(mask uint32) (ipMask net.IPMask, err error) { @@ -574,11 +920,6 @@ func Mask4ToIPMask(mask uint32) (ipMask net.IPMask, err error) { /* Mask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a netmask *in bitmask form*. -See also: - - * [Mask4ToCidr] - * [Mask4ToIPMask] - Inverse of [Mask4StrToMask]. */ func Mask4ToStr(mask uint32) (maskStr string, err error) { @@ -597,14 +938,9 @@ func Mask4ToStr(mask uint32) (maskStr string, err error) { } /* -Mask4StrToCidr parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns am IPv4 CIDR/bit size/prefix length. +Mask4StrToCidr parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns an IPv4 CIDR/bit size/prefix length. -See also: - - * [Mask4StrToIPMask] - * [Mask4StrToMask] - -Inverse of [Cidr4ToMaskStr]. +Inverse of [Cidr4ToStr]. */ func Mask4StrToCidr(maskStr string) (cidr uint8, err error) { @@ -624,11 +960,6 @@ func Mask4StrToCidr(maskStr string) (cidr uint8, err error) { /* Mask4StrToIPMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a [net.IPMask]. -See also: - - * [Mask4StrToCidr] - * [Mask4StrToMask] - Inverse of [IPMask4ToStr]. */ func Mask4StrToIPMask(maskStr string) (mask net.IPMask, err error) { @@ -661,11 +992,6 @@ func Mask4StrToIPMask(maskStr string) (mask net.IPMask, err error) { /* Mask4StrToMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a netmask *in bitmask form*. -See also: - - * [Mask4StrToCidr] - * [Mask4StrToIPMask] - Inverse of [Mask4ToStr]. */ func Mask4StrToMask(maskStr string) (mask uint32, err error) { @@ -683,8 +1009,207 @@ func Mask4StrToMask(maskStr string) (mask uint32, err error) { return } +// MustIPSetCombined wraps [IPSetCombined] but will panic on error instead of returning it. +func MustIPSetCombined(sets []*netipx.IPSet) (combined *netipx.IPSet) { + + var err error + + if combined, err = IPSetCombined(sets); err != nil { + panic(err) + } + + return +} + +// MustIPSetFrom wraps [IPSetFrom] but will panic on error instead of returning it. +func MustIPSetFrom(addrs, pfxs, ranges []string) (ipset *netipx.IPSet) { + + var err error + + if ipset, err = IPSetFrom(addrs, pfxs, ranges); err != nil { + panic(err) + } + + return +} + +// MustIPSetFromNative wraps [IPSetFromNative] but will panic on error instead of returning it. +func MustIPSetFromNative(addrs []netip.Addr, pfxs []netip.Prefix, ranges []netipx.IPRange) (ipset *netipx.IPSet) { + + var err error + + if ipset, err = IPSetFromNative(addrs, pfxs, ranges); err != nil { + panic(err) + } + + return +} + /* -VerToFamily takes a "human-readable" IP version ipVer (4 or 6) and returns +Pfx4in6Compat takes network prefix `pfx` and, if an IPv4 network, returns it +as an "IPv4-Compatible IPv6 Address" (RFC [4291 § 2.5.5.1]) network. + +If `pfx` is already an IPv6 network and NOT in the '::/96' or '::0:0/96' prefix (inclusive), +`pfx` will be returned as `compatPfx`. + +If `pfx` is an IPv6 network and is an IPv4-Mapped prefix ('::0:0/96'), +then `compatPfx` will be an IPv4-Compatible network conversion of `pfx`. + +If `pfx` is an IPv6 network and is in the '::/96' prefix (inclusive), +`pfx` will be returned as `compatPfx`. + +If `pfx` is invalid or the zero network, `compatPfx` will be ::/0. + +Note that this is the DEPRECATED format for IPv4-addresses-as-IPv6! +The modern, proper equivalent is "IPv4-Mapped IPv6 Address" (RFC [4291 § 2.5.5.2]), +which can be performed via [Pfx4in6Mapped] instead. + + - If you need an IPv6 network format for an IPv4 network and don't know + if you need this function or not, you want [Pfx4in6Mapped] instead. + - If you THINK you want this function, you want [Pfx4in6Mapped] instead. + - If you ABSOLUTELY KNOW you need this function, you're probably dealing with + some very old IPv6 implementations. + But you at least know you need this function. + +[4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 +*/ +func Pfx4in6Compat(pfx netip.Prefix) (compatPfx netip.Prefix) { + + var bitLen uint8 + var a netip.Addr + + if !a.IsValid() || !pfx.IsValid() { + compatPfx = netip.PrefixFrom(netip.IPv6Unspecified(), 0) + return + } + + a = pfx.Addr() + bitLen = uint8(pfx.Bits()) + compatPfx = pfx + + if a.Is6() { + if a.Is4In6() { + a = Addr4in6Compat(UnmapAddr(a)) + compatPfx = netip.PrefixFrom(a, int(bitLen)) + } + return + } + + a = netip.MustParseAddr(fmt.Sprintf("%s%s", ip4in6Legacy96, a.String())) + bitLen = IP4MapPfx4to6(bitLen) + + compatPfx = netip.PrefixFrom(a, int(bitLen)) + + return +} + +/* +Pfx4in6Mapped takes network prefix `pfx` and, if an IPv4 network, returns it +as an "IPv4-Mapped IPv6 Address" (RFC [4291 § 2.5.5.2]) network. + +You should almost assuredly be using this function instead of [Pfx4in6Compat]. + +If `pfx` is already an IPv6 network and NOT in the '::/96' or '::0:0/96' prefix, +`pfx` will be returned as `mappedPfx`. + +If `pfx` is an IPv6 network and is an IPv4-Compatible prefix ('::/96'), +then `mappedPfx` will be an IPv4-Mapped network conversion of `pfx`. + +If `pfx` is an IPv6 address and is in the '::ffff:0:0/96' prefix (inclusive), +`pfx` will be returned as `mappedPfx`. + +If `pfx` is invalid or the zero network, `mappedPfx` will be ::/0. + +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 +*/ +func Pfx4in6Mapped(pfx netip.Prefix) (mappedPfx netip.Prefix) { + + var bitLen uint8 + var a netip.Addr + + if !a.IsValid() || !pfx.IsValid() { + mappedPfx = netip.PrefixFrom(netip.IPv6Unspecified(), 0) + return + } + + a = pfx.Addr() + bitLen = uint8(pfx.Bits()) + mappedPfx = pfx + + if a.Is6() { + if !IsAddr4Compat(a) { + return + } + a = UnmapAddr(a) + } + + a = netip.MustParseAddr(fmt.Sprintf("%s%s", ip4in6Modern96, a.String())) + bitLen = IP4MapPfx4to6(bitLen) + + mappedPfx = netip.PrefixFrom(a, int(bitLen)) + + return +} + +/* +UnmapAddr unmaps both a current-supported RFC [4291 § 2.5.5.2] format ("IPv4-Mapped") +*and* legacy RFC [4291 § 2.5.5.1] ("IPv4-Compatible") address. + +The Unmap method of a [netip.Addr] only works for the former. +This function works for both. + +If `ip` is not either of those formats (or a "true" IPv6 address, etc.), +then `unmapped` will be equal to `ip` (but a new/copied [net/netip.Addr]). + +If `ip` is the "unspecified" address (0.0.0.0, ::) for either family, it is returned as-is. + +[4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 +*/ +func UnmapAddr(ip netip.Addr) (unmapped netip.Addr) { + + if ip.IsUnspecified() { + unmapped = ip + return + } + + // Do an initial .Unmap() - this should be safe regardless. + unmapped = ip.Unmap() + + // If it's in the legacy prefix, it requires manual unmapping. + if ip4in6Legacy.Contains(unmapped) { + unmapped, _ = netip.AddrFromSlice(unmapped.AsSlice()[12:16]) + } + + return +} + +/* +UnmapPfx unmaps both a current-supported RFC 4291 § 2.5.5.2 format ("IPv4-Mapped") +*and* legacy RFC 4291 § 2.5.5.1 ("IPv4-Compatible") prefix. + +It's much like [UnmapAddr] in this sense, but requires some additional parsing/conversion. + +[4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 +[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 +*/ +func UnmapPfx(pfx netip.Prefix) (unmapped netip.Prefix) { + + var a netip.Addr = pfx.Addr() + + unmapped = pfx + + if IsAddr4Compat(a) || a.Is4In6() { + a = UnmapAddr(a) + unmapped = netip.PrefixFrom(a, int(IP4MapPfx6to4(uint8(pfx.Bits())))) + } + + return +} + +/* +VerToFamily takes a "human-readable" IP version `ipVer` (4 or 6) and returns a system-level constant (e.g. [AFUnspec], [AFInet], [AFInet6]). If not a known IP version (i.e. neither 4 nor 6), family will be [AFUnspec]. diff --git a/netx/funcs_test.go b/netx/funcs_test.go index b5740c2..55f0d2e 100644 --- a/netx/funcs_test.go +++ b/netx/funcs_test.go @@ -1,12 +1,13 @@ package netx import ( - `math` - `net` - `net/netip` + "math" + "net" + "net/netip" "testing" ) +/* func TestFuncsDns(t *testing.T) { var err error @@ -29,6 +30,7 @@ func TestFuncsDns(t *testing.T) { return } +*/ func TestFuncsIP(t *testing.T) { diff --git a/tplx/sprigx/README.html b/tplx/sprigx/README.html index 20df275..2385791 100644 --- a/tplx/sprigx/README.html +++ b/tplx/sprigx/README.html @@ -636,7 +636,7 @@ pre.rouge .gs {
Brent Saner

-Last rendered 2026-07-15 19:04:01 -0400 +Last rendered 2026-07-29 15:05:41 -0400
Table of Contents
diff --git a/tplx/sprigx/README.md b/tplx/sprigx/README.md index 26c26d2..ac22b1f 100644 --- a/tplx/sprigx/README.md +++ b/tplx/sprigx/README.md @@ -6,7 +6,7 @@ Brent Saner -Last rendered 2026-07-15 19:04:05 -0400 +Last rendered 2026-07-29 15:05:45 -0400