Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RevSocksRelay

A cloud SOCKS5 proxy whose exit nodes live behind NAT.

The machine that traffic actually exits from never opens a port, never needs a static IP, and never needs to be reachable from the internet. It dials out to a small relay server you run on a VPS, and stays registered there. Clients connect to that one relay address and pick an exit by username.

curl -x socks5h://alice:secret@relay.example.com:1080 https://ifconfig.co/ip
#      └── routed to whichever agent registered as "alice",
#          and exits from the network interface that agent chose

Why

Running a SOCKS5 proxy on a remote machine normally means port forwarding on the router, a firewall rule, and a stable public IP. That breaks the moment the machine sits behind CGNAT, the ISP rotates the address, or you simply do not control the router.

RevSocksRelay inverts the direction. The exit machine (the agent) opens an outbound connection to your relay server and keeps it alive. When a client asks the relay for a connection, the relay hands the job down that existing tunnel. Nothing ever connects to the agent.

Problem How it is solved
Router port forwarding Agent only makes outbound connections
CGNAT / no public IP Same — outbound works from anywhere
IP address changes Agent re-registers; the relay never stores its address
Many exits, one entry point One SOCKS5 port, routed by username

How it works

Three components: the agent (exit node), the relay server (rendezvous point), and any SOCKS5 client.

[Agent machine]                  [Relay server / VPS]              [Client]
 behind NAT                       public IP + domain

 1. outbound TCP ───────────>  :7000 control      registry["alice"] = agent
    HELLO_CONTROL
    {user, pass, exitIp}

 2. outbound TCP ───────────>  :7000 worker × 4   parked in pool
    HELLO_WORKER                                              <──── 3. SOCKS5 :1080
                                                                     auth alice:secret
                                                                     CONNECT host:port
                               4. look up registry, take a parked worker
    <──── TASK {host, port} ───┘
 5. resolve DNS,
    bind(exitIp), connect
    ──── TASK_OK {bnd} ──────>
                               6. <========= raw byte relay =========>
 7. open a replacement
    worker to refill the pool

Connection pooling. As soon as an agent registers, it opens a few idle worker connections that park on the relay. When a client arrives, a worker is already waiting — so there is no round trip spent asking the agent to dial back. If the pool happens to be empty the relay sends SPAWN over the control channel and waits briefly, so pooling is purely an optimization, never a correctness requirement.

DNS resolves on the agent. Hostnames travel over the wire and are resolved at the exit node. That avoids DNS leaks and gives geographically correct answers for the exit's location. Use socks5h:// (not socks5://) so your client sends the hostname instead of resolving it locally.


Quick start

1. Build

.\deploy\publish.ps1
Output Size Requirements on target
publish/linux-x64/revsocks-relay-server 13.4 MB None — .NET is not needed
publish/win-x64/RevSocksRelayAgent.exe 46.9 MB None — .NET is not needed

Both are self-contained, single-file and compressed. No DLLs, no runtime, no config files sitting next to them — settings files are created on first run.

The Android agent is built separately with Gradle — see Android agent → Build. Every push to main also builds all three components and attaches them to a GitHub Release automatically; see Automated builds and releases.

The server is additionally trimmed (70 MB → 13 MB). To make trimming safe, JSON serialization uses a source generator rather than reflection (ProtocolJsonContext). The agent is not trimmed, because WinForms does not support it.

2. Relay server (Ubuntu)

Copy the single binary and the deploy/ folder to your VPS:

chmod +x deploy/install-ubuntu.sh
sudo ./deploy/install-ubuntu.sh ./revsocks-relay-server

This creates a revsocks-relay system user, installs the binary to /opt/revsocks-relay, writes /etc/revsocks-relay/server.json, and enables a systemd service.

journalctl -u revsocks-relay-server -f    # live logs
systemctl restart revsocks-relay-server   # after editing the config

Open the firewall:

sudo ufw allow 7000/tcp                            # agent registration port
sudo ufw allow from <your-ip> to any port 1080     # SOCKS5 — restrict if you can

Anyone who learns a username/password pair can use your proxy. If you connect from a known address, restrict port 1080 to it rather than exposing it to the internet.

3. Agent (Windows)

