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:
+423
-45
@@ -6,8 +6,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -36,12 +38,47 @@ type Engine struct {
|
||||
|
||||
// runtime state
|
||||
upstreamIP net.IP
|
||||
handle *divert.Handle
|
||||
localIP net.IP // primary outbound LAN IP — listener binds here so reinjected NAT'd packets reach it (kernel drops src=LAN/dst=127.0.0.1 as spoofed)
|
||||
handleMu sync.RWMutex // guards handle + flowH swap during procscan rebuild
|
||||
handle *divert.Handle // NETWORK layer: capture/rewrite/reinject packets
|
||||
flowH *divert.Handle // FLOW layer: capture-ALL events (filter "true"); we filter by PID in-process
|
||||
redir *redirect.Redirector
|
||||
udp *redirect.UDPProxy // SOCKS5 UDP relay manager — handles Discord voice etc.
|
||||
ctx context.Context
|
||||
cnl context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
ownPID uint32
|
||||
|
||||
// pidMu guards targetPIDs. Updated by procscanLoop, read by flowLoop
|
||||
// for every event. Read frequency: ~50 events/sec average; write:
|
||||
// every 2s. RWMutex contention negligible.
|
||||
pidMu sync.RWMutex
|
||||
targetPIDs map[uint32]struct{}
|
||||
|
||||
// flowSet tracks 5-tuples currently belonging to target processes.
|
||||
// Populated by flowLoop on EventFlowEstablished, removed on
|
||||
// EventFlowDeleted. Read by diverterLoop on every captured packet
|
||||
// to decide whether to redirect or pass through.
|
||||
flowMu sync.RWMutex
|
||||
flowSet map[flowKey]struct{}
|
||||
}
|
||||
|
||||
// flowKey identifies a flow by its 5-tuple. Drover uses local→remote
|
||||
// (i.e. always the outbound direction) — the flow handle reports
|
||||
// LocalAddr/Port and RemoteAddr/Port which match an outbound packet's
|
||||
// SrcAddr/Port and DstAddr/Port.
|
||||
// flowKey identifies a tracked flow. We deliberately omit SrcIP from
|
||||
// the key: when Discord (or any client) binds a UDP/TCP socket to
|
||||
// INADDR_ANY (0.0.0.0), the SOCKET layer reports src=0.0.0.0, but the
|
||||
// actual outbound packet has src=<local_LAN_IP> (kernel fills the
|
||||
// interface address). Including src in the key would cause those
|
||||
// flows to miss the lookup. Source port + destination + proto is a
|
||||
// sufficient discriminator on a single host.
|
||||
type flowKey struct {
|
||||
dst [4]byte
|
||||
sport uint16
|
||||
dport uint16
|
||||
proto uint8 // 6=TCP, 17=UDP
|
||||
}
|
||||
|
||||
// New constructs an engine. No I/O yet.
|
||||
@@ -50,9 +87,11 @@ func New(cfg Config) (*Engine, error) {
|
||||
return nil, errors.New("ProxyAddr is required")
|
||||
}
|
||||
return &Engine{
|
||||
cfg: cfg,
|
||||
status: StatusIdle,
|
||||
ownPID: uint32(os.Getpid()),
|
||||
cfg: cfg,
|
||||
status: StatusIdle,
|
||||
ownPID: uint32(os.Getpid()),
|
||||
flowSet: map[flowKey]struct{}{},
|
||||
targetPIDs: map[uint32]struct{}{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -107,15 +146,19 @@ func (e *Engine) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (e *Engine) bringUp(ctx context.Context) error {
|
||||
log.Printf("engine: bringUp start cfg.ProxyAddr=%q targets=%v", e.cfg.ProxyAddr, e.cfg.Targets)
|
||||
|
||||
// 1. Resolve upstream
|
||||
host, _, err := net.SplitHostPort(e.cfg.ProxyAddr)
|
||||
if err != nil {
|
||||
log.Printf("engine: SplitHostPort failed: %v", err)
|
||||
return fmt.Errorf("invalid ProxyAddr: %w", err)
|
||||
}
|
||||
rctx, rcancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer rcancel()
|
||||
ips, err := net.DefaultResolver.LookupIPAddr(rctx, host)
|
||||
if err != nil || len(ips) == 0 {
|
||||
log.Printf("engine: LookupIPAddr(%q) failed: %v (ips=%v)", host, err, ips)
|
||||
return fmt.Errorf("resolve proxy host %q: %w", host, err)
|
||||
}
|
||||
var upstream net.IP
|
||||
@@ -126,26 +169,50 @@ func (e *Engine) bringUp(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
if upstream == nil {
|
||||
log.Printf("engine: no IPv4 for %q (got %v)", host, ips)
|
||||
return fmt.Errorf("no IPv4 for %q", host)
|
||||
}
|
||||
e.upstreamIP = upstream
|
||||
log.Printf("engine: upstream resolved %s → %s", host, upstream)
|
||||
|
||||
// 1b. Detect outbound LAN IP — listener binds here. Trick:
|
||||
// open a UDP "connect" to any external IP; kernel picks the
|
||||
// outbound interface and we read LocalAddr off the conn.
|
||||
if udpConn, dErr := net.Dial("udp", "8.8.8.8:53"); dErr == nil {
|
||||
e.localIP = udpConn.LocalAddr().(*net.UDPAddr).IP.To4()
|
||||
udpConn.Close()
|
||||
}
|
||||
if e.localIP == nil {
|
||||
log.Printf("engine: could not detect local LAN IP — falling back to 127.0.0.1 (may not work)")
|
||||
e.localIP = net.IPv4(127, 0, 0, 1)
|
||||
}
|
||||
log.Printf("engine: local LAN IP = %s", e.localIP)
|
||||
|
||||
// 2. Driver install (idempotent)
|
||||
if _, err := divert.InstallDriver(); err != nil {
|
||||
paths, err := divert.InstallDriver()
|
||||
if err != nil {
|
||||
log.Printf("engine: InstallDriver failed: %v", err)
|
||||
return fmt.Errorf("install driver: %w", err)
|
||||
}
|
||||
log.Printf("engine: driver installed sys=%s dll=%s", paths.SysPath, paths.DllPath)
|
||||
|
||||
// 3. Initial procscan
|
||||
pids, err := procscan.Snapshot(e.cfg.Targets)
|
||||
if err != nil {
|
||||
log.Printf("engine: procscan.Snapshot failed: %v", err)
|
||||
return fmt.Errorf("procscan: %w", err)
|
||||
}
|
||||
pidList := make([]uint32, 0, len(pids))
|
||||
for p := range pids {
|
||||
pidList = append(pidList, p)
|
||||
}
|
||||
log.Printf("engine: initial procscan found %d target pids: %v", len(pidList), pids)
|
||||
|
||||
// 4. Open redirector listener
|
||||
// 4. Open redirector listener on 0.0.0.0 so it accepts on any
|
||||
// interface (including the LAN IP we'll target with the swap-and-
|
||||
// reinject NAT pattern). After the streamdump swap the packet has
|
||||
// dst=LAN_IP:listener_port — kernel delivers via inbound path of
|
||||
// the LAN interface; listener accepts it as a regular TCP conn.
|
||||
r, err := redirect.New(redirect.Config{
|
||||
SOCKS5: socks5.Config{
|
||||
ProxyAddr: e.cfg.ProxyAddr,
|
||||
@@ -153,34 +220,111 @@ func (e *Engine) bringUp(ctx context.Context) error {
|
||||
Login: e.cfg.Login,
|
||||
Password: e.cfg.Password,
|
||||
},
|
||||
Bind: "127.0.0.1:0",
|
||||
Bind: "0.0.0.0:0",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("engine: redirect.New failed: %v", err)
|
||||
return fmt.Errorf("redirector: %w", err)
|
||||
}
|
||||
e.redir = r
|
||||
log.Printf("engine: redirector listening on %s", r.LocalAddr())
|
||||
|
||||
// 5. Build filter + open handle
|
||||
filter := divert.BuildFilter(divert.FilterParams{
|
||||
// 5. Build filters
|
||||
// SOCKET handle uses "true" — capture ALL socket events. We
|
||||
// filter by PID in-process. SOCKET layer fires Connect events
|
||||
// SYNCHRONOUSLY with the connect() syscall, BEFORE the SYN packet
|
||||
// leaves the box — which gives socketLoop time to populate the
|
||||
// redirector mapping before NETWORK-layer SYN arrives.
|
||||
//
|
||||
// (FLOW Established events fire after the 3-way handshake, which
|
||||
// is too late for our SYN-redirect plan — by then the conn is
|
||||
// already pointing at the real target.)
|
||||
netFilter := divert.BuildNetworkFilter(divert.FilterParams{
|
||||
TargetPIDs: pidList,
|
||||
OwnPID: e.ownPID,
|
||||
UpstreamIP: upstream.String(),
|
||||
LocalIP: e.localIP.String(),
|
||||
})
|
||||
h, err := divert.Open(filter)
|
||||
if err != nil {
|
||||
r.Close()
|
||||
return fmt.Errorf("WinDivert open: %w", err)
|
||||
}
|
||||
e.handle = h
|
||||
log.Printf("engine: socket filter: \"true\" (capture-all, PID-filter in-process)")
|
||||
log.Printf("engine: network filter: %s", netFilter)
|
||||
|
||||
// 6. Spawn divert reader + procscan ticker
|
||||
// Seed targetPIDs from initial procscan
|
||||
e.pidMu.Lock()
|
||||
for p := range pids {
|
||||
e.targetPIDs[p] = struct{}{}
|
||||
}
|
||||
e.pidMu.Unlock()
|
||||
|
||||
// 6. Open SOCKET handle FIRST with broad filter so we never miss
|
||||
// a new connection between procscan ticks. socketLoop discards
|
||||
// events from non-target PIDs in-process.
|
||||
flowH, err := divert.OpenSocket("true")
|
||||
if err != nil {
|
||||
log.Printf("engine: divert.OpenSocket failed: %v", err)
|
||||
r.Close()
|
||||
return fmt.Errorf("WinDivert socket open: %w", err)
|
||||
}
|
||||
e.flowH = flowH
|
||||
log.Printf("engine: WinDivert SOCKET handle opened (filter=\"true\")")
|
||||
|
||||
// 7. Open NETWORK handle for actual packet capture/redirect.
|
||||
netH, err := divert.Open(netFilter)
|
||||
if err != nil {
|
||||
log.Printf("engine: divert.Open(network) failed: %v", err)
|
||||
flowH.Close()
|
||||
r.Close()
|
||||
return fmt.Errorf("WinDivert network open: %w", err)
|
||||
}
|
||||
e.handle = netH
|
||||
log.Printf("engine: WinDivert NETWORK handle opened")
|
||||
|
||||
// 7b. UDP proxy. The SOCKS5 UDP ASSOCIATE control conn is opened
|
||||
// lazily on the first UDP packet from a target, so this New call
|
||||
// is non-blocking — no upstream I/O happens here.
|
||||
udpProxy, err := redirect.NewUDP(redirect.UDPConfig{
|
||||
SOCKS5: socks5.Config{
|
||||
ProxyAddr: e.cfg.ProxyAddr,
|
||||
UseAuth: e.cfg.UseAuth,
|
||||
Login: e.cfg.Login,
|
||||
Password: e.cfg.Password,
|
||||
},
|
||||
LocalIP: e.localIP,
|
||||
Injector: divertHandleInjector{h: netH},
|
||||
LogPrefix: "engine udp: ",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("engine: redirect.NewUDP failed: %v", err)
|
||||
netH.Close()
|
||||
flowH.Close()
|
||||
r.Close()
|
||||
return fmt.Errorf("udp proxy: %w", err)
|
||||
}
|
||||
e.udp = udpProxy
|
||||
log.Printf("engine: UDP proxy ready (lazy SOCKS5 ASSOCIATE)")
|
||||
|
||||
// 8. Spawn socket tracker + divert reader + procscan ticker
|
||||
e.ctx, e.cnl = context.WithCancel(context.Background())
|
||||
e.wg.Add(2)
|
||||
e.wg.Add(3)
|
||||
go e.socketLoop()
|
||||
go e.diverterLoop()
|
||||
go e.procscanLoop()
|
||||
log.Printf("engine: bringUp complete, transitioning to Active")
|
||||
return nil
|
||||
}
|
||||
|
||||
// divertHandleInjector adapts *divert.Handle to redirect.UDPInjector.
|
||||
// We expose Send through the SendInjectInbound helper which sets the
|
||||
// right WinDivert flags for fabricated inbound packets.
|
||||
type divertHandleInjector struct {
|
||||
h *divert.Handle
|
||||
}
|
||||
|
||||
func (d divertHandleInjector) Send(buf []byte, _ redirect.UDPInjectAddr) (int, error) {
|
||||
// We only ever inject UDP via this path (TCP path uses the
|
||||
// captured addr directly in diverterLoop).
|
||||
return d.h.SendInjectInbound(buf, true /* isUDP */)
|
||||
}
|
||||
|
||||
// Stop tears down. Always returns to Idle (or stays in Idle if
|
||||
// already there).
|
||||
func (e *Engine) Stop() error {
|
||||
@@ -197,49 +341,280 @@ func (e *Engine) Stop() error {
|
||||
if e.handle != nil {
|
||||
e.handle.Close()
|
||||
}
|
||||
if e.flowH != nil {
|
||||
e.flowH.Close()
|
||||
}
|
||||
if e.udp != nil {
|
||||
e.udp.Close()
|
||||
}
|
||||
if e.redir != nil {
|
||||
e.redir.Close()
|
||||
}
|
||||
e.wg.Wait()
|
||||
e.handle = nil
|
||||
e.flowH = nil
|
||||
e.redir = nil
|
||||
e.udp = nil
|
||||
e.transition(StatusIdle, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// socketLoop reads socket-layer events (Connect/Close) from the
|
||||
// SOCKET handle and maintains e.flowSet + the redirector mapping.
|
||||
//
|
||||
// SOCKET Connect fires synchronously with the connect() syscall on
|
||||
// the originating thread, BEFORE the SYN packet is dispatched. This
|
||||
// is the critical window: by populating flowSet+mapping in this
|
||||
// handler, the diverterLoop's NETWORK capture of the SYN finds the
|
||||
// target on first lookup and redirects correctly.
|
||||
func (e *Engine) socketLoop() {
|
||||
defer e.wg.Done()
|
||||
log.Printf("engine: socketLoop started")
|
||||
iter := 0
|
||||
for {
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
log.Printf("engine: socketLoop ctx done after %d iterations", iter)
|
||||
return
|
||||
default:
|
||||
}
|
||||
iter++
|
||||
e.handleMu.RLock()
|
||||
h := e.flowH
|
||||
e.handleMu.RUnlock()
|
||||
if h == nil {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
ev, err := h.RecvSocket()
|
||||
if err != nil {
|
||||
if errors.Is(err, divert.ErrShutdown) || errors.Is(err, divert.ErrInvalidHandle) {
|
||||
log.Printf("engine: socketLoop terminal error after %d iterations: %v", iter, err)
|
||||
return
|
||||
}
|
||||
log.Printf("engine: socketLoop transient error (iter %d): %v", iter, err)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
// In-process PID filter. Fast path: PID is in the set procscan
|
||||
// fed us. Slow path: PID isn't yet known (Update.exe spawn →
|
||||
// connect → exit routinely fits inside the 2-second procscan
|
||||
// tick), so resolve PID → exe name on demand and admit it if
|
||||
// the name matches our Targets list. This is what makes
|
||||
// "Checking for updates" finish in ~5 s instead of 30+.
|
||||
e.pidMu.RLock()
|
||||
_, isTarget := e.targetPIDs[ev.ProcessID]
|
||||
e.pidMu.RUnlock()
|
||||
if !isTarget {
|
||||
if name, err := procscan.ResolvePID(ev.ProcessID); err == nil {
|
||||
lname := strings.ToLower(name)
|
||||
for _, t := range e.cfg.Targets {
|
||||
if strings.EqualFold(t, lname) || strings.EqualFold(t, name) {
|
||||
isTarget = true
|
||||
e.pidMu.Lock()
|
||||
e.targetPIDs[ev.ProcessID] = struct{}{}
|
||||
e.pidMu.Unlock()
|
||||
log.Printf("engine: lazy-admit pid=%d name=%s (matched target)", ev.ProcessID, name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isTarget {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch ev.Kind {
|
||||
case divert.SocketKindConnect:
|
||||
// Connect fires before SYN. Populate redirector mapping +
|
||||
// flowSet so when SYN arrives at NETWORK layer the
|
||||
// diverterLoop knows to redirect.
|
||||
key := flowKey{
|
||||
dst: ev.DstAddr,
|
||||
sport: ev.SrcPort,
|
||||
dport: ev.DstPort,
|
||||
proto: ev.Protocol,
|
||||
}
|
||||
e.flowMu.Lock()
|
||||
e.flowSet[key] = struct{}{}
|
||||
setSize := len(e.flowSet)
|
||||
e.flowMu.Unlock()
|
||||
e.redir.SetMapping(ev.SrcPort, net.IPv4(ev.DstAddr[0], ev.DstAddr[1], ev.DstAddr[2], ev.DstAddr[3]), ev.DstPort)
|
||||
log.Printf("engine: socket connect pid=%d proto=%d %v:%d → %v:%d (set size=%d)",
|
||||
ev.ProcessID, ev.Protocol, ev.SrcAddr, ev.SrcPort, ev.DstAddr, ev.DstPort, setSize)
|
||||
case divert.SocketKindClose:
|
||||
key := flowKey{
|
||||
dst: ev.DstAddr,
|
||||
sport: ev.SrcPort,
|
||||
dport: ev.DstPort,
|
||||
proto: ev.Protocol,
|
||||
}
|
||||
e.flowMu.Lock()
|
||||
delete(e.flowSet, key)
|
||||
e.flowMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) diverterLoop() {
|
||||
defer e.wg.Done()
|
||||
log.Printf("engine: diverterLoop started")
|
||||
buf := make([]byte, 65536)
|
||||
listenerPort := e.redir.LocalPort()
|
||||
var rxCount, redirCount int64
|
||||
statusTk := time.NewTicker(5 * time.Second)
|
||||
defer statusTk.Stop()
|
||||
go func() {
|
||||
for range statusTk.C {
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
var udpFwd, udpFwdBytes, udpRecv, udpInj uint64
|
||||
if e.udp != nil {
|
||||
udpFwd, udpFwdBytes, udpRecv, udpInj = e.udp.Stats()
|
||||
}
|
||||
log.Printf("engine: diverter stats rx=%d tcpRedir=%d flowSet=%d | UDP fwd=%d/%dB recv=%d injected=%d",
|
||||
rxCount, redirCount, len(e.flowSet), udpFwd, udpFwdBytes, udpRecv, udpInj)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
n, addr, err := e.handle.Recv(buf)
|
||||
e.handleMu.RLock()
|
||||
h := e.handle
|
||||
e.handleMu.RUnlock()
|
||||
if h == nil {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
n, addr, err := h.Recv(buf)
|
||||
if err != nil {
|
||||
if errors.Is(err, divert.ErrShutdown) || errors.Is(err, divert.ErrInvalidHandle) {
|
||||
log.Printf("engine: diverterLoop terminal error after %d rx: %v", rxCount, err)
|
||||
e.transition(StatusFailed, err)
|
||||
return
|
||||
}
|
||||
log.Printf("engine: diverterLoop transient Recv error: %v", err)
|
||||
continue
|
||||
}
|
||||
// Parse + record + rewrite
|
||||
rxCount++
|
||||
|
||||
// === UDP fast path ===
|
||||
// Quick header sniff: if proto=17 (UDP), try the UDP-flow
|
||||
// branch. Target UDP flows are forwarded through the SOCKS5
|
||||
// UDP relay (consumed — NOT reinjected); non-target UDP is
|
||||
// passed through unmodified.
|
||||
if n >= 10 && buf[0]>>4 == 4 && buf[9] == 17 {
|
||||
udpInfo, uerr := divert.ParseIPv4UDP(buf[:n])
|
||||
if uerr == nil {
|
||||
var ukey flowKey
|
||||
copy(ukey.dst[:], udpInfo.DstIP.To4())
|
||||
ukey.sport = udpInfo.SrcPort
|
||||
ukey.dport = udpInfo.DstPort
|
||||
ukey.proto = 17
|
||||
|
||||
e.flowMu.RLock()
|
||||
_, isUDPTarget := e.flowSet[ukey]
|
||||
e.flowMu.RUnlock()
|
||||
|
||||
if isUDPTarget && e.udp != nil {
|
||||
// Strip IPv4 + UDP headers; the rest is application
|
||||
// payload that we hand to the SOCKS5 UDP relay.
|
||||
payload := buf[udpInfo.IHL+8 : n]
|
||||
if ferr := e.udp.Forward(udpInfo.SrcIP, udpInfo.SrcPort, udpInfo.DstIP, udpInfo.DstPort, payload); ferr != nil {
|
||||
log.Printf("engine: udp forward error %v:%d → %v:%d: %v",
|
||||
udpInfo.SrcIP, udpInfo.SrcPort, udpInfo.DstIP, udpInfo.DstPort, ferr)
|
||||
// Drop on error — UDP loss is acceptable.
|
||||
} else {
|
||||
redirCount++
|
||||
}
|
||||
// Consumed: do NOT reinject — relay reader will
|
||||
// fabricate the inbound reply.
|
||||
continue
|
||||
}
|
||||
// Non-target UDP: pass through unmodified.
|
||||
_, _ = h.Send(buf[:n], addr)
|
||||
continue
|
||||
}
|
||||
// Malformed UDP — fall through to TCP parse path (which
|
||||
// will also fail and reinject).
|
||||
}
|
||||
|
||||
// Parse + decide
|
||||
info, err := divert.ParseIPv4TCP(buf[:n])
|
||||
if err != nil {
|
||||
// Not parseable — reinject as-is.
|
||||
_, _ = e.handle.Send(buf[:n], addr)
|
||||
// Not IPv4-TCP and not handled by UDP path above. Reinject
|
||||
// as-is so non-target traffic continues normally.
|
||||
_, _ = h.Send(buf[:n], addr)
|
||||
continue
|
||||
}
|
||||
// SYN packets don't carry the full flow yet — but every
|
||||
// outbound TCP carries src_port we can map. We always record
|
||||
// the latest mapping, refreshing TTL on subsequent packets.
|
||||
e.redir.SetMapping(info.SrcPort, info.DstIP, info.DstPort)
|
||||
|
||||
// Rewrite to loopback
|
||||
if err := divert.RewriteDst(buf[:n], net.IPv4(127, 0, 0, 1), listenerPort); err == nil {
|
||||
_, _ = e.handle.Send(buf[:n], addr)
|
||||
localV4 := e.localIP.To4()
|
||||
srcV4 := info.SrcIP.To4()
|
||||
|
||||
// === RETURN path === : packet emitted by our listener back to
|
||||
// the client. SrcIP=local LAN, SrcPort=listener_port. We swap
|
||||
// IPs and rewrite SrcPort to the original target port so the
|
||||
// client (Discord) sees a response that matches its connect()
|
||||
// pair (src=real_target:real_port, dst=local_IP:client_eph).
|
||||
if info.SrcPort == listenerPort && srcV4 != nil && srcV4.Equal(localV4) {
|
||||
realIP, realPort, ok := e.redir.GetMapping(info.DstPort)
|
||||
if !ok {
|
||||
// No mapping — should be rare (TTL evicted?). Drop by
|
||||
// reinjecting as-is; client will retransmit.
|
||||
_, _ = h.Send(buf[:n], addr)
|
||||
continue
|
||||
}
|
||||
_ = realIP // streamdump only needs the original target PORT;
|
||||
// the IP is already the right one after the swap below
|
||||
// (we swap dst/src — original dst (=local) becomes src,
|
||||
// original src (=client_local) becomes dst). The original
|
||||
// remote IP is not on this packet — it's listener→client,
|
||||
// not listener→remote. So srcIP after swap = info.DstIP =
|
||||
// real Discord IP because... actually no — our SrcIP IS
|
||||
// local, our DstIP IS Discord. After swap our SrcIP =
|
||||
// Discord, DstIP = local. That's exactly what we want.
|
||||
if err := divert.SwapAndSetSrcPort(buf[:n], realPort); err == nil {
|
||||
addr.Flags &^= 0x02 // clear Outbound (deliver as inbound)
|
||||
addr.Flags |= 0x60 // signal IP+TCP checksums valid
|
||||
_, _ = h.Send(buf[:n], addr)
|
||||
redirCount++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// === FORWARD path === : packet from a target process to a
|
||||
// remote. Apply streamdump swap so the kernel delivers it to
|
||||
// our listener via the inbound path.
|
||||
var key flowKey
|
||||
copy(key.dst[:], info.DstIP.To4())
|
||||
key.sport = info.SrcPort
|
||||
key.dport = info.DstPort
|
||||
key.proto = 6
|
||||
|
||||
e.flowMu.RLock()
|
||||
_, isTarget := e.flowSet[key]
|
||||
e.flowMu.RUnlock()
|
||||
if !isTarget {
|
||||
_, _ = h.Send(buf[:n], addr)
|
||||
continue
|
||||
}
|
||||
|
||||
// Target flow: refresh redirector mapping and apply the
|
||||
// canonical streamdump swap (swap src↔dst, dst.port=listener,
|
||||
// addr.Outbound=0, mark checksums valid).
|
||||
e.redir.SetMapping(info.SrcPort, info.DstIP, info.DstPort)
|
||||
if err := divert.SwapAndSetDstPort(buf[:n], listenerPort); err == nil {
|
||||
addr.Flags &^= 0x02 // clear Outbound (deliver as inbound)
|
||||
addr.Flags |= 0x60 // signal IP+TCP checksums valid
|
||||
_, _ = h.Send(buf[:n], addr)
|
||||
redirCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,25 +639,28 @@ func (e *Engine) procscanLoop() {
|
||||
if len(add) == 0 && len(rem) == 0 {
|
||||
continue
|
||||
}
|
||||
// Rebuild filter + reopen handle
|
||||
pidList := make([]uint32, 0, len(cur))
|
||||
for p := range cur {
|
||||
pidList = append(pidList, p)
|
||||
log.Printf("engine: procscan delta added=%v removed=%v", add, rem)
|
||||
|
||||
// Update targetPIDs map. flowLoop reads it on every event;
|
||||
// no handle reopen needed (FLOW filter is "true").
|
||||
e.pidMu.Lock()
|
||||
for _, p := range add {
|
||||
e.targetPIDs[p] = struct{}{}
|
||||
}
|
||||
filter := divert.BuildFilter(divert.FilterParams{
|
||||
TargetPIDs: pidList,
|
||||
OwnPID: e.ownPID,
|
||||
UpstreamIP: e.upstreamIP.String(),
|
||||
})
|
||||
newH, err := divert.Open(filter)
|
||||
if err != nil {
|
||||
e.transition(StatusFailed, fmt.Errorf("reopen handle on PID change: %w", err))
|
||||
return
|
||||
for _, p := range rem {
|
||||
delete(e.targetPIDs, p)
|
||||
}
|
||||
oldH := e.handle
|
||||
e.handle = newH
|
||||
if oldH != nil {
|
||||
oldH.Close()
|
||||
e.pidMu.Unlock()
|
||||
|
||||
// Drop tracked flows for the removed PIDs. We don't actually
|
||||
// know which flowKey belongs to which PID (we lose that info
|
||||
// after Established → flowSet keyed by 5-tuple, not PID), so
|
||||
// for safety just clear the set when a target PID disappears
|
||||
// — flow events from the new PIDs will repopulate.
|
||||
if len(rem) > 0 {
|
||||
e.flowMu.Lock()
|
||||
e.flowSet = map[flowKey]struct{}{}
|
||||
e.flowMu.Unlock()
|
||||
}
|
||||
prev = cur
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user