mirror of
https://github.com/Mo3he/Axis_Cam_Tailscale.git
synced 2026-10-01 03:55:40 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cbe14b8cf | ||
|
|
230a9f4861 | ||
|
|
1997941965 | ||
|
|
8b70caaf70 | ||
|
|
08f791ed24 | ||
|
|
7d8d3a04cb | ||
|
|
ccd670591e | ||
|
|
7dffcd1f58 | ||
|
|
e23272fedd | ||
|
|
559fe42294 |
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: monthly
|
|
||||||
# One PR per repo instead of one per action; the workflow is generated from
|
|
||||||
# Axis_Cam_Template/ci/build.yml.tmpl, so apply bumps there and re-sync.
|
|
||||||
groups:
|
|
||||||
github-actions:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
commit-message:
|
|
||||||
prefix: ci
|
|
||||||
labels:
|
|
||||||
- dependencies
|
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
---
|
---
|
||||||
# GENERATED by acap-ci.sh from Axis_Cam_Template/ci/build.yml.tmpl
|
|
||||||
# Per-repo settings live in .acap.json. Do not edit this file directly.
|
|
||||||
#
|
|
||||||
# Upstream release -> build -> DRAFT release holding unsigned .eap files.
|
# Upstream release -> build -> DRAFT release holding unsigned .eap files.
|
||||||
# Signing is manual (Axis has no signing API); ../acap-sign.sh uploads the
|
# Signing is manual (Axis has no signing API); ../acap-sign.sh uploads the
|
||||||
# signed packages and publishes the release. The acap-ops repo notifies.
|
# signed packages and publishes the release. The acap-ops repo notifies.
|
||||||
@@ -61,6 +58,18 @@ jobs:
|
|||||||
- name: Apply version and upstream pins
|
- name: Apply version and upstream pins
|
||||||
run: ./ci/apply-version.sh "$VERSION" "$UPSTREAM_VERSION"
|
run: ./ci/apply-version.sh "$VERSION" "$UPSTREAM_VERSION"
|
||||||
|
|
||||||
|
# Repos without a tests/run.sh simply skip this.
|
||||||
|
- name: Run unit tests
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -x tests/run.sh ] || [ -f tests/run.sh ]; then
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends pkg-config libglib2.0-dev
|
||||||
|
sh tests/run.sh
|
||||||
|
else
|
||||||
|
echo "no tests/run.sh, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: ./ci/build-packages.sh
|
run: ./ci/build-packages.sh
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
# Advanced setup: default setup cannot select javascript-typescript here
|
||||||
|
# because the UI scripts live inside index.html rather than a .js file.
|
||||||
|
name: CodeQL
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: "24 4 * * 1"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze ${{ matrix.language }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [c-cpp, javascript-typescript]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v4
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
build-mode: none
|
||||||
|
|
||||||
|
- name: Perform CodeQL analysis
|
||||||
|
uses: github/codeql-action/analyze@v4
|
||||||
|
with:
|
||||||
|
category: "/language:${{ matrix.language }}"
|
||||||
@@ -6,7 +6,6 @@ ARG SDK=acap-native-sdk
|
|||||||
|
|
||||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||||
|
|
||||||
# Building the ACAP application
|
|
||||||
COPY common/app /opt/app/
|
COPY common/app /opt/app/
|
||||||
COPY aarch64/app/manifest.json /opt/app/manifest.json
|
COPY aarch64/app/manifest.json /opt/app/manifest.json
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ ARG SDK=acap-native-sdk
|
|||||||
|
|
||||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||||
|
|
||||||
# Building the ACAP application
|
|
||||||
COPY common/app /opt/app/
|
COPY common/app /opt/app/
|
||||||
COPY aarch64_ROOT/app/manifest.json /opt/app/manifest.json
|
COPY aarch64_ROOT/app/manifest.json /opt/app/manifest.json
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ ARG SDK=acap-native-sdk
|
|||||||
|
|
||||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||||
|
|
||||||
# Building the ACAP application
|
|
||||||
COPY common/app /opt/app/
|
COPY common/app /opt/app/
|
||||||
COPY arm/app/manifest.json /opt/app/manifest.json
|
COPY arm/app/manifest.json /opt/app/manifest.json
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ ARG SDK=acap-native-sdk
|
|||||||
|
|
||||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||||
|
|
||||||
# Building the ACAP application
|
|
||||||
COPY common/app /opt/app/
|
COPY common/app /opt/app/
|
||||||
COPY arm_ROOT/app/manifest.json /opt/app/manifest.json
|
COPY arm_ROOT/app/manifest.json /opt/app/manifest.json
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|||||||
+6
-14
@@ -7,18 +7,15 @@ RUN apt-get update -qq && apt-get install -y --no-install-recommends upx-ucl &&
|
|||||||
COPY arm_acap3/app /opt/app/
|
COPY arm_acap3/app /opt/app/
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|
||||||
# Rename the shell startup script (the ELF launcher will take the Tailscale_VPN name)
|
# The ELF launcher takes over the Tailscale_VPN name.
|
||||||
RUN mv Tailscale_VPN start.sh && chmod +x start.sh
|
RUN mv Tailscale_VPN start.sh && chmod +x start.sh
|
||||||
|
|
||||||
# Compile a minimal ELF launcher as APPNAME.
|
# ACAP 3 elflibcheck needs APPNAME to be ELF and uses pidof(APPNAME) for status.
|
||||||
# ACAP 3 elflibcheck requires an ELF binary and uses pidof(APPNAME) for status.
|
|
||||||
# We fork+exec start.sh so the parent "Tailscale_VPN" process stays resident,
|
|
||||||
# making pidof find it and the camera UI correctly show Running/Stopped.
|
|
||||||
RUN . /opt/axis/acapsdk/environment-setup* && \
|
RUN . /opt/axis/acapsdk/environment-setup* && \
|
||||||
${CC} -o Tailscale_VPN launcher.c && \
|
${CC} -o Tailscale_VPN launcher.c && \
|
||||||
${STRIP} -s Tailscale_VPN
|
${STRIP} -s Tailscale_VPN
|
||||||
|
|
||||||
# Strip then UPX-compress the Tailscale binaries so they fit on flash
|
# UPX so the binaries fit on flash
|
||||||
RUN . /opt/axis/acapsdk/environment-setup* && \
|
RUN . /opt/axis/acapsdk/environment-setup* && \
|
||||||
${STRIP} -s lib/tailscale lib/tailscaled 2>/dev/null || true && \
|
${STRIP} -s lib/tailscale lib/tailscaled 2>/dev/null || true && \
|
||||||
upx --best lib/tailscale lib/tailscaled
|
upx --best lib/tailscale lib/tailscaled
|
||||||
@@ -26,20 +23,15 @@ RUN . /opt/axis/acapsdk/environment-setup* && \
|
|||||||
# ACAP 3 firmware expects the settings page at the app root, not in html/
|
# ACAP 3 firmware expects the settings page at the app root, not in html/
|
||||||
RUN cp html/index.html index.html
|
RUN cp html/index.html index.html
|
||||||
|
|
||||||
# Symlink tailscaled.log into html/ so the web UI can fetch it via HTTP.
|
# Runtime files live in localdata/; symlink them so the web UI can fetch them.
|
||||||
# The log is written at runtime to localdata/ (resolved path at runtime).
|
|
||||||
RUN ln -sf ../localdata/tailscaled.log html/tailscaled.log
|
RUN ln -sf ../localdata/tailscaled.log html/tailscaled.log
|
||||||
|
|
||||||
# Symlink the runtime status.json (written by start.sh from `tailscale status
|
|
||||||
# --json`) into html/ so the web UI can read Tailscale's authoritative state.
|
|
||||||
RUN ln -sf ../localdata/status.json html/status.json
|
RUN ln -sf ../localdata/status.json html/status.json
|
||||||
|
|
||||||
# Build and package
|
|
||||||
RUN . /opt/axis/acapsdk/environment-setup* && create-package.sh ./
|
RUN . /opt/axis/acapsdk/environment-setup* && create-package.sh ./
|
||||||
|
|
||||||
# Patch STARTMODE: create-package.sh hardcodes "never" unless RESTRICTION_STARTMODE is set,
|
# create-package.sh hardcodes STARTMODE="never" (ignoring package.conf) unless
|
||||||
# but the ACAP 3 SDK does not honour our package.conf's STARTMODE=respawn without it.
|
# RESTRICTION_STARTMODE is set, so repack the .eap with respawn.
|
||||||
# Repack the .eap with the corrected value.
|
|
||||||
RUN for eap in *.eap; do \
|
RUN for eap in *.eap; do \
|
||||||
tmpdir=$(mktemp -d) && tar xf "$eap" -C "$tmpdir" && \
|
tmpdir=$(mktemp -d) && tar xf "$eap" -C "$tmpdir" && \
|
||||||
sed -i 's/STARTMODE="never"/STARTMODE="respawn"/' "$tmpdir/package.conf" && \
|
sed -i 's/STARTMODE="never"/STARTMODE="respawn"/' "$tmpdir/package.conf" && \
|
||||||
|
|||||||
@@ -300,14 +300,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status -->
|
|
||||||
<div id="status-banner" class="status-banner connecting">
|
<div id="status-banner" class="status-banner connecting">
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span id="status-text" class="status-text">Checking...</span>
|
<span id="status-text" class="status-text">Checking...</span>
|
||||||
<span id="status-time" class="status-time"></span>
|
<span id="status-time" class="status-time"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Update available -->
|
|
||||||
<div id="update-banner" class="update-banner">
|
<div id="update-banner" class="update-banner">
|
||||||
<div class="update-text">Update available: <strong id="update-version"></strong></div>
|
<div class="update-text">Update available: <strong id="update-version"></strong></div>
|
||||||
<a id="update-link" class="update-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
<a id="update-link" class="update-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||||
@@ -316,7 +314,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auth (hidden by default) -->
|
|
||||||
<div id="auth-block" class="auth-block" style="display:none;">
|
<div id="auth-block" class="auth-block" style="display:none;">
|
||||||
<p>Authenticate this device to connect to your Tailscale network:</p>
|
<p>Authenticate this device to connect to your Tailscale network:</p>
|
||||||
<a id="auth-link" class="auth-btn" href="#" target="_blank">
|
<a id="auth-link" class="auth-btn" href="#" target="_blank">
|
||||||
@@ -326,7 +323,6 @@
|
|||||||
<span id="auth-url-text" class="auth-url"></span>
|
<span id="auth-url-text" class="auth-url"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connection Info -->
|
|
||||||
<div class="card" id="info-card" style="display:none;">
|
<div class="card" id="info-card" style="display:none;">
|
||||||
<div class="card-title">Connection Details</div>
|
<div class="card-title">Connection Details</div>
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
@@ -352,7 +348,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="log-controls">
|
<div class="log-controls">
|
||||||
<div class="card-title" style="margin-bottom:0;">Service Log</div>
|
<div class="card-title" style="margin-bottom:0;">Service Log</div>
|
||||||
@@ -436,11 +431,11 @@
|
|||||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary: extract hostname from Axis syslog header (always the real device hostname)
|
// The syslog header always carries the real device hostname.
|
||||||
var node = null;
|
var node = null;
|
||||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||||
if (hostLine) node = hostLine[1];
|
if (hostLine) node = hostLine[1];
|
||||||
// Fallback: popBrowserAuthNow/StartLoginInteractiveAs (may contain stale acap-tailscale_vpn)
|
// Fallback; may contain a stale acap-tailscale_vpn name.
|
||||||
if (!node) {
|
if (!node) {
|
||||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||||
@@ -493,7 +488,6 @@
|
|||||||
if (acap3ip) tsIP = acap3ip[1];
|
if (acap3ip) tsIP = acap3ip[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache when found, restore from cache when missing
|
|
||||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||||
tsIP = tsIP || cacheGet('ip');
|
tsIP = tsIP || cacheGet('ip');
|
||||||
node = node || cacheGet('node');
|
node = node || cacheGet('node');
|
||||||
@@ -609,10 +603,8 @@
|
|||||||
.catch(function() { return false; });
|
.catch(function() { return false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACAP3: also fetch the raw tailscaled.log (symlinked into html/) so the parser
|
// ACAP3: tailscaled's own log has the IP, version, tailnet and Running state.
|
||||||
// can find IP, version, tailnet and Running state from tailscaled's own output.
|
|
||||||
var DAEMON_LOG_URL = 'tailscaled.log';
|
var DAEMON_LOG_URL = 'tailscaled.log';
|
||||||
// Authoritative backend state published by start.sh (symlinked into html/).
|
|
||||||
var STATUS_URL = 'status.json';
|
var STATUS_URL = 'status.json';
|
||||||
|
|
||||||
// Ground truth published by start.sh from `tailscale status --json`.
|
// Ground truth published by start.sh from `tailscale status --json`.
|
||||||
@@ -633,7 +625,6 @@
|
|||||||
if (st.Version) result.version = String(st.Version).split('-')[0];
|
if (st.Version) result.version = String(st.Version).split('-')[0];
|
||||||
|
|
||||||
if (bs === 'Running' && self.Online === true) {
|
if (bs === 'Running' && self.Online === true) {
|
||||||
// Genuinely connected and reachable on the tailnet
|
|
||||||
result.state = 'connected';
|
result.state = 'connected';
|
||||||
result.url = null;
|
result.url = null;
|
||||||
result.ip = ip4 || result.ip;
|
result.ip = ip4 || result.ip;
|
||||||
@@ -645,9 +636,8 @@
|
|||||||
result.state = 'connecting';
|
result.state = 'connecting';
|
||||||
result.url = st.AuthURL || result.url;
|
result.url = st.AuthURL || result.url;
|
||||||
} else if (bs === 'Running') {
|
} else if (bs === 'Running') {
|
||||||
// Backend running but node not online: transient network drop or the
|
// Not online: transient drop or node removed/expired. Keep the log
|
||||||
// node was removed/expired and needs re-auth. Not connected. Keep any
|
// parser's URL so the login button still appears.
|
||||||
// login URL the log parser found so the login button still appears.
|
|
||||||
result.state = 'connecting';
|
result.state = 'connecting';
|
||||||
result.url = st.AuthURL || result.url;
|
result.url = st.AuthURL || result.url;
|
||||||
} else if (bs === 'Stopped') {
|
} else if (bs === 'Stopped') {
|
||||||
@@ -672,8 +662,8 @@
|
|||||||
var txt = res[0] + '\n' + res[1];
|
var txt = res[0] + '\n' + res[1];
|
||||||
var st = res[2];
|
var st = res[2];
|
||||||
var result = parse(txt);
|
var result = parse(txt);
|
||||||
renderLogs(res[0]); // show syslog in log panel; daemon log is too verbose
|
renderLogs(res[0]); // daemon log is too verbose to show
|
||||||
// Always verify with the app status API - logs can have stale entries
|
// Logs can have stale entries; confirm the app is running.
|
||||||
checkAppRunning().then(function(running) {
|
checkAppRunning().then(function(running) {
|
||||||
if (!running) {
|
if (!running) {
|
||||||
result.state = 'disconnected';
|
result.state = 'disconnected';
|
||||||
|
|||||||
@@ -4,19 +4,9 @@
|
|||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ACAP 3 supervisor launcher for Tailscale_VPN.
|
* ACAP 3 launcher: elflibcheck needs APPNAME to be ELF, and acap-startstop,
|
||||||
*
|
* respawnd and list.cgi use pidof(APPNAME). So this stays resident, restarts
|
||||||
* elflibcheck.sh requires APPNAME to be an ELF binary.
|
* start.sh whenever it dies, and only exits on SIGTERM/SIGINT.
|
||||||
* acap-startstop / respawnd / list.cgi all use pidof(APPNAME) for status.
|
|
||||||
*
|
|
||||||
* This binary NEVER exits voluntarily — it loops restarting start.sh if it
|
|
||||||
* dies, so:
|
|
||||||
* - pidof Tailscale_VPN always finds this process → UI shows "Running"
|
|
||||||
* - respawnd never triggers (it only fires when APPNAME exits)
|
|
||||||
* - If tailscaled OOMs and start.sh exits, we cleanly restart it
|
|
||||||
*
|
|
||||||
* To stop the app, acap-startstop calls stop_daemon which sends SIGTERM here.
|
|
||||||
* We forward SIGTERM/SIGINT to the child and then exit.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
static volatile int g_stop = 0;
|
static volatile int g_stop = 0;
|
||||||
@@ -37,7 +27,6 @@ int main(void)
|
|||||||
while (!g_stop) {
|
while (!g_stop) {
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
/* child: reset signals and exec start.sh */
|
|
||||||
signal(SIGTERM, SIG_DFL);
|
signal(SIGTERM, SIG_DFL);
|
||||||
signal(SIGINT, SIG_DFL);
|
signal(SIGINT, SIG_DFL);
|
||||||
execl("/usr/local/packages/Tailscale_VPN/start.sh",
|
execl("/usr/local/packages/Tailscale_VPN/start.sh",
|
||||||
@@ -59,7 +48,6 @@ int main(void)
|
|||||||
g_child = -1;
|
g_child = -1;
|
||||||
|
|
||||||
if (!g_stop) {
|
if (!g_stop) {
|
||||||
/* start.sh died unexpectedly — wait before restarting */
|
|
||||||
sleep(3);
|
sleep(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,9 @@
|
|||||||
# ./build.sh # build every variant
|
# ./build.sh # build every variant
|
||||||
# ./build.sh aarch64 arm # build only the named variant folders
|
# ./build.sh aarch64 arm # build only the named variant folders
|
||||||
#
|
#
|
||||||
# Downloads the prebuilt Tailscale binaries, strips them, then builds each
|
# Variant folders map to release .eap suffixes: *_ROOT -> _root, *_acap3 -> _acap3.
|
||||||
# variant folder that contains an app/ directory. Variant folders map to the
|
# RUNTIME=docker|podman forces a container runtime; TAILSCALE_VERSION overrides
|
||||||
# .eap suffixes used in releases: *_ROOT -> _root, *_acap3 -> _acap3.
|
# the version resolved by ci/upstream-version.sh.
|
||||||
#
|
|
||||||
# Override the container runtime with RUNTIME=docker|podman.
|
|
||||||
# TAILSCALE_VERSION pins the upstream binaries; it defaults to whatever
|
|
||||||
# ci/upstream-version.sh resolves.
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
REPO_ROOT=$(cd -P "$(dirname "$0")" && pwd)
|
REPO_ROOT=$(cd -P "$(dirname "$0")" && pwd)
|
||||||
@@ -57,10 +53,8 @@ fetch_arch() {
|
|||||||
fetch_arch arm arm
|
fetch_arch arm arm
|
||||||
fetch_arch arm64 arm64
|
fetch_arch arm64 arm64
|
||||||
|
|
||||||
# Tailscale is the only upstream here that ships binaries with symbols, so the
|
# Upstream ships unstripped binaries; stripping saves ~23 MB per package. Do it
|
||||||
# strip is worth ~23 MB per package. It runs inside the SDK container: relying
|
# inside the SDK container: without host cross-binutils it silently did nothing.
|
||||||
# on host cross-binutils meant a machine without them silently produced an
|
|
||||||
# unstripped package under the same version number.
|
|
||||||
SDK_IMAGE=axisecp/acap-native-sdk:12.10.0
|
SDK_IMAGE=axisecp/acap-native-sdk:12.10.0
|
||||||
SDK_UBUNTU=ubuntu24.04
|
SDK_UBUNTU=ubuntu24.04
|
||||||
|
|
||||||
|
|||||||
@@ -74,24 +74,29 @@ TAILSCALED_PID=$!
|
|||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
|
# Arguments for `tailscale up`, built as a list so no setting is ever re-parsed
|
||||||
|
# by the shell.
|
||||||
|
set -- --socket="$SOCKET_PATH" up --reset --hostname="$(hostname)"
|
||||||
|
|
||||||
if [ -n "$CUSTOM_SERVER" ]; then
|
if [ -n "$CUSTOM_SERVER" ]; then
|
||||||
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
set -- "$@" --login-server "$CUSTOM_SERVER"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$AUTH_KEY" ]; then
|
if [ -n "$AUTH_KEY" ]; then
|
||||||
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
set -- "$@" --authkey "$AUTH_KEY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$ACCEPT_DNS" = "true" ]; then
|
if [ "$ACCEPT_DNS" = "true" ]; then
|
||||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
|
set -- "$@" --accept-dns=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
set -- "$@" --accept-routes=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# "192.168.1.0/24, 10.0.0.0/24" is a natural way to type the list.
|
||||||
|
ADVERTISE_ROUTES=$(printf '%s' "$ADVERTISE_ROUTES" | tr -d ' \t\r\n')
|
||||||
|
|
||||||
# Advertise LAN subnets so this camera acts as a subnet router. Comma-separated
|
# Advertise LAN subnets so this camera acts as a subnet router. Comma-separated
|
||||||
# CIDRs (e.g. 192.168.1.0/24,10.0.0.0/8). In userspace-networking mode the
|
# CIDRs (e.g. 192.168.1.0/24,10.0.0.0/8). In userspace-networking mode the
|
||||||
# tailscaled netstack forwards tailnet traffic to these subnets, so no kernel IP
|
# tailscaled netstack forwards tailnet traffic to these subnets, so no kernel IP
|
||||||
@@ -103,7 +108,7 @@ if [ -n "$ADVERTISE_ROUTES" ]; then
|
|||||||
echo 1 >/proc/sys/net/ipv4/ip_forward 2>/dev/null || true
|
echo 1 >/proc/sys/net/ipv4/ip_forward 2>/dev/null || true
|
||||||
echo 1 >/proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || true
|
echo 1 >/proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
TAILSCALE_CMD="$TAILSCALE_CMD --advertise-routes=$ADVERTISE_ROUTES"
|
set -- "$@" --advertise-routes="$ADVERTISE_ROUTES"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
||||||
@@ -115,7 +120,7 @@ fi
|
|||||||
# for it from here, because in POSIX sh `wait` only works on children of the
|
# for it from here, because in POSIX sh `wait` only works on children of the
|
||||||
# current shell — a subshell waiting on the parent's child returns 127.
|
# current shell — a subshell waiting on the parent's child returns 127.
|
||||||
{
|
{
|
||||||
eval "$TAILSCALE_CMD"
|
"$TAILSCALE_PATH" "$@"
|
||||||
up_exit=$?
|
up_exit=$?
|
||||||
if [ "$up_exit" -eq 0 ]; then
|
if [ "$up_exit" -eq 0 ]; then
|
||||||
if [ "$VARIANT" = "root" ]; then
|
if [ "$VARIANT" = "root" ]; then
|
||||||
|
|||||||
+20
-44
@@ -351,14 +351,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status -->
|
|
||||||
<div id="status-banner" class="status-banner connecting">
|
<div id="status-banner" class="status-banner connecting">
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span id="status-text" class="status-text">Checking...</span>
|
<span id="status-text" class="status-text">Checking...</span>
|
||||||
<span id="status-time" class="status-time"></span>
|
<span id="status-time" class="status-time"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Update available -->
|
|
||||||
<div id="update-banner" class="update-banner">
|
<div id="update-banner" class="update-banner">
|
||||||
<div class="update-text">Update available: <strong id="update-version"></strong></div>
|
<div class="update-text">Update available: <strong id="update-version"></strong></div>
|
||||||
<a id="update-link" class="update-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
<a id="update-link" class="update-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||||
@@ -367,7 +365,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auth (hidden by default) -->
|
|
||||||
<div id="auth-block" class="auth-block" style="display:none;">
|
<div id="auth-block" class="auth-block" style="display:none;">
|
||||||
<p>Authenticate this device to connect to your Tailscale network:</p>
|
<p>Authenticate this device to connect to your Tailscale network:</p>
|
||||||
<a id="auth-link" class="auth-btn" href="#" target="_blank">
|
<a id="auth-link" class="auth-btn" href="#" target="_blank">
|
||||||
@@ -377,7 +374,6 @@
|
|||||||
<span id="auth-url-text" class="auth-url"></span>
|
<span id="auth-url-text" class="auth-url"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connection Info -->
|
|
||||||
<div class="card" id="info-card" style="display:none;">
|
<div class="card" id="info-card" style="display:none;">
|
||||||
<div class="card-title">Connection Details</div>
|
<div class="card-title">Connection Details</div>
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
@@ -403,7 +399,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Proxy Info (hidden on ROOT builds, which have no local proxy) -->
|
<!-- Hidden on ROOT builds, which have no local proxy -->
|
||||||
<div class="card" id="proxy-info-card" style="display:none;">
|
<div class="card" id="proxy-info-card" style="display:none;">
|
||||||
<div class="card-title">Proxy Configuration</div>
|
<div class="card-title">Proxy Configuration</div>
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
@@ -418,7 +414,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Settings</div>
|
<div class="card-title">Settings</div>
|
||||||
<div class="settings-form">
|
<div class="settings-form">
|
||||||
@@ -474,7 +469,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="log-controls">
|
<div class="log-controls">
|
||||||
<div class="card-title" style="margin-bottom:0;">Service Log</div>
|
<div class="card-title" style="margin-bottom:0;">Service Log</div>
|
||||||
@@ -559,11 +553,11 @@
|
|||||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary: extract hostname from Axis syslog header (always the real device hostname)
|
// The syslog header always carries the real device hostname.
|
||||||
var node = null;
|
var node = null;
|
||||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||||
if (hostLine) node = hostLine[1];
|
if (hostLine) node = hostLine[1];
|
||||||
// Fallback: popBrowserAuthNow/StartLoginInteractiveAs (may contain stale acap-tailscale_vpn)
|
// Fallback; may contain a stale acap-tailscale_vpn name.
|
||||||
if (!node) {
|
if (!node) {
|
||||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||||
@@ -605,7 +599,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse proxy ports from log — use last match so old entries don't win
|
// Use the last match so older log entries don't win.
|
||||||
var httpPort = null;
|
var httpPort = null;
|
||||||
var httpProxyMatches = txt.match(/HTTP\/HTTPS proxy: http:\/\/127\.0\.0\.1:(\d+)/g);
|
var httpProxyMatches = txt.match(/HTTP\/HTTPS proxy: http:\/\/127\.0\.0\.1:(\d+)/g);
|
||||||
if (httpProxyMatches) { var m = httpProxyMatches[httpProxyMatches.length - 1].match(/:(\d+)$/); if (m) httpPort = m[1]; }
|
if (httpProxyMatches) { var m = httpProxyMatches[httpProxyMatches.length - 1].match(/:(\d+)$/); if (m) httpPort = m[1]; }
|
||||||
@@ -613,7 +607,6 @@
|
|||||||
var socksProxyMatches = txt.match(/SOCKS5 proxy:\s+127\.0\.0\.1:(\d+)/g);
|
var socksProxyMatches = txt.match(/SOCKS5 proxy:\s+127\.0\.0\.1:(\d+)/g);
|
||||||
if (socksProxyMatches) { var ms = socksProxyMatches[socksProxyMatches.length - 1].match(/:(\d+)$/); if (ms) socksPort = ms[1]; }
|
if (socksProxyMatches) { var ms = socksProxyMatches[socksProxyMatches.length - 1].match(/:(\d+)$/); if (ms) socksPort = ms[1]; }
|
||||||
|
|
||||||
// Cache when found, restore from cache when missing
|
|
||||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||||
cacheSet('http-port', httpPort); cacheSet('socks-port', socksPort);
|
cacheSet('http-port', httpPort); cacheSet('socks-port', socksPort);
|
||||||
tsIP = tsIP || cacheGet('ip');
|
tsIP = tsIP || cacheGet('ip');
|
||||||
@@ -627,9 +620,8 @@
|
|||||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
var isRunning = /-> Running/.test(lastState);
|
var isRunning = /-> Running/.test(lastState);
|
||||||
|
|
||||||
// Fallbacks only when syslog has rotated and no state transitions are visible.
|
// Only when syslog rotated away all state lines: our own "Tailscale VPN
|
||||||
// If we CAN see state lines (e.g. "-> NeedsLogin"), trust them over our own
|
// is running" line lingers in syslog and must not override them.
|
||||||
// "Tailscale VPN is running" message which stays in syslog indefinitely.
|
|
||||||
if (!isRunning && stateLines.length === 0) {
|
if (!isRunning && stateLines.length === 0) {
|
||||||
isRunning = /Tailscale VPN is running/.test(txt) ||
|
isRunning = /Tailscale VPN is running/.test(txt) ||
|
||||||
/health\(warnable=[^)]+\): ok/.test(txt) ||
|
/health\(warnable=[^)]+\): ok/.test(txt) ||
|
||||||
@@ -638,12 +630,11 @@
|
|||||||
/localapi:/.test(txt);
|
/localapi:/.test(txt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an auth URL appears AFTER the last Running state, re-auth is needed
|
// An auth URL after the last Running state means re-auth is needed
|
||||||
// (handles stale Running entries in syslog after reinstall or token expiry)
|
// (stale Running entries after reinstall or token expiry).
|
||||||
if (isRunning && latestUrl) {
|
if (isRunning && latestUrl) {
|
||||||
// Use the LATEST of '-> Running' (tailscaled state) or 'Tailscale VPN is running'
|
// Our shell log line is written after auth completes, so it post-dates
|
||||||
// (our shell log). The shell log is written AFTER auth completes, so it correctly
|
// the auth URL on success.
|
||||||
// post-dates the auth URL when connection succeeds.
|
|
||||||
var lastRunIdx = txt.lastIndexOf('-> Running');
|
var lastRunIdx = txt.lastIndexOf('-> Running');
|
||||||
var lastRunningMsgIdx = txt.lastIndexOf('Tailscale VPN is running');
|
var lastRunningMsgIdx = txt.lastIndexOf('Tailscale VPN is running');
|
||||||
if (lastRunningMsgIdx > lastRunIdx) lastRunIdx = lastRunningMsgIdx;
|
if (lastRunningMsgIdx > lastRunIdx) lastRunIdx = lastRunningMsgIdx;
|
||||||
@@ -707,7 +698,6 @@
|
|||||||
auth.style.display = 'none';
|
auth.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy card is always visible — update ports whenever known
|
|
||||||
if (r.httpPort) document.getElementById('ts-http-proxy').textContent = 'http://127.0.0.1:' + r.httpPort;
|
if (r.httpPort) document.getElementById('ts-http-proxy').textContent = 'http://127.0.0.1:' + r.httpPort;
|
||||||
if (r.socksPort) document.getElementById('ts-socks-proxy').textContent = '127.0.0.1:' + r.socksPort;
|
if (r.socksPort) document.getElementById('ts-socks-proxy').textContent = '127.0.0.1:' + r.socksPort;
|
||||||
|
|
||||||
@@ -761,7 +751,6 @@
|
|||||||
if (st.Version) result.version = String(st.Version).split('-')[0];
|
if (st.Version) result.version = String(st.Version).split('-')[0];
|
||||||
|
|
||||||
if (bs === 'Running' && self.Online === true) {
|
if (bs === 'Running' && self.Online === true) {
|
||||||
// Genuinely connected and reachable on the tailnet
|
|
||||||
result.state = 'connected';
|
result.state = 'connected';
|
||||||
result.url = null;
|
result.url = null;
|
||||||
result.ip = ip4 || result.ip;
|
result.ip = ip4 || result.ip;
|
||||||
@@ -773,11 +762,8 @@
|
|||||||
result.state = 'connecting';
|
result.state = 'connecting';
|
||||||
result.url = st.AuthURL || result.url;
|
result.url = st.AuthURL || result.url;
|
||||||
} else if (bs === 'Running') {
|
} else if (bs === 'Running') {
|
||||||
// Backend running but node not online: either a transient network
|
// Not online: transient drop or node removed/expired. Keep the log
|
||||||
// drop (no action needed) or the node was removed/expired and needs
|
// parser's URL; status.json's AuthURL lags during re-auth.
|
||||||
// re-auth. Not connected. Keep any login URL the log parser found
|
|
||||||
// (status.json's AuthURL lags during the `tailscale up` re-auth
|
|
||||||
// window) so the login button still appears when re-auth is needed.
|
|
||||||
result.state = 'connecting';
|
result.state = 'connecting';
|
||||||
result.url = st.AuthURL || result.url;
|
result.url = st.AuthURL || result.url;
|
||||||
} else if (bs === 'Stopped') {
|
} else if (bs === 'Stopped') {
|
||||||
@@ -801,7 +787,7 @@
|
|||||||
var st = arr[1];
|
var st = arr[1];
|
||||||
var result = parse(txt || '');
|
var result = parse(txt || '');
|
||||||
if (txt) renderLogs(txt);
|
if (txt) renderLogs(txt);
|
||||||
// Verify the app is actually running - status.json can be stale if stopped
|
// status.json goes stale when the app is stopped.
|
||||||
checkAppRunning().then(function(running) {
|
checkAppRunning().then(function(running) {
|
||||||
if (!running) {
|
if (!running) {
|
||||||
result.state = 'disconnected';
|
result.state = 'disconnected';
|
||||||
@@ -869,7 +855,7 @@
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings — load current param values and save on submit
|
// Settings
|
||||||
var PARAM_URL = '/axis-cgi/param.cgi';
|
var PARAM_URL = '/axis-cgi/param.cgi';
|
||||||
var serverInput = document.getElementById('input-server');
|
var serverInput = document.getElementById('input-server');
|
||||||
var authInput = document.getElementById('input-authkey');
|
var authInput = document.getElementById('input-authkey');
|
||||||
@@ -881,17 +867,11 @@
|
|||||||
var saveBtn = document.getElementById('save-btn');
|
var saveBtn = document.getElementById('save-btn');
|
||||||
var saveStatus = document.getElementById('save-status');
|
var saveStatus = document.getElementById('save-status');
|
||||||
|
|
||||||
// param.cgi is used when available; on devices that lack it (e.g. some
|
// Fallback for devices without param.cgi (e.g. recorders).
|
||||||
// recorder/NVR-class devices) we fall back to the app's own endpoint,
|
|
||||||
// exposed through the manifest reverseProxy mapping at API_URL.
|
|
||||||
var API_URL = '/local/' + APP + '/api/settings';
|
var API_URL = '/local/' + APP + '/api/settings';
|
||||||
|
|
||||||
// Whether this build exposes local HTTP/SOCKS5 proxies (absent on ROOT
|
// False on ROOT builds (no local proxy). Also keeps the ports out of saves:
|
||||||
// builds, which use kernel networking directly). Detected from whichever
|
// param.cgi fails the whole update on a name the manifest never registered.
|
||||||
// settings response actually comes back — set once and used to hide the
|
|
||||||
// proxy card/fields and to keep them out of the save request, since
|
|
||||||
// param.cgi errors the whole call's status line if asked to set a
|
|
||||||
// parameter name the manifest never registered.
|
|
||||||
var hasProxyPorts = false;
|
var hasProxyPorts = false;
|
||||||
|
|
||||||
function toggleProxyUi(visible) {
|
function toggleProxyUi(visible) {
|
||||||
@@ -915,8 +895,7 @@
|
|||||||
var dm = txt.match(/root\.\S+\.AcceptDNS=(.*)/);
|
var dm = txt.match(/root\.\S+\.AcceptDNS=(.*)/);
|
||||||
var rm = txt.match(/root\.\S+\.AcceptRoutes=(.*)/);
|
var rm = txt.match(/root\.\S+\.AcceptRoutes=(.*)/);
|
||||||
var avm = txt.match(/root\.\S+\.AdvertiseRoutes=(.*)/);
|
var avm = txt.match(/root\.\S+\.AdvertiseRoutes=(.*)/);
|
||||||
// If none of the expected keys are present the endpoint isn't param.cgi
|
// No expected keys means this isn't param.cgi output (e.g. a 404 page).
|
||||||
// (e.g. a generic 404 page); signal the caller to use the fallback.
|
|
||||||
if (!sm && !hm && !km) return false;
|
if (!sm && !hm && !km) return false;
|
||||||
if (sm) serverInput.value = sm[1].trim();
|
if (sm) serverInput.value = sm[1].trim();
|
||||||
if (am) authInput.value = am[1].trim();
|
if (am) authInput.value = am[1].trim();
|
||||||
@@ -979,8 +958,7 @@
|
|||||||
.then(function(r) { return r.ok ? r.text() : Promise.reject(); })
|
.then(function(r) { return r.ok ? r.text() : Promise.reject(); })
|
||||||
.then(function(txt) {
|
.then(function(txt) {
|
||||||
if (/OK/.test(txt)) {
|
if (/OK/.test(txt)) {
|
||||||
// The app applies the change and restarts its tunnel itself,
|
// The app restarts its tunnel itself; no control.cgi call needed.
|
||||||
// so no separate control.cgi restart is needed here.
|
|
||||||
setStatus('Saved. Restarting...', 'ok');
|
setStatus('Saved. Restarting...', 'ok');
|
||||||
} else {
|
} else {
|
||||||
setStatus('Error saving settings', 'err');
|
setStatus('Error saving settings', 'err');
|
||||||
@@ -1011,15 +989,13 @@
|
|||||||
.then(function(txt) {
|
.then(function(txt) {
|
||||||
if (/^OK/.test(txt.trim())) {
|
if (/^OK/.test(txt.trim())) {
|
||||||
setStatus('Saved. Restarting...', 'ok');
|
setStatus('Saved. Restarting...', 'ok');
|
||||||
// Restart the app so new settings take effect
|
|
||||||
return fetch('/axis-cgi/applications/control.cgi?action=restart&package=' + APP,
|
return fetch('/axis-cgi/applications/control.cgi?action=restart&package=' + APP,
|
||||||
{ method: 'POST', credentials: 'same-origin' });
|
{ method: 'POST', credentials: 'same-origin' });
|
||||||
}
|
}
|
||||||
// param.cgi reachable but rejected the update — surface the error.
|
|
||||||
setStatus('Error: ' + txt.trim(), 'err');
|
setStatus('Error: ' + txt.trim(), 'err');
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
// param.cgi unavailable (e.g. recorder-class device) — use the fallback.
|
// param.cgi unavailable (e.g. recorder-class device).
|
||||||
return saveViaFallback(httpPort, socksPort);
|
return saveViaFallback(httpPort, socksPort);
|
||||||
})
|
})
|
||||||
.then(function() { saveBtn.disabled = false; })
|
.then(function() { saveBtn.disabled = false; })
|
||||||
|
|||||||
+34
-43
@@ -1,22 +1,10 @@
|
|||||||
// Copyright (C) 2024 Mo3he
|
// Copyright (C) 2024 Mo3he
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* ACAP parameter bridge for Tailscale VPN.
|
* ACAP parameter bridge for Tailscale VPN: mirrors axparameter values into
|
||||||
*
|
* CONFIG_FILE, runs Tailscale_VPN_run as a child and restarts it on changes.
|
||||||
* Responsibilities:
|
* Build with -DHAS_PROXY_PORTS for the userspace variants; ROOT has no proxy.
|
||||||
* 1. Read Tailscale parameters from the ACAP parameter store (axparameter).
|
|
||||||
* 2. Write them to CONFIG_FILE so the shell script can source them.
|
|
||||||
* 3. Launch the shell script (Tailscale_VPN_run) as a child process.
|
|
||||||
* 4. On any parameter change: rewrite CONFIG_FILE and do a full stop+restart
|
|
||||||
* of the child so the new config is picked up.
|
|
||||||
* Rapid changes within 300 ms are coalesced into a single restart.
|
|
||||||
* 5. Watchdog: if the child exits unexpectedly, restart it.
|
|
||||||
*
|
|
||||||
* Shared across the userspace-networking variants (unprivileged 'sdk' ACAP
|
|
||||||
* user) and the ROOT / kernel-networking variant. Build with -DHAS_PROXY_PORTS
|
|
||||||
* for the userspace variants, which exposes the HTTP/SOCKS5 proxy port
|
|
||||||
* parameters; the ROOT variant omits them since it has no local proxy.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <axsdk/axparameter.h>
|
#include <axsdk/axparameter.h>
|
||||||
@@ -70,10 +58,8 @@ static const char *cache_get(char **field, const char *fallback) {
|
|||||||
return (*field && **field) ? *field : fallback;
|
return (*field && **field) ? *field : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure a parameter exists in the device parameter database. On in-place ACAP
|
/* In-place upgrades don't always register new manifest params (param.cgi then
|
||||||
* upgrades a newly introduced manifest parameter is not always auto-registered,
|
* 404s); ax_parameter_add fails harmlessly if the param already exists. */
|
||||||
* which makes param.cgi return a 404 when the web UI tries to set it. Creating
|
|
||||||
* it here is idempotent: if it already exists, ax_parameter_add fails harmlessly. */
|
|
||||||
static void ensure_param(AXParameter *handle, const char *name, const char *def) {
|
static void ensure_param(AXParameter *handle, const char *name, const char *def) {
|
||||||
GError *err = NULL;
|
GError *err = NULL;
|
||||||
if (!ax_parameter_add(handle, name, def, "string", &err)) {
|
if (!ax_parameter_add(handle, name, def, "string", &err)) {
|
||||||
@@ -127,7 +113,7 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
|||||||
int exit_code = WEXITSTATUS(status);
|
int exit_code = WEXITSTATUS(status);
|
||||||
syslog(LOG_WARNING, "child exited (status %d), restarting", exit_code);
|
syslog(LOG_WARNING, "child exited (status %d), restarting", exit_code);
|
||||||
child_pid = -1;
|
child_pid = -1;
|
||||||
/* If child exited 0, auth succeeded — clear AuthKey via axparameter */
|
/* Legacy AuthKey clear: the run script only exits 0 from its TERM/INT trap. */
|
||||||
if (exit_code == 0 && g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
if (exit_code == 0 && g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||||
GError *err = NULL;
|
GError *err = NULL;
|
||||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||||
@@ -147,10 +133,8 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
|||||||
|
|
||||||
/* ── auth-key sentinel ───────────────────────────────────────────────────── */
|
/* ── auth-key sentinel ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
/* The run script drops SENTINEL_FILE after a successful `tailscale up` that
|
/* The run script drops SENTINEL_FILE after `tailscale up` used the auth key.
|
||||||
* used a one-time auth key. Clear the stored AuthKey so it is not reused and
|
* The exit-code-0 path in watchdog_cb rarely fires since the child stays up. */
|
||||||
* disappears from the settings UI. This replaces the old exit-code-0 path,
|
|
||||||
* which never fired because tailscaled keeps the child alive indefinitely. */
|
|
||||||
static gboolean authkey_sentinel_cb(gpointer G_GNUC_UNUSED data) {
|
static gboolean authkey_sentinel_cb(gpointer G_GNUC_UNUSED data) {
|
||||||
if (access(SENTINEL_FILE, F_OK) != 0)
|
if (access(SENTINEL_FILE, F_OK) != 0)
|
||||||
return G_SOURCE_CONTINUE;
|
return G_SOURCE_CONTINUE;
|
||||||
@@ -199,6 +183,19 @@ static void load_config_cache(AXParameter *handle) {
|
|||||||
#undef LOAD
|
#undef LOAD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The run script sources this file, so every value must be a single-quoted
|
||||||
|
* shell literal or it would be executed. */
|
||||||
|
static void write_var(FILE *f, const char *name, const char *value) {
|
||||||
|
fprintf(f, "%s='", name);
|
||||||
|
for (const char *p = value; *p; p++) {
|
||||||
|
if (*p == '\'')
|
||||||
|
fputs("'\\''", f);
|
||||||
|
else
|
||||||
|
fputc(*p, f);
|
||||||
|
}
|
||||||
|
fputs("'\n", f);
|
||||||
|
}
|
||||||
|
|
||||||
static void write_config_file(void) {
|
static void write_config_file(void) {
|
||||||
FILE *f = fopen(CONFIG_FILE, "w");
|
FILE *f = fopen(CONFIG_FILE, "w");
|
||||||
if (!f) {
|
if (!f) {
|
||||||
@@ -206,15 +203,15 @@ static void write_config_file(void) {
|
|||||||
CONFIG_FILE, strerror(errno));
|
CONFIG_FILE, strerror(errno));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
|
write_var(f, "CUSTOM_SERVER", cache_get(&cfg_custom_server, ""));
|
||||||
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
write_var(f, "AUTH_KEY", cache_get(&cfg_auth_key, ""));
|
||||||
#ifdef HAS_PROXY_PORTS
|
#ifdef HAS_PROXY_PORTS
|
||||||
fprintf(f, "CONF_HTTP=%s\n", cache_get(&cfg_http_proxy_port, "8080"));
|
write_var(f, "CONF_HTTP", cache_get(&cfg_http_proxy_port, "8080"));
|
||||||
fprintf(f, "CONF_SOCKS=%s\n", cache_get(&cfg_socks5_port, "1080"));
|
write_var(f, "CONF_SOCKS", cache_get(&cfg_socks5_port, "1080"));
|
||||||
#endif
|
#endif
|
||||||
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
|
write_var(f, "ACCEPT_DNS", cache_get(&cfg_accept_dns, "false"));
|
||||||
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
|
write_var(f, "ACCEPT_ROUTES", cache_get(&cfg_accept_routes, "false"));
|
||||||
fprintf(f, "ADVERTISE_ROUTES=%s\n", cache_get(&cfg_advertise_routes, ""));
|
write_var(f, "ADVERTISE_ROUTES", cache_get(&cfg_advertise_routes, ""));
|
||||||
fclose(f);
|
fclose(f);
|
||||||
chmod(CONFIG_FILE, 0600);
|
chmod(CONFIG_FILE, 0600);
|
||||||
#ifdef HAS_PROXY_PORTS
|
#ifdef HAS_PROXY_PORTS
|
||||||
@@ -264,11 +261,8 @@ static void parameter_changed(const gchar *name, const gchar *value,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── embedded settings HTTP server (reverse-proxy fallback) ──────────────────
|
/* ── embedded settings HTTP server (reverse-proxy fallback) ──────────────────
|
||||||
* Some AXIS device classes (e.g. recorders/NVRs) do not expose the legacy
|
* For devices without /axis-cgi/param.cgi (e.g. recorders); reached via the
|
||||||
* /axis-cgi/param.cgi VAPIX endpoint, so the web UI cannot load or save
|
* manifest reverseProxy at /local/Tailscale_VPN/api/settings. */
|
||||||
* settings through it. This tiny HTTP server, reached through the manifest
|
|
||||||
* reverseProxy mapping at /local/Tailscale_VPN/api/settings, lets the web UI
|
|
||||||
* fall back to reading and writing the parameters directly. */
|
|
||||||
|
|
||||||
#define HTTP_PORT 2201
|
#define HTTP_PORT 2201
|
||||||
|
|
||||||
@@ -331,9 +325,8 @@ static gchar *http_build_settings_json(AXParameter *handle) {
|
|||||||
g_free(val);
|
g_free(val);
|
||||||
}
|
}
|
||||||
g_string_append_c(out, '}');
|
g_string_append_c(out, '}');
|
||||||
/* g_string_free(out, FALSE) is inlined by glib >= 2.76 headers into a call
|
/* Not g_string_free(out, FALSE): glib >= 2.76 headers turn it into
|
||||||
* to g_string_free_and_steal(), which doesn't exist in older glib runtimes
|
* g_string_free_and_steal(), missing from older runtimes (AXIS OS 11.x). */
|
||||||
* (e.g. AXIS OS 11.x). Copy out and fully free instead to stay portable. */
|
|
||||||
gchar *json_result = g_strdup(out->str);
|
gchar *json_result = g_strdup(out->str);
|
||||||
g_string_free(out, TRUE);
|
g_string_free(out, TRUE);
|
||||||
return json_result;
|
return json_result;
|
||||||
@@ -531,11 +524,9 @@ int main(void) {
|
|||||||
openlog(APP_NAME, LOG_PID, LOG_USER);
|
openlog(APP_NAME, LOG_PID, LOG_USER);
|
||||||
syslog(LOG_INFO, "starting");
|
syslog(LOG_INFO, "starting");
|
||||||
|
|
||||||
/* Ensure localdata dir exists */
|
|
||||||
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
|
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
|
||||||
|
|
||||||
/* Drop any stale auth-key sentinel from a previous run so we don't clear a
|
/* A stale sentinel would clear a freshly configured key before use. */
|
||||||
* freshly configured key before it has been used. */
|
|
||||||
unlink(SENTINEL_FILE);
|
unlink(SENTINEL_FILE);
|
||||||
|
|
||||||
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||||
|
|||||||
@@ -346,7 +346,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Hero -->
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="badge">Open Source · ACAP Package</div>
|
<div class="badge">Open Source · ACAP Package</div>
|
||||||
<h1>Tailscale VPN for<br>Axis <span class="hero-word" id="heroWord">Cameras</span></h1>
|
<h1>Tailscale VPN for<br>Axis <span class="hero-word" id="heroWord">Cameras</span></h1>
|
||||||
@@ -363,7 +362,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Disclaimer -->
|
|
||||||
<div style="max-width:720px;margin:0 auto;padding:0 1.5rem 1.5rem;">
|
<div style="max-width:720px;margin:0 auto;padding:0 1.5rem 1.5rem;">
|
||||||
<div style="background:rgba(79,143,247,0.07);border:1px solid rgba(79,143,247,0.18);border-radius:10px;padding:12px 18px;font-size:12.5px;color:var(--muted);line-height:1.6;">
|
<div style="background:rgba(79,143,247,0.07);border:1px solid rgba(79,143,247,0.18);border-radius:10px;padding:12px 18px;font-size:12.5px;color:var(--muted);line-height:1.6;">
|
||||||
<strong style="color:var(--text);">Disclaimer:</strong>
|
<strong style="color:var(--text);">Disclaimer:</strong>
|
||||||
@@ -373,7 +371,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Features -->
|
|
||||||
<section id="features" class="features">
|
<section id="features" class="features">
|
||||||
<div class="feature-card">
|
<div class="feature-card">
|
||||||
<div class="icon">🔒</div>
|
<div class="icon">🔒</div>
|
||||||
@@ -407,12 +404,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Downloads -->
|
|
||||||
<section id="downloads" class="downloads">
|
<section id="downloads" class="downloads">
|
||||||
<h2>Download</h2>
|
<h2>Download</h2>
|
||||||
<p class="subtitle">Pick the right variant for your device and Axis OS version.</p>
|
<p class="subtitle">Pick the right variant for your device and Axis OS version.</p>
|
||||||
<div class="download-grid">
|
<div class="download-grid">
|
||||||
<!-- Standard -->
|
|
||||||
<div class="download-card">
|
<div class="download-card">
|
||||||
<div class="tag tag-recommended">Recommended</div>
|
<div class="tag tag-recommended">Recommended</div>
|
||||||
<h3>Standard (Non-Root)</h3>
|
<h3>Standard (Non-Root)</h3>
|
||||||
@@ -428,7 +423,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- ROOT -->
|
|
||||||
<div class="download-card">
|
<div class="download-card">
|
||||||
<div class="tag tag-root">Root · Legacy</div>
|
<div class="tag tag-root">Root · Legacy</div>
|
||||||
<h3>Root (Full Networking)</h3>
|
<h3>Root (Full Networking)</h3>
|
||||||
@@ -444,7 +438,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- ACAP3 -->
|
|
||||||
<div class="download-card">
|
<div class="download-card">
|
||||||
<div class="tag tag-acap3">ACAP3 · Legacy</div>
|
<div class="tag tag-acap3">ACAP3 · Legacy</div>
|
||||||
<h3>ACAP3 (Older Axis OS)</h3>
|
<h3>ACAP3 (Older Axis OS)</h3>
|
||||||
@@ -459,7 +452,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Install -->
|
|
||||||
<section id="install" class="install">
|
<section id="install" class="install">
|
||||||
<h2>Installation</h2>
|
<h2>Installation</h2>
|
||||||
<div class="steps">
|
<div class="steps">
|
||||||
@@ -487,7 +479,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="https://github.com/Mo3he/Axis_Cam_Tailscale" target="_blank" rel="noopener">GitHub</a>
|
<a href="https://github.com/Mo3he/Axis_Cam_Tailscale" target="_blank" rel="noopener">GitHub</a>
|
||||||
@@ -540,7 +531,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rotate hero word
|
|
||||||
var heroWord = document.getElementById('heroWord');
|
var heroWord = document.getElementById('heroWord');
|
||||||
var devices = ['Cameras', 'Door Stations', 'Intercoms', 'Speakers', 'Radars', 'Encoders'];
|
var devices = ['Cameras', 'Door Stations', 'Intercoms', 'Speakers', 'Radars', 'Encoders'];
|
||||||
var wordIdx = 0;
|
var wordIdx = 0;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"dependencyDashboard": true,
|
||||||
|
"prConcurrentLimit": 3,
|
||||||
|
"prHourlyLimit": 0,
|
||||||
|
"schedule": ["before 5am on Monday"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "An SDK bump raises the package's minimum AXIS OS, so decide it deliberately.",
|
||||||
|
"matchDatasources": ["docker"],
|
||||||
|
"matchPackageNames": [
|
||||||
|
"axisecp/acap-native-sdk",
|
||||||
|
"axisecp/acap-sdk",
|
||||||
|
"docker.io/axisecp/acap-native-sdk",
|
||||||
|
"docker.io/axisecp/acap-sdk"
|
||||||
|
],
|
||||||
|
"dependencyDashboardApproval": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Transitive Go modules are off by default; enable so CVE fixes can land.",
|
||||||
|
"matchManagers": ["gomod"],
|
||||||
|
"matchDepTypes": ["indirect"],
|
||||||
|
"enabled": true,
|
||||||
|
"groupName": "Go indirect dependencies",
|
||||||
|
"groupSlug": "go-indirect-dependencies"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user