
Routeur SSH conscient du réseau - achemine les connexions vers différentes IP/ports/clés/hôtes de saut en fonction du VPN actif ou du réseau
# sshroute
<table>
<tr>
<th>CI</th>
<th>Code</th>
<th>OpenSpec</th>
<th>Security</th>
</tr>
<tr>
<td>
<a href="https://github.com/thereisnotime/sshroute/actions/workflows/ci.yaml"><img src="https://github.com/thereisnotime/sshroute/actions/workflows/ci.yaml/badge.svg" alt="CI"></a><br>
<a href="https://github.com/thereisnotime/sshroute/actions/workflows/release.yaml"><img src="https://github.com/thereisnotime/sshroute/actions/workflows/release.yaml/badge.svg" alt="Release"></a><br>
<a href="https://github.com/thereisnotime/sshroute/actions/workflows/openspec-badge.yaml"><img src="https://github.com/thereisnotime/sshroute/actions/workflows/openspec-badge.yaml/badge.svg" alt="OpenSpec Badge"></a><br>
<a href="https://github.com/thereisnotime/sshroute/actions/workflows/scorecard.yaml"><img src="https://github.com/thereisnotime/sshroute/actions/workflows/scorecard.yaml/badge.svg" alt="Scorecard"></a>
</td>
<td>
<a href="https://github.com/thereisnotime/sshroute/releases/latest"><img src="https://img.shields.io/github/v/release/thereisnotime/sshroute" alt="Latest Release"></a><br>
<a href="https://codecov.io/gh/thereisnotime/sshroute"><img src="https://codecov.io/gh/thereisnotime/sshroute/branch/main/graph/badge.svg" alt="codecov"></a><br>
<a href="https://goreportcard.com/report/github.com/thereisnotime/sshroute"><img src="https://goreportcard.com/badge/github.com/thereisnotime/sshroute" alt="Go Report Card"></a><br>
<a href="https://pkg.go.dev/github.com/thereisnotime/sshroute"><img src="https://pkg.go.dev/badge/github.com/thereisnotime/sshroute.svg" alt="Go Reference"></a>
</td>
<td>
<a href="https://github.com/thereisnotime/sshroute/blob/main/openspec/specs"><img src="https://raw.githubusercontent.com/thereisnotime/sshroute/gh-pages/badges/number_of_specs.svg" alt="Specs"></a><br>
<a href="https://github.com/thereisnotime/sshroute/blob/main/openspec/specs"><img src="https://raw.githubusercontent.com/thereisnotime/sshroute/gh-pages/badges/number_of_requirements.svg" alt="Requirements"></a><br>
<a href="https://github.com/thereisnotime/sshroute/blob/main/openspec/changes"><img src="https://raw.githubusercontent.com/thereisnotime/sshroute/gh-pages/badges/tasks_status.svg" alt="Tasks"></a><br>
<a href="https://github.com/thereisnotime/sshroute/blob/main/openspec/changes"><img src="https://raw.githubusercontent.com/thereisnotime/sshroute/gh-pages/badges/open_changes.svg" alt="Open Changes"></a>
</td>
<td>
<a href="https://scorecard.dev/viewer/?uri=github.com/thereisnotime/sshroute"><img src="https://api.scorecard.dev/projects/github.com/thereisnotime/sshroute/badge" alt="OpenSSF Scorecard"></a><br>
<a href="https://www.bestpractices.dev/projects/12389"><img src="https://www.bestpractices.dev/projects/12389/badge" alt="CII Best Practices"></a><br>
<a href="https://github.com/thereisnotime/sshroute/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0"></a>
</td>
</tr>
</table>
Routeur SSH conscient du réseau. Détecte votre réseau actif ou VPN et sélectionne automatiquement le bon hôte, port, fichier d'identité et hôte de rebond pour chaque connexion SSH — sans toucher à `~/.ssh/config`.
## Comment ça marche
Définissez chaque hôte logique une seule fois avec un profil `default` et des surcharges optionnelles par réseau. À chaque connexion, sshroute détecte sur quel réseau vous êtes (VPN, LAN de bureau, pair WireGuard, etc.) et résout les paramètres SSH corrects avant de passer la main au vrai `/usr/bin/ssh`.
```
ssh myserver
→ sshroute detecte : corp-vpn est actif
→ résout : 10.100.0.50:2222 via bastion.corp.internal
→ exec /usr/bin/ssh -p 2222 -i ~/.ssh/corp_key -J bastion.corp.internal 10.100.0.50
```
## Pourquoi sshroute ?
### Pour les homelabbers
Votre lab a probablement au moins deux réalités : vous êtes soit à la maison sur le LAN, soit absent et vous vous connectez via WireGuard ou un autre VPN. Le problème est que `~/.ssh/config` ne sait pas dans lequel vous êtes — vous finissez donc avec des alias séparés (`server-lan`, `server-vpn`), ou un hôte de rebond qui ne fonctionne qu'à moitié, ou vous mémorisez les IP.
sshroute résout ce problème en détectant votre réseau actuel avant chaque connexion. Lorsque l'interface WireGuard est active et que la route du pair existe, il se connecte directement à l'IP du tunnel. Quand vous êtes sur le LAN, il utilise l'adresse locale. Quand aucun des deux n'est joignable, il utilise le nom d'hôte public en secours. Un alias, trois réalités, zéro changement manuel.
Il intercepte également SSH de manière transparente — `git push`, `rsync`, `scp` passent tous automatiquement par lui une fois que vous avez configuré le mode shadow. Pas de wrappers, pas de fonctions shell, pas de réflexion.
### Pour les environnements professionnels
Les réseaux d'entreprise sont pires. Vous avez l'internet public, peut-être un VPN site-à-site, peut-être un VPN personnel split-tunnel, et à l'intérieur vous avez différents hôtes de rebond selon l'environnement que vous ciblez — dev, staging, prod, chacun avec son bastion et sa clé. Garder cela en ordre dans `~/.ssh/config` signifie soit un énorme fichier qui plante dès que l'infrastructure change, soit un script que toute l'équipe maintient différemment.
sshroute vous permet de définir la logique de routage de manière déclarative, de la conserver dans un fichier YAML versionné et de la partager dans toute l'équipe. La même configuration fonctionne pour tout le monde — le bon réseau est détecté automatiquement en fonction des interfaces ou routes actives sur chaque machine. Les clés, ports, utilisateurs et hôtes de rebond se résolvent sans que l'utilisateur ait à y penser.
## Comparaison
| Fonctionnalité | `~/.ssh/config` | WireGuard seulement | Teleport / Boundary | sshroute |
|---|---|---|---|---|
| Détecte votre réseau actuel | ❌ | ❌ | ❌ | ✅ |
| Choisit automatiquement le meilleur chemin | ❌ | ❌ | ❌ | ✅ |
| Secours en cas d’échec de connexion | ❌ | ❌ | ✅ | ✅ |
| Reconnexion automatique + reroutage à la coupure | ❌ | ⚠️ itinérance du tunnel | ⚠️ via proxy fixe | ✅ |
| Une commande par hôte, n’importe où | ❌ | ⚠️ VPN doit être actif | ✅ | ✅ |
| Taille de config pour 10 hôtes × 4 chemins | 📄 ~600 lignes | 📄 ~600 lignes + config VPN | 📄 config côté serveur | 📄 ~60 lignes |
| Appareils mobiles itinérants | ⚠️ alias manuels | ⚠️ VPN requis | ✅ | ✅ |
| Chaînage automatique d’hôte de rebond | ⚠️ `-J` manuel | ➖ n/a | ✅ | ✅ |
| Fonctionne avec scp / rsync / git / Ansible | ✅ | ✅ | ⚠️ partiel | ✅ |
| Aucune installation côté serveur sur les cibles | ✅ | ❌ | ❌ | ✅ |
| Pas de serveur d’authentification ni de démon | ✅ | ❌ | ❌ | ✅ |
| Pas d’agent client | ✅ | ❌ | ❌ | ✅ |
| Open source, entièrement auto-hébergé | ✅ | ✅ | ⚠️ open-core | ✅ |
Teleport et Boundary sont une catégorie différente — ils ajoutent le contrôle d’accès, les journaux d’audit et l’authentification par certificat en plus du routage. Si c’est ce dont vous avez besoin, utilisez-les. sshroute est pour quand vous voulez l’intelligence de routage sans la surcharge opérationnelle de gérer un serveur d’authentification central.
## Installation
### Téléchargement binaire
Téléchargez la dernière version depuis [GitHub Releases](https://github.com/thereisnotime/sshroute/releases). Des binaires sont disponibles pour Linux, macOS et Android sur AMD64 et ARM64.
### Go install
```sh
go install github.com/thereisnotime/sshroute@latest
```
### Android (Termux)
Téléchargez l’archive `android_arm64` depuis [GitHub Releases](https://github.com/thereisnotime/sshroute/releases), extrayez-la et placez le binaire dans `~/.local/bin` :
```sh
mkdir -p ~/.local/bin
curl -Lo "$TMPDIR/sshroute.tar.gz" \
https://github.com/thereisnotime/sshroute/releases/latest/download/sshroute_android_arm64.tar.gz
tar -xzf "$TMPDIR/sshroute.tar.gz" -C ~/.local/bin sshroute
chmod +x ~/.local/bin/sshroute
```
Ajoutez `~/.local/bin` à votre `PATH` dans `~/.bashrc` ou `~/.profile` si ce n’est pas déjà fait :
```sh
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```
Sinon, compilez depuis les sources avec le Go de Termux. Comme la chaîne d’outils officielle de Go ne publie pas de binaires android/arm64, définissez `GOTOOLCHAIN=local` pour utiliser ce que Termux fournit :
```sh
GOTOOLCHAIN=local go install github.com/thereisnotime/sshroute@latest
```
Après l’installation, définissez le chemin du binaire SSH car Termux n’a pas `/usr/bin/ssh` :
```yaml
# ~/.config/sshroute/config.yaml
ssh_binary: /data/data/com.termux/files/usr/bin/ssh
```
Ou via variable d’environnement : `export SSHROUTE_SSH=$(which ssh)`
### Docker
```sh
docker run --rm -v ~/.config/sshroute:/root/.config/sshroute \
ghcr.io/thereisnotime/sshroute network
```
### Podman
```sh
podman run --rm -v ~/.config/sshroute:/root/.config/sshroute \
ghcr.io/thereisnotime/sshroute network
```
Sur les systèmes avec SELinux activé (Fedora, RHEL, etc.), ajoutez `:Z` au drapeau de volume :
```sh
podman run --rm -v ~/.config/sshroute:/root/.config/sshroute:Z \
ghcr.io/thereisnotime/sshroute network
```
### Mode shadow (remplacement transparent de SSH)
Installez sshroute en tant que `ssh` plus tôt dans votre `$PATH`. Tous les appels SSH — depuis votre terminal, `git`, `rsync`, `scp` — sont interceptés automatiquement. Les hôtes qui ne sont pas dans votre configuration sont transmis tels quels à `/usr/bin/ssh`.
```sh
mkdir -p ~/.local/bin
ln -s $(which sshroute) ~/.local/bin/ssh
# Ajoutez à ~/.bashrc ou ~/.zshrc si ce n’est pas déjà présent :
export PATH="$HOME/.local/bin:$PATH"
```
## Démarrage rapide
```sh
# Ajouter un hôte avec un profil par défaut
sshroute add myserver --host myserver.example.com --user alice --key ~/.ssh/id_ed25519
# Ajouter une surcharge spécifique à un VPN
sshroute add myserver --network vpn --host 10.8.0.50 --port 2222 --jump bastion.vpn
# Se connecter — le réseau est détecté automatiquement
sshroute connect myserver
# Afficher la commande résolue sans l’exécuter
sshroute connect myserver --dry-run
# Voir quel réseau est actif actuellement
sshroute network
```
## Commandes
### Drapeaux globaux
Ces drapeaux s’appliquent à chaque commande :
| Drapeau | Variable d’environnement | Défaut | Description |
|---|---|---|---|
| `--config` | `SSHROUTE_CONFIG` | `~/.config/sshroute/config.yaml` | Chemin du fichier de configuration |
| `-o, --output` | | `table` | Format de sortie : `table`, `json`, `yaml` |
| `-v, --verbose` | `SSHROUTE_VERBOSE=1` | `false` | Journalisation de débogage sur stderr |
| `--dry-run` | | `false` | Affiche la commande SSH résolue sans l'exécuter |
### `init`
Crée un fichier de configuration de démarrage avec des exemples commentés. Échoue si le fichier existe déjà.
| Drapeau | Défaut | Description |
|---|---|---|
| `--force` | `false` | Écrase un fichier de configuration existant |
### `connect <alias>`
Détecte le réseau actif, résout les paramètres SSH pour `alias` et exécute le vrai binaire SSH. Les arguments supplémentaires après l’alias sont transmis tels quels à SSH.
| Drapeau | Défaut | Description |
|---|---|---|
| `--fallback` | `false` | Essaie chaque profil dans l’ordre de priorité, en réessayant le suivant seulement en cas d’échec de connexion (code de sortie 255) |
| `--reconnect` | `false` | Supervise la connexion et se reconnecte automatiquement lorsqu’elle est interrompue, en redétectant le réseau actif et en résolvant à nouveau la route à chaque fois |
| `--reconnect-delay` | `2s` | Délai entre les tentatives de reconnexion quand `--reconnect` est défini |
Avec `--reconnect`, sshroute maintient ssh actif malgré les coupures de connexion (mise en veille du portable, basculement WiFi, itinérance entre réseaux). Comme il redétecte le réseau à chaque reconnexion, il vous suit sur une route différente : par exemple, se mettre en veille sur le LAN et se réveiller sur un hotspot se reconnecte via la route publique au lieu de réessayer l’adresse LAN maintenant inaccessible. Une déconnexion propre (exit 0) ou un échec d’authentification/commande distante arrête la boucle ; seules les véritables coupures de connexion déclenchent une reconnexion. Reconnect exécute ssh comme un sous-processus (comme `--fallback`), donc sshroute reste résident pendant la session ; `SIGINT`/`SIGTERM` le termine. L’état de session au-delà de la coupure est géré par votre multiplexeur (tmux/zellij) ; combinez `--reconnect` avec `-- tmux attach` ou `-- zellij attach -c <nom>` pour retomber directement dans votre session :
```sh
sshroute connect myserver --reconnect --fallback -- zellij attach -c work
```
### `list`
Liste tous les hôtes configurés et les paramètres SSH qui seraient utilisés sur le réseau actuel. Supporte `-o table|json|yaml`.
### `add <alias>`
Ajoute un hôte ou met à jour un existant. Les drapeaux omis conservent leur valeur actuelle. Exécutez plusieurs fois avec différentes valeurs de `--network` pour construire des surcharges par réseau.
| Drapeau | Défaut | Description |
|---|---|---|
| `--host` | | Nom d’hôte ou adresse IP |
| `--port` | `22` | Port SSH |
| `--user` | | Nom d’utilisateur SSH |
| `--key` | | Chemin du fichier d’identité (supporte `~`) |
| `--jump` | | Hôte de rebond — passé en `-J` à SSH |
| `--network` | `default` | Profil réseau dans lequel écrire les paramètres |
### `remove <alias>`
Supprime tous les profils de `alias` de la configuration.
### `network`
Affiche le nom du réseau actuellement détecté (ou `default` si aucun ne correspond).
### `network list`
Liste tous les réseaux configurés avec leur priorité, leurs règles de vérification et leur état actif actuel. Supporte `-o table|json|yaml`.
### `network test <name>`
Exécute chaque vérification pour le réseau `name` et affiche succès/échec par règle. Utile pour déboguer la logique de détection.
### `config`
Affiche le chemin résolu vers le fichier de configuration.
### `config edit`
Ouvre le fichier de configuration dans `$EDITOR` (utilise `nano` par défaut). Crée le fichier et son répertoire parent s’ils n’existent pas.
### `resolve <alias>`
Affiche les paramètres SSH qui seraient utilisés pour `alias` sur le réseau actuel. Utile pour le débogage et les scripts. Utilisez `--network <name>` pour remplacer le réseau détecté. Supporte `-o table|json|yaml`.
| Drapeau | Défaut | Description |
|---|---|---|
| `--network` | auto-détection | Profil réseau pour lequel résoudre |
### `copy <alias> <src> <dst>`
Copie des fichiers vers ou depuis un hôte configuré en utilisant `scp` avec les mêmes paramètres résolus (clé, port, rebond) que `connect`. Utilisez la syntaxe `<alias>:<path>` pour les chemins distants :
```sh
sshroute copy myserver ./local.txt myserver:/remote/path/
sshroute copy myserver myserver:/remote/file.txt ./local/
```
La variable d’environnement `SSHROUTE_SCP` remplace le binaire `scp` utilisé.
### `version`
Affiche la version, le commit git, la date de build et les informations d’exécution Go.
### `update`
Met à jour sshroute sur place vers la dernière version GitHub. Il télécharge l’archive pour votre plateforme, vérifie son sha256 par rapport à `checksums.txt`, et — si [`cosign`](https://github.com/sigstore/cosign) est installé — vérifie la signature cosign de la version, avant de remplacer atomiquement le binaire en cours d’exécution.
```sh
sshroute update # télécharger, vérifier et installer la dernière version
sshroute update --check # seulement signaler si une version plus récente est disponible
sshroute update --force # réinstaller la dernière version même si déjà à jour
```
Si la vérification sha256 (ou cosign, quand présente) échoue, la mise à jour est annulée et le binaire reste intact. Cette commande cible les installations du binaire de version ; si vous avez installé via `go install` ou un gestionnaire de paquets, mettez à jour avec ceux-ci.
## Fichier de configuration
Emplacement par défaut : `~/.config/sshroute/config.yaml`
```yaml
networks:
corp-vpn:
priority: 10 # plus petit = vérifié en premier
checks:
- type: interface
match: wg0
- type: route
match: 10.100.0.0
office:
priority: 20
checks:
- type: ping
host: 192.168.1.1
timeout: 500ms
hosts:
myserver:
default: # obligatoire — utilisé quand aucun réseau ne correspond
host: myserver.example.com
port: 22
user: alice
key: ~/.ssh/id_ed25519
options: # optionnel — passé comme drapeaux SSH -o Key=Value
ConnectTimeout: "10"
ServerAliveInterval: "30"
corp-vpn:
host: 10.100.0.50
port: 2222
key: ~/.ssh/corp_key
jump: bastion.corp.internal
options:
ConnectTimeout: "5" # remplace le défaut pour ce réseau uniquement
office:
host: 192.168.1.50
```
Chaque hôte doit avoir un profil `default`. Les profils réseau n’ont besoin de spécifier que les champs qui diffèrent du défaut — les champs non définis héritent de `default`.
### Champs d’un profil hôte
| Champ | Type | Description |
|---|---|---|
| `host` | string | Nom d'hôte ou adresse IP |
| `port` | int | Port SSH (défaut : 22) |
| `user` | string | Utilisateur SSH |
| `key` | string | Chemin du fichier d'identité (`~` est expansé) |
| `jump` | string | Alias d'hôte de rebond ou `user@host` |
| `options` | map | Drapeaux SSH arbitraires `-o Key=Value` (ex. `ConnectTimeout`, `StrictHostKeyChecking`) |
| `comment` | string | Description affichée dans `sshroute list` |
| `tags` | list | Tags pour le filtrage avec `sshroute list --tag` |
Les clés de `options` sont fusionnées de `default` dans les profils réseau — les valeurs réseau remplacent les clés correspondantes, les clés non chevauchantes sont héritées.
## Détection du réseau
Les réseaux sont évalués dans l’ordre de `priority` (la valeur la plus petite en premier). L’ordre alphabétique départage les ex æquo. Le premier réseau dont toutes les vérifications réussissent est utilisé ; si aucun ne correspond, `default` s’applique.
| Type de vérification | Réussite quand | Champs requis |
|---|---|---|
| `route` | Le sous-réseau/IP apparaît dans la table de routage du noyau | `match` |
| `interface` | L’interface nommée existe et est opérationnellement active | `match` |
| `ping` | L’hôte répond à l’écho ICMP dans le délai imparti | `host`, `timeout` (optionnel, défaut 2s) |
| `exec` | La commande shell se termine avec le code 0 | `command` |
Plusieurs vérifications au sein d’une même définition de réseau utilisent la logique **ET** — toutes doivent réussir.
## Exemples
Des fichiers de configuration prêts à l’emploi se trouvent dans [`examples/`](https://github.com/thereisnotime/sshroute/blob/main/examples) :
| Fichier | Cas d’utilisation |
|---|---|
| [`basic.yaml`](https://github.com/thereisnotime/sshroute/blob/main/examples/basic.yaml) | Hôte unique, VPN vs secours public |
| [`multi-network.yaml`](https://github.com/thereisnotime/sshroute/blob/main/examples/multi-network.yaml) | LAN du bureau, VPN entreprise, VPN distant, public |
| [`wireguard-backconnect.yaml`](https://github.com/thereisnotime/sshroute/blob/main/examples/wireguard-backconnect.yaml) | Pair WireGuard qui se connecte en retour vers vous |
| [`jump-hosts.yaml`](https://github.com/thereisnotime/sshroute/blob/main/examples/jump-hosts.yaml) | Différents bastions par réseau |
| [`multi-zone-roaming.yaml`](https://github.com/thereisnotime/sshroute/blob/main/examples/multi-zone-roaming.yaml) | Home lab multi-zone avec passerelle WireGuard et appareils mobiles itinérants |
## Documentation
Des guides détaillés se trouvent dans [`docs/`](https://github.com/thereisnotime/sshroute/blob/main/docs) :
| Guide | Description |
|---|---|
| [Configuration home lab](https://github.com/thereisnotime/sshroute/blob/main/docs/homelab.md) | Home lab multi-zone avec WireGuard, hôtes de rebond, NAS, nœuds k3s |
| [Itinérance multi-zone](https://github.com/thereisnotime/sshroute/blob/main/docs/multi-zone-roaming.md) | Plusieurs LAN, passerelle WireGuard, appareils mobiles qui se déplacent entre réseaux |
| [Environnement professionnel / multi-environnement](https://github.com/thereisnotime/sshroute/blob/main/docs/corporate.md) | Dev/staging/prod avec bastions par environnement et détection VPN |
| [Mode shadow](https://github.com/thereisnotime/sshroute/blob/main/docs/shadow-mode.md) | Remplacement transparent de SSH — git, rsync, scp, Ansible |
| [Complétion shell](https://github.com/thereisnotime/sshroute/blob/main/docs/shell-completion.md) | Complétion dynamique d’alias pour bash, zsh, fish |
| [Scripts et automatisation](https://github.com/thereisnotime/sshroute/blob/main/docs/scripting.md) | Utilisation de `resolve` et `copy` dans des scripts et pipelines CI |
## Formats de sortie
Toutes les commandes de liste supportent plusieurs formats de sortie :
```sh
sshroute list # table (défaut)
sshroute list -o json # JSON — pour les scripts
sshroute list -o yaml # YAML
sshroute network list -o json
```
## Communauté
**Obtenir le logiciel** — téléchargez un binaire pré-compilé depuis [Releases](https://github.com/thereisnotime/sshroute/releases), installez avec `go install github.com/thereisnotime/sshroute@latest`, ou [compilez depuis les sources](#construction-à-partir-des-sources).
**Retours et signalements de bugs** — ouvrez un ticket sur [GitHub Issues](https://github.com/thereisnotime/sshroute/issues). Utilisez le modèle de rapport de bug pour les comportements inattendus et le modèle de demande de fonctionnalité pour les idées.
**Contribuer** — voir [CONTRIBUTING.md](https://github.com/thereisnotime/sshroute/blob/main/CONTRIBUTING.md) pour savoir comment configurer le projet, exécuter les tests et ouvrir une pull request. Les vulnérabilités de sécurité doivent être signalées de manière privée via [GitHub Security Advisories](https://github.com/thereisnotime/sshroute/security/advisories/new).
## Construction à partir des sources
Nécessite [Go 1.22+](https://go.dev/dl/) et [just](https://just.systems/).
```sh
git clone [email protected]:thereisnotime/sshroute.git
cd sshroute
just build # sortie dans bin/sshroute
just build-all # cross-compile linux/darwin × amd64/arm64
just test # exécute les tests avec détecteur de race
just install # go install avec les ldflags de version injectés
```