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