1 unstable release
Uses new Rust 2024
| new 0.1.0 | Jun 11, 2026 |
|---|
#23 in #relay
Used in gs-netcat
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.rsand thesrp_password_preserves_c_truncation_quirktest. - 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::newgenerates one random token; clone the config to reuse it across re-LISTENs. - TLS layer is TLS 1.2 with
SRP-AES-256-CBC-SHAonly. rustls doesn't support SRP at all; that's why this workspace ships its own hand-rolled TLS ingsocket-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