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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user