mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
chore: check SSH TCP forwarding for dial operations and return a friendlier error
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper/commandconn"
|
||||
"github.com/psviderski/uncloud/internal/grpcversion"
|
||||
@@ -23,6 +25,10 @@ type SSHCLIConnector struct {
|
||||
config SSHConnectorConfig
|
||||
// Path to SSH control socket for connection reuse.
|
||||
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 {
|
||||
@@ -158,6 +164,9 @@ func (c *SSHCLIConnector) DialContext(ctx context.Context, network, address stri
|
||||
if network != "tcp" {
|
||||
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)
|
||||
conn, err := commandconn.New(ctx, "ssh", args...)
|
||||
@@ -168,6 +177,34 @@ func (c *SSHCLIConnector) DialContext(ctx context.Context, network, address stri
|
||||
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 {
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user