Files
tunnel/internal/server/proto_test.go
T
TZGyn 682a341010 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.
2026-10-25 11:52:45 +08:00

188 lines
4.7 KiB
Go

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
}