slackhq/nebula · error
unable to determine IP version from packet
Error message
unable to determine IP version from packet
What it means
NetBSD tun devices in 'link-layer' mode require each outgoing packet to be prefixed with a 4-byte header containing the address family. Write() inspects the first byte(s) to determine IPv4 vs IPv6 and writes AF_INET/AF_INET6 into the header; if the payload does not look like an IP packet (version nibble neither 4 nor 6), it refuses with this error.
Source
Thrown at overlay/tun_netbsd.go:205
return bytesRead - 4, nil
}
}
// 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
if ipVer == 4 {
head[3] = syscall.AF_INET
} else if ipVer == 6 {
head[3] = syscall.AF_INET6
} else {
return 0, fmt.Errorf("unable to determine IP version from packet")
}
rc, err := t.f.SyscallConn()
if err != nil {
return 0, err
}
var errno syscall.Errno
var n uintptr
err = rc.Write(func(fd uintptr) bool {
iovecs := []syscall.Iovec{
{&head[0], 4},
{&from[0], uint64(len(from))},
}
n, _, errno = syscall.Syscall(syscall.SYS_WRITEV, fd, uintptr(unsafe.Pointer(&iovecs[0])), uintptr(2))
// According to NetBSD documentation for TUN, writes will only return errors in which
// this packet will never be delivered so just go on living life.View on GitHub (pinned to dd8f660c0a)
Solutions
- Only write well-formed IPv4/IPv6 packets to the tun (check buf[0]>>4 is 4 or 6)
- Verify you are not passing Ethernet-encapsulated frames (strip the L2 header first)
- Check buffer offsets/slicing so the IP packet starts at index 0
Example fix
// before
tun.Write(frame) // frame is an Ethernet frame
// after
if len(frame) > 0 && (frame[0]>>4 == 4 || frame[0]>>4 == 6) {
tun.Write(frame[14:]) // strip Ethernet header, write IP packet
} Defensive patterns
Strategy: validation
Validate before calling
func isIPPacket(b []byte) bool {
if len(b) < 1 { return false }
v := b[0] >> 4
return v == 4 || v == 6
}
if !isIPPacket(pkt) { skip or log } Type guard
func isIPPacket(b []byte) bool {
return len(b) >= 1 && (b[0]>>4 == 4 || b[0]>>4 == 6)
} Prevention
- Never write Ethernet/L2 frames to a tun device
- Validate the IP version nibble before every Write
- Guard against zero-length buffers upstream
When it happens
Trigger: Write() is called with a buffer whose first nibble is not 4 or 6 — e.g. an empty buffer, a truncated/malformed packet, a non-IP frame (ARP, Ethernet header), or data starting at the wrong offset.
Common situations: Writing raw Ethernet frames instead of IP packets; slicing bugs that pass header-less data; corruption or misaligned reads elsewhere in the pipeline.
Related errors
- unable to determine IP version from packet
- newTunFromFd not supported in NetBSD
- a device name in the format of /dev/tunN must be specified
- error closing tun file: %w
- failed to get syscall conn for tun: %w
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/18f1b408a361508c.
Report an issue: GitHub.