Skip to content

Code Examples 4: Converting to and from Other Formats

Sean C Foley edited this page Aug 26, 2024 · 28 revisions

Convert to/from Binary String from/to IP Address

str := "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
addrStr := ipaddr.NewIPAddressString(str)
addr := addrStr.GetAddress()
binaryStr, _ := addr.ToBinaryString(false)
fmt.Println(binaryStr)

Output:

00100000000000010000110110111000100001011010001100000000000000000000000000000000100010100010111000000011011100000111001100110100

Parse it as binary:

addrStr = ipaddr.NewIPAddressString(binaryStr)
addr = addrStr.GetAddress()
fmt.Println(addr)

Output:

2001:db8:85a3::8a2e:370:7334

You can preserve the segments when printing:

binaryStr = addr.ToSegmentedBinaryString()
fmt.Println(binaryStr)

Output:

0b0010000000000001:0b0000110110111000:0b1000010110100011:0b0000000000000000:0b0000000000000000:0b1000101000101110:0b0000001101110000:0b0111001100110100

Parse it as binary segments:

addrStr = ipaddr.NewIPAddressString(binaryStr)
addr = addrStr.GetAddress()
fmt.Println(addr)

Output:

2001:db8:85a3::8a2e:370:7334

Convert to/from IPv6 Address from/to MAC Address

// start with a /64 prefix and a mac address
subnet := ipaddr.NewIPAddressString("1111:2222:3333:4444::/64").
	GetAddress().ToIPv6()
mac := ipaddr.NewMACAddressString("aa:bb:cc:dd:ee:ff").GetAddress()

// break into the components and combine ourselves
prefix := subnet.GetNetworkSection()
macConverted, _ := mac.ToEUI64IPv6()
converted, _ := ipaddr.NewIPv6Address(prefix.Append(macConverted))
fmt.Printf("combined %v with %v resulting in %v\n", subnet, mac, converted)

// or use a shortcut
convertedAgain, _ := ipaddr.NewIPv6AddressFromMAC(subnet, mac)
fmt.Printf("combined %v with %v resulting in %v\n", subnet, mac, convertedAgain)

// back to mac again
macAgain, _ := converted.ToEUI(false)
fmt.Println("extracted", macAgain.String())

// convert to the link-local IPv6 address
linkLocal, _ := macAgain.ToLinkLocalIPv6()
fmt.Printf("converted %v to link local %v\n", mac, linkLocal)

Output:

combined 1111:2222:3333:4444::/64 with aa:bb:cc:dd:ee:ff resulting in 1111:2222:3333:4444:a8bb:ccff:fedd:eeff/64
combined 1111:2222:3333:4444::/64 with aa:bb:cc:dd:ee:ff resulting in 1111:2222:3333:4444:a8bb:ccff:fedd:eeff/64
extracted aa:bb:cc:dd:ee:ff
converted aa:bb:cc:dd:ee:ff to link local fe80::a8bb:ccff:fedd:eeff

Convert to/from IPv6 address from/to Ascii Base 85 Encoding

Storing an IPv6 address, with no scope or zone, takes 16 bytes. When using ascii chars with conventional notation, it takes anywhere from two bytes ("::") to 45 bytes ("aaaa:bbbb:cccc:dddd:eeee:ffff:255.255.255.255"), typically extending to 39 bytes ("aaa:bbbb:cccc:dddd:eeee:ffff:aaaa:bbbb"). RFC 1924 describes a compact representation of IPv6 addresses using base 85 digits, implemented by this library. Using a base 85 ascii string takes 20 bytes, not much more than using bytes directly.

print := func(addr *ipaddr.IPv6Address, bytes []byte) {
	fmt.Println("got", addr, "from []byte", bytes, "of len", len(bytes))
}

addr := ipaddr.NewIPAddressString("102:304:506:708:90a:b0c:d0e:fff").
	GetAddress().ToIPv6()

