Compare commits

...
16 Commits
Author SHA1 Message Date
github-actions[bot] 5b09a40231 Update Tailscale to v1.96.4 2026-03-28 02:52:43 +00:00
Weston BliedenandGitHub 98918ca6f5 Update README.md 2026-03-26 10:18:16 +01:00
Weston Blieden 08b0fdc081 Create LICENSE 2026-03-19 16:39:22 +01:00
Weston Blieden f3c6dd54f3 Update build.yml 2026-03-19 16:26:26 +01:00
Weston Blieden c3cbf8d433 Improve all variants: new UI, respawn mode, arm_custom, binary gitignore 2026-03-19 16:23:44 +01:00
github-actions[bot] 39a5a42def Update Tailscale to v1.96.2 2026-03-19 02:55:58 +00:00
github-actions[bot] 713550d1c0 Update Tailscale to v1.94.2 2026-02-26 02:46:36 +00:00
github-actions[bot] 14db783425 Update Tailscale to v1.94.1 2026-01-28 02:26:05 +00:00
github-actions[bot] c02f0aa498 Update Tailscale to v1.92.5 2026-01-07 02:22:01 +00:00
github-actions[bot] 96477cdb0d Update Tailscale to v1.92.3 2025-12-17 08:38:44 +00:00
Weston BliedenandGitHub 4e0eabeec3 Update version in manifest.json to 1.92.1 2025-12-17 09:36:08 +01:00
Weston BliedenandGitHub 59161ccd91 Downgrade version from 1.92.3 to 1.92.1 2025-12-17 09:35:26 +01:00
Weston BliedenandGitHub e1b2ac3490 Downgrade version from 1.92.3 to 1.92.1 2025-12-17 09:35:04 +01:00
Weston BliedenandGitHub 5f03ac918f Update version in manifest.json to 1.92.1 2025-12-17 09:34:45 +01:00
Weston BliedenandGitHub 2db9f27181 Update version in manifest.json to 1.92.1 2025-12-17 09:34:26 +01:00
Weston Blieden 396b450415 Store state in localdata
The tailscaled.state file in localdata will now persist across updates, so your camera stays connected to Tailscale without re-authentication
2025-12-17 09:31:34 +01:00
42 changed files with 2137 additions and 129 deletions
+5 -4
View File
@@ -57,8 +57,12 @@ jobs:
run: |
echo "Repo version: $CURRENT_VERSION"
echo "Latest Tailscale version: $RELEASE_VERSION"
echo "Trigger: ${{ github.event_name }}"
if [ "$CURRENT_VERSION" = "$RELEASE_VERSION" ]; then
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "build_needed=true" >> $GITHUB_ENV
echo "Manual trigger — building regardless of version."
elif [ "$CURRENT_VERSION" = "$RELEASE_VERSION" ]; then
echo "build_needed=false" >> $GITHUB_ENV
echo "Already up to date. Skipping build."
else
@@ -103,9 +107,6 @@ jobs:
cp tailscale_bins/tailscaled_arm64 "$folder/app/lib/tailscaled"
fi
# Stage updated binaries for commit
git add "$folder/app/lib/tailscale" "$folder/app/lib/tailscaled"
# Detect variant suffix for .eap naming
if [[ "$FOLDER_NAME" == *_ROOT ]]; then
VARIANT="_root"
+4
View File
@@ -8,3 +8,7 @@ build/
# Do not track downloaded Tailscale tarballs and temp bins
tailscale_bins/
*.tgz
# Tailscale binaries - downloaded fresh by CI at build time, not stored in git
*/app/lib/tailscale
*/app/lib/tailscaled
+3 -1
View File
@@ -9,7 +9,9 @@ This repository contains an **ACAP package** that installs the [Tailscale VPN cl
[![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)
[![Sponsor](https://img.shields.io/badge/sponsor-%E2%9D%A4-lightgrey?logo=github)](https://github.com/sponsors/Mo3he)
![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)
---
+28 -8
View File
@@ -1,10 +1,30 @@
#!/bin/sh
echo "Starting Service"
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
/usr/local/packages/Tailscale_VPN/lib/tailscaled --socks5-server=localhost:1055 --tun=userspace-networking --socket=/usr/local/packages/Tailscale_VPN/tailscaled.sock &
echo "Service Started"
echo "Scroll to Bottom for link"
/usr/local/packages/Tailscale_VPN/lib/tailscale --socket=/usr/local/packages/Tailscale_VPN/tailscaled.sock up
wait
APP_DIR="/usr/local/packages/Tailscale_VPN"
STATE_DIR="$APP_DIR/localdata"
logger -t "Tailscale_VPN" "Starting Tailscale VPN service"
mkdir -p "$STATE_DIR"
chmod 755 "$APP_DIR/lib/tailscale"
chmod 755 "$APP_DIR/lib/tailscaled"
# Kill any leftover daemon from a previous run
killall tailscaled 2>/dev/null || true
logger -t "Tailscale_VPN" "Starting tailscaled daemon (userspace networking)"
"$APP_DIR/lib/tailscaled" \
--state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" \
--socks5-server=localhost:1055 \
--tun=userspace-networking &
TAILSCALED_PID=$!
sleep 2
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
logger -t "Tailscale_VPN" "Tailscale VPN is running"
wait $TAILSCALED_PID
logger -t "Tailscale_VPN" "tailscaled exited"
+252 -13
View File
@@ -1,19 +1,258 @@
<!DOCTYPE html>
<html>
<body>
<html lang="en">
<head>
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
<script>
function refreshIFrame() {
var x = document.getElementById("*Your_iframe_id*");
x.contentWindow.location.reload();
var t = setTimeout(refreshIFrame, 5000);
<meta charset="UTF-8">
<title>Tailscale VPN</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
background: #f5f6f8;
color: #1a1a2e;
padding: 20px;
font-size: 14px;
}
</script>
header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
header svg { flex-shrink: 0; }
header h1 {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
}
.card {
background: #fff;
border-radius: 10px;
padding: 18px 20px;
margin-bottom: 14px;
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
}
/* ── Status row ── */
.status-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
.status-label {
font-size: 15px;
font-weight: 600;
}
/* ── Auth URL block ── */
#auth-block {
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 14px 16px;
}
#auth-block p {
font-size: 12px;
color: #92400e;
margin-bottom: 10px;
font-weight: 500;
}
#auth-link {
display: inline-block;
background: #0166ff;
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 13px;
padding: 8px 16px;
border-radius: 6px;
margin-bottom: 10px;
}
#auth-link:hover { background: #0052cc; }
#auth-url-text {
display: block;
font-size: 11px;
color: #888;
word-break: break-all;
font-family: monospace;
}
/* ── Info grid (connected state) ── */
.info-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 6px 16px;
font-size: 13px;
}
.info-grid .label { color: #888; }
.info-grid .value { font-weight: 500; font-family: monospace; }
/* ── Log section ── */
.log-header {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #999;
margin-bottom: 10px;
}
#log-frame {
width: 100%;
height: 500px;
border: 1px solid #e8e8ec;
border-radius: 6px;
display: block;
}
</style>
</head>
<body onload="refreshIFrame()">
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
</body>
<body>
<header>
<!-- Tailscale wordmark icon -->
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="28" fill="#0166FF"/>
<circle cx="64" cy="64" r="18" fill="white"/>
<circle cx="64" cy="22" r="10" fill="white"/>
<circle cx="64" cy="106" r="10" fill="white"/>
<circle cx="22" cy="64" r="10" fill="white"/>
<circle cx="106" cy="64" r="10" fill="white"/>
<circle cx="35" cy="35" r="10" fill="white"/>
<circle cx="93" cy="35" r="10" fill="white"/>
<circle cx="35" cy="93" r="10" fill="white"/>
<circle cx="93" cy="93" r="10" fill="white"/>
</svg>
<h1>Tailscale VPN</h1>
</header>
<!-- Status card -->
<div class="card">
<div class="status-row">
<span id="dot" class="dot connecting"></span>
<span id="status-label" class="status-label">Checking&hellip;</span>
</div>
<!-- Shown when waiting for auth -->
<div id="auth-block" style="display:none;">
<p>Open this link in a browser to authenticate this device:</p>
<a id="auth-link" href="#" target="_blank">Authenticate &rarr;</a>
<span id="auth-url-text"></span>
</div>
<!-- Shown when connected -->
<div id="info-block" style="display:none;" class="info-grid">
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">&mdash;</span>
<span class="label">Node</span><span class="value" id="ts-node">&mdash;</span>
</div>
</div>
<!-- Log card -->
<div class="card">
<div class="log-header">Service Log</div>
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN"></iframe>
</div>
<script>
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN';
function parse(txt) {
// Use the LAST occurrence of each pattern so stale log entries don't win
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
// Tailscale IP — 100.x.x.x range
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
// Node name from "Logged into tailnet" line or "acap-" username pattern
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
var node = nodeMatch ? nodeMatch[1] : null;
// Determine state from the LAST state-transition log line
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
if (isRunning) {
return { state: 'connected', url: null, ip: tsIP, node: node };
}
if (latestUrl) {
return { state: 'connecting', url: latestUrl, ip: null, node: null };
}
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
return { state: 'connecting', url: null, ip: null, node: null };
}
return { state: 'disconnected', url: null, ip: null, node: null };
}
function render(r) {
var dot = document.getElementById('dot');
var label = document.getElementById('status-label');
var auth = document.getElementById('auth-block');
var info = document.getElementById('info-block');
dot.className = 'dot ' + r.state;
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
label.textContent = labels[r.state];
// Auth block
if (r.state === 'connecting' && r.url) {
document.getElementById('auth-link').href = r.url;
document.getElementById('auth-url-text').textContent = r.url;
auth.style.display = '';
} else {
auth.style.display = 'none';
}
// Info block
if (r.state === 'connected') {
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
document.getElementById('ts-node').textContent = r.node || '\u2014';
info.style.display = '';
} else {
info.style.display = 'none';
}
}
function refresh() {
// Reload the log iframe (same-origin)
var frame = document.getElementById('log-frame');
try { frame.contentWindow.location.reload(); } catch(e) {}
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(txt) { render(parse(txt)); })
.catch(function() {
document.getElementById('status-label').textContent = 'Unknown';
});
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>
</html>
View File
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -7,12 +7,12 @@
"vendor": "Tailscale - Packaged by Mo3he",
"embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"version": "1.92.3",
"runMode": "respawn",
"version": "1.96.4",
"architecture": "aarch64"
},
"configuration": {
"settingPage": "index.html"
}
}
}
}
+26 -8
View File
@@ -1,10 +1,28 @@
#!/bin/sh
echo "Starting Service"
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
/usr/local/packages/Tailscale_VPN/lib/tailscaled &
echo "Service Started"
echo "Scroll to Bottom for link"
/usr/local/packages/Tailscale_VPN/lib/tailscale up --accept-routes
wait
APP_DIR="/usr/local/packages/Tailscale_VPN"
STATE_DIR="$APP_DIR/localdata"
logger -t "Tailscale_VPN" "Starting Tailscale VPN service (root mode)"
mkdir -p "$STATE_DIR"
chmod 755 "$APP_DIR/lib/tailscale"
chmod 755 "$APP_DIR/lib/tailscaled"
# Kill any leftover daemon from a previous run
killall tailscaled 2>/dev/null || true
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
"$APP_DIR/lib/tailscaled" \
--state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" &
TAILSCALED_PID=$!
sleep 2
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
logger -t "Tailscale_VPN" "Tailscale VPN is running"
wait $TAILSCALED_PID
logger -t "Tailscale_VPN" "tailscaled exited"
+252 -13
View File
@@ -1,19 +1,258 @@
<!DOCTYPE html>
<html>
<body>
<html lang="en">
<head>
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
<script>
function refreshIFrame() {
var x = document.getElementById("*Your_iframe_id*");
x.contentWindow.location.reload();
var t = setTimeout(refreshIFrame, 5000);
<meta charset="UTF-8">
<title>Tailscale VPN</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
background: #f5f6f8;
color: #1a1a2e;
padding: 20px;
font-size: 14px;
}
</script>
header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
header svg { flex-shrink: 0; }
header h1 {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
}
.card {
background: #fff;
border-radius: 10px;
padding: 18px 20px;
margin-bottom: 14px;
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
}
/* ── Status row ── */
.status-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
.status-label {
font-size: 15px;
font-weight: 600;
}
/* ── Auth URL block ── */
#auth-block {
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 14px 16px;
}
#auth-block p {
font-size: 12px;
color: #92400e;
margin-bottom: 10px;
font-weight: 500;
}
#auth-link {
display: inline-block;
background: #0166ff;
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 13px;
padding: 8px 16px;
border-radius: 6px;
margin-bottom: 10px;
}
#auth-link:hover { background: #0052cc; }
#auth-url-text {
display: block;
font-size: 11px;
color: #888;
word-break: break-all;
font-family: monospace;
}
/* ── Info grid (connected state) ── */
.info-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 6px 16px;
font-size: 13px;
}
.info-grid .label { color: #888; }
.info-grid .value { font-weight: 500; font-family: monospace; }
/* ── Log section ── */
.log-header {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #999;
margin-bottom: 10px;
}
#log-frame {
width: 100%;
height: 500px;
border: 1px solid #e8e8ec;
border-radius: 6px;
display: block;
}
</style>
</head>
<body onload="refreshIFrame()">
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
</body>
<body>
<header>
<!-- Tailscale wordmark icon -->
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="28" fill="#0166FF"/>
<circle cx="64" cy="64" r="18" fill="white"/>
<circle cx="64" cy="22" r="10" fill="white"/>
<circle cx="64" cy="106" r="10" fill="white"/>
<circle cx="22" cy="64" r="10" fill="white"/>
<circle cx="106" cy="64" r="10" fill="white"/>
<circle cx="35" cy="35" r="10" fill="white"/>
<circle cx="93" cy="35" r="10" fill="white"/>
<circle cx="35" cy="93" r="10" fill="white"/>
<circle cx="93" cy="93" r="10" fill="white"/>
</svg>
<h1>Tailscale VPN</h1>
</header>
<!-- Status card -->
<div class="card">
<div class="status-row">
<span id="dot" class="dot connecting"></span>
<span id="status-label" class="status-label">Checking&hellip;</span>
</div>
<!-- Shown when waiting for auth -->
<div id="auth-block" style="display:none;">
<p>Open this link in a browser to authenticate this device:</p>
<a id="auth-link" href="#" target="_blank">Authenticate &rarr;</a>
<span id="auth-url-text"></span>
</div>
<!-- Shown when connected -->
<div id="info-block" style="display:none;" class="info-grid">
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">&mdash;</span>
<span class="label">Node</span><span class="value" id="ts-node">&mdash;</span>
</div>
</div>
<!-- Log card -->
<div class="card">
<div class="log-header">Service Log</div>
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN"></iframe>
</div>
<script>
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN';
function parse(txt) {
// Use the LAST occurrence of each pattern so stale log entries don't win
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
// Tailscale IP — 100.x.x.x range
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
// Node name from "Logged into tailnet" line or "acap-" username pattern
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
var node = nodeMatch ? nodeMatch[1] : null;
// Determine state from the LAST state-transition log line
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
if (isRunning) {
return { state: 'connected', url: null, ip: tsIP, node: node };
}
if (latestUrl) {
return { state: 'connecting', url: latestUrl, ip: null, node: null };
}
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
return { state: 'connecting', url: null, ip: null, node: null };
}
return { state: 'disconnected', url: null, ip: null, node: null };
}
function render(r) {
var dot = document.getElementById('dot');
var label = document.getElementById('status-label');
var auth = document.getElementById('auth-block');
var info = document.getElementById('info-block');
dot.className = 'dot ' + r.state;
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
label.textContent = labels[r.state];
// Auth block
if (r.state === 'connecting' && r.url) {
document.getElementById('auth-link').href = r.url;
document.getElementById('auth-url-text').textContent = r.url;
auth.style.display = '';
} else {
auth.style.display = 'none';
}
// Info block
if (r.state === 'connected') {
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
document.getElementById('ts-node').textContent = r.node || '\u2014';
info.style.display = '';
} else {
info.style.display = 'none';
}
}
function refresh() {
// Reload the log iframe (same-origin)
var frame = document.getElementById('log-frame');
try { frame.contentWindow.location.reload(); } catch(e) {}
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(txt) { render(parse(txt)); })
.catch(function() {
document.getElementById('status-label').textContent = 'Unknown';
});
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>
</html>
View File
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -11,12 +11,12 @@
"username": "root"
},
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"version": "1.92.3",
"runMode": "respawn",
"version": "1.96.4",
"architecture": "aarch64"
},
"configuration": {
"settingPage": "index.html"
}
}
}
}
+27 -6
View File
@@ -17,7 +17,9 @@
#include <errno.h>
#define APP_NAME "serverconfig"
#define CONFIG_FILE "/usr/local/packages/serverconfig/config.txt"
#define APP_DIR "/usr/local/packages/serverconfig"
#define STATE_DIR APP_DIR "/localdata"
#define CONFIG_FILE STATE_DIR "/config.txt"
#define SCRIPT_PATH "/usr/local/packages/serverconfig/start_tailscale.sh"
#define SCRIPT_SOURCE "/usr/local/packages/serverconfig/lib/start_tailscale.sh"
@@ -27,6 +29,19 @@ static gboolean signal_handler(gpointer loop) {
return G_SOURCE_REMOVE;
}
// Create localdata directory
static void ensure_localdata_exists(void) {
struct stat st = {0};
if (stat(STATE_DIR, &st) == -1) {
if (mkdir(STATE_DIR, 0755) != 0) {
syslog(LOG_ERR, "Failed to create localdata directory: %s", strerror(errno));
} else {
syslog(LOG_INFO, "Created localdata directory: %s", STATE_DIR);
}
}
}
// Copy script from lib folder to main directory
static void copy_script_file(void) {
char buffer[4096];
@@ -109,6 +124,9 @@ static void update_config_file(AXParameter* handle) {
gchar* key_value = NULL;
FILE* file;
// Ensure localdata directory exists
ensure_localdata_exists();
// Get parameter values
if (!ax_parameter_get(handle, "CustomServer", &server_value, &error)) {
syslog(LOG_ERR, "Failed to get CustomServer: %s",
@@ -125,7 +143,7 @@ static void update_config_file(AXParameter* handle) {
key_value = g_strdup("");
}
// Write to config file
// Write to config file in localdata
file = fopen(CONFIG_FILE, "w");
if (file) {
fprintf(file, "custom_server=%s\n", server_value ? server_value : "");
@@ -135,12 +153,12 @@ static void update_config_file(AXParameter* handle) {
// Set permissions to ensure the file is readable
chmod(CONFIG_FILE, 0644);
syslog(LOG_INFO, "Updated configuration file: custom_server=%s",
syslog(LOG_INFO, "Updated configuration file in local custom_server=%s",
server_value ? server_value : "");
syslog(LOG_INFO, "Updated configuration file: auth_key=%s",
syslog(LOG_INFO, "Updated configuration file in local auth_key=%s",
key_value && strlen(key_value) > 0 ? "(set)" : "(empty)");
} else {
syslog(LOG_ERR, "Failed to open config file for writing");
syslog(LOG_ERR, "Failed to open config file for writing: %s", strerror(errno));
}
// Clean up
@@ -185,6 +203,9 @@ int main(void) {
exit(1);
}
// Ensure localdata directory exists
ensure_localdata_exists();
// Ensure script is copied from lib folder
copy_script_file();
@@ -229,4 +250,4 @@ int main(void) {
ax_parameter_free(handle);
return 0;
}
}
+252 -13
View File
@@ -1,19 +1,258 @@
<!DOCTYPE html>
<html>
<body>
<html lang="en">
<head>
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
<script>
function refreshIFrame() {
var x = document.getElementById("*Your_iframe_id*");
x.contentWindow.location.reload();
var t = setTimeout(refreshIFrame, 5000);
<meta charset="UTF-8">
<title>Tailscale VPN</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
background: #f5f6f8;
color: #1a1a2e;
padding: 20px;
font-size: 14px;
}
</script>
header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
header svg { flex-shrink: 0; }
header h1 {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
}
.card {
background: #fff;
border-radius: 10px;
padding: 18px 20px;
margin-bottom: 14px;
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
}
/* ── Status row ── */
.status-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
.status-label {
font-size: 15px;
font-weight: 600;
}
/* ── Auth URL block ── */
#auth-block {
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 14px 16px;
}
#auth-block p {
font-size: 12px;
color: #92400e;
margin-bottom: 10px;
font-weight: 500;
}
#auth-link {
display: inline-block;
background: #0166ff;
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 13px;
padding: 8px 16px;
border-radius: 6px;
margin-bottom: 10px;
}
#auth-link:hover { background: #0052cc; }
#auth-url-text {
display: block;
font-size: 11px;
color: #888;
word-break: break-all;
font-family: monospace;
}
/* ── Info grid (connected state) ── */
.info-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 6px 16px;
font-size: 13px;
}
.info-grid .label { color: #888; }
.info-grid .value { font-weight: 500; font-family: monospace; }
/* ── Log section ── */
.log-header {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #999;
margin-bottom: 10px;
}
#log-frame {
width: 100%;
height: 500px;
border: 1px solid #e8e8ec;
border-radius: 6px;
display: block;
}
</style>
</head>
<body onload="refreshIFrame()">
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=serverconfig" style="height:600px;width:100%;border: none;" ></iframe>
</body>
<body>
<header>
<!-- Tailscale wordmark icon -->
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="28" fill="#0166FF"/>
<circle cx="64" cy="64" r="18" fill="white"/>
<circle cx="64" cy="22" r="10" fill="white"/>
<circle cx="64" cy="106" r="10" fill="white"/>
<circle cx="22" cy="64" r="10" fill="white"/>
<circle cx="106" cy="64" r="10" fill="white"/>
<circle cx="35" cy="35" r="10" fill="white"/>
<circle cx="93" cy="35" r="10" fill="white"/>
<circle cx="35" cy="93" r="10" fill="white"/>
<circle cx="93" cy="93" r="10" fill="white"/>
</svg>
<h1>Tailscale VPN</h1>
</header>
<!-- Status card -->
<div class="card">
<div class="status-row">
<span id="dot" class="dot connecting"></span>
<span id="status-label" class="status-label">Checking&hellip;</span>
</div>
<!-- Shown when waiting for auth -->
<div id="auth-block" style="display:none;">
<p>Open this link in a browser to authenticate this device:</p>
<a id="auth-link" href="#" target="_blank">Authenticate &rarr;</a>
<span id="auth-url-text"></span>
</div>
<!-- Shown when connected -->
<div id="info-block" style="display:none;" class="info-grid">
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">&mdash;</span>
<span class="label">Node</span><span class="value" id="ts-node">&mdash;</span>
</div>
</div>
<!-- Log card -->
<div class="card">
<div class="log-header">Service Log</div>
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=serverconfig"></iframe>
</div>
<script>
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=serverconfig';
function parse(txt) {
// Use the LAST occurrence of each pattern so stale log entries don't win
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
// Tailscale IP — 100.x.x.x range
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
// Node name from "Logged into tailnet" line or "acap-" username pattern
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
var node = nodeMatch ? nodeMatch[1] : null;
// Determine state from the LAST state-transition log line
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
if (isRunning) {
return { state: 'connected', url: null, ip: tsIP, node: node };
}
if (latestUrl) {
return { state: 'connecting', url: latestUrl, ip: null, node: null };
}
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
return { state: 'connecting', url: null, ip: null, node: null };
}
return { state: 'disconnected', url: null, ip: null, node: null };
}
function render(r) {
var dot = document.getElementById('dot');
var label = document.getElementById('status-label');
var auth = document.getElementById('auth-block');
var info = document.getElementById('info-block');
dot.className = 'dot ' + r.state;
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
label.textContent = labels[r.state];
// Auth block
if (r.state === 'connecting' && r.url) {
document.getElementById('auth-link').href = r.url;
document.getElementById('auth-url-text').textContent = r.url;
auth.style.display = '';
} else {
auth.style.display = 'none';
}
// Info block
if (r.state === 'connected') {
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
document.getElementById('ts-node').textContent = r.node || '\u2014';
info.style.display = '';
} else {
info.style.display = 'none';
}
}
function refresh() {
// Reload the log iframe (same-origin)
var frame = document.getElementById('log-frame');
try { frame.contentWindow.location.reload(); } catch(e) {}
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(txt) { render(parse(txt)); })
.catch(function() {
document.getElementById('status-label').textContent = 'Unknown';
});
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>
</html>
View File
+24 -7
View File
@@ -2,13 +2,18 @@
# Make sure this script terminates any existing Tailscale processes before starting new ones
# Kill any existing tailscaled processes
pkill -f tailscaled || true
killall tailscaled 2>/dev/null || true
# Simple script to start Tailscale with custom configuration
CONFIG_FILE="/usr/local/packages/serverconfig/config.txt"
TAILSCALED_PATH="/usr/local/packages/serverconfig/lib/tailscaled"
TAILSCALE_PATH="/usr/local/packages/serverconfig/lib/tailscale"
SOCKET_PATH="/usr/local/packages/serverconfig/tailscaled.sock"
APP_DIR="/usr/local/packages/serverconfig"
STATE_DIR="$APP_DIR/localdata"
CONFIG_FILE="$STATE_DIR/config.txt"
TAILSCALED_PATH="$APP_DIR/lib/tailscaled"
TAILSCALE_PATH="$APP_DIR/lib/tailscale"
SOCKET_PATH="$STATE_DIR/tailscaled.sock"
# Create localdata directory if it doesn't exist
mkdir -p "$STATE_DIR"
# Log to syslog
logger -t "tailscale_script" "Starting Tailscale VPN service"
@@ -31,9 +36,13 @@ if [ -f "$CONFIG_FILE" ]; then
done < "$CONFIG_FILE"
fi
# Start tailscaled
# Start tailscaled with state stored in localdata
logger -t "tailscale_script" "Starting tailscaled daemon"
$TAILSCALED_PATH --socks5-server=localhost:1055 --tun=userspace-networking --socket=$SOCKET_PATH &
$TAILSCALED_PATH \
--state="$STATE_DIR/tailscaled.state" \
--socket=$SOCKET_PATH \
--socks5-server=localhost:1055 \
--tun=userspace-networking &
TAILSCALED_PID=$!
# Wait for tailscaled to initialize
@@ -55,6 +64,14 @@ fi
# Connect to Tailscale network
logger -t "tailscale_script" "Running: $TAILSCALE_CMD"
eval $TAILSCALE_CMD
UP_EXIT=$?
# Clear auth key from config after first use — Tailscale auth keys are single-use
# and the node identity is persisted in tailscaled.state, so the key is no longer needed.
if [ -n "$AUTH_KEY" ] && [ "$UP_EXIT" -eq 0 ] && [ -f "$CONFIG_FILE" ]; then
logger -t "tailscale_script" "Clearing auth key from config after successful authentication"
printf 'custom_server=%s\nauth_key=\n' "$CUSTOM_SERVER" > "$CONFIG_FILE"
fi
# Keep the script running to maintain the tailscaled process
logger -t "tailscale_script" "Tailscale VPN is running"
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -8,7 +8,7 @@
"embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com",
"runMode": "respawn",
"version": "1.92.3"
"version": "1.96.4"
},
"configuration": {
"settingPage": "index.html",
@@ -26,4 +26,4 @@
]
}
}
}
}
+28 -8
View File
@@ -1,10 +1,30 @@
#!/bin/sh
echo "Starting Service"
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
/usr/local/packages/Tailscale_VPN/lib/tailscaled --socks5-server=localhost:1055 --tun=userspace-networking --socket=/usr/local/packages/Tailscale_VPN/tailscaled.sock &
echo "Service Started"
echo "Scroll to Bottom for link"
/usr/local/packages/Tailscale_VPN/lib/tailscale --socket=/usr/local/packages/Tailscale_VPN/tailscaled.sock up
wait
APP_DIR="/usr/local/packages/Tailscale_VPN"
STATE_DIR="$APP_DIR/localdata"
logger -t "Tailscale_VPN" "Starting Tailscale VPN service"
mkdir -p "$STATE_DIR"
chmod 755 "$APP_DIR/lib/tailscale"
chmod 755 "$APP_DIR/lib/tailscaled"
# Kill any leftover daemon from a previous run
killall tailscaled 2>/dev/null || true
logger -t "Tailscale_VPN" "Starting tailscaled daemon (userspace networking)"
"$APP_DIR/lib/tailscaled" \
--state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" \
--socks5-server=localhost:1055 \
--tun=userspace-networking &
TAILSCALED_PID=$!
sleep 2
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
logger -t "Tailscale_VPN" "Tailscale VPN is running"
wait $TAILSCALED_PID
logger -t "Tailscale_VPN" "tailscaled exited"
+252 -13
View File
@@ -1,19 +1,258 @@
<!DOCTYPE html>
<html>
<body>
<html lang="en">
<head>
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
<script>
function refreshIFrame() {
var x = document.getElementById("*Your_iframe_id*");
x.contentWindow.location.reload();
var t = setTimeout(refreshIFrame, 5000);
<meta charset="UTF-8">
<title>Tailscale VPN</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
background: #f5f6f8;
color: #1a1a2e;
padding: 20px;
font-size: 14px;
}
</script>
header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
header svg { flex-shrink: 0; }
header h1 {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
}
.card {
background: #fff;
border-radius: 10px;
padding: 18px 20px;
margin-bottom: 14px;
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
}
/* ── Status row ── */
.status-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
.status-label {
font-size: 15px;
font-weight: 600;
}
/* ── Auth URL block ── */
#auth-block {
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 14px 16px;
}
#auth-block p {
font-size: 12px;
color: #92400e;
margin-bottom: 10px;
font-weight: 500;
}
#auth-link {
display: inline-block;
background: #0166ff;
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 13px;
padding: 8px 16px;
border-radius: 6px;
margin-bottom: 10px;
}
#auth-link:hover { background: #0052cc; }
#auth-url-text {
display: block;
font-size: 11px;
color: #888;
word-break: break-all;
font-family: monospace;
}
/* ── Info grid (connected state) ── */
.info-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 6px 16px;
font-size: 13px;
}
.info-grid .label { color: #888; }
.info-grid .value { font-weight: 500; font-family: monospace; }
/* ── Log section ── */
.log-header {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #999;
margin-bottom: 10px;
}
#log-frame {
width: 100%;
height: 500px;
border: 1px solid #e8e8ec;
border-radius: 6px;
display: block;
}
</style>
</head>
<body onload="refreshIFrame()">
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
</body>
<body>
<header>
<!-- Tailscale wordmark icon -->
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="28" fill="#0166FF"/>
<circle cx="64" cy="64" r="18" fill="white"/>
<circle cx="64" cy="22" r="10" fill="white"/>
<circle cx="64" cy="106" r="10" fill="white"/>
<circle cx="22" cy="64" r="10" fill="white"/>
<circle cx="106" cy="64" r="10" fill="white"/>
<circle cx="35" cy="35" r="10" fill="white"/>
<circle cx="93" cy="35" r="10" fill="white"/>
<circle cx="35" cy="93" r="10" fill="white"/>
<circle cx="93" cy="93" r="10" fill="white"/>
</svg>
<h1>Tailscale VPN</h1>
</header>
<!-- Status card -->
<div class="card">
<div class="status-row">
<span id="dot" class="dot connecting"></span>
<span id="status-label" class="status-label">Checking&hellip;</span>
</div>
<!-- Shown when waiting for auth -->
<div id="auth-block" style="display:none;">
<p>Open this link in a browser to authenticate this device:</p>
<a id="auth-link" href="#" target="_blank">Authenticate &rarr;</a>
<span id="auth-url-text"></span>
</div>
<!-- Shown when connected -->
<div id="info-block" style="display:none;" class="info-grid">
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">&mdash;</span>
<span class="label">Node</span><span class="value" id="ts-node">&mdash;</span>
</div>
</div>
<!-- Log card -->
<div class="card">
<div class="log-header">Service Log</div>
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN"></iframe>
</div>
<script>
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN';
function parse(txt) {
// Use the LAST occurrence of each pattern so stale log entries don't win
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
// Tailscale IP — 100.x.x.x range
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
// Node name from "Logged into tailnet" line or "acap-" username pattern
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
var node = nodeMatch ? nodeMatch[1] : null;
// Determine state from the LAST state-transition log line
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
if (isRunning) {
return { state: 'connected', url: null, ip: tsIP, node: node };
}
if (latestUrl) {
return { state: 'connecting', url: latestUrl, ip: null, node: null };
}
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
return { state: 'connecting', url: null, ip: null, node: null };
}
return { state: 'disconnected', url: null, ip: null, node: null };
}
function render(r) {
var dot = document.getElementById('dot');
var label = document.getElementById('status-label');
var auth = document.getElementById('auth-block');
var info = document.getElementById('info-block');
dot.className = 'dot ' + r.state;
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
label.textContent = labels[r.state];
// Auth block
if (r.state === 'connecting' && r.url) {
document.getElementById('auth-link').href = r.url;
document.getElementById('auth-url-text').textContent = r.url;
auth.style.display = '';
} else {
auth.style.display = 'none';
}
// Info block
if (r.state === 'connected') {
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
document.getElementById('ts-node').textContent = r.node || '\u2014';
info.style.display = '';
} else {
info.style.display = 'none';
}
}
function refresh() {
// Reload the log iframe (same-origin)
var frame = document.getElementById('log-frame');
try { frame.contentWindow.location.reload(); } catch(e) {}
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(txt) { render(parse(txt)); })
.catch(function() {
document.getElementById('status-label').textContent = 'Unknown';
});
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>
</html>
View File
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -7,12 +7,12 @@
"vendor": "Tailscale - Packaged by Mo3he",
"embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"version": "1.92.3",
"runMode": "respawn",
"version": "1.96.4",
"architecture": "armv7hf"
},
"configuration": {
"settingPage": "index.html"
}
}
}
}
+26 -8
View File
@@ -1,10 +1,28 @@
#!/bin/sh
echo "Starting Service"
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
/usr/local/packages/Tailscale_VPN/lib/tailscaled &
echo "Service Started"
echo "Scroll to Bottom for link"
/usr/local/packages/Tailscale_VPN/lib/tailscale up --accept-routes
wait
APP_DIR="/usr/local/packages/Tailscale_VPN"
STATE_DIR="$APP_DIR/localdata"
logger -t "Tailscale_VPN" "Starting Tailscale VPN service (root mode)"
mkdir -p "$STATE_DIR"
chmod 755 "$APP_DIR/lib/tailscale"
chmod 755 "$APP_DIR/lib/tailscaled"
# Kill any leftover daemon from a previous run
killall tailscaled 2>/dev/null || true
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
"$APP_DIR/lib/tailscaled" \
--state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" &
TAILSCALED_PID=$!
sleep 2
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
logger -t "Tailscale_VPN" "Tailscale VPN is running"
wait $TAILSCALED_PID
logger -t "Tailscale_VPN" "tailscaled exited"
+252 -13
View File
@@ -1,19 +1,258 @@
<!DOCTYPE html>
<html>
<body>
<html lang="en">
<head>
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
<script>
function refreshIFrame() {
var x = document.getElementById("*Your_iframe_id*");
x.contentWindow.location.reload();
var t = setTimeout(refreshIFrame, 5000);
<meta charset="UTF-8">
<title>Tailscale VPN</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
background: #f5f6f8;
color: #1a1a2e;
padding: 20px;
font-size: 14px;
}
</script>
header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
header svg { flex-shrink: 0; }
header h1 {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
}
.card {
background: #fff;
border-radius: 10px;
padding: 18px 20px;
margin-bottom: 14px;
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
}
/* ── Status row ── */
.status-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
.status-label {
font-size: 15px;
font-weight: 600;
}
/* ── Auth URL block ── */
#auth-block {
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 14px 16px;
}
#auth-block p {
font-size: 12px;
color: #92400e;
margin-bottom: 10px;
font-weight: 500;
}
#auth-link {
display: inline-block;
background: #0166ff;
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 13px;
padding: 8px 16px;
border-radius: 6px;
margin-bottom: 10px;
}
#auth-link:hover { background: #0052cc; }
#auth-url-text {
display: block;
font-size: 11px;
color: #888;
word-break: break-all;
font-family: monospace;
}
/* ── Info grid (connected state) ── */
.info-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 6px 16px;
font-size: 13px;
}
.info-grid .label { color: #888; }
.info-grid .value { font-weight: 500; font-family: monospace; }
/* ── Log section ── */
.log-header {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #999;
margin-bottom: 10px;
}
#log-frame {
width: 100%;
height: 500px;
border: 1px solid #e8e8ec;
border-radius: 6px;
display: block;
}
</style>
</head>
<body onload="refreshIFrame()">
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
</body>
<body>
<header>
<!-- Tailscale wordmark icon -->
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="28" fill="#0166FF"/>
<circle cx="64" cy="64" r="18" fill="white"/>
<circle cx="64" cy="22" r="10" fill="white"/>
<circle cx="64" cy="106" r="10" fill="white"/>
<circle cx="22" cy="64" r="10" fill="white"/>
<circle cx="106" cy="64" r="10" fill="white"/>
<circle cx="35" cy="35" r="10" fill="white"/>
<circle cx="93" cy="35" r="10" fill="white"/>
<circle cx="35" cy="93" r="10" fill="white"/>
<circle cx="93" cy="93" r="10" fill="white"/>
</svg>
<h1>Tailscale VPN</h1>
</header>
<!-- Status card -->
<div class="card">
<div class="status-row">
<span id="dot" class="dot connecting"></span>
<span id="status-label" class="status-label">Checking&hellip;</span>
</div>
<!-- Shown when waiting for auth -->
<div id="auth-block" style="display:none;">
<p>Open this link in a browser to authenticate this device:</p>
<a id="auth-link" href="#" target="_blank">Authenticate &rarr;</a>
<span id="auth-url-text"></span>
</div>
<!-- Shown when connected -->
<div id="info-block" style="display:none;" class="info-grid">
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">&mdash;</span>
<span class="label">Node</span><span class="value" id="ts-node">&mdash;</span>
</div>
</div>
<!-- Log card -->
<div class="card">
<div class="log-header">Service Log</div>
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN"></iframe>
</div>
<script>
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN';
function parse(txt) {
// Use the LAST occurrence of each pattern so stale log entries don't win
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
// Tailscale IP — 100.x.x.x range
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
// Node name from "Logged into tailnet" line or "acap-" username pattern
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
var node = nodeMatch ? nodeMatch[1] : null;
// Determine state from the LAST state-transition log line
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
if (isRunning) {
return { state: 'connected', url: null, ip: tsIP, node: node };
}
if (latestUrl) {
return { state: 'connecting', url: latestUrl, ip: null, node: null };
}
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
return { state: 'connecting', url: null, ip: null, node: null };
}
return { state: 'disconnected', url: null, ip: null, node: null };
}
function render(r) {
var dot = document.getElementById('dot');
var label = document.getElementById('status-label');
var auth = document.getElementById('auth-block');
var info = document.getElementById('info-block');
dot.className = 'dot ' + r.state;
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
label.textContent = labels[r.state];
// Auth block
if (r.state === 'connecting' && r.url) {
document.getElementById('auth-link').href = r.url;
document.getElementById('auth-url-text').textContent = r.url;
auth.style.display = '';
} else {
auth.style.display = 'none';
}
// Info block
if (r.state === 'connected') {
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
document.getElementById('ts-node').textContent = r.node || '\u2014';
info.style.display = '';
} else {
info.style.display = 'none';
}
}
function refresh() {
// Reload the log iframe (same-origin)
var frame = document.getElementById('log-frame');
try { frame.contentWindow.location.reload(); } catch(e) {}
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(txt) { render(parse(txt)); })
.catch(function() {
document.getElementById('status-label').textContent = 'Unknown';
});
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>
</html>
View File
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -11,12 +11,12 @@
"username": "root"
},
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"version": "1.92.3",
"runMode": "respawn",
"version": "1.96.4",
"architecture": "armv7hf"
},
"configuration": {
"settingPage": "index.html"
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
ARG ARCH=armv7hf
ARG VERSION=12.3.0
ARG UBUNTU_VERSION=24.04
ARG REPO=axisecp
ARG SDK=acap-native-sdk
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
# Building the ACAP application
COPY ./app /opt/app/
WORKDIR /opt/app
RUN . /opt/axis/acapsdk/environment-setup* && acap-build .
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2020 Tailscale & AUTHORS.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+29
View File
@@ -0,0 +1,29 @@
PROGS = serverconfig
SRCS = config_updater.c
OBJS = $(SRCS:.c=.o)
PKGS = glib-2.0 gio-2.0 axparameter
CFLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --cflags $(PKGS))
LDLIBS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --libs $(PKGS))
CFLAGS += -Wall \
-Wextra \
-Wformat=2 \
-Wpointer-arith \
-Wbad-function-cast \
-Wstrict-prototypes \
-Wmissing-prototypes \
-Winline \
-Wdisabled-optimization \
-Wfloat-equal \
-W \
-Werror
all: $(PROGS)
$(PROGS): $(OBJS)
$(CC) $(LDFLAGS) $^ $(LIBS) $(LDLIBS) -o $@
clean:
rm -f $(PROGS) *.o *.eap* *_LICENSE.txt package.conf* param.conf tmp*
+253
View File
@@ -0,0 +1,253 @@
/**
* Simple file-based configuration updater for Tailscale
* This avoids the AXParameter system and just writes directly to a config file
*/
#include <axsdk/axparameter.h>
#include <glib-unix.h>
#include <stdbool.h>
#include <syslog.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define APP_NAME "serverconfig"
#define APP_DIR "/usr/local/packages/serverconfig"
#define STATE_DIR APP_DIR "/localdata"
#define CONFIG_FILE STATE_DIR "/config.txt"
#define SCRIPT_PATH "/usr/local/packages/serverconfig/start_tailscale.sh"
#define SCRIPT_SOURCE "/usr/local/packages/serverconfig/lib/start_tailscale.sh"
static gboolean signal_handler(gpointer loop) {
g_main_loop_quit((GMainLoop*)loop);
syslog(LOG_INFO, "Configuration updater stopping.");
return G_SOURCE_REMOVE;
}
// Create localdata directory
static void ensure_localdata_exists(void) {
struct stat st = {0};
if (stat(STATE_DIR, &st) == -1) {
if (mkdir(STATE_DIR, 0755) != 0) {
syslog(LOG_ERR, "Failed to create localdata directory: %s", strerror(errno));
} else {
syslog(LOG_INFO, "Created localdata directory: %s", STATE_DIR);
}
}
}
// Copy script from lib folder to main directory
static void copy_script_file(void) {
char buffer[4096];
ssize_t bytes_read, bytes_written;
int source_fd, dest_fd;
syslog(LOG_INFO, "Copying script from %s to %s", SCRIPT_SOURCE, SCRIPT_PATH);
// Open source file
source_fd = open(SCRIPT_SOURCE, O_RDONLY);
if (source_fd < 0) {
syslog(LOG_ERR, "Failed to open source script: %s", strerror(errno));
return;
}
// Open destination file (create if doesn't exist, truncate if exists)
dest_fd = open(SCRIPT_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0755);
if (dest_fd < 0) {
syslog(LOG_ERR, "Failed to open destination script: %s", strerror(errno));
close(source_fd);
return;
}
// Copy the file
while ((bytes_read = read(source_fd, buffer, sizeof(buffer))) > 0) {
bytes_written = write(dest_fd, buffer, bytes_read);
if (bytes_written != bytes_read) {
syslog(LOG_ERR, "Error writing to destination file: %s", strerror(errno));
close(source_fd);
close(dest_fd);
return;
}
}
// Close file descriptors
close(source_fd);
close(dest_fd);
// Make the script executable
if (chmod(SCRIPT_PATH, 0755) != 0) {
syslog(LOG_ERR, "Failed to make script executable: %s", strerror(errno));
return;
}
syslog(LOG_INFO, "Script copied and made executable successfully");
}
// Execute the Tailscale script
static void start_tailscale(void) {
syslog(LOG_INFO, "Starting Tailscale VPN script");
// Check if script exists, if not, copy it
struct stat st;
if (stat(SCRIPT_PATH, &st) != 0) {
syslog(LOG_INFO, "Script not found at %s, copying from lib folder", SCRIPT_PATH);
copy_script_file();
}
// Fork and execute the script
pid_t pid = fork();
if (pid < 0) {
syslog(LOG_ERR, "Failed to fork for Tailscale script: %s", strerror(errno));
return;
} else if (pid == 0) {
// Child process - execute the script
execl(SCRIPT_PATH, "start_tailscale.sh", NULL);
// If we get here, execl failed
syslog(LOG_ERR, "Failed to execute Tailscale script: %s", strerror(errno));
_exit(1);
}
syslog(LOG_INFO, "Tailscale script started with PID: %d", pid);
}
// Update the configuration file with current parameter values
static void update_config_file(AXParameter* handle) {
GError* error = NULL;
gchar* server_value = NULL;
gchar* key_value = NULL;
FILE* file;
// Ensure localdata directory exists
ensure_localdata_exists();
// Get parameter values
if (!ax_parameter_get(handle, "CustomServer", &server_value, &error)) {
syslog(LOG_ERR, "Failed to get CustomServer: %s",
error ? error->message : "unknown error");
if (error) g_error_free(error);
error = NULL;
server_value = g_strdup("");
}
if (!ax_parameter_get(handle, "AuthKey", &key_value, &error)) {
syslog(LOG_ERR, "Failed to get AuthKey: %s",
error ? error->message : "unknown error");
if (error) g_error_free(error);
key_value = g_strdup("");
}
// Write to config file in localdata
file = fopen(CONFIG_FILE, "w");
if (file) {
fprintf(file, "custom_server=%s\n", server_value ? server_value : "");
fprintf(file, "auth_key=%s\n", key_value ? key_value : "");
fclose(file);
// Set permissions to ensure the file is readable
chmod(CONFIG_FILE, 0644);
syslog(LOG_INFO, "Updated configuration file in local custom_server=%s",
server_value ? server_value : "");
syslog(LOG_INFO, "Updated configuration file in local auth_key=%s",
key_value && strlen(key_value) > 0 ? "(set)" : "(empty)");
} else {
syslog(LOG_ERR, "Failed to open config file for writing: %s", strerror(errno));
}
// Clean up
g_free(server_value);
g_free(key_value);
}
// Handle parameter changes
static void parameter_changed(const gchar* name, const gchar* value, gpointer handle_void_ptr) {
AXParameter* handle = handle_void_ptr;
// Extract simple parameter name from the fully qualified name
const char* simple_name = name;
const char* prefix = "root." APP_NAME ".";
if (strncmp(name, prefix, strlen(prefix)) == 0) {
simple_name = name + strlen(prefix);
}
syslog(LOG_INFO, "Parameter changed: %s = %s", simple_name, value);
// Update config file whenever any parameter changes
update_config_file(handle);
// Restart Tailscale to apply the new settings
start_tailscale();
}
int main(void) {
GError* error = NULL;
GMainLoop* loop = NULL;
// Open syslog for logging
openlog(APP_NAME, LOG_PID, LOG_USER);
syslog(LOG_INFO, "Config updater starting");
// Initialize parameter handling
AXParameter* handle = ax_parameter_new(APP_NAME, &error);
if (handle == NULL) {
syslog(LOG_ERR, "Failed to initialize parameters: %s",
error ? error->message : "unknown error");
if (error) g_error_free(error);
exit(1);
}
// Ensure localdata directory exists
ensure_localdata_exists();
// Ensure script is copied from lib folder
copy_script_file();
// Create initial config file
update_config_file(handle);
// Start Tailscale VPN script
start_tailscale();
// Register for parameter changes
if (!ax_parameter_register_callback(handle, "CustomServer", parameter_changed, handle, &error)) {
syslog(LOG_ERR, "Failed to register CustomServer callback: %s",
error ? error->message : "unknown error");
if (error) g_error_free(error);
error = NULL;
}
if (!ax_parameter_register_callback(handle, "AuthKey", parameter_changed, handle, &error)) {
syslog(LOG_ERR, "Failed to register AuthKey callback: %s",
error ? error->message : "unknown error");
if (error) g_error_free(error);
}
// Register for parameter changes with fully qualified names as fallback
if (!ax_parameter_register_callback(handle, "root." APP_NAME ".CustomServer", parameter_changed, handle, NULL)) {
syslog(LOG_INFO, "Fallback CustomServer registration failed (this may be normal)");
}
if (!ax_parameter_register_callback(handle, "root." APP_NAME ".AuthKey", parameter_changed, handle, NULL)) {
syslog(LOG_INFO, "Fallback AuthKey registration failed (this may be normal)");
}
// Set up main loop
loop = g_main_loop_new(NULL, FALSE);
g_unix_signal_add(SIGTERM, signal_handler, loop);
g_unix_signal_add(SIGINT, signal_handler, loop);
syslog(LOG_INFO, "Config updater running. Waiting for parameter changes...");
g_main_loop_run(loop);
// Clean up
g_main_loop_unref(loop);
ax_parameter_free(handle);
return 0;
}
+258
View File
@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tailscale VPN</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
background: #f5f6f8;
color: #1a1a2e;
padding: 20px;
font-size: 14px;
}
header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
header svg { flex-shrink: 0; }
header h1 {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
}
.card {
background: #fff;
border-radius: 10px;
padding: 18px 20px;
margin-bottom: 14px;
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
}
/* ── Status row ── */
.status-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
.status-label {
font-size: 15px;
font-weight: 600;
}
/* ── Auth URL block ── */
#auth-block {
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 14px 16px;
}
#auth-block p {
font-size: 12px;
color: #92400e;
margin-bottom: 10px;
font-weight: 500;
}
#auth-link {
display: inline-block;
background: #0166ff;
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 13px;
padding: 8px 16px;
border-radius: 6px;
margin-bottom: 10px;
}
#auth-link:hover { background: #0052cc; }
#auth-url-text {
display: block;
font-size: 11px;
color: #888;
word-break: break-all;
font-family: monospace;
}
/* ── Info grid (connected state) ── */
.info-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 6px 16px;
font-size: 13px;
}
.info-grid .label { color: #888; }
.info-grid .value { font-weight: 500; font-family: monospace; }
/* ── Log section ── */
.log-header {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #999;
margin-bottom: 10px;
}
#log-frame {
width: 100%;
height: 500px;
border: 1px solid #e8e8ec;
border-radius: 6px;
display: block;
}
</style>
</head>
<body>
<header>
<!-- Tailscale wordmark icon -->
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="28" fill="#0166FF"/>
<circle cx="64" cy="64" r="18" fill="white"/>
<circle cx="64" cy="22" r="10" fill="white"/>
<circle cx="64" cy="106" r="10" fill="white"/>
<circle cx="22" cy="64" r="10" fill="white"/>
<circle cx="106" cy="64" r="10" fill="white"/>
<circle cx="35" cy="35" r="10" fill="white"/>
<circle cx="93" cy="35" r="10" fill="white"/>
<circle cx="35" cy="93" r="10" fill="white"/>
<circle cx="93" cy="93" r="10" fill="white"/>
</svg>
<h1>Tailscale VPN</h1>
</header>
<!-- Status card -->
<div class="card">
<div class="status-row">
<span id="dot" class="dot connecting"></span>
<span id="status-label" class="status-label">Checking&hellip;</span>
</div>
<!-- Shown when waiting for auth -->
<div id="auth-block" style="display:none;">
<p>Open this link in a browser to authenticate this device:</p>
<a id="auth-link" href="#" target="_blank">Authenticate &rarr;</a>
<span id="auth-url-text"></span>
</div>
<!-- Shown when connected -->
<div id="info-block" style="display:none;" class="info-grid">
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">&mdash;</span>
<span class="label">Node</span><span class="value" id="ts-node">&mdash;</span>
</div>
</div>
<!-- Log card -->
<div class="card">
<div class="log-header">Service Log</div>
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=serverconfig"></iframe>
</div>
<script>
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=serverconfig';
function parse(txt) {
// Use the LAST occurrence of each pattern so stale log entries don't win
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
// Tailscale IP — 100.x.x.x range
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
// Node name from "Logged into tailnet" line or "acap-" username pattern
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
var node = nodeMatch ? nodeMatch[1] : null;
// Determine state from the LAST state-transition log line
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
if (isRunning) {
return { state: 'connected', url: null, ip: tsIP, node: node };
}
if (latestUrl) {
return { state: 'connecting', url: latestUrl, ip: null, node: null };
}
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
return { state: 'connecting', url: null, ip: null, node: null };
}
return { state: 'disconnected', url: null, ip: null, node: null };
}
function render(r) {
var dot = document.getElementById('dot');
var label = document.getElementById('status-label');
var auth = document.getElementById('auth-block');
var info = document.getElementById('info-block');
dot.className = 'dot ' + r.state;
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
label.textContent = labels[r.state];
// Auth block
if (r.state === 'connecting' && r.url) {
document.getElementById('auth-link').href = r.url;
document.getElementById('auth-url-text').textContent = r.url;
auth.style.display = '';
} else {
auth.style.display = 'none';
}
// Info block
if (r.state === 'connected') {
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
document.getElementById('ts-node').textContent = r.node || '\u2014';
info.style.display = '';
} else {
info.style.display = 'none';
}
}
function refresh() {
// Reload the log iframe (same-origin)
var frame = document.getElementById('log-frame');
try { frame.contentWindow.location.reload(); } catch(e) {}
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(txt) { render(parse(txt)); })
.catch(function() {
document.getElementById('status-label').textContent = 'Unknown';
});
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>
View File
+81
View File
@@ -0,0 +1,81 @@
#!/bin/sh
# Make sure this script terminates any existing Tailscale processes before starting new ones
# Kill any existing tailscaled processes
killall tailscaled 2>/dev/null || true
# Simple script to start Tailscale with custom configuration
APP_DIR="/usr/local/packages/serverconfig"
STATE_DIR="$APP_DIR/localdata"
CONFIG_FILE="$STATE_DIR/config.txt"
TAILSCALED_PATH="$APP_DIR/lib/tailscaled"
TAILSCALE_PATH="$APP_DIR/lib/tailscale"
SOCKET_PATH="$STATE_DIR/tailscaled.sock"
# Create localdata directory if it doesn't exist
mkdir -p "$STATE_DIR"
# Log to syslog
logger -t "tailscale_script" "Starting Tailscale VPN service"
# Set execute permissions
chmod 755 $TAILSCALED_PATH
chmod 755 $TAILSCALE_PATH
# Read configuration (if exists)
CUSTOM_SERVER=""
AUTH_KEY=""
if [ -f "$CONFIG_FILE" ]; then
logger -t "tailscale_script" "Reading configuration from $CONFIG_FILE"
# Read values from config file
while IFS='=' read -r key value; do
case "$key" in
"custom_server") CUSTOM_SERVER="$value" ;;
"auth_key") AUTH_KEY="$value" ;;
esac
done < "$CONFIG_FILE"
fi
# Start tailscaled with state stored in localdata
logger -t "tailscale_script" "Starting tailscaled daemon"
$TAILSCALED_PATH \
--state="$STATE_DIR/tailscaled.state" \
--socket=$SOCKET_PATH \
--socks5-server=localhost:1055 \
--tun=userspace-networking &
TAILSCALED_PID=$!
# Wait for tailscaled to initialize
sleep 2
# Build up the command based on available parameters
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up"
if [ -n "$CUSTOM_SERVER" ]; then
logger -t "tailscale_script" "Using custom server: $CUSTOM_SERVER"
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
fi
if [ -n "$AUTH_KEY" ]; then
logger -t "tailscale_script" "Using authentication key"
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
fi
# Connect to Tailscale network
logger -t "tailscale_script" "Running: $TAILSCALE_CMD"
eval $TAILSCALE_CMD
UP_EXIT=$?
# Clear auth key from config after first use — Tailscale auth keys are single-use
# and the node identity is persisted in tailscaled.state, so the key is no longer needed.
if [ -n "$AUTH_KEY" ] && [ "$UP_EXIT" -eq 0 ] && [ -f "$CONFIG_FILE" ]; then
logger -t "tailscale_script" "Clearing auth key from config after successful authentication"
printf 'custom_server=%s\nauth_key=\n' "$CUSTOM_SERVER" > "$CONFIG_FILE"
fi
# Keep the script running to maintain the tailscaled process
logger -t "tailscale_script" "Tailscale VPN is running"
logger -t "tailscale_script" "To change settings, modify parameters in ACAP web interface"
# Wait for tailscaled process to exit
wait $TAILSCALED_PID
+30
View File
@@ -0,0 +1,30 @@
{
"schemaVersion": "1.7.3",
"acapPackageConf": {
"setup": {
"friendlyName": "Tailscale VPN",
"appName": "serverconfig",
"vendor": "Tailscale - Packaged by Mo3he",
"embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com",
"runMode": "respawn",
"version": "1.96.4",
"architecture": "armv7hf"
},
"configuration": {
"settingPage": "index.html",
"paramConfig": [
{
"name": "CustomServer",
"default": "",
"type": "string"
},
{
"name": "AuthKey",
"default": "",
"type": "string"
}
]
}
}
}