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
+19
View File
@@ -93,6 +93,8 @@ func ValidateProfile(p *models.Profile) error {
errs.Errors = append(errs.Errors, ve.Errors...)
}
}
case models.ProtocolGitHub:
// GitHub profiles require no host-level validation beyond the common checks.
default:
errs.Add("protocol", fmt.Sprintf("unsupported protocol %q", p.Protocol))
}
@@ -217,6 +219,23 @@ func ValidateSSHConfig(cfg *models.SSHConfig) error {
}
}
// Validate each port-forwarding rule.
for i, pf := range cfg.PortForwards {
prefix := fmt.Sprintf("ssh.portForwards[%d]", i)
if pf.Direction != models.PortForwardLocal && pf.Direction != models.PortForwardRemote {
errs.Add(prefix+".direction", fmt.Sprintf("unsupported forwarding direction %q", pf.Direction))
}
if pf.BindPort < 1 || pf.BindPort > 65535 {
errs.Add(prefix+".bindPort", "bind port must be between 1 and 65535")
}
if pf.DestPort < 1 || pf.DestPort > 65535 {
errs.Add(prefix+".destPort", "destination port must be between 1 and 65535")
}
if strings.TrimSpace(pf.DestHost) == "" {
errs.Add(prefix+".destHost", "destination host must not be empty")
}
}
if errs.HasErrors() {
return errs
}