// to bytes and back
bytes := addr.Bytes()
addr, _ = ipaddr.NewIPv6AddressFromBytes(bytes)
print(addr, bytes)

// to ascii bytes and back
base85Str, _ := addr.ToBase85String()
fmt.Println("base 85 string is", base85Str)
addr = ipaddr.NewIPAddressString(base85Str).GetAddress().ToIPv6()
print(addr, []byte(base85Str))

Output:

got 102:304:506:708:90a:b0c:d0e:fff from []byte [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 255] of len 16
base 85 string is 0O|s)GT}-*WUn!Z$mO4Z
got 102:304:506:708:90a:b0c:d0e:fff from []byte [48 79 124 115 41 71 84 125 45 42 87 85 110 33 90 36 109 79 52 90] of len 20

Convert to/from IPv4 Address from/to IPv6 Address

The library provides numerous common standard conversions between IPv4 and IPv6, if you are using a tunneling solution or other IPv4/v6 transition mechanism for managing IPv4/IPv6 integration. Such conversion methods generally insert an IPv4 address into the lower 4 bytes of a 16-byte IPv6 address.

Starting with an IPv6 address, the following convert method checks for various conversion formats, such as Teredo, 6 to 4, 6 over 4, Isatap, IPv4-translation, and IPv4-mapping. It returns the converted address and an instance of ConversionFlags that indicates how the IPv4 address was converted from IPv6, so that it can be converted back again.

func convert(ipv6Address *ipaddr.IPv6Address) (*ipaddr.IPv4Address, *ConversionFlags) {
	var result *ipaddr.IPv4Address
	flags := ConversionFlags{}
	switch {
	case ipv6Address.IsTeredo():
		flags.isTeredo = true
		embedded, _ := ipv6Address.GetEmbeddedIPv4Address()
		result = ipaddr.NewIPv4AddressFromUint32(^embedded.Uint32Value())
		flags.teredoServer, _ = ipv6Address.GetEmbeddedIPv4AddressAt(4)
		flags.teredoFlags = ipv6Address.GetSegment(4).GetIPv6SegmentValue()
		flags.teredoPort = ^ipv6Address.GetSegment(5).GetIPv6SegmentValue()
	case ipv6Address.Is6To4():
		flags.is6To4 = true
		result, _ = ipv6Address.Get6To4IPv4Address()
	case ipv6Address.Is6Over4():
		flags.is6Over4 = true
		result, _ = ipv6Address.GetEmbeddedIPv4Address()
	case ipv6Address.IsIsatap():
		flags.isIsatap = true
		result, _ = ipv6Address.GetEmbeddedIPv4Address()
	case ipv6Address.IsIPv4Translatable():
		flags.isIPv4Translatable = true
		result, _ = ipv6Address.GetEmbeddedIPv4Address()
	case ipv6Address.IsIPv4Mapped():
		flags.isIPv4Mapped = true
		result, _ = ipv6Address.GetEmbeddedIPv4Address()
	case ipv6Address.IsLoopback():
		result = ipaddr.IPv4Network.GetLoopback()
	}
	return result, &flags
}

type ConversionFlags struct {
	isTeredo, is6To4, is6Over4, isIsatap, isIPv4Translatable, isIPv4Mapped bool

	teredoFlags  uint16
	teredoPort   uint16
	teredoServer *ipaddr.IPv4Address
}