Run RevSocksRelayAgent.exe on the machine you want traffic to exit from.

  1. Relay server — your VPS hostname and port (7000).
  2. Uplink interface — which local connection carries the tunnel itself. Leave it on Automatic to follow the operating system's routing table, or pin it to one address. See below for why this matters on a machine with more than one internet connection.
  3. Add one row per exit:
    • Username / Password — the credentials clients will use.
    • Exit IP — pick from the dropdown of local addresses. The agent binds this address when connecting to targets, so traffic leaves through that interface.
  4. Press Start.

Settings are saved to agent.json next to the executable.

The Status column shows Registered (green), Connecting / Reconnecting (orange), or Rejected / Error (red). Pool shows how many worker connections are currently parked and ready.

Running in the background

Minimizing the window sends the agent to the notification area instead of the taskbar, and the tray icon is colour-coded so you can read its state at a glance:

Icon Meaning
Grey Stopped
Amber Connecting, or only some profiles registered
Green All profiles registered
Red No profile could connect

Left-click the icon to bring the window back, right-click for a menu with Show, Start, Stop and Exit. Closing the window while profiles are running also minimizes to the tray rather than killing the tunnels — use Exit in the tray menu to actually quit.

4. Client

curl -x socks5h://alice:secret@relay.example.com:1080 https://ifconfig.co/json

In Firefox: Settings → Network Settings → Manual proxy → SOCKS v5, tick Proxy DNS when using SOCKS v5. Credentials are prompted on first use.

Authentication is mandatory — the relay rejects the no-auth method, because the username is what selects the route.


5. Android agent

The Android agent speaks the same protocol as the desktop one, so the relay server needs no changes. Its reason to exist: register over Wi-Fi while exiting through the mobile carrier, giving you a carrier IP without any port forwarding on either side.

[Android phone]                        [Relay server]              [Client]
 someone's Wi-Fi + own SIM

 control + workers ── Wi-Fi ─────────>  :7000
                                                             <──── SOCKS5 :1080
 target connections ── cellular ─────>  the internet

Android lets several networks be active at once. The agent asks for the cellular network explicitly, which keeps mobile data up even while Wi-Fi is the default route, then binds each socket to the network it belongs to:

val request = NetworkRequest.Builder()
    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    .build()
connectivity.requestNetwork(request, callback)   // holds the radio up

val socket = cellularNetwork.socketFactory.createSocket()   // bypasses default routing

DNS is resolved through the same network (Network.getAllByName), so a cellular exit does not leak lookups onto the Wi-Fi resolver.

bindProcessToNetwork() would have been simpler but is process-wide — it cannot hold two networks at once, which is exactly what this design needs.

Build

cd android
.\gradlew.bat assembleDebug

Output: android/app/build/outputs/apk/debug/app-debug.apk. Install with adb install -r app-debug.apk or copy it to the phone. Requires JDK 17+ (Android Studio's bundled JBR works) and Android SDK 36.

For a distributable build, run .\gradlew.bat assembleRelease instead — the release variant is signed with the debug key (see app/build.gradle.kts), so it installs without any signing setup. This is also what CI attaches to each release as revsocks-relay-agent-android.apk — see Automated builds and releases.

Settings

Setting Meaning
Main server Relay host and port (7000)
Uplink network Which network carries the tunnel — Wi-Fi, mobile data, or automatic
Exit network (per profile) Which network reaches the target — mobile data, Wi-Fi, or automatic
Auto-start on boot Reconnects after a reboot

As on desktop, one profile is one username:password, and the client picks the exit by authenticating with that pair. You can run a Wi-Fi-exit profile and a cellular-exit profile side by side under different usernames.

Staying alive in the background

Android fights long-running background work, so the app:

  • runs as a foreground service with an ongoing notification;
  • declares the specialUse service type. This is deliberate — dataSync is capped at 6 hours per day on Android 15 and would cut a permanent tunnel;
  • holds a partial wake lock while the tunnel is up;
  • offers a battery-optimisation exemption prompt, without which Doze can drop the tunnel;
  • optionally restarts itself after a reboot.

Aggressive OEM task killers (Xiaomi, Huawei, Samsung and friends) still need the app whitelisted by hand in their own battery settings. No amount of code fixes that.

Verified on real hardware

Tested on a Android 16 with both Wi-Fi and mobile data active at once, against the C# relay server:

Check Result
Registered over Wi-Fi Server saw the connection arrive from the phone's Wi-Fi address
Exit bound to cellular
Foreground service Started with the specialUse type; system allowed it
Pool + relay + auth Same protocol as desktop, server unchanged

That IP split is the whole point: the phone stayed on Wi-Fi for the tunnel, but every proxied request left through the mobile network.

Costs and limits

  • Mobile data pays for everything. With a cellular exit, every proxied byte comes out of your data plan. The Wi-Fi leg carries the same bytes but does not spare the SIM.
  • Battery. Holding the cellular radio up while Wi-Fi is connected keeps it from sleeping deeply. Fine on a charger, noticeable in a pocket.
  • No SIM1/SIM2 picker. Most dual-SIM devices only run data on one subscription at a time, and switching the default data SIM needs MODIFY_PHONE_STATE, a signature-level permission unavailable to normal apps. The agent uses whichever SIM currently holds mobile data; change it in Android's own settings.
  • No TLS yet on the Android agent — plain TCP, matching the server default.

Uplink vs exit

These are two different interfaces, and on a multi-homed machine you usually want to set both deliberately:

Setting Scope Which leg it controls
Uplink interface One per agent Agent → relay server. Carries every byte of every tunnel.
Exit IP One per profile Agent → target host. Decides the source address the target sees.

Proxied traffic crosses both. If the machine has a metered mobile connection alongside an unmetered one, pinning the uplink to the unmetered link keeps the tunnel off your data cap — regardless of which interface a profile exits from.

The uplink is enforced by binding the socket, not by hinting to the router. If the chosen interface cannot reach the relay server, the agent reports the error and retries with backoff rather than silently falling back to another connection — which is the point: a silent fallback onto a metered link is exactly what you were trying to avoid.

Because a bound socket cannot cross address families, an IPv4 uplink resolves the relay hostname to IPv4 only (and likewise for IPv6).


Multiple exits from one machine

If the agent machine has several interfaces, add one row per interface with a different username. All of them register at once, and the username a client authenticates with decides which interface the traffic leaves through.

Username Exit IP Interface
alice 192.168.10.191 Wi-Fi
bob 10.8.0.6 VPN adapter

Configuration

Server — /etc/revsocks-relay/server.json

Key Default Meaning
RelayListenIp / RelayListenPort 0.0.0.0 / 7000 Where agents connect
Socks5ListenIp / Socks5ListenPort 0.0.0.0 / 1080 Where SOCKS5 clients connect
PoolSize 4 Idle worker connections kept per agent
HeartbeatSeconds 20 Control-channel ping interval; 2.5× silence means dead
WorkerWaitMs 15000 How long to wait for a worker when the pool is empty
TaskTimeoutMs 30000 How long the agent has to reach the target
HandshakeTimeoutMs 15000 First-frame and SOCKS5 handshake timeout
UseTls false Wrap agent connections in TLS (must match the agent setting)
Verbose false Debug logging (also via REVSOCKS_RELAY_VERBOSE=1)

Agent — agent.json

Key Meaning
ServerHost / ServerPort Relay server address
UplinkIp Local address to bind when connecting to the relay; empty means system routing
UseTls Must match the server setting
SkipCertificateValidation Accept any certificate (only relevant with TLS)
PinnedThumbprint If set, the server certificate's SHA-256 must match exactly
Profiles[] { Enabled, Username, Password, ExitIp } per exit

Passwords are stored in plain text in agent.json.


How registration works

There is no user database on the server. Credentials are defined by the agent:

  • An agent registers with username:password; the relay stores that pair in memory.
  • A client presenting the same pair is routed to that agent.
  • A client presenting an unknown username is rejected — that route simply does not exist.

If the same username registers a second time:

Case Result
Same password The old session is dropped and the new one takes over (reconnect)
Different password Registration is rejected; the existing session is untouched

That lets an agent reclaim its name after a network drop, while stopping anyone else from squatting on it.


Protocol

Length-prefixed frames between agent and relay:

[uint8 type][uint32 big-endian length][UTF-8 JSON payload]
Type Direction Payload
0x01 HELLO_CONTROL agent → relay {username, password, exitIp, agentVersion}
0x02 HELLO_WORKER agent → relay {username, password}
0x10 ACCEPTED relay → agent {poolSize, heartbeatSeconds}
0x11 REJECTED relay → agent {reason}
0x20 PING / 0x21 PONG both
0x30 SPAWN relay → agent {count} — pool exhausted
0x40 TASK relay → agent {host, port} — on a worker channel
0x41 TASK_OK agent → relay {bndAddr, bndPort}
0x42 TASK_FAIL agent → relay {socksCode, message}

After TASK_OK framing stops and the connection carries raw bytes in both directions.


TLS

TLS is off by default; plain TCP works fine. Its only job here is protecting the password the agent sends during registration from anyone on the network path.

That password has to arrive in usable form, because the relay compares it against the plaintext password a SOCKS5 client sends per RFC 1929 — so hashing it in transit is not an option, and channel encryption is the only lever.

To enable it, set "UseTls": true on the server and tick Use TLS in the agent. If no certificate is supplied the server generates a self-signed one next to the binary and logs its SHA-256 fingerprint; the agent can either skip validation or pin that fingerprint via PinnedThumbprint.


Limitations

  • TCP CONNECT only. UDP ASSOCIATE and BIND are not implemented, so UDP-based traffic (QUIC fallback aside, most games, WebRTC media) will not pass.
  • Address families must match. A socket bound to an IPv4 exit address cannot reach an IPv6 target. The agent filters DNS results to the exit address's family and returns 0x04 host unreachable when nothing matches. For IPv6 targets, register a second profile with an IPv6 exit address.
  • Bandwidth doubles at the relay. Every byte crosses the VPS twice, once in and once out. Watch your traffic quota.
  • One extra hop of latency. Client → relay → agent → target.
  • Credentials are plaintext in memory on the relay and in agent.json on the agent.

Tested scenarios

Verified end to end with a relay server, two agents on different interfaces, and curl:

Scenario Result
HTTPS via hostname (socks5h), DNS resolved by the agent Correct public IP returned, byte-identical to a direct request
Two usernames → two different exit interfaces Each exited through its own interface
Exit IP binding Target observed the configured exit address as the source
8 concurrent requests 8/8 succeeded, pool refilled itself
Wrong password SOCKS5 authentication rejected
Agent process killed Relay deregistered it; later requests rejected
Agent restarted Re-registered automatically, traffic resumed
Same username, different password Rejected; existing session unaffected
Trimmed single-file server Identical behaviour after trimming
Uplink pinned to a chosen interface Relay saw the connection arrive from that exact address
Uplink pinned to an interface with no route Clear error, exponential backoff, no silent fallback
Minimize to tray Left the taskbar, process kept running

Project layout

Project Target Contents
src/RevSocksRelay.Protocol net10.0 Frame codec, message types, SOCKS5 constants, relay pump
src/RevSocksRelay.Server net10.0 Relay server — console, Linux and Windows
src/RevSocksRelay.Agent net10.0-windows Agent — WinForms

The server does not reference the agent, so it publishes standalone on Linux.

Building by hand:

dotnet publish src\RevSocksRelay.Server\RevSocksRelay.Server.csproj -c Release -r linux-x64 -o publish\linux-x64
dotnet publish src\RevSocksRelay.Agent\RevSocksRelay.Agent.csproj   -c Release -r win-x64   -o publish\win-x64

Single-file, compression and trimming settings live in a PropertyGroup that activates whenever a RuntimeIdentifier is supplied, so passing -r is enough to turn them on.


Automated builds and releases

.github/workflows/release.yml builds every component on each push to main (and on demand from the Actions tab) and publishes the results to the repository's Releases page.

Job Runner Produces
Server ubuntu-latest revsocks-relay-server-linux-x64, revsocks-relay-server-win-x64.exe
Agent windows-latest RevSocksRelayAgent-win-x64.exe
Android ubuntu-latest revsocks-relay-agent-android.apk

The server is a console app and cross-compiles both runtimes on Linux; the WinForms agent needs a Windows toolchain, so it builds on a Windows runner; the Android APK is a release build signed with the debug key, so it needs no signing secrets.

Each run creates a release tagged build-<run-number> against the pushed commit with all four files attached. The workflow authenticates with the built-in GITHUB_TOKEN — no extra configuration is required.


License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages