{"record":{"id":"e404f6aab90c0fbb","repo":"slackhq/nebula","slug":"unable-to-determine-ip-version-from-packet-e404f6","errorCode":null,"errorMessage":"unable to determine IP version from packet","messagePattern":"unable to determine IP version from packet","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"overlay/tun_openbsd.go","lineNumber":190,"sourceCode":"\t}\n\treturn n - 4, nil\n}\n\n// Write pushes one IP packet onto the tun device.\nfunc (t *tun) Write(from []byte) (int, error) {\n\tif len(from) == 0 {\n\t\treturn 0, syscall.EIO\n\t}\n\n\tipVer := from[0] >> 4\n\tvar head [4]byte\n\tswitch ipVer {\n\tcase 4:\n\t\thead[3] = syscall.AF_INET\n\tcase 6:\n\t\thead[3] = syscall.AF_INET6\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"unable to determine IP version from packet\")\n\t}\n\n\t// Grab rc as a local so the compiler can devirtualize the call and keep the closure on the stack.\n\trc, err := t.f.SyscallConn()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar n int\n\tvar callErr error\n\terr = rc.Write(func(fd uintptr) bool {\n\t\tiovecs := []unix.Iovec{\n\t\t\t{Base: &head[0], Len: 4},\n\t\t\t{Base: &from[0], Len: uint64(len(from))},\n\t\t}\n\t\tn, callErr = tunWritev(int(fd), iovecs)\n\t\t// Type-assert to syscall.Errno so the EAGAIN/EWOULDBLOCK/EINTR check doesn't box the errno\n\t\t// constants into error interfaces on every call.","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/overlay/tun_openbsd.go#L172-L208","documentation":"Write() inspects the first byte of the packet to determine IPv4 vs IPv6 so it can prepend the correct OpenBSD tun address family header (AF_INET/AF_INET6 in the 4-byte head). If the IP version byte is neither 4 nor 6, the packet is not a valid IP packet and the write is rejected. This guards against writing garbage or truncated frames to the tun device.","triggerScenarios":"Calling t.Write() with a buffer whose first byte is not 0x4x or 0x6x — e.g. an empty/truncated packet, non-IP frames, or a misaligned read offset into a receive buffer.","commonSituations":"Custom handoff code writing raw non-IP payloads into the tun; packet parsing bugs that pass wrong offsets; corrupted buffers from upstream decryption failures.","solutions":["Verify the packet slice starts at the IP header (offset 0)","Check minimum packet length (>=1 byte) and version nibble before writing","Log/inspect the first byte to find who produced the malformed packet","Fix upstream framing so only complete IP packets reach tun.Write"],"exampleFix":"// before\nb := buf[off:] // off misaligned, first byte not IP version\nt.Write(b)\n// after\nif len(buf) > 0 && (buf[0]>>4 == 4 || buf[0]>>4 == 6) {\n    t.Write(buf)\n}","handlingStrategy":"validation","validationCode":"func isIPPacket(b []byte) bool {\n    return len(b) >= 20 && (b[0]>>4 == 4 || b[0]>>4 == 6)\n}\nif !isIPPacket(pkt) {\n    return fmt.Errorf(\"refusing to write non-IP packet, first byte=0x%02x\", pkt[0])\n}","typeGuard":null,"tryCatchPattern":"n, err := t.Write(pkt)\nif err != nil && strings.Contains(err.Error(), \"unable to determine IP version\") {\n    log.Warn(\"dropping malformed packet\", \"firstByte\", pkt[0])\n    return // drop, don't crash\n}","preventionTips":["Slice packets at offset 0 of the IP header","Sanity-check the version nibble before handing to tun.Write","Fix upstream framing/decryption that emits non-IP bytes"],"tags":["openbsd","tun","packet","ip-version"],"backgroundTag":"invalid-ip-packet","analyzedSha":"dd8f660c0ac37903ec4080ca4d3c861ba9342ceb","analyzedAt":"2026-09-03T11:13:55.444Z","contentChangedAt":"2026-09-03T11:13:55.444Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}