slackhq/nebula · warning
unable to determine IP version from packet
Error message
unable to determine IP version from packet
What it means
The iOS tun transport's Write inspects the first nibble of the outgoing packet to select the address family (AF_INET/AF_INET6) stamped into the 4-byte write header. If the first byte's high nibble is neither 4 nor 6, the packet is not recognizable IP and Write returns errors.New("unable to determine IP version from packet") with 0 bytes written.
Source
Thrown at overlay/tun_ios.go:142
return 0, syscall.EIO
}
tr.wMu.Lock()
defer tr.wMu.Unlock()
if cap(tr.wBuf) < len(from)+4 {
tr.wBuf = make([]byte, len(from)+4)
}
tr.wBuf = tr.wBuf[:len(from)+4]
// Determine the IP Family for the NULL L2 Header
ipVer := from[0] >> 4
if ipVer == 4 {
tr.wBuf[3] = syscall.AF_INET
} else if ipVer == 6 {
tr.wBuf[3] = syscall.AF_INET6
} else {
return 0, errors.New("unable to determine IP version from packet")
}
copy(tr.wBuf[4:], from)
n, err := tr.f.Write(tr.wBuf)
return n - 4, err
}
func (tr *tunReadCloser) Close() error {
return tr.f.Close()
}
func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks
}
func (t *tun) Name() string {
return "iOS"View on GitHub (pinned to dd8f660c0a)
Solutions
- Check from[0]>>4 is 4 or 6 (and len(from) > 0) before calling Write and drop invalid packets.
- Audit callers of Write to ensure they pass the start of an IP datagram, not a link-layer or offset frame.
- Add validation on the read path so only well-formed IP packets are ever queued for writing.
- Log the first bytes of offending packets to identify the source of non-IP frames.
- Return io.ErrBadRequest-style sentinel so callers can distinguish this from write I/O errors.
Example fix
// before: unconditional write
n, err := tw.Write(buf)
// after: pre-validate IP version
if len(buf) == 0 || (buf[0]>>4 != 4 && buf[0]>>4 != 6) {
return // drop non-IP frame
}
n, err := tw.Write(buf) Defensive patterns
Strategy: validation
Validate before calling
func isIPPacket(b []byte) bool {
if len(b) == 0 {
return false
}
switch b[0] >> 4 {
case 4, 6:
return true
}
return false
}
if !isIPPacket(buf) {
return // drop; do not call tun Write
} Try / catch
n, err := tw.Write(pkt)
if err != nil && strings.Contains(err.Error(), "unable to determine IP version") {
log.Debug("dropping non-IP frame on ios tun write", "len", len(pkt))
return nil
} else if err != nil {
return err
} Prevention
- Only queue buffers that begin with a valid IP version nibble toward Write.
- Never echo zero-length or malformed read buffers back through Write.
- Keep the write path IP-datagram-only; strip or handle any link-layer framing upstream.
- Log the first bytes of rejected frames to spot sources of non-IP traffic.
When it happens
Trigger: tun_ios Write (overlay/tun_ios.go:142) receives a from []byte whose first byte's top nibble is not 4 or 6 — e.g. empty/garbage buffer, non-IP frame, or a caller passing the wrong buffer to Write.
Common situations: Writing non-IP link-layer frames (ARP-like or vendor frames) to the tun; a preceding read returned a zero-length or corrupted buffer that was echoed back; slicing errors handing Write a wrong-offset buffer; iOS network extension receiving unexpected datagrams from NWPath/pcap-style sources.
Related errors
- ErrIPv6CouldNotFindPayload
- ErrIPv4InvalidHeaderLength
- ErrIPv4PacketTooShort
- ErrIPv6PacketTooShort
- invalid mask: %s for cidr: %s
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/54517485e37a9721.
Report an issue: GitHub.