Files
drover-go/internal/divert/filter.go
T
root 4074e68715 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>
2026-05-01 22:27:54 +03:00

111 lines
3.8 KiB
Go

package divert
import (
"fmt"
"net"
"strings"
)
// FilterParams collects the inputs needed to build a WinDivert filter
// expression for Drover's outbound capture.
type FilterParams struct {
// TargetPIDs is the set of PIDs whose outbound traffic should be
// captured (e.g. Discord variants). When empty, the resulting
// filter is "false" — captures nothing — which is the right
// behaviour while procscan reports zero Discord processes.
TargetPIDs []uint32
// OwnPID is drover.exe's own PID. Excluded from capture so our
// SOCKS5 traffic to the upstream proxy doesn't get re-captured.
OwnPID uint32
// UpstreamIP is the resolved IPv4 of the upstream SOCKS5 proxy.
// Excluded from capture as a second line of defence against
// self-loops. If unparseable, "0.0.0.0" is substituted (caller
// should validate before calling).
UpstreamIP string
// LocalIP is the machine's LAN IP — listener binds here, so
// reinjected NAT'd packets (which still bear the original src)
// reach it. Must be excluded from the filter to prevent infinite
// recapture of NAT'd packets (we'd see them outbound again).
LocalIP string
}
// BuildFlowFilter returns a filter expression for the FLOW layer handle.
// processId is ONLY available at FLOW/SOCKET layers, not NETWORK — that's
// why we run two handles in parallel: this FLOW handle observes which
// 5-tuples belong to target PIDs, and the NETWORK handle (BuildNetworkFilter)
// captures actual packets.
//
// Empty PID list → "false" (matches no flows).
func BuildFlowFilter(p FilterParams) string {
if len(p.TargetPIDs) == 0 {
return "false"
}
pidClauses := make([]string, len(p.TargetPIDs))
for i, pid := range p.TargetPIDs {
pidClauses[i] = fmt.Sprintf("processId == %d", pid)
}
pidClause := "(" + strings.Join(pidClauses, " or ") + ")"
parts := []string{
"(tcp or udp)",
"ip",
pidClause,
fmt.Sprintf("processId != %d", p.OwnPID),
}
return strings.Join(parts, " and ")
}
// BuildNetworkFilter returns a filter expression for the NETWORK layer
// handle. It captures all outbound IPv4 TCP/UDP except loopback,
// multicast, link-local, and the upstream proxy. The engine then
// narrows by consulting the flow tracker fed by the FLOW handle.
//
// We don't (can't) filter by processId here — see BuildFlowFilter.
// Self-loop protection: ip.DstAddr != upstream blocks our own SOCKS5
// uplink, and 127.0.0.0/8 exclusion blocks our loopback redirector.
//
// Range exclusions are spelled with explicit `<`/`>` rather than
// `not (a and b)` because some WinDivert versions reject the latter
// at filter compile time.
func BuildNetworkFilter(p FilterParams) string {
upstream := p.UpstreamIP
if net.ParseIP(upstream).To4() == nil {
upstream = "0.0.0.0"
}
parts := []string{
"outbound",
"ip",
"(tcp or udp)",
fmt.Sprintf("ip.DstAddr != %s", upstream),
// Loopback 127.0.0.0/8
"(ip.DstAddr < 127.0.0.0 or ip.DstAddr > 127.255.255.255)",
// Multicast 224.0.0.0/4
"(ip.DstAddr < 224.0.0.0 or ip.DstAddr > 239.255.255.255)",
// Link-local 169.254.0.0/16
"(ip.DstAddr < 169.254.0.0 or ip.DstAddr > 169.254.255.255)",
}
// Exclude packets DESTINED to our own LAN IP — they're either
// intra-machine traffic we don't care about OR our own NAT'd
// reinjects coming back around. Without this we infinite-loop.
if p.LocalIP != "" && net.ParseIP(p.LocalIP).To4() != nil {
parts = append(parts, fmt.Sprintf("ip.DstAddr != %s", p.LocalIP))
}
return strings.Join(parts, " and ")
}
// BuildFilter is the legacy single-filter API. Kept for callers that
// don't yet use the dual-handle architecture; equivalent to
// BuildNetworkFilter (no processId — that clause is invalid at NETWORK
// layer).
//
// Deprecated: use BuildFlowFilter + BuildNetworkFilter together.
func BuildFilter(p FilterParams) string {
if len(p.TargetPIDs) == 0 {
return "false"
}
return BuildNetworkFilter(p)
}