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
- Treat the packet as undeliverable and drop it (the error is per-packet; nebula continues serving traffic).
- Check the upstream path for corruption: verify MTU settings match across the tunnel (tun.mtu) so packets aren't truncated.
- Ensure you're on a current nebula version; upstream fixes handle malformed packets more gracefully.
- 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
- Only pass full IP packets (not Ethernet frames) to tun.Write
- Match tun.mtu across the overlay so packets aren't truncated
- Keep nebula updated for upstream malformed-packet handling
- Log and drop bad packets rather than retrying them
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
- failed to run tun device: %s
- ErrPacketTooShort
- ErrPacketTooShort
- newTunFromFd not supported in Darwin
- failed to set tun v4 address: %s
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/979df23f0f73dbf0.
Report an issue: GitHub.