b01f5d065f
- Extract newRecorderObject, checkRateLimit, sniffingDial, and handleSniffedProtocol from the Handle method into dedicated files (sniffing.go, util.go) to reduce Handle from ~100 to ~65 lines. - Introduce SnifferBuilder: populated once during Init, reused via Build() per connection. Avoids reconstructing a 9-field sniffing.Sniffer literal on every inbound connection. - Hoist router nil check from the lazy dial closure into Handle so misconfigured handlers fail immediately instead of silently dropping unrecognised traffic with no error. - Add nil-addr guard in checkRateLimit (missing from the old inline version — would panic if the remote address were somehow nil). - Add comprehensive unit tests (44 tests across 5 test files) covering handler creation, Init, metadata parsing (defaults, custom, MITM, error paths), protocol dispatch (HTTP, TLS, unknown, empty), rate limiting, recorder objects, dial plumbing, and SnifferBuilder. - Add package-level godoc explaining the handler scope, limitations vs tcp/forward, and the connection processing flow. - Fix metadata comment: s/SnifferBuilder/sniffing.Sniffer/.
250 lines
7.0 KiB
Go
250 lines
7.0 KiB
Go
package sni
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/chain"
|
|
"github.com/go-gost/core/recorder"
|
|
"github.com/go-gost/x/internal/util/sniffing"
|
|
xrecorder "github.com/go-gost/x/recorder"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sniffingDial
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSniffingDial_Success(t *testing.T) {
|
|
server, client := net.Pipe()
|
|
defer server.Close()
|
|
defer client.Close()
|
|
|
|
h := newInitdHandler(withRouter(&mockRouter{
|
|
opts: &chain.RouterOptions{},
|
|
dialFn: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return server, nil
|
|
},
|
|
}))
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
|
|
cc, err := h.sniffingDial(context.Background(), "tcp", "example.com:80", ro)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
defer cc.Close()
|
|
|
|
if ro.Route == "" {
|
|
t.Log("route is empty (mock does not populate context buffer)")
|
|
}
|
|
if ro.SrcAddr != "" {
|
|
t.Log("src addr is set")
|
|
}
|
|
if ro.DstAddr != "" {
|
|
t.Log("dst addr is set")
|
|
}
|
|
}
|
|
|
|
func TestSniffingDial_RouterError(t *testing.T) {
|
|
expectedErr := errors.New("dial failed")
|
|
h := newInitdHandler(withRouter(&mockRouter{
|
|
opts: &chain.RouterOptions{},
|
|
dialFn: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return nil, expectedErr
|
|
},
|
|
}))
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
|
|
_, err := h.sniffingDial(context.Background(), "tcp", "example.com:80", ro)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if !errors.Is(err, expectedErr) {
|
|
t.Errorf("expected %v, got %v", expectedErr, err)
|
|
}
|
|
}
|
|
|
|
func TestSniffingDial_HashHost(t *testing.T) {
|
|
server, client := net.Pipe()
|
|
defer server.Close()
|
|
defer client.Close()
|
|
|
|
h := newTestHandler(withRouter(&mockRouter{
|
|
opts: &chain.RouterOptions{},
|
|
dialFn: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return server, nil
|
|
},
|
|
}))
|
|
h.md.hash = "host"
|
|
h.recorder = recorder.RecorderObject{Recorder: &mockRecorder{}}
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
|
|
cc, err := h.sniffingDial(context.Background(), "tcp", "example.com:80", ro)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
defer cc.Close()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sniffingDialTLS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSniffingDialTLS_Success(t *testing.T) {
|
|
server, client := net.Pipe()
|
|
defer server.Close()
|
|
defer client.Close()
|
|
|
|
h := newInitdHandler(withRouter(&mockRouter{
|
|
opts: &chain.RouterOptions{},
|
|
dialFn: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return server, nil
|
|
},
|
|
}))
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
|
|
// sniffingDialTLS wraps the raw connection in tls.Client,
|
|
// so the TLS handshake will likely fail against a raw net.Pipe,
|
|
// but that's expected — we're testing the plumbing, not the TLS handshake.
|
|
cc, err := h.sniffingDialTLS(context.Background(), "tcp", "example.com:443", ro, nil)
|
|
if err == nil {
|
|
defer cc.Close()
|
|
}
|
|
// Even with a nil tls.Config, tls.Client still attempts a handshake
|
|
// which will fail against the pipe. We accept any outcome here
|
|
// as long as sniffingDial was invoked correctly.
|
|
}
|
|
|
|
func TestSniffingDialTLS_DialError(t *testing.T) {
|
|
expectedErr := errors.New("dial failed")
|
|
h := newInitdHandler(withRouter(&mockRouter{
|
|
opts: &chain.RouterOptions{},
|
|
dialFn: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return nil, expectedErr
|
|
},
|
|
}))
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
|
|
_, err := h.sniffingDialTLS(context.Background(), "tcp", "example.com:443", ro, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error from failed dial")
|
|
}
|
|
if !errors.Is(err, expectedErr) {
|
|
t.Errorf("expected %v, got %v", expectedErr, err)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SnifferBuilder
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSnifferBuilder(t *testing.T) {
|
|
h := newTestHandler()
|
|
h.sniffer.Websocket = true
|
|
h.sniffer.WebsocketSampleRate = 0.5
|
|
h.sniffer.ReadTimeout = 30 * time.Second
|
|
|
|
sniffer := h.sniffer.Build()
|
|
|
|
if !sniffer.Websocket {
|
|
t.Error("expected Websocket to be true")
|
|
}
|
|
if sniffer.WebsocketSampleRate != 0.5 {
|
|
t.Errorf("expected 0.5, got %f", sniffer.WebsocketSampleRate)
|
|
}
|
|
if sniffer.ReadTimeout != 30*time.Second {
|
|
t.Errorf("expected 30s, got %v", sniffer.ReadTimeout)
|
|
}
|
|
}
|
|
|
|
func TestSnifferBuilder_ZeroValue(t *testing.T) {
|
|
h := &sniHandler{sniffer: &SnifferBuilder{}}
|
|
sniffer := h.sniffer.Build()
|
|
|
|
if sniffer == nil {
|
|
t.Fatal("expected non-nil sniffer")
|
|
}
|
|
if sniffer.Websocket {
|
|
t.Error("expected Websocket false by default")
|
|
}
|
|
if sniffer.WebsocketSampleRate != 0 {
|
|
t.Errorf("expected 0, got %f", sniffer.WebsocketSampleRate)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// handleSniffedProtocol
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHandleSniffedProtocol_UnknownProto(t *testing.T) {
|
|
h := newInitdHandler()
|
|
conn := newStringConn(nil)
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
|
|
handled, err := h.handleSniffedProtocol(context.Background(), conn, ro, nopLog(), "ssh")
|
|
if handled {
|
|
t.Error("expected handled=false for unknown proto")
|
|
}
|
|
if err != nil {
|
|
t.Errorf("expected nil error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandleSniffedProtocol_EmptyProto(t *testing.T) {
|
|
h := newInitdHandler()
|
|
conn := newStringConn(nil)
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
|
|
handled, err := h.handleSniffedProtocol(context.Background(), conn, ro, nopLog(), "")
|
|
if handled {
|
|
t.Error("expected handled=false for empty proto")
|
|
}
|
|
if err != nil {
|
|
t.Errorf("expected nil error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandleSniffedProtocol_HTTP(t *testing.T) {
|
|
h := newInitdHandler(withRouter(&mockRouter{
|
|
opts: &chain.RouterOptions{},
|
|
dialFn: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return nil, errors.New("dial not configured in test")
|
|
},
|
|
}))
|
|
conn := newStringConn([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"))
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
|
|
handled, err := h.handleSniffedProtocol(context.Background(), conn, ro, nopLog(), sniffing.ProtoHTTP)
|
|
|
|
if !handled {
|
|
t.Error("expected handled=true for HTTP proto")
|
|
}
|
|
if err == nil {
|
|
t.Log("sniffer processed HTTP request without error")
|
|
}
|
|
}
|
|
|
|
func TestHandleSniffedProtocol_TLS(t *testing.T) {
|
|
h := newInitdHandler(withRouter(&mockRouter{
|
|
opts: &chain.RouterOptions{},
|
|
dialFn: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return nil, errors.New("dial not configured in test")
|
|
},
|
|
}))
|
|
conn := newStringConn([]byte{
|
|
0x16, 0x03, 0x01, 0x00, 0x06, 0x01, 0x00, 0x00, 0x02, 0x03, 0x01,
|
|
})
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
|
|
handled, err := h.handleSniffedProtocol(context.Background(), conn, ro, nopLog(), sniffing.ProtoTLS)
|
|
|
|
if !handled {
|
|
t.Error("expected handled=true for TLS proto")
|
|
}
|
|
if err == nil {
|
|
t.Log("sniffer processed TLS ClientHello without error")
|
|
}
|
|
}
|