slackhq/nebula · error
unable to determine IP version from packet
Error message
unable to determine IP version from packet
What it means
The tun device write path (tun.Write on FreeBSD) prepends a 4-byte BSD header whose byte 3 encodes the address family (AF_INET or AF_INET6). The first byte of the outgoing packet is inspected to determine the IP version; if it is neither 4 nor 6 the packet is malformed and the write fails with this error. The library refuses to guess the family because writing it into the tun header incorrectly would corrupt routing on the FreeBSD tun interface.
Source
Thrown at overlay/tun_freebsd.go:205
}
}
// Write is only valid for single threaded use
func (t *tun) Write(from []byte) (int, error) {
if len(from) <= 1 {
return 0, syscall.EIO
}
ipVer := from[0] >> 4
var head [4]byte
// first 4 bytes is protocol family, in network byte order
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")
}
iovecs := [2]syscall.Iovec{
{&head[0], 4},
{&from[0], uint64(len(from))},
}
for {
n, _, errno := syscall.Syscall(syscall.SYS_WRITEV, uintptr(t.fd), uintptr(unsafe.Pointer(&iovecs[0])), 2)
if errno == 0 {
return int(n) - 4, nil
}
switch errno {
case unix.EAGAIN:
if err := t.blockOnWrite(); err != nil {
return 0, err
}
case unix.EINTR:
// retryView on GitHub (pinned to dd8f660c0a)
Solutions
- Verify the packet buffer begins with a valid IP version nibble before calling Write: (b[0]>>4) must equal 4 or 6
- If your source traffic is Ethernet-framed, strip the 14-byte Ethernet header and pass only the IP payload
- Check upstream code for off-by-one slicing that shifts the IP header out of position
- Drop the malformed packet and log it instead of propagating an error into the write loop
Example fix
// before
_, err := tunDev.Write(buf)
// after
if len(buf) == 0 || (buf[0]>>4 != 4 && buf[0]>>4 != 6) {
log.Warn("dropping non-IP packet", "firstByte", buf)
continue
}
_, err := tunDev.Write(buf) Defensive patterns
Strategy: validation
Validate before calling
func isValidIPPacket(b []byte) bool {
return len(b) >= 1 && (b[0]>>4 == 4 || b[0]>>4 == 6)
}
// call site:
if !isValidIPPacket(buf) { log.Warn("drop non-IP packet"); continue } Type guard
func isIPv4(b []byte) bool { return len(b) >= 20 && b[0]>>4 == 4 }
func isIPv6(b []byte) bool { return len(b) >= 40 && b[0]>>4 == 6 } Try / catch
n, err := tunDev.Write(buf)
if err != nil {
if strings.Contains(err.Error(), "unable to determine IP version") {
log.Warn("skipping malformed packet", "err", err)
continue
}
return err
} Prevention
- Validate the first nibble of every packet before writing to the tun
- Strip Ethernet headers from any L2-sourced traffic before handing it to an L3 tun
- Never write empty or partially-read buffers
- Log and drop malformed packets instead of failing the write loop
When it happens
Trigger: Calling Write() on the FreeBSD tun with a buffer that does not begin with a valid IP header — the first nibble of the first byte must be 0x4 (IPv4) or 0x6 (IPv6). This happens with zero-length packets, packets containing Ethernet frames instead of raw IP, or corrupted/partial buffers.
Common situations: Passing L2 (Ethernet-framed) traffic from a layer-2 VPN mode into an L3 tun; reading packets from a socket and slicing off too many header bytes; a peer sending non-IP garbage over a raw channel; an empty or uninitialized buffer after a failed read upstream.
Related errors
- no inside interface (tun)
- ErrIPv6CouldNotFindPayload
- ErrIPv6PacketTooShort
- unable to determine IP version from packet
- ErrInvalidIPv6RemoteForSocket
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/4b71a10a80175a25.
Report an issue: GitHub.