slackhq/nebula · warning

unable to determine IP version from packet

Error message

unable to determine IP version from packet

What it means

tun.Write() prepends a 4-byte Darwin utun protocol header whose 4th byte must be the address family (AF_INET or AF_INET6) of the packet being injected into the interface. This error is returned when the first nibble of the packet is neither 4 nor 6, so nebula cannot determine which family byte to write into the utun header.

Source

Thrown at overlay/tun_darwin.go:569

}

// Write pushes one IP packet onto the utun device. Safe for concurrent use:
// the AF prefix and iovecs are per-call stack state, and the fd write itself
// serializes on the runtime's fd mutex (see the Queue contract in tio.go).
func (t *tun) Write(from []byte) (int, error) {
	if len(from) == 0 {
		return 0, syscall.EIO
	}

	ipVer := from[0] >> 4
	var head [4]byte
	switch ipVer {
	case 4:
		head[3] = syscall.AF_INET
	case 6:
		head[3] = syscall.AF_INET6
	default:
		return 0, fmt.Errorf("unable to determine IP version from packet")
	}

	// Grab rc as a local so the compiler can devirtualize the call and keep the closure on the stack.
	rc, err := t.f.SyscallConn()
	if err != nil {
		return 0, err
	}

	var n int
	var callErr error
	err = rc.Write(func(fd uintptr) bool {
		iovecs := []unix.Iovec{
			{Base: &head[0], Len: 4},
			{Base: &from[0], Len: uint64(len(from))},
		}
		n, callErr = tunWritev(int(fd), iovecs)
		// Type-assert to syscall.Errno so the EAGAIN/EWOULDBLOCK/EINTR check doesn't box the errno
		// constants into error interfaces on every call.

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Treat the packet as undeliverable and drop it (the error is per-packet; nebula continues serving traffic).
  2. Check the upstream path for corruption: verify MTU settings match across the tunnel (tun.mtu) so packets aren't truncated.
  3. Ensure you're on a current nebula version; upstream fixes handle malformed packets more gracefully.
  4. If running an embedded/custom build, confirm the caller of tun.Write only passes full IP packets, not link-layer frames.

Example fix

// before: writing raw frames
tun.Write(ethernetFrame)
// after: strip the link layer and write full IP packets only
ipPacket := stripEthernetHeader(ethernetFrame)
tun.Write(ipPacket)
Defensive patterns

Strategy: validation

Validate before calling

func isIPPacket(b []byte) bool {
    if len(b) < 1 {
        return false
    }
    switch b[0] >> 4 {
    case 4, 6:
        return true
    default:
        return false
    }
}
// call before handing data to the tun writer

Type guard

func isIPPacket(b []byte) bool {
    return len(b) >= 1 && (b[0]>>4 == 4 || b[0]>>4 == 6)
}

Try / catch

n, err := iface.Write(packet)
if err != nil {
    if strings.Contains(err.Error(), "unable to determine IP version from packet") {
        log.Debug("dropping non-IP packet", "len", len(packet))
        return // per-packet error; do not crash
    }
    return err
}

Prevention

When it happens

Trigger: Write() called with a packet whose first byte's high nibble is not 0x4 (IPv4) or 0x6 (IPv6) — i.e. corrupted, truncated, non-IP, or garbage data handed to the tun writer.

Common situations: Packet corruption or memory issues upstream; a bug in another component feeding non-IP frames into the tun writer; encrypted/garbage payloads misrouted to the tun; truncated packets read from the wire.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/979df23f0f73dbf0. Report an issue: GitHub.