Improve all variants: new UI, respawn mode, arm_custom, binary gitignore

This commit is contained in:
Weston Blieden
2026-03-19 16:23:44 +01:00
parent 39a5a42def
commit c3cbf8d433
28 changed files with 1988 additions and 121 deletions
-3
View File
@@ -103,9 +103,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
+12 -12
View File
@@ -1,30 +1,30 @@
#!/bin/sh
echo "Starting Service"
# Define paths
APP_DIR="/usr/local/packages/Tailscale_VPN"
STATE_DIR="$APP_DIR/localdata"
# Create localdata directory if it doesn't exist
logger -t "Tailscale_VPN" "Starting Tailscale VPN service"
mkdir -p "$STATE_DIR"
chmod 755 "$APP_DIR/lib/tailscale"
chmod 755 "$APP_DIR/lib/tailscaled"
# Set permissions
chmod 777 "$APP_DIR/lib/tailscale"
chmod 777 "$APP_DIR/lib/tailscaled"
# Kill any leftover daemon from a previous run
killall tailscaled 2>/dev/null || true
# Start tailscaled with state stored in localdata
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=$!
echo "Service Started"
sleep 2
echo "Scroll to Bottom for link"
# Run tailscale up with the socket in localdata
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
wait
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
+1 -1
View File
@@ -7,7 +7,7 @@
"vendor": "Tailscale - Packaged by Mo3he",
"embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"runMode": "respawn",
"version": "1.96.2",
"architecture": "aarch64"
},
+12 -12
View File
@@ -1,28 +1,28 @@
#!/bin/sh
echo "Starting Service"
# Define paths
APP_DIR="/usr/local/packages/Tailscale_VPN"
STATE_DIR="$APP_DIR/localdata"
# Create localdata directory if it doesn't exist
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"
# Set permissions
chmod 777 "$APP_DIR/lib/tailscale"
chmod 777 "$APP_DIR/lib/tailscaled"
# Kill any leftover daemon from a previous run
killall tailscaled 2>/dev/null || true
# Start tailscaled with state stored in localdata
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
"$APP_DIR/lib/tailscaled" \
--state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" &
TAILSCALED_PID=$!
echo "Service Started"
sleep 2
echo "Scroll to Bottom for link"
# Run tailscale up with the socket in localdata
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
wait
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
+1 -1
View File
@@ -11,7 +11,7 @@
"username": "root"
},
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"runMode": "respawn",
"version": "1.96.2",
"architecture": "aarch64"
},
+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
+9 -1
View File
@@ -2,7 +2,7 @@
# 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
APP_DIR="/usr/local/packages/serverconfig"
@@ -64,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"
+12 -12
View File
@@ -1,30 +1,30 @@
#!/bin/sh
echo "Starting Service"
# Define paths
APP_DIR="/usr/local/packages/Tailscale_VPN"
STATE_DIR="$APP_DIR/localdata"
# Create localdata directory if it doesn't exist
logger -t "Tailscale_VPN" "Starting Tailscale VPN service"
mkdir -p "$STATE_DIR"
chmod 755 "$APP_DIR/lib/tailscale"
chmod 755 "$APP_DIR/lib/tailscaled"
# Set permissions
chmod 777 "$APP_DIR/lib/tailscale"
chmod 777 "$APP_DIR/lib/tailscaled"
# Kill any leftover daemon from a previous run
killall tailscaled 2>/dev/null || true
# Start tailscaled with state stored in localdata
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=$!
echo "Service Started"
sleep 2
echo "Scroll to Bottom for link"
# Run tailscale up with the socket in localdata
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
wait
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
+1 -1
View File
@@ -7,7 +7,7 @@
"vendor": "Tailscale - Packaged by Mo3he",
"embeddedSdkVersion": "3.0",
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"runMode": "respawn",
"version": "1.96.2",
"architecture": "armv7hf"
},
+12 -12
View File
@@ -1,28 +1,28 @@
#!/bin/sh
echo "Starting Service"
# Define paths
APP_DIR="/usr/local/packages/Tailscale_VPN"
STATE_DIR="$APP_DIR/localdata"
# Create localdata directory if it doesn't exist
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"
# Set permissions
chmod 777 "$APP_DIR/lib/tailscale"
chmod 777 "$APP_DIR/lib/tailscaled"
# Kill any leftover daemon from a previous run
killall tailscaled 2>/dev/null || true
# Start tailscaled with state stored in localdata
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
"$APP_DIR/lib/tailscaled" \
--state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" &
TAILSCALED_PID=$!
echo "Service Started"
sleep 2
echo "Scroll to Bottom for link"
# Run tailscale up with the socket in localdata
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
wait
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
+1 -1
View File
@@ -11,7 +11,7 @@
"username": "root"
},
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"runMode": "respawn",
"version": "1.96.2",
"architecture": "armv7hf"
},
+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 @@
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.2",
"architecture": "armv7hf"
},
"configuration": {
"settingPage": "index.html",
"paramConfig": [
{
"name": "CustomServer",
"default": "",
"type": "string"
},
{
"name": "AuthKey",
"default": "",
"type": "string"
}
]
}
}
}