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

  1. Check from[0]>>4 is 4 or 6 (and len(from) > 0) before calling Write and drop invalid packets.
  2. Audit callers of Write to ensure they pass the start of an IP datagram, not a link-layer or offset frame.
  3. Add validation on the read path so only well-formed IP packets are ever queued for writing.
  4. Log the first bytes of offending packets to identify the source of non-IP frames.
  5. 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

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


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