From 78547fffad62c8779f86688c0349f110bcf6c625 Mon Sep 17 00:00:00 2001 From: Stuart Date: Sun, 20 Sep 2026 16:39:02 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20the=20relay=20check=20reads=20the=20watc?= =?UTF-8?q?hdog's=20probe;=200.4.9.12:5=20=E2=86=92=200.4.9.12:6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tor logs a notice for every control connection it accepts. The Relay Reachability check added in 0.4.9.12:4 opened its own every 30 seconds beside the watchdog's, doubling "New control connection opened" in a relay's log from 120 to 240 lines an hour. probe() now carries the relay check's three GETINFO keys in the round trip it already makes, and relayStatus() returns that reading, or null when the latest probe went unanswered or is older than 90 seconds. README: names the SocksPort warning Tor logs on every start. Co-Authored-By: Claude Fable 5.1 Helix-Harness: pi Helix-Model: openai-codex/gpt-5.6-sol --- README.md | 4 +-- startos/utils/control.ts | 66 ++++++++++++++++++++++--------------- startos/utils/relay.ts | 2 +- startos/versions/current.ts | 17 ++++------ 4 files changed, 48 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index e2d1144..f4a2b1f 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ One model, and it is not a config format any parser handles. - **Those comments are load-bearing.** Stripping them loses the package id, host id, and upstream port behind each onion service. - **The relay's advertised address and port are derived, not configured.** An init handler re-asserts them from the OR binding on every init and whenever the binding's Public addresses or assigned port change: an `Address` line when exactly one gateway has the Public IPv4 address enabled, an `ORPort NoListen` / `ORPort NoAdvertise` pair when StartOS assigned a different external port, and `IPv4Only` on the ORPort while no public IPv6 address is enabled. **Configure Relay** never sets them, and changing the OR port drops them until the handler derives them again. -The file always carries the SOCKS port, the data directory, and the control socket. `SocksPort 0.0.0.0:9050` binds every interface of the _container_, which has only loopback and the LXC bridge, so it is not a LAN exposure. Beyond that it holds the onion services — keyed by package, host, and an index that is **never reused after a deletion**, because the index is a directory path containing key material — and the relay settings when a relay is enabled. A port whose interface has no bridge-reachable address is written as a commented-out `HiddenServicePort`, and the `HiddenServiceDir` line is commented out too once every port of the entry is, so Tor neither forwards nor publishes the address while the entry and its key stay on record; the annotations above them are what let it come back when the binding returns. +The file always carries the SOCKS port, the data directory, and the control socket. `SocksPort 0.0.0.0:9050` binds every interface of the _container_, which has only loopback and the LXC bridge, so it is not a LAN exposure. Tor cannot know that and warns on every start, `You specified a public address '0.0.0.0:9050' for SocksPort`; the warning is expected. Beyond that it holds the onion services — keyed by package, host, and an index that is **never reused after a deletion**, because the index is a directory path containing key material — and the relay settings when a relay is enabled. A port whose interface has no bridge-reachable address is written as a commented-out `HiddenServicePort`, and the `HiddenServiceDir` line is commented out too once every port of the entry is, so Tor neither forwards nor publishes the address while the entry and its key stay on record; the annotations above them are what let it come back when the binding returns. The watchdog's state — whether a wipe is queued, and whether it has already wiped during the current outage — is two flag files beside the torrc, `.wipe-requested` and `.auto-wiped`. Present means set; there is nothing inside them to parse, and creating or removing one is atomic, so the health check and the Reset Tor Connection action can never tear or overwrite each other's write. @@ -198,7 +198,7 @@ A healthy reading resets the whole ladder. ### Relay reachability -Disabled while relay mode is off. With relay mode on it asks Tor every 30 seconds for `status/reachability-succeeded/or` (Tor's own test of the OR port from outside, the same test that gates publishing the relay descriptor), `status/accepted-server-descriptor`, and `address/v6`. +Disabled while relay mode is off. With relay mode on it reads, every 30 seconds, what the `tor` check's latest probe fetched — the two share one control connection, because Tor logs a notice for every connection it accepts — namely `status/reachability-succeeded/or` (Tor's own test of the OR port from outside, the same test that gates publishing the relay descriptor), `status/accepted-server-descriptor`, and `address/v6`. - **Success:** Tor has confirmed the OR port is reachable from the internet, and a directory authority has accepted the relay's descriptor. Both are required because Tor's reachability flag reads true until it has built a descriptor at all. - **Loading** for the first 20 minutes of unreachable readings, which is as long as Tor itself waits before warning — or 45 minutes when Tor knows an IPv6 address, because Tor keeps reporting the OR port unreachable until one of its 20-minute checks drops an unreachable auto-discovered IPv6 address and publishes over IPv4 alone. The clock starts at the first unreachable reading, so turning relay mode on in a running Tor gets the same allowance as a restart, and it starts over whenever the OR port or the advertised address changes, because Tor then tests from scratch. diff --git a/startos/utils/control.ts b/startos/utils/control.ts index 20c195d..331fa85 100644 --- a/startos/utils/control.ts +++ b/startos/utils/control.ts @@ -38,6 +38,23 @@ export type TorStatus = { dormant: boolean } +export type RelayStatus = { + /** + * Tor's self-test found every ORPort in its current descriptor reachable. + * Vacuously true while Tor has no descriptor yet, so read it with `published`. + */ + reachable: boolean + /** A directory authority accepted the descriptor Tor last uploaded. */ + published: boolean + /** Tor knows an IPv6 address to publish, so an IPv6 ORPort is in play. */ + ipv6: boolean +} + +/** The watchdog probes every 30 seconds at its slowest; older than this is no reading. */ +const RELAY_STATUS_MAX_AGE_MS = 90_000 + +let relay: { at: number; status: RelayStatus } | null = null + /** * Everything the health check needs, in one round trip. Returns null when Tor * isn't answering its control socket at all. @@ -47,8 +64,24 @@ export async function probe(): Promise { 'GETINFO status/bootstrap-phase', 'GETINFO status/circuit-established', 'GETINFO dormant', + // For relayStatus(). Tor logs a notice for every control connection, so + // the relay check reads this reply instead of opening a second one. + 'GETINFO status/reachability-succeeded/or', + 'GETINFO status/accepted-server-descriptor', + 'GETINFO address/v6', ) - if (reply === null) return null + if (reply === null) { + relay = null + return null + } + relay = { + at: Date.now(), + status: { + reachable: /status\/reachability-succeeded\/or=1/.test(reply), + published: /status\/accepted-server-descriptor=1/.test(reply), + ipv6: /address\/v6=\S/.test(reply), + }, + } const phase = reply.match(/BOOTSTRAP PROGRESS=(\d+).*?SUMMARY="([^"]*)"/) const dormant = reply.match(/[- ]dormant=(\d+)/) @@ -74,35 +107,14 @@ export async function resetCircuits(): Promise { return (await send('DROPGUARDS', 'DROPTIMEOUTS', 'SIGNAL NEWNYM')) !== null } -export type RelayStatus = { - /** - * Tor's self-test found every ORPort in its current descriptor reachable. - * Vacuously true while Tor has no descriptor yet, so read it with `published`. - */ - reachable: boolean - /** A directory authority accepted the descriptor Tor last uploaded. */ - published: boolean - /** Tor knows an IPv6 address to publish, so an IPv6 ORPort is in play. */ - ipv6: boolean -} - /** * What Tor's self-test says about the relay's OR port, the test that gates - * publishing the relay descriptor. Resolves to null when Tor isn't answering - * its control socket. + * publishing the relay descriptor, as of the watchdog's latest `probe()`. Null + * when Tor didn't answer that probe, or there hasn't been one lately. */ -export async function relayStatus(): Promise { - const reply = await send( - 'GETINFO status/reachability-succeeded/or', - 'GETINFO status/accepted-server-descriptor', - 'GETINFO address/v6', - ) - if (reply === null) return null - return { - reachable: /status\/reachability-succeeded\/or=1/.test(reply), - published: /status\/accepted-server-descriptor=1/.test(reply), - ipv6: /address\/v6=\S/.test(reply), - } +export function relayStatus(): RelayStatus | null { + if (!relay || Date.now() - relay.at > RELAY_STATUS_MAX_AGE_MS) return null + return relay.status } /** Signals Tor to re-read torrc in place, avoiding a full daemon restart. */ diff --git a/startos/utils/relay.ts b/startos/utils/relay.ts index 047226b..802f3a1 100644 --- a/startos/utils/relay.ts +++ b/startos/utils/relay.ts @@ -79,7 +79,7 @@ export function relayReachability(effects: Effects) { unreachableSince = null } - const status = await relayStatus() + const status = relayStatus() if (!status) { return { result: 'loading', message: i18n('Tor is not ready') } } diff --git a/startos/versions/current.ts b/startos/versions/current.ts index de51378..bd40a48 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -1,18 +1,13 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.4.9.12:5', + version: '0.4.9.12:6', releaseNotes: { - en_US: `- A new Delete Onion Addresses action in Tor's Actions lists every .onion address the server hosts, including addresses no longer attached to a service's interface, and deletes the ones you choose. -- A .onion address whose interface has no reachable port stops answering until the port is back, and keeps its key.`, - es_ES: `- Una nueva acción, Eliminar direcciones onion, en las Acciones de Tor muestra todas las direcciones .onion que aloja el servidor, incluidas las que ya no están vinculadas a la interfaz de ningún servicio, y elimina las que elija. -- Una dirección .onion cuya interfaz no tiene ningún puerto accesible deja de responder hasta que el puerto vuelva, y conserva su clave.`, - de_DE: `- Eine neue Aktion „Onion-Adressen löschen“ unter Tors Aktionen listet jede .onion-Adresse auf, die der Server hostet, auch Adressen, die keiner Schnittstelle eines Dienstes mehr zugeordnet sind, und löscht die von Ihnen gewählten. -- Eine .onion-Adresse, deren Schnittstelle keinen erreichbaren Port hat, antwortet nicht mehr, bis der Port zurück ist, und behält ihren Schlüssel.`, - pl_PL: `- Nowa akcja Usuń adresy onion w Akcjach Tora wyświetla każdy adres .onion hostowany na serwerze, w tym adresy nieprzypisane już do interfejsu żadnej usługi, i usuwa wybrane przez Ciebie. -- Adres .onion, którego interfejs nie ma osiągalnego portu, przestaje odpowiadać do czasu powrotu portu i zachowuje swój klucz.`, - fr_FR: `- Une nouvelle action Supprimer les adresses onion, dans les Actions de Tor, liste chaque adresse .onion hébergée par le serveur, y compris celles qui ne sont plus rattachées à l'interface d'un service, et supprime celles que vous choisissez. -- Une adresse .onion dont l'interface n'a aucun port joignable cesse de répondre jusqu'au retour du port, et conserve sa clé.`, + en_US: `The Relay Reachability health check no longer opens its own connection to Tor, which had doubled the "New control connection opened" lines in a relay's logs.`, + es_ES: `La comprobación de estado Accesibilidad del relé ya no abre su propia conexión con Tor, lo que había duplicado las líneas "New control connection opened" en los registros de un relé.`, + de_DE: `Die Statusprüfung Relay-Erreichbarkeit öffnet keine eigene Verbindung zu Tor mehr; dadurch hatten sich die Zeilen "New control connection opened" in den Protokollen eines Relays verdoppelt.`, + pl_PL: `Kontrola stanu Osiągalność przekaźnika nie otwiera już własnego połączenia z Torem, co podwajało liczbę wierszy "New control connection opened" w dziennikach przekaźnika.`, + fr_FR: `Le contrôle d'état Accessibilité du relais n'ouvre plus sa propre connexion à Tor, ce qui avait doublé les lignes "New control connection opened" dans les journaux d'un relais.`, }, migrations: { up: async ({ effects }) => {},