[client] Add port forwarding to ssh proxy (#5031)

* Implement port forwarding for the ssh proxy

* Allow user switching for port forwarding
This commit is contained in:
Viktor Liu
2026-01-07 12:18:04 +08:00
committed by GitHub
parent 7142d45ef3
commit f012fb8592
15 changed files with 1006 additions and 370 deletions

View File

@@ -224,6 +224,96 @@ func TestServer_PortForwardingRestriction(t *testing.T) {
}
}
func TestServer_PrivilegedPortAccess(t *testing.T) {
hostKey, err := ssh.GeneratePrivateKey(ssh.ED25519)
require.NoError(t, err)
serverConfig := &Config{
HostKeyPEM: hostKey,
}
server := New(serverConfig)
server.SetAllowRemotePortForwarding(true)
tests := []struct {
name string
forwardType string
port uint32
username string
expectError bool
errorMsg string
skipOnWindows bool
}{
{
name: "non-root user remote forward privileged port",
forwardType: "remote",
port: 80,
username: "testuser",
expectError: true,
errorMsg: "cannot bind to privileged port",
skipOnWindows: true,
},
{
name: "non-root user tcpip-forward privileged port",
forwardType: "tcpip-forward",
port: 443,
username: "testuser",
expectError: true,
errorMsg: "cannot bind to privileged port",
skipOnWindows: true,
},
{
name: "non-root user remote forward unprivileged port",
forwardType: "remote",
port: 8080,
username: "testuser",
expectError: false,
},
{
name: "non-root user remote forward port 0",
forwardType: "remote",
port: 0,
username: "testuser",
expectError: false,
},
{
name: "root user remote forward privileged port",
forwardType: "remote",
port: 22,
username: "root",
expectError: false,
},
{
name: "local forward privileged port allowed for non-root",
forwardType: "local",
port: 80,
username: "testuser",
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipOnWindows && runtime.GOOS == "windows" {
t.Skip("Windows does not have privileged port restrictions")
}
result := PrivilegeCheckResult{
Allowed: true,
User: &user.User{Username: tt.username},
}
err := server.checkPrivilegedPortAccess(tt.forwardType, tt.port, result)
if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
require.NoError(t, err)
}
})
}
}
func TestServer_PortConflictHandling(t *testing.T) {
// Test that multiple sessions requesting the same local port are handled naturally by the OS
// Get current user for SSH connection
@@ -392,3 +482,95 @@ func TestServer_IsPrivilegedUser(t *testing.T) {
})
}
}
func TestServer_PortForwardingOnlySession(t *testing.T) {
// Test that sessions without PTY and command are allowed when port forwarding is enabled
currentUser, err := user.Current()
require.NoError(t, err, "Should be able to get current user")
// Generate host key for server
hostKey, err := ssh.GeneratePrivateKey(ssh.ED25519)
require.NoError(t, err)
tests := []struct {
name string
allowLocalForwarding bool
allowRemoteForwarding bool
expectAllowed bool
description string
}{
{
name: "session_allowed_with_local_forwarding",
allowLocalForwarding: true,
allowRemoteForwarding: false,
expectAllowed: true,
description: "Port-forwarding-only session should be allowed when local forwarding is enabled",
},
{
name: "session_allowed_with_remote_forwarding",
allowLocalForwarding: false,
allowRemoteForwarding: true,
expectAllowed: true,
description: "Port-forwarding-only session should be allowed when remote forwarding is enabled",
},
{
name: "session_allowed_with_both",
allowLocalForwarding: true,
allowRemoteForwarding: true,
expectAllowed: true,
description: "Port-forwarding-only session should be allowed when both forwarding types enabled",
},
{
name: "session_denied_without_forwarding",
allowLocalForwarding: false,
allowRemoteForwarding: false,
expectAllowed: false,
description: "Port-forwarding-only session should be denied when all forwarding is disabled",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serverConfig := &Config{
HostKeyPEM: hostKey,
JWT: nil,
}
server := New(serverConfig)
server.SetAllowRootLogin(true)
server.SetAllowLocalPortForwarding(tt.allowLocalForwarding)
server.SetAllowRemotePortForwarding(tt.allowRemoteForwarding)
serverAddr := StartTestServer(t, server)
defer func() {
_ = server.Stop()
}()
// Connect to the server without requesting PTY or command
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client, err := sshclient.Dial(ctx, serverAddr, currentUser.Username, sshclient.DialOptions{
InsecureSkipVerify: true,
})
require.NoError(t, err)
defer func() {
_ = client.Close()
}()
// Execute a command without PTY - this simulates ssh -T with no command
// The server should either allow it (port forwarding enabled) or reject it
output, err := client.ExecuteCommand(ctx, "")
if tt.expectAllowed {
// When allowed, the session stays open until cancelled
// ExecuteCommand with empty command should return without error
assert.NoError(t, err, "Session should be allowed when port forwarding is enabled")
assert.NotContains(t, output, "port forwarding is disabled",
"Output should not contain port forwarding disabled message")
} else if err != nil {
// When denied, we expect an error message about port forwarding being disabled
assert.Contains(t, err.Error(), "port forwarding is disabled",
"Should get port forwarding disabled message")
}
})
}
}