https://github.com/xutianyi1999/agent-pair
Label-based TCP tunnel over WebSocket + yamux. Expose local services through a public relay — no public IP needed.
https://github.com/xutianyi1999/agent-pair
multiplexing relay reverse-tunnel rust tcp-tunnel websocket yamux
Last synced: 2 days ago
JSON representation
Label-based TCP tunnel over WebSocket + yamux. Expose local services through a public relay — no public IP needed.
- Host: GitHub
- URL: https://github.com/xutianyi1999/agent-pair
- Owner: xutianyi1999
- Created: 2026-06-30T06:44:41.000Z (8 days ago)
- Default Branch: main
- Last Pushed: 2026-06-30T17:47:50.000Z (7 days ago)
- Last Synced: 2026-06-30T19:24:26.666Z (7 days ago)
- Topics: multiplexing, relay, reverse-tunnel, rust, tcp-tunnel, websocket, yamux
- Language: Rust
- Size: 15.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
[中文](README.zh.md)
# agent-pair
**Label-based TCP tunnel over WebSocket + yamux.** Expose a local service through a relay so
multiple remote machines can reach it — no public IP needed.
---
## How it works
```
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ Bind machine │ │ Broker │ │ Forward machine(s) │
│ (no public IP) │ │ (public relay) │ │ (clients) │
└────────┬────────┘ └────────┬─────────┘ └───────────┬──────────┘
│ │ │
│── REGISTER "web" ────────→│ │
│ (WebSocket session) │ │
│ │←────── FORWARD "web" ───────│
│ │ (WebSocket session) │
│ bind(8080, "web") │ │ forward(9090, "web")
│ │ │ listen :9090
│ │ │ ↓ accept
│ │◄─── yamux stream ──────────│ ctrl.open_stream()
│← broker opens bind stream─┤ │
│ connect(:8080)→bridge │ │ client gets response
│ │ │
│ │←─── yamux stream ──────────│ Forward 2
│←── broker opens streams──┤ │
│ connect(:8080)→bridge │ │ all share bind
```
A **Broker** runs on a public server. A **bind** agent connects and registers a
label pointing at a local service. **Forward** agents connect with the same label
and get a local port — traffic is multiplexed over a single yamux session per
agent.
---
## CLI
The `agentd` binary wraps `AgentClient` for terminal usage.
```bash
# Broker (public relay) — only --server matters, no bind/forward needed
cargo run --bin agentd --features cli
# Service machine — register label "web" for local :8080
cargo run --bin agentd --features cli -- --bind 8080:web
# Remote machine — listen on :9090, tunnel to the "web" service
cargo run --bin agentd --features cli -- --forward 9090:web
# Combine bind and forward on the same connection
cargo run --bin agentd --features cli -- --bind 3000:api --forward 9090:web
```
| Option | Description |
|--------|-------------|
| `-s`, `--server ` | Broker WebSocket address (default `127.0.0.1:7799`) |
| `--bind ` | Register a label and bridge incoming streams to a local port (repeatable) |
| `--forward ` | Listen on a local port and tunnel to a registered label (repeatable) |
On disconnect the agent retries every 3 s.
---
## API
### Broker
```rust
use agent_pair::Broker;
Broker::listen("0.0.0.0:7799").await?;
```
### Agent — bind (service owner)
```rust
use agent_pair::AgentClient;
let agent = AgentClient::connect("relay:7799").await?;
// Register label "web" pointing at local :8080
agent.bind(8080, "web").await?;
```
### Agent — forward (remote consumer)
```rust
use agent_pair::AgentClient;
let agent = AgentClient::connect("relay:7799").await?;
// Listen on :9090, tunnel to the service registered as "web"
agent.forward(9090, "web").await?;
```
---
## Features
- **Labels** — one broker can route many different services by name
- **WebSocket transport** — all agent connections tunnel through WebSocket, compatible with standard HTTP proxies
- **Shared connection** — a single `AgentClient` can `bind` and `forward` on the same
connection, and can register multiple labels
- **Multiple forwards** — any number of forward agents can share one label
- **Resilient** — if the bind agent disconnects, the broker cleans up stale
entries; new bind agents can re-register seamlessly
- **Concurrent** — built on yamux multiplexing; tested with 100 concurrent
streams and 50 simultaneous 100 KB transfers
- **No public IP needed** — only the broker needs a public address
---
## Protocol
Frames are length-prefixed postcard-encoded messages over yamux streams:
| Type | Payload | Direction |
|------|---------|-----------|
| `Register { label }` | label string | Bind → Broker |
| `Data { label }` | label string | Forward → Broker → Bind |
Each yamux stream carries exactly one control frame (Register or Data). After
the control frame the stream is bridged to the target TCP connection.
---
## Rules
| Rule | Behaviour |
|------|-----------|
| Bind first | A forward stream with an unregistered label is dropped |
| One bind per label per broker | Duplicate `bind` returns an error |
| N forwards per label | Each forward is an independent TCP connection to the broker |
| Bind disconnects | Stale entries are cleaned up; forwards get errors |
| Labels are scoped per broker | The same label on different brokers is independent |
---
## Tests
```bash
cargo test
```
22 integration and unit tests covering bind/forward, concurrent streams, data
integrity, reconnection, backpressure, and error handling.