From 460d6cd198f62f3ed8679ffbf5fb9cefec4d98c0 Mon Sep 17 00:00:00 2001 From: Bailey Dixon Date: Sun, 30 Aug 2026 19:54:51 -0400 Subject: [PATCH] fix(installer): dedicate Tailscale listener port --- install.sh | 14 +++++++------ .../tests/test_install_tailscale_contract.py | 21 ++++++++++++++++--- plugin/tests/test_pair_cli_surface.py | 3 ++- skills/devops/hermes-relay-pair/SKILL.md | 9 ++++---- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/install.sh b/install.sh index 2db344f4..7c6679fe 100644 --- a/install.sh +++ b/install.sh @@ -734,8 +734,9 @@ cat > "$TS_SHIM_PATH" </dev/null 2>&1; then else printf "\n" printf " ${C_DIM}%s${C_RESET} %s\n" "$SYM_INFO" "Tailscale is installed. Publish Dashboard + optional API over your tailnet?" - printf " ${C_DIM}%s${C_RESET} %s\n" "$SYM_INFO" "Runs HTTPS ${C_BOLD}:443${C_RESET} → local Dashboard ${C_BOLD}:9119${C_RESET} (including Relay path), plus ${C_BOLD}:8642${C_RESET} for optional API fallback." + printf " ${C_DIM}%s${C_RESET} %s\n" "$SYM_INFO" "Runs dedicated HTTPS ${C_BOLD}:10443${C_RESET} → local Dashboard ${C_BOLD}:9119${C_RESET} (including Relay path), plus ${C_BOLD}:8642${C_RESET} for optional API fallback." printf " ${C_DIM}%s${C_RESET} %s\n" "$SYM_INFO" "Loopback services stay private; Tailscale handles TLS + identity." printf "\n ${C_BOLD}Enable now?${C_RESET} ${C_DIM}[y/N]${C_RESET} " read -r ts_reply /dev/null 2>&1; then if [ -n "$ts_do" ]; then if "$VENV_PY" -m plugin.relay.tailscale_cli enable >/dev/null 2>&1; then - ok "tailscale serve enabled on HTTPS :443 for local Dashboard :9119, plus optional API :8642" + ok "tailscale serve enabled on HTTPS :10443 for local Dashboard :9119, plus optional API :8642" info " Check: ${C_BOLD}hermes-relay-tailscale status${C_RESET}" info " Revoke: ${C_BOLD}hermes-relay-tailscale disable${C_RESET}" else @@ -967,7 +968,8 @@ printf " ${C_DIM}%s${C_RESET} ${C_BOLD}hermes-pair --mode auto --prefer tails printf " ${C_DIM}%s${C_RESET} ${C_BOLD}hermes-status${C_RESET} ${C_DIM}# show live phone state${C_RESET}\n" "$SYM_INFO" printf "\n" printf " ${C_BOLD}${C_CYAN}Remote access${C_RESET}\n" -printf " ${C_DIM}%s${C_RESET} ${C_BOLD}hermes-relay-tailscale enable${C_RESET} ${C_DIM}# HTTPS :443 → Dashboard :9119 + optional API :8642${C_RESET}\n" "$SYM_INFO" +printf " ${C_DIM}%s${C_RESET} ${C_BOLD}hermes-relay-tailscale enable${C_RESET} ${C_DIM}# HTTPS :10443 → Dashboard :9119 + optional API :8642${C_RESET}\n" "$SYM_INFO" +printf " ${C_DIM}%s${C_RESET} ${C_BOLD}hermes-relay-tailscale enable --dashboard-listener-port 443${C_RESET} ${C_DIM}# advanced override; only if :443 is free${C_RESET}\n" "$SYM_INFO" printf " ${C_DIM}%s${C_RESET} ${C_BOLD}hermes-relay-tailscale enable --port 8767${C_RESET} ${C_DIM}# legacy/direct Relay only${C_RESET}\n" "$SYM_INFO" printf " ${C_DIM}%s${C_RESET} ${C_BOLD}hermes-relay-tailscale status${C_RESET} ${C_DIM}# show served Tailscale ports${C_RESET}\n" "$SYM_INFO" printf "\n" diff --git a/plugin/tests/test_install_tailscale_contract.py b/plugin/tests/test_install_tailscale_contract.py index 8059cad4..dad5d2c2 100644 --- a/plugin/tests/test_install_tailscale_contract.py +++ b/plugin/tests/test_install_tailscale_contract.py @@ -23,16 +23,26 @@ class InstallTailscaleContractTests(unittest.TestCase): self.script, ) self.assertIn( - "HTTPS :443 for local Dashboard :9119, plus optional API :8642", + "HTTPS :10443 for local Dashboard :9119, plus optional API :8642", self.script, ) def test_installed_shim_marks_direct_relay_as_explicit_legacy(self) -> None: self.assertIn( - "hermes-relay-tailscale enable [--dashboard-listener-port 443] " + "hermes-relay-tailscale enable [--dashboard-listener-port 10443] " "[--dashboard-target-port 9119] [--api-port 8642] [--no-api]", self.script, ) + self.assertIn( + "hermes-relay-tailscale disable [--dashboard-listener-port 10443] " + "[--dashboard-target-port 9119] [--api-port 8642] [--no-api]", + self.script, + ) + self.assertIn( + "hermes-relay-tailscale enable --dashboard-listener-port 443 " + "# advanced override; only when :443 is free", + self.script, + ) self.assertIn( "hermes-relay-tailscale enable --port 8767 # explicit legacy/direct Relay", self.script, @@ -48,7 +58,12 @@ class InstallTailscaleContractTests(unittest.TestCase): )[1].split( '${C_BOLD}${C_CYAN}Self-setup / troubleshoot${C_RESET}', 1 )[0] - self.assertIn("HTTPS :443 → Dashboard :9119 + optional API :8642", remote_access) + self.assertIn("HTTPS :10443 → Dashboard :9119 + optional API :8642", remote_access) + self.assertIn( + "enable --dashboard-listener-port 443${C_RESET} " + "${C_DIM}# advanced override; only if :443 is free", + remote_access, + ) self.assertIn("legacy/direct Relay only", remote_access) self.assertNotIn("publish relay :8767", remote_access.lower()) self.assertNotIn("--https=9119", remote_access) diff --git a/plugin/tests/test_pair_cli_surface.py b/plugin/tests/test_pair_cli_surface.py index 1f111419..9ca581a0 100644 --- a/plugin/tests/test_pair_cli_surface.py +++ b/plugin/tests/test_pair_cli_surface.py @@ -40,8 +40,9 @@ class PairCliSurfaceTests(unittest.TestCase): ).read_text(encoding="utf-8") self.assertIn("--dashboard-url", skill) - self.assertIn("Tailscale Serve normally exposes HTTPS `443`", skill) + self.assertIn("Tailscale Serve normally exposes dedicated HTTPS `10443`", skill) self.assertIn("local Dashboard on `9119`", skill) + self.assertIn("Listener `443` is an advanced explicit override", skill) self.assertIn("LAN/raw-tailnet Dashboard candidates on\n `9119`", skill) self.assertIn("--legacy-direct-relay", skill) self.assertIn("does not imply or expose public port `8767`", skill) diff --git a/skills/devops/hermes-relay-pair/SKILL.md b/skills/devops/hermes-relay-pair/SKILL.md index 4d856bac..c82c52b7 100644 --- a/skills/devops/hermes-relay-pair/SKILL.md +++ b/skills/devops/hermes-relay-pair/SKILL.md @@ -36,7 +36,7 @@ Operators with the Hermes dashboard open can also mint the same QR from the web 1. **Hermes-Relay plugin installed into the Hermes venv.** Verify by running `python -m plugin.pair --help` — if it errors with `ModuleNotFoundError: No module named 'plugin'`, install it first: `pip install -e `. 2. **Hermes API server reachable** on `API_SERVER_HOST:API_SERVER_PORT` (default `127.0.0.1:8642`). `plugin.pair` auto-reads this from `~/.hermes/config.yaml` → `~/.hermes/.env` → env vars → defaults. -3. **Relay server running** on `RELAY_HOST:RELAY_PORT` (default `0.0.0.0:8767`) if the user wants terminal/bridge channels. The Relay may stay host-internal: current Android pairing normally reaches it through the Dashboard's same-origin plugin transport. Tailscale Serve normally exposes HTTPS `443` and proxies the host-local Dashboard on `9119`; a raw LAN/tailnet route may reach `9119` directly. Without a live relay, the QR will configure chat only. +3. **Relay server running** on `RELAY_HOST:RELAY_PORT` (default `0.0.0.0:8767`) if the user wants terminal/bridge channels. The Relay may stay host-internal: current Android pairing normally reaches it through the Dashboard's same-origin plugin transport. Tailscale Serve normally exposes dedicated HTTPS `10443` and proxies the host-local Dashboard on `9119`; a raw LAN/tailnet route may reach `9119` directly. Listener `443` is an advanced explicit override only when it is free. Without a live relay, the QR will configure chat only. 4. **Host is Linux or macOS.** The relay uses a real PTY backend, which is POSIX-only. Windows hosts can generate API-only QRs but the terminal channel will not work. ## Procedure @@ -60,9 +60,10 @@ Operators with the Hermes dashboard open can also mint the same QR from the web A trusted configured `HERMES_DASHBOARD_PUBLIC_URL` / Dashboard public URL is also suitable. If no exact origin is available, omit the flag; the pair backend will construct conventional LAN/raw-tailnet Dashboard candidates on - `9119`, or advertise the HTTPS `443` listener when Tailscale Serve maps it to - local Dashboard `9119`. Never substitute the API server URL or infer public - port `8767`. + `9119`, or advertise the HTTPS `10443` listener when Tailscale Serve maps it + to local Dashboard `9119`. HTTPS `443` is used only after the operator + explicitly selects that advanced listener and confirms it is free. Never + substitute the API server URL or infer public port `8767`. If `python` resolves to the wrong interpreter (plugin not found), use the Hermes venv explicitly: