internal/divert: filter expression builder
Pure-Go assembly of the WinDivert filter clause. Empty PID list → "false" (captures nothing — used during Discord-not-running window). Non-IPv4 upstream → 0.0.0.0 fallback (caller should validate; the builder degrades gracefully rather than panicking). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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)
|
||||
}
|
||||
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 ")
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package divert
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildFilter_HappyPath(t *testing.T) {
|
||||
got := BuildFilter(FilterParams{
|
||||
TargetPIDs: []uint32{12345, 67890},
|
||||
OwnPID: 999,
|
||||
UpstreamIP: "95.165.72.59",
|
||||
})
|
||||
// Required clauses
|
||||
assert.Contains(t, got, "outbound")
|
||||
assert.Contains(t, got, "(tcp or udp)")
|
||||
assert.Contains(t, got, "ip")
|
||||
assert.Contains(t, got, "processId == 12345")
|
||||
assert.Contains(t, got, "processId == 67890")
|
||||
assert.Contains(t, got, "processId != 999")
|
||||
assert.Contains(t, got, "ip.DstAddr != 95.165.72.59")
|
||||
// Loopback / multicast / link-local exclusions
|
||||
assert.Contains(t, got, "127.0.0.0")
|
||||
assert.Contains(t, got, "224.0.0.0")
|
||||
assert.Contains(t, got, "169.254.0.0")
|
||||
}
|
||||
|
||||
func TestBuildFilter_SinglePID(t *testing.T) {
|
||||
got := BuildFilter(FilterParams{
|
||||
TargetPIDs: []uint32{42},
|
||||
OwnPID: 1,
|
||||
UpstreamIP: "1.2.3.4",
|
||||
})
|
||||
assert.Contains(t, got, "processId == 42")
|
||||
}
|
||||
|
||||
func TestBuildFilter_NoTargetPIDs(t *testing.T) {
|
||||
// No Discord running. We still produce a syntactically valid filter
|
||||
// that matches nothing (we can't pass an empty filter to WinDivert).
|
||||
got := BuildFilter(FilterParams{
|
||||
TargetPIDs: nil,
|
||||
OwnPID: 999,
|
||||
UpstreamIP: "1.2.3.4",
|
||||
})
|
||||
// "false" alone is a valid filter that captures nothing — perfect
|
||||
// for "Discord not running" interim.
|
||||
assert.Equal(t, "false", got)
|
||||
}
|
||||
|
||||
func TestBuildFilter_OwnPIDNotInTargets(t *testing.T) {
|
||||
// Defensive: even if OwnPID accidentally appears in TargetPIDs, the
|
||||
// processId != ownPid clause still excludes it.
|
||||
got := BuildFilter(FilterParams{
|
||||
TargetPIDs: []uint32{999, 1234},
|
||||
OwnPID: 999,
|
||||
UpstreamIP: "1.2.3.4",
|
||||
})
|
||||
assert.Contains(t, got, "processId != 999")
|
||||
// The exclusion takes precedence syntactically because of the AND.
|
||||
assert.True(t, strings.Contains(got, "and processId != 999"))
|
||||
}
|
||||
|
||||
func TestBuildFilter_UpstreamIPv4Format(t *testing.T) {
|
||||
got := BuildFilter(FilterParams{
|
||||
TargetPIDs: []uint32{1},
|
||||
OwnPID: 2,
|
||||
UpstreamIP: "not-an-ip",
|
||||
})
|
||||
// We just substitute a placeholder and document it.
|
||||
assert.Contains(t, got, "ip.DstAddr != 0.0.0.0")
|
||||
}
|
||||
Reference in New Issue
Block a user