mirror of
https://github.com/Mo3he/Axis_Cam_Tailscale.git
synced 2026-09-18 22:04:16 +00:00
fix: status.json connection detection, auth-key auto-clear, IP display
Replace process-liveness/log-scraping heuristics with Tailscale's authoritative backend state published as status.json. - UI now uses BackendState + Self.Online so "no Internet" no longer shows Connected; surfaces the real Tailscale IP, node and tailnet. - Auth key is auto-cleared from the UI after a successful keyed login via a sentinel file picked up by param_bridge (non-acap3 variants). - Run scripts background `tailscale up` and publish status every 5s so re-auth (NeedsLogin + AuthURL) surfaces without starving the loop. - Ported across aarch64, aarch64_ROOT, arm, arm_ROOT, and arm_acap3 (acap3 uses the status.json detection; it has no auth-key param). - Bump bundled Tailscale binaries to 1.98.4 for all variants. - Fix CONTRIBUTING.md issue/discussion links to this repo. - Add packaging/wrapper copyright to LICENSE.
This commit is contained in:
+5
-5
@@ -126,9 +126,9 @@ Before opening a Pull Request (PR), please consider the following guidelines:
|
||||
And finally when you are satisfied with your changes, open a new PR.
|
||||
|
||||
<!-- markdownlint-disable MD034 -->
|
||||
[issues]: https://github.com/AxisCommunications/tailscale-acap/issues
|
||||
[issues_new]: https://github.com/AxisCommunications/tailscale-acap/issues/new
|
||||
[issues_bugs]: https://github.com/AxisCommunications/tailscale-acap/issues?q=label%3Abug
|
||||
[discussions]: https://github.com/AxisCommunications/tailscale-acap/discussions
|
||||
[discussions_new]: https://github.com/AxisCommunications/tailscale-acap/discussions/new
|
||||
[issues]: https://github.com/Mo3he/Axis_Cam_Tailscale/issues
|
||||
[issues_new]: https://github.com/Mo3he/Axis_Cam_Tailscale/issues/new
|
||||
[issues_bugs]: https://github.com/Mo3he/Axis_Cam_Tailscale/issues?q=label%3Abug
|
||||
[discussions]: https://github.com/Mo3he/Axis_Cam_Tailscale/discussions
|
||||
[discussions_new]: https://github.com/Mo3he/Axis_Cam_Tailscale/discussions/new
|
||||
<!-- markdownlint-enable MD034 -->
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||
Copyright (c) 2022 Weston Blieden (ACAP packaging and wrapper code)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
||||
@@ -76,11 +76,66 @@ if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||
fi
|
||||
|
||||
eval $TAILSCALE_CMD
|
||||
UP_EXIT=$?
|
||||
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
||||
# (re-)authentication, `up` blocks until the user logs in; backgrounding it
|
||||
# ensures the status publisher below keeps running so the UI can surface the
|
||||
# login URL (tailscaled reports BackendState=NeedsLogin + AuthURL while waiting).
|
||||
# NOTE: `up` runs synchronously *inside* this backgrounded block so its real exit
|
||||
# code is captured directly. We must NOT background `up` separately and `wait`
|
||||
# for it from here, because in POSIX sh `wait` only works on children of the
|
||||
# current shell — a subshell waiting on the parent's child returns 127.
|
||||
{
|
||||
eval "$TAILSCALE_CMD"
|
||||
up_exit=$?
|
||||
if [ "$up_exit" -eq 0 ]; then
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
# Auth succeeded with a one-time auth key — signal param_bridge to clear it
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
: > "$STATE_DIR/authkey_clear"
|
||||
fi
|
||||
else
|
||||
logger -t "Tailscale_VPN" "ERROR: tailscale up failed (exit $up_exit)"
|
||||
fi
|
||||
} &
|
||||
TAILSCALE_UP_PID=$!
|
||||
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
logger -t "Tailscale_VPN" "HTTP/HTTPS proxy: http://127.0.0.1:$CONF_HTTP"
|
||||
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:$CONF_SOCKS"
|
||||
|
||||
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||
# TailscaleIPs / AuthURL) instead of scraping syslog. Served statically at
|
||||
# /local/Tailscale_VPN/status.json.
|
||||
STATUS_FILE="$APP_DIR/html/status.json"
|
||||
|
||||
publish_status() {
|
||||
if "$TAILSCALE_PATH" --socket="$SOCKET_PATH" status --json > "$STATUS_FILE.tmp" 2>/dev/null; then
|
||||
mv "$STATUS_FILE.tmp" "$STATUS_FILE" 2>/dev/null
|
||||
chmod 644 "$STATUS_FILE" 2>/dev/null
|
||||
else
|
||||
rm -f "$STATUS_FILE.tmp" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
status_loop() {
|
||||
while true; do
|
||||
publish_status
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
status_loop &
|
||||
STATUS_LOOP_PID=$!
|
||||
|
||||
# Clean up the status writer, up watcher, daemon and published status on
|
||||
# stop/restart so param_bridge (which signals this script) leaves no orphans or
|
||||
# stale state.
|
||||
cleanup() {
|
||||
[ -n "$STATUS_LOOP_PID" ] && kill "$STATUS_LOOP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALE_UP_PID" ] && kill "$TAILSCALE_UP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALED_PID" ] && kill "$TAILSCALED_PID" 2>/dev/null
|
||||
rm -f "$STATUS_FILE" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap cleanup TERM INT
|
||||
|
||||
wait $TAILSCALED_PID
|
||||
|
||||
+75
-21
@@ -490,6 +490,7 @@
|
||||
(function() {
|
||||
var APP = 'Tailscale_VPN';
|
||||
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
||||
var STATUS_URL = 'status.json';
|
||||
var logBox = document.getElementById('log-box');
|
||||
var autoScroll = true;
|
||||
|
||||
@@ -737,29 +738,82 @@
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
// Ground truth published by the run script from `tailscale status --json`.
|
||||
function fetchStatus() {
|
||||
return fetch(STATUS_URL + '?t=' + Date.now(), { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.catch(function() { return null; });
|
||||
}
|
||||
|
||||
// Apply Tailscale's authoritative backend state onto the result object.
|
||||
function applyStatus(result, st) {
|
||||
var self = st.Self || {};
|
||||
var ips = self.TailscaleIPs || st.TailscaleIPs || [];
|
||||
var ip4 = null;
|
||||
for (var i = 0; i < ips.length; i++) { if (/^100\./.test(ips[i])) { ip4 = ips[i]; break; } }
|
||||
var bs = st.BackendState;
|
||||
|
||||
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;
|
||||
result.node = self.HostName || result.node;
|
||||
result.tailnet = (st.CurrentTailnet && st.CurrentTailnet.Name) || result.tailnet;
|
||||
cacheSet('ip', result.ip); cacheSet('node', result.node);
|
||||
cacheSet('tailnet', result.tailnet); cacheSet('version', result.version);
|
||||
} else if (bs === 'NeedsLogin' || bs === 'NeedsMachineAuth') {
|
||||
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.
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Stopped') {
|
||||
result.state = 'disconnected';
|
||||
result.url = null;
|
||||
} else {
|
||||
// NoState / Starting / unknown
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
// Always verify with the app status API - syslog can have stale entries
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
} else if (!result.url && result.state !== 'connected') {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
Promise.all([
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.catch(function() { return ''; }),
|
||||
fetchStatus()
|
||||
]).then(function(arr) {
|
||||
var txt = arr[0];
|
||||
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
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
} else if (st && st.BackendState) {
|
||||
// Authoritative: Tailscale's own backend state
|
||||
applyStatus(result, st);
|
||||
} else if (!result.url && result.state !== 'connected') {
|
||||
// Fallback to log heuristic when status.json is unavailable
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -31,9 +31,10 @@
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define SENTINEL_FILE "/usr/local/packages/Tailscale_VPN/localdata/authkey_clear"
|
||||
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
static pid_t child_pid = -1;
|
||||
@@ -120,6 +121,31 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* ── 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. */
|
||||
static gboolean authkey_sentinel_cb(gpointer G_GNUC_UNUSED data) {
|
||||
if (access(SENTINEL_FILE, F_OK) != 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
||||
if (g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth (sentinel)");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
unlink(SENTINEL_FILE);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* ── config file ─────────────────────────────────────────────────────────── */
|
||||
|
||||
static void load_config_cache(AXParameter *handle) {
|
||||
@@ -219,6 +245,10 @@ int main(void) {
|
||||
/* 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. */
|
||||
unlink(SENTINEL_FILE);
|
||||
|
||||
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||
if (!handle) {
|
||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||
@@ -249,6 +279,7 @@ int main(void) {
|
||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
||||
g_timeout_add_seconds(5, authkey_sentinel_cb, NULL);
|
||||
|
||||
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||
g_main_loop_run(loop);
|
||||
|
||||
@@ -50,9 +50,63 @@ if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||
fi
|
||||
|
||||
eval $TAILSCALE_CMD
|
||||
UP_EXIT=$?
|
||||
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
||||
# (re-)authentication, `up` blocks until the user logs in; backgrounding it
|
||||
# ensures the status publisher below keeps running so the UI can surface the
|
||||
# login URL (tailscaled reports BackendState=NeedsLogin + AuthURL while waiting).
|
||||
# NOTE: `up` runs synchronously *inside* this backgrounded block so its real exit
|
||||
# code is captured directly. We must NOT background `up` separately and `wait`
|
||||
# for it from here, because in POSIX sh `wait` only works on children of the
|
||||
# current shell — a subshell waiting on the parent's child returns 127.
|
||||
{
|
||||
eval "$TAILSCALE_CMD"
|
||||
up_exit=$?
|
||||
if [ "$up_exit" -eq 0 ]; then
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running (root mode)"
|
||||
# Auth succeeded with a one-time auth key — signal param_bridge to clear it
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
: > "$STATE_DIR/authkey_clear"
|
||||
fi
|
||||
else
|
||||
logger -t "Tailscale_VPN" "ERROR: tailscale up failed (exit $up_exit)"
|
||||
fi
|
||||
} &
|
||||
TAILSCALE_UP_PID=$!
|
||||
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running (root mode)"
|
||||
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||
# TailscaleIPs / AuthURL) instead of scraping syslog. Served statically at
|
||||
# /local/Tailscale_VPN/status.json.
|
||||
STATUS_FILE="$APP_DIR/html/status.json"
|
||||
|
||||
publish_status() {
|
||||
if "$TAILSCALE_PATH" --socket="$SOCKET_PATH" status --json > "$STATUS_FILE.tmp" 2>/dev/null; then
|
||||
mv "$STATUS_FILE.tmp" "$STATUS_FILE" 2>/dev/null
|
||||
chmod 644 "$STATUS_FILE" 2>/dev/null
|
||||
else
|
||||
rm -f "$STATUS_FILE.tmp" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
status_loop() {
|
||||
while true; do
|
||||
publish_status
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
status_loop &
|
||||
STATUS_LOOP_PID=$!
|
||||
|
||||
# Clean up the status writer, up watcher, daemon and published status on
|
||||
# stop/restart so param_bridge (which signals this script) leaves no orphans or
|
||||
# stale state.
|
||||
cleanup() {
|
||||
[ -n "$STATUS_LOOP_PID" ] && kill "$STATUS_LOOP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALE_UP_PID" ] && kill "$TAILSCALE_UP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALED_PID" ] && kill "$TAILSCALED_PID" 2>/dev/null
|
||||
rm -f "$STATUS_FILE" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap cleanup TERM INT
|
||||
|
||||
wait $TAILSCALED_PID
|
||||
|
||||
@@ -490,6 +490,7 @@
|
||||
(function() {
|
||||
var APP = 'Tailscale_VPN';
|
||||
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
||||
var STATUS_URL = 'status.json';
|
||||
var logBox = document.getElementById('log-box');
|
||||
var autoScroll = true;
|
||||
|
||||
@@ -737,29 +738,82 @@
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
// Ground truth published by the run script from `tailscale status --json`.
|
||||
function fetchStatus() {
|
||||
return fetch(STATUS_URL + '?t=' + Date.now(), { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.catch(function() { return null; });
|
||||
}
|
||||
|
||||
// Apply Tailscale's authoritative backend state onto the result object.
|
||||
function applyStatus(result, st) {
|
||||
var self = st.Self || {};
|
||||
var ips = self.TailscaleIPs || st.TailscaleIPs || [];
|
||||
var ip4 = null;
|
||||
for (var i = 0; i < ips.length; i++) { if (/^100\./.test(ips[i])) { ip4 = ips[i]; break; } }
|
||||
var bs = st.BackendState;
|
||||
|
||||
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;
|
||||
result.node = self.HostName || result.node;
|
||||
result.tailnet = (st.CurrentTailnet && st.CurrentTailnet.Name) || result.tailnet;
|
||||
cacheSet('ip', result.ip); cacheSet('node', result.node);
|
||||
cacheSet('tailnet', result.tailnet); cacheSet('version', result.version);
|
||||
} else if (bs === 'NeedsLogin' || bs === 'NeedsMachineAuth') {
|
||||
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.
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Stopped') {
|
||||
result.state = 'disconnected';
|
||||
result.url = null;
|
||||
} else {
|
||||
// NoState / Starting / unknown
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
// Always verify with the app status API - syslog can have stale entries
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
} else if (!result.url && result.state !== 'connected') {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
Promise.all([
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.catch(function() { return ''; }),
|
||||
fetchStatus()
|
||||
]).then(function(arr) {
|
||||
var txt = arr[0];
|
||||
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
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
} else if (st && st.BackendState) {
|
||||
// Authoritative: Tailscale's own backend state
|
||||
applyStatus(result, st);
|
||||
} else if (!result.url && result.state !== 'connected') {
|
||||
// Fallback to log heuristic when status.json is unavailable
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -21,9 +21,10 @@
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define SENTINEL_FILE "/usr/local/packages/Tailscale_VPN/localdata/authkey_clear"
|
||||
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
static pid_t child_pid = -1;
|
||||
@@ -104,6 +105,29 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
static gboolean authkey_sentinel_cb(gpointer G_GNUC_UNUSED data) {
|
||||
if (access(SENTINEL_FILE, F_OK) != 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
||||
if (g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth (sentinel)");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
unlink(SENTINEL_FILE);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static void load_config_cache(AXParameter *handle) {
|
||||
GError *error = NULL;
|
||||
gchar *val = NULL;
|
||||
@@ -185,6 +209,10 @@ int main(void) {
|
||||
|
||||
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. */
|
||||
unlink(SENTINEL_FILE);
|
||||
|
||||
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||
if (!handle) {
|
||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||
@@ -212,6 +240,7 @@ int main(void) {
|
||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
||||
g_timeout_add_seconds(5, authkey_sentinel_cb, NULL);
|
||||
|
||||
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||
g_main_loop_run(loop);
|
||||
|
||||
@@ -76,11 +76,66 @@ if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||
fi
|
||||
|
||||
eval $TAILSCALE_CMD
|
||||
UP_EXIT=$?
|
||||
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
||||
# (re-)authentication, `up` blocks until the user logs in; backgrounding it
|
||||
# ensures the status publisher below keeps running so the UI can surface the
|
||||
# login URL (tailscaled reports BackendState=NeedsLogin + AuthURL while waiting).
|
||||
# NOTE: `up` runs synchronously *inside* this backgrounded block so its real exit
|
||||
# code is captured directly. We must NOT background `up` separately and `wait`
|
||||
# for it from here, because in POSIX sh `wait` only works on children of the
|
||||
# current shell — a subshell waiting on the parent's child returns 127.
|
||||
{
|
||||
eval "$TAILSCALE_CMD"
|
||||
up_exit=$?
|
||||
if [ "$up_exit" -eq 0 ]; then
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
# Auth succeeded with a one-time auth key — signal param_bridge to clear it
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
: > "$STATE_DIR/authkey_clear"
|
||||
fi
|
||||
else
|
||||
logger -t "Tailscale_VPN" "ERROR: tailscale up failed (exit $up_exit)"
|
||||
fi
|
||||
} &
|
||||
TAILSCALE_UP_PID=$!
|
||||
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
logger -t "Tailscale_VPN" "HTTP/HTTPS proxy: http://127.0.0.1:$CONF_HTTP"
|
||||
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:$CONF_SOCKS"
|
||||
|
||||
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||
# TailscaleIPs / AuthURL) instead of scraping syslog. Served statically at
|
||||
# /local/Tailscale_VPN/status.json.
|
||||
STATUS_FILE="$APP_DIR/html/status.json"
|
||||
|
||||
publish_status() {
|
||||
if "$TAILSCALE_PATH" --socket="$SOCKET_PATH" status --json > "$STATUS_FILE.tmp" 2>/dev/null; then
|
||||
mv "$STATUS_FILE.tmp" "$STATUS_FILE" 2>/dev/null
|
||||
chmod 644 "$STATUS_FILE" 2>/dev/null
|
||||
else
|
||||
rm -f "$STATUS_FILE.tmp" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
status_loop() {
|
||||
while true; do
|
||||
publish_status
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
status_loop &
|
||||
STATUS_LOOP_PID=$!
|
||||
|
||||
# Clean up the status writer, up watcher, daemon and published status on
|
||||
# stop/restart so param_bridge (which signals this script) leaves no orphans or
|
||||
# stale state.
|
||||
cleanup() {
|
||||
[ -n "$STATUS_LOOP_PID" ] && kill "$STATUS_LOOP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALE_UP_PID" ] && kill "$TAILSCALE_UP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALED_PID" ] && kill "$TAILSCALED_PID" 2>/dev/null
|
||||
rm -f "$STATUS_FILE" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap cleanup TERM INT
|
||||
|
||||
wait $TAILSCALED_PID
|
||||
|
||||
+75
-21
@@ -490,6 +490,7 @@
|
||||
(function() {
|
||||
var APP = 'Tailscale_VPN';
|
||||
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
||||
var STATUS_URL = 'status.json';
|
||||
var logBox = document.getElementById('log-box');
|
||||
var autoScroll = true;
|
||||
|
||||
@@ -737,29 +738,82 @@
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
// Ground truth published by the run script from `tailscale status --json`.
|
||||
function fetchStatus() {
|
||||
return fetch(STATUS_URL + '?t=' + Date.now(), { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.catch(function() { return null; });
|
||||
}
|
||||
|
||||
// Apply Tailscale's authoritative backend state onto the result object.
|
||||
function applyStatus(result, st) {
|
||||
var self = st.Self || {};
|
||||
var ips = self.TailscaleIPs || st.TailscaleIPs || [];
|
||||
var ip4 = null;
|
||||
for (var i = 0; i < ips.length; i++) { if (/^100\./.test(ips[i])) { ip4 = ips[i]; break; } }
|
||||
var bs = st.BackendState;
|
||||
|
||||
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;
|
||||
result.node = self.HostName || result.node;
|
||||
result.tailnet = (st.CurrentTailnet && st.CurrentTailnet.Name) || result.tailnet;
|
||||
cacheSet('ip', result.ip); cacheSet('node', result.node);
|
||||
cacheSet('tailnet', result.tailnet); cacheSet('version', result.version);
|
||||
} else if (bs === 'NeedsLogin' || bs === 'NeedsMachineAuth') {
|
||||
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.
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Stopped') {
|
||||
result.state = 'disconnected';
|
||||
result.url = null;
|
||||
} else {
|
||||
// NoState / Starting / unknown
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
// Always verify with the app status API - syslog can have stale entries
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
} else if (!result.url && result.state !== 'connected') {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
Promise.all([
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.catch(function() { return ''; }),
|
||||
fetchStatus()
|
||||
]).then(function(arr) {
|
||||
var txt = arr[0];
|
||||
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
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
} else if (st && st.BackendState) {
|
||||
// Authoritative: Tailscale's own backend state
|
||||
applyStatus(result, st);
|
||||
} else if (!result.url && result.state !== 'connected') {
|
||||
// Fallback to log heuristic when status.json is unavailable
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+35
-4
@@ -31,13 +31,14 @@
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define SENTINEL_FILE "/usr/local/packages/Tailscale_VPN/localdata/authkey_clear"
|
||||
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
static pid_t child_pid = -1;
|
||||
static guint reload_timer_id = 0;
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
|
||||
static char *cfg_custom_server = NULL;
|
||||
static char *cfg_auth_key = NULL;
|
||||
@@ -120,6 +121,31 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* ── 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. */
|
||||
static gboolean authkey_sentinel_cb(gpointer G_GNUC_UNUSED data) {
|
||||
if (access(SENTINEL_FILE, F_OK) != 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
||||
if (g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth (sentinel)");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
unlink(SENTINEL_FILE);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* ── config file ─────────────────────────────────────────────────────────── */
|
||||
|
||||
static void load_config_cache(AXParameter *handle) {
|
||||
@@ -219,6 +245,10 @@ int main(void) {
|
||||
/* 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. */
|
||||
unlink(SENTINEL_FILE);
|
||||
|
||||
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||
if (!handle) {
|
||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||
@@ -249,6 +279,7 @@ int main(void) {
|
||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
||||
g_timeout_add_seconds(5, authkey_sentinel_cb, NULL);
|
||||
|
||||
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||
g_main_loop_run(loop);
|
||||
|
||||
@@ -50,9 +50,63 @@ if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||
fi
|
||||
|
||||
eval $TAILSCALE_CMD
|
||||
UP_EXIT=$?
|
||||
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
||||
# (re-)authentication, `up` blocks until the user logs in; backgrounding it
|
||||
# ensures the status publisher below keeps running so the UI can surface the
|
||||
# login URL (tailscaled reports BackendState=NeedsLogin + AuthURL while waiting).
|
||||
# NOTE: `up` runs synchronously *inside* this backgrounded block so its real exit
|
||||
# code is captured directly. We must NOT background `up` separately and `wait`
|
||||
# for it from here, because in POSIX sh `wait` only works on children of the
|
||||
# current shell — a subshell waiting on the parent's child returns 127.
|
||||
{
|
||||
eval "$TAILSCALE_CMD"
|
||||
up_exit=$?
|
||||
if [ "$up_exit" -eq 0 ]; then
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running (root mode)"
|
||||
# Auth succeeded with a one-time auth key — signal param_bridge to clear it
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
: > "$STATE_DIR/authkey_clear"
|
||||
fi
|
||||
else
|
||||
logger -t "Tailscale_VPN" "ERROR: tailscale up failed (exit $up_exit)"
|
||||
fi
|
||||
} &
|
||||
TAILSCALE_UP_PID=$!
|
||||
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running (root mode)"
|
||||
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||
# TailscaleIPs / AuthURL) instead of scraping syslog. Served statically at
|
||||
# /local/Tailscale_VPN/status.json.
|
||||
STATUS_FILE="$APP_DIR/html/status.json"
|
||||
|
||||
publish_status() {
|
||||
if "$TAILSCALE_PATH" --socket="$SOCKET_PATH" status --json > "$STATUS_FILE.tmp" 2>/dev/null; then
|
||||
mv "$STATUS_FILE.tmp" "$STATUS_FILE" 2>/dev/null
|
||||
chmod 644 "$STATUS_FILE" 2>/dev/null
|
||||
else
|
||||
rm -f "$STATUS_FILE.tmp" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
status_loop() {
|
||||
while true; do
|
||||
publish_status
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
status_loop &
|
||||
STATUS_LOOP_PID=$!
|
||||
|
||||
# Clean up the status writer, up watcher, daemon and published status on
|
||||
# stop/restart so param_bridge (which signals this script) leaves no orphans or
|
||||
# stale state.
|
||||
cleanup() {
|
||||
[ -n "$STATUS_LOOP_PID" ] && kill "$STATUS_LOOP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALE_UP_PID" ] && kill "$TAILSCALE_UP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALED_PID" ] && kill "$TAILSCALED_PID" 2>/dev/null
|
||||
rm -f "$STATUS_FILE" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap cleanup TERM INT
|
||||
|
||||
wait $TAILSCALED_PID
|
||||
|
||||
@@ -490,6 +490,7 @@
|
||||
(function() {
|
||||
var APP = 'Tailscale_VPN';
|
||||
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
||||
var STATUS_URL = 'status.json';
|
||||
var logBox = document.getElementById('log-box');
|
||||
var autoScroll = true;
|
||||
|
||||
@@ -737,29 +738,82 @@
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
// Ground truth published by the run script from `tailscale status --json`.
|
||||
function fetchStatus() {
|
||||
return fetch(STATUS_URL + '?t=' + Date.now(), { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.catch(function() { return null; });
|
||||
}
|
||||
|
||||
// Apply Tailscale's authoritative backend state onto the result object.
|
||||
function applyStatus(result, st) {
|
||||
var self = st.Self || {};
|
||||
var ips = self.TailscaleIPs || st.TailscaleIPs || [];
|
||||
var ip4 = null;
|
||||
for (var i = 0; i < ips.length; i++) { if (/^100\./.test(ips[i])) { ip4 = ips[i]; break; } }
|
||||
var bs = st.BackendState;
|
||||
|
||||
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;
|
||||
result.node = self.HostName || result.node;
|
||||
result.tailnet = (st.CurrentTailnet && st.CurrentTailnet.Name) || result.tailnet;
|
||||
cacheSet('ip', result.ip); cacheSet('node', result.node);
|
||||
cacheSet('tailnet', result.tailnet); cacheSet('version', result.version);
|
||||
} else if (bs === 'NeedsLogin' || bs === 'NeedsMachineAuth') {
|
||||
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.
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Stopped') {
|
||||
result.state = 'disconnected';
|
||||
result.url = null;
|
||||
} else {
|
||||
// NoState / Starting / unknown
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
// Always verify with the app status API - syslog can have stale entries
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
} else if (!result.url && result.state !== 'connected') {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
Promise.all([
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.catch(function() { return ''; }),
|
||||
fetchStatus()
|
||||
]).then(function(arr) {
|
||||
var txt = arr[0];
|
||||
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
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
} else if (st && st.BackendState) {
|
||||
// Authoritative: Tailscale's own backend state
|
||||
applyStatus(result, st);
|
||||
} else if (!result.url && result.state !== 'connected') {
|
||||
// Fallback to log heuristic when status.json is unavailable
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -21,13 +21,14 @@
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define SENTINEL_FILE "/usr/local/packages/Tailscale_VPN/localdata/authkey_clear"
|
||||
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
static pid_t child_pid = -1;
|
||||
static guint reload_timer_id = 0;
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
|
||||
static char *cfg_custom_server = NULL;
|
||||
static char *cfg_auth_key = NULL;
|
||||
@@ -104,6 +105,29 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
static gboolean authkey_sentinel_cb(gpointer G_GNUC_UNUSED data) {
|
||||
if (access(SENTINEL_FILE, F_OK) != 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
||||
if (g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth (sentinel)");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
unlink(SENTINEL_FILE);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static void load_config_cache(AXParameter *handle) {
|
||||
GError *error = NULL;
|
||||
gchar *val = NULL;
|
||||
@@ -185,6 +209,10 @@ int main(void) {
|
||||
|
||||
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. */
|
||||
unlink(SENTINEL_FILE);
|
||||
|
||||
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||
if (!handle) {
|
||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||
@@ -212,6 +240,7 @@ int main(void) {
|
||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
||||
g_timeout_add_seconds(5, authkey_sentinel_cb, NULL);
|
||||
|
||||
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||
g_main_loop_run(loop);
|
||||
|
||||
@@ -30,6 +30,10 @@ RUN cp html/index.html index.html
|
||||
# The log is written at runtime to localdata/ (resolved path at runtime).
|
||||
RUN ln -sf ../localdata/tailscaled.log html/tailscaled.log
|
||||
|
||||
# Symlink the runtime status.json (written by start.sh from `tailscale status
|
||||
# --json`) into html/ so the web UI can read Tailscale's authoritative state.
|
||||
RUN ln -sf ../localdata/status.json html/status.json
|
||||
|
||||
# Build and package
|
||||
RUN . /opt/axis/acapsdk/environment-setup* && create-package.sh ./
|
||||
|
||||
|
||||
@@ -55,11 +55,37 @@ logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
logger -t "Tailscale_VPN" "HTTP/HTTPS proxy: http://127.0.0.1:8080"
|
||||
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:1055"
|
||||
|
||||
# Monitoring loop: stay alive while tailscaled is running.
|
||||
# This keeps the parent Tailscale_VPN (C launcher) in the process table
|
||||
# so pidof finds it and the camera web UI shows "Running" instead of "Stopped".
|
||||
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||
# TailscaleIPs / AuthURL) instead of scraping logs, which otherwise reports
|
||||
# "connected" whenever the launcher keeps the process alive (e.g. no Internet).
|
||||
# Written to localdata and exposed at html/status.json via a build-time symlink.
|
||||
STATUS_FILE="$STATE_DIR/status.json"
|
||||
|
||||
publish_status() {
|
||||
if "$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" status --json > "$STATUS_FILE.tmp" 2>/dev/null; then
|
||||
mv "$STATUS_FILE.tmp" "$STATUS_FILE" 2>/dev/null
|
||||
chmod 644 "$STATUS_FILE" 2>/dev/null
|
||||
else
|
||||
rm -f "$STATUS_FILE.tmp" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove stale status on stop so the UI does not show a connected node after exit.
|
||||
cleanup() {
|
||||
rm -f "$STATUS_FILE" 2>/dev/null
|
||||
[ -n "$TAILSCALED_PID" ] && kill "$TAILSCALED_PID" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap cleanup TERM INT
|
||||
|
||||
# Monitoring loop: stay alive while tailscaled is running and keep the published
|
||||
# status fresh. This keeps the parent Tailscale_VPN (C launcher) in the process
|
||||
# table so pidof finds it and the camera web UI shows "Running" instead of "Stopped".
|
||||
while kill -0 "$TAILSCALED_PID" 2>/dev/null; do
|
||||
publish_status
|
||||
sleep 5
|
||||
done
|
||||
|
||||
rm -f "$STATUS_FILE" 2>/dev/null
|
||||
logger -t "Tailscale_VPN" "tailscaled exited"
|
||||
|
||||
@@ -612,6 +612,53 @@
|
||||
// ACAP3: also fetch the raw tailscaled.log (symlinked into html/) so the parser
|
||||
// can find IP, version, tailnet and Running state from tailscaled's own output.
|
||||
var DAEMON_LOG_URL = 'tailscaled.log';
|
||||
// Authoritative backend state published by start.sh (symlinked into html/).
|
||||
var STATUS_URL = 'status.json';
|
||||
|
||||
// Ground truth published by start.sh from `tailscale status --json`.
|
||||
function fetchStatus() {
|
||||
return fetch(STATUS_URL + '?t=' + Date.now(), { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.catch(function() { return null; });
|
||||
}
|
||||
|
||||
// Apply Tailscale's authoritative backend state onto the result object.
|
||||
function applyStatus(result, st) {
|
||||
var self = st.Self || {};
|
||||
var ips = self.TailscaleIPs || st.TailscaleIPs || [];
|
||||
var ip4 = null;
|
||||
for (var i = 0; i < ips.length; i++) { if (/^100\./.test(ips[i])) { ip4 = ips[i]; break; } }
|
||||
var bs = st.BackendState;
|
||||
|
||||
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;
|
||||
result.node = self.HostName || result.node;
|
||||
result.tailnet = (st.CurrentTailnet && st.CurrentTailnet.Name) || result.tailnet;
|
||||
cacheSet('ip', result.ip); cacheSet('node', result.node);
|
||||
cacheSet('tailnet', result.tailnet); cacheSet('version', result.version);
|
||||
} else if (bs === 'NeedsLogin' || bs === 'NeedsMachineAuth') {
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Running') {
|
||||
// Backend running but node not online: transient network drop or the
|
||||
// node was removed/expired and needs re-auth. Not connected. Keep any
|
||||
// login URL the log parser found so the login button still appears.
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Stopped') {
|
||||
result.state = 'disconnected';
|
||||
result.url = null;
|
||||
} else {
|
||||
// NoState / Starting / unknown
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
var syslogFetch = fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
@@ -619,17 +666,22 @@
|
||||
var daemonFetch = fetch(DAEMON_LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); }).catch(function() { return ''; });
|
||||
|
||||
Promise.all([syslogFetch, daemonFetch]).then(function(res) {
|
||||
Promise.all([syslogFetch, daemonFetch, fetchStatus()]).then(function(res) {
|
||||
// Syslog provides Axis timestamp headers (node name) and start/stop events.
|
||||
// tailscaled.log provides IP, version, tailnet, and -> Running state.
|
||||
var txt = res[0] + '\n' + res[1];
|
||||
var st = res[2];
|
||||
var result = parse(txt);
|
||||
renderLogs(res[0]); // show syslog in log panel; daemon log is too verbose
|
||||
// Always verify with the app status API - syslog can have stale entries
|
||||
// Always verify with the app status API - logs can have stale entries
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
} else if (st && st.BackendState) {
|
||||
// Authoritative: Tailscale's own backend state
|
||||
applyStatus(result, st);
|
||||
} else if (!result.url && result.state !== 'connected') {
|
||||
// Fallback to log heuristic when status.json is unavailable
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
|
||||
Reference in New Issue
Block a user