#1762·nebula

bug: parseV6 reads ICMPv6 Code byte instead of Type byte, breaking Echo conntrack

Author: herdiyana256Created Jun 14, 2026Updated Jul 19, 2026

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.

go
// 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: Identifier

For 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

go
// 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

go
// 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