#async #tokio #relay #gsrn

gsocket

Async Tokio client for the Global Socket Relay Network (GSRN)

1 unstable release

Uses new Rust 2024

new 0.1.0 Jun 11, 2026

#23 in #relay


Used in gs-netcat

BSD-2-Clause

180KB
3.5K SLoC

SkunkedApostle

Rust implementation of THC's "gsocket" protocol, with example implementation of gs-netcat.

It is just slop code to see what the LLM could do while I was kinda bored at work, but I think it is kinda neat to have and lets you use GSRN from your rust projects.


About

A memory-safe Rust port of gsocket, the Global Socket Relay Network (GSRN) library. Lets two processes anywhere on the internet talk to each other over an end-to-end encrypted tunnel, brokered by the public GSRN relay at gs.thc.org. No port forwarding, no public IP, no NAT punch.

Wire-compatible with the C reference, so a Rust peer can talk to a C peer on the other end (verified against the public GSRN). The whole workspace is #![forbid(unsafe_code)] and ships 72 unit tests.


Workspace

Crate What it does
gsocket-proto Wire-format codec for GSRN messages (LISTEN/CONNECT/START/PING/…) and the secret → address / SRP-password KDF
gsocket-tls-srp Hand-rolled TLS 1.2 with SRP-SHA1 key exchange and AES-256-CBC. We hand-roll because rustls doesn't implement SRP and the C reference uses OpenSSL's SRP-AES-256-CBC-SHA cipher suite
gsocket-pktmgr In-band packet manager (0xFB escape framing) and file-transfer state machines that ride inside the encrypted tunnel
gsocket Async Tokio client tying the above together: GSRN handshake + SRP-TLS handshake → an AsyncRead + AsyncWrite stream
gs-netcat The nc-style CLI: reverse shells, port forwards, SOCKS5 proxy, file transfer

Install the CLI

cargo install gs-netcat              # from crates.io
# or, from a checkout of this repo:
cargo install --path gs-netcat

Both ends share a secret. Generate one with openssl rand -hex 16 (or anything else) and use it on both sides.

CLI quickstart

All commands take -s SECRET (or GSOCKET_SECRET=); both ends must use the same value.

Reverse shell

# Server (whoever should run the shell):
gs-netcat -l -i -e "bash -il" -s "$SECRET"

# Client:
gs-netcat -i -s "$SECRET"

The server's bash runs in a PTY; on the client, -i puts the local terminal into raw mode so keystrokes pass through directly.

Port forwarding

Forward a remote service to a local port without opening any inbound firewall hole:

# Server, exposing 192.168.1.10:22 over GSRN:
gs-netcat -l -d 192.168.1.10 -p 22 -s "$SECRET"

# Client, accepting on local port 2222:
gs-netcat -p 2222 -s "$SECRET"
ssh -p 2222 user@127.0.0.1

SOCKS5

# Server:
gs-netcat -l -S -s "$SECRET"

# Client (binds local SOCKS5 on 1080):
gs-netcat -p 1080 -s "$SECRET"
curl --socks5 127.0.0.1:1080 https://example.com

File transfer

Server runs in interactive/exec mode so the in-band packet manager is enabled:

gs-netcat -l -i -e "sleep 3600" -s "$SECRET"

Then from the client:

# Upload one or more files
gs-netcat --put file1.tar.gz --put dir/file2.bin -s "$SECRET"

# List remote files
gs-netcat --ls '*.log' -s "$SECRET"

# Download a single file
gs-netcat --get archive.tar.gz --save-as ./local-copy.tar.gz -s "$SECRET"

# Receive uploads into a directory (loops to accept many)
gs-netcat -l --receive-dir ./incoming -s "$SECRET"

Plain stdio

Without -i, -e, -S, or -p, gs-netcat just pipes stdin↔stdout through the encrypted tunnel — the classic nc mode:

gs-netcat -l -s "$SECRET" > received.tar
gs-netcat   -s "$SECRET" < send.tar

Other flags

Flag Meaning
-l, --listen Listening side
-w, --wait Block until peer becomes available instead of failing
--host HOST Override the GSRN hostname (default gs.thc.org)
-d, --dest IP Destination IP for server-side port forward (default 127.0.0.1)
--bind ADDR Local bind address for client-side port forward

Run gs-netcat --help for the full list.


Library usage

Add to your Cargo.toml:

[dependencies]
gsocket        = "0.1"
gsocket-proto  = "0.1"
tokio          = { version = "1", features = ["full"] }

End-to-end: derive an address from a shared secret, perform the GSRN handshake, then run the SRP-TLS handshake to get an encrypted AsyncRead + AsyncWrite stream.

