A minimal Privileged Access Management (PAM) tunnel system consisting of two Rust binaries:
| Binary | Host | Role |
|---|---|---|
vps-agent |
Public VPS (Linux) | Telegram bot + tunnel orchestrator |
gateway-agent |
Local network gateway machine (Linux) | Reverse-tunnel client |
Telegram user
│ /rdp 192.168.1.23
▼
┌─────────────┐ control channel (persistent TCP) ┌──────────────────┐
│ vps-agent │ ──────────────────────────────────── │ gateway-agent │
│ │ ◄── ControlReply::TunnelReady ─────── │ │
│ public │ │ LAN │
│ port XXXXX │ data channel (single-use TCP) │ │
│ (user RDP) │ │ 192.168.1.23:3389│
│ │ ◄── DataHandshake{token} ─────────── │ │
│ data port │ └──────────────────┘
│ YYYYY │
└─────────────┘
▲
│ RDP/SSH client connects to VPS_HOST:XXXXX
│ (from .rdp file or ssh command sent by bot)
- User sends
/rdp 192.168.1.23to the bot. - VPS binds two random ports from the configured range:
public_port– the end-user's RDP/SSH client will connect here.data_port– the gateway will connect here as the server-side of the tunnel.
- VPS sends
OpenTunnel { target_host, target_port, token, data_port }to the gateway over the persistent control channel. - Gateway connects to
VPS:data_port, sendsDataHandshake { token }(one-time auth), then connects to192.168.1.23:3389locally. - VPS authenticates the token, then waits for the user's RDP client on
public_port. - Once both sides are connected, VPS proxies the two TCP streams bidirectionally.
- When the session ends (disconnect or 120 s timeout), both ports are closed and the session is removed.
Each session uses a fresh random token and random ports — truly single-use.
Download the latest release for your architecture from the Releases page:
| File | Target |
|---|---|
pamtunnel-*-x86_64-unknown-linux-gnu.tar.gz |
amd64 VPS / server (glibc) |
pamtunnel-*-x86_64-unknown-linux-musl.tar.gz |
amd64 static (containers, minimal images) |
pamtunnel-*-aarch64-unknown-linux-gnu.tar.gz |
arm64 (Oracle ARM, Raspberry Pi 4 64-bit) |
pamtunnel-*-aarch64-unknown-linux-musl.tar.gz |
arm64 static |
pamtunnel-*-armv7-unknown-linux-gnueabihf.tar.gz |
armv7 (Raspberry Pi 2/3 32-bit) |
Each archive contains both vps-agent and gateway-agent plus the .env.example files.
cargo build --releaseProduces:
target/release/vps-agenttarget/release/gateway-agent
# Copy binary
scp target/release/vps-agent user@vps:/usr/local/bin/
# Configure
cp vps-agent/vps-agent.env.example /etc/pamtunnel/vps-agent.env
$EDITOR /etc/pamtunnel/vps-agent.env
# Run
export $(cat /etc/pamtunnel/vps-agent.env | grep -v '^#' | tr -d '\r' | xargs)
RUST_LOG=info /usr/local/bin/vps-agentcp vps-agent/vps-agent.env.example vps-agent.env
$EDITOR vps-agent.env
docker compose up -d # uses docker-compose.yml
docker compose logs -fFirewall rules required on the VPS:
ufw allow 9000/tcp # control port (gateway dials in)
ufw allow 20000:30000/tcp # ephemeral user-facing port range# Copy binary
scp target/release/gateway-agent user@gateway:/usr/local/bin/
# Configure
cp gateway-agent/gateway-agent.env.example /etc/pamtunnel/gateway-agent.env
$EDITOR /etc/pamtunnel/gateway-agent.env # set VPS_HOST
# Run
export $(cat /etc/pamtunnel/gateway-agent.env | grep -v '^#' | tr -d '\r' | xargs)
RUST_LOG=info /usr/local/bin/gateway-agentcp gateway-agent/gateway-agent.env.example gateway-agent.env
$EDITOR gateway-agent.env # set VPS_HOST
docker compose -f docker-compose.gateway.yml up -d
docker compose -f docker-compose.gateway.yml logs -fThe gateway only needs outbound TCP to VPS_HOST:CONTROL_PORT and the ephemeral data port range. No inbound ports need to be opened on the LAN machine.
Both Dockerfiles produce statically-linked binaries (musl) via cargo-zigbuild and package them in a scratch image (~5 MB).
# Single architecture
docker build --platform linux/amd64 -f Dockerfile.vps-agent -t pamtunnel/vps-agent:latest .
docker build --platform linux/arm64 -f Dockerfile.gateway-agent -t pamtunnel/gateway-agent:latest .
# Multi-arch manifest (requires docker buildx)
docker buildx create --use --name pamtunnel-builder
docker buildx build --platform linux/amd64,linux/arm64 \
-f Dockerfile.vps-agent -t youruser/vps-agent:latest --push .
docker buildx build --platform linux/amd64,linux/arm64 \
-f Dockerfile.gateway-agent -t youruser/gateway-agent:latest --push .| Command | Description |
|---|---|
/rdp 192.168.1.23 |
Tunnel RDP (port 3389) from the LAN machine. Bot replies with a .rdp file. |
/rdp HOME-PC |
Same, using a hostname resolvable on the gateway's LAN. |
/ssh 192.168.1.22 |
Tunnel SSH (port 22). Bot replies with the ssh command. |
/help |
Show command list. |
| Variable | Required | Default | Description |
|---|---|---|---|
TELEGRAM_TOKEN |
✅ | — | Bot token from @BotFather |
ALLOWED_USERS |
✅ | — | Comma-separated Telegram user IDs |
VPS_HOST |
✅ | — | Public IP / hostname of the VPS (embedded in generated files) |
CONTROL_PORT |
9000 |
Port the gateway control listener binds on | |
PORT_RANGE_START |
20000 |
Start of ephemeral port range | |
PORT_RANGE_END |
30000 |
End of ephemeral port range | |
RUST_LOG |
info |
Log level (trace, debug, info, warn, error) |
| Variable | Required | Default | Description |
|---|---|---|---|
VPS_HOST |
✅ | — | Public IP / hostname of the VPS |
CONTROL_PORT |
9000 |
Must match CONTROL_PORT on the VPS |
|
KEEPALIVE_SECS |
30 |
Keepalive ping interval | |
RUST_LOG |
info |
Log level |
- Only Telegram user IDs listed in
ALLOWED_USERScan issue commands. - Target host is validated (alphanumeric +
-+., RFC-1123) before use — no shell injection possible. - Every session uses a cryptographically random 128-bit one-time token (CSPRNG via
rand). - Each session uses two freshly-bound random ports — never reused.
- Sessions time out after 120 seconds if neither the gateway nor the user connects.
- The gateway agent only dials outbound; no ports need to be open on the LAN machine.
- Control messages are length-prefixed with a 16 MiB size guard to prevent memory exhaustion.
- Binaries use
rustlsexclusively — no OpenSSL dependency.
pamtunnel/
├── Cargo.toml # workspace
├── Dockerfile.vps-agent # multi-arch Docker image (vps-agent)
├── Dockerfile.gateway-agent # multi-arch Docker image (gateway-agent)
├── docker-compose.yml # VPS deployment
├── docker-compose.gateway.yml # Gateway deployment
├── .github/
│ └── workflows/
│ └── release.yml # CI: test → cross-compile → GitHub Release
├── shared/ # protocol types + framing helpers
│ ├── Cargo.toml
│ └── src/lib.rs
├── vps-agent/ # Telegram bot + tunnel orchestrator
│ ├── Cargo.toml
│ ├── vps-agent.env.example
│ └── src/
│ ├── main.rs # entrypoint, bot dispatcher
│ ├── config.rs # env-var config
│ ├── session.rs # session store (DashMap)
│ ├── control.rs # gateway control channel listener
│ ├── tunnel.rs # per-session TCP proxy
│ ├── bot_handler.rs # /rdp and /ssh command logic
│ └── files.rs # .rdp file & SSH message generation
└── gateway-agent/ # reverse-tunnel client
├── Cargo.toml
├── gateway-agent.env.example
└── src/
├── main.rs # entrypoint, control channel loop
├── config.rs # env-var config
└── tunnel.rs # data tunnel (VPS data port ↔ LAN target)