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:
@@ -0,0 +1,256 @@
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.okcu.io/root/drover-go/internal/socks5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// fakeInjector captures injected packets for assertions.
|
||||
type fakeInjector struct {
|
||||
mu sync.Mutex
|
||||
packets [][]byte
|
||||
addrs []UDPInjectAddr
|
||||
}
|
||||
|
||||
func (f *fakeInjector) Send(buf []byte, addr UDPInjectAddr) (int, error) {
|
||||
f.mu.Lock()
|
||||
cp := make([]byte, len(buf))
|
||||
copy(cp, buf)
|
||||
f.packets = append(f.packets, cp)
|
||||
f.addrs = append(f.addrs, addr)
|
||||
f.mu.Unlock()
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (f *fakeInjector) packetsLen() int {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return len(f.packets)
|
||||
}
|
||||
|
||||
func (f *fakeInjector) get(idx int) ([]byte, UDPInjectAddr) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.packets[idx], f.addrs[idx]
|
||||
}
|
||||
|
||||
// startUDPRelayProxy starts a fake SOCKS5 proxy with UDP ASSOCIATE
|
||||
// support. It echoes any datagram it receives on the relay back to
|
||||
// the sender, with the SOCKS5 envelope's DST.ADDR/DST.PORT preserved.
|
||||
// The "echoOrigin" return-path is what the real upstream relay does:
|
||||
// when an upstream UDP server responds, the proxy puts that server's
|
||||
// addr in DST.ADDR/DST.PORT for the inbound envelope.
|
||||
func startUDPRelayProxy(t *testing.T) (tcpAddr string, relay *net.UDPConn) {
|
||||
tcpLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { tcpLn.Close() })
|
||||
|
||||
relay, err = net.ListenUDP("udp4", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { relay.Close() })
|
||||
|
||||
go func() {
|
||||
for {
|
||||
c, err := tcpLn.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(c net.Conn) {
|
||||
defer c.Close()
|
||||
buf := make([]byte, 256)
|
||||
// Greet
|
||||
io.ReadFull(c, buf[:2])
|
||||
nm := int(buf[1])
|
||||
io.ReadFull(c, buf[:nm])
|
||||
c.Write([]byte{0x05, 0x00})
|
||||
// UDP ASSOCIATE
|
||||
io.ReadFull(c, buf[:4])
|
||||
if buf[1] != 0x03 {
|
||||
return
|
||||
}
|
||||
atyp := buf[3]
|
||||
switch atyp {
|
||||
case 1:
|
||||
io.ReadFull(c, buf[:4])
|
||||
case 3:
|
||||
io.ReadFull(c, buf[:1])
|
||||
io.ReadFull(c, buf[:int(buf[0])])
|
||||
}
|
||||
io.ReadFull(c, buf[:2])
|
||||
// Reply with relay addr
|
||||
ra := relay.LocalAddr().(*net.UDPAddr)
|
||||
rep := []byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}
|
||||
copy(rep[4:8], ra.IP.To4())
|
||||
binary.BigEndian.PutUint16(rep[8:10], uint16(ra.Port))
|
||||
c.Write(rep)
|
||||
// Hold open
|
||||
io.Copy(io.Discard, c)
|
||||
}(c)
|
||||
}
|
||||
}()
|
||||
return tcpLn.Addr().String(), relay
|
||||
}
|
||||
|
||||
func TestUDPProxy_ForwardEncapsulates(t *testing.T) {
|
||||
tcpAddr, relay := startUDPRelayProxy(t)
|
||||
inj := &fakeInjector{}
|
||||
|
||||
u, err := NewUDP(UDPConfig{
|
||||
SOCKS5: socks5.Config{ProxyAddr: tcpAddr},
|
||||
LocalIP: net.IPv4(127, 0, 0, 1),
|
||||
Injector: inj,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { u.Close() })
|
||||
|
||||
// Forward a packet and verify the relay receives it encapsulated.
|
||||
srcIP := net.IPv4(127, 0, 0, 1)
|
||||
dstIP := net.IPv4(140, 82, 121, 4)
|
||||
payload := []byte("hello voice")
|
||||
require.NoError(t, u.Forward(srcIP, 50100, dstIP, 50007, payload))
|
||||
|
||||
// Read from the relay to verify the SOCKS5 envelope.
|
||||
buf := make([]byte, 1500)
|
||||
_ = relay.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
n, _, err := relay.ReadFromUDP(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := buf[:n]
|
||||
gotIP, gotPort, gotPayload, err := socks5.DecapUDPv4(got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "140.82.121.4", gotIP.String())
|
||||
assert.Equal(t, uint16(50007), gotPort)
|
||||
assert.Equal(t, payload, gotPayload)
|
||||
|
||||
assert.Equal(t, 1, u.FlowCount(), "should have one tracked flow")
|
||||
}
|
||||
|
||||
func TestUDPProxy_RelayResponseInjectsBackToDiscord(t *testing.T) {
|
||||
tcpAddr, relay := startUDPRelayProxy(t)
|
||||
inj := &fakeInjector{}
|
||||
|
||||
u, err := NewUDP(UDPConfig{
|
||||
SOCKS5: socks5.Config{ProxyAddr: tcpAddr},
|
||||
LocalIP: net.IPv4(127, 0, 0, 1),
|
||||
Injector: inj,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { u.Close() })
|
||||
|
||||
// Establish a flow by forwarding one packet
|
||||
discordSrcIP := net.IPv4(127, 0, 0, 1)
|
||||
discordSrcPort := uint16(50100)
|
||||
realDstIP := net.IPv4(140, 82, 121, 4)
|
||||
realDstPort := uint16(50007)
|
||||
require.NoError(t, u.Forward(discordSrcIP, discordSrcPort, realDstIP, realDstPort, []byte("hi")))
|
||||
|
||||
// Drain the encapsulated forward
|
||||
drainBuf := make([]byte, 1500)
|
||||
_ = relay.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
_, clientRelayAddr, err := relay.ReadFromUDP(drainBuf)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate upstream UDP server response: relay sends back an
|
||||
// envelope where DST.ADDR/DST.PORT = real upstream origin.
|
||||
respPayload := []byte("voice response")
|
||||
envelope, err := socks5.EncapUDPv4(realDstIP, realDstPort, respPayload)
|
||||
require.NoError(t, err)
|
||||
_, err = relay.WriteToUDP(envelope, clientRelayAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The proxy's relayReadLoop should receive, decap, and inject.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) && inj.packetsLen() == 0 {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
require.Equal(t, 1, inj.packetsLen(), "expected one injected packet")
|
||||
|
||||
pkt, addr := inj.get(0)
|
||||
assert.False(t, addr.Outbound, "injected as inbound")
|
||||
|
||||
// Parse the fabricated IPv4+UDP packet
|
||||
require.GreaterOrEqual(t, len(pkt), 28)
|
||||
// Verify proto=UDP
|
||||
assert.Equal(t, byte(17), pkt[9], "IPv4 proto field")
|
||||
srcIP := net.IPv4(pkt[12], pkt[13], pkt[14], pkt[15])
|
||||
dstIP := net.IPv4(pkt[16], pkt[17], pkt[18], pkt[19])
|
||||
srcPort := binary.BigEndian.Uint16(pkt[20:22])
|
||||
dstPort := binary.BigEndian.Uint16(pkt[22:24])
|
||||
|
||||
assert.Equal(t, "140.82.121.4", srcIP.String(), "fabricated src = real upstream origin")
|
||||
assert.Equal(t, "127.0.0.1", dstIP.String(), "fabricated dst = Discord-side IP")
|
||||
assert.Equal(t, realDstPort, srcPort, "fabricated src port = real upstream port")
|
||||
assert.Equal(t, discordSrcPort, dstPort, "fabricated dst port = Discord ephemeral port")
|
||||
|
||||
// Payload after IPv4(20)+UDP(8) headers
|
||||
assert.Equal(t, respPayload, pkt[28:])
|
||||
}
|
||||
|
||||
func TestUDPProxy_NoFlowDropsResponse(t *testing.T) {
|
||||
tcpAddr, relay := startUDPRelayProxy(t)
|
||||
inj := &fakeInjector{}
|
||||
|
||||
u, err := NewUDP(UDPConfig{
|
||||
SOCKS5: socks5.Config{ProxyAddr: tcpAddr},
|
||||
LocalIP: net.IPv4(127, 0, 0, 1),
|
||||
Injector: inj,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { u.Close() })
|
||||
|
||||
// Force association without registering any flow.
|
||||
require.NoError(t, u.ensureAssociated())
|
||||
|
||||
// Read the local relay socket's port and substitute 127.0.0.1 for
|
||||
// 0.0.0.0 (kernel binds wildcard but Windows refuses to send TO
|
||||
// 0.0.0.0:N — it requires a routable destination).
|
||||
localAddr := u.relayConn.LocalAddr().(*net.UDPAddr)
|
||||
dst := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: localAddr.Port}
|
||||
|
||||
// Send a "stray" relay datagram with an origin we never registered.
|
||||
envelope, _ := socks5.EncapUDPv4(net.IPv4(8, 8, 8, 8), 53, []byte("dns"))
|
||||
_, err = relay.WriteToUDP(envelope, dst)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Give the reader time to process and drop.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
assert.Equal(t, 0, inj.packetsLen(), "stray response should be dropped, not injected")
|
||||
}
|
||||
|
||||
func TestUDPProxy_RejectsIPv6(t *testing.T) {
|
||||
inj := &fakeInjector{}
|
||||
u, err := NewUDP(UDPConfig{
|
||||
SOCKS5: socks5.Config{ProxyAddr: "127.0.0.1:0"},
|
||||
LocalIP: net.IPv4(127, 0, 0, 1),
|
||||
Injector: inj,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { u.Close() })
|
||||
|
||||
v6 := net.ParseIP("::1")
|
||||
err = u.Forward(net.IPv4(1, 2, 3, 4), 1000, v6, 80, []byte("x"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewUDP_RejectsNilInjector(t *testing.T) {
|
||||
_, err := NewUDP(UDPConfig{
|
||||
LocalIP: net.IPv4(127, 0, 0, 1),
|
||||
})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewUDP_RejectsNonIPv4LocalIP(t *testing.T) {
|
||||
_, err := NewUDP(UDPConfig{
|
||||
LocalIP: net.ParseIP("::1"),
|
||||
Injector: &fakeInjector{},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user