// 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 }