Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
357 lines (283 sloc) 14.2 KB

Building ICMPv6 Messages using google/gopacket

For a few months in 2018 I was working at a company working on network programming with GoLang. We sometimes need to write test code which sends individual packets using protocols, such as ARP or ICMPv6. At one point I needed to send ICMPv6 Neighbor Solicitation messages using google/gopacket - and found there wasn’t much material online about how to get it to work. So I thought I’d best write one, and then let it sit around for 6 months before actually publishing it online.

Overall, I’m going to take it pretty slow, but if you want to you can skip to the very end where the entire program is put together.


For example, let's say an ICMPv6 Neighbor Solicitation message is needed, as described in RFC 4861 Section 4.3 - something like this hex dump:

33 33 ff 00 00 01 0e 31 b1 c5 4c f6 86 dd 60 00
00 00 00 20 3a ff fd 10 00 00 00 00 00 00 00 00
00 00 00 00 00 02 ff 02 00 00 00 00 00 00 00 00
00 01 ff 00 00 01 87 00 72 8c 00 00 00 00 fd 10
00 00 00 00 00 00 00 00 00 00 00 00 00 01 01 01
0e 31 b1 c5 4c f6

That hex dump represents an ICMPv6 Neighbor Solicitation message. If the above were imported as a hex dump into Wireshark, the packet summary would read something like this:

Source Destination Protocol Length Info
fd10::2 ff02::1:ff00:1 ICMPv6 86 Neighbor Solicitation for fd10::1 from 0e:31:b1:c5:4c:f6

