Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions varlink/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
66 changes: 66 additions & 0 deletions varlink/connection_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
41 changes: 41 additions & 0 deletions varlink/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading