[IMP] deploy: probe the remote port before opening the tunnel
A stopped service on the far side showed up only as a wall of « channel N:
open failed: connect failed: Connection refused », one line per browser
request, saying nothing about WHICH end refused. The tunnel itself was fine;
there was simply nothing to reach. Diagnosing it took three commands.
The port is now probed first, and the answer is plain:
⚠ Rien n'écoute sur le port 8069 de test-vm_02+erplibre-ubuntu-2404
Démarrer le service là-bas, ou continuer quand même.
Continuer quand même ? (o/N)
The probe opens a real TCP connection to « localhost:<port> » from the remote
host rather than reading its listening table. That is exactly what the tunnel
will do — same host resolution, same IPv4/IPv6 choice — so it cannot say open
where the tunnel would fail. It also needs no ss or netstat, which minimal
images lack.
Three outcomes, three behaviours: listening goes straight through, closed
warns and asks (default no), and an inconclusive probe — unreachable host, no
bash — says so and continues rather than blocking on its own uncertainty.
Verified against the real VM: port 22 open, port 9999 closed, an unknown host
inconclusive, and port 8069 correctly reported closed after the Odoo service
had been stopped — the very case that prompted this. Then at flow level:
refusing aborts without opening anything, forcing opens the tunnel anyway, and
an inconclusive probe still opens it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2997dfeb26
commit
3e53cbd47d
2 changed files with 77 additions and 0 deletions
|
|
@ -5372,6 +5372,43 @@ class TODO:
|
||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _remote_port_open(host, port):
|
||||||
|
"""Quelqu'un écoute-t-il sur ce port DEPUIS l'hôte distant ?
|
||||||
|
|
||||||
|
True / False / None quand on n'a pas pu conclure (hôte injoignable,
|
||||||
|
pas de bash). On teste une vraie connexion TCP vers « localhost » et
|
||||||
|
non la table d'écoute : c'est exactement ce que fera le tunnel, y
|
||||||
|
compris le choix IPv4/IPv6 de la résolution.
|
||||||
|
"""
|
||||||
|
probe = (
|
||||||
|
f"exec 3<>/dev/tcp/localhost/{int(port)} && echo OPEN"
|
||||||
|
" || echo CLOSED"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
res = subprocess.run(
|
||||||
|
[
|
||||||
|
"ssh",
|
||||||
|
"-o",
|
||||||
|
"BatchMode=yes",
|
||||||
|
"-o",
|
||||||
|
"ConnectTimeout=10",
|
||||||
|
host,
|
||||||
|
f"bash -c {shlex.quote(probe)} 2>/dev/null",
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=45,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return None
|
||||||
|
out = res.stdout.strip()
|
||||||
|
if "OPEN" in out:
|
||||||
|
return True
|
||||||
|
if "CLOSED" in out:
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
def _deploy_port_forward(self):
|
def _deploy_port_forward(self):
|
||||||
"""Ouvre un tunnel SSH pour joindre un service distant depuis le
|
"""Ouvre un tunnel SSH pour joindre un service distant depuis le
|
||||||
navigateur local.
|
navigateur local.
|
||||||
|
|
@ -5397,6 +5434,22 @@ class TODO:
|
||||||
raw = input(f"{t('Local port (default:')} {remote}): ").strip()
|
raw = input(f"{t('Local port (default:')} {remote}): ").strip()
|
||||||
local = raw if raw.isdigit() else remote
|
local = raw if raw.isdigit() else remote
|
||||||
|
|
||||||
|
# Sonde AVANT d'ouvrir : sans elle, un service arrêté à l'autre bout
|
||||||
|
# ne se manifeste que par un mur de « channel N: open failed » à
|
||||||
|
# chaque requête du navigateur, qui ne dit pas d'où vient le refus.
|
||||||
|
print(f" {t('Checking the remote port...')}")
|
||||||
|
listening = self._remote_port_open(host, remote)
|
||||||
|
if listening is False:
|
||||||
|
print(
|
||||||
|
f" ⚠ {t('Nothing is listening on port')} {remote}"
|
||||||
|
f" {t('of')} {host}"
|
||||||
|
)
|
||||||
|
print(f" {t('Start the service there, or continue anyway.')}")
|
||||||
|
if not self._is_yes(input(t("Continue anyway? (y/N): "))):
|
||||||
|
return
|
||||||
|
elif listening is None:
|
||||||
|
print(f" ℹ️ {t('Could not probe the remote port; going on.')}")
|
||||||
|
|
||||||
if not self._port_is_free(local):
|
if not self._port_is_free(local):
|
||||||
print(f" ⚠ {t('Local port already in use:')} {local}")
|
print(f" ⚠ {t('Local port already in use:')} {local}")
|
||||||
if not self._is_yes(input(t("Try anyway? (y/N): "))):
|
if not self._is_yes(input(t("Try anyway? (y/N): "))):
|
||||||
|
|
|
||||||
|
|
@ -1419,6 +1419,30 @@ TRANSLATIONS = {
|
||||||
"fr": "Port local (défaut :",
|
"fr": "Port local (défaut :",
|
||||||
"en": "Local port (default:",
|
"en": "Local port (default:",
|
||||||
},
|
},
|
||||||
|
"Checking the remote port...": {
|
||||||
|
"fr": "Vérification du port distant...",
|
||||||
|
"en": "Checking the remote port...",
|
||||||
|
},
|
||||||
|
"Nothing is listening on port": {
|
||||||
|
"fr": "Rien n'écoute sur le port",
|
||||||
|
"en": "Nothing is listening on port",
|
||||||
|
},
|
||||||
|
"of": {
|
||||||
|
"fr": "de",
|
||||||
|
"en": "of",
|
||||||
|
},
|
||||||
|
"Start the service there, or continue anyway.": {
|
||||||
|
"fr": "Démarrer le service là-bas, ou continuer quand même.",
|
||||||
|
"en": "Start the service there, or continue anyway.",
|
||||||
|
},
|
||||||
|
"Continue anyway? (y/N): ": {
|
||||||
|
"fr": "Continuer quand même ? (o/N, défaut : non) : ",
|
||||||
|
"en": "Continue anyway? (y/N, default: no): ",
|
||||||
|
},
|
||||||
|
"Could not probe the remote port; going on.": {
|
||||||
|
"fr": "Impossible de sonder le port distant ; on continue.",
|
||||||
|
"en": "Could not probe the remote port; going on.",
|
||||||
|
},
|
||||||
"Local port already in use:": {
|
"Local port already in use:": {
|
||||||
"fr": "Port local déjà occupé :",
|
"fr": "Port local déjà occupé :",
|
||||||
"en": "Local port already in use:",
|
"en": "Local port already in use:",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue