Skip to content

Repository files navigation

🌀 holt

Reverse HTTP tunnels for services that can only dial out.

Go Reference Go Report Card License: MIT Status: alpha

A holt is an otter's den: a burrow in the riverbank, reachable only through the underwater tunnel its owner dug. Same idea here. A peer that cannot accept inbound connections (NAT, locked-down container, field device) dials out to a hub, then serves an ordinary http.Handler back through the connection it opened. The hub gets an http.RoundTripper per peer, and presence for free. No listener, no inbound port, nothing published on the peer.

holt is a Go library first; the holt CLI is one opinionated packaging of it.

⚠️ Alpha, extracted from openotters, where it is the daemon-to-agent channel. The wire protocol may still change.

The library

Two constructors, both at the module root:

// The peer: attaches to the hub, serves the handler through the
// tunnel, redials with backoff. Cancel ctx to stop.
c := holt.NewClient("wss://holt.example.com", myHandler,
	holt.WithBearerToken(token))
err := c.Run(ctx)
// The hub: a tunnel listener peers attach to, an optional proxy to
// reach them from outside.
srv := holt.NewServer(
	holt.WithTunnel(holt.NewTunnel(":7200", holt.WithAuthBearer(peerForToken))),
	holt.WithProxy(holt.NewProxy(":7202")),
)
go srv.Run(ctx)

// Anywhere in the hub process, a peer is an ordinary HTTP backend:
client := &http.Client{Transport: srv.Registry().RoundTripper(peerID)}

Bring your own auth, middleware, listeners and storage — everything the CLI adds (JWT identity, SQLite/PostgreSQL state, the console) is built on this surface. See Library and How it works.

The CLI

One binary for hub, peer, and operations (all install methods):

brew install openotters/tap/holt   # or: go install github.com/openotters/holt/cmd/holt@latest

holt hub --ui &                         # hub + web console (127.0.0.1:7201)
holt expose localhost:3000 --peer web   # enrolls itself, serves the tunnel
curl -H 'x-tunnel-peer: web' http://127.0.0.1:7202/

Peers authenticate with a JWT and attach over a WebSocket, so the tunnel passes through Cloudflare, ingresses and access proxies. TLS is your edge's job: advertise its wss:// URL. With a domain, each peer gets its own hostname — for a browser, a webhook, an OAuth callback:

holt hub --advertise-addr wss://holt.example.com \
  --proxy-routing both --proxy-domain example.com
holt expose localhost:3000 --peer checkout
# https://checkout.example.com/ now reaches the service

Operate with holt ls, holt kill web, holt block web. On Kubernetes, helm install holt oci://ghcr.io/openotters/charts/holt; several hubs share one PostgreSQL for presence, denylist and signing identity (Kubernetes).

The console

holt web console

holt hub --ui serves a web console: live tunnels, per-peer traffic with payloads (any request is one click from a curl that replays it), and capture endpoints — throwaway addresses that accept any call and show it live. Point a webhook or an OAuth redirect at one and inspect what arrives without exposing a real service. See Web console.

Prometheus metrics and a Grafana dashboard come with it (Observability).

Where holt fits

frp, ngrok and inlets do more, at a bigger scale: TCP/UDP, load balancing, teams, hosted service. holt is HTTP(S) and gRPC through one hub on your own infra — for embedding tunnels in a Go program, and for the simple case where your traffic should stay yours.

Documentation

Full docs live in docs/:

License

MIT

About

🌀 Reverse HTTP tunnels for services that can only dial out. WebSocket carrier, JWT auth, a hub CLI, and a web console with live traffic capture.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages