{"id":51446463,"url":"https://github.com/xutianyi1999/agent-pair","last_synced_at":"2026-07-05T16:30:49.740Z","repository":{"id":368474970,"uuid":"1284742232","full_name":"xutianyi1999/agent-pair","owner":"xutianyi1999","description":"Label-based TCP tunnel over WebSocket + yamux. Expose local services through a public relay — no public IP needed.","archived":false,"fork":false,"pushed_at":"2026-06-30T17:47:50.000Z","size":16,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-30T19:24:26.666Z","etag":null,"topics":["multiplexing","relay","reverse-tunnel","rust","tcp-tunnel","websocket","yamux"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/xutianyi1999.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-30T06:44:41.000Z","updated_at":"2026-06-30T17:47:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/xutianyi1999/agent-pair","commit_stats":null,"previous_names":["xutianyi1999/agent-pair"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/xutianyi1999/agent-pair","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xutianyi1999%2Fagent-pair","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xutianyi1999%2Fagent-pair/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xutianyi1999%2Fagent-pair/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xutianyi1999%2Fagent-pair/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/xutianyi1999","download_url":"https://codeload.github.com/xutianyi1999/agent-pair/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xutianyi1999%2Fagent-pair/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35162070,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["multiplexing","relay","reverse-tunnel","rust","tcp-tunnel","websocket","yamux"],"created_at":"2026-07-05T16:30:49.425Z","updated_at":"2026-07-05T16:30:49.732Z","avatar_url":"https://github.com/xutianyi1999.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"[中文](README.zh.md)\n\n# agent-pair\n\n**Label-based TCP tunnel over WebSocket + yamux.** Expose a local service through a relay so\nmultiple remote machines can reach it — no public IP needed.\n\n---\n\n## How it works\n\n```\n┌─────────────────┐         ┌──────────────────┐        ┌──────────────────────┐\n│  Bind machine   │         │     Broker       │        │  Forward machine(s)  │\n│  (no public IP) │         │   (public relay)  │        │       (clients)      │\n└────────┬────────┘         └────────┬─────────┘        └───────────┬──────────┘\n         │                           │                              │\n         │── REGISTER \"web\" ────────→│                              │\n         │    (WebSocket session)    │                              │\n         │                           │←────── FORWARD \"web\" ───────│\n         │                           │    (WebSocket session)       │\n         │ bind(8080, \"web\")         │                              │ forward(9090, \"web\")\n         │                           │                              │ listen :9090\n         │                           │                              │   ↓ accept\n         │                           │◄─── yamux stream ──────────│   ctrl.open_stream()\n         │← broker opens bind stream─┤                            │\n         │ connect(:8080)→bridge     │                             │ client gets response\n         │                           │                              │\n         │                           │←─── yamux stream ──────────│ Forward 2\n         │←── broker opens streams──┤                            │\n         │ connect(:8080)→bridge     │                             │ all share bind\n```\n\nA **Broker** runs on a public server. A **bind** agent connects and registers a\nlabel pointing at a local service. **Forward** agents connect with the same label\nand get a local port — traffic is multiplexed over a single yamux session per\nagent.\n\n---\n\n## CLI\n\nThe `agentd` binary wraps `AgentClient` for terminal usage.\n\n```bash\n# Broker (public relay) — only --server matters, no bind/forward needed\ncargo run --bin agentd --features cli\n\n# Service machine — register label \"web\" for local :8080\ncargo run --bin agentd --features cli -- --bind 8080:web\n\n# Remote machine — listen on :9090, tunnel to the \"web\" service\ncargo run --bin agentd --features cli -- --forward 9090:web\n\n# Combine bind and forward on the same connection\ncargo run --bin agentd --features cli -- --bind 3000:api --forward 9090:web\n```\n\n| Option | Description |\n|--------|-------------|\n| `-s`, `--server \u003cADDR\u003e` | Broker WebSocket address (default `127.0.0.1:7799`) |\n| `--bind \u003cPORT:LABEL\u003e` | Register a label and bridge incoming streams to a local port (repeatable) |\n| `--forward \u003cPORT:LABEL\u003e` | Listen on a local port and tunnel to a registered label (repeatable) |\n\nOn disconnect the agent retries every 3 s.\n\n---\n\n## API\n\n### Broker\n\n```rust\nuse agent_pair::Broker;\n\nBroker::listen(\"0.0.0.0:7799\").await?;\n```\n\n### Agent — bind (service owner)\n\n```rust\nuse agent_pair::AgentClient;\n\nlet agent = AgentClient::connect(\"relay:7799\").await?;\n\n// Register label \"web\" pointing at local :8080\nagent.bind(8080, \"web\").await?;\n```\n\n### Agent — forward (remote consumer)\n\n```rust\nuse agent_pair::AgentClient;\n\nlet agent = AgentClient::connect(\"relay:7799\").await?;\n\n// Listen on :9090, tunnel to the service registered as \"web\"\nagent.forward(9090, \"web\").await?;\n```\n\n---\n\n## Features\n\n- **Labels** — one broker can route many different services by name\n- **WebSocket transport** — all agent connections tunnel through WebSocket, compatible with standard HTTP proxies\n- **Shared connection** — a single `AgentClient` can `bind` and `forward` on the same\n  connection, and can register multiple labels\n- **Multiple forwards** — any number of forward agents can share one label\n- **Resilient** — if the bind agent disconnects, the broker cleans up stale\n  entries; new bind agents can re-register seamlessly\n- **Concurrent** — built on yamux multiplexing; tested with 100 concurrent\n  streams and 50 simultaneous 100 KB transfers\n- **No public IP needed** — only the broker needs a public address\n\n---\n\n## Protocol\n\nFrames are length-prefixed postcard-encoded messages over yamux streams:\n\n| Type | Payload | Direction |\n|------|---------|-----------|\n| `Register { label }` | label string | Bind → Broker |\n| `Data { label }` | label string | Forward → Broker → Bind |\n\nEach yamux stream carries exactly one control frame (Register or Data). After\nthe control frame the stream is bridged to the target TCP connection.\n\n---\n\n## Rules\n\n| Rule | Behaviour |\n|------|-----------|\n| Bind first | A forward stream with an unregistered label is dropped |\n| One bind per label per broker | Duplicate `bind` returns an error |\n| N forwards per label | Each forward is an independent TCP connection to the broker |\n| Bind disconnects | Stale entries are cleaned up; forwards get errors |\n| Labels are scoped per broker | The same label on different brokers is independent |\n\n---\n\n## Tests\n\n```bash\ncargo test\n```\n\n22 integration and unit tests covering bind/forward, concurrent streams, data\nintegrity, reconnection, backpressure, and error handling.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxutianyi1999%2Fagent-pair","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxutianyi1999%2Fagent-pair","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxutianyi1999%2Fagent-pair/lists"}