chore: check SSH TCP forwarding for dial operations and return a friendlier error

This commit is contained in:
Pasha Sviderski
2026-04-13 13:54:36 +10:00
parent 07e380b95f
commit f60a9ff293
+37
View File
@@ -9,6 +9,8 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"time"
"github.com/docker/cli/cli/connhelper/commandconn" "github.com/docker/cli/cli/connhelper/commandconn"
"github.com/psviderski/uncloud/internal/grpcversion" "github.com/psviderski/uncloud/internal/grpcversion"
@@ -23,6 +25,10 @@ type SSHCLIConnector struct {
config SSHConnectorConfig config SSHConnectorConfig
// Path to SSH control socket for connection reuse. // Path to SSH control socket for connection reuse.
controlSockPath string controlSockPath string
// fwdCheckOnce ensures the TCP forwarding check runs only once.
fwdCheckOnce sync.Once
// fwdCheckErr caches the result of the TCP forwarding check.
fwdCheckErr error
} }
func NewSSHCLIConnector(cfg *SSHConnectorConfig) *SSHCLIConnector { func NewSSHCLIConnector(cfg *SSHConnectorConfig) *SSHCLIConnector {
@@ -158,6 +164,9 @@ func (c *SSHCLIConnector) DialContext(ctx context.Context, network, address stri
if network != "tcp" { if network != "tcp" {
return nil, fmt.Errorf("unsupported network type: %s", network) return nil, fmt.Errorf("unsupported network type: %s", network)
} }
if err := c.CheckTCPForwarding(ctx); err != nil {
return nil, err
}
args := append(c.buildSSHArgs(), "-W", address) args := append(c.buildSSHArgs(), "-W", address)
conn, err := commandconn.New(ctx, "ssh", args...) conn, err := commandconn.New(ctx, "ssh", args...)
@@ -168,6 +177,34 @@ func (c *SSHCLIConnector) DialContext(ctx context.Context, network, address stri
return conn, nil return conn, nil
} }
// CheckTCPForwarding verifies that TCP forwarding is enabled on the remote SSH server. It probes the server once
// and caches the result for subsequent calls. Returns an error with actionable instructions if forwarding is disabled.
func (c *SSHCLIConnector) CheckTCPForwarding(ctx context.Context) error {
c.fwdCheckOnce.Do(func() {
// Probe TCP forwarding by requesting a forward to 127.0.0.1:1 (a port almost never in use).
// If forwarding is disabled, sshd rejects the channel. The error message differs depending on
// whether the connection goes through a ControlMaster mux or directly:
// - Multiplexed: "Session open refused by peer"
// - Direct: "administratively prohibited"
probeCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
args := append(c.buildSSHArgs(), "-W", "127.0.0.1:1")
probe := exec.CommandContext(probeCtx, "ssh", args...)
output, _ := probe.CombinedOutput()
outStr := string(output)
if strings.Contains(outStr, "administratively prohibited") ||
strings.Contains(outStr, "Session open refused by peer") {
c.fwdCheckErr = fmt.Errorf(
"SSH TCP forwarding appears to be disabled on '%s': ensure 'AllowTcpForwarding yes' is set "+
"in /etc/ssh/sshd_config on the remote machine and restart sshd (sudo systemctl restart ssh)",
c.config.Destination(),
)
}
})
return c.fwdCheckErr
}
func (c *SSHCLIConnector) Close() error { func (c *SSHCLIConnector) Close() error {
// Individual connections are managed by gRPC and closed when the gRPC connection closes. // Individual connections are managed by gRPC and closed when the gRPC connection closes.
// The SSH control socket may persist for connection reuse across CLI invocations. // The SSH control socket may persist for connection reuse across CLI invocations.