... and Wireshark would make it easy to see the encapsulated layers this packet is organized into:

  • First, the Ethernet II layer:

    33 33 ff 00 00 01 0e 31 b1 c5 4c f6 86 dd`
    
  • Next, an Internet Protocol Version 6 layer:

    -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60 00
    00 00 00 20 3a ff fd 10 00 00 00 00 00 00 00 00
    00 00 00 00 00 02 ff 02 00 00 00 00 00 00 00 00
    00 01 ff 00 00 01
    
  • And lastly an Internet Control Message Protocol v6 layer:

    -- -- -- -- -- -- 87 00 72 8c 00 00 00 00 fd 10
    00 00 00 00 00 00 00 00 00 00 00 00 00 01 01 01
    0e 31 b1 c5 4c f6
    

To build this packet in golang - or at least one like it - each of these layers needs to be constructed individually, using google/gopacket/layers.

The Ethernet Layer

The packet's Ethernet layer is actually composed of three parts:

  • a destination address - MAC address 33-33-ff-00-00-01
  • a source address - MAC address 0e-31-b1-c5-4c-f6
  • an EtherType - 0x86dd, specifying that the next layer is IPv6 per RFC 7042

Conveniently, the google/gopacket/layers package has a layers.Ethernet type with exactly those fields:

	ethLayer := layers.Ethernet{
		SrcMAC:       srcMacAddr,
		DstMAC:       dstMacAddr,
		EthernetType: layers.EthernetTypeIPv6,
	}

But what values should be used for the SrcMAC and DstMAC? The source address is easy - it should be the MAC address for the network interface from which the message will be sent. That can be determined pretty easily using vishvananda/netlink:

	intfName := "eth0" // TODO: change name of networking interface to one which exists on one's personal computer

	intf, err := netlink.LinkByName(intfName)
	if err != nil {
		// handle the error
	}

	srcMacAddr := intf.Attrs().HardwareAddr

The destination address is a little trickier, since Neighbor Solicitation messages are supposed to be sent to an IPv6 solicited-node multicast address. That means the destination MAC will have to use an Ethernet multicast address as described in RFC 2464:

An IPv6 packet with a multicast destination address DST, consisting of the sixteen octets DST[1] through DST[16], is transmitted to the Ethernet multicast address whose first two octets are the value 3333 hexadecimal and whose last four octets are the last four octets of DST.

So the destination MAC address will be determined by the bytes in the destination IPv6 address, which in turn should be a solicited-node multicast address as described in RFC 429, Section 2.7.1:

Solicited-Node multicast address are computed as a function of a node's unicast and anycast addresses. A Solicited-Node multicast address is formed by taking the low-order 24 bits of an address (unicast or anycast) and appending those bits to the prefix FF02:0:0:0:0:1:FF00::/104 resulting in a multicast address in the range

The google/gopacket library doesn't currently offer any convenience methods for creating these multicast addresses - nor does package net - but it won't take much to do it the old fashioned way:

	ipv6Addr := net.ParseIP("fd10::1") // fd10::1 was the target IP for our solicitation
	solicitedMulticastIPAddr, _, _ := net.ParseCIDR("FF02:0:0:0:0:1:FF00::/104")
	for i:=len(ipv6Addr)-3; i<len(ipv6Addr); i++ {
		solicitedMulticastIPAddr[i] = ipv6Addr[i]
	}

	byteArr := []byte {0x33, 0x33}
	// the last 32 bits (last 4 bytes) come from the IPv6 multicast address
	for i:=len(ipV6Addr)-4; i<len(solicitedMulticastIPAddr); i++{
		byteArr = append(byteArr, solicitedMulticastIPAddr[i])
	}

	dstMacAddr := net.HardwareAddr(byteArr)

The IPv6 Layer

Like the Ethernet layer which came before, the packet's IPv6 layer is actually composed of several parts:

  • the 4-bit IP version - always 6 according to RFC 1883 section 3
  • the 4-bit Priority and 24-bit Flow Label - all zeroes in our example and not really relevant to this exercise
  • a Payload Length - 0x0020 or 32
  • the Next Header - 0x3a or ICMPv6 as per RFC 4443 secion 2.3
  • a Hop Limit - 0xff or 255, but not really relevant to this exercise
  • the IPv6 Source Address - 16-byte address of the originator of the packet, in this case fd10:0000:0000:0000:0000:0000:0000:0002 or just fd10::2
  • the IPv6 Destination Address - 16-byte address of the intended recipient of the packet, in this case the solicited multicast address ff02:00:00:00:00:00:00:00:00:00:01:ff:00:00:01

Like with the Ethernet layer, the google/gopacket/layers package has a convenient layers.IPv6 type with those fields:

	ipV6Layer := layers.IPv6{
		SrcIP: srcIPAddr,
		DstIP: solicitedMulticastIPAddr,
		Version: 6,
		NextHeader: layers.IPProtocolICMPv6,
		HopLimit: 255,
	}

The source address can be determined programmatically by inspecting that network interface using using the golang net package:

	intfName := "eth0" // TODO: change name of networking interface to one which exists on one's personal computer

        intf, err := net.InterfaceByName(intfName)
        if err != nil {
                fmt.Printf("failed to get intf - %v", err)
                return err
        }

        addrs, err := intf.Addrs()
        if err != nil {
                fmt.Printf("failed to get intf addresses - %v", err)
                return err
        }
        for _, addr := range addrs {
                var ip net.IP
                switch addr := addr.(type) {
                case *net.IPAddr:
                        ip = addr.IP
                case *net.IPNet:
                        ip = addr.IP
                }
		// Maybe check that it's not a link local IP address?
                fmt.Println("%s", ip)
        }

... and the destination address - a solicited-node multicast address - was already constructed while populating values for the Ethernet layer.

Of note is that the length field has been ommited from the created layers.IPv6 instance. While there is a Length field on the layers.IPv6 struct, it's best not to specify its value and to calculate instead, while serializing all the layers into a packet.

The ICMPv6 Layer

The last 32 bytes of the packet are the ICMPv6 Layer and its various fields:

  • the ICMPv6 Type and Code - 0x87 (135) and 0x00 (0) for Neighbor Solicitation, as per RFC 4861 section 4.3
  • the ICMP Checksum - for this packet it's 0x728c
  • 4 reserved bytes, 0x00000000
  • the IPv6 Target Address - the 16-byte IP Address of the neighbor we're attempting to solicit a response from
  • An 8-byte Source Link-Layer Address option, as described in RFC 4861 section 4.6.1, with:
    • Option Type 0x01
    • Length 0x01
    • Link Layer Address 0e-31-b1-c5-4c-f6 - which matches the source address from the Ethernet layer

Like the layers which came before, the google/gopacket/layers package does have a layers.ICMPv6 type which can be used to create the ICMPv6 layer. However, the layers.ICMPv6 struct must be used in conjection with another layer like layers.ICMPv6NeighborSolicitation:

	targetIPAddr := net.ParseIP("fd10::1")

	icmpv6Layer := layers.ICMPv6{
		TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeNeighborSolicitation, 0),
	}

	ndLayer := layers.ICMPv6NeighborSolicitation{
		TargetAddress: targetIPAddr,
		Options: layers.ICMPv6Options{
			layers.ICMPv6Option{
				Type: layers.ICMPv6OptSourceAddress,
				Data: srcMacAddr,
			},
		},
	}

	err = icmpv6Layer.SetNetworkLayerForChecksum(&ipV6Layer)
	if err != nil {
		return fmt.Errorf("Error while setting the checksum network layer for ICMPv6 layer: %s", err)
	}

Like the IPv6 layer, it has a Length which is best left blank until serializing all the layers into a packet. The same goes for the Checksum field, although the network layer to use while calculating the checksum - the IPv6 layer - must be specified using SetNetworkLayerForChecksum(...).

Serializing the Layers

Once all the fields for each layer of the message are populated (except lengths and checksums) the layers can be serialized into a byte array using the SerializeLayers(...) function in google/gopacket. The second argument - of type SerializeOptions - can specify that any Lengths or Checksum values on any layers should be calculated and be populated:

	buffer := gopacket.NewSerializeBuffer()
	options := gopacket.SerializeOptions{
		FixLengths:       true,
		ComputeChecksums: true,
	}
	err = gopacket.SerializeLayers(buffer, options, &ethLayer, &ipV6Layer, &icmpv6Layer, &ndLayer)
	if err != nil {
		return fmt.Errorf("unable to serialize layers for Neighbor Solicitation request: %s", err)
	}

Putting It All Together

Here's the final script. Its output can be piped into a file and imported as a hex dump into Wireshark to confirm the resulting packet is valid:

package main

import (
	"fmt"
	"encoding/hex"
	"net"
	"github.com/vishvananda/netlink"
	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
)

func main() {
	intfName := "sw1p2"

	link, err := netlink.LinkByName(intfName)
	if err != nil {
		fmt.Printf("Error from netlink.LinkByName(...): %s\n", err)
		return
	}

	srcMacAddr := link.Attrs().HardwareAddr

	ipv6Addr := net.ParseIP("fd10::1") // fd10::1 was the target IP for our solicitation
	solicitedMulticastIPAddr, _, _ := net.ParseCIDR("FF02:0:0:0:0:1:FF00::/104")
	for i:=len(ipv6Addr)-3; i<len(ipv6Addr); i++ {
		solicitedMulticastIPAddr[i] = ipv6Addr[i]
	}

	byteArr := []byte {0x33, 0x33}
	// the last 32 bits (last 4 bytes) come from the IPv6 multicast address
	for i:=len(ipv6Addr)-4; i<len(solicitedMulticastIPAddr); i++{
		byteArr = append(byteArr, solicitedMulticastIPAddr[i])
	}

	dstMacAddr := net.HardwareAddr(byteArr)

	ethLayer := layers.Ethernet{
		SrcMAC:       srcMacAddr,
		DstMAC:       dstMacAddr,
		EthernetType: layers.EthernetTypeIPv6,
	}

        intf, err := net.InterfaceByName(intfName)
        if err != nil {
                fmt.Printf("failed to get intf - %v", err)
                return
        }

	var srcIPAddr net.IP
        addrs, err := intf.Addrs()
        if err != nil {
                fmt.Printf("failed to get intf addresses - %v", err)
                return
        }
        for _, addr := range addrs {
                var ip net.IP
                switch addr := addr.(type) {
                case *net.IPAddr:
                        ip = addr.IP
                case *net.IPNet:
                        ip = addr.IP
                }
		isIPv6 := !ip.To4().Equal(ip)
		if isIPv6 && !ip.IsLinkLocalUnicast() { // this is an IPv6 address
			srcIPAddr = ip
			break
		}
        }

	ipV6Layer := layers.IPv6{
		SrcIP: srcIPAddr,
		DstIP: solicitedMulticastIPAddr,
		Version: 6,
		NextHeader: layers.IPProtocolICMPv6,
		HopLimit: 255,
	}

	targetIPAddr := net.ParseIP("fd10::1")

	icmpv6Layer := layers.ICMPv6{
		TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeNeighborSolicitation, 0),
	}

	ndLayer := layers.ICMPv6NeighborSolicitation{
		TargetAddress: targetIPAddr,
		Options: layers.ICMPv6Options{
			layers.ICMPv6Option{
				Type: layers.ICMPv6OptSourceAddress,
				Data: srcMacAddr,
			},
		},
	}

	err = icmpv6Layer.SetNetworkLayerForChecksum(&ipV6Layer)
	if err != nil {
		fmt.Printf("Error while setting the checksum network layer for ICMPv6 layer: %s", err)
		return
	}

	buffer := gopacket.NewSerializeBuffer()
	options := gopacket.SerializeOptions{
		FixLengths:       true,
		ComputeChecksums: true,
	}
	err = gopacket.SerializeLayers(buffer, options, &ethLayer, &ipV6Layer, &icmpv6Layer, &ndLayer)
	if err != nil {
		fmt.Printf("unable to serialize layers for Neighbor Solicitation request: %s", err)
		return
	}

	for i, b := range(buffer.Bytes()) {
		fmt.Printf("%s", hex.EncodeToString([]byte {b}))
		if i%16 == 15 {
			fmt.Println()
		} else {
			fmt.Print(" ")
		}
	}

	fmt.Println()

	return
}
You can’t perform that action at this time.