The network layer for AI agents. Reach your customers' on-prem data, show what you build, keep it running — through one relay that your machines only ever dial out to.
tunr.sh · Live demo · Docs · Dashboard · Discussions · Contributing
# The vendor sends one link:
$ tunr invite create --org finbot --customer "Acme"
https://app.tunr.sh/connect/tunr_inv_… # Acme's IT signs in, names a site, gets the install command
# Customer IT, once — inside their network, outbound HTTPS only:
$ tunr connect resource add erp --kind mssql --target 10.0.0.7:1433 --database TIGER --profile logo-tiger
$ tunr connect mssql-role --database TIGER --resource erp > mssql-role.sql # SELECT-only login, for the DBA
$ tunr connect resource verify erp # login can't write? which Logo tables exist?
# The vendor's agent — anywhere:
$ tunr access request acme/erp --ttl 2h --reason "Reconcile October invoices"
pending grt_… acme/erp (ro, 2h0m0s) # a person at Acme approves
$ tunr access acme/erp --port 15433
TUNR_QUERY_URL=http://127.0.0.1:15433/query # the connector runs each statement read-only
$ curl -s $TUNR_QUERY_URL -H "Authorization: Bearer $TUNR_QUERY_TOKEN" -d '{"sql":"DELETE FROM LG_001_CLCARD"}'
{"error":{"code":"blocked","message":"tunr bridge: DELETE is not allowed on a read-only bridge. …"}}
Or let the agent do it over MCP: tunr_describe_resource (what the ERP's
tables mean), tunr_request_access, tunr_query (rows as JSON).
One layer, three directions
The same rules hold in every direction: your machines and your customer's only open outbound connections to the relay, there is one CLI and one MCP server (16 tools) for all of it, and the relay is self-hostable. Agent Bridge is the part that carries the most guarantees — grants, read-only databases, audit — because it touches someone else's data.
Why
AI vendors increasingly sell agents that need data living inside their customers' networks — on-prem ERPs, ledgers, file servers. Today each vendor writes its own on-prem agent or asks for a VPN, and every deal waits weeks in a security review, because the customer can't see or limit what the agent touches.
tunr Bridge is the standard way across. The customer installs one connector that only connects out, exposes named resources, and approves each agent, resource and time window — with a reason they can read. The agent never sees a database password; databases are read-only in three layers; the stream is end-to-end encrypted to the customer's connector; every query lands in the customer's audit log; one click cuts everything. Once a customer has the connector for one vendor, the next vendor is a link and an approval away.
The bridge is one direction of a network layer an agent needs anyway. The same relay gives you a public link for what you're building (the free 3-second tunnel tunr started as), hosts the apps your agent builds from that data, and can host the agent itself — private, woken by cron or signed webhooks, with its identity issued automatically.
Install
# macOS / Linux — Homebrew
brew install tunr-dev/tap/tunr
# macOS / Linux — install script
curl -fsSL https://raw.githubusercontent.com/tunr-dev/tunr/main/install.sh | sh
# Go
go install github.com/tunr-dev/tunr/cmd/tunr@latest
# From source
git clone https://github.com/tunr-dev/tunr.git && cd tunr && make build
A single static binary. macOS, Linux and Windows, amd64 and arm64, no runtime
dependencies. Verify it with tunr doctor.
SDKs — for driving tunnels from code rather than the shell:
pip install tunr-sdk # Python
npm install @tunr/cli # Node.js
Agent Bridge — reach in
vendor's agent ──► tunr access ──(TLS 1.3, pinned)──────────────► connector ──► customer's database
(anywhere) 127.0.0.1:port ▲ relay sees ciphertext ▲ (outbound only) (read-only role)
What it guarantees, how, and what it doesn't: docs/bridge/security.md. Getting started: customer · vendor · protocol: docs/protocol/bridge.md. For the customer's IT team: English · Türkçe. For your coding agent: agent-prompt.md.
Status: preview. Live on tunr.sh and end-to-end tested (two docker suites in CI); expect sharp edges. PostgreSQL, SQL Server (query mode) and raw TCP resources today; MySQL later.
Tunnels — show out
Still free, still unlimited, still 3 seconds.
tunr share --port 3000
# 🚀 https://abc1x2y3.tunr.sh
HTTP/HTTPS with WebSocket (HMR works), plus experimental raw TCP, UDP and TLS tunnels — all multiplexed over one connection. Traffic is served from a single EU relay today; more regions are planned.
# Basic auth (user optional)
tunr share -p 8080 --password "secret"
tunr share -p 8080 --password "client:secret"
# Bearer token — Authorization: Bearer <token> or ?token=<token>
tunr share -p 3000 --auth-token "my-super-secret-key"
# IP whitelist (CIDR)
tunr share -p 3000 --allow-ip "203.0.113.0/24"
tunr share -p 3000 --allow-ip "10.0.0.0/8,172.16.0.0/12"
# Auto-expire — the tunnel closes itself
tunr share -p 3000 --ttl 1h30m
# QR code, for opening it on a phone
tunr share -p 3000 --qr
# Path routing — one public URL, several local ports
tunr share --route /=3000 --route /api=8080
# Region hint — accepted today, takes effect once more regions launch
# (tunr currently runs a single relay in the EU)
tunr share --port 3000 --region ams
# Header rewriting
tunr share -p 3000 --header-add "X-Debug: true"
tunr share -p 3000 --header-replace "Host: internal.local"
tunr share -p 3000 --header-remove "X-Powered-By"
# Forwarded headers — X-Forwarded-For (real client IP), X-Original-URL
tunr share -p 3000 --x-forwarded-for --original-url
# CORS preflight without touching your server
tunr share -p 3000 --cors-origin "https://myapp.com"
# Behind a corporate proxy
tunr share -p 3000 --proxy http://proxy:8080
# Custom domain (Pro)
tunr share -p 3000 --domain demo.client.com
tunr tcp --port 5432
tunr udp --port 53
tunr tls --port 8443 # a local TLS port over the same raw transport
Experimental, and narrower than the names suggest. The relay exposes a raw
tunnel only as a WebSocket stream at wss://<subdomain>.tunr.sh/tunnel/tcp
(binary frames carry the bytes). There is no raw TCP/UDP port on the relay yet,
so psql, ssh or a game client can't connect directly — you need a client
that speaks that WebSocket. tunr tls is not SNI passthrough: the relay
terminates TLS for *.tunr.sh. --allow-ip isn't enforced on raw tunnels, so
it returns an error rather than silently leaving the port open.
# Background daemon
tunr start --port 3000
tunr status
tunr stop
# Several tunnels from .tunr.json
tunr up
tunr down
# Install as a system service (systemd / launchd) — starts on boot
tunr service install --port 3000
tunr service status
tunr service uninstall
tunr open # dashboard at http://localhost:19842
tunr logs --follow # stream requests in the terminal
tunr replay <id> # re-send a captured request to your local server
Live request/response stream, headers, body, timing, one-click replay, export
as a curl command. Everything stays on your machine.
Global flags: --relay URL (point at your own relay), --verbose.
Client demos — show out, safely
Showing a client something that still runs on your laptop? Three flags make the free tunnel safe to hand to someone who isn't a developer.
tunr share -p 3000 --demo --freeze --inject-widget --qr
Deploy & host — stay up
tunr login # magic-link, token goes to your OS keychain
tunr deploy --name my-app # build + run; prints the live URL
tunr apps # list your apps
tunr apps logs my-app --follow # stream build + runtime logs
tunr apps delete my-app # remove it, free the subdomain
tunr deploy --name sprint --port 3000 --env DATABASE_URL=postgres://…
Status: preview. Deploy works end to end and is what the MCP tools drive. Role-based sharing, per-app SQLite and
tunr rollbackare not built yet — see Roadmap for what's real and what isn't.
Agent-native (MCP)
The CLI and the MCP server are two faces of the same control plane. Anything you can do in a terminal, your agent can do in a tool call.
Claude Code:
claude mcp add tunr -- tunr mcp
Claude Desktop (~/.claude/claude_desktop_config.json) / Cursor (.cursor/mcp.json):
{
"mcpServers": {
"tunr": { "command": "tunr", "args": ["mcp"] }
}
}
Tools
tunr_deploy and tunr_share are deliberately distinct, and the tool
descriptions say so: deploy means "host this, it should outlive my laptop",
share means "expose what's running on :3000 right now". Cloud tools need
tunr login first.
SDKs
from tunr import TunrClient, TunnelOptions
client = TunrClient()
tunnel = client.share(port=3000)
print(tunnel.public_url)
db_tunnel = client.tcp(port=5432)
dns_tunnel = client.udp(port=53)
tls_tunnel = client.tls(port=8443)
opts = TunnelOptions(
subdomain="myapp",
password="demo123",
allow_ips=["10.0.0.0/8"],
freeze=True,
inject_widget=True,
proxy="http://proxy:8080",
ttl="2h",
)
tunnel = client.share(port=8080, opts=opts)
requests = client.get_requests(tunnel.subdomain)
client.replay_request(tunnel.subdomain, requests[0]["id"], port=3000)
metrics = client.get_metrics() # Prometheus text
health = client.health_check() # {"status": "ok"}
tunnel.close()
import { TunrClient } from '@tunr/cli'
const client = new TunrClient()
const tunnel = await client.share(3000)
console.log(tunnel.publicUrl)
const dbTunnel = await client.tcp(5432)
const dnsTunnel = await client.udp(53)
const tlsTunnel = await client.tls(8443)
const appTunnel = await client.share(8080, {
subdomain: 'myapp',
password: 'demo123',
allowIps: ['10.0.0.0/8'],
freeze: true,
injectWidget: true,
proxy: 'http://proxy:8080',
ttl: '2h',
})
tunnel.on('ready', () => console.log('Tunnel live'))
tunnel.on('error', (err) => console.error(err))
tunnel.on('exit', () => console.log('Tunnel closed'))
const requests = await client.getRequests('myapp')
await client.replayRequest('myapp', requests[0].id, 3000)
await tunnel.close()
Configuration (.tunr.json)
tunr config init
{
"$schema": "https://tunr.sh/schema/.tunr.schema.json",
"port": 3000,
"inspectorEnabled": true,
"dashboardPort": 19842,
"mcp": { "enabled": true }
}
Architecture
┌── [WebSocket, dialed out] ◀── tunr connect ──▶ customer DB reach in (bridge)
agent / browser ──▶ relay ─┼── [WebSocket, dialed out] ◀── tunr share ───▶ localhost:PORT show out (tunnel)
└── control plane ──▶ tunr-runner ──▶ gVisor sandbox stay up (deploy)
│
Postgres + route cache
Bridge. The customer's connector dials out to the relay over one
WebSocket and announces resource names only. An agent's tunr access opens a
stream per connection; the relay checks the grant, and the connector checks
it again (Ed25519, key pinned at install) before it reads anything. Streams
are TLS 1.3 end to end between tunr access and the connector. Details:
docs/protocol/bridge.md.
Tunnels. The CLI starts a local proxy with an embedded inspector and opens
one WebSocket to the relay. The relay issues a *.tunr.sh subdomain and
terminates HTTPS. HTTP, WebSocket, TCP, UDP and TLS all multiplex over that
single connection — a typed message discriminator routes them.
Cloud. tunr deploy uploads a tarball to the control plane, which hands it
to tunr-runner. The runner builds with Nixpacks and runs the result in a
gVisor sandbox with cgroup quotas. The relay routes the subdomain straight at
the container, waking it if it's asleep.
Scale to zero. Apps move HOT → WARM → STOPPED on an idle timer. WARM is a
cgroup memory reclaim followed by a pause, which cuts real memory cost by ~55%
while keeping wake latency around 150 ms. Health-check probes are answered at
the edge so a monitored app can still fall asleep; search crawlers get the real
page but can't keep the app awake.
Observability. Prometheus metrics at /metrics, K8s probes at /healthz
and /readyz on the inspector port.
Self-hosting. docker-compose.yml runs the tunnel stack (relay + Caddy +
Postgres); docker-compose.runner.yml adds the cloud runner. Point the CLI at
it with --relay https://tunnel.yourcompany.com or TUNR_RELAY_URL. See
docs/SELF_HOSTING.md.
Security
- Agent Bridge: default deny, read-only databases in three layers, no credentials for agents, end-to-end TLS to the connector, per-org isolation, full audit — details and limits in docs/bridge/security.md
- Auth tokens live in the OS keychain, never in a dotfile
- All relay traffic over TLS 1.3. HTTP tunnels terminate TLS at the relay, like any hosted tunnel; self-host the relay (
--relay) if that matters to you - Cloud apps run under gVisor, not bare containers
.envfiles are excluded from deploy uploads by default- The CLI ships no telemetry, no analytics, no phone-home
- Supply chain:
go mod verify+govulncheckin CI, cosign-signed checksums
Found a vulnerability? Don't open a public issue — see SECURITY.md.
Roadmap
Honest status. "Preview" means it works but the edges are sharp; "planned" means there is no code yet.
Comparing tunr's tunnel against ngrok, Cloudflare Tunnel, LocalXpose and localtunnel: docs/compare-tunnels.md.
Contributing
Contributions are welcome — read CONTRIBUTING.md first.
Good places to start are issues labelled
good first issue
and help wanted.
make check # vet + lint + test + govulncheck
make pre-push # the above, plus a build
The relay is a separate Go module and isn't covered by the Makefile:
cd relay && go build ./cmd/server && go test ./...
Community & support
Please follow our Code of Conduct.
License
This repository is dual-licensed by directory:
Running the whole stack yourself, for yourself or inside your company, is allowed under both. See NOTICE for the exact boundary.
Built with 💜 in Go