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

6.2 KiB

netx Notes

1. IPv4 Addresses as IPv6

There are, effectively, two RFC-specified ways of representing an IPv4 address in IPv6 notation/address space/context.

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 RFC 6052 and RFC 8215.

This library may add supporting functions for these translation prefixes at a later time.

1.1. Addresses

While net/netip.ParseAddr (or 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.

For example…​
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:

::192.0.2.10 -> ::c000:20a

For addresses, this is an easy enough fix:

  1. Take the IPv6 as a byte slice, b (b = ip4in6.AsSlice())

  2. Slice out the last 4 bytes as b2 (b2 = b[12:16])

  3. 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.

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):

      # 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):

      # 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:

      []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, just a type alias for uint8):

      []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.

1.2. 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:

  1. Taking the address from the prefix (ip4in6 = pfx4in6.Addr())

    1. And converting to native IPv4 (ip4 = netx.UnmapAddr(ip4in6))

  2. Taking the IPv6 prefix length (len6 = pfx4in6.Bits())

    1. And converting it to an IPv4 prefix length (len4 = netx.IP4MapPfx6to4(uint8(len6)))

  3. Then re-assembling (pfx4 = netip.PrefixFrom(ip4, int(len4)))

2. Miscellaneous

2.1. 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