-
Notifications
You must be signed in to change notification settings - Fork 9
Code Examples 4: Converting to and from Other Formats
- Convert to/from Binary String from/to IP Address
- Convert to/from IPv6 Address from/to MAC Address
- Convert to/from IPv6 Address from/to Ascii Base 85 Encoding
- Convert to/from IPv4 Address from/to IPv6 Address
- Write/Read Addresses to/from Byte Buffer in Native Byte Order
- Range over Trie Iterators of Subnet Mappings to Produce Nested Subnet Yaml
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
// 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
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
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
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]
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.