use gsocket::{
    client_handshake_with_prefix, handshake, server_handshake_with_prefix,
    GsConfig, Role,
};
use gsocket_proto::{GsSecret, StartRole};

async fn open_session(secret_str: &str, role: Role) -> anyhow::Result<()> {
    let secret = GsSecret::new(secret_str.to_string());
    let cfg = GsConfig::new(secret.derive_addr(), role);

    // 1. GSRN handshake — TCP to the relay, send LISTEN/CONNECT, wait
    //    for START.
    let relay = handshake(cfg).await?;

    // 2. SRP-TLS handshake over the matched stream. Whoever LISTENed
    //    becomes TLS server; whoever CONNECTed becomes TLS client.
    let wrapped = secret.derive_srp_password().wrapped().to_vec();
    let tls = match relay.role {
        StartRole::Client => {
            client_handshake_with_prefix(
                relay.stream,
                relay.initial_bytes,
                b"user".to_vec(), // SRP identity (any non-empty value)
                wrapped,
            )
            .await?
        }
        StartRole::Server => {
            server_handshake_with_prefix(relay.stream, relay.initial_bytes, wrapped)
                .await?
        }
    };

    // 3. `tls` is now an encrypted byte pipe to the peer.
    //    AsyncReadExt::read / AsyncWriteExt::write_all work as usual.
    let _ = tls;
    Ok(())
}

Why _with_prefix? The GSRN handshake may consume bytes that already belong to the TLS layer (peer messages arrive coalesced with the final GSRN START). RelayStream::initial_bytes carries those bytes; the _with_prefix variant injects them at the start of the TLS read path. If you don't need that — you're feeding TLS over a fresh stream — use the plain client_handshake / server_handshake.

File-transfer state machine

The gsocket-pktmgr crate exposes the framing and the PutSender / PutReceiver state machines. Drive them with a TLS stream from gsocket to send/receive files over the same encrypted tunnel:

use gsocket_pktmgr::{Pkt, PktEvent};
use gsocket_pktmgr::ft::{PutSender, PutReceiver};

See gs-netcat/src/main.rs (ft_send_one_inner, ft_receive_loop, ft_get_one, ft_ls) for complete, working examples.


Wire compatibility

The Rust port talks to the existing C gs-netcat over the public GSRN. This was the design constraint, and a few things had to be preserved bit-for-bit:

  • SRP password derivation has a 33-character truncation of the SHA-256 hex string. The C code's fixed-size buffer caused this and every C release since has kept it. See gsocket-proto/src/address.rs and the srp_password_preserves_c_truncation_quirk test.
  • SRP secret wrapping wraps the 33-byte derived value as "Blah." + raw + ".blubb-SRPSEC" before handing it to the SRP layer. Required by OpenSSL's SRP API in the C reference.
  • GSRN token linger — re-LISTENs from the same logical process must reuse the same token within GSRN_TOKEN_LINGER_SEC (7 s) or the relay rejects. GsConfig::new generates one random token; clone the config to reuse it across re-LISTENs.
  • TLS layer is TLS 1.2 with SRP-AES-256-CBC-SHA only. rustls doesn't support SRP at all; that's why this workspace ships its own hand-rolled TLS in gsocket-tls-srp. It is intentionally minimal — exactly one cipher suite — and not a general-purpose TLS stack.

Building and testing

cargo build --workspace
cargo test  --workspace                   # 72 unit tests
cargo doc   --workspace --no-deps         # rustdoc

For interop tests against the C reference, build it in gsocket/tools/gs-netcat from the upstream repo, then run a peer with a shared secret and exercise the Rust client. The relay is at gs.thc.org; everything goes through TCP/443.


Safety

[workspace.lints.rust]
unsafe_code = "forbid"

No unsafe anywhere in the workspace. Cryptographic primitives come from the RustCrypto ecosystem (sha2, sha1, hmac, aes, cbc) and num-bigint-dig for SRP big-int math.


License

BSD-2-Clause, matching the upstream C project.

This is an independent reimplementation of the gsocket library by The Hacker's Choice. Wire-format and cryptographic protocol details are theirs; the Rust implementation is original.

Thanks

THC guys for being awesome and writing gsocket and running the GSRN infrastructure.
My colleagues, for letting me fuck around on the computer for a bit.
Whoever is paying for the starlink.

Issues/Complaints

idk man. I might be able to help, maybe not.

Support my guys.

If you enjoyed this, supporting my guys would be appreciated. There is a fundraiser for the team running right now. donation link.

This thing created somewhere in the Pokrovsk direction.

Dependencies

~9–15MB
~196K SLoC