// ToIPv6 converts an IPv4 address to IPv6 based on the flags
func (flags *ConversionFlags) ToIPv6(ipv4Address *ipaddr.IPv4Address) *ipaddr.IPv6Address {
	zero := ipaddr.NewIPv6Segment(0)
	var segs []*ipaddr.IPv6AddressSegment
	switch {
	case flags.isTeredo:
		segs = make([]*ipaddr.IPv6AddressSegment, ipaddr.IPv6SegmentCount)
		segs[0] = ipaddr.NewIPv6Segment(0x2001)
		segs[1] = zero
		segs[2], _ = flags.teredoServer.GetSegment(0).
			Join(flags.teredoServer.GetSegment(1))
		segs[3], _ = flags.teredoServer.GetSegment(2).
			Join(flags.teredoServer.GetSegment(3))
		segs[4] = ipaddr.NewIPv6Segment(flags.teredoFlags)
		segs[5] = ipaddr.NewIPv6Segment(^flags.teredoPort)
		embeddedVal := ^ipv4Address.Uint32Value()
		segs[6] = ipaddr.NewIPv6Segment(ipaddr.IPv6SegInt(embeddedVal >> 16))
		segs[7] = ipaddr.NewIPv6Segment(ipaddr.IPv6SegInt(embeddedVal & 0xffff))
		addr, _ := ipaddr.NewIPv6AddressFromSegs(segs)
		return addr
	case flags.is6Over4:
		segs = make([]*ipaddr.IPv6AddressSegment, ipaddr.IPv6SegmentCount)
		segs[1], segs[2], segs[3], segs[4], segs[5] = zero, zero, zero, zero, zero
		segs[0] = ipaddr.NewIPv6Segment(0xfe80)
		addr, _ := ipv4Address.GetIPv6Address(ipaddr.NewIPv6Section(segs))
		return addr
	case flags.isIsatap:
		segs = make([]*ipaddr.IPv6AddressSegment, ipaddr.IPv6SegmentCount)
		segs[1], segs[2], segs[3], segs[4] = zero, zero, zero, zero
		segs[0] = ipaddr.NewIPv6Segment(0xfe80)
		segs[5] = ipaddr.NewIPv6Segment(0x5efe)
		addr, _ := ipv4Address.GetIPv6Address(ipaddr.NewIPv6Section(segs))
		return addr
	case flags.isIPv4Translatable:
		segs = make([]*ipaddr.IPv6AddressSegment, ipaddr.IPv6SegmentCount)
		segs[0], segs[1], segs[2], segs[3], segs[5] = zero, zero, zero, zero, zero
		segs[4] = ipaddr.NewIPv6Segment(0xffff)
		addr, _ := ipv4Address.GetIPv6Address(ipaddr.NewIPv6Section(segs))
		return addr
	case flags.is6To4:
		segs = make([]*ipaddr.IPv6AddressSegment, ipaddr.IPv6SegmentCount)
		segs[0] = ipaddr.NewIPv6Segment(0x2002)
		segs[1], _ = ipv4Address.GetSegment(0).Join(ipv4Address.GetSegment(1))
		segs[2], _ = ipv4Address.GetSegment(2).Join(ipv4Address.GetSegment(3))
		segs[3], segs[4], segs[5], segs[6], segs[7] = zero, zero, zero, zero, zero
		addr, _ := ipaddr.NewIPv6AddressFromSegs(segs)
		return addr
	case flags.isIPv4Mapped:
		addr, _ := ipv4Address.GetIPv4MappedAddress()
		return addr
	case ipv4Address.IsLoopback():
		return ipaddr.IPv6Network.GetLoopback()
	default:
		// default conversion is IPv4-mapped
		addr, _ := ipv4Address.GetIPv4MappedAddress()
		return addr
	}
}

Here we show some sample code demonstrating the various conversions.

