mirror of
https://github.com/Mo3he/Axis_Cam_Tailscale.git
synced 2026-10-01 12:05:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0ef4852ae | ||
|
|
741062bcef | ||
|
|
c4ac8ab601 | ||
|
|
fd5cec50bb | ||
|
|
9a35f4e584 | ||
|
|
896fe380ff |
+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.
|
And finally when you are satisfied with your changes, open a new PR.
|
||||||
|
|
||||||
<!-- markdownlint-disable MD034 -->
|
<!-- markdownlint-disable MD034 -->
|
||||||
[issues]: https://github.com/AxisCommunications/tailscale-acap/issues
|
[issues]: https://github.com/Mo3he/Axis_Cam_Tailscale/issues
|
||||||
[issues_new]: https://github.com/AxisCommunications/tailscale-acap/issues/new
|
[issues_new]: https://github.com/Mo3he/Axis_Cam_Tailscale/issues/new
|
||||||
[issues_bugs]: https://github.com/AxisCommunications/tailscale-acap/issues?q=label%3Abug
|
[issues_bugs]: https://github.com/Mo3he/Axis_Cam_Tailscale/issues?q=label%3Abug
|
||||||
[discussions]: https://github.com/AxisCommunications/tailscale-acap/discussions
|
[discussions]: https://github.com/Mo3he/Axis_Cam_Tailscale/discussions
|
||||||
[discussions_new]: https://github.com/AxisCommunications/tailscale-acap/discussions/new
|
[discussions_new]: https://github.com/Mo3he/Axis_Cam_Tailscale/discussions/new
|
||||||
<!-- markdownlint-enable MD034 -->
|
<!-- markdownlint-enable MD034 -->
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
BSD 3-Clause License
|
BSD 3-Clause License
|
||||||
|
|
||||||
Copyright (c) 2020 Tailscale & AUTHORS.
|
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||||
|
Copyright (c) 2022 Weston Blieden (ACAP packaging and wrapper code)
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ This repository provides an **ACAP package** that installs the [Tailscale VPN cl
|
|||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Settings](#settings)
|
- [Settings](#settings)
|
||||||
- [Proxy Support](#proxy-support)
|
- [Proxy Support](#proxy-support)
|
||||||
|
- [Accessing Tailnet Services from the Camera](#accessing-tailnet-services-from-the-camera)
|
||||||
- [Updating Tailscale](#updating-tailscale)
|
- [Updating Tailscale](#updating-tailscale)
|
||||||
- [Purpose](#purpose)
|
- [Purpose](#purpose)
|
||||||
- [Useful Links](#useful-links)
|
- [Useful Links](#useful-links)
|
||||||
@@ -103,6 +104,35 @@ For ACAP apps or services that support SOCKS5, set their proxy to `127.0.0.1:<po
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Accessing Tailnet Services from the Camera
|
||||||
|
|
||||||
|
There is an important asymmetry to understand. Making the camera **reachable from** the tailnet (browsing to it, VAPIX, SSH from another tailnet node) works on every build. The harder direction is the camera **reaching out to** a tailnet peer, for example mounting an SMB/CIFS network share hosted on another node. How well this works depends on which build you use.
|
||||||
|
|
||||||
|
### Why the build matters
|
||||||
|
|
||||||
|
| Build | Networking mode | Camera-initiated access to tailnet peers |
|
||||||
|
|---|---|---|
|
||||||
|
| Non-root (`aarch64`, `armv7hf`) and `armv7hf_acap3` | `--tun=userspace-networking` (no kernel `tailscale0` interface) | Only through the local **SOCKS5 / HTTP proxies**, and only for **proxy-aware** apps. Firmware system services (the SMB share client, NTP, etc.) are proxy-unaware, so they **cannot** reach a peer's `100.x` Tailscale IP directly. |
|
||||||
|
| **ROOT** (`aarch64_root`, `armv7hf_root`) | Kernel networking with a real `tailscale0` interface | Peer `100.x` IPs are routable at the OS level, so firmware services **can** connect directly. Enable **Accept Routes** to also reach subnets behind other nodes. |
|
||||||
|
|
||||||
|
In short: on non-root builds the proxies cover apps that know how to use a proxy, but a system feature like "add network share" opens a raw socket that never touches the tunnel. The ROOT build is the clean way to let the camera *consume* tailnet services.
|
||||||
|
|
||||||
|
### Plan B: reverse-SSH tunnel
|
||||||
|
> **Requires root on the camera.** Port 445 is privileged, so binding it needs a root-capable build (e.g. developer certificates installed).
|
||||||
|
|
||||||
|
If you cannot use the ROOT build but still need the camera to mount a share on a machine that is on your tailnet, you can make the remote share appear **local** to the camera with a reverse SSH tunnel. Because the destination becomes `127.0.0.1`, the proxy-unaware SMB client never has to route over the tailnet.
|
||||||
|
|
||||||
|
From a computer that has both the share and tailnet access to the camera:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Forward the camera's local port 445 back to the SMB share on this machine
|
||||||
|
ssh -R 445:localhost:445 root@<camera-tailscale-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in the camera's **System → Storage → Add network share** dialog, use `127.0.0.1` as the share host and connect.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Updating Tailscale
|
## Updating Tailscale
|
||||||
|
|
||||||
- New `.eap` files are auto-built and released **weekly** (if a new Tailscale version is available).
|
- New `.eap` files are auto-built and released **weekly** (if a new Tailscale version is available).
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ CUSTOM_SERVER=""
|
|||||||
AUTH_KEY=""
|
AUTH_KEY=""
|
||||||
CONF_HTTP="8080"
|
CONF_HTTP="8080"
|
||||||
CONF_SOCKS="1080"
|
CONF_SOCKS="1080"
|
||||||
|
ACCEPT_DNS="false"
|
||||||
|
ACCEPT_ROUTES="false"
|
||||||
|
|
||||||
if [ -f "$STATE_DIR/params.conf" ]; then
|
if [ -f "$STATE_DIR/params.conf" ]; then
|
||||||
. "$STATE_DIR/params.conf"
|
. "$STATE_DIR/params.conf"
|
||||||
@@ -56,7 +58,7 @@ TAILSCALED_PID=$!
|
|||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --hostname=$(hostname)"
|
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
|
||||||
|
|
||||||
if [ -n "$CUSTOM_SERVER" ]; then
|
if [ -n "$CUSTOM_SERVER" ]; then
|
||||||
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
||||||
@@ -66,11 +68,74 @@ if [ -n "$AUTH_KEY" ]; then
|
|||||||
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval $TAILSCALE_CMD
|
if [ "$ACCEPT_DNS" = "true" ]; then
|
||||||
UP_EXIT=$?
|
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||||
|
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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" "HTTP/HTTPS proxy: http://127.0.0.1:$CONF_HTTP"
|
||||||
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:$CONF_SOCKS"
|
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
|
wait $TAILSCALED_PID
|
||||||
|
|||||||
+121
-22
@@ -272,6 +272,23 @@
|
|||||||
.save-status.ok { color: var(--green); }
|
.save-status.ok { color: var(--green); }
|
||||||
.save-status.err { color: var(--red); }
|
.save-status.err { color: var(--red); }
|
||||||
|
|
||||||
|
/* Toggle switch */
|
||||||
|
.toggle-row { display: flex; align-items: flex-start; gap: 12px; }
|
||||||
|
.toggle-switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; margin-top: 2px; }
|
||||||
|
.toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute; cursor: pointer; inset: 0;
|
||||||
|
background: var(--border); border-radius: 20px; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-slider:before {
|
||||||
|
content: ''; position: absolute;
|
||||||
|
height: 14px; width: 14px; left: 3px; bottom: 3px;
|
||||||
|
background: white; border-radius: 50%; transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-switch input:checked + .toggle-slider { background: var(--green); }
|
||||||
|
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(16px); }
|
||||||
|
.toggle-info { flex: 1; }
|
||||||
|
|
||||||
/* Refresh indicator */
|
/* Refresh indicator */
|
||||||
.refresh-bar {
|
.refresh-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -425,6 +442,26 @@
|
|||||||
<input class="settings-input" id="input-socks-port" type="text" autocomplete="off" placeholder="1080">
|
<input class="settings-input" id="input-socks-port" type="text" autocomplete="off" placeholder="1080">
|
||||||
<span class="settings-hint">Port for the SOCKS5 proxy. Default: 1080.</span>
|
<span class="settings-hint">Port for the SOCKS5 proxy. Default: 1080.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row toggle-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="input-accept-dns">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="settings-label">Accept DNS</div>
|
||||||
|
<span class="settings-hint">Pass <code>--accept-dns=true</code> to tailscale up. Allows the tailnet to push DNS settings to this device. Off by default to avoid overriding the camera's DNS configuration.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row toggle-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="input-accept-routes">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="settings-label">Accept Routes</div>
|
||||||
|
<span class="settings-hint">Pass <code>--accept-routes=true</code> to tailscale up. Allows this device to use subnet routes advertised by other nodes in the tailnet.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
<span class="save-status" id="save-status"></span>
|
<span class="save-status" id="save-status"></span>
|
||||||
<button class="save-btn" id="save-btn">Save & Restart</button>
|
<button class="save-btn" id="save-btn">Save & Restart</button>
|
||||||
@@ -453,6 +490,7 @@
|
|||||||
(function() {
|
(function() {
|
||||||
var APP = 'Tailscale_VPN';
|
var APP = 'Tailscale_VPN';
|
||||||
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
||||||
|
var STATUS_URL = 'status.json';
|
||||||
var logBox = document.getElementById('log-box');
|
var logBox = document.getElementById('log-box');
|
||||||
var autoScroll = true;
|
var autoScroll = true;
|
||||||
|
|
||||||
@@ -700,29 +738,82 @@
|
|||||||
.catch(function() { return false; });
|
.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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
Promise.all([
|
||||||
.then(function(r) { return r.text(); })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(txt) {
|
.then(function(r) { return r.text(); })
|
||||||
var result = parse(txt);
|
.catch(function() { return ''; }),
|
||||||
renderLogs(txt);
|
fetchStatus()
|
||||||
// Always verify with the app status API - syslog can have stale entries
|
]).then(function(arr) {
|
||||||
checkAppRunning().then(function(running) {
|
var txt = arr[0];
|
||||||
if (!running) {
|
var st = arr[1];
|
||||||
result.state = 'disconnected';
|
var result = parse(txt || '');
|
||||||
} else if (!result.url && result.state !== 'connected') {
|
if (txt) renderLogs(txt);
|
||||||
result.state = 'connected';
|
// Verify the app is actually running - status.json can be stale if stopped
|
||||||
result.ip = result.ip || cacheGet('ip');
|
checkAppRunning().then(function(running) {
|
||||||
result.node = result.node || cacheGet('node');
|
if (!running) {
|
||||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
result.state = 'disconnected';
|
||||||
result.version = result.version || cacheGet('version');
|
} else if (st && st.BackendState) {
|
||||||
}
|
// Authoritative: Tailscale's own backend state
|
||||||
render(result);
|
applyStatus(result, st);
|
||||||
});
|
} else if (!result.url && result.state !== 'connected') {
|
||||||
})
|
// Fallback to log heuristic when status.json is unavailable
|
||||||
.catch(function() {
|
result.state = 'connected';
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
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();
|
refresh();
|
||||||
@@ -779,6 +870,8 @@
|
|||||||
var authInput = document.getElementById('input-authkey');
|
var authInput = document.getElementById('input-authkey');
|
||||||
var httpPortInput = document.getElementById('input-http-port');
|
var httpPortInput = document.getElementById('input-http-port');
|
||||||
var socksPortInput= document.getElementById('input-socks-port');
|
var socksPortInput= document.getElementById('input-socks-port');
|
||||||
|
var acceptDnsInput = document.getElementById('input-accept-dns');
|
||||||
|
var acceptRoutesInput = document.getElementById('input-accept-routes');
|
||||||
var saveBtn = document.getElementById('save-btn');
|
var saveBtn = document.getElementById('save-btn');
|
||||||
var saveStatus = document.getElementById('save-status');
|
var saveStatus = document.getElementById('save-status');
|
||||||
|
|
||||||
@@ -790,10 +883,14 @@
|
|||||||
var am = txt.match(/root\.\S+\.AuthKey=(.*)/);
|
var am = txt.match(/root\.\S+\.AuthKey=(.*)/);
|
||||||
var hm = txt.match(/root\.\S+\.HttpProxyPort=(.*)/);
|
var hm = txt.match(/root\.\S+\.HttpProxyPort=(.*)/);
|
||||||
var km = txt.match(/root\.\S+\.Socks5Port=(.*)/);
|
var km = txt.match(/root\.\S+\.Socks5Port=(.*)/);
|
||||||
|
var dm = txt.match(/root\.\S+\.AcceptDNS=(.*)/);
|
||||||
|
var rm = txt.match(/root\.\S+\.AcceptRoutes=(.*)/);
|
||||||
if (sm) serverInput.value = sm[1].trim();
|
if (sm) serverInput.value = sm[1].trim();
|
||||||
if (am) authInput.value = am[1].trim();
|
if (am) authInput.value = am[1].trim();
|
||||||
if (hm) httpPortInput.value = hm[1].trim();
|
if (hm) httpPortInput.value = hm[1].trim();
|
||||||
if (km) socksPortInput.value = km[1].trim();
|
if (km) socksPortInput.value = km[1].trim();
|
||||||
|
if (dm) acceptDnsInput.checked = dm[1].trim() === 'true';
|
||||||
|
if (rm) acceptRoutesInput.checked = rm[1].trim() === 'true';
|
||||||
// Update proxy display card with authoritative param values
|
// Update proxy display card with authoritative param values
|
||||||
// and overwrite the localStorage cache so stale ports don't win on next render
|
// and overwrite the localStorage cache so stale ports don't win on next render
|
||||||
var httpPort = hm ? hm[1].trim() : null;
|
var httpPort = hm ? hm[1].trim() : null;
|
||||||
@@ -819,7 +916,9 @@
|
|||||||
'&root.' + APP + '.CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
'&root.' + APP + '.CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
||||||
'&root.' + APP + '.AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
'&root.' + APP + '.AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
||||||
'&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) +
|
'&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) +
|
||||||
'&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort);
|
'&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort) +
|
||||||
|
'&root.' + APP + '.AcceptDNS=' + (acceptDnsInput.checked ? 'true' : 'false') +
|
||||||
|
'&root.' + APP + '.AcceptRoutes=' + (acceptRoutesInput.checked ? 'true' : 'false');
|
||||||
fetch(PARAM_URL, {
|
fetch(PARAM_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -8,7 +8,7 @@
|
|||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.98.3",
|
"version": "1.98.8",
|
||||||
"architecture": "aarch64"
|
"architecture": "aarch64"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -33,6 +33,16 @@
|
|||||||
"name": "Socks5Port",
|
"name": "Socks5Port",
|
||||||
"default": "1080",
|
"default": "1080",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AcceptDNS",
|
||||||
|
"default": "false",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AcceptRoutes",
|
||||||
|
"default": "false",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,10 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
|
||||||
#define APP_NAME "Tailscale_VPN"
|
#define APP_NAME "Tailscale_VPN"
|
||||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
#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 AXParameter *g_ax_handle = NULL;
|
||||||
static pid_t child_pid = -1;
|
static pid_t child_pid = -1;
|
||||||
@@ -43,6 +44,8 @@ static char *cfg_custom_server = NULL;
|
|||||||
static char *cfg_auth_key = NULL;
|
static char *cfg_auth_key = NULL;
|
||||||
static char *cfg_http_proxy_port = NULL;
|
static char *cfg_http_proxy_port = NULL;
|
||||||
static char *cfg_socks5_port = NULL;
|
static char *cfg_socks5_port = NULL;
|
||||||
|
static char *cfg_accept_dns = NULL;
|
||||||
|
static char *cfg_accept_routes = NULL;
|
||||||
|
|
||||||
static void cache_set(char **field, const char *value) {
|
static void cache_set(char **field, const char *value) {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
@@ -118,6 +121,31 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
|||||||
return G_SOURCE_CONTINUE;
|
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 ─────────────────────────────────────────────────────────── */
|
/* ── config file ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
static void load_config_cache(AXParameter *handle) {
|
static void load_config_cache(AXParameter *handle) {
|
||||||
@@ -139,6 +167,8 @@ static void load_config_cache(AXParameter *handle) {
|
|||||||
LOAD("AuthKey", cfg_auth_key)
|
LOAD("AuthKey", cfg_auth_key)
|
||||||
LOAD("HttpProxyPort", cfg_http_proxy_port)
|
LOAD("HttpProxyPort", cfg_http_proxy_port)
|
||||||
LOAD("Socks5Port", cfg_socks5_port)
|
LOAD("Socks5Port", cfg_socks5_port)
|
||||||
|
LOAD("AcceptDNS", cfg_accept_dns)
|
||||||
|
LOAD("AcceptRoutes", cfg_accept_routes)
|
||||||
#undef LOAD
|
#undef LOAD
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +183,8 @@ static void write_config_file(void) {
|
|||||||
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
||||||
fprintf(f, "CONF_HTTP=%s\n", cache_get(&cfg_http_proxy_port, "8080"));
|
fprintf(f, "CONF_HTTP=%s\n", cache_get(&cfg_http_proxy_port, "8080"));
|
||||||
fprintf(f, "CONF_SOCKS=%s\n", cache_get(&cfg_socks5_port, "1080"));
|
fprintf(f, "CONF_SOCKS=%s\n", cache_get(&cfg_socks5_port, "1080"));
|
||||||
|
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
|
||||||
|
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
|
||||||
fclose(f);
|
fclose(f);
|
||||||
chmod(CONFIG_FILE, 0600);
|
chmod(CONFIG_FILE, 0600);
|
||||||
syslog(LOG_INFO, "config updated: http=%s socks=%s server=%s",
|
syslog(LOG_INFO, "config updated: http=%s socks=%s server=%s",
|
||||||
@@ -185,6 +217,8 @@ static void parameter_changed(const gchar *name, const gchar *value,
|
|||||||
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||||
else if (strcmp(short_name, "HttpProxyPort") == 0) cache_set(&cfg_http_proxy_port, value);
|
else if (strcmp(short_name, "HttpProxyPort") == 0) cache_set(&cfg_http_proxy_port, value);
|
||||||
else if (strcmp(short_name, "Socks5Port") == 0) cache_set(&cfg_socks5_port, value);
|
else if (strcmp(short_name, "Socks5Port") == 0) cache_set(&cfg_socks5_port, value);
|
||||||
|
else if (strcmp(short_name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||||
|
else if (strcmp(short_name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||||
|
|
||||||
if (reload_timer_id)
|
if (reload_timer_id)
|
||||||
g_source_remove(reload_timer_id);
|
g_source_remove(reload_timer_id);
|
||||||
@@ -211,6 +245,10 @@ int main(void) {
|
|||||||
/* Ensure localdata dir exists */
|
/* Ensure localdata dir exists */
|
||||||
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
|
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);
|
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||||
@@ -225,7 +263,8 @@ int main(void) {
|
|||||||
start_child();
|
start_child();
|
||||||
|
|
||||||
const char *params[] = {
|
const char *params[] = {
|
||||||
"CustomServer", "AuthKey", "HttpProxyPort", "Socks5Port"
|
"CustomServer", "AuthKey", "HttpProxyPort", "Socks5Port",
|
||||||
|
"AcceptDNS", "AcceptRoutes"
|
||||||
};
|
};
|
||||||
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
||||||
if (!ax_parameter_register_callback(handle, params[i],
|
if (!ax_parameter_register_callback(handle, params[i],
|
||||||
@@ -240,6 +279,7 @@ int main(void) {
|
|||||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
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");
|
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||||
g_main_loop_run(loop);
|
g_main_loop_run(loop);
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ chmod 755 $TAILSCALE_PATH
|
|||||||
|
|
||||||
CUSTOM_SERVER=""
|
CUSTOM_SERVER=""
|
||||||
AUTH_KEY=""
|
AUTH_KEY=""
|
||||||
|
ACCEPT_DNS="false"
|
||||||
|
ACCEPT_ROUTES="false"
|
||||||
|
|
||||||
if [ -f "$STATE_DIR/params.conf" ]; then
|
if [ -f "$STATE_DIR/params.conf" ]; then
|
||||||
. "$STATE_DIR/params.conf"
|
. "$STATE_DIR/params.conf"
|
||||||
@@ -30,7 +32,7 @@ TAILSCALED_PID=$!
|
|||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --accept-routes --hostname=$(hostname)"
|
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
|
||||||
|
|
||||||
if [ -n "$CUSTOM_SERVER" ]; then
|
if [ -n "$CUSTOM_SERVER" ]; then
|
||||||
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
||||||
@@ -40,9 +42,71 @@ if [ -n "$AUTH_KEY" ]; then
|
|||||||
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval $TAILSCALE_CMD
|
if [ "$ACCEPT_DNS" = "true" ]; then
|
||||||
UP_EXIT=$?
|
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
|
||||||
|
fi
|
||||||
|
|
||||||
logger -t "Tailscale_VPN" "Tailscale VPN is running (root mode)"
|
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||||
|
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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=$!
|
||||||
|
|
||||||
|
# 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
|
wait $TAILSCALED_PID
|
||||||
|
|||||||
@@ -272,6 +272,23 @@
|
|||||||
.save-status.ok { color: var(--green); }
|
.save-status.ok { color: var(--green); }
|
||||||
.save-status.err { color: var(--red); }
|
.save-status.err { color: var(--red); }
|
||||||
|
|
||||||
|
/* Toggle switch */
|
||||||
|
.toggle-row { display: flex; align-items: flex-start; gap: 12px; }
|
||||||
|
.toggle-switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; margin-top: 2px; }
|
||||||
|
.toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute; cursor: pointer; inset: 0;
|
||||||
|
background: var(--border); border-radius: 20px; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-slider:before {
|
||||||
|
content: ''; position: absolute;
|
||||||
|
height: 14px; width: 14px; left: 3px; bottom: 3px;
|
||||||
|
background: white; border-radius: 50%; transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-switch input:checked + .toggle-slider { background: var(--green); }
|
||||||
|
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(16px); }
|
||||||
|
.toggle-info { flex: 1; }
|
||||||
|
|
||||||
/* Refresh indicator */
|
/* Refresh indicator */
|
||||||
.refresh-bar {
|
.refresh-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -425,6 +442,26 @@
|
|||||||
<input class="settings-input" id="input-socks-port" type="text" autocomplete="off" placeholder="1080">
|
<input class="settings-input" id="input-socks-port" type="text" autocomplete="off" placeholder="1080">
|
||||||
<span class="settings-hint">Port for the SOCKS5 proxy. Default: 1080.</span>
|
<span class="settings-hint">Port for the SOCKS5 proxy. Default: 1080.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row toggle-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="input-accept-dns">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="settings-label">Accept DNS</div>
|
||||||
|
<span class="settings-hint">Pass <code>--accept-dns=true</code> to tailscale up. Allows the tailnet to push DNS settings to this device. Off by default to avoid overriding the camera's DNS configuration.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row toggle-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="input-accept-routes">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="settings-label">Accept Routes</div>
|
||||||
|
<span class="settings-hint">Pass <code>--accept-routes=true</code> to tailscale up. Allows this device to use subnet routes advertised by other nodes in the tailnet.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
<span class="save-status" id="save-status"></span>
|
<span class="save-status" id="save-status"></span>
|
||||||
<button class="save-btn" id="save-btn">Save & Restart</button>
|
<button class="save-btn" id="save-btn">Save & Restart</button>
|
||||||
@@ -453,6 +490,7 @@
|
|||||||
(function() {
|
(function() {
|
||||||
var APP = 'Tailscale_VPN';
|
var APP = 'Tailscale_VPN';
|
||||||
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
||||||
|
var STATUS_URL = 'status.json';
|
||||||
var logBox = document.getElementById('log-box');
|
var logBox = document.getElementById('log-box');
|
||||||
var autoScroll = true;
|
var autoScroll = true;
|
||||||
|
|
||||||
@@ -700,29 +738,82 @@
|
|||||||
.catch(function() { return false; });
|
.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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
Promise.all([
|
||||||
.then(function(r) { return r.text(); })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(txt) {
|
.then(function(r) { return r.text(); })
|
||||||
var result = parse(txt);
|
.catch(function() { return ''; }),
|
||||||
renderLogs(txt);
|
fetchStatus()
|
||||||
// Always verify with the app status API - syslog can have stale entries
|
]).then(function(arr) {
|
||||||
checkAppRunning().then(function(running) {
|
var txt = arr[0];
|
||||||
if (!running) {
|
var st = arr[1];
|
||||||
result.state = 'disconnected';
|
var result = parse(txt || '');
|
||||||
} else if (!result.url && result.state !== 'connected') {
|
if (txt) renderLogs(txt);
|
||||||
result.state = 'connected';
|
// Verify the app is actually running - status.json can be stale if stopped
|
||||||
result.ip = result.ip || cacheGet('ip');
|
checkAppRunning().then(function(running) {
|
||||||
result.node = result.node || cacheGet('node');
|
if (!running) {
|
||||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
result.state = 'disconnected';
|
||||||
result.version = result.version || cacheGet('version');
|
} else if (st && st.BackendState) {
|
||||||
}
|
// Authoritative: Tailscale's own backend state
|
||||||
render(result);
|
applyStatus(result, st);
|
||||||
});
|
} else if (!result.url && result.state !== 'connected') {
|
||||||
})
|
// Fallback to log heuristic when status.json is unavailable
|
||||||
.catch(function() {
|
result.state = 'connected';
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
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();
|
refresh();
|
||||||
@@ -779,6 +870,8 @@
|
|||||||
var authInput = document.getElementById('input-authkey');
|
var authInput = document.getElementById('input-authkey');
|
||||||
var httpPortInput = document.getElementById('input-http-port');
|
var httpPortInput = document.getElementById('input-http-port');
|
||||||
var socksPortInput= document.getElementById('input-socks-port');
|
var socksPortInput= document.getElementById('input-socks-port');
|
||||||
|
var acceptDnsInput = document.getElementById('input-accept-dns');
|
||||||
|
var acceptRoutesInput = document.getElementById('input-accept-routes');
|
||||||
var saveBtn = document.getElementById('save-btn');
|
var saveBtn = document.getElementById('save-btn');
|
||||||
var saveStatus = document.getElementById('save-status');
|
var saveStatus = document.getElementById('save-status');
|
||||||
|
|
||||||
@@ -790,10 +883,14 @@
|
|||||||
var am = txt.match(/root\.\S+\.AuthKey=(.*)/);
|
var am = txt.match(/root\.\S+\.AuthKey=(.*)/);
|
||||||
var hm = txt.match(/root\.\S+\.HttpProxyPort=(.*)/);
|
var hm = txt.match(/root\.\S+\.HttpProxyPort=(.*)/);
|
||||||
var km = txt.match(/root\.\S+\.Socks5Port=(.*)/);
|
var km = txt.match(/root\.\S+\.Socks5Port=(.*)/);
|
||||||
|
var dm = txt.match(/root\.\S+\.AcceptDNS=(.*)/);
|
||||||
|
var rm = txt.match(/root\.\S+\.AcceptRoutes=(.*)/);
|
||||||
if (sm) serverInput.value = sm[1].trim();
|
if (sm) serverInput.value = sm[1].trim();
|
||||||
if (am) authInput.value = am[1].trim();
|
if (am) authInput.value = am[1].trim();
|
||||||
if (hm) httpPortInput.value = hm[1].trim();
|
if (hm) httpPortInput.value = hm[1].trim();
|
||||||
if (km) socksPortInput.value = km[1].trim();
|
if (km) socksPortInput.value = km[1].trim();
|
||||||
|
if (dm) acceptDnsInput.checked = dm[1].trim() === 'true';
|
||||||
|
if (rm) acceptRoutesInput.checked = rm[1].trim() === 'true';
|
||||||
// Update proxy display card with authoritative param values
|
// Update proxy display card with authoritative param values
|
||||||
// and overwrite the localStorage cache so stale ports don't win on next render
|
// and overwrite the localStorage cache so stale ports don't win on next render
|
||||||
var httpPort = hm ? hm[1].trim() : null;
|
var httpPort = hm ? hm[1].trim() : null;
|
||||||
@@ -819,7 +916,9 @@
|
|||||||
'&root.' + APP + '.CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
'&root.' + APP + '.CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
||||||
'&root.' + APP + '.AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
'&root.' + APP + '.AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
||||||
'&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) +
|
'&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) +
|
||||||
'&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort);
|
'&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort) +
|
||||||
|
'&root.' + APP + '.AcceptDNS=' + (acceptDnsInput.checked ? 'true' : 'false') +
|
||||||
|
'&root.' + APP + '.AcceptRoutes=' + (acceptRoutesInput.checked ? 'true' : 'false');
|
||||||
fetch(PARAM_URL, {
|
fetch(PARAM_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.98.3",
|
"version": "1.98.8",
|
||||||
"architecture": "aarch64"
|
"architecture": "aarch64"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -27,6 +27,16 @@
|
|||||||
"name": "AuthKey",
|
"name": "AuthKey",
|
||||||
"default": "",
|
"default": "",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AcceptDNS",
|
||||||
|
"default": "false",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AcceptRoutes",
|
||||||
|
"default": "false",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,10 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
|
||||||
#define APP_NAME "Tailscale_VPN"
|
#define APP_NAME "Tailscale_VPN"
|
||||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
#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 AXParameter *g_ax_handle = NULL;
|
||||||
static pid_t child_pid = -1;
|
static pid_t child_pid = -1;
|
||||||
@@ -31,6 +32,8 @@ static guint reload_timer_id = 0;
|
|||||||
|
|
||||||
static char *cfg_custom_server = NULL;
|
static char *cfg_custom_server = NULL;
|
||||||
static char *cfg_auth_key = NULL;
|
static char *cfg_auth_key = NULL;
|
||||||
|
static char *cfg_accept_dns = NULL;
|
||||||
|
static char *cfg_accept_routes = NULL;
|
||||||
|
|
||||||
static void cache_set(char **field, const char *value) {
|
static void cache_set(char **field, const char *value) {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
@@ -102,6 +105,29 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
|||||||
return G_SOURCE_CONTINUE;
|
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) {
|
static void load_config_cache(AXParameter *handle) {
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
gchar *val = NULL;
|
gchar *val = NULL;
|
||||||
@@ -119,6 +145,8 @@ static void load_config_cache(AXParameter *handle) {
|
|||||||
|
|
||||||
LOAD("CustomServer", cfg_custom_server)
|
LOAD("CustomServer", cfg_custom_server)
|
||||||
LOAD("AuthKey", cfg_auth_key)
|
LOAD("AuthKey", cfg_auth_key)
|
||||||
|
LOAD("AcceptDNS", cfg_accept_dns)
|
||||||
|
LOAD("AcceptRoutes", cfg_accept_routes)
|
||||||
#undef LOAD
|
#undef LOAD
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +159,8 @@ static void write_config_file(void) {
|
|||||||
}
|
}
|
||||||
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
|
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
|
||||||
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
||||||
|
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
|
||||||
|
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
|
||||||
fclose(f);
|
fclose(f);
|
||||||
chmod(CONFIG_FILE, 0600);
|
chmod(CONFIG_FILE, 0600);
|
||||||
syslog(LOG_INFO, "config updated: server=%s",
|
syslog(LOG_INFO, "config updated: server=%s",
|
||||||
@@ -156,6 +186,8 @@ static void parameter_changed(const gchar *name, const gchar *value,
|
|||||||
|
|
||||||
if (strcmp(short_name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
if (strcmp(short_name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||||
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||||
|
else if (strcmp(short_name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||||
|
else if (strcmp(short_name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||||
|
|
||||||
if (reload_timer_id)
|
if (reload_timer_id)
|
||||||
g_source_remove(reload_timer_id);
|
g_source_remove(reload_timer_id);
|
||||||
@@ -177,6 +209,10 @@ int main(void) {
|
|||||||
|
|
||||||
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
|
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);
|
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||||
@@ -190,7 +226,7 @@ int main(void) {
|
|||||||
write_config_file();
|
write_config_file();
|
||||||
start_child();
|
start_child();
|
||||||
|
|
||||||
const char *params[] = { "CustomServer", "AuthKey" };
|
const char *params[] = { "CustomServer", "AuthKey", "AcceptDNS", "AcceptRoutes" };
|
||||||
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
||||||
if (!ax_parameter_register_callback(handle, params[i],
|
if (!ax_parameter_register_callback(handle, params[i],
|
||||||
parameter_changed, handle, &error)) {
|
parameter_changed, handle, &error)) {
|
||||||
@@ -204,6 +240,7 @@ int main(void) {
|
|||||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
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");
|
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||||
g_main_loop_run(loop);
|
g_main_loop_run(loop);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ CUSTOM_SERVER=""
|
|||||||
AUTH_KEY=""
|
AUTH_KEY=""
|
||||||
CONF_HTTP="8080"
|
CONF_HTTP="8080"
|
||||||
CONF_SOCKS="1080"
|
CONF_SOCKS="1080"
|
||||||
|
ACCEPT_DNS="false"
|
||||||
|
ACCEPT_ROUTES="false"
|
||||||
|
|
||||||
if [ -f "$STATE_DIR/params.conf" ]; then
|
if [ -f "$STATE_DIR/params.conf" ]; then
|
||||||
. "$STATE_DIR/params.conf"
|
. "$STATE_DIR/params.conf"
|
||||||
@@ -56,7 +58,7 @@ TAILSCALED_PID=$!
|
|||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --hostname=$(hostname)"
|
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
|
||||||
|
|
||||||
if [ -n "$CUSTOM_SERVER" ]; then
|
if [ -n "$CUSTOM_SERVER" ]; then
|
||||||
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
||||||
@@ -66,11 +68,74 @@ if [ -n "$AUTH_KEY" ]; then
|
|||||||
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval $TAILSCALE_CMD
|
if [ "$ACCEPT_DNS" = "true" ]; then
|
||||||
UP_EXIT=$?
|
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||||
|
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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" "HTTP/HTTPS proxy: http://127.0.0.1:$CONF_HTTP"
|
||||||
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:$CONF_SOCKS"
|
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
|
wait $TAILSCALED_PID
|
||||||
|
|||||||
+121
-22
@@ -272,6 +272,23 @@
|
|||||||
.save-status.ok { color: var(--green); }
|
.save-status.ok { color: var(--green); }
|
||||||
.save-status.err { color: var(--red); }
|
.save-status.err { color: var(--red); }
|
||||||
|
|
||||||
|
/* Toggle switch */
|
||||||
|
.toggle-row { display: flex; align-items: flex-start; gap: 12px; }
|
||||||
|
.toggle-switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; margin-top: 2px; }
|
||||||
|
.toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute; cursor: pointer; inset: 0;
|
||||||
|
background: var(--border); border-radius: 20px; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-slider:before {
|
||||||
|
content: ''; position: absolute;
|
||||||
|
height: 14px; width: 14px; left: 3px; bottom: 3px;
|
||||||
|
background: white; border-radius: 50%; transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-switch input:checked + .toggle-slider { background: var(--green); }
|
||||||
|
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(16px); }
|
||||||
|
.toggle-info { flex: 1; }
|
||||||
|
|
||||||
/* Refresh indicator */
|
/* Refresh indicator */
|
||||||
.refresh-bar {
|
.refresh-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -425,6 +442,26 @@
|
|||||||
<input class="settings-input" id="input-socks-port" type="text" autocomplete="off" placeholder="1080">
|
<input class="settings-input" id="input-socks-port" type="text" autocomplete="off" placeholder="1080">
|
||||||
<span class="settings-hint">Port for the SOCKS5 proxy. Default: 1080.</span>
|
<span class="settings-hint">Port for the SOCKS5 proxy. Default: 1080.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row toggle-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="input-accept-dns">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="settings-label">Accept DNS</div>
|
||||||
|
<span class="settings-hint">Pass <code>--accept-dns=true</code> to tailscale up. Allows the tailnet to push DNS settings to this device. Off by default to avoid overriding the camera's DNS configuration.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row toggle-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="input-accept-routes">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="settings-label">Accept Routes</div>
|
||||||
|
<span class="settings-hint">Pass <code>--accept-routes=true</code> to tailscale up. Allows this device to use subnet routes advertised by other nodes in the tailnet.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
<span class="save-status" id="save-status"></span>
|
<span class="save-status" id="save-status"></span>
|
||||||
<button class="save-btn" id="save-btn">Save & Restart</button>
|
<button class="save-btn" id="save-btn">Save & Restart</button>
|
||||||
@@ -453,6 +490,7 @@
|
|||||||
(function() {
|
(function() {
|
||||||
var APP = 'Tailscale_VPN';
|
var APP = 'Tailscale_VPN';
|
||||||
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
||||||
|
var STATUS_URL = 'status.json';
|
||||||
var logBox = document.getElementById('log-box');
|
var logBox = document.getElementById('log-box');
|
||||||
var autoScroll = true;
|
var autoScroll = true;
|
||||||
|
|
||||||
@@ -700,29 +738,82 @@
|
|||||||
.catch(function() { return false; });
|
.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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
Promise.all([
|
||||||
.then(function(r) { return r.text(); })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(txt) {
|
.then(function(r) { return r.text(); })
|
||||||
var result = parse(txt);
|
.catch(function() { return ''; }),
|
||||||
renderLogs(txt);
|
fetchStatus()
|
||||||
// Always verify with the app status API - syslog can have stale entries
|
]).then(function(arr) {
|
||||||
checkAppRunning().then(function(running) {
|
var txt = arr[0];
|
||||||
if (!running) {
|
var st = arr[1];
|
||||||
result.state = 'disconnected';
|
var result = parse(txt || '');
|
||||||
} else if (!result.url && result.state !== 'connected') {
|
if (txt) renderLogs(txt);
|
||||||
result.state = 'connected';
|
// Verify the app is actually running - status.json can be stale if stopped
|
||||||
result.ip = result.ip || cacheGet('ip');
|
checkAppRunning().then(function(running) {
|
||||||
result.node = result.node || cacheGet('node');
|
if (!running) {
|
||||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
result.state = 'disconnected';
|
||||||
result.version = result.version || cacheGet('version');
|
} else if (st && st.BackendState) {
|
||||||
}
|
// Authoritative: Tailscale's own backend state
|
||||||
render(result);
|
applyStatus(result, st);
|
||||||
});
|
} else if (!result.url && result.state !== 'connected') {
|
||||||
})
|
// Fallback to log heuristic when status.json is unavailable
|
||||||
.catch(function() {
|
result.state = 'connected';
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
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();
|
refresh();
|
||||||
@@ -779,6 +870,8 @@
|
|||||||
var authInput = document.getElementById('input-authkey');
|
var authInput = document.getElementById('input-authkey');
|
||||||
var httpPortInput = document.getElementById('input-http-port');
|
var httpPortInput = document.getElementById('input-http-port');
|
||||||
var socksPortInput= document.getElementById('input-socks-port');
|
var socksPortInput= document.getElementById('input-socks-port');
|
||||||
|
var acceptDnsInput = document.getElementById('input-accept-dns');
|
||||||
|
var acceptRoutesInput = document.getElementById('input-accept-routes');
|
||||||
var saveBtn = document.getElementById('save-btn');
|
var saveBtn = document.getElementById('save-btn');
|
||||||
var saveStatus = document.getElementById('save-status');
|
var saveStatus = document.getElementById('save-status');
|
||||||
|
|
||||||
@@ -790,10 +883,14 @@
|
|||||||
var am = txt.match(/root\.\S+\.AuthKey=(.*)/);
|
var am = txt.match(/root\.\S+\.AuthKey=(.*)/);
|
||||||
var hm = txt.match(/root\.\S+\.HttpProxyPort=(.*)/);
|
var hm = txt.match(/root\.\S+\.HttpProxyPort=(.*)/);
|
||||||
var km = txt.match(/root\.\S+\.Socks5Port=(.*)/);
|
var km = txt.match(/root\.\S+\.Socks5Port=(.*)/);
|
||||||
|
var dm = txt.match(/root\.\S+\.AcceptDNS=(.*)/);
|
||||||
|
var rm = txt.match(/root\.\S+\.AcceptRoutes=(.*)/);
|
||||||
if (sm) serverInput.value = sm[1].trim();
|
if (sm) serverInput.value = sm[1].trim();
|
||||||
if (am) authInput.value = am[1].trim();
|
if (am) authInput.value = am[1].trim();
|
||||||
if (hm) httpPortInput.value = hm[1].trim();
|
if (hm) httpPortInput.value = hm[1].trim();
|
||||||
if (km) socksPortInput.value = km[1].trim();
|
if (km) socksPortInput.value = km[1].trim();
|
||||||
|
if (dm) acceptDnsInput.checked = dm[1].trim() === 'true';
|
||||||
|
if (rm) acceptRoutesInput.checked = rm[1].trim() === 'true';
|
||||||
// Update proxy display card with authoritative param values
|
// Update proxy display card with authoritative param values
|
||||||
// and overwrite the localStorage cache so stale ports don't win on next render
|
// and overwrite the localStorage cache so stale ports don't win on next render
|
||||||
var httpPort = hm ? hm[1].trim() : null;
|
var httpPort = hm ? hm[1].trim() : null;
|
||||||
@@ -819,7 +916,9 @@
|
|||||||
'&root.' + APP + '.CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
'&root.' + APP + '.CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
||||||
'&root.' + APP + '.AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
'&root.' + APP + '.AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
||||||
'&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) +
|
'&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) +
|
||||||
'&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort);
|
'&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort) +
|
||||||
|
'&root.' + APP + '.AcceptDNS=' + (acceptDnsInput.checked ? 'true' : 'false') +
|
||||||
|
'&root.' + APP + '.AcceptRoutes=' + (acceptRoutesInput.checked ? 'true' : 'false');
|
||||||
fetch(PARAM_URL, {
|
fetch(PARAM_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+11
-1
@@ -8,7 +8,7 @@
|
|||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.98.3",
|
"version": "1.98.8",
|
||||||
"architecture": "armv7hf"
|
"architecture": "armv7hf"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -33,6 +33,16 @@
|
|||||||
"name": "Socks5Port",
|
"name": "Socks5Port",
|
||||||
"default": "1080",
|
"default": "1080",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AcceptDNS",
|
||||||
|
"default": "false",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AcceptRoutes",
|
||||||
|
"default": "false",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-5
@@ -31,18 +31,21 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
|
||||||
#define APP_NAME "Tailscale_VPN"
|
#define APP_NAME "Tailscale_VPN"
|
||||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
#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 pid_t child_pid = -1;
|
||||||
static guint reload_timer_id = 0;
|
static guint reload_timer_id = 0;
|
||||||
static AXParameter *g_ax_handle = NULL;
|
|
||||||
|
|
||||||
static char *cfg_custom_server = NULL;
|
static char *cfg_custom_server = NULL;
|
||||||
static char *cfg_auth_key = NULL;
|
static char *cfg_auth_key = NULL;
|
||||||
static char *cfg_http_proxy_port = NULL;
|
static char *cfg_http_proxy_port = NULL;
|
||||||
static char *cfg_socks5_port = NULL;
|
static char *cfg_socks5_port = NULL;
|
||||||
|
static char *cfg_accept_dns = NULL;
|
||||||
|
static char *cfg_accept_routes = NULL;
|
||||||
|
|
||||||
static void cache_set(char **field, const char *value) {
|
static void cache_set(char **field, const char *value) {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
@@ -118,6 +121,31 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
|||||||
return G_SOURCE_CONTINUE;
|
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 ─────────────────────────────────────────────────────────── */
|
/* ── config file ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
static void load_config_cache(AXParameter *handle) {
|
static void load_config_cache(AXParameter *handle) {
|
||||||
@@ -139,6 +167,8 @@ static void load_config_cache(AXParameter *handle) {
|
|||||||
LOAD("AuthKey", cfg_auth_key)
|
LOAD("AuthKey", cfg_auth_key)
|
||||||
LOAD("HttpProxyPort", cfg_http_proxy_port)
|
LOAD("HttpProxyPort", cfg_http_proxy_port)
|
||||||
LOAD("Socks5Port", cfg_socks5_port)
|
LOAD("Socks5Port", cfg_socks5_port)
|
||||||
|
LOAD("AcceptDNS", cfg_accept_dns)
|
||||||
|
LOAD("AcceptRoutes", cfg_accept_routes)
|
||||||
#undef LOAD
|
#undef LOAD
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +183,8 @@ static void write_config_file(void) {
|
|||||||
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
||||||
fprintf(f, "CONF_HTTP=%s\n", cache_get(&cfg_http_proxy_port, "8080"));
|
fprintf(f, "CONF_HTTP=%s\n", cache_get(&cfg_http_proxy_port, "8080"));
|
||||||
fprintf(f, "CONF_SOCKS=%s\n", cache_get(&cfg_socks5_port, "1080"));
|
fprintf(f, "CONF_SOCKS=%s\n", cache_get(&cfg_socks5_port, "1080"));
|
||||||
|
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
|
||||||
|
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
|
||||||
fclose(f);
|
fclose(f);
|
||||||
chmod(CONFIG_FILE, 0600);
|
chmod(CONFIG_FILE, 0600);
|
||||||
syslog(LOG_INFO, "config updated: http=%s socks=%s server=%s",
|
syslog(LOG_INFO, "config updated: http=%s socks=%s server=%s",
|
||||||
@@ -185,6 +217,8 @@ static void parameter_changed(const gchar *name, const gchar *value,
|
|||||||
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||||
else if (strcmp(short_name, "HttpProxyPort") == 0) cache_set(&cfg_http_proxy_port, value);
|
else if (strcmp(short_name, "HttpProxyPort") == 0) cache_set(&cfg_http_proxy_port, value);
|
||||||
else if (strcmp(short_name, "Socks5Port") == 0) cache_set(&cfg_socks5_port, value);
|
else if (strcmp(short_name, "Socks5Port") == 0) cache_set(&cfg_socks5_port, value);
|
||||||
|
else if (strcmp(short_name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||||
|
else if (strcmp(short_name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||||
|
|
||||||
if (reload_timer_id)
|
if (reload_timer_id)
|
||||||
g_source_remove(reload_timer_id);
|
g_source_remove(reload_timer_id);
|
||||||
@@ -211,6 +245,10 @@ int main(void) {
|
|||||||
/* Ensure localdata dir exists */
|
/* Ensure localdata dir exists */
|
||||||
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
|
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);
|
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||||
@@ -225,7 +263,8 @@ int main(void) {
|
|||||||
start_child();
|
start_child();
|
||||||
|
|
||||||
const char *params[] = {
|
const char *params[] = {
|
||||||
"CustomServer", "AuthKey", "HttpProxyPort", "Socks5Port"
|
"CustomServer", "AuthKey", "HttpProxyPort", "Socks5Port",
|
||||||
|
"AcceptDNS", "AcceptRoutes"
|
||||||
};
|
};
|
||||||
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
||||||
if (!ax_parameter_register_callback(handle, params[i],
|
if (!ax_parameter_register_callback(handle, params[i],
|
||||||
@@ -240,6 +279,7 @@ int main(void) {
|
|||||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
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");
|
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||||
g_main_loop_run(loop);
|
g_main_loop_run(loop);
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ chmod 755 $TAILSCALE_PATH
|
|||||||
|
|
||||||
CUSTOM_SERVER=""
|
CUSTOM_SERVER=""
|
||||||
AUTH_KEY=""
|
AUTH_KEY=""
|
||||||
|
ACCEPT_DNS="false"
|
||||||
|
ACCEPT_ROUTES="false"
|
||||||
|
|
||||||
if [ -f "$STATE_DIR/params.conf" ]; then
|
if [ -f "$STATE_DIR/params.conf" ]; then
|
||||||
. "$STATE_DIR/params.conf"
|
. "$STATE_DIR/params.conf"
|
||||||
@@ -30,7 +32,7 @@ TAILSCALED_PID=$!
|
|||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --accept-routes --hostname=$(hostname)"
|
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
|
||||||
|
|
||||||
if [ -n "$CUSTOM_SERVER" ]; then
|
if [ -n "$CUSTOM_SERVER" ]; then
|
||||||
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
||||||
@@ -40,9 +42,71 @@ if [ -n "$AUTH_KEY" ]; then
|
|||||||
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval $TAILSCALE_CMD
|
if [ "$ACCEPT_DNS" = "true" ]; then
|
||||||
UP_EXIT=$?
|
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
|
||||||
|
fi
|
||||||
|
|
||||||
logger -t "Tailscale_VPN" "Tailscale VPN is running (root mode)"
|
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||||
|
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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=$!
|
||||||
|
|
||||||
|
# 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
|
wait $TAILSCALED_PID
|
||||||
|
|||||||
+121
-22
@@ -272,6 +272,23 @@
|
|||||||
.save-status.ok { color: var(--green); }
|
.save-status.ok { color: var(--green); }
|
||||||
.save-status.err { color: var(--red); }
|
.save-status.err { color: var(--red); }
|
||||||
|
|
||||||
|
/* Toggle switch */
|
||||||
|
.toggle-row { display: flex; align-items: flex-start; gap: 12px; }
|
||||||
|
.toggle-switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; margin-top: 2px; }
|
||||||
|
.toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute; cursor: pointer; inset: 0;
|
||||||
|
background: var(--border); border-radius: 20px; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-slider:before {
|
||||||
|
content: ''; position: absolute;
|
||||||
|
height: 14px; width: 14px; left: 3px; bottom: 3px;
|
||||||
|
background: white; border-radius: 50%; transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-switch input:checked + .toggle-slider { background: var(--green); }
|
||||||
|
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(16px); }
|
||||||
|
.toggle-info { flex: 1; }
|
||||||
|
|
||||||
/* Refresh indicator */
|
/* Refresh indicator */
|
||||||
.refresh-bar {
|
.refresh-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -425,6 +442,26 @@
|
|||||||
<input class="settings-input" id="input-socks-port" type="text" autocomplete="off" placeholder="1080">
|
<input class="settings-input" id="input-socks-port" type="text" autocomplete="off" placeholder="1080">
|
||||||
<span class="settings-hint">Port for the SOCKS5 proxy. Default: 1080.</span>
|
<span class="settings-hint">Port for the SOCKS5 proxy. Default: 1080.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row toggle-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="input-accept-dns">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="settings-label">Accept DNS</div>
|
||||||
|
<span class="settings-hint">Pass <code>--accept-dns=true</code> to tailscale up. Allows the tailnet to push DNS settings to this device. Off by default to avoid overriding the camera's DNS configuration.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row toggle-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="input-accept-routes">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="settings-label">Accept Routes</div>
|
||||||
|
<span class="settings-hint">Pass <code>--accept-routes=true</code> to tailscale up. Allows this device to use subnet routes advertised by other nodes in the tailnet.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
<span class="save-status" id="save-status"></span>
|
<span class="save-status" id="save-status"></span>
|
||||||
<button class="save-btn" id="save-btn">Save & Restart</button>
|
<button class="save-btn" id="save-btn">Save & Restart</button>
|
||||||
@@ -453,6 +490,7 @@
|
|||||||
(function() {
|
(function() {
|
||||||
var APP = 'Tailscale_VPN';
|
var APP = 'Tailscale_VPN';
|
||||||
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
|
||||||
|
var STATUS_URL = 'status.json';
|
||||||
var logBox = document.getElementById('log-box');
|
var logBox = document.getElementById('log-box');
|
||||||
var autoScroll = true;
|
var autoScroll = true;
|
||||||
|
|
||||||
@@ -700,29 +738,82 @@
|
|||||||
.catch(function() { return false; });
|
.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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
Promise.all([
|
||||||
.then(function(r) { return r.text(); })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(txt) {
|
.then(function(r) { return r.text(); })
|
||||||
var result = parse(txt);
|
.catch(function() { return ''; }),
|
||||||
renderLogs(txt);
|
fetchStatus()
|
||||||
// Always verify with the app status API - syslog can have stale entries
|
]).then(function(arr) {
|
||||||
checkAppRunning().then(function(running) {
|
var txt = arr[0];
|
||||||
if (!running) {
|
var st = arr[1];
|
||||||
result.state = 'disconnected';
|
var result = parse(txt || '');
|
||||||
} else if (!result.url && result.state !== 'connected') {
|
if (txt) renderLogs(txt);
|
||||||
result.state = 'connected';
|
// Verify the app is actually running - status.json can be stale if stopped
|
||||||
result.ip = result.ip || cacheGet('ip');
|
checkAppRunning().then(function(running) {
|
||||||
result.node = result.node || cacheGet('node');
|
if (!running) {
|
||||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
result.state = 'disconnected';
|
||||||
result.version = result.version || cacheGet('version');
|
} else if (st && st.BackendState) {
|
||||||
}
|
// Authoritative: Tailscale's own backend state
|
||||||
render(result);
|
applyStatus(result, st);
|
||||||
});
|
} else if (!result.url && result.state !== 'connected') {
|
||||||
})
|
// Fallback to log heuristic when status.json is unavailable
|
||||||
.catch(function() {
|
result.state = 'connected';
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
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();
|
refresh();
|
||||||
@@ -779,6 +870,8 @@
|
|||||||
var authInput = document.getElementById('input-authkey');
|
var authInput = document.getElementById('input-authkey');
|
||||||
var httpPortInput = document.getElementById('input-http-port');
|
var httpPortInput = document.getElementById('input-http-port');
|
||||||
var socksPortInput= document.getElementById('input-socks-port');
|
var socksPortInput= document.getElementById('input-socks-port');
|
||||||
|
var acceptDnsInput = document.getElementById('input-accept-dns');
|
||||||
|
var acceptRoutesInput = document.getElementById('input-accept-routes');
|
||||||
var saveBtn = document.getElementById('save-btn');
|
var saveBtn = document.getElementById('save-btn');
|
||||||
var saveStatus = document.getElementById('save-status');
|
var saveStatus = document.getElementById('save-status');
|
||||||
|
|
||||||
@@ -790,10 +883,14 @@
|
|||||||
var am = txt.match(/root\.\S+\.AuthKey=(.*)/);
|
var am = txt.match(/root\.\S+\.AuthKey=(.*)/);
|
||||||
var hm = txt.match(/root\.\S+\.HttpProxyPort=(.*)/);
|
var hm = txt.match(/root\.\S+\.HttpProxyPort=(.*)/);
|
||||||
var km = txt.match(/root\.\S+\.Socks5Port=(.*)/);
|
var km = txt.match(/root\.\S+\.Socks5Port=(.*)/);
|
||||||
|
var dm = txt.match(/root\.\S+\.AcceptDNS=(.*)/);
|
||||||
|
var rm = txt.match(/root\.\S+\.AcceptRoutes=(.*)/);
|
||||||
if (sm) serverInput.value = sm[1].trim();
|
if (sm) serverInput.value = sm[1].trim();
|
||||||
if (am) authInput.value = am[1].trim();
|
if (am) authInput.value = am[1].trim();
|
||||||
if (hm) httpPortInput.value = hm[1].trim();
|
if (hm) httpPortInput.value = hm[1].trim();
|
||||||
if (km) socksPortInput.value = km[1].trim();
|
if (km) socksPortInput.value = km[1].trim();
|
||||||
|
if (dm) acceptDnsInput.checked = dm[1].trim() === 'true';
|
||||||
|
if (rm) acceptRoutesInput.checked = rm[1].trim() === 'true';
|
||||||
// Update proxy display card with authoritative param values
|
// Update proxy display card with authoritative param values
|
||||||
// and overwrite the localStorage cache so stale ports don't win on next render
|
// and overwrite the localStorage cache so stale ports don't win on next render
|
||||||
var httpPort = hm ? hm[1].trim() : null;
|
var httpPort = hm ? hm[1].trim() : null;
|
||||||
@@ -819,7 +916,9 @@
|
|||||||
'&root.' + APP + '.CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
'&root.' + APP + '.CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
||||||
'&root.' + APP + '.AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
'&root.' + APP + '.AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
||||||
'&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) +
|
'&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) +
|
||||||
'&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort);
|
'&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort) +
|
||||||
|
'&root.' + APP + '.AcceptDNS=' + (acceptDnsInput.checked ? 'true' : 'false') +
|
||||||
|
'&root.' + APP + '.AcceptRoutes=' + (acceptRoutesInput.checked ? 'true' : 'false');
|
||||||
fetch(PARAM_URL, {
|
fetch(PARAM_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.98.3",
|
"version": "1.98.8",
|
||||||
"architecture": "armv7hf"
|
"architecture": "armv7hf"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -27,6 +27,16 @@
|
|||||||
"name": "AuthKey",
|
"name": "AuthKey",
|
||||||
"default": "",
|
"default": "",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AcceptDNS",
|
||||||
|
"default": "false",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AcceptRoutes",
|
||||||
|
"default": "false",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,19 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
|
||||||
#define APP_NAME "Tailscale_VPN"
|
#define APP_NAME "Tailscale_VPN"
|
||||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
#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 pid_t child_pid = -1;
|
||||||
static guint reload_timer_id = 0;
|
static guint reload_timer_id = 0;
|
||||||
static AXParameter *g_ax_handle = NULL;
|
|
||||||
|
|
||||||
static char *cfg_custom_server = NULL;
|
static char *cfg_custom_server = NULL;
|
||||||
static char *cfg_auth_key = NULL;
|
static char *cfg_auth_key = NULL;
|
||||||
|
static char *cfg_accept_dns = NULL;
|
||||||
|
static char *cfg_accept_routes = NULL;
|
||||||
|
|
||||||
static void cache_set(char **field, const char *value) {
|
static void cache_set(char **field, const char *value) {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
@@ -102,6 +105,29 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
|||||||
return G_SOURCE_CONTINUE;
|
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) {
|
static void load_config_cache(AXParameter *handle) {
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
gchar *val = NULL;
|
gchar *val = NULL;
|
||||||
@@ -119,6 +145,8 @@ static void load_config_cache(AXParameter *handle) {
|
|||||||
|
|
||||||
LOAD("CustomServer", cfg_custom_server)
|
LOAD("CustomServer", cfg_custom_server)
|
||||||
LOAD("AuthKey", cfg_auth_key)
|
LOAD("AuthKey", cfg_auth_key)
|
||||||
|
LOAD("AcceptDNS", cfg_accept_dns)
|
||||||
|
LOAD("AcceptRoutes", cfg_accept_routes)
|
||||||
#undef LOAD
|
#undef LOAD
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +159,8 @@ static void write_config_file(void) {
|
|||||||
}
|
}
|
||||||
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
|
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
|
||||||
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
||||||
|
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
|
||||||
|
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
|
||||||
fclose(f);
|
fclose(f);
|
||||||
chmod(CONFIG_FILE, 0600);
|
chmod(CONFIG_FILE, 0600);
|
||||||
syslog(LOG_INFO, "config updated: server=%s",
|
syslog(LOG_INFO, "config updated: server=%s",
|
||||||
@@ -156,6 +186,8 @@ static void parameter_changed(const gchar *name, const gchar *value,
|
|||||||
|
|
||||||
if (strcmp(short_name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
if (strcmp(short_name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||||
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||||
|
else if (strcmp(short_name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||||
|
else if (strcmp(short_name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||||
|
|
||||||
if (reload_timer_id)
|
if (reload_timer_id)
|
||||||
g_source_remove(reload_timer_id);
|
g_source_remove(reload_timer_id);
|
||||||
@@ -177,6 +209,10 @@ int main(void) {
|
|||||||
|
|
||||||
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
|
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);
|
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||||
@@ -190,7 +226,7 @@ int main(void) {
|
|||||||
write_config_file();
|
write_config_file();
|
||||||
start_child();
|
start_child();
|
||||||
|
|
||||||
const char *params[] = { "CustomServer", "AuthKey" };
|
const char *params[] = { "CustomServer", "AuthKey", "AcceptDNS", "AcceptRoutes" };
|
||||||
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
||||||
if (!ax_parameter_register_callback(handle, params[i],
|
if (!ax_parameter_register_callback(handle, params[i],
|
||||||
parameter_changed, handle, &error)) {
|
parameter_changed, handle, &error)) {
|
||||||
@@ -204,6 +240,7 @@ int main(void) {
|
|||||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
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");
|
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||||
g_main_loop_run(loop);
|
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).
|
# The log is written at runtime to localdata/ (resolved path at runtime).
|
||||||
RUN ln -sf ../localdata/tailscaled.log html/tailscaled.log
|
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
|
# Build and package
|
||||||
RUN . /opt/axis/acapsdk/environment-setup* && create-package.sh ./
|
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" "HTTP/HTTPS proxy: http://127.0.0.1:8080"
|
||||||
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:1055"
|
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:1055"
|
||||||
|
|
||||||
# Monitoring loop: stay alive while tailscaled is running.
|
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||||
# This keeps the parent Tailscale_VPN (C launcher) in the process table
|
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||||
# so pidof finds it and the camera web UI shows "Running" instead of "Stopped".
|
# 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
|
while kill -0 "$TAILSCALED_PID" 2>/dev/null; do
|
||||||
|
publish_status
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
|
|
||||||
|
rm -f "$STATUS_FILE" 2>/dev/null
|
||||||
logger -t "Tailscale_VPN" "tailscaled exited"
|
logger -t "Tailscale_VPN" "tailscaled exited"
|
||||||
|
|||||||
@@ -612,6 +612,53 @@
|
|||||||
// ACAP3: also fetch the raw tailscaled.log (symlinked into html/) so the parser
|
// 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.
|
// can find IP, version, tailnet and Running state from tailscaled's own output.
|
||||||
var DAEMON_LOG_URL = 'tailscaled.log';
|
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() {
|
function refresh() {
|
||||||
var syslogFetch = fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
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' })
|
var daemonFetch = fetch(DAEMON_LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(r) { return r.text(); }).catch(function() { return ''; });
|
.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.
|
// Syslog provides Axis timestamp headers (node name) and start/stop events.
|
||||||
// tailscaled.log provides IP, version, tailnet, and -> Running state.
|
// tailscaled.log provides IP, version, tailnet, and -> Running state.
|
||||||
var txt = res[0] + '\n' + res[1];
|
var txt = res[0] + '\n' + res[1];
|
||||||
|
var st = res[2];
|
||||||
var result = parse(txt);
|
var result = parse(txt);
|
||||||
renderLogs(res[0]); // show syslog in log panel; daemon log is too verbose
|
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) {
|
checkAppRunning().then(function(running) {
|
||||||
if (!running) {
|
if (!running) {
|
||||||
result.state = 'disconnected';
|
result.state = 'disconnected';
|
||||||
|
} else if (st && st.BackendState) {
|
||||||
|
// Authoritative: Tailscale's own backend state
|
||||||
|
applyStatus(result, st);
|
||||||
} else if (!result.url && result.state !== 'connected') {
|
} else if (!result.url && result.state !== 'connected') {
|
||||||
|
// Fallback to log heuristic when status.json is unavailable
|
||||||
result.state = 'connected';
|
result.state = 'connected';
|
||||||
result.ip = result.ip || cacheGet('ip');
|
result.ip = result.ip || cacheGet('ip');
|
||||||
result.node = result.node || cacheGet('node');
|
result.node = result.node || cacheGet('node');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ MENUNAME="Tailscale VPN"
|
|||||||
VENDOR="Mo3he"
|
VENDOR="Mo3he"
|
||||||
APPMAJORVERSION=1
|
APPMAJORVERSION=1
|
||||||
APPMINORVERSION=98
|
APPMINORVERSION=98
|
||||||
APPMICROVERSION=3
|
APPMICROVERSION=8
|
||||||
APPTYPE=armv7hf
|
APPTYPE=armv7hf
|
||||||
APPNAME=Tailscale_VPN
|
APPNAME=Tailscale_VPN
|
||||||
APPOPTS=""
|
APPOPTS=""
|
||||||
|
|||||||
Reference in New Issue
Block a user