# Router console Status: shipped in [#54](https://github.com/tcivie/eepview/pull/54). The console tab and the one link (R7, R12, R15, R23–R30): in progress in [#76](https://github.com/tcivie/eepview/pull/76). Router statistics from the console (R31–R46): in progress in [#78](https://github.com/tcivie/eepview/pull/78). eepview shows router information. It does not change the router configuration. All router configuration goes through the router's own console pages. eepview finds the console of the router in use and gives one link to it, "I2P Router Console". The user reaches every other console page from the console itself. When the router has no eepview helper, eepview also reads the router statistics from that console (R31–R46). ## How it works 1. **Detect.** When a page that shows the links opens, eepview reads the console port from the router configuration files, when it finds them. Then it sends one loopback `GET` to each candidate port. A port counts only when the answer looks like that router's console. 2. **Show.** The link shows in the router panel, on the home page and in Settings. With no console, it is hidden and one line says "No router console found". 3. **Open.** The link opens the console home page in the console tab: a tab of its own kind in the tab strip. Its webview, `console`, sits in the content area like a web tab. It loads only the detected console origin, `http://127.0.0.1:`. 4. **Read the statistics.** Without a router helper, `router_stats()` reads the figures of the router panel and the Network page from the detected console: one read-only loopback `GET` per refresh, while those figures show (R31–R46). The console tab is not a web tab, and its webview is not a `tab-*` webview. The webview is built in `src-tauri/src/shell/console.rs` only. It never uses the gatekeeper, and the gatekeeper never sees it. An engine rule list confines it to the console origin (R19). The `tab-*` webviews keep all five layers and still cannot reach loopback. [ADR 0001](adr-0001-no-leak-architecture.md#router-console-exception) records this exception. ## Requirements ### Detection - **R1 Candidate ports.** The candidates are, in this order, with no duplicates: 1. the Java I2P console ports read from the Java I2P configuration (R2); 2. the i2pd web console port read from `i2pd.conf` (R2); 3. the Java I2P default, `7657`; 4. the i2pd default, `7070`. - **R2 Configuration files.** - Java I2P: in the configuration folder, every file in `clients.config.d/` (by file name), then `clients.config`. The console is the client whose `clientApp..main` is `net.i2p.router.web.RouterConsoleRunner`. Its port is the first whitespace-separated token of `clientApp..args` that parses fully as a non-zero `u16` and does not follow `-s` (for example `7657 ::1,127.0.0.1 ./webapps/` gives `7657`). The token after `-s` is a TLS port and is skipped, so the TLS-only `-s 7667 ::1,127.0.0.1 ./webapps/` gives no port; `127.0.0.1` is not a `u16` token. A client with `clientApp..startOnLoad=false` gives no port. - i2pd: the `port` key in the `[http]` section of `i2pd.conf`. `enabled = false` in that section gives no port. Keys outside `[http]` are ignored. `#` starts a comment. - Folders. Java I2P: macOS `$HOME/Library/Application Support/i2p`; Linux `$HOME/.i2p` and `/var/lib/i2p/i2p-config`; Windows `%LOCALAPPDATA%\I2P` and `%APPDATA%\I2P`. i2pd: macOS `$HOME/Library/Application Support/i2pd/i2pd.conf`; Linux `$HOME/.i2pd/i2pd.conf` and `/etc/i2pd/i2pd.conf`; Windows `%APPDATA%\i2pd\i2pd.conf`. A path whose variable is not set is left out. A missing or unreadable file gives no port. - **R3 Verification.** A candidate counts only when one `GET` to `http://127.0.0.1:` answers `200` and the body has the marker of that console: | Router | Probe path | Marker (all must be in the body) | |---|---|---| | Java I2P | `/home` | `/themes/console/` and `console.css` | | i2pd | `/` | `?page=i2p_tunnels` | A closed port, another status, a body without the marker, or the console of the other router type does not count. The markers do not depend on the console language. - **R4 Loopback only.** The probe connects only through `LoopbackAddr` to `127.0.0.1`, with a 5 s timeout. It sends one `GET HTTP/1.0` in origin form, with `Host: 127.0.0.1:`, so the answer is never chunked. It never follows a redirect. The probe code lives in `src-tauri/src/net/` (ADR 0001 rule 2). - **R5 Result.** The first candidate that passes R3 is the console. If none passes, there is no console. Only the detector makes a `VerifiedConsole`. - **R6 On demand.** eepview never probes at start. Detection runs only on `console_detect()`. The UI calls it when the router panel opens, and when the home page or the Settings page loads while it is the active tab. While a console is known, eepview checks it again every 10 s (R21). When a trigger finds none, eepview retries (R20). A run that never shows these pages, such as the leak test with `EEPVIEW_START_URL`, opens no extra socket. When the result changes, eepview emits `console-changed`. When the console goes away (R21) or moves to another port, the console tab closes (R30). ### The console page - **R7 Home page.** The link opens the console home page. It is the probe page of R3: | Router | Home page | |---|---| | Java I2P | `/home` | | i2pd | `/` | eepview links to no other console page. The user reaches the tunnels, the address book, the configuration and the logs from the console itself. ### The console view - **R8 One factory.** Only `src-tauri/src/shell/console.rs` builds the console webview. Its constructor takes a `&VerifiedConsole`. The webview label is exactly `console`. It is a child of the `main` window, like the tab webviews, and there is no other console window. It is never a `tab-*` webview, it is never in the tab-webview label map, and it never uses the content-webview factory. - **R9 Origin.** The console view loads only URLs on the detected origin: scheme `http`, host `127.0.0.1`, the detected port. - **R10 Navigation guard.** For each navigation in the console view: - same origin: it stays in the console view; - `http(s)://*.i2p`: the navigation is cancelled, and the URL opens in a new normal tab through the existing guard; - anything else: cancelled. A new-window request on the same origin loads in the console view. One for `*.i2p` opens a new tab. Anything else is dropped. The engine never opens a window by itself. - **R11 Hardening.** The console view has no `proxy_url` (it is loopback), the engine rule list of R19, no IPC capability, WebRTC off in every frame (the same script as the tabs), downloads refused, and JavaScript on (the console needs it). It runs incognito, so nothing persists after exit. - **R12 `console_open()`.** It takes no argument. With no console: `{ok: false, reason: "no-console"}`, and no tab and no webview are made. Otherwise it opens the console tab (R24), loads the console home page (R7) in it, and answers `{ok: true}`. - **R13 `console_status()`.** It answers the current `ConsoleInfo`. It does not probe. ### Web tabs stay closed to loopback - **R14 Tabs unchanged.** The five layers of the `tab-*` webviews do not change. A `tab-*` webview never receives a loopback URL. `proxy_url` stays only in `content.rs`. No capability names the `console` webview. ### UI - **R15 One link.** The router panel, the home page and the Router section of Settings show one link, "I2P Router Console", for the detected router. It is a button that calls `console_open()`. No page holds an `http://127.0.0.1` link. With no console, the link is hidden and one line says "No router console found". The five per-page links of #54 (Console, Tunnels, Address book, Config, Logs) are gone. - **R16 No router configuration in eepview.** No eepview page changes router configuration. The Settings page has no bandwidth, share, relay (transit) or subscription control. Its Router section has the one console link (R15) instead. Pause and resume of the connection stay. The Router updates and Restore controls stay: they are managed-install controls (a disabled Phase 3 preview), not router configuration. The About list "Where eepview connects" names the console probe and the console tab. - **R17 Leak test.** The leak test passes unchanged. ### Router version - **R18 Version, display only.** `ConsoleInfo.version` is the router version read from the probe page, or `null` when it is not found. It costs no extra request. Java I2P: the version in the console stylesheet link, `console.css?`. i2pd: the first `:
` value, where `` is digits and dots (the label before it is translated). The router panel and the home page show it when `RouterStatus.version` is `null`. It changes nothing else. ### Confinement and retries - **R19 Console rule list.** Before its first load, the console view gets an engine rule list: block every URL, then allow only URLs on the console origin (R9) and `about:`, `data:`, `blob:`. The view starts on `about:blank` and loads the page only after the list is attached. If it cannot be attached, nothing loads (fail closed): the console tab closes, its `console` webview is destroyed, and a warning toast says "The router console could not be opened safely" (`Core::console_failed`). On Windows the same rule answers each `WebResourceRequested` with 403. Linux has no engine filter yet (the same limit as L3b for tabs); there the console view relies on R10 and the router's own pages. - **R20 Retry after a miss.** When a `console_detect()` finds no console, eepview retries every 10 s for 2 minutes (12 retries), and stops at the first console found. A new trigger during the retries does not start a second retry loop. The UI also calls `console_detect()` when `router-status` turns `ok` (from any other state, not paused) while a page with the links shows: the home page or Settings as the active tab, or the open router panel. - **R21 Re-check misses.** While a console is known, a re-check runs every 10 s. A re-check that finds a different console (another port or type) replaces it at once. A re-check that finds none counts a miss; the known console stays, with no event, until 3 misses in a row. The third miss clears it: `console-changed` with `found: false`, and the console tab closes (R30). A re-check that finds the same console resets the count. - **R22 Stop.** `shell::console::stop(app)` ends every re-check and retry loop at its next tick (at most one tick, 10 s). After stop, no thread opens a connection to a console port. The shell calls it on `RunEvent::Exit`. A later `detect_now` starts the loops again. ### The console tab - **R23 Tab kind.** The console tab is a tab of kind `console` in the tab strip (`TabInfo.kind`). While it is the active tab, the content area shows the `console` webview, at the place of a web tab. While another tab is active, the `console` webview is hidden. Its `TabInfo` has `kind: "console"`, `zoom: 1`, `jsOn: true` (R11), `bookmarked: false` and `icon: null`. The router state (verifying, down, paused) does not change what the console tab shows: the console is on loopback and does not go through the router proxy. - **R24 One console tab.** There is at most one console tab. `console_open()` with a console tab open selects it and loads the home page in it. With none, it makes one right after the active tab and selects it. It never opens a second `console` webview. One field of the core, `console_tab`, names the console tab; a tab has no console flag. - **R25 Tab state.** The tab title is the document title of the console page. Until the first title arrives, it is `Router console`. The tab URL is the URL the `console` webview shows (main frame), and `loading` follows its page loads. A console page never enters history, never gets a site icon and is never bookmarked. The bookmark star and the JavaScript toggle are disabled in a console tab. Find in page and zoom do nothing there: the find shortcut and `find()` leave the find bar closed and make no engine call. - **R26 Address bar.** In a console tab the address bar shows the console URL and a `Router console` badge in place of the `I2P` badge. Typing in the address bar and pressing Enter navigates like in any tab: the input goes through the normal address rules and the tab guard. When the input is allowed (an I2P address, an internal page or a search), the console tab becomes a normal tab (`web` or `internal`) with a new back/forward list, and the `console` webview is destroyed. When the input is refused (`not-i2p`, `invalid`), for example an edited console address, the answer is the refusal and the console tab stays as it is: no blocked page, no effect on the `console` webview. A console address typed by hand never loads; only `console_open()` loads the console. - **R27 Back, forward, reload, stop.** In a console tab, back, forward, reload, hard reload and stop act on the `console` webview. `canBack` and `canForward` follow the console pages loaded in that tab. - **R28 Close.** Closing the console tab destroys the `console` webview. "Reopen closed tab" never brings a console tab back. eepview does not restore tabs at start, so a restart never shows a console tab. - **R29 No console URL in a web tab.** The core never makes a `WebOp` (load, engine call, destroy) for the console tab. A console page URL never reaches a `tab-*` webview: a load of a non-I2P URL in a web tab shows the blocked page, as before. ADR 0001 and the leak test do not change. - **R30 Console gone.** When the console goes away or moves (R6, R21), the console tab closes and the `console` webview is destroyed. ### Router statistics from the console The router panel and the Network page (`eepview://stats`) show the router statistics. Until now they came only from the router helper (`EEPVIEW_ROUTER_STATUS`), which an external router does not have. Java I2P keeps I2PControl off by default, and eepview never turns it on: that is a router configuration change. So, without the helper, eepview reads the same figures from the console that detection already verified. It only reads. It never changes the router. - **R31 Source order.** `router_stats()` takes the statistics from the first source that answers: 1. the router helper, when `EEPVIEW_ROUTER_STATUS` and its token are set, and it answers `200` with JSON; 2. the console stored by detection (`shell::console::current`), when one is stored and it answers `200` (R32, R33); 3. else no source: every field is `null`. When the helper answers, eepview does not ask the console. `router_stats()` never runs detection: with no stored console it does not probe. No page shows which source gave the figures. - **R32 One read-only request.** The console request is one loopback `GET HTTP/1.0` in origin form on the stored console origin (`127.0.0.1:`), through `LoopbackAddr`, with the headers of R4 (`Host`, `User-Agent: eepview`, `Accept: text/html`, `Connection: close`) and nothing else: no cookie, no body. It never sends a `POST`, never follows a redirect, and never goes through a webview. The path is fixed per router type: | Router | Path | |---|---| | Java I2P | `/xhr1.jsp?requestURI=/summaryframe` | | i2pd | `/` | The path never carries a `lang`, `action` or `consoleNonce` parameter. (A Java I2P console saves `?lang=` in the router configuration.) The request code lives in `src-tauri/src/net/` (ADR 0001 rule 2). The Java path is the sidebar data that the console's own script loads every 15 s, with the section list of the sidebar page (`/summaryframe`). It has the least markup of the pages that carry the figures. - **R33 Bounds.** The whole request, connect and reads together, takes at most 3 s. It reads at most 256 KiB of the page. A peer that sends slowly cannot hold a call longer than 3 s. A closed port, a timeout before any answer, a status other than `200`, or an answer that is not HTTP counts as "the console does not answer": R31 moves on. A body cut by the size cap or by the timeout is still parsed; R35 makes every cut value `null`. - **R34 Cadence.** Rust starts no thread and no timer for the console statistics: each `router_stats()` call makes at most one console request. The UI calls `router_stats()` every 5 s, and only while the figures show: - the router panel: while it is open (unchanged); - the Network page: while `document.visibilityState` is `"visible"`. It stops while the page is hidden, and refreshes at once when it shows again. The Network page calls `console_detect()` once when it loads (a new trigger for R6). The router panel and the Network page load the figures again when that `console_detect()` answers, so a console found at that moment fills the figures at once. After `RunEvent::Exit`, no page calls `router_stats()`, so no console request runs. After `shell::console::stop` (R22), `router_stats()` asks no console either, until the next detection. A call that starts after the stop sends no request; a request already sent ends within 3 s (R33). - **R35 Fail closed, per field.** Each field is parsed alone. A field that does not match its rule exactly is `null`, and the UI shows "—". eepview never shows a guessed or a derived number. - An integer is one or more ASCII digits and fits in `u64`. A decimal is ASCII digits, then optionally `.` and one or more digits. No sign, no group separator, no `,` as the decimal mark, no other digit set. A value that does not match is `null`. - A value must end with the terminator its rule names. A value at the end of the body (cut by R33) is `null`. - A unit conversion is exact on the decimal digits, then rounds to the nearest integer, a half rounds up. For example, `53.91` KBps is 53 910 B/s, and `12.34` KiB/s is 12 636 B/s (12 636.16). - When an anchor (a Java I2P table `id`, or an i2pd label) is in the body more than once, the fields it gives are `null`. - A missing section gives `null` for its fields only. The other fields still parse. - **R36 Java I2P figures.** The parser reads tables by their `id` and their rows by position. It never reads a row label or a `title`, so it works in every console language. A row is a `…` element in the table. Its value is the text of the last `` cell, up to ``. When a table has another number of rows than the rule names, every field of that table is `null`. | Field | Table and row | Value form | Result | |---|---|---|---| | `uptimeMs`, `uptimeResolutionMs` | `sb_general` or `sb_shortgeneral` with 2 rows: row 1. Else `sb_advancedgeneral` with 4 rows: row 1, with 5 rows: row 2. The first of the three ids in this order wins. | `N ` | R37 | | `bandwidthBytesPerSecond.in1s`, `.out1s` | `sb_bandwidth`, any row count from 2 to 4: row 0 | `A / B KBps` or `A / B MBps` | `A` and `B` (decimals) × 1 000 (`K`) or × 1 000 000 (`M`) | | `bandwidthBytesPerSecond.in5m`, `.out5m` | `sb_bandwidth` with 4 rows: row 1. With 2 or 3 rows: `null` (the router is younger than 6 minutes). | as above | as above | | `activePeers` | `sb_peers` with 5 rows, or `sb_peersadvanced` with 6 rows: row 0 | `A / B` | `A` (peers with a connection now) | | `floodfills` | the same table: row 3 | `N` | `N` | | `knownRouters` | the same table: row 4 | `N` | `N` | | `tunnels.exploratory` | `sb_tunnels` with 4 rows: row 0 | `N` | `N` (inbound + outbound) | | `tunnels.client` | `sb_tunnels` with 4 rows: row 1 | `N` | `N` (inbound + outbound) | | `tunnels.participating` | `sb_tunnels` with 4 rows: row 2 | `N` | `N` | | `networkStatus` | the class after `sb_netstatus` in the first `` | `running`, `firewalled`, `testing`, `hidden`, `warn`, `error`, `clockskew`, `vmcomm` | `OK`, `FIREWALLED`, `TESTING`, `HIDDEN`, `WARN`, `ERROR`, `CLOCK_SKEW`, `VMCOMM`; another class gives `null` | `A / B` is `A`, a space, `/`, a space, `B`. The rows Fast and High capacity (rows 1 and 2 of `sb_peers`) have no field and are not read. Always `null` from Java I2P: `version` (R18 shows the console version), `tunnels.in`, `tunnels.out` (the sidebar gives only in + out together), and every `tunnelBuildSuccessPercent` field: the sidebar shows no build success figure, and eepview does not compute one from other statistics. - **R37 Java I2P uptime.** The console rounds the uptime down to one unit. The value is `N ` with `N` an integer. Only the English units parse: | Unit | `uptimeMs` | `uptimeResolutionMs` | |---|---|---| | `ms` | N | 1 | | `sec` | N × 1 000 | 1 000 | | `min` | N × 60 000 | 60 000 | | `hour`, `hours` | N × 3 600 000 | 3 600 000 | | `day`, `days` | N × 86 400 000 | 86 400 000 | Any other unit, such as `years` or a translated unit, gives `null` for both fields. - **R38 i2pd figures.** This rule comes from the i2pd source, `daemon/HTTPServer.cpp` (`ShowStatus`, `ShowUptime`, `ShowNetworkStatus`), of i2pd 2.5x. No i2pd router was available to capture a real page. i2pd translates its labels and has no table ids, so the parser reads the English page only: when the body has no `