M1: single-node OpenTunnel-compatible server

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.
This commit is contained in:
2026-10-25 11:52:45 +08:00
commit 682a341010
15 changed files with 2511 additions and 0 deletions
+341
View File
@@ -0,0 +1,341 @@
package server
import (
"encoding/json"
"errors"
"io"
"mime"
"net/http"
"strings"
)
// API is the HTTP control plane. Its answers match the recorded contract in
// worker-contract.json: paths, status codes, bodies, and JSON key order.
type API struct {
Store *Store
Hub *Hub
Domain string
}
func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// The reference tolerates duplicate slashes and a trailing slash.
path := "/" + strings.Join(strings.FieldsFunc(r.URL.Path, func(c rune) bool { return c == '/' }), "/")
switch {
case path == "/health" && r.Method == http.MethodGet:
w.Header().Set("Content-Type", "application/json;charset=utf-8")
w.Write([]byte(`{"ok":true}`))
case path == "/openapi.json" && r.Method == http.MethodGet:
writeJSON(w, http.StatusOK, json.RawMessage(openapiStub))
case path == "/api/tunnel" && r.Method == http.MethodPost:
a.createTunnel(w, r)
default:
a.tunnelRoutes(w, r, strings.Split(strings.TrimPrefix(path, "/"), "/"))
}
}
// tunnelRoutes routes /api/tunnel/{id}[/certificate|connect].
func (a *API) tunnelRoutes(w http.ResponseWriter, r *http.Request, parts []string) {
if len(parts) < 2 || parts[0] != "api" || parts[1] != "tunnel" {
notFoundEmpty(w)
return
}
switch len(parts) {
case 3:
id := parts[2]
switch r.Method {
case http.MethodGet, http.MethodHead:
a.getTunnel(w, r, id)
case http.MethodDelete:
a.deleteTunnel(w, r, id)
default:
notFoundEmpty(w)
}
case 4:
id := parts[2]
switch parts[3] {
case "certificate":
switch r.Method {
case http.MethodPost:
a.bindCertificate(w, r, id)
case http.MethodGet, http.MethodHead:
a.getCertificate(w, r, id)
default:
notFoundEmpty(w)
}
case "connect":
if r.Method != http.MethodGet {
notFoundEmpty(w)
return
}
a.connect(w, r, id)
default:
notFoundEmpty(w)
}
default:
notFoundEmpty(w)
}
}
// ---- POST /api/tunnel
func (a *API) createTunnel(w http.ResponseWriter, r *http.Request) {
body, ok := readJSONBody(w, r)
if !ok {
return
}
if name, present := body["name"]; present && name != nil {
if _, isString := name.(string); !isString {
w.WriteHeader(http.StatusBadRequest) // wrong type: 400, empty body
return
}
writeJSON(w, http.StatusBadRequest, errInvalidRequest{
Tag: "InvalidRequestError",
Message: "Custom tunnel names are not supported; omit name to get a random hostname",
})
return
}
tunnel, token, err := a.Store.createTunnel(a.Domain)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, errInvalidRequest{
Tag: "ServiceUnavailableError", Message: "storage unavailable",
})
return
}
writeJSON(w, http.StatusCreated, createResponse{
Tunnel: TunnelInfo{ID: tunnel.ID, Hostname: tunnel.Hostname, State: "offline"},
Token: token,
})
}
// ---- GET /api/tunnel/{id}
func (a *API) getTunnel(w http.ResponseWriter, r *http.Request, id string) {
tunnel, ok := a.lookupTunnel(w, id)
if !ok || !a.authorized(w, r, tunnel) {
return
}
info := TunnelInfo{ID: tunnel.ID, Hostname: tunnel.Hostname, State: "offline"}
if a.Hub.online(tunnel.ID) {
info.State = "online"
}
if tunnel.Certificate != nil {
info.CertificateID = tunnel.Certificate.ID
}
writeJSON(w, http.StatusOK, info)
}
// ---- DELETE /api/tunnel/{id}
func (a *API) deleteTunnel(w http.ResponseWriter, r *http.Request, id string) {
tunnel, ok := a.lookupTunnel(w, id)
if !ok || !a.authorized(w, r, tunnel) {
return
}
a.Store.remove(id)
a.Hub.closeTunnel(id)
w.WriteHeader(http.StatusNoContent)
}
// ---- POST /api/tunnel/{id}/certificate
func (a *API) bindCertificate(w http.ResponseWriter, r *http.Request, id string) {
body, ok := readJSONBody(w, r)
if !ok {
return
}
tunnel, ok := a.lookupTunnel(w, id)
if !ok || !a.authorized(w, r, tunnel) {
return
}
csrValue, present := body["csr"]
if !present {
w.WriteHeader(http.StatusBadRequest)
return
}
csr, isString := csrValue.(string)
if !isString {
w.WriteHeader(http.StatusBadRequest)
return
}
// Binding the CSR the tunnel already has answers with its current
// certificate; a different one re-issues (with the local CA, at once).
if tunnel.Certificate != nil && tunnel.Certificate.CSR == csr {
a.writeCertificate(w, http.StatusAccepted, tunnel.Certificate)
return
}
ca, err := a.Store.ca()
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, errInvalidRequest{
Tag: "ServiceUnavailableError", Message: "storage unavailable",
})
return
}
certPEM, chainPEM, expiry, err := ca.signCSR(csr, tunnel.Hostname)
if err != nil {
var hostErr errCSRHostname
if errors.As(err, &hostErr) {
writeJSON(w, http.StatusBadRequest, errInvalidHostname{
Tag: "InvalidHostnameError",
Provided: hostErr.provided,
Expected: hostErr.expected,
Message: "CSR hostname does not match tunnel hostname",
})
return
}
writeJSON(w, http.StatusBadRequest, errInvalidRequest{
Tag: "InvalidRequestError", Message: "Failed to parse CSR",
})
return
}
cert := storedCert{
ID: certID(),
CSR: csr,
Certificate: string(certPEM),
Chain: string(chainPEM),
Expiry: expiryString(expiry),
}
if err := a.Store.bindCertificate(id, cert); err != nil {
notFoundEmpty(w)
return
}
// The contract's bind response is 202 with state "issuing"; with the local
// CA the very next GET already answers "ready".
writeJSON(w, http.StatusAccepted, CertificateInfo{ID: cert.ID, State: CertState{Type: "issuing"}})
}
// ---- GET /api/tunnel/{id}/certificate
func (a *API) getCertificate(w http.ResponseWriter, r *http.Request, id string) {
tunnel, ok := a.lookupTunnel(w, id)
if !ok || !a.authorized(w, r, tunnel) {
return
}
if tunnel.Certificate == nil {
writeJSON(w, http.StatusNotFound, errTaggedID{
Tag: "CertificateNotFoundError", TunnelID: id, Message: "Certificate not found",
})
return
}
a.writeCertificate(w, http.StatusOK, tunnel.Certificate)
}
func (a *API) writeCertificate(w http.ResponseWriter, status int, cert *storedCert) {
writeJSON(w, status, CertificateInfo{
ID: cert.ID,
State: CertState{
Type: "ready",
Certificate: cert.Certificate,
Chain: cert.Chain,
Expiry: cert.Expiry,
},
})
}
// ---- GET /api/tunnel/{id}/connect
func (a *API) connect(w http.ResponseWriter, r *http.Request, id string) {
tunnel, ok := a.Store.get(id)
if !ok {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Tunnel not found"))
return
}
if !upgradeRequested(r) {
w.WriteHeader(http.StatusUpgradeRequired)
w.Write([]byte("Expected WebSocket upgrade"))
return
}
if !subprotocolOffered(r) {
w.WriteHeader(http.StatusUpgradeRequired)
w.Write([]byte("Expected opentunnel WebSocket subprotocol"))
return
}
a.Hub.attach(w, r, tunnel)
}
// ---- helpers
// lookupTunnel answers 404 TunnelNotFoundError when id is unknown. Existence
// is checked before authorization, as the contract records.
func (a *API) lookupTunnel(w http.ResponseWriter, id string) (storedTunnel, bool) {
tunnel, ok := a.Store.get(id)
if !ok {
writeJSON(w, http.StatusNotFound, errTaggedID{
Tag: "TunnelNotFoundError", TunnelID: id, Message: "Tunnel not found",
})
return tunnel, false
}
return tunnel, true
}
// authorized answers 401 UnauthorizedError unless the request carries the
// tunnel's bearer token (the scheme is case-insensitive).
func (a *API) authorized(w http.ResponseWriter, r *http.Request, tunnel storedTunnel) bool {
scheme, token, _ := strings.Cut(r.Header.Get("Authorization"), " ")
if strings.EqualFold(scheme, "bearer") && token != "" && tokenMatches(token, tunnel.TokenHash) {
return true
}
writeJSON(w, http.StatusUnauthorized, errUnauthorized{
Tag: "UnauthorizedError", Message: "Invalid bearer token",
})
return false
}
// readJSONBody enforces the contract's body rules: a non-JSON content type is
// 415, a missing, empty, or malformed body is 400 with an empty body.
func readJSONBody(w http.ResponseWriter, r *http.Request) (map[string]any, bool) {
if ct := r.Header.Get("Content-Type"); ct != "" {
mediaType, _, err := mime.ParseMediaType(ct)
if err != nil || !strings.EqualFold(mediaType, "application/json") {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusUnsupportedMediaType)
w.Write([]byte("Unsupported content-type: " + ct))
return nil, false
}
}
raw, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil || len(raw) == 0 {
w.WriteHeader(http.StatusBadRequest)
return nil, false
}
var body map[string]any
if err := json.Unmarshal(raw, &body); err != nil {
w.WriteHeader(http.StatusBadRequest)
return nil, false
}
return body, true
}
func writeJSON(w http.ResponseWriter, status int, body any) {
raw, err := json.Marshal(body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(raw)
}
// notFoundEmpty is the 404 with no body the contract records for unknown
// paths and methods.
func notFoundEmpty(w http.ResponseWriter) {
w.WriteHeader(http.StatusNotFound)
}
const openapiStub = `{"openapi":"3.1.0","info":{"title":"OpenTunnel API","version":"0.1.0","description":"Create and manage blind TLS tunnels."},"paths":{"/api/tunnel":{"post":{"responses":{"201":{"description":"Success"}}}},"/api/tunnel/{id}":{"get":{"responses":{"200":{"description":"Tunnel/Info"}}},"delete":{"responses":{"204":{"description":"Deleted"}}}},"/api/tunnel/{id}/certificate":{"post":{"responses":{"202":{"description":"Accepted"}}},"get":{"responses":{"200":{"description":"Certificate"}}}},"/api/tunnel/{id}/connect":{"get":{"responses":{"101":{"description":"WebSocket upgrade"}}}}}}`
// redirectHandler is HTTP_MODE=redirect: /health answers, everything else
// permanently redirects to the API domain.
func RedirectHandler(domain string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/health" {
w.Header().Set("Content-Type", "application/json;charset=utf-8")
w.Write([]byte(`{"ok":true}`))
return
}
target := "https://" + domain + r.URL.RequestURI()
http.Redirect(w, r, target, http.StatusMovedPermanently)
})
}
+276
View File
@@ -0,0 +1,276 @@
package server
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// testAPI returns an API against an in-memory store, on the domain
// "opentunnel.test" like the recorded contract.
func testAPI(t *testing.T) (*API, *httptest.Server) {
t.Helper()
store, err := OpenStore("")
if err != nil {
t.Fatal(err)
}
api := &API{Store: store, Hub: NewHub(store), Domain: "opentunnel.test"}
return api, httptest.NewServer(api)
}
type createdTunnel struct {
ID string
Hostname string
Token string
}
func createTunnel(t *testing.T, srv *httptest.Server) createdTunnel {
t.Helper()
resp, err := http.Post(srv.URL+"/api/tunnel", "application/json", strings.NewReader("{}"))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
t.Fatalf("create: status %d", resp.StatusCode)
}
var body struct {
Tunnel TunnelInfo `json:"tunnel"`
Token string `json:"token"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if len(body.Tunnel.ID) != 12 || body.Tunnel.Hostname != body.Tunnel.ID+".opentunnel.test" {
t.Fatalf("create: odd tunnel %+v", body.Tunnel)
}
if body.Tunnel.State != "offline" {
t.Fatalf("create: state %q", body.Tunnel.State)
}
if !strings.HasPrefix(body.Token, "rly_") {
t.Fatalf("create: token %q", body.Token)
}
return createdTunnel{ID: body.Tunnel.ID, Hostname: body.Tunnel.Hostname, Token: body.Token}
}
func request(t *testing.T, method, url, token, contentType, body string) *http.Response {
t.Helper()
var reader io.Reader
if body != "" {
reader = strings.NewReader(body)
}
req, err := http.NewRequest(method, url, reader)
if err != nil {
t.Fatal(err)
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
return resp
}
func bodyString(t *testing.T, resp *http.Response) string {
t.Helper()
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
return string(raw)
}
// makeCSR builds a PEM PKCS#10 CSR for cn with the given SANs, as the CLI does.
func makeCSR(t *testing.T, cn string, sans ...string) string {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
der, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
Subject: pkix.Name{CommonName: cn},
DNSNames: sans,
}, key)
if err != nil {
t.Fatal(err)
}
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: der}))
}
func TestAPIContract(t *testing.T) {
_, srv := testAPI(t)
defer srv.Close()
base := srv.URL
// create, and the malformed variants the contract pins
resp := request(t, "POST", base+"/api/tunnel", "", "application/json", "{}")
if resp.StatusCode != 201 {
t.Fatalf("create: %d", resp.StatusCode)
}
resp.Body.Close()
resp = request(t, "POST", base+"/api/tunnel", "", "application/json", `{"name":"x"}`)
if got := bodyString(t, resp); resp.StatusCode != 400 ||
got != `{"_tag":"InvalidRequestError","message":"Custom tunnel names are not supported; omit name to get a random hostname"}` {
t.Errorf("create with name: %d %q", resp.StatusCode, got)
}
resp = request(t, "POST", base+"/api/tunnel", "", "application/json", "{")
if got := bodyString(t, resp); resp.StatusCode != 400 || got != "" {
t.Errorf("create with invalid json: %d %q", resp.StatusCode, got)
}
resp = request(t, "POST", base+"/api/tunnel", "", "text/plain", "{}")
if got := bodyString(t, resp); resp.StatusCode != 415 || got != "Unsupported content-type: text/plain" {
t.Errorf("create with text body: %d %q", resp.StatusCode, got)
}
resp = request(t, "POST", base+"/api/tunnel", "", "application/json", `{"name":null}`)
if resp.StatusCode != 201 {
t.Errorf("create with null name: %d", resp.StatusCode)
}
resp.Body.Close()
tunnel := createTunnel(t, srv)
// get: auth behavior and the unknown-tunnel 404
resp = request(t, "GET", base+"/api/tunnel/"+tunnel.ID, "", "", "")
if got := bodyString(t, resp); resp.StatusCode != 401 ||
got != `{"_tag":"UnauthorizedError","message":"Invalid bearer token"}` {
t.Errorf("get without auth: %d %q", resp.StatusCode, got)
}
resp = request(t, "GET", base+"/api/tunnel/zzzzzzzzzzzz", "", "", "")
if got := bodyString(t, resp); resp.StatusCode != 404 ||
got != `{"_tag":"TunnelNotFoundError","tunnelID":"zzzzzzzzzzzz","message":"Tunnel not found"}` {
t.Errorf("get unknown: %d %q", resp.StatusCode, got)
}
resp = request(t, "GET", base+"/api/tunnel/"+tunnel.ID, tunnel.Token, "", "")
want := `{"id":"` + tunnel.ID + `","hostname":"` + tunnel.Hostname + `","state":"offline"}`
if got := bodyString(t, resp); resp.StatusCode != 200 || got != want {
t.Errorf("get: %d %q, want %q", resp.StatusCode, got, want)
}
// lowercase bearer, double slash, trailing slash
req, _ := http.NewRequest("GET", base+"/api//tunnel/"+tunnel.ID+"/", nil)
req.Header.Set("Authorization", "bearer "+tunnel.Token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if got := bodyString(t, resp); resp.StatusCode != 200 || got != want {
t.Errorf("get double slash: %d %q", resp.StatusCode, got)
}
// certificate flow
resp = request(t, "GET", base+"/api/tunnel/"+tunnel.ID+"/certificate", tunnel.Token, "", "")
if got := bodyString(t, resp); resp.StatusCode != 404 ||
got != `{"_tag":"CertificateNotFoundError","tunnelID":"`+tunnel.ID+`","message":"Certificate not found"}` {
t.Errorf("certificate before bind: %d %q", resp.StatusCode, got)
}
resp = request(t, "POST", base+"/api/tunnel/"+tunnel.ID+"/certificate", tunnel.Token, "application/json", `{"csr":"garbage"}`)
if got := bodyString(t, resp); resp.StatusCode != 400 ||
got != `{"_tag":"InvalidRequestError","message":"Failed to parse CSR"}` {
t.Errorf("bind garbage: %d %q", resp.StatusCode, got)
}
wrong := makeCSR(t, "other.opentunnel.test", "other.opentunnel.test")
resp = request(t, "POST", base+"/api/tunnel/"+tunnel.ID+"/certificate", tunnel.Token, "application/json",
`{"csr":`+jsonString(wrong)+`}`)
wantWrong := `{"_tag":"InvalidHostnameError","provided":"other.opentunnel.test","expected":"` +
tunnel.Hostname + `","message":"CSR hostname does not match tunnel hostname"}`
if got := bodyString(t, resp); resp.StatusCode != 400 || got != wantWrong {
t.Errorf("bind wrong hostname: %d %q, want %q", resp.StatusCode, got, wantWrong)
}
csr := makeCSR(t, tunnel.Hostname, tunnel.Hostname, "*."+tunnel.Hostname)
resp = request(t, "POST", base+"/api/tunnel/"+tunnel.ID+"/certificate", tunnel.Token, "application/json",
`{"csr":`+jsonString(csr)+`}`)
body := bodyString(t, resp)
if resp.StatusCode != 202 || !strings.Contains(body, `"type":"issuing"`) || !strings.Contains(body, `"id":"cert_`) {
t.Errorf("bind: %d %q", resp.StatusCode, body)
}
resp = request(t, "GET", base+"/api/tunnel/"+tunnel.ID+"/certificate", tunnel.Token, "", "")
var cert CertificateInfo
if err := json.Unmarshal([]byte(bodyString(t, resp)), &cert); err != nil {
t.Fatal(err)
}
if cert.State.Type != "ready" || cert.State.Certificate == "" || cert.State.Chain == "" || cert.State.Expiry == "" {
t.Errorf("certificate after bind: %+v", cert.State)
}
// rebinding the same CSR answers with the current certificate
resp = request(t, "POST", base+"/api/tunnel/"+tunnel.ID+"/certificate", tunnel.Token, "application/json",
`{"csr":`+jsonString(csr)+`}`)
if got := bodyString(t, resp); resp.StatusCode != 202 || !strings.Contains(got, `"type":"ready"`) {
t.Errorf("rebind: %d %q", resp.StatusCode, got)
}
// get now carries certificateID
resp = request(t, "GET", base+"/api/tunnel/"+tunnel.ID, tunnel.Token, "", "")
if got := bodyString(t, resp); !strings.Contains(got, `"certificateID":"cert_`) {
t.Errorf("get after bind: %q", got)
}
// connect: the pinned pre-upgrade answers
resp = request(t, "GET", base+"/api/tunnel/"+tunnel.ID+"/connect", "", "", "")
if got := bodyString(t, resp); resp.StatusCode != 426 || got != "Expected WebSocket upgrade" {
t.Errorf("connect without upgrade: %d %q", resp.StatusCode, got)
}
req, _ = http.NewRequest("GET", base+"/api/tunnel/"+tunnel.ID+"/connect", nil)
req.Header.Set("Upgrade", "websocket")
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if got := bodyString(t, resp); resp.StatusCode != 426 || got != "Expected opentunnel WebSocket subprotocol" {
t.Errorf("connect without subprotocol: %d %q", resp.StatusCode, got)
}
req, _ = http.NewRequest("GET", base+"/api/tunnel/zzzzzzzzzzzz/connect", nil)
req.Header.Set("Upgrade", "websocket")
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if got := bodyString(t, resp); resp.StatusCode != 404 || got != "Tunnel not found" {
t.Errorf("connect unknown: %d %q", resp.StatusCode, got)
}
// unknown paths and methods are an empty 404; health answers
resp = request(t, "PUT", base+"/api/tunnel", "", "", "")
if got := bodyString(t, resp); resp.StatusCode != 404 || got != "" {
t.Errorf("put: %d %q", resp.StatusCode, got)
}
resp = request(t, "GET", base+"/api/nope", "", "", "")
if got := bodyString(t, resp); resp.StatusCode != 404 || got != "" {
t.Errorf("unknown path: %d %q", resp.StatusCode, got)
}
resp = request(t, "GET", base+"/health", "", "", "")
if got := bodyString(t, resp); resp.StatusCode != 200 || got != `{"ok":true}` {
t.Errorf("health: %d %q", resp.StatusCode, got)
}
// delete
resp = request(t, "DELETE", base+"/api/tunnel/"+tunnel.ID, tunnel.Token, "", "")
if resp.StatusCode != 204 {
t.Errorf("delete: %d", resp.StatusCode)
}
resp = request(t, "GET", base+"/api/tunnel/"+tunnel.ID, tunnel.Token, "", "")
if resp.StatusCode != 404 {
t.Errorf("get after delete: %d", resp.StatusCode)
}
resp.Body.Close()
}
// jsonString encodes s as a JSON string literal.
func jsonString(s string) string {
raw, _ := json.Marshal(s)
return string(raw)
}
+533
View File
@@ -0,0 +1,533 @@
package server
import (
"context"
"encoding/json"
"io"
"log"
"net"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/coder/websocket"
)
// Hub tracks the attached bridges: which tunnel they belong to and which
// routes each claimed. A route belongs to one bridge at a time.
type Hub struct {
store *Store
mu sync.Mutex
bridges map[*Bridge]struct{}
byRoute map[string]map[string]*Bridge // tunnel ID -> route -> bridge
}
func NewHub(store *Store) *Hub {
return &Hub{
store: store,
bridges: map[*Bridge]struct{}{},
byRoute: map[string]map[string]*Bridge{},
}
}
// online reports whether the tunnel has an attached bridge.
func (h *Hub) online(tunnelID string) bool {
h.mu.Lock()
defer h.mu.Unlock()
for _, b := range h.byRoute[tunnelID] {
_ = b
return true
}
return false
}
// bridgeFor returns the bridge that claimed route on the tunnel, or nil.
func (h *Hub) bridgeFor(tunnelID, route string) *Bridge {
h.mu.Lock()
defer h.mu.Unlock()
return h.byRoute[tunnelID][route]
}
// closeTunnel closes every bridge of the tunnel (it was deleted).
func (h *Hub) closeTunnel(tunnelID string) {
h.mu.Lock()
var bridges []*Bridge
for b := range h.bridges {
if b.tunnelID == tunnelID {
bridges = append(bridges, b)
}
}
h.mu.Unlock()
for _, b := range bridges {
b.close(websocket.StatusGoingAway, "tunnel deleted")
}
}
// drainAll announces drain to every bridge and closes it, so clients
// reconnect elsewhere (used on shutdown).
func (h *Hub) DrainAll() {
h.mu.Lock()
bridges := make([]*Bridge, 0, len(h.bridges))
for b := range h.bridges {
bridges = append(bridges, b)
}
h.mu.Unlock()
for _, b := range bridges {
drain, _ := json.Marshal(drainMessage{Type: "drain", Reason: "shutdown"})
b.trySend(websocket.MessageText, drain)
b.close(websocket.StatusServiceRestart, "shutdown")
}
}
func (h *Hub) register(b *Bridge) bool {
h.mu.Lock()
defer h.mu.Unlock()
claimed := h.byRoute[b.tunnelID]
if claimed == nil {
claimed = map[string]*Bridge{}
h.byRoute[b.tunnelID] = claimed
}
for _, route := range b.routes {
if _, taken := claimed[route]; taken {
return false
}
}
for _, route := range b.routes {
claimed[route] = b
}
h.bridges[b] = struct{}{}
return true
}
func (h *Hub) unregister(b *Bridge) {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.bridges, b)
if claimed := h.byRoute[b.tunnelID]; claimed != nil {
for route, owner := range claimed {
if owner == b {
delete(claimed, route)
}
}
if len(claimed) == 0 {
delete(h.byRoute, b.tunnelID)
}
}
}
// Bridge is one attached client session: the WebSocket, its routes, and its
// visitor connections.
type Bridge struct {
hub *Hub
tunnelID string
session string
routes []string
maxConns int
ws *websocket.Conn
outbox chan outbound
done chan struct{}
closeOnce sync.Once
mu sync.Mutex
conns map[uint32]*visitorConn
nextConn uint32
lastActivity atomic.Int64
}
type outbound struct {
typ websocket.MessageType
data []byte
}
// attach upgrades the HTTP connection and runs the bridge session. It blocks
// until the session ends.
func (h *Hub) attach(w http.ResponseWriter, r *http.Request, tunnel storedTunnel) {
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{BridgeSubprotocol},
CompressionMode: websocket.CompressionDisabled,
OriginPatterns: nil, // CLI and SDK send no Origin header
})
if err != nil {
return // Accept already replied
}
ws.SetReadLimit(clientHelloLimit) // 64 KiB before attach
ctx := context.Background()
fail := func(code string) {
msg, _ := json.Marshal(attachErrorMessage{Type: "attach_error", Code: code})
writeWS(ctx, ws, websocket.MessageText, msg)
ws.Close(websocket.StatusPolicyViolation, code)
}
// The attach must arrive within 10 seconds.
readCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
typ, raw, err := ws.Read(readCtx)
cancel()
if err != nil {
ws.Close(websocket.StatusPolicyViolation, "attach timeout")
return
}
var attach attachMessage
if typ != websocket.MessageText || json.Unmarshal(raw, &attach) != nil || attach.Type != "attach" {
fail("bad_attach")
return
}
if !tokenMatches(attach.Token, tunnel.TokenHash) {
fail("bad_token")
return
}
if tunnel.Certificate == nil {
fail("cert_not_ready")
return
}
if len(attach.Routes) > maxRoutesPerAttach {
fail("too_many_routes")
return
}
routes := dedupe(attach.Routes)
for _, route := range routes {
if !validRoute(route) {
fail("invalid_route")
return
}
}
b := &Bridge{
hub: h,
tunnelID: tunnel.ID,
session: sessionID(),
routes: routes,
maxConns: maxConnsPerBridge,
ws: ws,
outbox: make(chan outbound, 256),
done: make(chan struct{}),
conns: map[uint32]*visitorConn{},
}
if attach.Client.MaxConns > 0 && attach.Client.MaxConns < b.maxConns {
b.maxConns = attach.Client.MaxConns
}
b.touch()
if !h.register(b) {
fail("route_conflict")
return
}
attached, _ := json.Marshal(attachedMessage{
Type: "attached",
Session: b.session,
Routes: routes,
HeartbeatMs: heartbeatMs,
IdleTimeoutMs: idleTimeoutMs,
})
if err := writeWS(ctx, ws, websocket.MessageText, attached); err != nil {
h.unregister(b)
ws.Close(websocket.StatusInternalError, "write failed")
return
}
ws.SetReadLimit(4 + maxPayload) // a data frame with the largest payload
go b.writeLoop()
go b.sweeper()
b.readLoop() // blocks until the WebSocket ends
b.close(websocket.StatusGoingAway, "session ended")
}
// openVisitor hands a new public connection to the bridge: an open control
// message, the ClientHello as the first data, and a pump for what follows.
func (b *Bridge) openVisitor(conn net.Conn, hello []byte, peer, sni, alpn string) {
b.mu.Lock()
if len(b.conns) >= b.maxConns {
b.mu.Unlock()
conn.Close()
return
}
id := b.nextConn
b.nextConn++
v := &visitorConn{id: id, conn: conn}
b.conns[id] = v
b.mu.Unlock()
open, _ := json.Marshal(openMessage{
Type: "open", Conn: id, Peer: peer, SNI: sni, ALPN: alpn,
})
if !b.send(websocket.MessageText, open) {
b.removeConn(id, true)
return
}
for len(hello) > 0 {
n := min(len(hello), maxPayload)
if !b.send(websocket.MessageBinary, buildFrame(id, hello[:n])) {
b.removeConn(id, true)
return
}
hello = hello[n:]
}
go b.pump(v)
}
// pump copies visitor bytes to the bridge and half-closes on EOF.
func (b *Bridge) pump(v *visitorConn) {
buf := make([]byte, maxPayload)
for {
n, err := v.conn.Read(buf)
v.touch()
if n > 0 {
if !b.send(websocket.MessageBinary, buildFrame(v.id, buf[:n])) {
return
}
}
if err != nil {
b.mu.Lock()
v.visitorEnded = true
bothEnded := v.clientEnded
stillKnown := b.conns[v.id] == v
b.mu.Unlock()
if !stillKnown {
return // reset or removed already
}
if err == io.EOF {
end, _ := json.Marshal(endMessage{Type: "end", Conn: v.id})
b.send(websocket.MessageText, end)
} else {
reset, _ := json.Marshal(resetMessage{Type: "reset", Conn: v.id, Code: ResetIOError})
b.send(websocket.MessageText, reset)
b.removeConn(v.id, true)
return
}
if bothEnded {
b.removeConn(v.id, false)
}
return
}
}
}
// readLoop applies client control messages and data frames until the
// WebSocket fails.
func (b *Bridge) readLoop() {
ctx := context.Background()
for {
typ, raw, err := b.ws.Read(ctx)
if err != nil {
log.Printf("bridge %s: read: %v", b.session, err)
return
}
b.touch()
if typ == websocket.MessageText {
b.handleControl(raw)
continue
}
connID, payload, ok := parseFrame(raw)
if !ok {
continue
}
b.mu.Lock()
v := b.conns[connID]
b.mu.Unlock()
if v == nil {
continue
}
v.touch()
if _, err := v.conn.Write(payload); err != nil {
b.removeConn(connID, true)
}
}
}
func (b *Bridge) handleControl(raw []byte) {
msg, err := parseClientControl(raw)
if err != nil {
return
}
switch msg.Type {
case "ping":
pong, _ := json.Marshal(pingPongMessage{Type: "pong", TimeSent: msg.TimeSent})
b.send(websocket.MessageText, pong)
case "end":
if msg.Conn != nil {
b.endFromClient(*msg.Conn)
}
case "reset":
if msg.Conn != nil {
b.removeConn(*msg.Conn, true)
}
}
// Unknown types and fields are ignored.
}
func (b *Bridge) endFromClient(connID uint32) {
b.mu.Lock()
v := b.conns[connID]
b.mu.Unlock()
if v == nil {
return
}
b.mu.Lock()
v.clientEnded = true
bothEnded := v.visitorEnded
b.mu.Unlock()
if tcp, ok := v.conn.(*net.TCPConn); ok {
tcp.CloseWrite()
}
if bothEnded {
b.removeConn(connID, false)
}
}
// writeLoop is the only writer on the WebSocket, keeping frames ordered.
func (b *Bridge) writeLoop() {
ctx := context.Background()
for {
select {
case <-b.done:
return
case msg := <-b.outbox:
if err := writeWS(ctx, b.ws, msg.typ, msg.data); err != nil {
b.close(websocket.StatusInternalError, "write failed")
return
}
}
}
}
// sweeper retires a silent bridge (idle_timeout + heartbeat) and resets
// visitor connections with no traffic for 15 minutes.
func (b *Bridge) sweeper() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-b.done:
return
case now := <-ticker.C:
if now.UnixMilli()-b.lastActivity.Load() > (idleTimeoutMs + heartbeatMs) {
b.close(websocket.StatusGoingAway, "idle timeout")
return
}
b.mu.Lock()
var stale []uint32
for id, v := range b.conns {
if now.UnixMilli()-v.lastActivity.Load() > 15*60*1000 {
stale = append(stale, id)
}
}
b.mu.Unlock()
for _, id := range stale {
reset, _ := json.Marshal(resetMessage{Type: "reset", Conn: id, Code: ResetIdleTimeout})
b.send(websocket.MessageText, reset)
b.removeConn(id, true)
}
}
}
}
// send queues a frame, applying backpressure when the client is slow.
// It returns false when the bridge is gone.
func (b *Bridge) send(typ websocket.MessageType, data []byte) bool {
select {
case <-b.done:
return false
case b.outbox <- outbound{typ, data}:
return true
}
}
// trySend queues a frame only if it fits immediately.
func (b *Bridge) trySend(typ websocket.MessageType, data []byte) {
select {
case b.outbox <- outbound{typ, data}:
default:
}
}
// removeConn closes the visitor socket (abortively when rst) and forgets it.
func (b *Bridge) removeConn(id uint32, rst bool) {
b.mu.Lock()
v := b.conns[id]
delete(b.conns, id)
b.mu.Unlock()
if v == nil {
return
}
if rst {
if tcp, ok := v.conn.(*net.TCPConn); ok {
tcp.SetLinger(0)
}
}
v.conn.Close()
}
func (b *Bridge) touch() {
b.lastActivity.Store(time.Now().UnixMilli())
}
// close ends the session exactly once: conns reset, routes released.
func (b *Bridge) close(status websocket.StatusCode, reason string) {
b.closeOnce.Do(func() {
close(b.done)
b.hub.unregister(b)
b.mu.Lock()
ids := make([]uint32, 0, len(b.conns))
for id := range b.conns {
ids = append(ids, id)
}
b.mu.Unlock()
for _, id := range ids {
b.removeConn(id, true)
}
b.ws.Close(status, reason)
})
}
// visitorConn is one public TCP connection carried over the bridge.
type visitorConn struct {
id uint32
conn net.Conn
visitorEnded bool // pump saw EOF: no more visitor data
clientEnded bool // client sent end: target will send no more
lastActivity atomic.Int64
}
func (v *visitorConn) touch() {
v.lastActivity.Store(time.Now().UnixMilli())
}
func writeWS(ctx context.Context, ws *websocket.Conn, typ websocket.MessageType, data []byte) error {
writeCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
return ws.Write(writeCtx, typ, data)
}
func dedupe(routes []string) []string {
seen := map[string]struct{}{}
out := make([]string, 0, len(routes))
for _, route := range routes {
if _, ok := seen[route]; !ok {
seen[route] = struct{}{}
out = append(out, route)
}
}
return out
}
// isWebSocketUpgrade reports the shape of the upgrade request, for the 426
// responses the contract pins.
func upgradeRequested(r *http.Request) bool {
return strings.EqualFold(r.Header.Get("Upgrade"), "websocket")
}
func subprotocolOffered(r *http.Request) bool {
for _, offered := range strings.Split(r.Header.Get("Sec-Websocket-Protocol"), ",") {
if strings.TrimSpace(offered) == BridgeSubprotocol {
return true
}
}
return false
}
+153
View File
@@ -0,0 +1,153 @@
package server
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"time"
)
// clientHelloLimit bounds the bytes read while looking for a ClientHello
// (the reference uses 64 KiB and 10 seconds).
const (
clientHelloLimit = 64 * 1024
clientHelloTimeout = 10 * time.Second
)
// readClientHello reads TLS records from conn until the ClientHello is
// complete, without consuming anything beyond it. It returns the exact bytes
// read (for replay or forwarding), the SNI server name, and the offered ALPN
// protocols. Reads are exact (no read-ahead), so conn is handed on intact.
func readClientHello(conn net.Conn) (raw []byte, sni string, alpn []string, err error) {
conn.SetReadDeadline(time.Now().Add(clientHelloTimeout))
defer conn.SetReadDeadline(time.Time{})
var hello []byte // handshake message fragments, starting with the 4-byte header
need := -1 // total ClientHello bytes once the header is known
for len(raw) < clientHelloLimit {
header := make([]byte, 5)
if _, err = io.ReadFull(conn, header); err != nil {
return nil, "", nil, fmt.Errorf("reading record header: %w", err)
}
if header[0] != 22 { // handshake
return nil, "", nil, fmt.Errorf("expected a handshake record, got %d", header[0])
}
bodyLen := int(binary.BigEndian.Uint16(header[3:]))
if len(raw)+5+bodyLen > clientHelloLimit {
return nil, "", nil, errors.New("ClientHello exceeds 64 KiB")
}
body := make([]byte, bodyLen)
if _, err = io.ReadFull(conn, body); err != nil {
return nil, "", nil, fmt.Errorf("reading record body: %w", err)
}
raw = append(raw, header...)
raw = append(raw, body...)
hello = append(hello, body...)
if need < 0 {
if len(hello) < 4 {
continue
}
if hello[0] != 1 { // client_hello
return nil, "", nil, fmt.Errorf("expected ClientHello, got handshake type %d", hello[0])
}
need = 4 + int(hello[1])<<16 | int(hello[2])<<8 | int(hello[3])
}
if len(hello) >= need {
sni, alpn = parseClientHello(hello[4:need])
return raw, sni, alpn, nil
}
}
return nil, "", nil, errors.New("ClientHello exceeds 64 KiB")
}
// parseClientHello extracts SNI and ALPN from a ClientHello body (everything
// after the 4-byte handshake header). Truncation is an error; a missing
// extension simply yields an empty result, as the relay closes such
// connections anyway.
func parseClientHello(body []byte) (sni string, alpn []string) {
p := parser{data: body}
p.skip(2) // version
p.skip(32) // random
p.skip(int(p.u8())) // session id
p.skip(int(p.u16())) // cipher suites
p.skip(int(p.u8())) // compression methods
extLen := int(p.u16()) // extensions block
if p.err {
return "", nil
}
exts := parser{data: p.take(extLen)}
for !exts.empty() {
typ := exts.u16()
data := exts.take(int(exts.u16()))
if exts.err {
break
}
switch typ {
case 0: // server_name
d := parser{data: data}
list := parser{data: d.take(int(d.u16()))}
for !list.empty() {
nameType := list.u8()
name := list.take(int(list.u16()))
if list.err {
break
}
if nameType == 0 {
sni = string(name)
}
}
case 16: // application_layer_protocol_negotiation
d := parser{data: data}
list := parser{data: d.take(int(d.u16()))}
for !list.empty() {
proto := list.take(int(list.u8()))
if list.err {
break
}
alpn = append(alpn, string(proto))
}
}
}
return sni, alpn
}
// parser is a cursor over a byte slice; any out-of-bounds read sets err and
// yields zero values from then on.
type parser struct {
data []byte
err bool
}
func (p *parser) empty() bool { return p.err || len(p.data) == 0 }
func (p *parser) take(n int) []byte {
if n < 0 || len(p.data) < n {
p.err = true
return nil
}
out := p.data[:n]
p.data = p.data[n:]
return out
}
func (p *parser) skip(n int) { p.take(n) }
func (p *parser) u8() byte {
b := p.take(1)
if b == nil {
return 0
}
return b[0]
}
func (p *parser) u16() uint16 {
b := p.take(2)
if b == nil {
return 0
}
return binary.BigEndian.Uint16(b)
}
+97
View File
@@ -0,0 +1,97 @@
package server
import (
"bytes"
"crypto/tls"
"net"
"testing"
"time"
)
// TestReadClientHello captures a real ClientHello produced by crypto/tls and
// checks the parser and the byte-exact replay capture.
func TestReadClientHello(t *testing.T) {
client, serverConn := net.Pipe()
defer client.Close()
var written bytes.Buffer
go func() {
// The client writes its ClientHello and then blocks waiting for the
// server; we only need the write side.
tlsClient := tls.Client(captureConn{client, &written}, &tls.Config{
ServerName: "api.demo.opentunnel.xyz",
NextProtos: []string{"h2", "http/1.1"},
InsecureSkipVerify: true,
})
tlsClient.Handshake() // fails: nobody answers
}()
serverConn.SetReadDeadline(time.Now().Add(5 * time.Second))
raw, sni, alpn, err := readClientHello(serverConn)
if err != nil {
t.Fatalf("readClientHello: %v", err)
}
if sni != "api.demo.opentunnel.xyz" {
t.Errorf("sni = %q", sni)
}
if len(alpn) != 2 || alpn[0] != "h2" || alpn[1] != "http/1.1" {
t.Errorf("alpn = %v", alpn)
}
if !bytes.HasPrefix(written.Bytes(), raw) {
t.Errorf("captured %d bytes are not a prefix of the %d written", len(raw), written.Len())
}
}
// captureConn records everything written to it.
type captureConn struct {
net.Conn
w *bytes.Buffer
}
func (c captureConn) Write(p []byte) (int, error) {
c.w.Write(p)
return c.Conn.Write(p)
}
func TestReadClientHelloGarbage(t *testing.T) {
client, serverConn := net.Pipe()
go func() {
client.Write([]byte("not tls at all"))
client.Close()
}()
serverConn.SetReadDeadline(time.Now().Add(2 * time.Second))
if _, _, _, err := readClientHello(serverConn); err == nil {
t.Error("expected an error for non-TLS bytes")
}
}
func TestReadClientHelloTrickle(t *testing.T) {
// A ClientHello trickling in byte by byte still parses.
client, serverConn := net.Pipe()
var written bytes.Buffer
go func() {
tlsClient := tls.Client(captureConn{client, &written}, &tls.Config{
ServerName: "demo.opentunnel.xyz",
InsecureSkipVerify: true,
})
tlsClient.Handshake()
}()
serverConn.SetReadDeadline(time.Now().Add(5 * time.Second))
slow := &slowConn{Conn: serverConn} // one-byte reads, a trickling sender
raw, sni, _, err := readClientHello(slow)
if err != nil {
t.Fatalf("readClientHello: %v", err)
}
if sni != "demo.opentunnel.xyz" {
t.Errorf("sni = %q", sni)
}
if !bytes.HasPrefix(written.Bytes(), raw) {
t.Error("capture mismatch")
}
}
type slowConn struct{ net.Conn }
func (c *slowConn) Read(p []byte) (int, error) {
return c.Conn.Read(p[:1])
}
+174
View File
@@ -0,0 +1,174 @@
package server
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"time"
)
// localCA is the insecure built-in issuer for development and self-hosting,
// the equivalent of ISSUER=local in the reference server. The CA and every
// certificate it issues use ECDSA P-256, like the ACME-issued ones.
type localCA struct {
key *ecdsa.PrivateKey
cert *x509.Certificate
certPEM []byte
}
func newLocalCA() (*localCA, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "tunnel local CA"},
NotBefore: time.Now().Add(-time.Minute),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, err
}
return &localCA{
key: key,
cert: cert,
certPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}),
}, nil
}
func loadLocalCA(keyPEM, certPEM []byte) (*localCA, error) {
keyBlock, _ := pem.Decode(keyPEM)
if keyBlock == nil {
return nil, errors.New("no PEM block in CA key")
}
key, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
if err != nil {
return nil, err
}
ecKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, errors.New("CA key is not ECDSA")
}
certBlock, _ := pem.Decode(certPEM)
if certBlock == nil {
return nil, errors.New("no PEM block in CA certificate")
}
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return nil, err
}
return &localCA{key: ecKey, cert: cert, certPEM: pem.EncodeToMemory(certBlock)}, nil
}
func (ca *localCA) keyPEM() ([]byte, error) {
der, err := x509.MarshalPKCS8PrivateKey(ca.key)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), nil
}
// issue signs a certificate for names with a fresh P-256 key. Used for the
// server's own API certificate.
func (ca *localCA) issue(names ...string) (certPEM, keyPEM []byte, err error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
der, err := ca.sign(&x509.Certificate{
SerialNumber: newSerial(),
Subject: pkix.Name{CommonName: names[0]},
DNSNames: names,
NotBefore: time.Now().Add(-time.Minute),
NotAfter: time.Now().Add(90 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}, &key.PublicKey)
if err != nil {
return nil, nil, err
}
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return nil, nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}),
pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), nil
}
// errCSRHostname reports a CSR whose names are not the tunnel's own.
type errCSRHostname struct {
provided string
expected string
}
func (e errCSRHostname) Error() string {
return "CSR hostname " + e.provided + " does not match tunnel hostname " + e.expected
}
// signCSR validates a tunnel CSR and issues the certificate. The rules mirror
// the reference's job runner: the common name must be the tunnel hostname and
// every DNS name must be the hostname or its wildcard.
func (ca *localCA) signCSR(csrPEM, hostname string) (certPEM, chainPEM []byte, expiry time.Time, err error) {
block, _ := pem.Decode([]byte(csrPEM))
if block == nil || block.Type != "CERTIFICATE REQUEST" {
return nil, nil, time.Time{}, errors.New("not a PEM certificate request")
}
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil, nil, time.Time{}, err
}
if err := csr.CheckSignature(); err != nil {
return nil, nil, time.Time{}, fmt.Errorf("CSR signature: %w", err)
}
if csr.Subject.CommonName != hostname {
return nil, nil, time.Time{}, errCSRHostname{provided: csr.Subject.CommonName, expected: hostname}
}
for _, name := range csr.DNSNames {
if name != hostname && name != "*."+hostname {
return nil, nil, time.Time{}, errCSRHostname{provided: name, expected: hostname}
}
}
expiry = time.Now().Add(90 * 24 * time.Hour)
der, err := ca.sign(&x509.Certificate{
SerialNumber: newSerial(),
Subject: pkix.Name{CommonName: hostname},
DNSNames: csr.DNSNames,
NotBefore: time.Now().Add(-time.Minute),
NotAfter: expiry,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}, csr.PublicKey)
if err != nil {
return nil, nil, time.Time{}, err
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), ca.certPEM, expiry, nil
}
func (ca *localCA) sign(template *x509.Certificate, pub any) ([]byte, error) {
return x509.CreateCertificate(rand.Reader, template, ca.cert, pub, ca.key)
}
var serialLimit = new(big.Int).Lsh(big.NewInt(1), 128)
func newSerial() *big.Int {
n, err := rand.Int(rand.Reader, serialLimit)
if err != nil {
panic(err)
}
return n
}
+223
View File
@@ -0,0 +1,223 @@
// Package server implements a single-node OpenTunnel-compatible server:
// the HTTP API, the bridge WebSocket, and SNI-based relaying described in
// docs/protocol.md of the reference implementation.
package server
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"regexp"
)
// Identifiers are generated exactly as the reference does
// (crates/opentunnel-server/src/crypto.rs): a tunnel ID is 12 random base32
// characters, a token is "rly_" plus 32 random bytes in base64url, and the
// stored form of a token is the lowercase hex of its SHA-256.
const slugAlphabet = "abcdefghijklmnopqrstuvwxyz234567"
func randomBytes(n int) []byte {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return b
}
func slug() string {
b := randomBytes(12)
for i := range b {
b[i] = slugAlphabet[b[i]&31]
}
return string(b)
}
func newToken() string {
return "rly_" + base64.RawURLEncoding.EncodeToString(randomBytes(32))
}
func hashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
func tokenMatches(token, hash string) bool {
actual := hashToken(token)
return subtle.ConstantTimeCompare([]byte(actual), []byte(hash)) == 1
}
func certID() string { return "cert_" + base64.RawURLEncoding.EncodeToString(randomBytes(12)) }
func sessionID() string { return "sess_" + base64.RawURLEncoding.EncodeToString(randomBytes(12)) }
// Route names: "@" for the tunnel hostname itself, or one DNS label.
var routeLabel = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`)
func validRoute(name string) bool {
return name == "@" || routeLabel.MatchString(name)
}
// ---- HTTP API wire types. Field order matters: the recorded contract
// (worker-contract.json) pins the key order of every body.
type TunnelInfo struct {
ID string `json:"id"`
Hostname string `json:"hostname"`
State string `json:"state"` // "offline" | "online"
CertificateID string `json:"certificateID,omitempty"`
}
type createResponse struct {
Tunnel TunnelInfo `json:"tunnel"`
Token string `json:"token"`
}
type CertState struct {
Type string `json:"type"`
Token string `json:"token,omitempty"` // challenge
Key string `json:"key,omitempty"` // challenge
Certificate string `json:"certificate,omitempty"` // ready
Chain string `json:"chain,omitempty"` // ready
Expiry string `json:"expiry,omitempty"` // ready
Reason string `json:"reason,omitempty"` // failed
}
type CertificateInfo struct {
ID string `json:"id"`
State CertState `json:"state"`
}
// Error bodies. The reference emits {"_tag": ..., extra..., "message": ...}.
type errUnauthorized struct {
Tag string `json:"_tag"`
Message string `json:"message"`
}
type errTaggedID struct {
Tag string `json:"_tag"`
TunnelID string `json:"tunnelID"`
Message string `json:"message"`
}
type errInvalidRequest struct {
Tag string `json:"_tag"`
Message string `json:"message"`
}
type errInvalidHostname struct {
Tag string `json:"_tag"`
Provided string `json:"provided"`
Expected string `json:"expected"`
Message string `json:"message"`
}
// ---- Bridge control messages.
const (
BridgeSubprotocol = "opentunnel"
heartbeatMs = 15000
idleTimeoutMs = 45000
maxPayload = 32768 // bytes per binary data frame
maxRoutesPerAttach = 128
maxConnsPerBridge = 256
)
// attachMessage is the one client message with required structure; all other
// client control messages are decoded loosely and unknown types ignored.
type attachMessage struct {
Type string `json:"type"`
Token string `json:"token"`
Transport string `json:"transport"`
Routes []string `json:"routes"`
Client struct {
Version string `json:"version"`
MaxConns int `json:"max_conns"`
} `json:"client"`
}
// clientControl decodes the non-attach client messages.
type clientControl struct {
Type string `json:"type"`
Conn *uint32 `json:"conn"`
Code string `json:"code"`
TimeSent int64 `json:"time_sent"`
}
func parseClientControl(text []byte) (clientControl, error) {
var msg clientControl
if err := json.Unmarshal(text, &msg); err != nil {
return msg, err
}
return msg, nil
}
// Server-to-client messages, in pinned key order.
type attachedMessage struct {
Type string `json:"type"`
Session string `json:"session"`
Routes []string `json:"routes"`
HeartbeatMs int `json:"heartbeat_ms"`
IdleTimeoutMs int `json:"idle_timeout_ms"`
}
type attachErrorMessage struct {
Type string `json:"type"`
Code string `json:"code"`
}
type openMessage struct {
Type string `json:"type"`
Conn uint32 `json:"conn"`
Peer string `json:"peer"`
SNI string `json:"sni"`
ALPN string `json:"alpn"`
}
type pingPongMessage struct {
Type string `json:"type"`
TimeSent int64 `json:"time_sent"`
}
type drainMessage struct {
Type string `json:"type"`
Reason string `json:"reason"`
}
type endMessage struct {
Type string `json:"type"`
Conn uint32 `json:"conn"`
}
type resetMessage struct {
Type string `json:"type"`
Conn uint32 `json:"conn"`
Code string `json:"code"`
}
// Reset codes used by this server (clients treat codes as opaque strings).
const (
ResetIdleTimeout = "idle_timeout"
ResetIOError = "io_error"
ResetDrain = "drain"
)
// buildFrame serializes a binary data frame: big-endian u32 conn + payload.
func buildFrame(conn uint32, payload []byte) []byte {
frame := make([]byte, 4+len(payload))
binary.BigEndian.PutUint32(frame, conn)
copy(frame[4:], payload)
return frame
}
// parseFrame splits a binary data frame into conn ID and payload.
func parseFrame(data []byte) (uint32, []byte, bool) {
if len(data) < 4 {
return 0, nil, false
}
return binary.BigEndian.Uint32(data), data[4:], true
}
+187
View File
@@ -0,0 +1,187 @@
package server
import (
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"testing"
)
// The spec vectors are shared with the reference implementation
// (.reference/opentunnel/spec/vectors).
type frameVector struct {
Conn uint32 `json:"conn"`
Payload string `json:"payload"`
Frame string `json:"frame"`
}
func TestFrameVectors(t *testing.T) {
var vectors []frameVector
readVector(t, "frames.json", &vectors)
for i, v := range vectors {
payload, err := hex.DecodeString(v.Payload)
if err != nil {
t.Fatalf("vector %d payload: %v", i, err)
}
want, err := hex.DecodeString(v.Frame)
if err != nil {
t.Fatalf("vector %d frame: %v", i, err)
}
if got := buildFrame(v.Conn, payload); !equalBytes(got, want) {
t.Errorf("vector %d: buildFrame = %x, want %x", i, got, want)
}
conn, gotPayload, ok := parseFrame(want)
if !ok || conn != v.Conn || !equalBytes(gotPayload, payload) {
t.Errorf("vector %d: parseFrame = (%d, %x, %v)", i, conn, gotPayload, ok)
}
}
}
type controlVector struct {
Client []json.RawMessage `json:"client"`
Server []json.RawMessage `json:"server"`
Ignored []json.RawMessage `json:"ignored"`
Invalid []json.RawMessage `json:"invalid"`
}
func TestControlVectors(t *testing.T) {
var vectors controlVector
readVector(t, "control.json", &vectors)
for i, raw := range vectors.Client {
msg, err := parseClientControl(raw)
if err != nil || msg.Type == "" {
t.Errorf("client vector %d: %v", i, err)
}
}
for i, raw := range vectors.Server {
var probe struct {
Type string `json:"type"`
}
if err := json.Unmarshal(raw, &probe); err != nil || probe.Type == "" {
t.Errorf("server vector %d should decode", i)
}
}
for i, raw := range vectors.Ignored {
if _, err := parseClientControl(raw); err != nil {
t.Errorf("ignored vector %d should still decode: %v", i, err)
}
}
// The invalid vectors either fail to decode or miss required fields.
for i, raw := range vectors.Invalid {
msg, err := parseClientControl(raw)
if err != nil {
continue
}
switch msg.Type {
case "": // {"conn":1} has no type
case "end": // {"type":"end"} misses conn
if msg.Conn != nil {
t.Errorf("invalid vector %d: end with conn?", i)
}
}
}
}
func TestAttachedVectorShape(t *testing.T) {
// Our attached message must marshal to exactly the vector's key order.
var vectors controlVector
readVector(t, "control.json", &vectors)
var want map[string]any
json.Unmarshal(vectors.Server[0], &want)
raw, err := json.Marshal(attachedMessage{
Type: "attached", Session: "sess_1", Routes: []string{"api"},
HeartbeatMs: 15000, IdleTimeoutMs: 45000,
})
if err != nil {
t.Fatal(err)
}
var got map[string]any
json.Unmarshal(raw, &got)
if len(got) != len(want) {
t.Fatalf("attached fields = %v, want %v", got, want)
}
for key := range want {
if _, ok := got[key]; !ok {
t.Errorf("attached misses %q", key)
}
}
}
type namesVector struct {
Route []struct {
Value string `json:"value"`
Valid bool `json:"valid"`
} `json:"route"`
}
func TestRouteNameVectors(t *testing.T) {
var vectors namesVector
readVector(t, "names.json", &vectors)
for _, v := range vectors.Route {
if got := validRoute(v.Value); got != v.Valid {
t.Errorf("validRoute(%q) = %v, want %v", v.Value, got, v.Valid)
}
}
}
type routeVector struct {
Hostname string `json:"hostname"`
SNI string `json:"sni"`
Route *string `json:"route"`
}
func TestRouteVectors(t *testing.T) {
var vectors []routeVector
readVector(t, "routes.json", &vectors)
const domain = "opentunnel.xyz"
for _, v := range vectors {
id, route, ok := routeForSNI(domain, v.SNI)
// The vectors know which tunnel exists; the server closes visitors of
// unknown tunnels at lookup, so a foreign ID resolves to no route.
tunnelID := v.Hostname[:len(v.Hostname)-len("."+domain)]
if ok && id != tunnelID {
ok = false
}
if v.Route == nil {
if ok {
t.Errorf("sni %q: got route %q, want no match", v.SNI, route)
}
continue
}
if !ok || route != *v.Route {
t.Errorf("sni %q: got (%q, %q, %v), want route %q", v.SNI, id, route, ok, *v.Route)
}
}
}
func readVector(t *testing.T, name string, out any) {
t.Helper()
raw, err := os.ReadFile(filepath.Join(vectorDir(t), name))
if err != nil {
t.Skipf("spec vectors not available: %v", err)
}
if err := json.Unmarshal(raw, out); err != nil {
t.Fatalf("parsing %s: %v", name, err)
}
}
// vectorDir locates the shared vectors: the reference checkout lives in
// .reference/opentunnel at the repository root.
func vectorDir(t *testing.T) string {
t.Helper()
return filepath.Join("..", "..", ".reference", "opentunnel", "spec", "vectors")
}
func equalBytes(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
+177
View File
@@ -0,0 +1,177 @@
package server
import (
"crypto/tls"
"log"
"net"
"strings"
"sync"
"time"
)
// Relay is the public TCP listener. It reads only the TLS ClientHello: SNI
// equal to the domain is terminated with the server's certificate (the HTTP
// API and bridge), tunnel hostnames are passed — still encrypted — to the
// bridge that claimed the route.
type Relay struct {
Domain string
Store *Store
Hub *Hub
Server *tls.Certificate // API domain certificate
APIConns chan net.Conn // terminated API connections, served by http.Server
}
// Serve accepts on ln until it is closed.
func (r *Relay) Serve(ln net.Listener) {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go r.handle(conn)
}
}
func (r *Relay) handle(conn net.Conn) {
raw, sni, alpn, err := readClientHello(conn)
if err != nil {
log.Printf("relay: %s: %v", conn.RemoteAddr(), err)
conn.Close()
return
}
sni = strings.ToLower(sni)
if sni == r.Domain {
r.serveAPI(conn, raw)
return
}
tunnelID, route, ok := routeForSNI(r.Domain, sni)
if !ok {
log.Printf("relay: %s: sni %q matches no tunnel hostname", conn.RemoteAddr(), sni)
conn.Close()
return
}
tunnel, ok := r.Store.get(tunnelID)
if !ok || tunnel.Certificate == nil {
log.Printf("relay: %s: tunnel %s unknown or certificate not ready", conn.RemoteAddr(), tunnelID)
conn.Close() // unknown tunnel or certificate not ready
return
}
bridge := r.Hub.bridgeFor(tunnelID, route)
if bridge == nil {
log.Printf("relay: %s: no bridge for route %q of tunnel %s", conn.RemoteAddr(), route, tunnelID)
conn.Close()
return
}
log.Printf("relay: %s: %s -> tunnel %s route %q", conn.RemoteAddr(), sni, tunnelID, route)
bridge.openVisitor(conn, raw, conn.RemoteAddr().String(), sni, strings.Join(alpn, ","))
}
// serveAPI replays the ClientHello into a TLS termination with the server's
// certificate and hands the connection to the shared API http.Server.
func (r *Relay) serveAPI(conn net.Conn, hello []byte) {
tlsConn := tls.Server(&replayConn{Conn: conn, prefix: hello}, &tls.Config{
Certificates: []tls.Certificate{*r.Server},
NextProtos: []string{"http/1.1"},
})
conn.SetReadDeadline(time.Now().Add(clientHelloTimeout))
err := tlsConn.Handshake()
conn.SetReadDeadline(time.Time{})
if err != nil {
conn.Close()
return
}
select {
case r.APIConns <- tlsConn:
default:
log.Printf("API connection queue full; dropping %s", conn.RemoteAddr())
tlsConn.Close()
}
}
// replayConn returns the already-read ClientHello bytes before reading from
// the underlying connection again.
type replayConn struct {
net.Conn
prefix []byte
}
func (c *replayConn) Read(p []byte) (int, error) {
if len(c.prefix) > 0 {
n := copy(p, c.prefix)
c.prefix = c.prefix[n:]
return n, nil
}
return c.Conn.Read(p)
}
// routeForSNI resolves a tunnel hostname to tunnel ID and route name:
// <id>.<domain> is route "@", <route>.<id>.<domain> is that route; anything
// deeper, malformed, or outside the domain does not match.
func routeForSNI(domain, sni string) (id, route string, ok bool) {
sni = strings.ToLower(sni)
suffix := "." + domain
if sni == "" || sni == domain || !strings.HasSuffix(sni, suffix) {
return "", "", false
}
labels := strings.Split(strings.TrimSuffix(sni, suffix), ".")
switch len(labels) {
case 1:
if !routeLabel.MatchString(labels[0]) {
return "", "", false
}
return labels[0], "@", true
case 2:
if !validRoute(labels[0]) || labels[0] == "@" || !routeLabel.MatchString(labels[1]) {
return "", "", false
}
return labels[1], labels[0], true
default:
return "", "", false
}
}
// chanListener turns a channel of connections into a net.Listener, so one
// http.Server can serve the connections the relay terminates for the domain.
type chanListener struct {
conns chan net.Conn
done chan struct{}
once sync.Once
addr net.Addr
}
func NewChanListener(capacity int, addr net.Addr) *chanListener {
return &chanListener{
conns: make(chan net.Conn, capacity),
done: make(chan struct{}),
addr: addr,
}
}
func (l *chanListener) Accept() (net.Conn, error) {
select {
case conn := <-l.conns:
return conn, nil
case <-l.done:
return nil, errListenerClosed
}
}
func (l *chanListener) Close() error {
l.once.Do(func() { close(l.done) })
return nil
}
func (l *chanListener) Addr() net.Addr { return l.addr }
// Conns returns the channel the listener serves connections from.
func (l *chanListener) Conns() chan net.Conn { return l.conns }
var errListenerClosed = &netError{}
type netError struct{}
func (e *netError) Error() string { return "listener closed" }
func (e *netError) Timeout() bool { return false }
func (e *netError) Temporary() bool { return false }
+193
View File
@@ -0,0 +1,193 @@
package server
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"
"time"
)
// storedCert is one issued certificate. With the local issuer, issuance is
// synchronous, so a bound CSR always has its certificate populated.
type storedCert struct {
ID string `json:"id"`
CSR string `json:"csr"`
Certificate string `json:"certificate"`
Chain string `json:"chain"`
Expiry string `json:"expiry"`
}
type storedTunnel struct {
ID string `json:"id"`
Hostname string `json:"hostname"`
TokenHash string `json:"tokenHash"`
Certificate *storedCert `json:"certificate,omitempty"`
}
// storeData is the on-disk shape. The CA key is a secret, so the file is
// written 0600.
type storeData struct {
CAKeyPEM string `json:"caKeyPem"`
CACertPEM string `json:"caCertPEM"`
ServerKey string `json:"serverKey"`
ServerCrt string `json:"serverCrt"`
Tunnels map[string]storedTunnel `json:"tunnels"`
}
// Store is a JSON-file-backed tunnel store: adequate for a single-node
// self-hosted deployment, and the one writer this server has.
type Store struct {
mu sync.Mutex
path string
data storeData
}
// openStore loads path, or initializes it (with a fresh local CA) when it
// does not exist. An empty path keeps state in memory only.
func OpenStore(path string) (*Store, error) {
s := &Store{path: path, data: storeData{Tunnels: map[string]storedTunnel{}}}
if path != "" {
if raw, err := os.ReadFile(path); err == nil {
if err := json.Unmarshal(raw, &s.data); err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
}
if s.data.Tunnels == nil {
s.data.Tunnels = map[string]storedTunnel{}
}
} else if !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
}
if s.data.CAKeyPEM == "" {
ca, err := newLocalCA()
if err != nil {
return nil, err
}
keyPEM, err := ca.keyPEM()
if err != nil {
return nil, err
}
s.data.CAKeyPEM = string(keyPEM)
s.data.CACertPEM = string(ca.certPEM)
if err := s.saveLocked(); err != nil {
return nil, err
}
}
return s, nil
}
// ca returns the store's local CA, loading it from the stored PEM.
func (s *Store) ca() (*localCA, error) {
s.mu.Lock()
defer s.mu.Unlock()
return loadLocalCA([]byte(s.data.CAKeyPEM), []byte(s.data.CACertPEM))
}
// CACertPEM is the CA certificate clients should trust.
func (s *Store) CACertPEM() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.data.CACertPEM
}
// serverCertificate returns the API domain's key and certificate PEM,
// issuing one with the CA when missing.
func (s *Store) ServerCertificate(domain string) (certPEM, keyPEM []byte, err error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.data.ServerCrt != "" {
return []byte(s.data.ServerCrt), []byte(s.data.ServerKey), nil
}
ca, err := loadLocalCA([]byte(s.data.CAKeyPEM), []byte(s.data.CACertPEM))
if err != nil {
return nil, nil, err
}
certPEM, keyPEM, err = ca.issue(domain)
if err != nil {
return nil, nil, err
}
s.data.ServerCrt = string(certPEM)
s.data.ServerKey = string(keyPEM)
if err := s.saveLocked(); err != nil {
return nil, nil, err
}
return certPEM, keyPEM, nil
}
// createTunnel stores a new tunnel and returns it with its bearer token, the
// only time the token is visible.
func (s *Store) createTunnel(domain string) (storedTunnel, string, error) {
token := newToken()
t := storedTunnel{
ID: slug(),
TokenHash: hashToken(token),
}
t.Hostname = t.ID + "." + domain
s.mu.Lock()
defer s.mu.Unlock()
s.data.Tunnels[t.ID] = t
return t, token, s.saveLocked()
}
// get returns the tunnel with id, or false.
func (s *Store) get(id string) (storedTunnel, bool) {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.data.Tunnels[id]
return t, ok
}
// bindCSR records the issued certificate for the tunnel. The CSR is kept so a
// renewal (and a resumed provision) can be answered without a new bind.
func (s *Store) bindCertificate(id string, cert storedCert) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.data.Tunnels[id]
if !ok {
return errors.New("tunnel not found")
}
t.Certificate = &cert
s.data.Tunnels[id] = t
return s.saveLocked()
}
// remove deletes the tunnel.
func (s *Store) remove(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.data.Tunnels[id]; !ok {
return errors.New("tunnel not found")
}
delete(s.data.Tunnels, id)
return s.saveLocked()
}
// saveLocked writes the data file atomically (0600); a no-op for an
// in-memory store.
func (s *Store) saveLocked() error {
if s.path == "" {
return nil
}
raw, err := json.MarshalIndent(s.data, "", " ")
if err != nil {
return err
}
tmp := s.path + ".tmp"
if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil {
return err
}
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
return err
}
return os.Rename(tmp, s.path)
}
// expiryString formats a time as the reference does: ISO 8601 UTC with
// milliseconds, e.g. 2026-10-03T00:00:00.000Z.
func expiryString(t time.Time) string {
return t.UTC().Format("2006-01-02T15:04:05.000Z")
}