From 7cb019974e7174703b4fd75b7c32e5f69e487998 Mon Sep 17 00:00:00 2001
From: shrwnsan <38465+shrwnsan@users.noreply.github.com>
Date: Tue, 27 Oct 2026 17:23:11 +0800
Subject: [PATCH 1/2] feat(site): document tunnel stop and start
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The man page's Browser tunnels section gains the park/resume verbs:
what stop keeps (registry + hosts mapping, parked across logins and
reboots), what start restores (same local port, same URL, no sudo),
the --all forms, and the restart-refuses-parked rule — with copyable
stop/start synopsis lines.
The proof section catches up to v0.9.0: the cumulative-release chart
gains the 2026-10-27 point (32) with the extended axis and hover
mapping to match, the release feed leads with 0.9.0, and the stale
counts are refreshed (529 tests, 7,733 sh + 217 Go). llms.txt picks
up the lifecycle clause and the current line count.
Co-authored-by: ZCode
---
site/assets/js/chart.js | 9 +++++----
site/docs/index.html | 14 +++++++++++++-
site/index.html | 33 +++++++++++++++++----------------
site/llms.txt | 4 ++--
4 files changed, 37 insertions(+), 23 deletions(-)
diff --git a/site/assets/js/chart.js b/site/assets/js/chart.js
index a98d915..af960ad 100644
--- a/site/assets/js/chart.js
+++ b/site/assets/js/chart.js
@@ -9,21 +9,22 @@
{ x: 520.2, y: 143.8, d: "2026-09-01", v: 11 }, { x: 525.9, y: 67.1, d: "2026-09-02", v: 23 },
{ x: 531.6, y: 47.9, d: "2026-09-03", v: 26 }, { x: 555.5, y: 41.5, d: "2026-09-11", v: 27 },
{ x: 588.3, y: 35.1, d: "2026-09-22", v: 28 }, { x: 591.4, y: 28.7, d: "2026-09-23", v: 29 },
- { x: 594.4, y: 22.4, d: "2026-09-24", v: 30 }, { x: 612.3, y: 16, d: "2026-09-30", v: 31 }
+ { x: 594.4, y: 22.4, d: "2026-09-24", v: 30 }, { x: 612.3, y: 16, d: "2026-09-30", v: 31 },
+ { x: 698, y: 9.6, d: "2026-10-27", v: 32 }
];
var svg = document.querySelector(".chart-card svg");
var tip = document.getElementById("chart-tip"), xh = document.getElementById("xhair"), hd = document.getElementById("hoverdot");
if (svg) {
svg.addEventListener("mousemove", function (ev) {
var r = svg.getBoundingClientRect();
- var sx = (ev.clientX - r.left) * (640 / r.width);
+ var sx = (ev.clientX - r.left) * (728 / r.width);
var best = PTS[0], bd = Math.abs(sx - PTS[0].x);
for (var i = 1; i < PTS.length; i++) { var d = Math.abs(sx - PTS[i].x); if (d < bd) { bd = d; best = PTS[i]; } }
xh.setAttribute("x1", best.x); xh.setAttribute("x2", best.x); xh.setAttribute("opacity", "1");
hd.setAttribute("cx", best.x); hd.setAttribute("cy", best.y); hd.setAttribute("opacity", "1");
tip.innerHTML = best.d + " · " + best.v + " release" + (best.v > 1 ? "s" : "");
- tip.style.left = (best.x / 640 * r.width) + "px";
- tip.style.top = (best.y / 250 * r.height) + "px";
+ tip.style.left = (best.x / 728 * r.width) + "px";
+ tip.style.top = ((best.y + 10) / 260 * r.height) + "px";
tip.style.opacity = "1";
});
svg.addEventListener("mouseleave", function () {
diff --git a/site/docs/index.html b/site/docs/index.html
index ceb63b9..fe0a407 100644
--- a/site/docs/index.html
+++ b/site/docs/index.html
@@ -304,6 +304,18 @@
Browser tunnels
$ tailroute tunnel add mypeer
→ https://mypeer.yourtailnet.ts.net:8443
+
Need a tunnel gone for a while? tailroute tunnel stop mypeer parks
+ it without tearing anything down: the URL refuses connections, but the
+ registry entry and hosts mapping survive, and the parked state outlasts
+ logins and reboots. tailroute tunnel start mypeer resumes it
+ with the same local port and the same URL—no sudo, no re-registration.
+ Both take --all; tunnel status shows a parked
+ tunnel as stopped (by request), and tunnel restart
+ refuses to implicitly start one.
+
+
$ tailroute tunnel stop mypeer
+
$ tailroute tunnel start mypeer
+
What it cannot fix
VPN Network Extensions may still block traffic to Tailscale's CGNAT range
@@ -321,7 +333,7 @@
Security
(/var/db/tailroute/installed.checksums) verified before any library loads.
No telemetry, no external network calls, no account, no traffic logging.
Interface names validated against utun[0-9]+; absolute paths only.
An install-time SHA-256 integrity manifest is verified before any library loads—tampered files
refuse to start, and say what changed. Strict validation, absolute paths, fail-safe on ambiguity,
- 417 tests and counting.
+ 529 tests and counting.
@@ -292,13 +292,13 @@
Shipped, not promised.
Tagged releases—cumulative
- 29 of 31 releases shipped since Aug 24
+ 30 of 32 releases shipped since Aug 24
-
What happens when I quit the VPN or uninstall? +
@@ -534,7 +535,7 @@
Asked, answered
"name": "It runs as root—why should I trust it?",
"acceptedAnswer": {
"@type": "Answer",
- "text": "Because it shows its work: install-time SHA-256 integrity manifest verified before any library loads, strict input validation, absolute paths only, fail-safe on ambiguous state. The whole thing is ~7,400 lines of auditable bash and Go (7,194 sh + 217 Go, wc -l, Oct 2026). Prefer the manual? Read the full man page, shipped with this site."
+ "text": "Because it shows its work: install-time SHA-256 integrity manifest verified before any library loads, strict input validation, absolute paths only, fail-safe on ambiguous state. The whole thing is ~7,900 lines of auditable bash and Go (7,733 sh + 217 Go, wc -l, Oct 2026). Prefer the manual? Read the full man page, shipped with this site."
}
},
{
diff --git a/site/llms.txt b/site/llms.txt
index e43c7dc..876d3f5 100644
--- a/site/llms.txt
+++ b/site/llms.txt
@@ -1,6 +1,6 @@
# tailroute-cli
-> tailroute is a fail-safe macOS daemon that keeps Tailscale and commercial VPNs (NordVPN, Mullvad, corporate VPN apps) working simultaneously. When a VPN connects it disables Tailscale's MagicDNS so internet keeps flowing through the VPN, and serves Tailscale peers through a local SOCKS5 proxy (127.0.0.1:1055) and launchd-managed browser tunnels with valid .ts.net TLS. Apache-2.0, no telemetry, no account.
+> tailroute is a fail-safe macOS daemon that keeps Tailscale and commercial VPNs (NordVPN, Mullvad, corporate VPN apps) working simultaneously. When a VPN connects it disables Tailscale's MagicDNS so internet keeps flowing through the VPN, and serves Tailscale peers through a local SOCKS5 proxy (127.0.0.1:1055) and launchd-managed browser tunnels with valid .ts.net TLS—`tunnel stop` parks a tunnel without tearing it down (it stays parked across reboots), `tunnel start` resumes it with the same URL. Apache-2.0, no telemetry, no account.
The tool answers one question: "How do I keep Tailscale and my VPN working at the same time on a Mac?" Install is two commands, it needs no configuration, and it reverses itself when the VPN disconnects.
@@ -14,7 +14,7 @@ The tool answers one question: "How do I keep Tailscale and my VPN working at th
- [Landing page](https://tailroute.app/): product overview, honest limits, FAQ
- [Manual page](https://tailroute.app/docs/): the full tailroute manual—states, tunnels, security, diagnostics
-- [GitHub repository](https://github.com/shrwnsan/tailroute-cli): source, ~7,400 auditable lines of bash and Go
+- [GitHub repository](https://github.com/shrwnsan/tailroute-cli): source, ~7,900 auditable lines of bash and Go
- [Changelog](https://github.com/shrwnsan/tailroute-cli/blob/main/CHANGELOG.md): per-release history with test counts
- [Issues](https://github.com/shrwnsan/tailroute-cli/issues): bug reports and VPN compatibility reports
From 9d7fb46c210e1e6dcc0e7689eba42799499f9eae Mon Sep 17 00:00:00 2001
From: shrwnsan <38465+shrwnsan@users.noreply.github.com>
Date: Tue, 27 Oct 2026 18:01:26 +0800
Subject: [PATCH 2/2] =?UTF-8?q?fix(site):=20every=20version=20surface=20no?=
=?UTF-8?q?w=20reads=200.9.0=20=E2=80=94=20and=20stays=20current?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A full-page sweep found the versioned surfaces the content refresh
missed: the JSON-LD softwareVersion on both pages, the manual page's
version badge, the landing's committed stat-card fallbacks, and the
'tests in five weeks' stat whose window had lapsed (348 at v0.8.11 →
529 at v0.9.0, eight weeks).
Rather than re-rot at the next release, the manual page joins the
deploy-time refresh: update-metrics.py now also injects the latest
version into the docs page's softwareVersion and its badge (new
data-metric="cli-version" span), so a release tag is all the site
needs. The span helper tolerates attributes beside data-metric, and
the landing contract (stat card, updated span) is unchanged —
check-site.py still passes 9/9.
Co-authored-by: ZCode
---
scripts/update-metrics.py | 50 ++++++++++++++++++++++++---------------
site/docs/index.html | 4 ++--
site/index.html | 6 ++---
3 files changed, 36 insertions(+), 24 deletions(-)
diff --git a/scripts/update-metrics.py b/scripts/update-metrics.py
index b7b578e..4da50e2 100755
--- a/scripts/update-metrics.py
+++ b/scripts/update-metrics.py
@@ -1,11 +1,12 @@
#!/usr/bin/env python3
-"""Inject live release metrics into site/index.html at deploy time.
+"""Inject live release metrics into the site at deploy time.
Counts CLI tagged releases (excludes app-v* releases, which are the menu bar
app's separate tag space) and the latest CLI version from the GitHub API, then
-updates the stat card and JSON-LD in place. A failure leaves the page as-is:
-slightly stale numbers are this page's failure mode, a broken deploy is not.
-Idempotent. Stdlib only.
+updates the pages in place: the landing's stat card and JSON-LD, and the
+manual page's JSON-LD plus its version badge. A failure leaves the pages
+as-is: slightly stale numbers are this site's failure mode, a broken deploy
+is not. Idempotent. Stdlib only.
"""
import json
import os
@@ -15,6 +16,7 @@
REPO = "shrwnsan/tailroute-cli"
PAGE = os.path.join(os.path.dirname(__file__), "..", "site", "index.html")
+DOCS_PAGE = os.path.join(os.path.dirname(__file__), "..", "site", "docs", "index.html")
def fetch_releases():
@@ -33,25 +35,35 @@ def main():
count = len(releases)
latest = releases[0]["tag_name"].lstrip("v") if releases else None
if not (0 < count < 500) or not latest:
- print(f"update-metrics: implausible metrics (count={count}, latest={latest}) — leaving page as-is")
+ print(f"update-metrics: implausible metrics (count={count}, latest={latest}) — leaving pages as-is")
return 0
- with open(PAGE) as f:
- html = f.read()
- original = html
-
- html = _replace_releases(html, count)
- html = _replace_version(html, latest)
-
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
- html = _replace_span(html, "updated", f" \u00b7 site updated {today}")
- if html == original:
- print(f"update-metrics: page already current (releases={count}, version={latest})")
+ def landing(html):
+ html = _replace_releases(html, count)
+ html = _replace_version(html, latest)
+ return _replace_span(html, "updated", f" \u00b7 site updated {today}")
+
+ def manual(html):
+ html = _replace_version(html, latest)
+ return _replace_span(html, "cli-version", f"v{latest}")
+
+ touched = 0
+ for path, inject in ((PAGE, landing), (DOCS_PAGE, manual)):
+ with open(path) as f:
+ html = f.read()
+ updated = inject(html)
+ if updated == html:
+ continue
+ with open(path, "w") as f:
+ f.write(updated)
+ touched += 1
+
+ if not touched:
+ print(f"update-metrics: pages already current (releases={count}, version={latest})")
return 0
- with open(PAGE, "w") as f:
- f.write(html)
- print(f"update-metrics: injected releases={count}, version={latest}, updated={today}")
+ print(f"update-metrics: injected releases={count}, version={latest}, updated={today} ({touched} page(s))")
return 0
@@ -75,7 +87,7 @@ def _replace_version(html, version):
def _replace_span(html, name, text):
import re
- pattern = re.compile(rf'()(.*?)()', re.S)
+ pattern = re.compile(rf'(]*\sdata-metric="{name}"[^>]*>)(.*?)()', re.S)
if not pattern.search(html):
print(f"update-metrics: WARNING span {name!r} not found")
return html
diff --git a/site/docs/index.html b/site/docs/index.html
index fe0a407..c6782f1 100644
--- a/site/docs/index.html
+++ b/site/docs/index.html
@@ -29,7 +29,7 @@
"operatingSystem": "macOS 15+",
"applicationCategory": "UtilitiesApplication",
"description": "Fail-safe macOS daemon that toggles Tailscale MagicDNS when a VPN connects—internet through the VPN, peers through the mesh. Includes a SOCKS5 mesh proxy and launchd-managed browser tunnels.",
- "softwareVersion": "0.8.17",
+ "softwareVersion": "0.9.0",
"license": "https://github.com/shrwnsan/tailroute-cli/blob/main/LICENSE",
"url": "https://tailroute.app/docs/",
"codeRepository": "https://github.com/shrwnsan/tailroute-cli",
@@ -203,7 +203,7 @@