mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03: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"
|
"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.
|
||||||
|
|||||||
Reference in New Issue
Block a user