Service Log
@@ -559,11 +553,11 @@
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
}
- // Primary: extract hostname from Axis syslog header (always the real device hostname)
+ // The syslog header always carries the real device hostname.
var node = null;
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
if (hostLine) node = hostLine[1];
- // Fallback: popBrowserAuthNow/StartLoginInteractiveAs (may contain stale acap-tailscale_vpn)
+ // Fallback; may contain a stale acap-tailscale_vpn name.
if (!node) {
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
@@ -605,7 +599,7 @@
}
}
- // Parse proxy ports from log — use last match so old entries don't win
+ // Use the last match so older log entries don't win.
var httpPort = null;
var httpProxyMatches = txt.match(/HTTP\/HTTPS proxy: http:\/\/127\.0\.0\.1:(\d+)/g);
if (httpProxyMatches) { var m = httpProxyMatches[httpProxyMatches.length - 1].match(/:(\d+)$/); if (m) httpPort = m[1]; }
@@ -613,7 +607,6 @@
var socksProxyMatches = txt.match(/SOCKS5 proxy:\s+127\.0\.0\.1:(\d+)/g);
if (socksProxyMatches) { var ms = socksProxyMatches[socksProxyMatches.length - 1].match(/:(\d+)$/); if (ms) socksPort = ms[1]; }
- // Cache when found, restore from cache when missing
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
cacheSet('http-port', httpPort); cacheSet('socks-port', socksPort);
tsIP = tsIP || cacheGet('ip');
@@ -627,9 +620,8 @@
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState);
- // Fallbacks only when syslog has rotated and no state transitions are visible.
- // If we CAN see state lines (e.g. "-> NeedsLogin"), trust them over our own
- // "Tailscale VPN is running" message which stays in syslog indefinitely.
+ // Only when syslog rotated away all state lines: our own "Tailscale VPN
+ // is running" line lingers in syslog and must not override them.
if (!isRunning && stateLines.length === 0) {
isRunning = /Tailscale VPN is running/.test(txt) ||
/health\(warnable=[^)]+\): ok/.test(txt) ||
@@ -638,12 +630,11 @@
/localapi:/.test(txt);
}
- // If an auth URL appears AFTER the last Running state, re-auth is needed
- // (handles stale Running entries in syslog after reinstall or token expiry)
+ // An auth URL after the last Running state means re-auth is needed
+ // (stale Running entries after reinstall or token expiry).
if (isRunning && latestUrl) {
- // Use the LATEST of '-> Running' (tailscaled state) or 'Tailscale VPN is running'
- // (our shell log). The shell log is written AFTER auth completes, so it correctly
- // post-dates the auth URL when connection succeeds.
+ // Our shell log line is written after auth completes, so it post-dates
+ // the auth URL on success.
var lastRunIdx = txt.lastIndexOf('-> Running');
var lastRunningMsgIdx = txt.lastIndexOf('Tailscale VPN is running');
if (lastRunningMsgIdx > lastRunIdx) lastRunIdx = lastRunningMsgIdx;
@@ -707,7 +698,6 @@
auth.style.display = 'none';
}
- // Proxy card is always visible — update ports whenever known
if (r.httpPort) document.getElementById('ts-http-proxy').textContent = 'http://127.0.0.1:' + r.httpPort;
if (r.socksPort) document.getElementById('ts-socks-proxy').textContent = '127.0.0.1:' + r.socksPort;
@@ -761,7 +751,6 @@
if (st.Version) result.version = String(st.Version).split('-')[0];
if (bs === 'Running' && self.Online === true) {
- // Genuinely connected and reachable on the tailnet
result.state = 'connected';
result.url = null;
result.ip = ip4 || result.ip;
@@ -773,11 +762,8 @@
result.state = 'connecting';
result.url = st.AuthURL || result.url;
} else if (bs === 'Running') {
- // Backend running but node not online: either a transient network
- // drop (no action needed) or the node was removed/expired and needs
- // re-auth. Not connected. Keep any login URL the log parser found
- // (status.json's AuthURL lags during the `tailscale up` re-auth
- // window) so the login button still appears when re-auth is needed.
+ // Not online: transient drop or node removed/expired. Keep the log
+ // parser's URL; status.json's AuthURL lags during re-auth.
result.state = 'connecting';
result.url = st.AuthURL || result.url;
} else if (bs === 'Stopped') {
@@ -801,7 +787,7 @@
var st = arr[1];
var result = parse(txt || '');
if (txt) renderLogs(txt);
- // Verify the app is actually running - status.json can be stale if stopped
+ // status.json goes stale when the app is stopped.
checkAppRunning().then(function(running) {
if (!running) {
result.state = 'disconnected';
@@ -869,7 +855,7 @@
return 0;
}
- // Settings — load current param values and save on submit
+ // Settings
var PARAM_URL = '/axis-cgi/param.cgi';
var serverInput = document.getElementById('input-server');
var authInput = document.getElementById('input-authkey');
@@ -881,17 +867,11 @@
var saveBtn = document.getElementById('save-btn');
var saveStatus = document.getElementById('save-status');
- // param.cgi is used when available; on devices that lack it (e.g. some
- // recorder/NVR-class devices) we fall back to the app's own endpoint,
- // exposed through the manifest reverseProxy mapping at API_URL.
+ // Fallback for devices without param.cgi (e.g. recorders).
var API_URL = '/local/' + APP + '/api/settings';
- // Whether this build exposes local HTTP/SOCKS5 proxies (absent on ROOT
- // builds, which use kernel networking directly). Detected from whichever
- // settings response actually comes back — set once and used to hide the
- // proxy card/fields and to keep them out of the save request, since
- // param.cgi errors the whole call's status line if asked to set a
- // parameter name the manifest never registered.
+ // False on ROOT builds (no local proxy). Also keeps the ports out of saves:
+ // param.cgi fails the whole update on a name the manifest never registered.
var hasProxyPorts = false;
function toggleProxyUi(visible) {
@@ -915,8 +895,7 @@
var dm = txt.match(/root\.\S+\.AcceptDNS=(.*)/);
var rm = txt.match(/root\.\S+\.AcceptRoutes=(.*)/);
var avm = txt.match(/root\.\S+\.AdvertiseRoutes=(.*)/);
- // If none of the expected keys are present the endpoint isn't param.cgi
- // (e.g. a generic 404 page); signal the caller to use the fallback.
+ // No expected keys means this isn't param.cgi output (e.g. a 404 page).
if (!sm && !hm && !km) return false;
if (sm) serverInput.value = sm[1].trim();
if (am) authInput.value = am[1].trim();
@@ -979,8 +958,7 @@
.then(function(r) { return r.ok ? r.text() : Promise.reject(); })
.then(function(txt) {
if (/OK/.test(txt)) {
- // The app applies the change and restarts its tunnel itself,
- // so no separate control.cgi restart is needed here.
+ // The app restarts its tunnel itself; no control.cgi call needed.
setStatus('Saved. Restarting...', 'ok');
} else {
setStatus('Error saving settings', 'err');
@@ -1011,15 +989,13 @@
.then(function(txt) {
if (/^OK/.test(txt.trim())) {
setStatus('Saved. Restarting...', 'ok');
- // Restart the app so new settings take effect
return fetch('/axis-cgi/applications/control.cgi?action=restart&package=' + APP,
{ method: 'POST', credentials: 'same-origin' });
}
- // param.cgi reachable but rejected the update — surface the error.
setStatus('Error: ' + txt.trim(), 'err');
})
.catch(function() {
- // param.cgi unavailable (e.g. recorder-class device) — use the fallback.
+ // param.cgi unavailable (e.g. recorder-class device).
return saveViaFallback(httpPort, socksPort);
})
.then(function() { saveBtn.disabled = false; })
diff --git a/common/app/param_bridge.c b/common/app/param_bridge.c
index 1d390a2..887efb8 100644
--- a/common/app/param_bridge.c
+++ b/common/app/param_bridge.c
@@ -1,22 +1,10 @@
// Copyright (C) 2024 Mo3he
// SPDX-License-Identifier: GPL-3.0-or-later
-/**
- * ACAP parameter bridge for Tailscale VPN.
- *
- * Responsibilities:
- * 1. Read Tailscale parameters from the ACAP parameter store (axparameter).
- * 2. Write them to CONFIG_FILE so the shell script can source them.
- * 3. Launch the shell script (Tailscale_VPN_run) as a child process.
- * 4. On any parameter change: rewrite CONFIG_FILE and do a full stop+restart
- * of the child so the new config is picked up.
- * Rapid changes within 300 ms are coalesced into a single restart.
- * 5. Watchdog: if the child exits unexpectedly, restart it.
- *
- * Shared across the userspace-networking variants (unprivileged 'sdk' ACAP
- * user) and the ROOT / kernel-networking variant. Build with -DHAS_PROXY_PORTS
- * for the userspace variants, which exposes the HTTP/SOCKS5 proxy port
- * parameters; the ROOT variant omits them since it has no local proxy.
+/*
+ * ACAP parameter bridge for Tailscale VPN: mirrors axparameter values into
+ * CONFIG_FILE, runs Tailscale_VPN_run as a child and restarts it on changes.
+ * Build with -DHAS_PROXY_PORTS for the userspace variants; ROOT has no proxy.
*/
#include
@@ -70,10 +58,8 @@ static const char *cache_get(char **field, const char *fallback) {
return (*field && **field) ? *field : fallback;
}
-/* Ensure a parameter exists in the device parameter database. On in-place ACAP
- * upgrades a newly introduced manifest parameter is not always auto-registered,
- * which makes param.cgi return a 404 when the web UI tries to set it. Creating
- * it here is idempotent: if it already exists, ax_parameter_add fails harmlessly. */
+/* In-place upgrades don't always register new manifest params (param.cgi then
+ * 404s); ax_parameter_add fails harmlessly if the param already exists. */
static void ensure_param(AXParameter *handle, const char *name, const char *def) {
GError *err = NULL;
if (!ax_parameter_add(handle, name, def, "string", &err)) {
@@ -127,7 +113,7 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
int exit_code = WEXITSTATUS(status);
syslog(LOG_WARNING, "child exited (status %d), restarting", exit_code);
child_pid = -1;
- /* If child exited 0, auth succeeded — clear AuthKey via axparameter */
+ /* Legacy AuthKey clear: the run script only exits 0 from its TERM/INT trap. */
if (exit_code == 0 && g_ax_handle && cfg_auth_key && *cfg_auth_key) {
GError *err = NULL;
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
@@ -147,10 +133,8 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
/* ── auth-key sentinel ───────────────────────────────────────────────────── */
-/* The run script drops SENTINEL_FILE after a successful `tailscale up` that
- * used a one-time auth key. Clear the stored AuthKey so it is not reused and
- * disappears from the settings UI. This replaces the old exit-code-0 path,
- * which never fired because tailscaled keeps the child alive indefinitely. */
+/* The run script drops SENTINEL_FILE after `tailscale up` used the auth key.
+ * The exit-code-0 path in watchdog_cb rarely fires since the child stays up. */
static gboolean authkey_sentinel_cb(gpointer G_GNUC_UNUSED data) {
if (access(SENTINEL_FILE, F_OK) != 0)
return G_SOURCE_CONTINUE;
@@ -277,11 +261,8 @@ static void parameter_changed(const gchar *name, const gchar *value,
}
/* ── embedded settings HTTP server (reverse-proxy fallback) ──────────────────
- * Some AXIS device classes (e.g. recorders/NVRs) do not expose the legacy
- * /axis-cgi/param.cgi VAPIX endpoint, so the web UI cannot load or save
- * settings through it. This tiny HTTP server, reached through the manifest
- * reverseProxy mapping at /local/Tailscale_VPN/api/settings, lets the web UI
- * fall back to reading and writing the parameters directly. */
+ * For devices without /axis-cgi/param.cgi (e.g. recorders); reached via the
+ * manifest reverseProxy at /local/Tailscale_VPN/api/settings. */
#define HTTP_PORT 2201
@@ -344,9 +325,8 @@ static gchar *http_build_settings_json(AXParameter *handle) {
g_free(val);
}
g_string_append_c(out, '}');
- /* g_string_free(out, FALSE) is inlined by glib >= 2.76 headers into a call
- * to g_string_free_and_steal(), which doesn't exist in older glib runtimes
- * (e.g. AXIS OS 11.x). Copy out and fully free instead to stay portable. */
+ /* Not g_string_free(out, FALSE): glib >= 2.76 headers turn it into
+ * g_string_free_and_steal(), missing from older runtimes (AXIS OS 11.x). */
gchar *json_result = g_strdup(out->str);
g_string_free(out, TRUE);
return json_result;
@@ -544,11 +524,9 @@ int main(void) {
openlog(APP_NAME, LOG_PID, LOG_USER);
syslog(LOG_INFO, "starting");
- /* Ensure localdata dir exists */
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
- /* Drop any stale auth-key sentinel from a previous run so we don't clear a
- * freshly configured key before it has been used. */
+ /* A stale sentinel would clear a freshly configured key before use. */
unlink(SENTINEL_FILE);
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
diff --git a/docs/index.html b/docs/index.html
index b769136..d2e323d 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -346,7 +346,6 @@