Implement SSH port forwarding and improve jump host UI

Port Forwarding (new):
- Add PortForward model with local/remote direction, bind/dest addresses
- Implement ForwardManager in sshclient/portforward.go with full
  bidirectional tunnel support for both local and remote forwarding
- Integrate ForwardManager into SSH client lifecycle (auto-start on
  connect, clean shutdown on close)
- Add session manager methods: ListPortForwards, StartPortForward,
  StopPortForward with event emission
- Add API surface: ListPortForwards, StartPortForward, StopPortForward
- Add validation for port-forward rules in ValidateSSHConfig
- Add port forwarding section to profile editor with direction, bind/dest
  host/port, label, and enable toggle
- Add frontend event handling for forward:started/stopped/error
- Style port-forward rows with charcoal card design matching jump hosts

Jump Host UI improvements:
- Add private key path field to jump host editor rows
- Collect privateKeyPath in form value collection
- Restyle jump host rows as two-line cards with clear field grouping
This commit is contained in:
2026-04-16 00:00:00 +01:00
parent 5e397195b9
commit 6d300f7dba
13 changed files with 664 additions and 8 deletions
+16 -1
View File
@@ -15,7 +15,7 @@ import (
// Client manages a single SSH session lifecycle.
// It handles connection establishment, authentication, PTY allocation,
// terminal resize propagation, and clean shutdown.
// terminal resize propagation, port forwarding, and clean shutdown.
type Client struct {
mu sync.Mutex
config *models.Profile
@@ -30,6 +30,7 @@ type Client struct {
onClose func()
ctx context.Context
cancel context.CancelFunc
forwards *ForwardManager // port forwarding tunnels
}
// NewClient creates a new SSH client bound to the given profile.
@@ -193,6 +194,7 @@ func (c *Client) connectInternal(
c.stdout = stdoutPipe
c.stderr = stderrPipe
c.connected = true
c.forwards = NewForwardManager(sshConn)
c.mu.Unlock()
go c.readLoop()
@@ -242,11 +244,17 @@ func (c *Client) Close() error {
c.connected = false
session := c.session
sshClient := c.sshClient
fwdMgr := c.forwards
onClose := c.onClose
c.mu.Unlock()
c.cancel()
// Shut down any active port forwards first.
if fwdMgr != nil {
fwdMgr.StopAll()
}
var firstErr error
if session != nil {
if err := session.Close(); err != nil && firstErr == nil {
@@ -272,6 +280,13 @@ func (c *Client) IsConnected() bool {
return c.connected
}
// Forwards returns the ForwardManager for this client, or nil if not connected.
func (c *Client) Forwards() *ForwardManager {
c.mu.Lock()
defer c.mu.Unlock()
return c.forwards
}
// buildAuthMethods constructs the SSH authentication method list from the
// profile's SSHConfig and the provided credential callbacks.
func (c *Client) buildAuthMethods(sc models.SSHConfig, getPassword func() (string, error), getPassphrase func() (string, error)) ([]ssh.AuthMethod, error) {