addressStrs := []string{
	"1.2.3.4",
	"a:b:c:d:e:f:a:b",
	"::ffff:a:b", // IPv4-mapped
	"::1",
	"2002:c000:0204::", // 6 to 4 192.0.2.4
	"2001:0000:4136:e378:8000:63bf:3fff:fdd2", // Teredo, 192.0.2.45
	"fe80::192.0.2.142",                       // 6 over 4
	"::ffff:0:192.0.2.4",                      // IPv4-translatable
	"fe80::0000:5efe:192.0.2.143",             // Isatap
}
for _, addrStr := range addressStrs {
	address := ipaddr.NewIPAddressString(addrStr).GetAddress()
	var converted, convertedBack *ipaddr.IPAddress
	if address.IsIPv4() {
		convertedIPv4, _ := address.ToIPv4().GetIPv4MappedAddress()
		converted = convertedIPv4.ToIP()
		convertedBackIPv6, _ := convertedIPv4.GetEmbeddedIPv4Address()
		convertedBack = convertedBackIPv6.ToIP()
	} else {
		convertedIPv6, flags := convert(address.ToIPv6())
		converted = convertedIPv6.ToIP()
		if converted != nil {
			convertedBack = flags.ToIPv6(convertedIPv6).ToIP()
		}
	}
	fmt.Println("\nstarting with", address)
	if converted != nil {
		fmt.Println("converted to", converted)
		if convertedBack != nil {
			fmt.Println("converted back to", convertedBack)
		} else {
			fmt.Println("not convertible back")
		}
	} else {
		fmt.Println("not convertible")
	}
}

Output:

starting with 1.2.3.4
converted to ::ffff:102:304
converted back to 1.2.3.4

starting with a:b:c:d:e:f:a:b
not convertible

starting with ::ffff:a:b
converted to 0.10.0.11
converted back to ::ffff:a:b

starting with ::1
converted to 127.0.0.1
converted back to ::1

starting with 2002:c000:204::
converted to 192.0.2.4
converted back to 2002:c000:204::

starting with 2001:0:4136:e378:8000:63bf:3fff:fdd2
converted to 192.0.2.45
converted back to 2001:0:4136:e378:8000:63bf:3fff:fdd2

starting with fe80::c000:28e
converted to 192.0.2.142
converted back to fe80::c000:28e

starting with ::ffff:0:c000:204
converted to 192.0.2.4
converted back to ::ffff:0:c000:204

starting with fe80::5efe:c000:28f
converted to 192.0.2.143
converted back to fe80::5efe:c000:28f

Write/Read Addresses to/from Byte Buffer in Native Byte Order

Addresses transmitted across a network as multi-byte integers, such as in the header of an IPv4 or IPv6 packet, are in network byte order (big endian), the IP protocol standard ordering of bytes for multi-byte integers.

IPAddress uses the same ordering for segments, byte arrays and integers, much like net.IP.

When using a native integer on a little-endian machine, or any other address value in little-endian byte order, you can switch the byte order before writing, or after reading. Most architectures in widespread use today use little-endian, whether Intel/AMD (MacOs or Windows), ARM (supports both, but most devices using ARM are little-endian), IBM Power (was big-endian but now little), so doing the byte reversal between network and native architecture is common.

The example separates IPv4 from IPv6, writes each set to a byte buffer in native byte order, then reads the addresses back in again from each buffer.

ipAddrStrings := getAddressStrings(
	"1.2.3.4",
	"1:2:3:4:5:6:7:8",
	"127.0.0.1",
	"::1")
ipv4Bytes := writeToBuffer(filterByVersion(ipaddr.IPv4, ipAddrStrings))
ipv6Bytes := writeToBuffer(filterByVersion(ipaddr.IPv6, ipAddrStrings))
ipv4Addresses := readFromBuffer(ipv4Bytes, ipaddr.IPv4ByteCount)
ipv6Addresses := readFromBuffer(ipv6Bytes, ipaddr.IPv6ByteCount)
fmt.Println(ipaddr.IPv4.String()+":", ipv4Addresses)
fmt.Println(ipaddr.IPv6.String()+":", ipv6Addresses)

func getAddressStrings(addrStrs ...string) (res []ipaddr.IPAddressString) {
	res = make([]ipaddr.IPAddressString, len(addrStrs))
	for i, str := range addrStrs {
		res[i] = *ipaddr.NewIPAddressString(str)
	}
	return
}

func filterByVersion(version ipaddr.IPVersion,
	ipAddrStrs []ipaddr.IPAddressString) []ipaddr.IPAddress {
	res := make([]ipaddr.IPAddress, 0, len(ipAddrStrs))
	for _, str := range ipAddrStrs {
		if str.GetIPVersion().Equal(version) {
			res = append(res, *str.GetAddress())
		}
	}
	return res
}

func isLittleEndian() bool {
	buf := [2]byte{}
	*(*uint16)(unsafe.Pointer(&buf[0])) = uint16(0xff00)
	return buf[1] == 0xff
}

func writeToBuffer(ipAddresses []ipaddr.IPAddress) []byte {
	if len(ipAddresses) > 0 {
		addrByteCount := ipAddresses[0].GetByteCount()
		buf := make([]byte, len(ipAddresses)*addrByteCount)
		isLittleEndian := isLittleEndian()
		for i, addr := range ipAddresses {
			if isLittleEndian {
				addrPtr, _ := addr.ReverseBytes()
				addr = *addrPtr
			}
			addr.CopyBytes(buf[i*addrByteCount:])
		}
		return buf
	}
	return nil
}

func readFromBuffer(bytes []byte, addressByteCount int) []ipaddr.IPAddress {
	result := make([]ipaddr.IPAddress, 0, len(bytes)/addressByteCount)
	isLittleEndian := isLittleEndian()
	for i, j := 0, addressByteCount; i < len(bytes); i, j = j, j+addressByteCount {
		addr, _ := ipaddr.NewIPAddressFromNetIP(bytes[i:j])
		if isLittleEndian {
			addr, _ = addr.ReverseBytes()
		}
		result = append(result, *addr)
	}
	return result
}

The output shows the addresses read back from the two buffers, which matches the original four addresses.

Output:

IPv4: [1.2.3.4 127.0.0.1]
IPv6: [1:2:3:4:5:6:7:8 ::1]

Range over Trie Iterators of Subnet Mappings to Produce Nested Subnet Yaml

In a preceding example we produced dual IPv4/IPv6 tries of subnet mappings.

In this example, the dual tries are converted to a yaml string showing the subnet containment and mappings.

The instance of ipaddr.DualIPv4v6AssociativeTries[AssignedBlock] does not provide node iterators directly, but you can still get a node iterator from each individual trie. Starting with the tries variable created in the function named allocate of the preceding example, the yaml is created from each individual trie.

builder := strings.Builder{}
builder.WriteString("---\n")
trieYaml(tries.GetIPv4Trie(), &builder)
trieYaml(tries.GetIPv6Trie(), &builder)
str := builder.String()
fmt.Println(str)

Type aliases make the code simpler. Simply remember that the mapped values are AssignedBlock, while the keys are *ipaddr.IPAddress.

type TrieType = ipaddr.AssociativeTrie[*ipaddr.IPAddress, AssignedBlock]
type NodeType = ipaddr.AssociativeTrieNode[*ipaddr.IPAddress, AssignedBlock]

For the proper nested structure in the yaml, each child subnet follows each parent, but with an indentation. This requires a pre-order trie traversal. The methods ContainingFirstIterator or ContainingFirstAllNodeIterator provide a node iterator with this ordering. Needing to show only the assigned subnets, not all subnets in the trie, ContainingFirstIterator is sufficient.

The iterator is converted to a standard library iter.Seq iterator to use the Go 1.23 range loop on iterator functionality.

The indentation of each yaml line is calculated by following each iterated node to the root of the trie, to get that node's depth.

func trieYaml(trie *TrieType, out io.StringWriter) {
	var iterator ipaddr.Iterator[*NodeType] = trie.ContainingFirstIterator(true)

	// convert to a standard libary iterator and use the range operator
	var seq iter.Seq[*NodeType] = ipaddr.StdPushIterator(iterator)

	for node := range seq {
		// The indentation of each line corresponds to the subnet depth,
		// the number of containing subnets per subnet,
		// which is the number of added parent nodes for each subnet.
		parentCount := 0
		for parent := node.GetParent(); parent != nil; parent = parent.GetParent() {
			if parent.IsAdded() {
				parentCount++
			}
		}
		indentation := 4 * parentCount // each indent is 4 spaces
		if indentation > len(spaces) {
			spaces += spaces
		}
		out.WriteString(spaces[:indentation])
		out.WriteString(node.GetKey().String())
		valStr := node.GetValue().String()
		if node.Size() > 1 && len(valStr) > 0 {
			out.WriteString(": # ")
		} else {
			out.WriteString(": ")
		}
		out.WriteString(valStr)
		out.WriteString("\n")
	}
}

