fix(direct): guard nil dialer and nil logger in Connect, add doc comments

Add nil checks for connect-options dialer and logger to prevent
nil-pointer panics when Connect is called without a DialerConnectOption
or LoggerOption. Add package and NewConnector godoc comments.

Add 10 unit tests covering construction, Init metadata parsing,
Connect success/reject/dial-error paths, nil dialer, nil logger,
and the reject conn stub.
This commit is contained in:
ginuerzh
2026-05-24 14:19:08 +08:00
parent effba3b690
commit 4efb4b9974
2 changed files with 301 additions and 15 deletions
+12
View File
@@ -1,7 +1,12 @@
// Package direct implements a direct (transparent) connector that establishes
// connections to destination addresses using the dialer provided via connect
// options. It also supports a "reject" action that returns a dead connection
// (reads return io.EOF, writes return io.ErrClosedPipe) without dialing.
package direct
import (
"context"
"errors"
"net"
"github.com/go-gost/core/connector"
@@ -20,6 +25,7 @@ type directConnector struct {
options connector.Options
}
// NewConnector creates a direct connector with the given options.
func NewConnector(opts ...connector.Option) connector.Connector {
options := connector.Options{}
for _, opt := range opts {
@@ -45,11 +51,16 @@ func (c *directConnector) Connect(ctx context.Context, _ net.Conn, network, addr
return &conn{}, nil
}
if cOpts.Dialer == nil {
return nil, errors.New("direct: missing dialer in connect options")
}
conn, err := cOpts.Dialer.Dial(ctx, network, address)
if err != nil {
return nil, err
}
if c.options.Logger != nil {
var localAddr, remoteAddr string
if addr := conn.LocalAddr(); addr != nil {
localAddr = addr.String()
@@ -66,6 +77,7 @@ func (c *directConnector) Connect(ctx context.Context, _ net.Conn, network, addr
"sid": string(ctxvalue.SidFromContext(ctx)),
})
log.Debugf("connect %s/%s", address, network)
}
return conn, nil
}
+274
View File
@@ -0,0 +1,274 @@
package direct
import (
"context"
"io"
"net"
"testing"
"time"
xnet "github.com/go-gost/core/common/net"
"github.com/go-gost/core/connector"
"github.com/go-gost/core/logger"
mdx "github.com/go-gost/x/metadata"
)
type testDialer struct {
conn net.Conn
err error
}
func (d *testDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
return d.conn, d.err
}
func TestNewConnector(t *testing.T) {
c := NewConnector()
if c == nil {
t.Fatal("NewConnector returned nil")
}
}
func TestNewConnectorWithOptions(t *testing.T) {
c := NewConnector(connector.LoggerOption(logger.Default()))
if c == nil {
t.Fatal("NewConnector returned nil")
}
if err := c.Init(mdx.NewMetadata(nil)); err != nil {
t.Fatalf("Init failed: %v", err)
}
}
func TestInit(t *testing.T) {
tests := []struct {
name string
metadata map[string]any
action string
}{
{
name: "nil metadata",
metadata: nil,
action: "",
},
{
name: "empty metadata",
metadata: map[string]any{},
action: "",
},
{
name: "action reject",
metadata: map[string]any{"action": "reject"},
action: "reject",
},
{
name: "action reject with capitals",
metadata: map[string]any{"action": "REJECT"},
action: "reject",
},
{
name: "unknown action",
metadata: map[string]any{"action": "unknown"},
action: "unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConnector()
if err := c.Init(mdx.NewMetadata(tt.metadata)); err != nil {
t.Fatalf("Init failed: %v", err)
}
dc, ok := c.(*directConnector)
if !ok {
t.Fatal("not a *directConnector")
}
if dc.md.action != tt.action {
t.Errorf("expected action %q, got %q", tt.action, dc.md.action)
}
})
}
}
func TestConnect(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
defer l.Close()
done := make(chan struct{})
go func() {
defer close(done)
conn, err := l.Accept()
if err != nil {
return
}
conn.Close()
}()
client, err := net.Dial("tcp", l.Addr().String())
if err != nil {
t.Fatalf("failed to dial: %v", err)
}
c := NewConnector()
c.Init(mdx.NewMetadata(nil))
conn, err := c.Connect(context.Background(), nil, "tcp", l.Addr().String(),
connector.DialerConnectOption(&testDialer{conn: client}),
)
if err != nil {
t.Fatalf("Connect failed: %v", err)
}
if conn == nil {
t.Fatal("Connect returned nil conn")
}
conn.Close()
<-done
}
func TestConnectRejectAction(t *testing.T) {
c := NewConnector()
c.Init(mdx.NewMetadata(map[string]any{"action": "reject"}))
conn, err := c.Connect(context.Background(), nil, "tcp", "127.0.0.1:12345",
connector.DialerConnectOption(&testDialer{}),
)
if err != nil {
t.Fatalf("Connect with reject action failed: %v", err)
}
if conn == nil {
t.Fatal("Connect with reject action returned nil conn")
}
// Verify dead conn behavior
b := make([]byte, 1)
_, err = conn.Read(b)
if err != io.EOF {
t.Errorf("expected io.EOF on Read from reject conn, got %v", err)
}
_, err = conn.Write(b)
if err != io.ErrClosedPipe {
t.Errorf("expected io.ErrClosedPipe on Write to reject conn, got %v", err)
}
if err := conn.Close(); err != nil {
t.Errorf("expected nil Close on reject conn, got %v", err)
}
}
func TestConnectNilDialer(t *testing.T) {
c := NewConnector()
c.Init(mdx.NewMetadata(nil))
_, err := c.Connect(context.Background(), nil, "tcp", "127.0.0.1:12345")
if err == nil {
t.Fatal("expected error for nil dialer, got nil")
}
if err.Error() != "direct: missing dialer in connect options" {
t.Errorf("unexpected error: %v", err)
}
}
func TestConnectNilLogger(t *testing.T) {
// No LoggerOption set — should not panic
c := NewConnector()
c.Init(mdx.NewMetadata(nil))
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
defer l.Close()
done := make(chan struct{})
go func() {
defer close(done)
conn, _ := l.Accept()
if conn != nil {
conn.Close()
}
}()
client, err := net.Dial("tcp", l.Addr().String())
if err != nil {
t.Fatalf("failed to dial: %v", err)
}
conn, err := c.Connect(context.Background(), nil, "tcp", l.Addr().String(),
connector.DialerConnectOption(&testDialer{conn: client}),
)
if err != nil {
t.Fatalf("Connect with nil logger failed: %v", err)
}
conn.Close()
<-done
}
func TestConnectDialError(t *testing.T) {
c := NewConnector()
c.Init(mdx.NewMetadata(nil))
dialErr := io.ErrUnexpectedEOF
_, err := c.Connect(context.Background(), nil, "tcp", "127.0.0.1:12345",
connector.DialerConnectOption(&testDialer{err: dialErr}),
)
if err != dialErr {
t.Errorf("expected %v, got %v", dialErr, err)
}
}
func TestRejectConn(t *testing.T) {
c := &conn{}
// Read returns io.EOF
b := make([]byte, 10)
n, err := c.Read(b)
if n != 0 {
t.Errorf("Read returned %d bytes, expected 0", n)
}
if err != io.EOF {
t.Errorf("Read returned %v, expected io.EOF", err)
}
// Write returns io.ErrClosedPipe
n, err = c.Write(b)
if n != 0 {
t.Errorf("Write returned %d bytes, expected 0", n)
}
if err != io.ErrClosedPipe {
t.Errorf("Write returned %v, expected io.ErrClosedPipe", err)
}
// Close returns nil
if err := c.Close(); err != nil {
t.Errorf("Close returned %v, expected nil", err)
}
// LocalAddr returns non-nil
if addr := c.LocalAddr(); addr == nil {
t.Error("LocalAddr returned nil")
}
// RemoteAddr returns non-nil
if addr := c.RemoteAddr(); addr == nil {
t.Error("RemoteAddr returned nil")
}
// Deadline methods return nil
if err := c.SetDeadline(time.Now()); err != nil {
t.Errorf("SetDeadline returned %v, expected nil", err)
}
if err := c.SetReadDeadline(time.Now()); err != nil {
t.Errorf("SetReadDeadline returned %v, expected nil", err)
}
if err := c.SetWriteDeadline(time.Now()); err != nil {
t.Errorf("SetWriteDeadline returned %v, expected nil", err)
}
}
// Ensure xnet.Dialer interface matches at compile time
var _ xnet.Dialer = (*testDialer)(nil)