mirror of
https://github.com/Mo3he/Axis_Cam_Tailscale.git
synced 2026-09-30 11:35:36 +00:00
Improve all variants: new UI, respawn mode, arm_custom, binary gitignore
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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…</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 →</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">—</span>
|
||||
<span class="label">Node</span><span class="value" id="ts-node">—</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>
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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…</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 →</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">—</span>
|
||||
<span class="label">Node</span><span class="value" id="ts-node">—</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>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"username": "root"
|
||||
},
|
||||
"vendorUrl": "https://www.tailscale.com",
|
||||
"runMode": "once",
|
||||
"runMode": "respawn",
|
||||
"version": "1.96.2",
|
||||
"architecture": "aarch64"
|
||||
},
|
||||
|
||||
@@ -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…</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 →</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">—</span>
|
||||
<span class="label">Node</span><span class="value" id="ts-node">—</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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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…</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 →</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">—</span>
|
||||
<span class="label">Node</span><span class="value" id="ts-node">—</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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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…</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 →</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">—</span>
|
||||
<span class="label">Node</span><span class="value" id="ts-node">—</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>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"username": "root"
|
||||
},
|
||||
"vendorUrl": "https://www.tailscale.com",
|
||||
"runMode": "once",
|
||||
"runMode": "respawn",
|
||||
"version": "1.96.2",
|
||||
"architecture": "armv7hf"
|
||||
},
|
||||
|
||||
@@ -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 .
|
||||
@@ -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*
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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…</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 →</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">—</span>
|
||||
<span class="label">Node</span><span class="value" id="ts-node">—</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>
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user