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:
@@ -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
|
package direct
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/go-gost/core/connector"
|
"github.com/go-gost/core/connector"
|
||||||
@@ -20,6 +25,7 @@ type directConnector struct {
|
|||||||
options connector.Options
|
options connector.Options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewConnector creates a direct connector with the given options.
|
||||||
func NewConnector(opts ...connector.Option) connector.Connector {
|
func NewConnector(opts ...connector.Option) connector.Connector {
|
||||||
options := connector.Options{}
|
options := connector.Options{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -45,11 +51,16 @@ func (c *directConnector) Connect(ctx context.Context, _ net.Conn, network, addr
|
|||||||
return &conn{}, nil
|
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)
|
conn, err := cOpts.Dialer.Dial(ctx, network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.options.Logger != nil {
|
||||||
var localAddr, remoteAddr string
|
var localAddr, remoteAddr string
|
||||||
if addr := conn.LocalAddr(); addr != nil {
|
if addr := conn.LocalAddr(); addr != nil {
|
||||||
localAddr = addr.String()
|
localAddr = addr.String()
|
||||||
@@ -66,6 +77,7 @@ func (c *directConnector) Connect(ctx context.Context, _ net.Conn, network, addr
|
|||||||
"sid": string(ctxvalue.SidFromContext(ctx)),
|
"sid": string(ctxvalue.SidFromContext(ctx)),
|
||||||
})
|
})
|
||||||
log.Debugf("connect %s/%s", address, network)
|
log.Debugf("connect %s/%s", address, network)
|
||||||
|
}
|
||||||
|
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user