diff --git a/varlink/connection.go b/varlink/connection.go index 4543df6..88280c7 100644 --- a/varlink/connection.go +++ b/varlink/connection.go @@ -91,6 +91,13 @@ type GetNetConn interface { NetConn() net.Conn } +// ContextDialer is an interface for network dialers that support context-aware dialing. +// The standard *net.Dialer implements this interface. Custom implementations can be used +// to dial through proxies, SSH tunnels, or with custom network configurations. +type ContextDialer interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + // Connection is a connection from a client to a service. type Connection struct { io.Closer @@ -280,6 +287,36 @@ func (c *Connection) Close() error { // is used when dialling. Once successfully connected, any expiration // of the context will not affect the connection. func NewConnection(ctx context.Context, address string) (*Connection, error) { + return newConnectionWithDialer(ctx, address, &net.Dialer{}) +} + +// NewConnectionWithDialer returns a new connection to the given address using a custom dialer. +// The dialer parameter allows using custom network configurations such as: +// - Dialing through SOCKS or HTTP proxies +// - Custom timeout and keepalive settings +// - Dialing through SSH tunnels +// - Custom DNS resolution +// +// The context is used when dialling. Once successfully connected, any expiration +// of the context will not affect the connection. +// +// Example with custom timeout: +// +// dialer := &net.Dialer{ +// Timeout: 30 * time.Second, +// KeepAlive: 30 * time.Second, +// } +// conn, err := varlink.NewConnectionWithDialer(ctx, "tcp:localhost:8080", dialer) +func NewConnectionWithDialer(ctx context.Context, address string, dialer ContextDialer) (*Connection, error) { + if dialer == nil { + return nil, fmt.Errorf("dialer cannot be nil") + } + return newConnectionWithDialer(ctx, address, dialer) +} + +// newConnectionWithDialer is the private implementation used by both NewConnection +// and NewConnectionWithDialer. +func newConnectionWithDialer(ctx context.Context, address string, dialer ContextDialer) (*Connection, error) { words := strings.SplitN(address, ":", 2) if len(words) != 2 { @@ -306,8 +343,7 @@ func NewConnection(ctx context.Context, address string) (*Connection, error) { return nil, fmt.Errorf("unknown protocol %s", protocol) } - var d net.Dialer - conn, err := d.DialContext(ctx, protocol, addr) + conn, err := dialer.DialContext(ctx, protocol, addr) if err != nil { return nil, err } diff --git a/varlink/connection_test.go b/varlink/connection_test.go new file mode 100644 index 0000000..6e2bc9a --- /dev/null +++ b/varlink/connection_test.go @@ -0,0 +1,66 @@ +package varlink + +import ( + "context" + "net" + "testing" +) + +func TestNewConnection(t *testing.T) { + // Start a simple TCP listener + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + + addr := listener.Addr().String() + + // Test NewConnection + conn, err := NewConnection(context.Background(), "tcp:"+addr) + if err != nil { + t.Fatal(err) + } + conn.Close() +} + +func TestNewConnectionWithDialer(t *testing.T) { + // Start a simple TCP listener + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + + addr := listener.Addr().String() + + // Track if custom dialer was called + called := false + customDialer := &testDialer{ + Dialer: &net.Dialer{}, + onDial: func() { called = true }, + } + + // Test NewConnectionWithDialer + conn, err := NewConnectionWithDialer(context.Background(), "tcp:"+addr, customDialer) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + if !called { + t.Fatal("custom dialer was not used") + } +} + +// testDialer wraps net.Dialer to track usage +type testDialer struct { + *net.Dialer + onDial func() +} + +func (d *testDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + d.onDial() + + return d.Dialer.DialContext(ctx, network, address) +} diff --git a/varlink/doc.go b/varlink/doc.go index de1ed23..3decf9f 100644 --- a/varlink/doc.go +++ b/varlink/doc.go @@ -59,5 +59,46 @@ Service implementing the interface and its method: service.RegisterInterface(orgexamplethis.VarlinkNew(&data)) err := service.Listen("unix:/run/org.example.this", 0) + +Client connecting to a service: + + ctx := context.Background() + conn, err := varlink.NewConnection(ctx, "unix:/run/org.example.this") + if err != nil { + // handle error + } + + defer conn.Close() + +Custom dialer for connecting to a Unix socket on a remote host via SSH: + + import "golang.org/x/crypto/ssh" + + // SSH into remote host + sshConfig := &ssh.ClientConfig{ + User: "user", + Auth: []ssh.AuthMethod{ + ssh.Password("password"), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), // don't do this + } + + sshClient, err := ssh.Dial("tcp", "remote.example.com:22", sshConfig) + if err != nil { + // handle error + } + defer sshClient.Close() + + // Custom dialer that connects through SSH + type sshDialer struct { + client *ssh.Client + } + + func (d *sshDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + return d.client.Dial(network, addr) + } + + // Connect to Unix socket on the remote host + conn, err := varlink.NewConnectionWithDialer(ctx, "unix:/run/org.example.service", &sshDialer{sshClient}) */ package varlink