bug: parseV6 reads ICMPv6 Code byte instead of Type byte, breaking Echo conntrack
Bug Description
In outside.go ~line 371, the parseV6 function identifies ICMPv6 packet types by reading data[offset+1] (the Code field), instead of data[offset] (the Type field) as specified in RFC 4443.
// outside.go ~L371 — current (buggy)
icmptype := data[offset+1] // reads Code, not Type
switch icmptype {
case layers.ICMPv6TypeEchoRequest, layers.ICMPv6TypeEchoReply:
fp.RemotePort = binary.BigEndian.Uint16(data[offset+4 : offset+6])
default:
fp.RemotePort = 0
}Root Cause
Per RFC 4443, the ICMPv6 header wire format is:
Byte 0: Type ← should be read
Byte 1: Code ← currently read by mistake
Byte 2-3: Checksum
Byte 4-5: IdentifierFor a real OS-generated Echo Request (Type=0x80, Code=0x00), data[offset+1] = 0, which does not match ICMPv6TypeEchoRequest (128). The identifier is never extracted, and fp.RemotePort is always set to 0 for all real ICMPv6 echo traffic.
Why the Test Doesn't Catch This
The existing test in outside_test.go uses layers.ICMPv6{TypeCode: layers.ICMPv6TypeEchoRequest}. The gopacket constant ICMPv6TypeEchoRequest = 128 = 0x0080 is serialized as BigEndian uint16 → wire bytes [0x00, 0x80], which puts the Type value at byte[1] instead of byte[0] (opposite of RFC 4443). This causes the two bugs to cancel out — tests pass, but production traffic is broken.
Reproduction
// Run from inside the nebula directory: go run /tmp/poc.go
package main
import (
"encoding/binary"
"fmt"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
func main() {
offset := 40
// Real OS ICMPv6 Echo Request (RFC 4443)
real := make([]byte, 40+8)
real[40] = 128 // Type = Echo Request
real[41] = 0 // Code = 0
real[44] = 0xCA; real[45] = 0xFE // Identifier = 0xCAFE
icmptype := real[offset+1] // bug: reads Code=0
fmt.Printf("Real packet → icmptype read=%d, want=128, match=%v\n",
icmptype, icmptype == uint8(layers.ICMPv6TypeEchoRequest))
if binary.BigEndian.Uint16(real[44:46]) != 0 && icmptype != 128 {
fmt.Println(" BUG: Identifier not extracted → RemotePort=0 in conntrack")
}
// gopacket test packet (masks the bug)
ip := layers.IPv6{Version: 6, HopLimit: 64, NextHeader: layers.IPProtocolICMPv6}
icmp := layers.ICMPv6{TypeCode: layers.ICMPv6TypeEchoRequest}
echo := layers.ICMPv6Echo{Identifier: 0xCAFE}
buf := gopacket.NewSerializeBuffer()
gopacket.SerializeLayers(buf, gopacket.SerializeOptions{}, &ip, &icmp, &echo)
b := buf.Bytes()
fmt.Printf("gopacket pkt → byte[40]=%d byte[41]=%d (Type at byte[1]! reversed vs RFC)\n",
b[40], b[41])
fmt.Printf("Bug reads byte[41]=%d → matches ICMPv6TypeEchoRequest=%d → test PASSES (false)\n",
b[41], uint8(layers.ICMPv6TypeEchoRequest))
}Output:
Real packet → icmptype read=0, want=128, match=false
BUG: Identifier not extracted → RemotePort=0 in conntrack
gopacket pkt → byte[40]=0 byte[41]=128 (Type at byte[1]! reversed vs RFC)
Bug reads byte[41]=128 → matches ICMPv6TypeEchoRequest=128 → test PASSES (false)Impact
All Nebula deployments using IPv6 are affected. Every ICMPv6 Echo session through a Nebula tunnel gets RemotePort=0 in conntrack regardless of the actual identifier, so multiple simultaneous ping sessions to the same host cannot be tracked independently.
Fix
// outside.go ~L371
// Before:
icmptype := data[offset+1]
// After (correct per RFC 4443):
icmptype := data[offset]Also update outside_test.go to use layers.CreateICMPv6TypeCode(128, 0) instead of layers.ICMPv6TypeEchoRequest when building test ICMPv6 packets, so tests use proper RFC 4443 byte ordering.
Environment
- nebula: latest (main branch)
- go: 1.25.0
- Tested on: linux/amd64
Source: slackhq/nebula