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:
+65
-16
@@ -24,22 +24,25 @@ type FilterParams struct {
|
||||
// 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
|
||||
}
|
||||
|
||||
// BuildFilter returns a WinDivert filter expression string suitable
|
||||
// for WinDivertOpen. The expression captures only outbound IPv4 TCP/UDP
|
||||
// from the listed PIDs, excluding our own process and the upstream
|
||||
// proxy's IP.
|
||||
func BuildFilter(p FilterParams) 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"
|
||||
}
|
||||
|
||||
upstream := p.UpstreamIP
|
||||
if net.ParseIP(upstream).To4() == nil {
|
||||
upstream = "0.0.0.0"
|
||||
}
|
||||
|
||||
pidClauses := make([]string, len(p.TargetPIDs))
|
||||
for i, pid := range p.TargetPIDs {
|
||||
pidClauses[i] = fmt.Sprintf("processId == %d", pid)
|
||||
@@ -47,15 +50,61 @@ func BuildFilter(p FilterParams) string {
|
||||
pidClause := "(" + strings.Join(pidClauses, " or ") + ")"
|
||||
|
||||
parts := []string{
|
||||
"outbound",
|
||||
"(tcp or udp)",
|
||||
"ip",
|
||||
pidClause,
|
||||
fmt.Sprintf("processId != %d", p.OwnPID),
|
||||
fmt.Sprintf("ip.DstAddr != %s", upstream),
|
||||
"not (ip.DstAddr >= 224.0.0.0 and ip.DstAddr <= 239.255.255.255)",
|
||||
"not (ip.DstAddr >= 127.0.0.0 and ip.DstAddr <= 127.255.255.255)",
|
||||
"not (ip.DstAddr >= 169.254.0.0 and ip.DstAddr <= 169.254.255.255)",
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user