Compare commits

...
Author SHA1 Message Date
Weston Blieden ef7d73ca0f Remove --verbose=1 from tailscaled to reduce syslog noise 2026-04-14 08:13:51 +02:00
Mo3heandClaude Sonnet 4.6 5080ec39fb Fix BSD 3-Clause license compliance across all variants
- Set vendor to "Mo3he" and vendorUrl to GitHub repo in all manifest.json
  files to avoid implying Tailscale Inc. endorsement (Clause 3)
- Add Tailscale third-party notice to README.md
- Add attribution/disclaimer footer to docs/index.html

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 21:19:20 +02:00
Weston BliedenandGitHub 219ff70c84 Update disclaimer text in index.html 2026-04-13 16:59:13 +02:00
Weston BliedenandGitHub 7a7ecec47c Update README.md 2026-04-13 16:57:22 +02:00
Weston Blieden 3ab196b7b5 docs: add disclaimer to homepage hero 2026-04-13 16:34:16 +02:00
Weston BliedenandGitHub 6be4d7d8ad Format disclaimer section in README 2026-04-13 16:25:46 +02:00
Weston BliedenandGitHub 64ee3e49ab Add disclaimer to README
Added a disclaimer about the project's independence from Axis Communications.
2026-04-13 16:22:51 +02:00
Weston Blieden 4df48e8ae6 fix: node name, account, version and IP persist after syslog rotation 2026-04-13 14:33:16 +02:00
Weston Blieden 806c3309b6 CI: strip debug info from Tailscale binaries to reduce .eap size ~30% 2026-04-11 21:17:55 +02:00
Weston Blieden a4deaeacae Fix empty syslog: pipe tailscaled output via --verbose=1 to logger, add app list API fallback to all variants 2026-04-11 20:59:35 +02:00
Weston Blieden 6ba1c1a1a1 Fix UI: cache connection details in localStorage to survive syslog rotation, detect running state from health/derp/c2n patterns 2026-04-11 18:04:22 +02:00
Weston BliedenandGitHub 25df94e489 Fix README formatting and remove duplicate link
Removed duplicate homepage link and updated formatting.
2026-04-11 12:45:05 +02:00
Weston Blieden a72987391c Update Tailscale binaries to v1.96.4, fix UI parsing (last-match), add Check for Updates button, direct download links on homepage 2026-04-11 12:41:40 +02:00
31 changed files with 790 additions and 117 deletions
+19 -3
View File
@@ -85,7 +85,23 @@ jobs:
mv tailscale_bins/tailscale tailscale_bins/tailscale_arm64 mv tailscale_bins/tailscale tailscale_bins/tailscale_arm64
mv tailscale_bins/tailscaled tailscale_bins/tailscaled_arm64 mv tailscale_bins/tailscaled tailscale_bins/tailscaled_arm64
# 4. Build each folder, update manifest, and copy .eap files # 4. Strip binaries to reduce package size
- name: Strip binaries
if: env.build_needed == 'true'
run: |
# Install cross-architecture strip tools
sudo apt-get update
sudo apt-get install -y binutils-aarch64-linux-gnu binutils-arm-linux-gnueabihf
# Strip debug info and symbol tables (zero runtime/memory cost)
aarch64-linux-gnu-strip -s tailscale_bins/tailscale_arm64
aarch64-linux-gnu-strip -s tailscale_bins/tailscaled_arm64
arm-linux-gnueabihf-strip -s tailscale_bins/tailscale_arm
arm-linux-gnueabihf-strip -s tailscale_bins/tailscaled_arm
ls -lh tailscale_bins/
# 5. Build each folder, update manifest, and copy .eap files
- name: Build all folders - name: Build all folders
if: env.build_needed == 'true' if: env.build_needed == 'true'
run: | run: |
@@ -143,7 +159,7 @@ jobs:
# Clean up # Clean up
rm -rf build tailscale_bins *.tgz rm -rf build tailscale_bins *.tgz
# 5. Commit updated manifests and .eap files directly to main # 6. Commit updated manifests and .eap files directly to main
- name: Commit updates to main - name: Commit updates to main
if: env.build_needed == 'true' if: env.build_needed == 'true'
run: | run: |
@@ -158,7 +174,7 @@ jobs:
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Mo3he/Axis_Cam_Tailscale.git main git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Mo3he/Axis_Cam_Tailscale.git main
fi fi
# 6. Create GitHub Release with all new .eap files # 7. Create GitHub Release with all new .eap files
- name: Create GitHub Release - name: Create GitHub Release
if: env.build_needed == 'true' if: env.build_needed == 'true'
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
+4 -2
View File
@@ -1,5 +1,7 @@
# Tailscale ACAP for Axis Cameras # Tailscale ACAP for Axis Cameras
🌐 **[Visit the Homepage](https://mo3he.github.io/Axis_Cam_Tailscale/)**
This repository provides an **ACAP package** that installs the [Tailscale VPN client](https://tailscale.com/) on Axis cameras. This repository provides an **ACAP package** that installs the [Tailscale VPN client](https://tailscale.com/) on Axis cameras.
- ✅ Secure remote access to cameras - ✅ Secure remote access to cameras
@@ -7,14 +9,14 @@ This repository provides an **ACAP package** that installs the [Tailscale VPN cl
- ✅ Works on **Axis OS 12+** (non-root version) - ✅ Works on **Axis OS 12+** (non-root version)
- ✅ Based on **WireGuard VPN** technology - ✅ Based on **WireGuard VPN** technology
🌐 **[Visit the Homepage](https://mo3he.github.io/Axis_Cam_Tailscale/)**
[![Releases](https://img.shields.io/github/v/release/Mo3he/Axis_Cam_Tailscale)](https://github.com/Mo3he/Axis_Cam_Tailscale/releases) [![Releases](https://img.shields.io/github/v/release/Mo3he/Axis_Cam_Tailscale)](https://github.com/Mo3he/Axis_Cam_Tailscale/releases)
[![License](https://img.shields.io/github/license/Mo3he/Axis_Cam_Tailscale)](LICENSE) [![License](https://img.shields.io/github/license/Mo3he/Axis_Cam_Tailscale)](LICENSE)
![Total Downloads](https://img.shields.io/github/downloads/Mo3he/Axis_Cam_Tailscale/total?style=flat&label=Downloads&color=blue) ![Total Downloads](https://img.shields.io/github/downloads/Mo3he/Axis_Cam_Tailscale/total?style=flat&label=Downloads&color=blue)
[![Sponsor](https://img.shields.io/badge/sponsor-%E2%9D%A4-lightgrey?logo=github)](https://github.com/sponsors/Mo3he) [![Sponsor](https://img.shields.io/badge/sponsor-%E2%9D%A4-lightgrey?logo=github)](https://github.com/sponsors/Mo3he)
### Disclaimer: This is an independent, community-developed ACAP package and is not an official Axis Communications product. It was developed entirely on personal time and is not affiliated with, endorsed by, or supported by Axis Communications AB. Use it at your own risk. For official Axis software, visit axis.com
> **Tailscale Notice:** Tailscale is a product of Tailscale Inc. This package independently redistributes the Tailscale binaries under the [BSD 3-Clause License](LICENSE) and is not affiliated with, endorsed by, or supported by Tailscale Inc. For the official Tailscale client, visit [tailscale.com](https://tailscale.com).
--- ---
## 📑 Table of Contents ## 📑 Table of Contents
+3 -2
View File
@@ -17,13 +17,14 @@ logger -t "Tailscale_VPN" "Starting tailscaled daemon (userspace networking)"
--state="$STATE_DIR/tailscaled.state" \ --state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" \ --socket="$STATE_DIR/tailscaled.sock" \
--socks5-server=localhost:1055 \ --socks5-server=localhost:1055 \
--tun=userspace-networking & --tun=userspace-networking \
2>&1 | logger -t "Tailscale_VPN" &
TAILSCALED_PID=$! TAILSCALED_PID=$!
sleep 2 sleep 2
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)" logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up "$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up 2>&1 | logger -t "Tailscale_VPN"
logger -t "Tailscale_VPN" "Tailscale VPN is running" logger -t "Tailscale_VPN" "Tailscale VPN is running"
wait $TAILSCALED_PID wait $TAILSCALED_PID
+116 -14
View File
@@ -359,6 +359,9 @@
<div class="info-value" id="ts-version">-</div> <div class="info-value" id="ts-version">-</div>
</div> </div>
</div> </div>
<div style="margin-top:14px;text-align:right;">
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
</div>
</div> </div>
<!-- Logs --> <!-- Logs -->
@@ -421,6 +424,10 @@
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40; autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
}); });
// Cache helpers - survive syslog rotation
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
function parse(txt) { function parse(txt) {
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || []; var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null; var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
@@ -432,25 +439,77 @@
var m = last.match(/http:\/\/(100\.[\d.]+):/); var m = last.match(/http:\/\/(100\.[\d.]+):/);
if (m) tsIP = m[1]; if (m) tsIP = m[1];
} }
if (!tsIP) {
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
if (nmSelf) tsIP = nmSelf[1];
}
if (!tsIP) { if (!tsIP) {
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || []; var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null; tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
} }
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/); var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/); if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
var node = nodeMatch ? nodeMatch[1] : null; var node = null;
if (nodeMatches) {
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
if (nm) node = nm[1];
}
if (!node) {
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
if (hostLine) node = hostLine[1];
}
var loginMatch = txt.match(/active login:\s+(\S+)/); var loginMatches = txt.match(/active login:\s+\S+/g);
var tailnet = loginMatch ? loginMatch[1] : null; var tailnet = null;
if (loginMatches) {
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
if (lm) tailnet = lm[1];
}
if (!tailnet) {
// Fallback: extract from periodic netmap lines "u=user@email.com"
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
if (userMatches) {
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
if (um) tailnet = um[1];
}
}
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/); var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/); var version = null;
var version = versionMatch ? versionMatch[1] : null; if (versionMatches) {
var last = versionMatches[versionMatches.length - 1];
var vm = last.match(/v(\d+\.\d+\.\d+)/);
if (vm) version = vm[1];
}
if (!version) {
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
if (peersMatches) {
var lp = peersMatches[peersMatches.length - 1];
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
if (pm) version = pm[1];
}
}
// Cache when found, restore from cache when missing
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
tsIP = tsIP || cacheGet('ip');
node = node || cacheGet('node');
tailnet = tailnet || cacheGet('tailnet');
version = version || cacheGet('version');
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || []; var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : ''; var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt); var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
// Detect running even after syslog rotation (daemon active but startup lines gone)
if (!isRunning && !latestUrl) {
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
/derp-\d+ connected/.test(txt) ||
/c2n: GET/.test(txt) ||
/localapi:/.test(txt);
}
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version }; if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version }; if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
@@ -526,12 +585,39 @@
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2); ('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
} }
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
function checkAppRunning() {
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(xml) {
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
return m && m[1] === 'Running';
})
.catch(function() { return false; });
}
function refresh() { function refresh() {
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' }) fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); }) .then(function(r) { return r.text(); })
.then(function(txt) { .then(function(txt) {
render(parse(txt)); var result = parse(txt);
renderLogs(txt); renderLogs(txt);
if (result.state !== 'disconnected') {
render(result);
} else {
// Syslog may be empty/rotated - check app status API
checkAppRunning().then(function(running) {
if (running) {
result.state = 'connected';
result.ip = result.ip || cacheGet('ip');
result.node = result.node || cacheGet('node');
result.tailnet = result.tailnet || cacheGet('tailnet');
result.version = result.version || cacheGet('version');
}
render(result);
});
}
}) })
.catch(function() { .catch(function() {
document.getElementById('status-text').textContent = 'Unable to fetch logs'; document.getElementById('status-text').textContent = 'Unable to fetch logs';
@@ -542,10 +628,15 @@
setInterval(refresh, 5000); setInterval(refresh, 5000);
// Check for updates from GitHub // Check for updates from GitHub
var latestChecked = false; var installedVersion = null;
function checkForUpdate(currentVersion) { var autoChecked = false;
if (!currentVersion || latestChecked) return; function checkForUpdate(currentVersion, manual) {
latestChecked = true; if (!currentVersion) return;
installedVersion = currentVersion;
if (!manual && autoChecked) return;
if (!manual) autoChecked = true;
var btn = document.getElementById('check-update-btn');
if (manual && btn) btn.textContent = 'Checking...';
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest') fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
@@ -555,11 +646,22 @@
document.getElementById('update-version').textContent = 'v' + tag; document.getElementById('update-version').textContent = 'v' + tag;
document.getElementById('update-banner').classList.add('visible'); document.getElementById('update-banner').classList.add('visible');
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)'; document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
if (btn) btn.textContent = 'Update Available';
} else {
if (manual && btn) btn.textContent = 'Up to date';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
} }
}) })
.catch(function() {}); .catch(function() {
if (manual && btn) btn.textContent = 'Check failed';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
});
} }
document.getElementById('check-update-btn').addEventListener('click', function() {
if (installedVersion) checkForUpdate(installedVersion, true);
});
function compareVersions(a, b) { function compareVersions(a, b) {
var pa = a.split('.').map(Number); var pa = a.split('.').map(Number);
var pb = b.split('.').map(Number); var pb = b.split('.').map(Number);
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -4,9 +4,9 @@
"setup": { "setup": {
"appName": "Tailscale_VPN", "appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Mo3he", "vendor": "Mo3he",
"embeddedSdkVersion": "3.0", "embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com", "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"runMode": "respawn", "runMode": "respawn",
"version": "1.96.4", "version": "1.96.4",
"architecture": "aarch64" "architecture": "aarch64"
+3 -2
View File
@@ -15,13 +15,14 @@ killall tailscaled 2>/dev/null || true
logger -t "Tailscale_VPN" "Starting tailscaled daemon" logger -t "Tailscale_VPN" "Starting tailscaled daemon"
"$APP_DIR/lib/tailscaled" \ "$APP_DIR/lib/tailscaled" \
--state="$STATE_DIR/tailscaled.state" \ --state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" & --socket="$STATE_DIR/tailscaled.sock" \
2>&1 | logger -t "Tailscale_VPN" &
TAILSCALED_PID=$! TAILSCALED_PID=$!
sleep 2 sleep 2
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)" logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes "$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes 2>&1 | logger -t "Tailscale_VPN"
logger -t "Tailscale_VPN" "Tailscale VPN is running" logger -t "Tailscale_VPN" "Tailscale VPN is running"
wait $TAILSCALED_PID wait $TAILSCALED_PID
+115 -14
View File
@@ -359,6 +359,9 @@
<div class="info-value" id="ts-version">-</div> <div class="info-value" id="ts-version">-</div>
</div> </div>
</div> </div>
<div style="margin-top:14px;text-align:right;">
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
</div>
</div> </div>
<!-- Logs --> <!-- Logs -->
@@ -421,6 +424,10 @@
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40; autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
}); });
// Cache helpers - survive syslog rotation
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
function parse(txt) { function parse(txt) {
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || []; var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null; var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
@@ -432,25 +439,77 @@
var m = last.match(/http:\/\/(100\.[\d.]+):/); var m = last.match(/http:\/\/(100\.[\d.]+):/);
if (m) tsIP = m[1]; if (m) tsIP = m[1];
} }
if (!tsIP) {
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
if (nmSelf) tsIP = nmSelf[1];
}
if (!tsIP) { if (!tsIP) {
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || []; var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null; tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
} }
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/); var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/); if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
var node = nodeMatch ? nodeMatch[1] : null; var node = null;
if (nodeMatches) {
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
if (nm) node = nm[1];
}
if (!node) {
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
if (hostLine) node = hostLine[1];
}
var loginMatch = txt.match(/active login:\s+(\S+)/); var loginMatches = txt.match(/active login:\s+\S+/g);
var tailnet = loginMatch ? loginMatch[1] : null; var tailnet = null;
if (loginMatches) {
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
if (lm) tailnet = lm[1];
}
if (!tailnet) {
// Fallback: extract from periodic netmap lines "u=user@email.com"
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
if (userMatches) {
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
if (um) tailnet = um[1];
}
}
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/); var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/); var version = null;
var version = versionMatch ? versionMatch[1] : null; if (versionMatches) {
var last = versionMatches[versionMatches.length - 1];
var vm = last.match(/v(\d+\.\d+\.\d+)/);
if (vm) version = vm[1];
}
if (!version) {
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
if (peersMatches) {
var lp = peersMatches[peersMatches.length - 1];
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
if (pm) version = pm[1];
}
}
// Cache when found, restore from cache when missing
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
tsIP = tsIP || cacheGet('ip');
node = node || cacheGet('node');
tailnet = tailnet || cacheGet('tailnet');
version = version || cacheGet('version');
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || []; var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : ''; var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt); var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
// Detect running even after syslog rotation (daemon active but startup lines gone)
if (!isRunning && !latestUrl) {
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
/derp-\d+ connected/.test(txt) ||
/c2n: GET/.test(txt) ||
/localapi:/.test(txt);
}
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version }; if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version }; if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
@@ -526,12 +585,38 @@
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2); ('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
} }
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
function checkAppRunning() {
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(xml) {
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
return m && m[1] === 'Running';
})
.catch(function() { return false; });
}
function refresh() { function refresh() {
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' }) fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); }) .then(function(r) { return r.text(); })
.then(function(txt) { .then(function(txt) {
render(parse(txt)); var result = parse(txt);
renderLogs(txt); renderLogs(txt);
if (result.state !== 'disconnected') {
render(result);
} else {
checkAppRunning().then(function(running) {
if (running) {
result.state = 'connected';
result.ip = result.ip || cacheGet('ip');
result.node = result.node || cacheGet('node');
result.tailnet = result.tailnet || cacheGet('tailnet');
result.version = result.version || cacheGet('version');
}
render(result);
});
}
}) })
.catch(function() { .catch(function() {
document.getElementById('status-text').textContent = 'Unable to fetch logs'; document.getElementById('status-text').textContent = 'Unable to fetch logs';
@@ -542,10 +627,15 @@
setInterval(refresh, 5000); setInterval(refresh, 5000);
// Check for updates from GitHub // Check for updates from GitHub
var latestChecked = false; var installedVersion = null;
function checkForUpdate(currentVersion) { var autoChecked = false;
if (!currentVersion || latestChecked) return; function checkForUpdate(currentVersion, manual) {
latestChecked = true; if (!currentVersion) return;
installedVersion = currentVersion;
if (!manual && autoChecked) return;
if (!manual) autoChecked = true;
var btn = document.getElementById('check-update-btn');
if (manual && btn) btn.textContent = 'Checking...';
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest') fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
@@ -555,11 +645,22 @@
document.getElementById('update-version').textContent = 'v' + tag; document.getElementById('update-version').textContent = 'v' + tag;
document.getElementById('update-banner').classList.add('visible'); document.getElementById('update-banner').classList.add('visible');
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)'; document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
if (btn) btn.textContent = 'Update Available';
} else {
if (manual && btn) btn.textContent = 'Up to date';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
} }
}) })
.catch(function() {}); .catch(function() {
if (manual && btn) btn.textContent = 'Check failed';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
});
} }
document.getElementById('check-update-btn').addEventListener('click', function() {
if (installedVersion) checkForUpdate(installedVersion, true);
});
function compareVersions(a, b) { function compareVersions(a, b) {
var pa = a.split('.').map(Number); var pa = a.split('.').map(Number);
var pb = b.split('.').map(Number); var pb = b.split('.').map(Number);
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -4,13 +4,13 @@
"setup": { "setup": {
"appName": "Tailscale_VPN", "appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Mo3he", "vendor": "Mo3he",
"embeddedSdkVersion": "3.0", "embeddedSdkVersion": "3.0",
"user": { "user": {
"group": "root", "group": "root",
"username": "root" "username": "root"
}, },
"vendorUrl": "https://www.tailscale.com", "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"runMode": "respawn", "runMode": "respawn",
"version": "1.96.4", "version": "1.96.4",
"architecture": "aarch64" "architecture": "aarch64"
+115 -14
View File
@@ -359,6 +359,9 @@
<div class="info-value" id="ts-version">-</div> <div class="info-value" id="ts-version">-</div>
</div> </div>
</div> </div>
<div style="margin-top:14px;text-align:right;">
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
</div>
</div> </div>
<!-- Logs --> <!-- Logs -->
@@ -421,6 +424,10 @@
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40; autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
}); });
// Cache helpers - survive syslog rotation
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
function parse(txt) { function parse(txt) {
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || []; var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null; var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
@@ -432,25 +439,77 @@
var m = last.match(/http:\/\/(100\.[\d.]+):/); var m = last.match(/http:\/\/(100\.[\d.]+):/);
if (m) tsIP = m[1]; if (m) tsIP = m[1];
} }
if (!tsIP) {
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
if (nmSelf) tsIP = nmSelf[1];
}
if (!tsIP) { if (!tsIP) {
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || []; var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null; tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
} }
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/); var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/); if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
var node = nodeMatch ? nodeMatch[1] : null; var node = null;
if (nodeMatches) {
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
if (nm) node = nm[1];
}
if (!node) {
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
if (hostLine) node = hostLine[1];
}
var loginMatch = txt.match(/active login:\s+(\S+)/); var loginMatches = txt.match(/active login:\s+\S+/g);
var tailnet = loginMatch ? loginMatch[1] : null; var tailnet = null;
if (loginMatches) {
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
if (lm) tailnet = lm[1];
}
if (!tailnet) {
// Fallback: extract from periodic netmap lines "u=user@email.com"
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
if (userMatches) {
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
if (um) tailnet = um[1];
}
}
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/); var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/); var version = null;
var version = versionMatch ? versionMatch[1] : null; if (versionMatches) {
var last = versionMatches[versionMatches.length - 1];
var vm = last.match(/v(\d+\.\d+\.\d+)/);
if (vm) version = vm[1];
}
if (!version) {
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
if (peersMatches) {
var lp = peersMatches[peersMatches.length - 1];
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
if (pm) version = pm[1];
}
}
// Cache when found, restore from cache when missing
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
tsIP = tsIP || cacheGet('ip');
node = node || cacheGet('node');
tailnet = tailnet || cacheGet('tailnet');
version = version || cacheGet('version');
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || []; var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : ''; var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt); var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
// Detect running even after syslog rotation (daemon active but startup lines gone)
if (!isRunning && !latestUrl) {
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
/derp-\d+ connected/.test(txt) ||
/c2n: GET/.test(txt) ||
/localapi:/.test(txt);
}
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version }; if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version }; if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
@@ -526,12 +585,38 @@
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2); ('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
} }
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
function checkAppRunning() {
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(xml) {
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
return m && m[1] === 'Running';
})
.catch(function() { return false; });
}
function refresh() { function refresh() {
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' }) fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); }) .then(function(r) { return r.text(); })
.then(function(txt) { .then(function(txt) {
render(parse(txt)); var result = parse(txt);
renderLogs(txt); renderLogs(txt);
if (result.state !== 'disconnected') {
render(result);
} else {
checkAppRunning().then(function(running) {
if (running) {
result.state = 'connected';
result.ip = result.ip || cacheGet('ip');
result.node = result.node || cacheGet('node');
result.tailnet = result.tailnet || cacheGet('tailnet');
result.version = result.version || cacheGet('version');
}
render(result);
});
}
}) })
.catch(function() { .catch(function() {
document.getElementById('status-text').textContent = 'Unable to fetch logs'; document.getElementById('status-text').textContent = 'Unable to fetch logs';
@@ -542,10 +627,15 @@
setInterval(refresh, 5000); setInterval(refresh, 5000);
// Check for updates from GitHub // Check for updates from GitHub
var latestChecked = false; var installedVersion = null;
function checkForUpdate(currentVersion) { var autoChecked = false;
if (!currentVersion || latestChecked) return; function checkForUpdate(currentVersion, manual) {
latestChecked = true; if (!currentVersion) return;
installedVersion = currentVersion;
if (!manual && autoChecked) return;
if (!manual) autoChecked = true;
var btn = document.getElementById('check-update-btn');
if (manual && btn) btn.textContent = 'Checking...';
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest') fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
@@ -555,11 +645,22 @@
document.getElementById('update-version').textContent = 'v' + tag; document.getElementById('update-version').textContent = 'v' + tag;
document.getElementById('update-banner').classList.add('visible'); document.getElementById('update-banner').classList.add('visible');
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)'; document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
if (btn) btn.textContent = 'Update Available';
} else {
if (manual && btn) btn.textContent = 'Up to date';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
} }
}) })
.catch(function() {}); .catch(function() {
if (manual && btn) btn.textContent = 'Check failed';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
});
} }
document.getElementById('check-update-btn').addEventListener('click', function() {
if (installedVersion) checkForUpdate(installedVersion, true);
});
function compareVersions(a, b) { function compareVersions(a, b) {
var pa = a.split('.').map(Number); var pa = a.split('.').map(Number);
var pb = b.split('.').map(Number); var pb = b.split('.').map(Number);
+2 -1
View File
@@ -42,7 +42,8 @@ $TAILSCALED_PATH \
--state="$STATE_DIR/tailscaled.state" \ --state="$STATE_DIR/tailscaled.state" \
--socket=$SOCKET_PATH \ --socket=$SOCKET_PATH \
--socks5-server=localhost:1055 \ --socks5-server=localhost:1055 \
--tun=userspace-networking & --tun=userspace-networking \
2>&1 | logger -t "tailscale_script" &
TAILSCALED_PID=$! TAILSCALED_PID=$!
# Wait for tailscaled to initialize # Wait for tailscaled to initialize
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -4,9 +4,9 @@
"setup": { "setup": {
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"appName": "serverconfig", "appName": "serverconfig",
"vendor": "Tailscale - Packaged by Mo3he", "vendor": "Mo3he",
"embeddedSdkVersion": "3.0", "embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com", "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"runMode": "respawn", "runMode": "respawn",
"version": "1.96.4" "version": "1.96.4"
}, },
+3 -2
View File
@@ -17,13 +17,14 @@ logger -t "Tailscale_VPN" "Starting tailscaled daemon (userspace networking)"
--state="$STATE_DIR/tailscaled.state" \ --state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" \ --socket="$STATE_DIR/tailscaled.sock" \
--socks5-server=localhost:1055 \ --socks5-server=localhost:1055 \
--tun=userspace-networking & --tun=userspace-networking \
2>&1 | logger -t "Tailscale_VPN" &
TAILSCALED_PID=$! TAILSCALED_PID=$!
sleep 2 sleep 2
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)" logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up "$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up 2>&1 | logger -t "Tailscale_VPN"
logger -t "Tailscale_VPN" "Tailscale VPN is running" logger -t "Tailscale_VPN" "Tailscale VPN is running"
wait $TAILSCALED_PID wait $TAILSCALED_PID
+115 -14
View File
@@ -359,6 +359,9 @@
<div class="info-value" id="ts-version">-</div> <div class="info-value" id="ts-version">-</div>
</div> </div>
</div> </div>
<div style="margin-top:14px;text-align:right;">
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
</div>
</div> </div>
<!-- Logs --> <!-- Logs -->
@@ -421,6 +424,10 @@
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40; autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
}); });
// Cache helpers - survive syslog rotation
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
function parse(txt) { function parse(txt) {
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || []; var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null; var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
@@ -432,25 +439,77 @@
var m = last.match(/http:\/\/(100\.[\d.]+):/); var m = last.match(/http:\/\/(100\.[\d.]+):/);
if (m) tsIP = m[1]; if (m) tsIP = m[1];
} }
if (!tsIP) {
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
if (nmSelf) tsIP = nmSelf[1];
}
if (!tsIP) { if (!tsIP) {
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || []; var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null; tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
} }
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/); var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/); if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
var node = nodeMatch ? nodeMatch[1] : null; var node = null;
if (nodeMatches) {
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
if (nm) node = nm[1];
}
if (!node) {
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
if (hostLine) node = hostLine[1];
}
var loginMatch = txt.match(/active login:\s+(\S+)/); var loginMatches = txt.match(/active login:\s+\S+/g);
var tailnet = loginMatch ? loginMatch[1] : null; var tailnet = null;
if (loginMatches) {
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
if (lm) tailnet = lm[1];
}
if (!tailnet) {
// Fallback: extract from periodic netmap lines "u=user@email.com"
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
if (userMatches) {
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
if (um) tailnet = um[1];
}
}
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/); var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/); var version = null;
var version = versionMatch ? versionMatch[1] : null; if (versionMatches) {
var last = versionMatches[versionMatches.length - 1];
var vm = last.match(/v(\d+\.\d+\.\d+)/);
if (vm) version = vm[1];
}
if (!version) {
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
if (peersMatches) {
var lp = peersMatches[peersMatches.length - 1];
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
if (pm) version = pm[1];
}
}
// Cache when found, restore from cache when missing
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
tsIP = tsIP || cacheGet('ip');
node = node || cacheGet('node');
tailnet = tailnet || cacheGet('tailnet');
version = version || cacheGet('version');
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || []; var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : ''; var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt); var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
// Detect running even after syslog rotation (daemon active but startup lines gone)
if (!isRunning && !latestUrl) {
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
/derp-\d+ connected/.test(txt) ||
/c2n: GET/.test(txt) ||
/localapi:/.test(txt);
}
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version }; if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version }; if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
@@ -526,12 +585,38 @@
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2); ('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
} }
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
function checkAppRunning() {
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(xml) {
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
return m && m[1] === 'Running';
})
.catch(function() { return false; });
}
function refresh() { function refresh() {
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' }) fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); }) .then(function(r) { return r.text(); })
.then(function(txt) { .then(function(txt) {
render(parse(txt)); var result = parse(txt);
renderLogs(txt); renderLogs(txt);
if (result.state !== 'disconnected') {
render(result);
} else {
checkAppRunning().then(function(running) {
if (running) {
result.state = 'connected';
result.ip = result.ip || cacheGet('ip');
result.node = result.node || cacheGet('node');
result.tailnet = result.tailnet || cacheGet('tailnet');
result.version = result.version || cacheGet('version');
}
render(result);
});
}
}) })
.catch(function() { .catch(function() {
document.getElementById('status-text').textContent = 'Unable to fetch logs'; document.getElementById('status-text').textContent = 'Unable to fetch logs';
@@ -542,10 +627,15 @@
setInterval(refresh, 5000); setInterval(refresh, 5000);
// Check for updates from GitHub // Check for updates from GitHub
var latestChecked = false; var installedVersion = null;
function checkForUpdate(currentVersion) { var autoChecked = false;
if (!currentVersion || latestChecked) return; function checkForUpdate(currentVersion, manual) {
latestChecked = true; if (!currentVersion) return;
installedVersion = currentVersion;
if (!manual && autoChecked) return;
if (!manual) autoChecked = true;
var btn = document.getElementById('check-update-btn');
if (manual && btn) btn.textContent = 'Checking...';
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest') fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
@@ -555,11 +645,22 @@
document.getElementById('update-version').textContent = 'v' + tag; document.getElementById('update-version').textContent = 'v' + tag;
document.getElementById('update-banner').classList.add('visible'); document.getElementById('update-banner').classList.add('visible');
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)'; document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
if (btn) btn.textContent = 'Update Available';
} else {
if (manual && btn) btn.textContent = 'Up to date';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
} }
}) })
.catch(function() {}); .catch(function() {
if (manual && btn) btn.textContent = 'Check failed';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
});
} }
document.getElementById('check-update-btn').addEventListener('click', function() {
if (installedVersion) checkForUpdate(installedVersion, true);
});
function compareVersions(a, b) { function compareVersions(a, b) {
var pa = a.split('.').map(Number); var pa = a.split('.').map(Number);
var pb = b.split('.').map(Number); var pb = b.split('.').map(Number);
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -4,9 +4,9 @@
"setup": { "setup": {
"appName": "Tailscale_VPN", "appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Mo3he", "vendor": "Mo3he",
"embeddedSdkVersion": "3.0", "embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com", "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"runMode": "respawn", "runMode": "respawn",
"version": "1.96.4", "version": "1.96.4",
"architecture": "armv7hf" "architecture": "armv7hf"
+3 -2
View File
@@ -15,13 +15,14 @@ killall tailscaled 2>/dev/null || true
logger -t "Tailscale_VPN" "Starting tailscaled daemon" logger -t "Tailscale_VPN" "Starting tailscaled daemon"
"$APP_DIR/lib/tailscaled" \ "$APP_DIR/lib/tailscaled" \
--state="$STATE_DIR/tailscaled.state" \ --state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" & --socket="$STATE_DIR/tailscaled.sock" \
2>&1 | logger -t "Tailscale_VPN" &
TAILSCALED_PID=$! TAILSCALED_PID=$!
sleep 2 sleep 2
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)" logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes "$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes 2>&1 | logger -t "Tailscale_VPN"
logger -t "Tailscale_VPN" "Tailscale VPN is running" logger -t "Tailscale_VPN" "Tailscale VPN is running"
wait $TAILSCALED_PID wait $TAILSCALED_PID
+115 -14
View File
@@ -359,6 +359,9 @@
<div class="info-value" id="ts-version">-</div> <div class="info-value" id="ts-version">-</div>
</div> </div>
</div> </div>
<div style="margin-top:14px;text-align:right;">
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
</div>
</div> </div>
<!-- Logs --> <!-- Logs -->
@@ -421,6 +424,10 @@
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40; autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
}); });
// Cache helpers - survive syslog rotation
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
function parse(txt) { function parse(txt) {
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || []; var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null; var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
@@ -432,25 +439,77 @@
var m = last.match(/http:\/\/(100\.[\d.]+):/); var m = last.match(/http:\/\/(100\.[\d.]+):/);
if (m) tsIP = m[1]; if (m) tsIP = m[1];
} }
if (!tsIP) {
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
if (nmSelf) tsIP = nmSelf[1];
}
if (!tsIP) { if (!tsIP) {
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || []; var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null; tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
} }
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/); var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/); if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
var node = nodeMatch ? nodeMatch[1] : null; var node = null;
if (nodeMatches) {
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
if (nm) node = nm[1];
}
if (!node) {
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
if (hostLine) node = hostLine[1];
}
var loginMatch = txt.match(/active login:\s+(\S+)/); var loginMatches = txt.match(/active login:\s+\S+/g);
var tailnet = loginMatch ? loginMatch[1] : null; var tailnet = null;
if (loginMatches) {
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
if (lm) tailnet = lm[1];
}
if (!tailnet) {
// Fallback: extract from periodic netmap lines "u=user@email.com"
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
if (userMatches) {
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
if (um) tailnet = um[1];
}
}
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/); var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/); var version = null;
var version = versionMatch ? versionMatch[1] : null; if (versionMatches) {
var last = versionMatches[versionMatches.length - 1];
var vm = last.match(/v(\d+\.\d+\.\d+)/);
if (vm) version = vm[1];
}
if (!version) {
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
if (peersMatches) {
var lp = peersMatches[peersMatches.length - 1];
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
if (pm) version = pm[1];
}
}
// Cache when found, restore from cache when missing
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
tsIP = tsIP || cacheGet('ip');
node = node || cacheGet('node');
tailnet = tailnet || cacheGet('tailnet');
version = version || cacheGet('version');
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || []; var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : ''; var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt); var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
// Detect running even after syslog rotation (daemon active but startup lines gone)
if (!isRunning && !latestUrl) {
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
/derp-\d+ connected/.test(txt) ||
/c2n: GET/.test(txt) ||
/localapi:/.test(txt);
}
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version }; if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version }; if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
@@ -526,12 +585,38 @@
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2); ('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
} }
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
function checkAppRunning() {
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(xml) {
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
return m && m[1] === 'Running';
})
.catch(function() { return false; });
}
function refresh() { function refresh() {
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' }) fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); }) .then(function(r) { return r.text(); })
.then(function(txt) { .then(function(txt) {
render(parse(txt)); var result = parse(txt);
renderLogs(txt); renderLogs(txt);
if (result.state !== 'disconnected') {
render(result);
} else {
checkAppRunning().then(function(running) {
if (running) {
result.state = 'connected';
result.ip = result.ip || cacheGet('ip');
result.node = result.node || cacheGet('node');
result.tailnet = result.tailnet || cacheGet('tailnet');
result.version = result.version || cacheGet('version');
}
render(result);
});
}
}) })
.catch(function() { .catch(function() {
document.getElementById('status-text').textContent = 'Unable to fetch logs'; document.getElementById('status-text').textContent = 'Unable to fetch logs';
@@ -542,10 +627,15 @@
setInterval(refresh, 5000); setInterval(refresh, 5000);
// Check for updates from GitHub // Check for updates from GitHub
var latestChecked = false; var installedVersion = null;
function checkForUpdate(currentVersion) { var autoChecked = false;
if (!currentVersion || latestChecked) return; function checkForUpdate(currentVersion, manual) {
latestChecked = true; if (!currentVersion) return;
installedVersion = currentVersion;
if (!manual && autoChecked) return;
if (!manual) autoChecked = true;
var btn = document.getElementById('check-update-btn');
if (manual && btn) btn.textContent = 'Checking...';
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest') fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
@@ -555,11 +645,22 @@
document.getElementById('update-version').textContent = 'v' + tag; document.getElementById('update-version').textContent = 'v' + tag;
document.getElementById('update-banner').classList.add('visible'); document.getElementById('update-banner').classList.add('visible');
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)'; document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
if (btn) btn.textContent = 'Update Available';
} else {
if (manual && btn) btn.textContent = 'Up to date';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
} }
}) })
.catch(function() {}); .catch(function() {
if (manual && btn) btn.textContent = 'Check failed';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
});
} }
document.getElementById('check-update-btn').addEventListener('click', function() {
if (installedVersion) checkForUpdate(installedVersion, true);
});
function compareVersions(a, b) { function compareVersions(a, b) {
var pa = a.split('.').map(Number); var pa = a.split('.').map(Number);
var pb = b.split('.').map(Number); var pb = b.split('.').map(Number);
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -4,13 +4,13 @@
"setup": { "setup": {
"appName": "Tailscale_VPN", "appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Mo3he", "vendor": "Mo3he",
"embeddedSdkVersion": "3.0", "embeddedSdkVersion": "3.0",
"user": { "user": {
"group": "root", "group": "root",
"username": "root" "username": "root"
}, },
"vendorUrl": "https://www.tailscale.com", "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"runMode": "respawn", "runMode": "respawn",
"version": "1.96.4", "version": "1.96.4",
"architecture": "armv7hf" "architecture": "armv7hf"
+115 -14
View File
@@ -359,6 +359,9 @@
<div class="info-value" id="ts-version">-</div> <div class="info-value" id="ts-version">-</div>
</div> </div>
</div> </div>
<div style="margin-top:14px;text-align:right;">
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
</div>
</div> </div>
<!-- Logs --> <!-- Logs -->
@@ -421,6 +424,10 @@
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40; autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
}); });
// Cache helpers - survive syslog rotation
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
function parse(txt) { function parse(txt) {
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || []; var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null; var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
@@ -432,25 +439,77 @@
var m = last.match(/http:\/\/(100\.[\d.]+):/); var m = last.match(/http:\/\/(100\.[\d.]+):/);
if (m) tsIP = m[1]; if (m) tsIP = m[1];
} }
if (!tsIP) {
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
if (nmSelf) tsIP = nmSelf[1];
}
if (!tsIP) { if (!tsIP) {
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || []; var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null; tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
} }
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/); var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/); if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
var node = nodeMatch ? nodeMatch[1] : null; var node = null;
if (nodeMatches) {
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
if (nm) node = nm[1];
}
if (!node) {
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
if (hostLine) node = hostLine[1];
}
var loginMatch = txt.match(/active login:\s+(\S+)/); var loginMatches = txt.match(/active login:\s+\S+/g);
var tailnet = loginMatch ? loginMatch[1] : null; var tailnet = null;
if (loginMatches) {
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
if (lm) tailnet = lm[1];
}
if (!tailnet) {
// Fallback: extract from periodic netmap lines "u=user@email.com"
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
if (userMatches) {
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
if (um) tailnet = um[1];
}
}
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/); var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/); var version = null;
var version = versionMatch ? versionMatch[1] : null; if (versionMatches) {
var last = versionMatches[versionMatches.length - 1];
var vm = last.match(/v(\d+\.\d+\.\d+)/);
if (vm) version = vm[1];
}
if (!version) {
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
if (peersMatches) {
var lp = peersMatches[peersMatches.length - 1];
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
if (pm) version = pm[1];
}
}
// Cache when found, restore from cache when missing
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
tsIP = tsIP || cacheGet('ip');
node = node || cacheGet('node');
tailnet = tailnet || cacheGet('tailnet');
version = version || cacheGet('version');
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || []; var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : ''; var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt); var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
// Detect running even after syslog rotation (daemon active but startup lines gone)
if (!isRunning && !latestUrl) {
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
/derp-\d+ connected/.test(txt) ||
/c2n: GET/.test(txt) ||
/localapi:/.test(txt);
}
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version }; if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version }; if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
@@ -526,12 +585,38 @@
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2); ('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
} }
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
function checkAppRunning() {
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(xml) {
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
return m && m[1] === 'Running';
})
.catch(function() { return false; });
}
function refresh() { function refresh() {
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' }) fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); }) .then(function(r) { return r.text(); })
.then(function(txt) { .then(function(txt) {
render(parse(txt)); var result = parse(txt);
renderLogs(txt); renderLogs(txt);
if (result.state !== 'disconnected') {
render(result);
} else {
checkAppRunning().then(function(running) {
if (running) {
result.state = 'connected';
result.ip = result.ip || cacheGet('ip');
result.node = result.node || cacheGet('node');
result.tailnet = result.tailnet || cacheGet('tailnet');
result.version = result.version || cacheGet('version');
}
render(result);
});
}
}) })
.catch(function() { .catch(function() {
document.getElementById('status-text').textContent = 'Unable to fetch logs'; document.getElementById('status-text').textContent = 'Unable to fetch logs';
@@ -542,10 +627,15 @@
setInterval(refresh, 5000); setInterval(refresh, 5000);
// Check for updates from GitHub // Check for updates from GitHub
var latestChecked = false; var installedVersion = null;
function checkForUpdate(currentVersion) { var autoChecked = false;
if (!currentVersion || latestChecked) return; function checkForUpdate(currentVersion, manual) {
latestChecked = true; if (!currentVersion) return;
installedVersion = currentVersion;
if (!manual && autoChecked) return;
if (!manual) autoChecked = true;
var btn = document.getElementById('check-update-btn');
if (manual && btn) btn.textContent = 'Checking...';
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest') fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
@@ -555,11 +645,22 @@
document.getElementById('update-version').textContent = 'v' + tag; document.getElementById('update-version').textContent = 'v' + tag;
document.getElementById('update-banner').classList.add('visible'); document.getElementById('update-banner').classList.add('visible');
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)'; document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
if (btn) btn.textContent = 'Update Available';
} else {
if (manual && btn) btn.textContent = 'Up to date';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
} }
}) })
.catch(function() {}); .catch(function() {
if (manual && btn) btn.textContent = 'Check failed';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
});
} }
document.getElementById('check-update-btn').addEventListener('click', function() {
if (installedVersion) checkForUpdate(installedVersion, true);
});
function compareVersions(a, b) { function compareVersions(a, b) {
var pa = a.split('.').map(Number); var pa = a.split('.').map(Number);
var pb = b.split('.').map(Number); var pb = b.split('.').map(Number);
+2 -1
View File
@@ -42,7 +42,8 @@ $TAILSCALED_PATH \
--state="$STATE_DIR/tailscaled.state" \ --state="$STATE_DIR/tailscaled.state" \
--socket=$SOCKET_PATH \ --socket=$SOCKET_PATH \
--socks5-server=localhost:1055 \ --socks5-server=localhost:1055 \
--tun=userspace-networking & --tun=userspace-networking \
2>&1 | logger -t "tailscale_script" &
TAILSCALED_PID=$! TAILSCALED_PID=$!
# Wait for tailscaled to initialize # Wait for tailscaled to initialize
+2 -2
View File
@@ -4,9 +4,9 @@
"setup": { "setup": {
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"appName": "serverconfig", "appName": "serverconfig",
"vendor": "Tailscale - Packaged by Mo3he", "vendor": "Mo3he",
"embeddedSdkVersion": "3.0", "embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com", "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"runMode": "respawn", "runMode": "respawn",
"version": "1.96.4", "version": "1.96.4",
"architecture": "armv7hf" "architecture": "armv7hf"
+48 -6
View File
@@ -358,6 +358,16 @@
</div> </div>
</section> </section>
<!-- Disclaimer -->
<div style="max-width:720px;margin:0 auto;padding:0 1.5rem 1.5rem;">
<div style="background:rgba(79,143,247,0.07);border:1px solid rgba(79,143,247,0.18);border-radius:10px;padding:12px 18px;font-size:12.5px;color:var(--muted);line-height:1.6;">
<strong style="color:var(--text);">Disclaimer:</strong>
This is an independent, community-developed ACAP package and is <strong style="color:var(--text);">not an official Axis Communications product</strong>.
It was developed entirely on personal time and is not affiliated with, endorsed by, or supported by Axis Communications AB.
Use at your own risk. For official Axis software, visit <a href="https://www.axis.com" target="_blank" rel="noopener">axis.com</a>.
</div>
</div>
<!-- Features --> <!-- Features -->
<section id="features" class="features"> <section id="features" class="features">
<div class="feature-card"> <div class="feature-card">
@@ -403,11 +413,11 @@
<h3>Standard (Non-Root)</h3> <h3>Standard (Non-Root)</h3>
<p>For Axis OS 12 and newer. Runs in user-space networking mode without root privileges.</p> <p>For Axis OS 12 and newer. Runs in user-space networking mode without root privileges.</p>
<div class="arch-buttons"> <div class="arch-buttons">
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener"> <a class="arch-btn" data-asset="Tailscale_VPN" data-arch="aarch64" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
AARCH64 AARCH64
</a> </a>
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener"> <a class="arch-btn" data-asset="Tailscale_VPN" data-arch="armv7hf" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
ARM ARM
</a> </a>
@@ -419,11 +429,11 @@
<h3>Custom Server</h3> <h3>Custom Server</h3>
<p>Supports custom Tailscale control servers and auth keys. Ideal for self-hosted Headscale setups.</p> <p>Supports custom Tailscale control servers and auth keys. Ideal for self-hosted Headscale setups.</p>
<div class="arch-buttons"> <div class="arch-buttons">
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener"> <a class="arch-btn" data-asset="custom" data-arch="aarch64" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
AARCH64 AARCH64
</a> </a>
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener"> <a class="arch-btn" data-asset="custom" data-arch="armv7hf" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
ARM ARM
</a> </a>
@@ -435,11 +445,11 @@
<h3>Root (Full Networking)</h3> <h3>Root (Full Networking)</h3>
<p>For Axis OS older than 12. Requires root access but provides full Tailscale networking features.</p> <p>For Axis OS older than 12. Requires root access but provides full Tailscale networking features.</p>
<div class="arch-buttons"> <div class="arch-buttons">
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener"> <a class="arch-btn" data-asset="ROOT" data-arch="aarch64" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
AARCH64 AARCH64
</a> </a>
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener"> <a class="arch-btn" data-asset="ROOT" data-arch="armv7hf" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
ARM ARM
</a> </a>
@@ -486,6 +496,7 @@
<a href="https://www.axis.com/" target="_blank" rel="noopener">Axis</a> <a href="https://www.axis.com/" target="_blank" rel="noopener">Axis</a>
</div> </div>
<p>&copy; 2025 Mo3he &middot; MIT License</p> <p>&copy; 2025 Mo3he &middot; MIT License</p>
<p style="font-size:0.75rem;opacity:0.5;margin-top:0.5rem;">Tailscale is a product of Tailscale Inc. This is an independent, unofficial package that redistributes Tailscale binaries under the <a href="https://github.com/Mo3he/Axis_Cam_Tailscale/blob/main/LICENSE" target="_blank" rel="noopener" style="color:inherit;">BSD 3-Clause License</a>. Not affiliated with or endorsed by Tailscale Inc. or Axis Communications AB.</p>
</footer> </footer>
<script> <script>
@@ -526,6 +537,37 @@
applyTheme(e.matches ? 'light' : 'dark'); applyTheme(e.matches ? 'light' : 'dark');
} }
}); });
// Rewrite download links to point directly to latest release assets
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
.then(function(r) { return r.json(); })
.then(function(data) {
var assets = data.assets || [];
var buttons = document.querySelectorAll('a[data-asset]');
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
var type = btn.getAttribute('data-asset');
var arch = btn.getAttribute('data-arch');
for (var j = 0; j < assets.length; j++) {
var name = assets[j].name.toLowerCase();
var archMatch = name.indexOf(arch) !== -1;
var typeMatch = false;
if (type === 'Tailscale_VPN') {
typeMatch = name.indexOf('custom') === -1 && name.indexOf('root') === -1;
} else if (type === 'custom') {
typeMatch = name.indexOf('custom') !== -1;
} else if (type === 'ROOT') {
typeMatch = name.indexOf('root') !== -1;
}
if (archMatch && typeMatch) {
btn.href = assets[j].browser_download_url;
btn.removeAttribute('target');
break;
}
}
}
})
.catch(function() {});
})(); })();
</script> </script>
</body> </body>