var spaces = "    " // 4 spaces to start, for the first indent

The yaml follows:

---
0.0.0.0/0: 
    10.0.0.0/8: 
        10.0.0.0/12: # eu-west-1
            10.0.0.0/18: # Account A
                10.0.0.0/21: Account A eu-west-1a
                10.0.8.0/21: Account A eu-west-1b
                10.0.16.0/21: Account A eu-west-1c
            10.0.64.0/18: # Account B
                10.0.64.0/21: Account B eu-west-1a
                10.0.72.0/21: Account B eu-west-1b
                10.0.80.0/21: Account B eu-west-1c
        10.16.0.0/12: # ap-southeast-1
            10.16.0.0/18: # Account A
                10.16.0.0/21: Account A ap-southeast-1a
                10.16.8.0/21: Account A ap-southeast-1b
            10.16.64.0/18: # Account B
                10.16.64.0/21: Account B ap-southeast-1a
                10.16.72.0/21: Account B ap-southeast-1b
::/0: 
    fd00::/64: 
        fd00::/108: # eu-west-1
            fd00::/114: # Account A
                fd00::/117: Account A eu-west-1a
                fd00::800/117: Account A eu-west-1b
                fd00::1000/117: Account A eu-west-1c
            fd00::4000/114: # Account B
                fd00::4000/117: Account B eu-west-1a
                fd00::4800/117: Account B eu-west-1b
                fd00::5000/117: Account B eu-west-1c
        fd00::10:0/108: # ap-southeast-1
            fd00::10:0/114: # Account A
                fd00::10:0/117: Account A ap-southeast-1a
                fd00::10:800/117: Account A ap-southeast-1b
            fd00::10:4000/114: # Account B
                fd00::10:4000/117: Account B ap-southeast-1a
                fd00::10:4800/117: Account B ap-southeast-1b

The iteration can be made more efficient with a bit more code. With the code above, the chain of parent nodes is visited with every node iteration. Caching the depth with each parent's sub-nodes avoids those traversals. For the caching to be pervasive across the trie iteration, all nodes must be visited, not just the "added" nodes, so it becomes necessary to use ContainingFirstAllNodeIterator.

func trieYaml(trie *TrieType, out io.StringWriter) {
	iterator := trie.ContainingFirstAllNodeIterator(true)

	// convert to standard libary iterators to use the range operator
	pointIterator := ipaddr.NewPointCachingTrieIterator(iterator)
	var seq iter.Seq[ipaddr.CachingTrieIteratorPosition[*NodeType]]
	seq = ipaddr.StdPushIterator(pointIterator)

	for point := range seq {
		node := point.Value()

		parentCount := 0
		if cachedDepth := point.GetCached(); cachedDepth != nil {
			// subnet depth was previously cached
			parentCount = cachedDepth.(int)
		}

		if node.IsAdded() { // only print added nodes
			indentation := 4 * parentCount // each indent is 4 spaces
			if indentation > len(spaces) {
				spaces += spaces
			}
			out.WriteString(spaces[:indentation])
			out.WriteString(node.GetKey().String())
			valStr := node.GetValue().String()
			if node.Size() > 1 && len(valStr) > 0 {
				out.WriteString(": # ")
			} else {
				out.WriteString(": ")
			}
			out.WriteString(valStr)
			out.WriteString("\n")
			parentCount++
		}

		// cache the subnet depth with each sub-node
		point.CacheWithLowerSubNode(parentCount)
		point.CacheWithUpperSubNode(parentCount)
	}
}

This produces yaml identical to the yaml shown above.