feat: chaînes ProxyJump multi-sauts
resolveJump remonte les jumps de host en host (-J hop1,hop2,…), passe les specs brutes telles quelles et coupe les cycles. Doc bastion/ProxyJump dans le README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 files changed
+38 −3
A
.DS_Store
Binary file — not shown.
M
README.md
+15 −0
M
internal/.DS_Store
Binary file — not shown.
M
main.go
+23 −3
.DS_Store
README.md
@@ -125,6 +125,21 @@ tags = ["client", "prod"] # optionnel
C'est un simple fichier : pour synchroniser deux machines, copie-le, ou versionne `~/.config/sssh/` dans un repo git.
+### Bastion / ProxyJump
+
+Le champ `jump` (option `-J` de `sssh add`, champ « Jump host » du formulaire) accepte :
+
+- le **nom d'un autre host sssh** : sa cible réelle (user, IP, port) est utilisée, et si ce host a lui-même un `jump`, la chaîne complète est construite (`ssh -J hop1,hop2 cible`) ;
+- une **spécification ssh brute** `[user@]host[:port]`, passée telle quelle.
+
+```sh
+sssh add bastion admin@203.0.113.10
+sssh add serveur-prive deploy@10.0.4.2 -J bastion
+sssh serveur-prive # → ssh -J admin@203.0.113.10 deploy@10.0.4.2
+```
+
+Limite d'OpenSSH : `-J` ne prend pas de clé par hop ; si un bastion demande une clé spécifique, déclare-la dans `~/.ssh/config` (`Host bastion` + `IdentityFile`).
+
### Importer depuis `~/.ssh/config`
`sssh import-ssh` liste les alias concrets de ton `~/.ssh/config` qui ne sont pas encore dans sssh et te laisse cocher ceux à importer (espace pour sélectionner, `ctrl+a` pour tout prendre). Les champs supportés sont copiés : `HostName`, `User`, `Port`, `IdentityFile`, `ProxyJump`. Avec des noms en arguments, l'import est direct, sans interaction. La touche `i` dans le TUI fait la même chose.
internal/.DS_Store
main.go
@@ -90,14 +90,34 @@ func sshConfigCandidates(st *store.Store) []host.Host {
return out
}
-// resolveJump remplace un jump qui référence un host sssh par sa cible réelle.
+// resolveJump construit la chaîne ProxyJump complète. Un jump qui référence
+// un host sssh est remplacé par sa cible réelle, et si ce host a lui-même
+// un jump, la chaîne remonte (-J hop1,hop2,…) jusqu'au premier hop.
+// Un jump inconnu de sssh est passé tel quel à ssh ([user@]host[:port]).
func resolveJump(st *store.Store, h *host.Host) {
if h.Jump == "" {
return
}
- if j, ok := st.Get(h.Jump); ok {
- h.Jump = j.JumpSpec()
+ var chain []string
+ seen := map[string]bool{h.Name: true}
+ jump := h.Jump
+ for {
+ if seen[jump] {
+ break // cycle dans les jumps : on coupe la chaîne là
+ }
+ j, ok := st.Get(jump)
+ if !ok {
+ chain = append([]string{jump}, chain...)
+ break
+ }
+ seen[jump] = true
+ chain = append([]string{j.JumpSpec()}, chain...)
+ if j.Jump == "" {
+ break
+ }
+ jump = j.Jump
}
+ h.Jump = strings.Join(chain, ",")
}
func cmdConnect(name string, extra []string) error {