refactor(handler/relay): split handler.go -> observe.go, fix observeStats event-loss bug (break->fallthrough)

Extract checkRateLimit and observeStats into their own file (observe.go).
Fix the one-tick event-loss bug in observeStats where the retry path used
'unconditional break' instead of fallthrough, causing events collected
after a successful retry to be discarded.

Add 87 unit tests across 7 files (helpers, metadata, conn, handler,
connect, forward, bind) covering all handler modes, error paths,
observer/stats integration, and conn wrapper edge cases.

handler.go: -43 lines (unused observer import, extracted functions)
observe.go: +52 lines (checkRateLimit, observeStats with fallthrough fix)
test files: +1959 lines
This commit is contained in:
ginuerzh
2026-06-02 23:51:19 +08:00
parent 31878a72ec
commit d432d28f81
9 changed files with 1959 additions and 43 deletions
+243
View File
@@ -0,0 +1,243 @@
package relay
import (
"bytes"
"context"
"net"
"testing"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/observer"
"github.com/go-gost/core/recorder"
"github.com/go-gost/relay"
xrecorder "github.com/go-gost/x/recorder"
)
func newConnectHandler(t *testing.T, opts ...handler.Option) *relayHandler {
t.Helper()
// Default: provide a working router
defaultOpts := []handler.Option{
handler.LoggerOption(&testLogger{}),
handler.RouterOption(&mockRouter{
dialFn: func(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
pr, pw := net.Pipe()
pw.Close()
return pr, nil
},
}),
}
return newInitdHandler(t, append(defaultOpts, opts...)...)
}
func TestHandleConnect_TCP(t *testing.T) {
rh := newConnectHandler(t)
rh.md.noDelay = true
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
err := rh.Handle(context.Background(), fc)
if err != nil {
t.Fatalf("Handle: %v", err)
}
// Should get a StatusOK response
resp := readRelayResponse(t, fc.writeBuf.Bytes())
if resp.Status != relay.StatusOK {
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusOK)
}
}
func TestHandleConnect_UDP(t *testing.T) {
rh := newConnectHandler(t)
rh.md.noDelay = true
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:53", "udp")}
err := rh.Handle(context.Background(), fc)
if err != nil {
t.Fatalf("Handle: %v", err)
}
resp := readRelayResponse(t, fc.writeBuf.Bytes())
if resp.Status != relay.StatusOK {
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusOK)
}
}
func TestHandleConnect_NoDelay(t *testing.T) {
rh := newConnectHandler(t)
// Override noDelay to true
rh.md.noDelay = true
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
err := rh.Handle(context.Background(), fc)
if err != nil {
t.Fatalf("Handle: %v", err)
}
// Response should be written before pipe
resp := readRelayResponse(t, fc.writeBuf.Bytes())
if resp.Status != relay.StatusOK {
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusOK)
}
}
func TestHandleConnect_RouterDialFails(t *testing.T) {
rh := newInitdHandler(t,
handler.RouterOption(&mockRouter{
dialFn: func(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
return nil, net.UnknownNetworkError("mock failure")
},
}),
)
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
err := rh.Handle(context.Background(), fc)
if err == nil {
t.Fatal("expected error")
}
resp := readRelayResponse(t, fc.writeBuf.Bytes())
if resp.Status != relay.StatusNetworkUnreachable {
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusNetworkUnreachable)
}
}
func TestHandleConnect_Bypass(t *testing.T) {
mb := &mockBypass{
containsFn: func(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
return true
},
}
rh := newInitdHandler(t,
handler.BypassOption(mb),
handler.RouterOption(&mockRouter{}),
)
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
err := rh.Handle(context.Background(), fc)
if err == nil {
t.Fatal("expected error")
}
resp := readRelayResponse(t, fc.writeBuf.Bytes())
if resp.Status != relay.StatusForbidden {
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusForbidden)
}
}
func TestHandleConnect_HashHost(t *testing.T) {
rh := newConnectHandler(t)
rh.md.hash = "host"
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
err := rh.Handle(context.Background(), fc)
if err != nil {
t.Fatalf("Handle: %v", err)
}
}
func TestHandleConnect_SniffingDisabled(t *testing.T) {
rh := newConnectHandler(t)
rh.md.noDelay = true
// sniffing is false by default — verify no sniffing activity
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
err := rh.Handle(context.Background(), fc)
if err != nil {
t.Fatalf("Handle: %v", err)
}
resp := readRelayResponse(t, fc.writeBuf.Bytes())
if resp.Status != relay.StatusOK {
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusOK)
}
}
func TestHandleConnect_Unix(t *testing.T) {
rh := newInitdHandler(t,
handler.LoggerOption(&testLogger{}),
handler.RouterOption(&mockRouter{}),
)
// Build a connect request with unix network
req := relay.Request{Version: relay.Version1, Cmd: relay.CmdConnect}
af := &relay.AddrFeature{Host: "/tmp/test.sock", Port: 0}
req.Features = append(req.Features, af)
req.Features = append(req.Features, &relay.NetworkFeature{Network: relay.NetworkUnix})
var buf bytes.Buffer
req.WriteTo(&buf)
fc := &fakeConn{buf: buf.Bytes()}
err := rh.Handle(context.Background(), fc)
// unix connect to /tmp/test.sock will fail, but it should reach the dial step
if err == nil {
t.Fatal("expected error for unix dial to /tmp/test.sock")
}
}
func TestHandleConnect_Serial(t *testing.T) {
rh := newInitdHandler(t,
handler.LoggerOption(&testLogger{}),
handler.RouterOption(&mockRouter{}),
)
// Build a connect request with serial network
req := relay.Request{Version: relay.Version1, Cmd: relay.CmdConnect}
af := &relay.AddrFeature{Host: "/invalid/serial", Port: 0}
req.Features = append(req.Features, af)
req.Features = append(req.Features, &relay.NetworkFeature{Network: relay.NetworkSerial})
var buf bytes.Buffer
req.WriteTo(&buf)
fc := &fakeConn{buf: buf.Bytes()}
err := rh.Handle(context.Background(), fc)
if err == nil {
t.Fatal("expected error for serial connect")
}
}
func TestHandleConnect_WithObserver(t *testing.T) {
obs := &fakeObserver{eventsCh: make(chan []observer.Event, 10)}
rh := newConnectHandler(t,
handler.ObserverOption(obs),
handler.ServiceOption("test-svc"),
)
// Init creates the stats — but with a new Init
// need to re-init with observer set
// Actually newConnectHandler uses newInitdHandler which sets up observer...
// The observer was set in handler options, so Init creates h.stats
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
err := rh.Handle(context.Background(), fc)
if err != nil {
t.Fatalf("Handle: %v", err)
}
}
func TestHandleConnect_ReadTimeout(t *testing.T) {
rh := &relayHandler{
options: handler.Options{Logger: &testLogger{}},
}
rh.parseMetadata(testMD(map[string]any{"readTimeout": "50ms"}))
fc := &fakeConn{} // empty buf — will block on read
err := rh.Handle(context.Background(), fc)
if err == nil {
t.Error("expected error from read timeout")
}
}
func TestHandleConnect_WithRecorder(t *testing.T) {
rec := recorder.RecorderObject{
Record: xrecorder.RecorderServiceHandler,
Recorder: &dummyRecorder{},
}
rh := newConnectHandler(t,
handler.RecordersOption(rec),
)
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
err := rh.Handle(context.Background(), fc)
if err != nil {
t.Fatalf("Handle: %v", err)
}
}