refactor(handler/tunnel): code review fixes and entrypoint subpackage extraction

- Fix dead-code branch in bind.go host assignment (always use endpoint hash)
- Return descriptive error on bypass match in connect.go (was masking as success)
- Update bypass test in connect_test.go for new error behavior
- Extract entrypoint subpackage from monolithic entrypoint.go (6 files)
- Fix observeStats event-loss bug (break -> fallthrough on retry success)
- Add 47 unit tests across handler, connect, bind, metadata packages
- Add architecture doc comments to all key files
- Build and vet clean, 173 tests pass with -race
This commit is contained in:
ginuerzh
2026-06-02 23:51:55 +08:00
parent d432d28f81
commit 81db46b725
26 changed files with 3145 additions and 467 deletions
+499
View File
@@ -0,0 +1,499 @@
package tunnel
import (
"context"
"testing"
"time"
mdx "github.com/go-gost/x/metadata"
)
func TestParseMetadata_Defaults(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.readTimeout != 0 {
t.Errorf("expected readTimeout 0, got %v", h.md.readTimeout)
}
if h.md.entryPoint != "" {
t.Errorf("expected empty entryPoint, got %q", h.md.entryPoint)
}
if h.md.tunnelTTL != defaultTTL {
t.Errorf("expected tunnelTTL %v, got %v", defaultTTL, h.md.tunnelTTL)
}
if h.md.entryPointReadTimeout != 15*time.Second {
t.Errorf("expected entryPointReadTimeout 15s, got %v", h.md.entryPointReadTimeout)
}
if h.md.muxCfg.Version != 2 {
t.Errorf("expected mux version 2, got %d", h.md.muxCfg.Version)
}
if h.md.muxCfg.MaxStreamBuffer != 1048576 {
t.Errorf("expected max stream buffer 1048576, got %d", h.md.muxCfg.MaxStreamBuffer)
}
if h.md.observerPeriod != 5*time.Second {
t.Errorf("expected observerPeriod 5s, got %v", h.md.observerPeriod)
}
if h.md.directTunnel {
t.Error("expected directTunnel false")
}
if h.md.sniffingWebsocket {
t.Error("expected sniffingWebsocket false")
}
if h.md.ingress != nil {
t.Error("expected nil ingress")
}
if h.md.sd != nil {
t.Error("expected nil sd")
}
}
func TestParseMetadata_ReadTimeout(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"readTimeout": "3s",
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.readTimeout != 3*time.Second {
t.Errorf("expected readTimeout 3s, got %v", h.md.readTimeout)
}
}
func TestParseMetadata_Entrypoint(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"entrypoint": ":8080",
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.entryPoint != ":8080" {
t.Errorf("expected entryPoint :8080, got %q", h.md.entryPoint)
}
}
func TestParseMetadata_DirectTunnel(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"tunnel.direct": true,
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !h.md.directTunnel {
t.Error("expected directTunnel true")
}
}
func TestParseMetadata_TunnelTTL(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"tunnel.ttl": "30s",
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.tunnelTTL != 30*time.Second {
t.Errorf("expected tunnelTTL 30s, got %v", h.md.tunnelTTL)
}
}
func TestParseMetadata_TunnelTTL_ZeroDefaults(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"tunnel.ttl": 0,
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.tunnelTTL != defaultTTL {
t.Errorf("expected tunnelTTL %v, got %v", defaultTTL, h.md.tunnelTTL)
}
}
func TestParseMetadata_Sniffing(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"sniffing.websocket": true,
"sniffing.websocket.sampleRate": 0.5,
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !h.md.sniffingWebsocket {
t.Error("expected sniffingWebsocket true")
}
if h.md.sniffingWebsocketSampleRate != 0.5 {
t.Errorf("expected sampleRate 0.5, got %f", h.md.sniffingWebsocketSampleRate)
}
}
func TestParseMetadata_ObserverPeriod(t *testing.T) {
tests := []struct {
name string
input string
expect time.Duration
}{
{"custom period", "10s", 10 * time.Second},
{"minimum clamped to 1s", "100ms", time.Second},
{"zero defaults to 5s", "", 5 * time.Second},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &tunnelHandler{}
data := map[string]any{}
if tt.input != "" {
data["observePeriod"] = tt.input
}
md := mdx.NewMetadata(data)
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.observerPeriod != tt.expect {
t.Errorf("expected %v, got %v", tt.expect, h.md.observerPeriod)
}
})
}
}
func TestParseMetadata_ObserverPeriod_AlternateKeys(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"observer.period": "3s",
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.observerPeriod != 3*time.Second {
t.Errorf("expected 3s, got %v", h.md.observerPeriod)
}
}
func TestParseMetadata_ObserverResetTraffic(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"observer.resetTraffic": true,
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !h.md.observerResetTraffic {
t.Error("expected observerResetTraffic true")
}
}
func TestParseMetadata_MuxConfig(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"mux.version": 1,
"mux.keepaliveInterval": "10s",
"mux.keepaliveDisabled": true,
"mux.keepaliveTimeout": "30s",
"mux.maxFrameSize": 4096,
"mux.maxReceiveBuffer": 8192,
"mux.maxStreamBuffer": 65536,
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.muxCfg.Version != 1 {
t.Errorf("expected mux version 1, got %d", h.md.muxCfg.Version)
}
if h.md.muxCfg.KeepAliveInterval != 10*time.Second {
t.Errorf("expected keepalive interval 10s, got %v", h.md.muxCfg.KeepAliveInterval)
}
if !h.md.muxCfg.KeepAliveDisabled {
t.Error("expected keepalive disabled")
}
if h.md.muxCfg.KeepAliveTimeout != 30*time.Second {
t.Errorf("expected keepalive timeout 30s, got %v", h.md.muxCfg.KeepAliveTimeout)
}
if h.md.muxCfg.MaxFrameSize != 4096 {
t.Errorf("expected max frame size 4096, got %d", h.md.muxCfg.MaxFrameSize)
}
if h.md.muxCfg.MaxReceiveBuffer != 8192 {
t.Errorf("expected max receive buffer 8192, got %d", h.md.muxCfg.MaxReceiveBuffer)
}
if h.md.muxCfg.MaxStreamBuffer != 65536 {
t.Errorf("expected max stream buffer 65536, got %d", h.md.muxCfg.MaxStreamBuffer)
}
}
func TestParseMetadata_MuxDefaultsNoVersion(t *testing.T) {
h := &tunnelHandler{}
// When no mux config is provided, defaults should be applied.
md := mdx.NewMetadata(map[string]any{})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.muxCfg.Version != 2 {
t.Errorf("expected default version 2, got %d", h.md.muxCfg.Version)
}
if h.md.muxCfg.MaxStreamBuffer != 1048576 {
t.Errorf("expected default max stream buffer 1048576, got %d", h.md.muxCfg.MaxStreamBuffer)
}
}
func TestParseMetadata_TunnelRules(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"tunnel": "example.com:6ba7b810-9dad-11d1-80b4-00c04fd430c8,app.example.com:7f2c3d4e-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.ingress == nil {
t.Fatal("expected ingress to be created from tunnel rules")
}
// Wait briefly for the ingress periodReload goroutine to populate rules.
time.Sleep(50 * time.Millisecond)
// Check that rules were parsed correctly
rule1 := h.md.ingress.GetRule(context.Background(), "example.com")
if rule1 == nil {
t.Fatal("expected rule for example.com")
}
if rule1.Hostname != "example.com" {
t.Errorf("expected hostname example.com, got %q", rule1.Hostname)
}
if rule1.Endpoint != "6ba7b810-9dad-11d1-80b4-00c04fd430c8" {
t.Errorf("expected endpoint 6ba7b810..., got %q", rule1.Endpoint)
}
rule2 := h.md.ingress.GetRule(context.Background(), "app.example.com")
if rule2 == nil {
t.Fatal("expected rule for app.example.com")
}
if rule2.Hostname != "app.example.com" {
t.Errorf("expected hostname app.example.com, got %q", rule2.Hostname)
}
if rule2.Endpoint != "7f2c3d4e-5a6b-7c8d-9e0f-1a2b3c4d5e6f" {
t.Errorf("expected endpoint 7f2c3d4e..., got %q", rule2.Endpoint)
}
}
func TestParseMetadata_TunnelRules_Malformed(t *testing.T) {
// Test that malformed tunnel rules are silently skipped.
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"tunnel": "valid.example.com:6ba7b810-9dad-11d1-80b4-00c04fd430c8,badrule",
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.ingress == nil {
t.Fatal("expected ingress to be created with at least valid rules")
}
// Wait briefly for the ingress periodReload goroutine to populate rules.
time.Sleep(50 * time.Millisecond)
// Valid rule should exist
rule := h.md.ingress.GetRule(context.Background(), "valid.example.com")
if rule == nil {
t.Fatal("expected rule for valid.example.com")
}
// Malformed rule should have been skipped
if len(h.md.entrypoints) != 0 {
t.Errorf("expected 0 entrypoints, got %d", len(h.md.entrypoints))
}
}
func TestParseMetadata_EntrypointsJSON(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"entrypoints": []map[string]any{
{"Addr": ":8080"},
{"Addr": ":9090"},
},
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(h.md.entrypoints) != 2 {
t.Fatalf("expected 2 entrypoints, got %d", len(h.md.entrypoints))
}
if h.md.entrypoints[0].Addr != ":8080" {
t.Errorf("expected entrypoints[0] addr :8080, got %q", h.md.entrypoints[0].Addr)
}
if h.md.entrypoints[1].Addr != ":9090" {
t.Errorf("expected entrypoints[1] addr :9090, got %q", h.md.entrypoints[1].Addr)
}
}
func TestParseMetadata_EntrypointsJSON_EmptyAddrSkipped(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"entrypoints": []map[string]any{
{"Addr": ""},
{"Addr": ":8080"},
},
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(h.md.entrypoints) != 1 {
t.Errorf("expected 1 entrypoint (empty addr skipped), got %d", len(h.md.entrypoints))
}
if h.md.entrypoints[0].Addr != ":8080" {
t.Errorf("expected entrypoint addr :8080, got %q", h.md.entrypoints[0].Addr)
}
}
func TestParseMetadata_LimiterIntervals(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"limiter.refreshInterval": "60s",
"limiter.cleanupInterval": "120s",
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.limiterRefreshInterval != 60*time.Second {
t.Errorf("expected limiterRefreshInterval 60s, got %v", h.md.limiterRefreshInterval)
}
if h.md.limiterCleanupInterval != 120*time.Second {
t.Errorf("expected limiterCleanupInterval 120s, got %v", h.md.limiterCleanupInterval)
}
}
func TestParseMetadata_EntrypointProxyProtocol(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"entrypoint.ProxyProtocol": 2,
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.entryPointProxyProtocol != 2 {
t.Errorf("expected proxy protocol 2, got %d", h.md.entryPointProxyProtocol)
}
}
func TestParseMetadata_EntrypointKeepalive(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"entrypoint.keepalive": true,
"entrypoint.compression": true,
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !h.md.entryPointKeepalive {
t.Error("expected entryPointKeepalive true")
}
if !h.md.entryPointCompression {
t.Error("expected entryPointCompression true")
}
}
func TestParseMetadata_EntrypointReadTimeout_ZeroDefaults(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.entryPointReadTimeout != 15*time.Second {
t.Errorf("expected default entryPointReadTimeout 15s, got %v", h.md.entryPointReadTimeout)
}
}
func TestParseMetadata_EmptyMetadata(t *testing.T) {
// With nil metadata, parseMetadata should not panic.
h := &tunnelHandler{}
err := h.parseMetadata(nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Defaults should still apply
if h.md.muxCfg.Version != 2 {
t.Errorf("expected default version 2, got %d", h.md.muxCfg.Version)
}
if h.md.tunnelTTL != defaultTTL {
t.Errorf("expected default TTL %v, got %v", defaultTTL, h.md.tunnelTTL)
}
}
// TestParseMetadata_EntrypointID tests parsing of the entrypointID metadata key.
func TestParseMetadata_EntrypointID(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"entrypoint.id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := ParseTunnelID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
if !h.md.entryPointID.Equal(expected) {
t.Errorf("expected entryPointID %v, got %v", expected, h.md.entryPointID)
}
}
// TestParseMetadata_TunnelRules_Empty tests that empty tunnel rules don't
// create an ingress.
func TestParseMetadata_TunnelRules_Empty(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"tunnel": "",
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if h.md.ingress != nil {
t.Error("expected nil ingress for empty tunnel rules")
}
}
// TestParseMetadata_EntrypointsJSON_WithIngressName tests that entrypoints
// JSON can reference named ingress objects. Without a registry entry, it
// should fall back to the handler's default ingress.
func TestParseMetadata_EntrypointsJSON_IngressFallback(t *testing.T) {
h := &tunnelHandler{}
md := mdx.NewMetadata(map[string]any{
"entrypoints": []map[string]any{
{"Addr": ":8080", "Ingress": "nonexistent"},
},
})
err := h.parseMetadata(md)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(h.md.entrypoints) != 1 {
t.Fatalf("expected 1 entrypoint, got %d", len(h.md.entrypoints))
}
// The registry always returns a non-nil ingressWrapper for non-empty names,
// even if the name is not registered (hot-reload design).
if h.md.entrypoints[0].Ingress == nil {
t.Error("expected non-nil ingress wrapper from registry")
}
}