Files
go_goutils/netx/NOTES.adoc
T
brent saner cbfaaddf34 v1.17.0
ADDED:
* netx:
** docs updated
** added NOTES.adoc for more extensive info that doesn't need to be in
the library docs
** Addr4in6Compat()
** Addr4in6Mapped()
** Addr4in6Mapped()
** HasSubnet()
** IP4MapPfx4to6()
** IP4MapPfx6to4()
** IPSetCombined()
** IPSetFrom()
** IPSetFromNative()
** IsAddr4Compat()
** IsPublic()
** MustIPSetCombined()
** MustIPSetFrom()
** MustIPSetFromNative()
** Pfx4in6Compat() (untested)
** Pfx4in6Mapped() (untested)
** UnmapAddr()
** UnmapPfx()
** Better support in other functions for IPv4-Compatible/IPv4-Mapped
addressing
2026-07-29 15:05:36 -04:00

207 lines
6.2 KiB
Plaintext

= `netx` Notes
Brent Saner <bts@square-r00t.net>
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