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:
@@ -0,0 +1,110 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/handler"
|
||||||
|
"github.com/go-gost/relay"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleBind tests use a real TCP listener on port 0 to verify the bind flow.
|
||||||
|
// The Handle method is called with a CmdBind request, and the response is
|
||||||
|
// checked for StatusOK + an AddrFeature.
|
||||||
|
|
||||||
|
func TestHandleBind_Disabled(t *testing.T) {
|
||||||
|
rh := newInitdHandler(t,
|
||||||
|
handler.LoggerOption(&testLogger{}),
|
||||||
|
)
|
||||||
|
// bind is disabled by default
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayBindRequest(t, "127.0.0.1:0", "")}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
// handleBind returns the error from resp.WriteTo(conn) — when the write
|
||||||
|
// succeeds the returned error is nil. The response status indicates the
|
||||||
|
// denial, not the returned error.
|
||||||
|
_ = err
|
||||||
|
|
||||||
|
resp := readRelayResponse(t, fc.writeBuf.Bytes())
|
||||||
|
if resp.Status != relay.StatusForbidden {
|
||||||
|
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusForbidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleBind_TCP(t *testing.T) {
|
||||||
|
rh := &relayHandler{
|
||||||
|
options: handler.Options{
|
||||||
|
Logger: &testLogger{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh.parseMetadata(testMD(map[string]any{"bind": true}))
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayBindRequest(t, "127.0.0.1:0", "")}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
errCh <- rh.Handle(ctx, fc)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for the response (bindTCP writes the response before blocking on Serve)
|
||||||
|
// bindTCP calls resp.WriteTo(conn) then upgrades to mux session.
|
||||||
|
// Since the fakeConn doesn't support mux (it's not a real mux-compatible conn),
|
||||||
|
// the mux.ClientSession call will fail.
|
||||||
|
// But we should at least get the response before that.
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
if err != nil {
|
||||||
|
// bindTCP will fail when mux.ClientSession tries to upgrade the fakeConn
|
||||||
|
// That's expected — the response was written before that
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for bind handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the response — should have StatusOK and an AddrFeature
|
||||||
|
resp := readRelayResponse(t, fc.writeBuf.Bytes())
|
||||||
|
if resp.Status != relay.StatusOK {
|
||||||
|
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify response has at least one feature
|
||||||
|
if len(resp.Features) == 0 {
|
||||||
|
t.Error("response has no features, expected AddrFeature")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleBind_UDP(t *testing.T) {
|
||||||
|
rh := &relayHandler{
|
||||||
|
options: handler.Options{
|
||||||
|
Logger: &testLogger{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh.parseMetadata(testMD(map[string]any{"bind": true}))
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayBindRequest(t, "127.0.0.1:0", "udp")}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
errCh <- rh.Handle(ctx, fc)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
if err != nil {
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for bind handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := readRelayResponse(t, fc.writeBuf.Bytes())
|
||||||
|
if resp.Status != relay.StatusOK {
|
||||||
|
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTCPConn_WriteWithCachedHeader(t *testing.T) {
|
||||||
|
var underlying bytes.Buffer
|
||||||
|
tc := &tcpConn{
|
||||||
|
Conn: &fakeConn{}, // embedded net.Conn — just for Read
|
||||||
|
}
|
||||||
|
// Simulate a cached header
|
||||||
|
tc.wbuf.Write([]byte("HEADER "))
|
||||||
|
|
||||||
|
n, err := tc.Write([]byte("data"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != 4 {
|
||||||
|
t.Errorf("n = %d, want 4", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The wbuf was flushed: header + data
|
||||||
|
combined := tc.wbuf.Bytes()
|
||||||
|
if len(combined) != 0 {
|
||||||
|
t.Errorf("wbuf should be empty after flush, got %d bytes", len(combined))
|
||||||
|
}
|
||||||
|
|
||||||
|
// tcpConn has no underlying writeBuf — writes go to the embedded conn
|
||||||
|
_ = underlying
|
||||||
|
_ = tc.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTCPConn_WriteWithoutCachedHeader(t *testing.T) {
|
||||||
|
// Nothing special to test — writes delegate to Conn.Write
|
||||||
|
// Just verify no panic and returns correctly
|
||||||
|
tc := &tcpConn{Conn: &fakeConn{}}
|
||||||
|
n, err := tc.Write([]byte("data"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != 4 {
|
||||||
|
t.Errorf("n = %d, want 4", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTCPConn_ReadDelegates(t *testing.T) {
|
||||||
|
buf := []byte("hello world")
|
||||||
|
fc := &fakeConn{buf: buf}
|
||||||
|
tc := &tcpConn{Conn: fc}
|
||||||
|
|
||||||
|
out := make([]byte, 5)
|
||||||
|
n, err := tc.Read(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != 5 || string(out[:n]) != "hello" {
|
||||||
|
t.Errorf("Read = %q, want %q", string(out[:n]), "hello")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPConn_ReadWithLengthPrefix(t *testing.T) {
|
||||||
|
// udpConn.Read expects [2-byte length][data]
|
||||||
|
payload := []byte("hello")
|
||||||
|
prefixed := make([]byte, 2+len(payload))
|
||||||
|
prefixed[0] = 0
|
||||||
|
prefixed[1] = byte(len(payload))
|
||||||
|
copy(prefixed[2:], payload)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: prefixed}
|
||||||
|
uc := &udpConn{Conn: fc}
|
||||||
|
|
||||||
|
out := make([]byte, 10)
|
||||||
|
n, err := uc.Read(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != 5 {
|
||||||
|
t.Errorf("n = %d, want 5", n)
|
||||||
|
}
|
||||||
|
if string(out[:n]) != "hello" {
|
||||||
|
t.Errorf("Read = %q, want %q", string(out[:n]), "hello")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPConn_ReadTruncated(t *testing.T) {
|
||||||
|
// When the read buffer is smaller than the UDP payload
|
||||||
|
payload := []byte("hello world")
|
||||||
|
prefixed := make([]byte, 2+len(payload))
|
||||||
|
prefixed[0] = 0
|
||||||
|
prefixed[1] = byte(len(payload))
|
||||||
|
copy(prefixed[2:], payload)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: prefixed}
|
||||||
|
uc := &udpConn{Conn: fc}
|
||||||
|
|
||||||
|
// Read with a small buffer
|
||||||
|
out := make([]byte, 5)
|
||||||
|
n, err := uc.Read(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != 5 {
|
||||||
|
t.Errorf("n = %d, want 5", n)
|
||||||
|
}
|
||||||
|
if string(out[:n]) != "hello" {
|
||||||
|
t.Errorf("Read = %q, want %q", string(out[:n]), "hello")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPConn_WriteWithCachedHeader(t *testing.T) {
|
||||||
|
fc := &fakeConn{}
|
||||||
|
uc := &udpConn{Conn: fc}
|
||||||
|
// Simulate a cached header (e.g., the relay response)
|
||||||
|
uc.wbuf.Write([]byte("HEADER "))
|
||||||
|
|
||||||
|
n, err := uc.Write([]byte("hi"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != 2 {
|
||||||
|
t.Errorf("n = %d, want 2", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the wbuf was flushed
|
||||||
|
if uc.wbuf.Len() > 0 {
|
||||||
|
t.Errorf("wbuf should be empty after flush, got %d bytes", uc.wbuf.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// The header + [2-byte length] + "hi" should be written to fc
|
||||||
|
written := fc.writeBuf.Bytes()
|
||||||
|
if len(written) == 0 {
|
||||||
|
t.Fatal("nothing written to underlying conn")
|
||||||
|
}
|
||||||
|
// Should contain: "HEADER " + length prefix + "hi"
|
||||||
|
expected := []byte("HEADER ")
|
||||||
|
expected = append(expected, 0x00, 0x02) // length prefix
|
||||||
|
expected = append(expected, 'h', 'i')
|
||||||
|
if !bytes.Equal(written, expected) {
|
||||||
|
t.Errorf("written = %v, want %v", written, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPConn_WriteWithoutCachedHeader(t *testing.T) {
|
||||||
|
fc := &fakeConn{}
|
||||||
|
uc := &udpConn{Conn: fc}
|
||||||
|
|
||||||
|
n, err := uc.Write([]byte("hi"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != 2 {
|
||||||
|
t.Errorf("n = %d, want 2", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should write [2-byte length] + "hi"
|
||||||
|
written := fc.writeBuf.Bytes()
|
||||||
|
if len(written) < 2 {
|
||||||
|
t.Fatal("nothing written to underlying conn")
|
||||||
|
}
|
||||||
|
if written[0] != 0 || written[1] != 2 {
|
||||||
|
t.Errorf("length prefix = %v, want [0, 2]", written[:2])
|
||||||
|
}
|
||||||
|
if string(written[2:]) != "hi" {
|
||||||
|
t.Errorf("data = %q, want %q", string(written[2:]), "hi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPConn_WriteExceedsMaxLength(t *testing.T) {
|
||||||
|
fc := &fakeConn{}
|
||||||
|
uc := &udpConn{Conn: fc}
|
||||||
|
|
||||||
|
// Write more than MaxUint16 bytes
|
||||||
|
data := make([]byte, 65536)
|
||||||
|
_, err := uc.Write(data)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for data > 65535 bytes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTCPConn_WriteWithCachedHeaderAndFlush(t *testing.T) {
|
||||||
|
fc := &fakeConn{}
|
||||||
|
tc := &tcpConn{Conn: fc}
|
||||||
|
tc.wbuf.Write([]byte("RESPONSE "))
|
||||||
|
|
||||||
|
n, err := tc.Write([]byte("body"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != 4 {
|
||||||
|
t.Errorf("n = %d, want 4", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wbuf should be empty after flush
|
||||||
|
if tc.wbuf.Len() > 0 {
|
||||||
|
t.Errorf("wbuf should be empty, got %d bytes", tc.wbuf.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// The conn should have: "RESPONSE body"
|
||||||
|
written := fc.writeBuf.Bytes()
|
||||||
|
if string(written) != "RESPONSE body" {
|
||||||
|
t.Errorf("written = %q, want %q", string(written), "RESPONSE body")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPConn_ReadEmpty(t *testing.T) {
|
||||||
|
// Read on an empty connection should return io.EOF
|
||||||
|
fc := &fakeConn{} // no buf
|
||||||
|
uc := &udpConn{Conn: fc}
|
||||||
|
|
||||||
|
_, err := uc.Read(make([]byte, 10))
|
||||||
|
if err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||||
|
t.Errorf("expected EOF, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPConn_ReadPartial(t *testing.T) {
|
||||||
|
// Length says 10 bytes but only 3 are available
|
||||||
|
prefixed := []byte{0x00, 0x0A, 'a', 'b', 'c'}
|
||||||
|
fc := &fakeConn{buf: prefixed}
|
||||||
|
uc := &udpConn{Conn: fc}
|
||||||
|
|
||||||
|
_, err := uc.Read(make([]byte, 10))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for truncated data")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/chain"
|
||||||
|
"github.com/go-gost/core/handler"
|
||||||
|
"github.com/go-gost/core/observer"
|
||||||
|
"github.com/go-gost/relay"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newForwardHandler(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
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
rh := newInitdHandler(t, append(defaultOpts, opts...)...)
|
||||||
|
return rh
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleForward_NoTarget(t *testing.T) {
|
||||||
|
mh := &mockHop{
|
||||||
|
selectFn: func(ctx context.Context) *chain.Node {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newForwardHandler(t)
|
||||||
|
rh.Forward(mh)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err == nil || err.Error() != "target not available" {
|
||||||
|
t.Errorf("err = %v, want target not available", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := readRelayResponse(t, fc.writeBuf.Bytes())
|
||||||
|
if resp.Status != relay.StatusServiceUnavailable {
|
||||||
|
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusServiceUnavailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleForward_DialSucceeds(t *testing.T) {
|
||||||
|
mh := &mockHop{
|
||||||
|
selectFn: func(ctx context.Context) *chain.Node {
|
||||||
|
return makeTestNode("example.com:80")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newForwardHandler(t)
|
||||||
|
rh.md.noDelay = true
|
||||||
|
rh.Forward(mh)
|
||||||
|
|
||||||
|
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 TestHandleForward_DialFails(t *testing.T) {
|
||||||
|
mh := &mockHop{
|
||||||
|
selectFn: func(ctx context.Context) *chain.Node {
|
||||||
|
return makeTestNode("example.com:80")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newInitdHandler(t,
|
||||||
|
handler.LoggerOption(&testLogger{}),
|
||||||
|
handler.RouterOption(&mockRouter{
|
||||||
|
dialFn: func(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
|
||||||
|
return nil, net.UnknownNetworkError("mock failure")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
rh.Forward(mh)
|
||||||
|
|
||||||
|
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.StatusHostUnreachable {
|
||||||
|
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusHostUnreachable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleForward_NoDelay(t *testing.T) {
|
||||||
|
mh := &mockHop{
|
||||||
|
selectFn: func(ctx context.Context) *chain.Node {
|
||||||
|
return makeTestNode("example.com:80")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newForwardHandler(t)
|
||||||
|
rh.md.noDelay = true
|
||||||
|
rh.Forward(mh)
|
||||||
|
|
||||||
|
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 TestHandleForward_UDP(t *testing.T) {
|
||||||
|
mh := &mockHop{
|
||||||
|
selectFn: func(ctx context.Context) *chain.Node {
|
||||||
|
return makeTestNode("example.com:53")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newForwardHandler(t)
|
||||||
|
rh.md.noDelay = true
|
||||||
|
rh.Forward(mh)
|
||||||
|
|
||||||
|
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 TestHandleForward_NilMarker(t *testing.T) {
|
||||||
|
// Create a node with no marker (nil Marker)
|
||||||
|
mh := &mockHop{
|
||||||
|
selectFn: func(ctx context.Context) *chain.Node {
|
||||||
|
return chain.NewNode("test", "example.com:80")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newForwardHandler(t)
|
||||||
|
rh.md.noDelay = true
|
||||||
|
rh.Forward(mh)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handle: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleForward_WithObserver(t *testing.T) {
|
||||||
|
obs := &fakeObserver{eventsCh: make(chan []observer.Event, 10)}
|
||||||
|
mh := &mockHop{
|
||||||
|
selectFn: func(ctx context.Context) *chain.Node {
|
||||||
|
return makeTestNode("example.com:80")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newForwardHandler(t,
|
||||||
|
handler.ObserverOption(obs),
|
||||||
|
handler.ServiceOption("test-svc"),
|
||||||
|
)
|
||||||
|
rh.md.noDelay = true
|
||||||
|
rh.Forward(mh)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handle: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/go-gost/core/limiter"
|
"github.com/go-gost/core/limiter"
|
||||||
"github.com/go-gost/core/limiter/traffic"
|
"github.com/go-gost/core/limiter/traffic"
|
||||||
md "github.com/go-gost/core/metadata"
|
md "github.com/go-gost/core/metadata"
|
||||||
"github.com/go-gost/core/observer"
|
|
||||||
"github.com/go-gost/core/observer/stats"
|
"github.com/go-gost/core/observer/stats"
|
||||||
"github.com/go-gost/core/recorder"
|
"github.com/go-gost/core/recorder"
|
||||||
"github.com/go-gost/relay"
|
"github.com/go-gost/relay"
|
||||||
@@ -245,45 +244,3 @@ func (h *relayHandler) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *relayHandler) checkRateLimit(addr net.Addr) bool {
|
|
||||||
if h.options.RateLimiter == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
host, _, _ := net.SplitHostPort(addr.String())
|
|
||||||
if limiter := h.options.RateLimiter.Limiter(host); limiter != nil {
|
|
||||||
return limiter.Allow(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *relayHandler) observeStats(ctx context.Context) {
|
|
||||||
if h.options.Observer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var events []observer.Event
|
|
||||||
|
|
||||||
ticker := time.NewTicker(h.md.observerPeriod)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
if len(events) > 0 {
|
|
||||||
if err := h.options.Observer.Observe(ctx, events); err == nil {
|
|
||||||
events = nil
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
evs := h.stats.Events()
|
|
||||||
if err := h.options.Observer.Observe(ctx, evs); err != nil {
|
|
||||||
events = evs
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,482 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/auth"
|
||||||
|
"github.com/go-gost/core/bypass"
|
||||||
|
"github.com/go-gost/core/chain"
|
||||||
|
"github.com/go-gost/core/handler"
|
||||||
|
"github.com/go-gost/core/limiter/rate"
|
||||||
|
"github.com/go-gost/core/observer"
|
||||||
|
"github.com/go-gost/core/recorder"
|
||||||
|
"github.com/go-gost/relay"
|
||||||
|
xbypass "github.com/go-gost/x/bypass"
|
||||||
|
rate_limiter "github.com/go-gost/x/limiter/rate"
|
||||||
|
xrecorder "github.com/go-gost/x/recorder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// newInitdHandler — convenience: construct + Init
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func newInitdHandler(t *testing.T, opts ...handler.Option) *relayHandler {
|
||||||
|
t.Helper()
|
||||||
|
// Ensure a logger is always set
|
||||||
|
h := NewHandler(append([]handler.Option{handler.LoggerOption(&testLogger{})}, opts...)...).(*relayHandler)
|
||||||
|
if err := h.Init(testMD(nil)); err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// NewHandler
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestNewHandler_Minimal(t *testing.T) {
|
||||||
|
h := NewHandler()
|
||||||
|
if h == nil {
|
||||||
|
t.Fatal("handler is nil")
|
||||||
|
}
|
||||||
|
rh := h.(*relayHandler)
|
||||||
|
if rh.options.Service != "" {
|
||||||
|
t.Errorf("Service = %q, want empty", rh.options.Service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHandler_WithOptions(t *testing.T) {
|
||||||
|
log := &testLogger{}
|
||||||
|
h := NewHandler(
|
||||||
|
handler.LoggerOption(log),
|
||||||
|
handler.ServiceOption("test-svc"),
|
||||||
|
)
|
||||||
|
rh := h.(*relayHandler)
|
||||||
|
if rh.options.Logger == nil {
|
||||||
|
t.Error("Logger not set")
|
||||||
|
}
|
||||||
|
if rh.options.Service != "test-svc" {
|
||||||
|
t.Errorf("Service = %q, want test-svc", rh.options.Service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Init
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestInit_Minimal(t *testing.T) {
|
||||||
|
rh := NewHandler().(*relayHandler)
|
||||||
|
if err := rh.Init(testMD(nil)); err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
if rh.cancel == nil {
|
||||||
|
t.Error("cancel is nil")
|
||||||
|
}
|
||||||
|
if rh.stats != nil {
|
||||||
|
t.Error("stats should be nil (no observer)")
|
||||||
|
}
|
||||||
|
if rh.limiter != nil {
|
||||||
|
t.Error("limiter should be nil (no limiter)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_WithObserver(t *testing.T) {
|
||||||
|
fakeObs := &fakeObserver{eventsCh: make(chan []observer.Event, 10)}
|
||||||
|
rh := NewHandler(
|
||||||
|
handler.ObserverOption(fakeObs),
|
||||||
|
handler.ServiceOption("test-svc"),
|
||||||
|
).(*relayHandler)
|
||||||
|
if err := rh.Init(testMD(nil)); err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
if rh.stats == nil {
|
||||||
|
t.Error("stats should be non-nil (observer set)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_WithLimiter(t *testing.T) {
|
||||||
|
ml := &mockTrafficLimiter{}
|
||||||
|
rh := NewHandler(
|
||||||
|
handler.TrafficLimiterOption(ml),
|
||||||
|
).(*relayHandler)
|
||||||
|
if err := rh.Init(testMD(nil)); err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
if rh.limiter == nil {
|
||||||
|
t.Error("limiter should be non-nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_WithRecorder(t *testing.T) {
|
||||||
|
rec := recorder.RecorderObject{
|
||||||
|
Record: xrecorder.RecorderServiceHandler,
|
||||||
|
Recorder: &dummyRecorder{},
|
||||||
|
}
|
||||||
|
rh := NewHandler(
|
||||||
|
handler.RecordersOption(rec),
|
||||||
|
).(*relayHandler)
|
||||||
|
if err := rh.Init(testMD(nil)); err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
if rh.recorder.Recorder == nil {
|
||||||
|
t.Error("recorder should be non-nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_CertPoolNilWithoutCerts(t *testing.T) {
|
||||||
|
rh := NewHandler().(*relayHandler)
|
||||||
|
if err := rh.Init(testMD(nil)); err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
if rh.certPool != nil {
|
||||||
|
t.Error("certPool should be nil (no certs configured)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Forward
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestForward_SetsHop(t *testing.T) {
|
||||||
|
rh := newInitdHandler(t)
|
||||||
|
mh := &mockHop{}
|
||||||
|
rh.Forward(mh)
|
||||||
|
if rh.hop != mh {
|
||||||
|
t.Error("hop not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Close
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestClose_CancelsContext(t *testing.T) {
|
||||||
|
rh := newInitdHandler(t)
|
||||||
|
if rh.cancel == nil {
|
||||||
|
t.Fatal("cancel is nil")
|
||||||
|
}
|
||||||
|
if err := rh.Close(); err != nil {
|
||||||
|
t.Errorf("Close: %v", err)
|
||||||
|
}
|
||||||
|
// Double close is safe
|
||||||
|
if err := rh.Close(); err != nil {
|
||||||
|
t.Errorf("Close (second): %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClose_WithoutInit(t *testing.T) {
|
||||||
|
rh := NewHandler().(*relayHandler)
|
||||||
|
if err := rh.Close(); err != nil {
|
||||||
|
t.Errorf("Close: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Handle — error cases
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandle_BadVersion(t *testing.T) {
|
||||||
|
rh := newInitdHandler(t)
|
||||||
|
|
||||||
|
req := relay.Request{Version: 0xFF, Cmd: relay.CmdConnect}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
req.WriteTo(&buf)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buf.Bytes()}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
// The relay library's Request.ReadFrom returns ErrBadVersion on version
|
||||||
|
// mismatch before the handler can write a response. The error is returned
|
||||||
|
// directly without a response frame.
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
// No response is written because ReadFrom fails before the handler's
|
||||||
|
// version check in Handle.
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_UnknownCmd(t *testing.T) {
|
||||||
|
rh := newInitdHandler(t)
|
||||||
|
|
||||||
|
req := relay.Request{Version: relay.Version1, Cmd: 0xFF}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
req.WriteTo(&buf)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buf.Bytes()}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err == nil || err.Error() != "relay: unknown command" {
|
||||||
|
t.Errorf("err = %v, want relay: unknown command", err)
|
||||||
|
}
|
||||||
|
resp := readRelayResponse(t, fc.writeBuf.Bytes())
|
||||||
|
if resp.Status != relay.StatusBadRequest {
|
||||||
|
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_EmptyAddress(t *testing.T) {
|
||||||
|
rh := newInitdHandler(t)
|
||||||
|
|
||||||
|
// Build a connect request with an AddrFeature that has empty host
|
||||||
|
req := relay.Request{Version: relay.Version1, Cmd: relay.CmdConnect}
|
||||||
|
req.Features = append(req.Features, &relay.AddrFeature{})
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_WithBypass(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))
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err != xbypass.ErrBypass {
|
||||||
|
t.Errorf("err = %v, want %v", err, xbypass.ErrBypass)
|
||||||
|
}
|
||||||
|
resp := readRelayResponse(t, fc.writeBuf.Bytes())
|
||||||
|
if resp.Status != relay.StatusForbidden {
|
||||||
|
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusForbidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_Unauthorized(t *testing.T) {
|
||||||
|
ma := &mockAuther{
|
||||||
|
authenticateFn: func(ctx context.Context, user, pass string, opts ...auth.Option) (string, bool) {
|
||||||
|
return "", false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newInitdHandler(t, handler.AutherOption(ma))
|
||||||
|
|
||||||
|
req := relay.Request{Version: relay.Version1, Cmd: relay.CmdConnect}
|
||||||
|
req.Features = append(req.Features, &relay.UserAuthFeature{Username: "user", Password: "pass"})
|
||||||
|
af := &relay.AddrFeature{}
|
||||||
|
af.ParseFrom("example.com:80")
|
||||||
|
req.Features = append(req.Features, af)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
req.WriteTo(&buf)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buf.Bytes()}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err == nil || err.Error() != "relay: unauthorized" {
|
||||||
|
t.Errorf("err = %v, want relay: unauthorized", err)
|
||||||
|
}
|
||||||
|
resp := readRelayResponse(t, fc.writeBuf.Bytes())
|
||||||
|
if resp.Status != relay.StatusUnauthorized {
|
||||||
|
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_Authorized(t *testing.T) {
|
||||||
|
ma := &mockAuther{
|
||||||
|
authenticateFn: func(ctx context.Context, user, pass string, opts ...auth.Option) (string, bool) {
|
||||||
|
return "client-1", true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mr := &mockRouter{
|
||||||
|
dialFn: func(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
|
||||||
|
pr, pw := net.Pipe()
|
||||||
|
pw.Close()
|
||||||
|
return pr, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newInitdHandler(t,
|
||||||
|
handler.AutherOption(ma),
|
||||||
|
handler.RouterOption(mr),
|
||||||
|
)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handle: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_RateLimited(t *testing.T) {
|
||||||
|
mrc := &mockRateLimiterContainer{
|
||||||
|
limiterFn: func(key string) rate.Limiter {
|
||||||
|
return &mockRateLimiter{
|
||||||
|
allowFn: func(n int) bool { return false },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := NewHandler(
|
||||||
|
handler.RateLimiterOption(mrc),
|
||||||
|
handler.LoggerOption(&testLogger{}),
|
||||||
|
).(*relayHandler)
|
||||||
|
if err := rh.Init(testMD(nil)); err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fc := &fakeConn{}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err != rate_limiter.ErrRateLimit {
|
||||||
|
t.Errorf("err = %v, want %v", err, rate_limiter.ErrRateLimit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_ReadTimeout(t *testing.T) {
|
||||||
|
h := &relayHandler{
|
||||||
|
options: handler.Options{Logger: &testLogger{}},
|
||||||
|
}
|
||||||
|
h.parseMetadata(testMD(map[string]any{"readTimeout": "100ms"}))
|
||||||
|
|
||||||
|
fc := &fakeConn{}
|
||||||
|
err := h.Handle(context.Background(), fc)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error from read timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_ContextCancelled(t *testing.T) {
|
||||||
|
mr := &mockRouter{
|
||||||
|
dialFn: func(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
|
||||||
|
pr, _ := net.Pipe()
|
||||||
|
return pr, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newInitdHandler(t, handler.RouterOption(mr))
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
|
||||||
|
// The request is read from conn first (which succeeds because fakeConn has data),
|
||||||
|
// then handleConnect tries to dial, which may fail if ctx is cancelled.
|
||||||
|
// This test verifies no panic occurs with cancelled context.
|
||||||
|
err := rh.Handle(ctx, fc)
|
||||||
|
_ = err // may or may not error depending on router dial behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Handle — forward mode
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandle_ForwardMode_NoTarget(t *testing.T) {
|
||||||
|
mh := &mockHop{
|
||||||
|
selectFn: func(ctx context.Context) *chain.Node {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newInitdHandler(t, handler.RouterOption(&mockRouter{}))
|
||||||
|
rh.Forward(mh)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err == nil || err.Error() != "target not available" {
|
||||||
|
t.Errorf("err = %v, want target not available", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_ForwardMode_TargetFound(t *testing.T) {
|
||||||
|
mh := &mockHop{
|
||||||
|
selectFn: func(ctx context.Context) *chain.Node {
|
||||||
|
return makeTestNode("example.com:80")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mr := &mockRouter{
|
||||||
|
dialFn: func(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
|
||||||
|
pr, pw := net.Pipe()
|
||||||
|
pw.Close()
|
||||||
|
return pr, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := newInitdHandler(t, handler.RouterOption(mr))
|
||||||
|
rh.Forward(mh)
|
||||||
|
|
||||||
|
fc := &fakeConn{buf: buildRelayConnectRequest(t, "example.com:80", "")}
|
||||||
|
err := rh.Handle(context.Background(), fc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handle: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// observeStats
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestObserveStats_NilObserver(t *testing.T) {
|
||||||
|
rh := &relayHandler{}
|
||||||
|
rh.observeStats(context.Background()) // should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObserveStats_NormalTick(t *testing.T) {
|
||||||
|
obs := &fakeObserver{eventsCh: make(chan []observer.Event, 10)}
|
||||||
|
rh := &relayHandler{
|
||||||
|
options: handler.Options{Observer: obs, Logger: &testLogger{}},
|
||||||
|
md: metadata{observerPeriod: 50 * time.Millisecond},
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
rh.observeStats(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObserveStats_Cancel(t *testing.T) {
|
||||||
|
obs := &fakeObserver{eventsCh: make(chan []observer.Event, 10)}
|
||||||
|
rh := &relayHandler{
|
||||||
|
options: handler.Options{Observer: obs},
|
||||||
|
md: metadata{observerPeriod: time.Hour},
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
rh.observeStats(ctx) // should return immediately
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// checkRateLimit
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestCheckRateLimit_NoLimiter(t *testing.T) {
|
||||||
|
rh := &relayHandler{}
|
||||||
|
if !rh.checkRateLimit(&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345}) {
|
||||||
|
t.Error("should allow when no rate limiter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckRateLimit_NoLimiterForKey(t *testing.T) {
|
||||||
|
mrc := &mockRateLimiterContainer{
|
||||||
|
limiterFn: func(key string) rate.Limiter { return nil },
|
||||||
|
}
|
||||||
|
rh := &relayHandler{
|
||||||
|
options: handler.Options{RateLimiter: mrc},
|
||||||
|
}
|
||||||
|
if !rh.checkRateLimit(&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345}) {
|
||||||
|
t.Error("should allow when no limiter for key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckRateLimit_LimiterDenies(t *testing.T) {
|
||||||
|
mrc := &mockRateLimiterContainer{
|
||||||
|
limiterFn: func(key string) rate.Limiter {
|
||||||
|
return &mockRateLimiter{
|
||||||
|
allowFn: func(n int) bool { return false },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rh := &relayHandler{
|
||||||
|
options: handler.Options{RateLimiter: mrc},
|
||||||
|
}
|
||||||
|
if rh.checkRateLimit(&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345}) {
|
||||||
|
t.Error("should deny when limiter says no")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// dummyRecorder
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type dummyRecorder struct{}
|
||||||
|
|
||||||
|
func (r *dummyRecorder) Record(ctx context.Context, b []byte, opts ...recorder.RecordOption) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
core_metadata "github.com/go-gost/core/metadata"
|
||||||
|
xmetadata "github.com/go-gost/x/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseMetadata_Defaults(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
err := h.parseMetadata(xmetadata.NewMetadata(nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.md.readTimeout != 15*time.Second {
|
||||||
|
t.Errorf("readTimeout = %v, want 15s", h.md.readTimeout)
|
||||||
|
}
|
||||||
|
if h.md.enableBind {
|
||||||
|
t.Error("enableBind = true, want false")
|
||||||
|
}
|
||||||
|
if h.md.noDelay {
|
||||||
|
t.Error("noDelay = true, want false")
|
||||||
|
}
|
||||||
|
if h.md.hash != "" {
|
||||||
|
t.Errorf("hash = %q, want empty", h.md.hash)
|
||||||
|
}
|
||||||
|
if h.md.udpBufferSize != 0 {
|
||||||
|
t.Errorf("udpBufferSize = %d, want 0", h.md.udpBufferSize)
|
||||||
|
}
|
||||||
|
if h.md.observerPeriod != 5*time.Second {
|
||||||
|
t.Errorf("observerPeriod = %v, want 5s", h.md.observerPeriod)
|
||||||
|
}
|
||||||
|
if h.md.observerResetTraffic {
|
||||||
|
t.Error("observerResetTraffic = true, want false")
|
||||||
|
}
|
||||||
|
if h.md.sniffing {
|
||||||
|
t.Error("sniffing = true, want false")
|
||||||
|
}
|
||||||
|
if h.md.muxCfg == nil {
|
||||||
|
t.Fatal("muxCfg = nil, want non-nil")
|
||||||
|
}
|
||||||
|
// Note: relay handler does NOT default muxCfg.Version or MaxStreamBuffer
|
||||||
|
// at parseMetadata time. Those defaults are in the tunnel handler only.
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_ReadTimeout(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input core_metadata.Metadata
|
||||||
|
want time.Duration
|
||||||
|
}{
|
||||||
|
{"zero", testMD(map[string]any{"readTimeout": 0}), 15 * time.Second},
|
||||||
|
{"string 30s", testMD(map[string]any{"readTimeout": "30s"}), 30 * time.Second},
|
||||||
|
{"int seconds", testMD(map[string]any{"readTimeout": 10}), 10 * time.Second},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(tt.input)
|
||||||
|
if h.md.readTimeout != tt.want {
|
||||||
|
t.Errorf("readTimeout = %v, want %v", h.md.readTimeout, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_UDPBufferSize(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input core_metadata.Metadata
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"zero", testMD(nil), 0},
|
||||||
|
{"udp.bufferSize", testMD(map[string]any{"udp.bufferSize": 4096}), 4096},
|
||||||
|
{"udpBufferSize alias", testMD(map[string]any{"udpBufferSize": 8192}), 8192},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(tt.input)
|
||||||
|
if h.md.udpBufferSize != tt.want {
|
||||||
|
t.Errorf("udpBufferSize = %d, want %d", h.md.udpBufferSize, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_Bind(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(testMD(map[string]any{"bind": true}))
|
||||||
|
if !h.md.enableBind {
|
||||||
|
t.Error("enableBind = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_NoDelay(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(testMD(map[string]any{"nodelay": true}))
|
||||||
|
if !h.md.noDelay {
|
||||||
|
t.Error("noDelay = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_Hash(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(testMD(map[string]any{"hash": "host"}))
|
||||||
|
if h.md.hash != "host" {
|
||||||
|
t.Errorf("hash = %q, want host", h.md.hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_MuxConfig(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(testMD(map[string]any{
|
||||||
|
"mux.version": 3,
|
||||||
|
"mux.keepaliveInterval": "10s",
|
||||||
|
"mux.keepaliveDisabled": true,
|
||||||
|
"mux.keepaliveTimeout": "5s",
|
||||||
|
"mux.maxFrameSize": 4096,
|
||||||
|
"mux.maxReceiveBuffer": 8192,
|
||||||
|
"mux.maxStreamBuffer": 65536,
|
||||||
|
}))
|
||||||
|
if h.md.muxCfg.Version != 3 {
|
||||||
|
t.Errorf("muxCfg.Version = %d, want 3", h.md.muxCfg.Version)
|
||||||
|
}
|
||||||
|
if h.md.muxCfg.KeepAliveInterval != 10*time.Second {
|
||||||
|
t.Errorf("muxCfg.KeepAliveInterval = %v, want 10s", h.md.muxCfg.KeepAliveInterval)
|
||||||
|
}
|
||||||
|
if !h.md.muxCfg.KeepAliveDisabled {
|
||||||
|
t.Error("muxCfg.KeepAliveDisabled = false, want true")
|
||||||
|
}
|
||||||
|
if h.md.muxCfg.KeepAliveTimeout != 5*time.Second {
|
||||||
|
t.Errorf("muxCfg.KeepAliveTimeout = %v, want 5s", h.md.muxCfg.KeepAliveTimeout)
|
||||||
|
}
|
||||||
|
if h.md.muxCfg.MaxFrameSize != 4096 {
|
||||||
|
t.Errorf("muxCfg.MaxFrameSize = %d, want 4096", h.md.muxCfg.MaxFrameSize)
|
||||||
|
}
|
||||||
|
if h.md.muxCfg.MaxReceiveBuffer != 8192 {
|
||||||
|
t.Errorf("muxCfg.MaxReceiveBuffer = %d, want 8192", h.md.muxCfg.MaxReceiveBuffer)
|
||||||
|
}
|
||||||
|
if h.md.muxCfg.MaxStreamBuffer != 65536 {
|
||||||
|
t.Errorf("muxCfg.MaxStreamBuffer = %d, want 65536", h.md.muxCfg.MaxStreamBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_Observer(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input core_metadata.Metadata
|
||||||
|
wantPeriod time.Duration
|
||||||
|
wantReset bool
|
||||||
|
}{
|
||||||
|
{"defaults", testMD(nil), 5 * time.Second, false},
|
||||||
|
{"custom period", testMD(map[string]any{"observePeriod": "10s"}), 10 * time.Second, false},
|
||||||
|
{"clamped below 1s", testMD(map[string]any{"observePeriod": "500ms"}), time.Second, false},
|
||||||
|
{"alias observer.period", testMD(map[string]any{"observer.period": "3s"}), 3 * time.Second, false},
|
||||||
|
{"alias observer.observePeriod", testMD(map[string]any{"observer.observePeriod": "7s"}), 7 * time.Second, false},
|
||||||
|
{"reset traffic", testMD(map[string]any{"observer.resetTraffic": true}), 5 * time.Second, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(tt.input)
|
||||||
|
if h.md.observerPeriod != tt.wantPeriod {
|
||||||
|
t.Errorf("observerPeriod = %v, want %v", h.md.observerPeriod, tt.wantPeriod)
|
||||||
|
}
|
||||||
|
if h.md.observerResetTraffic != tt.wantReset {
|
||||||
|
t.Errorf("observerResetTraffic = %v, want %v", h.md.observerResetTraffic, tt.wantReset)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_Sniffing(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(testMD(map[string]any{
|
||||||
|
"sniffing": true,
|
||||||
|
"sniffing.timeout": "2s",
|
||||||
|
"sniffing.websocket": true,
|
||||||
|
"sniffing.websocket.sampleRate": 0.5,
|
||||||
|
}))
|
||||||
|
if !h.md.sniffing {
|
||||||
|
t.Error("sniffing = false, want true")
|
||||||
|
}
|
||||||
|
if h.md.sniffingTimeout != 2*time.Second {
|
||||||
|
t.Errorf("sniffingTimeout = %v, want 2s", h.md.sniffingTimeout)
|
||||||
|
}
|
||||||
|
if !h.md.sniffingWebsocket {
|
||||||
|
t.Error("sniffingWebsocket = false, want true")
|
||||||
|
}
|
||||||
|
if h.md.sniffingWebsocketSampleRate != 0.5 {
|
||||||
|
t.Errorf("sniffingWebsocketSampleRate = %v, want 0.5", h.md.sniffingWebsocketSampleRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_LimiterRefresh(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(testMD(map[string]any{
|
||||||
|
"limiter.refreshInterval": "30s",
|
||||||
|
"limiter.cleanupInterval": "60s",
|
||||||
|
}))
|
||||||
|
if h.md.limiterRefreshInterval != 30*time.Second {
|
||||||
|
t.Errorf("limiterRefreshInterval = %v, want 30s", h.md.limiterRefreshInterval)
|
||||||
|
}
|
||||||
|
if h.md.limiterCleanupInterval != 60*time.Second {
|
||||||
|
t.Errorf("limiterCleanupInterval = %v, want 60s", h.md.limiterCleanupInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_Mitm(t *testing.T) {
|
||||||
|
t.Run("not set", func(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
err := h.parseMetadata(testMD(nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if h.md.certificate != nil {
|
||||||
|
t.Error("certificate should be nil")
|
||||||
|
}
|
||||||
|
if h.md.privateKey != nil {
|
||||||
|
t.Error("privateKey should be nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cert file not found", func(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
err := h.parseMetadata(testMD(map[string]any{
|
||||||
|
"mitm.certFile": "/nonexistent/cert.pem",
|
||||||
|
"mitm.keyFile": "/nonexistent/key.pem",
|
||||||
|
}))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for nonexistent cert file")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("alpn set", func(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(testMD(map[string]any{"mitm.alpn": "h2,http/1.1"}))
|
||||||
|
if h.md.alpn != "h2,http/1.1" {
|
||||||
|
t.Errorf("alpn = %q, want h2,http/1.1", h.md.alpn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetadata_ObserverPeriodDefaultsTo5s(t *testing.T) {
|
||||||
|
h := &relayHandler{}
|
||||||
|
h.parseMetadata(testMD(map[string]any{"observePeriod": 0}))
|
||||||
|
if h.md.observerPeriod != 5*time.Second {
|
||||||
|
t.Errorf("observerPeriod = %v, want 5s", h.md.observerPeriod)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/observer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *relayHandler) checkRateLimit(addr net.Addr) bool {
|
||||||
|
if h.options.RateLimiter == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
host, _, _ := net.SplitHostPort(addr.String())
|
||||||
|
if limiter := h.options.RateLimiter.Limiter(host); limiter != nil {
|
||||||
|
return limiter.Allow(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *relayHandler) observeStats(ctx context.Context) {
|
||||||
|
if h.options.Observer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var events []observer.Event
|
||||||
|
|
||||||
|
ticker := time.NewTicker(h.md.observerPeriod)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if len(events) > 0 {
|
||||||
|
if err := h.options.Observer.Observe(ctx, events); err == nil {
|
||||||
|
events = nil
|
||||||
|
}
|
||||||
|
// fallthrough: still collect fresh events for this tick
|
||||||
|
}
|
||||||
|
|
||||||
|
evs := h.stats.Events()
|
||||||
|
if err := h.options.Observer.Observe(ctx, evs); err != nil {
|
||||||
|
events = evs
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user