ShadowSocketProxy demonstrates a WSL networking model where outbound TCP/UDP flows are intercepted inside the Linux side and transparently proxied through a host‑side process. The container believes it is connecting normally; the host actually owns the external socket.
Outbound packets are rewritten to target a host‑visible proxy socket. The proxy establishes the real external connection, forwards data bidirectionally, and preserves flow semantics. Inbound packets are rewritten back to the container’s original tuple so applications see a normal connection.
- TC‑attached BPF program — Intercepts inbound/outbound packets, rewrites L3/L4 tuples, and maintains a per‑flow redirection map in a BPF hash.
- Container gRPC control service — Loads the BPF program, exposes the redirection map to the host, and provides minimal control/inspection hooks.
- Host shadow proxy — Listens for redirected flows, performs the real outbound connect(), and bridges traffic between the container and the external endpoint.
WSL’s NAT model breaks VPNs, packet inspection, and tools that rely on owning the real socket. ShadowSocketProxy gives the host full visibility and control over outbound flows while keeping the container unmodified.
- Container app calls
connect(). - BPF rewrites the destination to the host proxy.
- Host proxy receives the synthetic connection, looks up the original tuple via gRPC.
- Host proxy establishes the real external connection.
- Proxy shuttles bytes between both sides until teardown.
Prototype. Global TCP/UDP packet rewriting, flow mapping, and proxy bridging are implemented. Kernel attachment and packet-path verification remain Linux-environment gated.
The Linux-targeted Rust control service is in crates/control-service, with
the shared protobuf contract in crates/proto and the Linux TC BPF artifact
source in crates/bpf:
cargo build --target x86_64-unknown-linux-gnu
cargo test
It provides the versioned mapping ABI, replaceable BPF/TC backend, maintenance worker, protobuf/gRPC service, configuration snapshots, and bounded log pull. On Linux, the production backend uses Aya for ELF loading, versioned map/program discovery, TC ingress/egress links, transactional rollback, and map operations. The TCP gRPC endpoint uses OpenSSL with TLS 1.2 PSK and h2 ALPN; invalid credentials or a build without PSK support fail startup rather than falling back to plaintext, metadata-only auth, or mTLS. Linux builds require an OpenSSL development installation whose build enables PSK.
The Windows host proxy is in crates/host-proxy. It listens for redirected
TCP and UDP flows, resolves each observed synthetic tuple through the
authenticated GetMapping RPC, connects TCP flows to the original destination,
and forwards UDP datagrams with response relaying.
Build the default workspace target with:
cargo build -p shadow-socket-proxy-host
Windows deployments that provide a PSK-capable OpenSSL installation must build the runnable proxy with:
cargo build -p shadow-socket-proxy-host --features tls-psk
Configure the listener and control service with CLI options; provide the PSK through --psk-secret,
SSP_TLS_PSK_SECRET, or --psk-secret-file. The proxy requires a nonzero
--udp-idle-timeout-secs and never falls back to direct forwarding when a
mapping lookup fails. The listen address must be a specific local IPv4 or IPv6
address, not a wildcard address, so UDP lookups preserve the actual local
destination tuple.
This is a prototype, not a hardened production service. The following procedure runs the same control service, BPF program, and Windows host proxy that a demo uses. It redirects all eligible new IPv4 TCP and UDP flows from the selected WSL interface through the Windows proxy. The proxy creates the actual outbound connections, so only use a disposable WSL distribution or a quiet demo environment.
The host needs Windows, WSL 2, a WSL distribution with BPF/TC support, Rust
1.96.1 available in both Windows and WSL, and a PSK-capable OpenSSL
installation. The examples use an Ubuntu distribution named Ubuntu, a
repository at C:\dev\ShadowSocketProxy, and the default WSL interface
eth0.
Install the Linux build and runtime prerequisites as WSL root, then build as the normal WSL user:
wsl -d Ubuntu -u root -- sh -c `
'apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y `
build-essential clang llvm linux-libc-dev libssl-dev pkg-config make `
iproute2 python3 ca-certificates'
wsl -d Ubuntu -- bash -lc `
'cd /mnt/c/dev/ShadowSocketProxy &&
make -C crates/bpf clean all &&
cargo build --locked --release -p shadow-socket-proxy-control'Install the Windows OpenSSL development package and build the host components:
winget install --id ShiningLight.OpenSSL.Dev --version 4.0.1 --exact `
--scope machine --accept-source-agreements --accept-package-agreements
$openssl = Get-ChildItem 'C:\Program Files' -Directory -Filter 'OpenSSL*' |
ForEach-Object { Join-Path $_.FullName 'bin\openssl.exe' } |
Where-Object { Test-Path $_ } |
Select-Object -First 1
$opensslRoot = Split-Path (Split-Path $openssl -Parent) -Parent
$env:OPENSSL_DIR = $opensslRoot
$env:OPENSSL_LIB_DIR = Join-Path $opensslRoot 'lib\VC\x64\MD'
cargo build --locked --release -p shadow-socket-proxy-host --features tls-pskOpen three PowerShell terminals in the repository. First, calculate the WSL gateway address and create one PSK shared by the control service and proxy:
$gateway = (wsl -d Ubuntu -- ip route show default).Split()[2]
$identity = 'ssp-demo'
$secret = [Convert]::ToHexString((1..32 | ForEach-Object { Get-Random -Maximum 256 }))
$secret | Set-Content -NoNewline .\ssp-demo.pskIn the first terminal, load the shared credentials and start the control
service as WSL root. WSL traffic leaves through physical egress, so
SSP_TC_HOOK_LAYOUT=wsl is required here. Do not set that variable for a
native Linux deployment, which uses the default ingress/egress layout.
$identity = 'ssp-demo'
$secret = (Get-Content -Raw .\ssp-demo.psk).Trim()
wsl -d Ubuntu -u root -- env `
RUST_LOG=info `
SSP_LISTEN_ADDR=127.0.0.1:50051 `
SSP_TC_HOOK_LAYOUT=wsl `
SSP_TLS_PSK_IDENTITY=$identity `
SSP_TLS_PSK_SECRET=$secret `
/mnt/c/dev/ShadowSocketProxy/target/release/shadow-socket-proxy-controlIn the second terminal, rediscover the gateway, identity, and OpenSSL path
before starting the Windows proxy. The 127.0.0.1 control endpoint uses WSL
localhost forwarding, while the proxy listens on the WSL gateway address that
BPF will use as its synthetic destination. The proxy authenticates to the
control service, attaches the supplied BPF ELF to the interface, and sets its
own listener as the global target before it accepts traffic.
$gateway = (wsl -d Ubuntu -- ip route show default).Split()[2]
$identity = 'ssp-demo'
$openssl = Get-ChildItem 'C:\Program Files' -Directory -Filter 'OpenSSL*' |
ForEach-Object { Join-Path $_.FullName 'bin\openssl.exe' } |
Where-Object { Test-Path $_ } |
Select-Object -First 1
$opensslRoot = Split-Path (Split-Path $openssl -Parent) -Parent
$env:PATH = "$opensslRoot\bin;$env:PATH"
$env:RUST_LOG = 'info'
.\target\release\shadow-socket-proxy-host.exe `
--listen "${gateway}:15000" `
--control-endpoint https://127.0.0.1:50051 `
--psk-identity $identity `
--psk-secret-file .\ssp-demo.psk `
--bpf-elf /mnt/c/dev/ShadowSocketProxy/crates/bpf/shadow-socket-proxy.bpf.o `
--interface eth0The proxy prints connected to control service followed by attached BPF program and configured proxy target. The control-service terminal prints
BPF program attached; wsl -d Ubuntu -u root -- bpftool prog list also shows
the loaded programs. Each accepted TCP connection and newly created UDP
association then prints an unconditional forwarding record with its client,
proxy, and original destination.
In the third terminal, optionally start a local marker server and demonstrate the redirected WSL-to-Windows path:
$gateway = (wsl -d Ubuntu -- ip route show default).Split()[2]
$marker = 'ssp-demo-marker'
$markerProcess = Start-Process pwsh -PassThru -ArgumentList @(
'-NoProfile', '-File', '.\scripts\tcp-marker-server.ps1',
'-BindAddress', $gateway, '-Port', '18080', '-Marker', $marker
)
wsl -d Ubuntu -- python3 -c `
"import socket; s = socket.create_connection(('$gateway', 18080), 10); s.sendall(b'demo\n'); print(s.recv(1024).decode().strip()); s.close()"After the marker validation succeeds, WSL applications can make normal outbound connections; their eligible IPv4 TCP and UDP flows are redirected through the Windows proxy. For example:
wsl -d Ubuntu -- python3 -c `
"import socket; s = socket.create_connection(('1.1.1.1', 443), 10); print(s.getpeername()); s.close()"Press Ctrl+C in the proxy terminal; it detaches the BPF links it attached.
Then stop the control service and marker process, and remove the temporary PSK
file:
Stop-Process -Id $markerProcess.Id
Remove-Item .\ssp-demo.pskThe generated site combines private-item Rustdoc with Doxygen for the canonical BPF source. From a clean checkout with Rust 1.96.1 and Doxygen:
python scripts/check-rustdoc.py
PowerShell:
$env:RUSTDOCFLAGS = "-D warnings"
cargo doc --locked --workspace --no-deps --document-private-itemsPOSIX shell:
RUSTDOCFLAGS="-D warnings" cargo doc --locked --workspace --no-deps --document-private-itemsThen, on either platform:
python -c "import shutil; shutil.rmtree('docs/.generated', ignore_errors=True); shutil.rmtree('site', ignore_errors=True)"
doxygen docs/Doxyfile
python scripts/assemble-docs.py --site-dir site
The disposable site/ directory contains index.html, rustdoc/, and
bpf/; it is not committed to the source branch.
The checked-in Windows/WSL driver exercises the deployed BPF, control service, and host proxy with an ephemeral TLS-PSK. It requires Windows, an installed WSL distribution, and the Windows OpenSSL/Rust prerequisites:
cargo build --locked --release -p shadow-socket-proxy-e2e-runner --features tls-psk
.\scripts\run-windows-wsl-e2e.ps1 `
-BpfArtifact .\artifacts\bpf\shadow-socket-proxy.bpf.o `
-ControlArtifact .\artifacts\control\shadow-socket-proxy-control `
-HostArtifact .\artifacts\hostThe command fails when WSL, BPF/TC, authentication, process, marker, mapping, counter, or cleanup prerequisites are unavailable; it never falls back to direct forwarding.
Local runs retain the selected WSL distribution and its existing TC setup.
CI passes -TerminateDistribution because it uses a disposable hosted
distribution.