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) }