experimental/windivert: P2.1+P2.2 with WinDivert NETWORK+SOCKET layers

WIP snapshot before pivot to sing-box+TUN. Reached:
- TCP redirect via streamdump pattern (swap+Outbound=0+reinject)
- SOCKET layer for SYN-stage flow detection (avoids FLOW Establish-too-late race)
- Lazy PID→name resolution (catches Update.exe inside procscan tick)
- UDP forward via SOCKS5 UDP ASSOCIATE relay + manual reinject
- Result: chat works, voice times out (Discord IP discovery / RTC handshake fails)

Reason for pivot: WinDivert NAT-reinject pattern has subtle layer-3
semantics issues that DLL-injection / TUN-based proxies sidestep
entirely. Going with embedded sing-box + wintun as the engine —
proven path for Discord voice through SOCKS5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 22:27:54 +03:00
parent 8ceb7775d7
commit 4074e68715
19 changed files with 2666 additions and 62 deletions
+293
View File
@@ -5,10 +5,83 @@ package divert
import (
"errors"
"fmt"
"unsafe"
idivert "github.com/imgk/divert-go"
)
// idivertAddrLayout mirrors the imgk/divert-go private Address fields
// so we can read the raw 64-byte union without going through their
// (mis-aligned for FLOW events) accessor.
type idivertAddrLayout struct {
Timestamp int64
Layer uint8
Event uint8
Flags uint8
_ uint8
Length uint32
Union [64]byte
}
// parseFlowUnion decodes a WINDIVERT_DATA_FLOW from raw union bytes.
// Layout per WinDivert v2 (MSVC default 8-byte alignment):
//
// offset 0..7 EndpointId UINT64
// offset 8..15 ParentEndpointId UINT64
// offset 16..19 ProcessId UINT32
// offset 20..23 (padding to 4) — not 8 because LocalAddr has 4-byte alignment
// offset 24..39 LocalAddr[4] UINT32 — NO, wait.
//
// Actually WinDivert struct uses UINT32 (4-byte aligned), no padding
// between ProcessId and LocalAddr. But we observed ProcessID and
// Ports parse correctly via imgk's struct (which assumes offset 20
// for LocalAddr). So that layout is right; the IPs zero-out must be
// because *imgk's struct member [16]uint8 doesn't read what we think*.
//
// Mystery: imgk's Flow struct should give correct addresses. Yet we
// see [0,0,0,0]. Re-inspect raw bytes.
func parseFlowUnion(b []byte) *FlowEvent {
if len(b) < 64 {
return &FlowEvent{}
}
ev := &FlowEvent{
ProcessID: leU32(b[16:20]),
LocalRaw: toAddr16(b[20:36]),
RemoteRaw: toAddr16(b[36:52]),
LocalPort: leU16(b[52:54]),
RemotePort: leU16(b[54:56]),
Protocol: b[56],
}
// WinDivert v2.2.2 stores IPv4 as little-endian uint32 in the
// first 4 bytes of the 16-byte address slot (bytes 4..7 hold the
// 0xFFFF mapped-IPv6 prefix; bytes 8..15 are zero). To get the
// dot-notation IP A.B.C.D, reverse the byte order:
// byte[0] = D (LSB), byte[1] = C, byte[2] = B, byte[3] = A (MSB).
ev.SrcAddr[0] = ev.LocalRaw[3]
ev.SrcAddr[1] = ev.LocalRaw[2]
ev.SrcAddr[2] = ev.LocalRaw[1]
ev.SrcAddr[3] = ev.LocalRaw[0]
ev.DstAddr[0] = ev.RemoteRaw[3]
ev.DstAddr[1] = ev.RemoteRaw[2]
ev.DstAddr[2] = ev.RemoteRaw[1]
ev.DstAddr[3] = ev.RemoteRaw[0]
ev.SrcPort = ev.LocalPort
ev.DstPort = ev.RemotePort
return ev
}
func leU32(b []byte) uint32 {
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
func leU16(b []byte) uint16 {
return uint16(b[0]) | uint16(b[1])<<8
}
func toAddr16(b []byte) [16]byte {
var a [16]byte
copy(a[:], b)
return a
}
// Handle wraps a WinDivert handle.
type Handle struct {
h *idivert.Handle
@@ -29,6 +102,187 @@ func Open(filter string) (*Handle, error) {
return &Handle{h: h}, nil
}
// OpenFlow opens a WinDivert handle at FLOW layer. FLOW handles
// observe TCP/UDP flow establish + delete events with processId info
// available — that's where we learn which 5-tuples belong to target
// processes (processId field is invalid on the NETWORK layer filter
// language). FLOW handles cannot Send packets — they're read-only by
// design.
//
// Per WinDivert reference, FLOW handles MUST be opened with both
// SNIFF (events only, no interception) and RECV_ONLY (no Send) flags,
// otherwise WinDivertOpen rejects the request.
func OpenFlow(filter string) (*Handle, error) {
h, err := idivert.Open(filter, idivert.LayerFlow, 0, idivert.FlagSniff|idivert.FlagRecvOnly)
if err != nil {
return nil, mapWinDivertErr(err)
}
return &Handle{h: h}, nil
}
// OpenSocket opens a WinDivert handle at SOCKET layer. SOCKET layer
// fires events synchronously with socket syscalls (bind/connect/
// listen/accept/close) — Connect specifically fires BEFORE the SYN
// packet leaves the box, which gives us a window to populate our
// redirect tables before the NETWORK-layer SYN arrives.
//
// Same flag rules as FLOW: must be SNIFF + RECV_ONLY.
func OpenSocket(filter string) (*Handle, error) {
h, err := idivert.Open(filter, idivert.LayerSocket, 0, idivert.FlagSniff|idivert.FlagRecvOnly)
if err != nil {
return nil, mapWinDivertErr(err)
}
return &Handle{h: h}, nil
}
// SocketEvent represents a socket-layer event (Connect/Close/etc).
type SocketEvent struct {
ProcessID uint32
Protocol uint8 // 6=TCP, 17=UDP
SrcAddr [4]byte
SrcPort uint16
DstAddr [4]byte
DstPort uint16
Kind SocketEventKind
LocalRaw [16]byte // raw 16-byte slot for diagnostic
RemoteRaw [16]byte
}
// SocketEventKind enumerates the socket-layer events we care about.
type SocketEventKind int
const (
SocketKindUnknown SocketEventKind = iota
SocketKindBind
SocketKindConnect
SocketKindListen
SocketKindAccept
SocketKindClose
)
// RecvSocket blocks until a socket event arrives on a SOCKET-layer
// handle. The packet payload is empty on SOCKET events; only the
// address metadata matters.
func (h *Handle) RecvSocket() (*SocketEvent, error) {
if h == nil || h.h == nil {
return nil, errors.New("handle closed")
}
buf := [4]byte{}
addr := new(idivert.Address)
_, err := h.h.Recv(buf[:], addr)
if err != nil {
return nil, mapWinDivertErr(err)
}
// SOCKET layer uses the same WINDIVERT_DATA_SOCKET layout as FLOW
// (verbatim per the WinDivert v2.2.2 header). We bypass the
// imgk/divert-go accessor for the same alignment-safety reason as
// RecvFlow and parse raw union bytes directly.
raw := (*idivertAddrLayout)(unsafe.Pointer(addr))
ev := parseSocketUnion(raw.Union[:])
switch addr.Event() {
case idivert.EventSocketBind:
ev.Kind = SocketKindBind
case idivert.EventSocketConnect:
ev.Kind = SocketKindConnect
case idivert.EventSocketListen:
ev.Kind = SocketKindListen
case idivert.EventSocketAccept:
ev.Kind = SocketKindAccept
case idivert.EventSocketClose:
ev.Kind = SocketKindClose
default:
return nil, fmt.Errorf("unexpected socket event %d", addr.Event())
}
return ev, nil
}
// parseSocketUnion mirrors parseFlowUnion: WINDIVERT_DATA_SOCKET is
// byte-identical to WINDIVERT_DATA_FLOW per windivert.h v2.2.2.
func parseSocketUnion(b []byte) *SocketEvent {
if len(b) < 64 {
return &SocketEvent{}
}
ev := &SocketEvent{
ProcessID: leU32(b[16:20]),
LocalRaw: toAddr16(b[20:36]),
RemoteRaw: toAddr16(b[36:52]),
SrcPort: leU16(b[52:54]),
DstPort: leU16(b[54:56]),
Protocol: b[56],
}
// Same byte-reverse trick as parseFlowUnion: WinDivert stores the
// IPv4 in the first 4 bytes of the slot as a host-byte-order
// uint32; reverse to get A.B.C.D in SrcAddr[0..3].
ev.SrcAddr[0] = ev.LocalRaw[3]
ev.SrcAddr[1] = ev.LocalRaw[2]
ev.SrcAddr[2] = ev.LocalRaw[1]
ev.SrcAddr[3] = ev.LocalRaw[0]
ev.DstAddr[0] = ev.RemoteRaw[3]
ev.DstAddr[1] = ev.RemoteRaw[2]
ev.DstAddr[2] = ev.RemoteRaw[1]
ev.DstAddr[3] = ev.RemoteRaw[0]
return ev
}
// FlowEvent represents a flow-establish/delete event from a FLOW
// handle. SrcAddr/DstAddr are the IPv4 addresses (4 bytes, network
// byte order: A.B.C.D = SrcAddr[0..3]). LocalRaw/RemoteRaw are the
// raw 16-byte slots from WinDivert for diagnostic dumps.
//
// Established=true on EventFlowEstablished; false on EventFlowDeleted.
type FlowEvent struct {
ProcessID uint32
Protocol uint8 // 6=TCP, 17=UDP
SrcAddr [4]byte
SrcPort uint16
DstAddr [4]byte
DstPort uint16
Established bool
// Diagnostic fields populated by parseFlowUnion. Used by
// debug-flow logging; production code should consume the
// SrcAddr/DstAddr/SrcPort/DstPort fields above.
LocalRaw [16]byte
RemoteRaw [16]byte
LocalPort uint16
RemotePort uint16
}
// RecvFlow blocks until a flow event arrives on a FLOW-layer handle.
// The packet payload is empty on FLOW events; only the address
// metadata matters.
//
// Returns the event or an error from the wrapped handle (Shutdown
// during close, etc).
func (h *Handle) RecvFlow() (*FlowEvent, error) {
if h == nil || h.h == nil {
return nil, errors.New("handle closed")
}
// Per WinDivert docs flow event has zero-byte packet; we still
// need a non-nil buffer for the API.
buf := [4]byte{}
addr := new(idivert.Address)
_, err := h.h.Recv(buf[:], addr)
if err != nil {
return nil, mapWinDivertErr(err)
}
// imgk/divert-go's Flow accessor mis-aligns the union for FLOW
// events (it assumes 4-byte alignment after ProcessID, but MSVC
// pads to 8-byte boundary because the struct contains UINT64).
// We bypass the accessor and parse the raw union bytes ourselves.
raw := (*idivertAddrLayout)(unsafe.Pointer(addr))
ev := parseFlowUnion(raw.Union[:])
switch addr.Event() {
case idivert.EventFlowEstablished:
ev.Established = true
case idivert.EventFlowDeleted:
ev.Established = false
default:
return nil, fmt.Errorf("unexpected flow event %d", addr.Event())
}
return ev, nil
}
// Close closes the handle. Safe to call multiple times.
func (h *Handle) Close() error {
if h == nil || h.h == nil {
@@ -72,6 +326,45 @@ func (h *Handle) Send(buf []byte, addr *idivert.Address) (int, error) {
return int(n), nil
}
// SendInjectInbound reinjects a fabricated IPv4 packet as inbound (i.e.
// kernel delivers it via the receive path of whatever interface owns
// the destination IP). Used by the UDPProxy to deliver SOCKS5 relay
// responses back to a target process: we synthesize an IPv4+UDP packet
// with src=remote_endpoint, dst=local_LAN_IP, then call this with
// outbound=false and IP+UDP-checksum-valid flags set.
//
// Internally builds a fresh *idivert.Address with NETWORK layer + the
// requested flags + zero interface index (WinDivert routes via default).
//
// Flags semantics (per WinDivert v2.2.2 windivert.h):
//
// bit 1 (0x02) = Outbound — set if outbound, clear for inbound
// bit 5 (0x20) = IPChecksum — packet has valid IPv4 header checksum
// bit 6 (0x40) = TCPChecksum — packet has valid TCP checksum
// bit 7 (0x80) = UDPChecksum — packet has valid UDP checksum
func (h *Handle) SendInjectInbound(buf []byte, isUDP bool) (int, error) {
if h == nil || h.h == nil {
return 0, errors.New("handle closed")
}
addr := new(idivert.Address)
addr.SetLayer(idivert.LayerNetwork)
addr.SetEvent(idivert.EventNetworkPacket)
// Outbound bit (0x02) cleared (inbound). Sniffed (0x01) cleared.
// IPChecksum (0x20) set. UDP (0x80) or TCP (0x40) set per call.
var flags uint8 = 0x20
if isUDP {
flags |= 0x80
} else {
flags |= 0x40
}
addr.Flags = flags
n, err := h.h.Send(buf, addr)
if err != nil {
return 0, mapWinDivertErr(err)
}
return int(n), nil
}
// Sentinel errors mapped from raw Windows errors so the engine layer
// can pattern-match without importing windows package.
var (