mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
156 lines
4.3 KiB
Go
156 lines
4.3 KiB
Go
package connector
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
|
|
"github.com/docker/cli/cli/connhelper/commandconn"
|
|
"github.com/psviderski/uncloud/internal/machine"
|
|
"golang.org/x/net/proxy"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
// SSHCLIConnector establishes a connection to the machine API by executing SSH CLI
|
|
// and running `uncloudd dial-stdio` on the remote machine.
|
|
type SSHCLIConnector struct {
|
|
config SSHConnectorConfig
|
|
conn net.Conn
|
|
}
|
|
|
|
func NewSSHCLIConnector(cfg *SSHConnectorConfig) *SSHCLIConnector {
|
|
return &SSHCLIConnector{config: *cfg}
|
|
}
|
|
|
|
// sshCLIDialer implements proxy.ContextDialer by spawning SSH processes with -W flag.
|
|
type sshCLIDialer struct {
|
|
config SSHConnectorConfig
|
|
}
|
|
|
|
// buildDialArgs constructs SSH command arguments for -W flag dialing.
|
|
func (d *sshCLIDialer) buildDialArgs(address string) []string {
|
|
args := []string{}
|
|
|
|
// Add connection timeout to fail fast when node is down.
|
|
args = append(args, "-o", "ConnectTimeout=5")
|
|
|
|
// Add port if non-standard.
|
|
if d.config.Port != 0 && d.config.Port != 22 {
|
|
args = append(args, "-p", strconv.Itoa(d.config.Port))
|
|
}
|
|
|
|
// Add identity file if specified.
|
|
if d.config.KeyPath != "" {
|
|
args = append(args, "-i", d.config.KeyPath)
|
|
}
|
|
|
|
// Add -W flag for stdin/stdout forwarding to target address.
|
|
args = append(args, "-W", address)
|
|
|
|
// Add user@host.
|
|
args = append(args, d.config.User+"@"+d.config.Host)
|
|
|
|
return args
|
|
}
|
|
|
|
// DialContext establishes a connection to the target address through an SSH tunnel using -W flag.
|
|
func (d *sshCLIDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
// Only support TCP connections.
|
|
if network != "tcp" {
|
|
return nil, fmt.Errorf("unsupported network type: %s", network)
|
|
}
|
|
|
|
// Build SSH command arguments.
|
|
args := d.buildDialArgs(address)
|
|
|
|
// Create connection using commandconn.
|
|
conn, err := commandconn.New(ctx, "ssh", args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("SSH connection to %s@%s for dialing %s: %w", d.config.User, d.config.Host, address, err)
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
func (c *SSHCLIConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
|
// Build SSH command arguments.
|
|
args := c.buildSSHArgs()
|
|
|
|
// Create connection using commandconn.
|
|
conn, err := commandconn.New(ctx, "ssh", args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("SSH connection to %s@%s: %w", c.config.User, c.config.Host, err)
|
|
}
|
|
c.conn = conn
|
|
|
|
// Create gRPC client over the connection.
|
|
// Use a custom dialer that returns our existing connection.
|
|
grpcConn, err := grpc.NewClient(
|
|
"passthrough:///", // Dummy target since we're using a custom dialer.
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithDefaultServiceConfig(defaultServiceConfig),
|
|
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
|
return c.conn, nil
|
|
}),
|
|
)
|
|
if err != nil {
|
|
c.conn.Close()
|
|
return nil, fmt.Errorf("create machine API client: %w", err)
|
|
}
|
|
|
|
return grpcConn, nil
|
|
}
|
|
|
|
// buildSSHArgs constructs the SSH command arguments.
|
|
func (c *SSHCLIConnector) buildSSHArgs() []string {
|
|
args := []string{}
|
|
|
|
// Add connection timeout to fail fast when node is down.
|
|
args = append(args, "-o", "ConnectTimeout=5")
|
|
|
|
// Add port if non-standard.
|
|
if c.config.Port != 0 && c.config.Port != 22 {
|
|
args = append(args, "-p", strconv.Itoa(c.config.Port))
|
|
}
|
|
|
|
// Add identity file if specified (backward compatibility with SSHKeyFile).
|
|
if c.config.KeyPath != "" {
|
|
args = append(args, "-i", c.config.KeyPath)
|
|
}
|
|
|
|
// Add user@host.
|
|
args = append(args, c.config.User+"@"+c.config.Host)
|
|
|
|
// Add remote command: uncloudd dial-stdio
|
|
args = append(args, "uncloudd", "dial-stdio")
|
|
|
|
// Add socket path if non-default.
|
|
if c.config.SockPath != "" && c.config.SockPath != machine.DefaultUncloudSockPath {
|
|
args = append(args, "--socket", c.config.SockPath)
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
// Dialer returns a proxy dialer for establishing connections within the cluster through SSH tunnels.
|
|
func (c *SSHCLIConnector) Dialer() (proxy.ContextDialer, error) {
|
|
if c.config == (SSHConnectorConfig{}) {
|
|
return nil, fmt.Errorf("SSH connector not configured")
|
|
}
|
|
|
|
return &sshCLIDialer{
|
|
config: c.config,
|
|
}, nil
|
|
}
|
|
|
|
func (c *SSHCLIConnector) Close() error {
|
|
if c.conn != nil {
|
|
err := c.conn.Close()
|
|
c.conn = nil
|
|
return err
|
|
}
|
|
return nil
|
|
}
|