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