Files
x/handler/relay/helpers_test.go
T
ginuerzh d432d28f81 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
2026-06-02 23:51:19 +08:00

406 lines
12 KiB
Go

package relay
import (
"bytes"
"context"
"errors"
"io"
"net"
"sync"
"time"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/hop"
"github.com/go-gost/core/limiter"
"github.com/go-gost/core/limiter/rate"
"github.com/go-gost/core/limiter/traffic"
"github.com/go-gost/core/logger"
core_metadata "github.com/go-gost/core/metadata"
"github.com/go-gost/core/observer"
"github.com/go-gost/relay"
xmetadata "github.com/go-gost/x/metadata"
)
// ---------------------------------------------------------------------------
// testLogger — silent logger
// ---------------------------------------------------------------------------
type testLogger struct{}
func (l *testLogger) WithFields(map[string]any) logger.Logger { return l }
func (l *testLogger) IsLevelEnabled(logger.LogLevel) bool { return false }
func (l *testLogger) GetLevel() logger.LogLevel { return logger.InfoLevel }
func (l *testLogger) Trace(args ...any) {}
func (l *testLogger) Tracef(string, ...any) {}
func (l *testLogger) Debug(args ...any) {}
func (l *testLogger) Debugf(string, ...any) {}
func (l *testLogger) Info(args ...any) {}
func (l *testLogger) Infof(string, ...any) {}
func (l *testLogger) Warn(args ...any) {}
func (l *testLogger) Warnf(string, ...any) {}
func (l *testLogger) Error(args ...any) {}
func (l *testLogger) Errorf(string, ...any) {}
func (l *testLogger) Fatal(args ...any) {}
func (l *testLogger) Fatalf(string, ...any) {}
// ---------------------------------------------------------------------------
// testMD — wraps a map into metadata.Metadata
// ---------------------------------------------------------------------------
func testMD(m map[string]any) core_metadata.Metadata {
return xmetadata.NewMetadata(m)
}
// ---------------------------------------------------------------------------
// fakeConn — byte-slice backed net.Conn (reads from buf, collects writes)
// ---------------------------------------------------------------------------
type fakeConn struct {
net.Conn
buf []byte
offset int
writeBuf bytes.Buffer
closed bool
mu sync.Mutex
addr net.Addr
}
func (c *fakeConn) Read(b []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.offset >= len(c.buf) {
return 0, io.EOF
}
n := copy(b, c.buf[c.offset:])
c.offset += n
return n, nil
}
func (c *fakeConn) Write(b []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
return c.writeBuf.Write(b)
}
func (c *fakeConn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
c.closed = true
return nil
}
func (c *fakeConn) LocalAddr() net.Addr {
if c.addr != nil {
return c.addr
}
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080}
}
func (c *fakeConn) RemoteAddr() net.Addr {
if c.addr != nil {
return c.addr
}
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345}
}
func (c *fakeConn) SetDeadline(time.Time) error { return nil }
func (c *fakeConn) SetReadDeadline(time.Time) error { return nil }
func (c *fakeConn) SetWriteDeadline(time.Time) error { return nil }
// ---------------------------------------------------------------------------
// pipeConn — bidirectional pipe-based net.Conn using io.Pipe
// ---------------------------------------------------------------------------
type pipeConn struct {
reader *io.PipeReader
writer *io.PipeWriter
laddr net.Addr
raddr net.Addr
closed bool
closeMu sync.Mutex
}
func newPipePair() (*pipeConn, *pipeConn) {
pr1, pw1 := io.Pipe()
pr2, pw2 := io.Pipe()
a := &pipeConn{reader: pr1, writer: pw2}
b := &pipeConn{reader: pr2, writer: pw1}
return a, b
}
func (c *pipeConn) Read(b []byte) (int, error) { return c.reader.Read(b) }
func (c *pipeConn) Write(b []byte) (int, error) { return c.writer.Write(b) }
func (c *pipeConn) Close() error {
c.closeMu.Lock()
defer c.closeMu.Unlock()
if c.closed {
return nil
}
c.closed = true
c.reader.Close()
c.writer.Close()
return nil
}
func (c *pipeConn) LocalAddr() net.Addr {
if c.laddr != nil {
return c.laddr
}
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080}
}
func (c *pipeConn) RemoteAddr() net.Addr {
if c.raddr != nil {
return c.raddr
}
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345}
}
func (c *pipeConn) SetDeadline(time.Time) error { return nil }
func (c *pipeConn) SetReadDeadline(time.Time) error { return nil }
func (c *pipeConn) SetWriteDeadline(time.Time) error { return nil }
// ---------------------------------------------------------------------------
// fakeObserver — channels-based observer with optional error function
// ---------------------------------------------------------------------------
type fakeObserver struct {
eventsCh chan []observer.Event
errFunc func() error // optional; called for each Observe
}
func (o *fakeObserver) Observe(ctx context.Context, events []observer.Event, opts ...observer.Option) error {
if o.errFunc != nil {
if err := o.errFunc(); err != nil {
return err
}
}
select {
case o.eventsCh <- events:
default:
}
return nil
}
func (o *fakeObserver) Events() <-chan []observer.Event { return o.eventsCh }
// ---------------------------------------------------------------------------
// mockRouter — implements chain.Router
// ---------------------------------------------------------------------------
type mockRouter struct {
dialFn func(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error)
}
func (r *mockRouter) Dial(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
if r.dialFn != nil {
return r.dialFn(ctx, network, address, opts...)
}
return nil, errors.New("mockRouter: no dial function")
}
// Required by chain.Router interface (stubs)
func (r *mockRouter) Options() *chain.RouterOptions { return nil }
func (r *mockRouter) Bind(ctx context.Context, network, address string, opts ...chain.BindOption) (net.Listener, error) {
return nil, errors.New("mockRouter: Bind not implemented")
}
// ---------------------------------------------------------------------------
// mockHop — implements hop.Hop
// ---------------------------------------------------------------------------
type mockHop struct {
selectFn func(ctx context.Context) *chain.Node
}
func (h *mockHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
if h.selectFn != nil {
return h.selectFn(ctx)
}
return nil
}
// ---------------------------------------------------------------------------
// mockBypass — implements bypass.Bypass
// ---------------------------------------------------------------------------
type mockBypass struct {
containsFn func(ctx context.Context, network, addr string, opts ...bypass.Option) bool
}
func (b *mockBypass) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
if b.containsFn != nil {
return b.containsFn(ctx, network, addr, opts...)
}
return false
}
func (b *mockBypass) IsWhitelist() bool { return false }
// ---------------------------------------------------------------------------
// mockAuther — implements auth.Authenticator
// ---------------------------------------------------------------------------
type mockAuther struct {
authenticateFn func(ctx context.Context, user, pass string, opts ...auth.Option) (string, bool)
}
func (a *mockAuther) Authenticate(ctx context.Context, user, pass string, opts ...auth.Option) (string, bool) {
if a.authenticateFn != nil {
return a.authenticateFn(ctx, user, pass, opts...)
}
return "", false
}
// ---------------------------------------------------------------------------
// mockRateLimiter — implements rate.Limiter (single rate limiter)
// ---------------------------------------------------------------------------
type mockRateLimiter struct {
allowFn func(n int) bool
waitFn func(ctx context.Context) error
}
func (l *mockRateLimiter) Allow(n int) bool {
if l.allowFn != nil {
return l.allowFn(n)
}
return true
}
func (l *mockRateLimiter) Limit() float64 { return 1 }
func (l *mockRateLimiter) Wait(ctx context.Context) error {
if l.waitFn != nil {
return l.waitFn(ctx)
}
return nil
}
// mockRateLimiterContainer — implements rate.RateLimiter (container by key)
type mockRateLimiterContainer struct {
limiterFn func(key string) rate.Limiter
}
func (c *mockRateLimiterContainer) Limiter(key string) rate.Limiter {
if c.limiterFn != nil {
return c.limiterFn(key)
}
return nil
}
// ---------------------------------------------------------------------------
// mockTrafficLimiter — implements traffic.TrafficLimiter
// ---------------------------------------------------------------------------
type mockTrafficLimiter struct{}
func (l *mockTrafficLimiter) In(ctx context.Context, key string, opts ...limiter.Option) traffic.Limiter {
return nil
}
func (l *mockTrafficLimiter) Out(ctx context.Context, key string, opts ...limiter.Option) traffic.Limiter {
return nil
}
// ---------------------------------------------------------------------------
// buildRelayRequest helpers — serialize relay requests for fakeConn.buf
// ---------------------------------------------------------------------------
func buildRelayConnectRequest(t testingT, address, network string) []byte {
t.Helper()
req := relay.Request{
Version: relay.Version1,
Cmd: relay.CmdConnect,
}
af := &relay.AddrFeature{}
if address != "" {
if err := af.ParseFrom(address); err != nil {
t.Fatal(err)
}
}
req.Features = append(req.Features, af)
if network != "" && network != "tcp" {
req.Features = append(req.Features, &relay.NetworkFeature{
Network: toRelayNetworkID(network),
})
}
var buf bytes.Buffer
if _, err := req.WriteTo(&buf); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
func buildRelayBindRequest(t testingT, address, network string) []byte {
t.Helper()
cmd := relay.CmdBind
if network == "udp" || network == "udp4" || network == "udp6" {
cmd |= relay.FUDP
}
req := relay.Request{
Version: relay.Version1,
Cmd: cmd,
}
af := &relay.AddrFeature{}
if address != "" {
if err := af.ParseFrom(address); err != nil {
t.Fatal(err)
}
}
req.Features = append(req.Features, af)
if network != "" && network != "tcp" {
req.Features = append(req.Features, &relay.NetworkFeature{
Network: toRelayNetworkID(network),
})
}
var buf bytes.Buffer
if _, err := req.WriteTo(&buf); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
func toRelayNetworkID(n string) relay.NetworkID {
switch n {
case "udp", "udp4", "udp6":
return relay.NetworkUDP
case "unix":
return relay.NetworkUnix
case "serial":
return relay.NetworkSerial
default:
return relay.NetworkTCP
}
}
// testingT is a subset of testing.T for use by helper functions.
type testingT interface {
Helper()
Fatal(args ...any)
Fatalf(format string, args ...any)
}
// ---------------------------------------------------------------------------
// makeTestNode — creates a minimal *hop.Node for mockHop returns
// ---------------------------------------------------------------------------
func makeTestNode(addr string) *chain.Node {
return chain.NewNode("test", addr)
}
// ---------------------------------------------------------------------------
// readRelayResponse — deserializes a relay.Response from bytes
// ---------------------------------------------------------------------------
func readRelayResponse(t testingT, data []byte) *relay.Response {
t.Helper()
resp := &relay.Response{}
if _, err := resp.ReadFrom(bytes.NewReader(data)); err != nil {
t.Fatalf("read relay response: %v", err)
}
return resp
}