HTTP API per the recorded contract, bridge WebSocket, SNI relay with pass-through tunnel TLS, local CA issuance, JSON store. Verified end-to-end with the official opentunnel CLI.
85 lines
3.4 KiB
Markdown
85 lines
3.4 KiB
Markdown
# tunnel
|
|
|
|
A single-node, self-hosted [OpenTunnel](https://github.com/anomalyco/opentunnel)-compatible
|
|
server in Go: blind TLS tunnels where the server routes by SNI and never sees
|
|
plaintext or private keys. It speaks the exact wire protocol from
|
|
`docs/protocol.md`, so the official `opentunnel` CLI and SDK work against it
|
|
unchanged.
|
|
|
|
Status: **M1** — core data path works end-to-end with the official CLI,
|
|
using the built-in local CA for certificates.
|
|
|
|
## Quick start (local development)
|
|
|
|
```bash
|
|
go build -o .local/tunneld ./cmd/tunneld
|
|
.local/tunneld -domain localhost
|
|
# tls: listening on 127.0.0.1:8443, http: listening on 127.0.0.1:8080 (mode serve)
|
|
```
|
|
|
|
In another terminal, with the official CLI (`npm i -g opentunnel` or
|
|
`brew install anomalyco/tap/opentunnel`):
|
|
|
|
```bash
|
|
python3 -m http.server 4096 & # something to expose
|
|
export OPENTUNNEL_API=http://localhost:8080
|
|
opentunnel route add 4096 --name api
|
|
# https://api.<id>.localhost
|
|
```
|
|
|
|
The tunnel's TLS is terminated by the CLI with a certificate from the server's
|
|
local CA; the CA certificate is written next to the data file (`ca.pem`). Test
|
|
the visitor side with SNI (macOS curl skips SNI on `*.localhost` names, so use
|
|
`openssl s_client` or Homebrew curl):
|
|
|
|
```bash
|
|
printf 'GET / HTTP/1.1\r\nHost: api.<id>.localhost\r\nConnection: close\r\n\r\n' \
|
|
| openssl s_client -connect 127.0.0.1:8443 -servername api.<id>.localhost \
|
|
-CAfile ~/.local/share/tunneld/ca.pem -quiet
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Every flag also reads an environment variable (`tunneld -h`):
|
|
|
|
| Flag | Env | Default | |
|
|
| --- | --- | --- | --- |
|
|
| `-domain` | `OPENTUNNEL_DOMAIN` | `localhost` | API domain; tunnels are `<id>.<domain>` |
|
|
| `-tls-listen` | `TLS_LISTEN` | `127.0.0.1:8443` | public SNI router |
|
|
| `-http-listen` | `HTTP_LISTEN` | `127.0.0.1:8080` | `off` disables |
|
|
| `-http-mode` | `HTTP_MODE` | `serve` | `serve` = plain-HTTP app (dev); `redirect` = 301 to the domain |
|
|
| `-data` | `TUNNELD_DATA` | `$XDG_DATA_HOME/tunneld/data.json` | tunnel store (0600) |
|
|
| `-ca-file` | `LOCAL_CA_FILE` | `<data dir>/ca.pem` | where the local CA cert is written |
|
|
|
|
## What works (M1)
|
|
|
|
- The full HTTP API contract (paths, status codes, bodies, key order) as
|
|
recorded in the reference's `worker-contract.json`
|
|
- Provisioning: tunnel creation, CSR bind with the reference's hostname rules,
|
|
certificates issued at once by the built-in local CA
|
|
- The bridge WebSocket: attach/attached, route claiming with conflicts,
|
|
heartbeats, idle retirement, `open`/`end`/`reset`, 32 KiB data frames
|
|
- SNI routing on the TLS listener: the domain is terminated (API), tunnel
|
|
hostnames are passed through still encrypted
|
|
- Persistence: JSON store with the CA, server certificate, and tunnels
|
|
- Graceful shutdown: `drain` + close to every bridge
|
|
|
|
## Not yet (later milestones)
|
|
|
|
- Real ACME issuance (DNS-01) and renewal jobs — currently local CA only
|
|
- Rate limits, metrics, admin endpoints
|
|
- Multi-region mesh, inbound PROXY protocol on the TLS listener
|
|
- 60 s half-close timeout, per-connection flow control, control messages
|
|
cutting ahead of queued data (single ordered outbound queue today)
|
|
- Go client/CLI — for now use the official `opentunnel` CLI
|
|
|
|
## Development
|
|
|
|
```bash
|
|
go build ./...
|
|
go test ./... # includes the shared spec vectors from .reference/opentunnel
|
|
```
|
|
|
|
The reference implementation lives in `.reference/` (untracked); see
|
|
`.reference/opentunnel.md` for what we take from it and what we skip.
|