4074e68715
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>
111 lines
3.8 KiB
Go
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)
|
|
}
|