mirror of
https://github.com/Mo3he/Axis_Cam_Tailscale.git
synced 2026-10-01 03:55:40 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47c3894002 |
@@ -0,0 +1,12 @@
|
||||
# All variants build from the repository root (docker build -f <variant>/Dockerfile .)
|
||||
# so keep the context lean. Do NOT exclude common/app/ or <variant>/app/ — the
|
||||
# Dockerfiles COPY those, including the Tailscale binaries placed in app/lib/.
|
||||
.git
|
||||
.github
|
||||
.DS_Store
|
||||
*.eap
|
||||
*.tgz
|
||||
build
|
||||
releases
|
||||
tailscale_bins
|
||||
README.md
|
||||
@@ -110,17 +110,27 @@ jobs:
|
||||
mkdir -p releases
|
||||
|
||||
for folder in */ ; do
|
||||
[[ ! -d "$folder/app" ]] && continue
|
||||
FOLDER_NAME="${folder%/}" # remove trailing slash
|
||||
[[ "$FOLDER_NAME" == "common" ]] && continue
|
||||
[[ ! -d "$folder/app" ]] && continue
|
||||
echo "Processing folder $FOLDER_NAME"
|
||||
|
||||
# aarch64/arm/aarch64_ROOT/arm_ROOT share their C source, run script,
|
||||
# HTML, and Makefile via common/app/ (see Dockerfile COPY layers);
|
||||
# only arm_acap3 still carries its own self-contained app/ tree.
|
||||
case "$FOLDER_NAME" in
|
||||
aarch64|arm|aarch64_ROOT|arm_ROOT) APP_LIB_DIR="common/app/lib" ;;
|
||||
*) APP_LIB_DIR="$folder/app/lib" ;;
|
||||
esac
|
||||
mkdir -p "$APP_LIB_DIR"
|
||||
|
||||
# Detect architecture
|
||||
if [[ "$FOLDER_NAME" == arm* ]]; then
|
||||
cp tailscale_bins/tailscale_arm "$folder/app/lib/tailscale"
|
||||
cp tailscale_bins/tailscaled_arm "$folder/app/lib/tailscaled"
|
||||
cp tailscale_bins/tailscale_arm "$APP_LIB_DIR/tailscale"
|
||||
cp tailscale_bins/tailscaled_arm "$APP_LIB_DIR/tailscaled"
|
||||
else
|
||||
cp tailscale_bins/tailscale_arm64 "$folder/app/lib/tailscale"
|
||||
cp tailscale_bins/tailscaled_arm64 "$folder/app/lib/tailscaled"
|
||||
cp tailscale_bins/tailscale_arm64 "$APP_LIB_DIR/tailscale"
|
||||
cp tailscale_bins/tailscaled_arm64 "$APP_LIB_DIR/tailscaled"
|
||||
fi
|
||||
|
||||
# Detect variant suffix for .eap naming
|
||||
@@ -145,7 +155,7 @@ jobs:
|
||||
# Docker build
|
||||
TAG_NAME=$(echo "$FOLDER_NAME" | tr '[:upper:]' '[:lower:]' | tr '/ ' '_') # lowercase and clean
|
||||
echo "Building $TAG_NAME"
|
||||
docker build -f "$folder/Dockerfile" --tag "$TAG_NAME" "$folder"
|
||||
docker build -f "$folder/Dockerfile" --tag "$TAG_NAME" .
|
||||
|
||||
# Extract .eap files into build folder
|
||||
EAP_OUTPUT="./build/${TAG_NAME}"
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
**/.DS_Store
|
||||
**/build
|
||||
|
||||
# Do not track release artifacts
|
||||
# Do not track release artifacts (local .eap build outputs stay untracked anywhere in the tree)
|
||||
releases/
|
||||
build/
|
||||
*.eap
|
||||
|
||||
# Do not track downloaded Tailscale tarballs and temp bins
|
||||
tailscale_bins/
|
||||
|
||||
@@ -6,7 +6,7 @@ This repository provides an **ACAP package** that installs the [Tailscale VPN cl
|
||||
|
||||
- Secure remote access to cameras
|
||||
- Easy to install via EAP package
|
||||
- Works on **Axis OS 11.11+** (non-root version)
|
||||
- Works on **Axis OS 10.12+** (non-root version, verified across 10.12–12.10)
|
||||
- Works on **legacy Axis OS 9.x / 10.x** via the ACAP 3 variant
|
||||
- Based on **WireGuard VPN** technology
|
||||
|
||||
@@ -140,7 +140,7 @@ Then, in the camera's **System → Storage → Add network share** dialog, use `
|
||||
|
||||
### Manual update (advanced)
|
||||
|
||||
Replace the binaries in the `lib/` folder:
|
||||
Replace the binaries in `common/app/lib/` (shared by `aarch64`, `armv7hf`, and their ROOT variants) or `arm_acap3/app/lib/` (legacy variant, kept separate):
|
||||
- `tailscale`
|
||||
- `tailscaled`
|
||||
|
||||
@@ -148,22 +148,26 @@ Download the latest versions: [Tailscale static builds](https://pkgs.tailscale.c
|
||||
|
||||
#### Build locally
|
||||
|
||||
From the main directory of the version you want (`arm` / `aarch64`):
|
||||
The Tailscale binaries are not stored in git, so first download them (see [Manual update](#manual-update-advanced) above) and place them in `common/app/lib/` — or `arm_acap3/app/lib/` for the legacy variant.
|
||||
|
||||
All variants build from the **repository root**, pointing at the variant's own `Dockerfile`:
|
||||
|
||||
```bash
|
||||
docker build --tag <package_name> .
|
||||
docker build -f aarch64/Dockerfile --tag <package_name> .
|
||||
docker cp $(docker create <package_name>):/opt/app ./build
|
||||
```
|
||||
|
||||
(Same for the others — just swap in `arm/Dockerfile`, `aarch64_ROOT/Dockerfile`, `arm_ROOT/Dockerfile`, or `arm_acap3/Dockerfile`.)
|
||||
|
||||
---
|
||||
|
||||
## Good News
|
||||
|
||||
Tailscale ACAP can now run **without root privileges**, making it compatible with **Axis OS 11.11+**.
|
||||
Tailscale ACAP can now run **without root privileges**, making it compatible with **Axis OS 10.12+** — verified working across Axis OS 10.12, 11.11, and 12.10.
|
||||
|
||||
- Runs in **user space networking mode**.
|
||||
|
||||
For **full kernel networking**, use the **ROOT** version. Note: ROOT mode requires Axis OS 11.11–11.x — Axis OS 12 and later removed root access for third-party applications.
|
||||
For **full kernel networking**, use the **ROOT** version. Note: ROOT mode requires Axis OS 10.12–11.x — Axis OS 12 and later removed root access for third-party applications.
|
||||
|
||||
### Legacy camera support (Axis OS 9.x / 10.x)
|
||||
|
||||
@@ -196,13 +200,15 @@ The Tailscale ACAP is compatible with Axis cameras with **ARM** and **AARCH64**-
|
||||
|
||||
| Variant | Architecture | Axis OS | Notes |
|
||||
|---|---|---|---|
|
||||
| `aarch64` | AArch64 | 11.11+ (ACAP 4) | Standard, userspace networking, configurable proxy ports |
|
||||
| `armv7hf` | ARMv7 | 11.11+ (ACAP 4) | Standard, userspace networking, configurable proxy ports |
|
||||
| `aarch64_root` | AArch64 | 11.11 – 11.x (ACAP 4) | Full kernel networking (root) — not supported on OS 12+ |
|
||||
| `armv7hf_root` | ARMv7 | 11.11 – 11.x (ACAP 4) | Full kernel networking (root) — not supported on OS 12+ |
|
||||
| `aarch64` | AArch64 | 10.12 – 13 (ACAP 4) | Standard, userspace networking, configurable proxy ports |
|
||||
| `armv7hf` | ARMv7 | 10.12 – 13 (ACAP 4) | Standard, userspace networking, configurable proxy ports |
|
||||
| `aarch64_root` | AArch64 | 10.12 – 11.x (ACAP 4) | Full kernel networking (root) — not supported on OS 12+ |
|
||||
| `armv7hf_root` | ARMv7 | 10.12 – 11.x (ACAP 4) | Full kernel networking (root) — not supported on OS 12+ |
|
||||
| `armv7hf_acap3` | ARMv7 | **9.x – 10.x** | Legacy cameras, ACAP SDK 3 |
|
||||
|
||||
> Not sure which variant to use? Check **System → Properties → Firmware version** on your camera. Axis OS 12+ → use the standard variant (`aarch64` or `armv7hf`). Axis OS 11.11–11.x → standard variant, or ROOT if you need kernel networking. Axis OS 9.x/10.x on ARMv7 → use `armv7hf_acap3`.
|
||||
> Not sure which variant to use? Check **System → Properties → Firmware version** on your camera. Axis OS 12+ → use the standard variant (`aarch64` or `armv7hf`). Axis OS 10.12–11.x → standard variant works too, or ROOT if you need kernel networking. Axis OS 9.x → use `armv7hf_acap3`.
|
||||
>
|
||||
> The standard variant's floor was verified by live-testing the same build on Axis OS 10.12.300, 11.11.212, and 12.10.68 — it is not limited to 11.11+ as earlier releases implied. The ROOT variant was also verified on Axis OS 10.12.300 with genuine kernel networking confirmed over SSH (processes running as `root`, a real `tailscale0` kernel interface present, and `ip_forward` correctly toggling on when subnet routes are advertised) — Axis OS 10.x ran third-party apps as root by default, before the privilege sandboxing introduced later, so ROOT was never actually limited to 11.11+.
|
||||
|
||||
You can verify your device details using the following command:
|
||||
|
||||
@@ -221,11 +227,11 @@ curl --anyauth "*" -u <username>:<password> <device_ip>/axis-cgi/basicdeviceinfo
|
||||
|
||||
AXIS OS 13 (scheduled for September 2026) introduces several breaking changes that affect all ACAP applications. The following items are required to maintain compatibility. See the full [AXIS OS 13 breaking changes](https://www.axis.com/for-developers/news/AXIS-OS-13-breaking-changes) announcement for details.
|
||||
|
||||
- [ ] **Recompile for 64-bit time (Y2038)** - AXIS OS 13 switches to a 64-bit time interface. All ACAP apps must be recompiled against the updated SDK. Cameras with incompatible apps installed will roll back the OS upgrade rather than proceed.
|
||||
- [ ] **Sign the ACAP via the Axis ACAP Portal** - AXIS OS 13 removes the ability to install unsigned applications in production environments. The app must be submitted and signed through the official Axis ACAP Portal to remain installable.
|
||||
- [ ] **Migrate to Manifest Schema v2** - The `manifest.json` must use Manifest Schema v2, including an explicit declaration of compatible AXIS OS versions, to satisfy the new signing and compatibility requirements.
|
||||
- [ ] **Audit for executable stack usage** - Any ACAP compiled with an executable stack must be recompiled to comply with the new security restrictions in AXIS OS 13.
|
||||
- [ ] **Verify web UI works over HTTPS** - AXIS OS 13 enforces HTTPS-only connections by default. The bundled web UI must be tested to confirm it functions correctly under this constraint.
|
||||
- [x] **Recompile for 64-bit time (Y2038)** - AXIS OS 13 switches to a 64-bit time interface. All ACAP apps must be recompiled against the updated SDK. Cameras with incompatible apps installed will roll back the OS upgrade rather than proceed. Done for the standard `aarch64`/`armv7hf` builds (now built against ACAP Native SDK 12.10.0); the ROOT variants intentionally stay on the older SDK since Axis OS 12+ never supports root third-party apps, so they can never reach OS 13 regardless.
|
||||
- [x] **Migrate to Manifest Schema v2** - The `manifest.json` must use Manifest Schema v2, including an explicit declaration of compatible AXIS OS versions, to satisfy the new signing and compatibility requirements. Done for `aarch64`/`armv7hf` (schema 2.0.0, `compatibleOsVersions` declared); verified this does not break installability on older firmware (OS 10.12–12.10 all tested and working) before promoting it as the standard build.
|
||||
- [x] **Audit for executable stack usage** - Any ACAP compiled with an executable stack must be recompiled to comply with the new security restrictions in AXIS OS 13. Checked all compiled binaries (`param_bridge` for `aarch64`/`armv7hf`, both standard and ROOT, plus the bundled `tailscale`/`tailscaled` Go binaries) via `objdump`'s `GNU_STACK` program header — all report `flags rw-` (no executable stack) on every architecture and variant.
|
||||
- [x] **Verify web UI works over HTTPS** - AXIS OS 13 enforces HTTPS-only connections by default. The bundled web UI must be tested to confirm it functions correctly under this constraint. Verified live: the page and every endpoint it calls (`param.cgi` GET/update, the `reverseProxy` settings API GET/POST, `applications/list.cgi`, `systemlog.cgi`, the restart trigger) all work correctly over HTTPS. The UI only ever issues relative-path requests (no hardcoded `http://` fetch targets), so it inherits the page's own protocol with no mixed-content risk.
|
||||
- [ ] **Sign the ACAP via the Axis ACAP Portal** - AXIS OS 13 removes the ability to install unsigned applications in production environments. The app must be submitted and signed through the official Axis ACAP Portal to remain installable. Deferred for now — the manifest's `vendorId` is a placeholder value, not yet a portal-registered one.
|
||||
|
||||
### General Improvements
|
||||
|
||||
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
ARG ARCH=aarch64
|
||||
ARG VERSION=1.15.1
|
||||
ARG UBUNTU_VERSION=22.04
|
||||
ARG VERSION=12.10.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/
|
||||
COPY common/app /opt/app/
|
||||
COPY aarch64/app/manifest.json /opt/app/manifest.json
|
||||
WORKDIR /opt/app
|
||||
ENV EXTRA_CFLAGS=-DHAS_PROXY_PORTS
|
||||
RUN aarch64-linux-gnu-strip -s lib/tailscale lib/tailscaled
|
||||
RUN . /opt/axis/acapsdk/environment-setup* && acap-build -a Tailscale_VPN_run ./
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -1,15 +1,20 @@
|
||||
{
|
||||
"schemaVersion": "1.7.0",
|
||||
"schemaVersion": "2.0.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"vendorId": "5741c1fb91",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.98.8",
|
||||
"architecture": "aarch64"
|
||||
"architecture": "aarch64",
|
||||
"runMode": "respawn",
|
||||
"compatibleOsVersions": [
|
||||
{
|
||||
"max": "13"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
|
||||
@@ -7,7 +7,8 @@ ARG SDK=acap-native-sdk
|
||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||
|
||||
# Building the ACAP application
|
||||
COPY ./app /opt/app/
|
||||
COPY common/app /opt/app/
|
||||
COPY aarch64_ROOT/app/manifest.json /opt/app/manifest.json
|
||||
WORKDIR /opt/app
|
||||
RUN aarch64-linux-gnu-strip -s lib/tailscale lib/tailscaled
|
||||
RUN . /opt/axis/acapsdk/environment-setup* && acap-build -a Tailscale_VPN_run ./
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,14 +0,0 @@
|
||||
PROG = Tailscale_VPN
|
||||
SRCS = param_bridge.c
|
||||
PKGS = axparameter glib-2.0 gio-2.0
|
||||
CFLAGS += $(shell pkg-config --cflags $(PKGS))
|
||||
LDADD = $(shell pkg-config --libs $(PKGS))
|
||||
|
||||
all: $(PROG)
|
||||
chmod +x Tailscale_VPN_run
|
||||
|
||||
$(PROG): $(SRCS)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDADD)
|
||||
|
||||
clean:
|
||||
rm -f $(PROG)
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Tailscale VPN run script (ROOT / kernel networking variant).
|
||||
# Sources config from params.conf written by param_bridge.
|
||||
killall tailscaled 2>/dev/null || true
|
||||
|
||||
APP_DIR="/usr/local/packages/Tailscale_VPN"
|
||||
STATE_DIR="$APP_DIR/localdata"
|
||||
TAILSCALED_PATH="$APP_DIR/lib/tailscaled"
|
||||
TAILSCALE_PATH="$APP_DIR/lib/tailscale"
|
||||
SOCKET_PATH="$STATE_DIR/tailscaled.sock"
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
chmod 755 $TAILSCALED_PATH
|
||||
chmod 755 $TAILSCALE_PATH
|
||||
|
||||
CUSTOM_SERVER=""
|
||||
AUTH_KEY=""
|
||||
ACCEPT_DNS="false"
|
||||
ACCEPT_ROUTES="false"
|
||||
ADVERTISE_ROUTES=""
|
||||
|
||||
if [ -f "$STATE_DIR/params.conf" ]; then
|
||||
. "$STATE_DIR/params.conf"
|
||||
fi
|
||||
|
||||
logger -t "Tailscale_VPN" "Starting (root mode): custom_server=${CUSTOM_SERVER:-(default)}"
|
||||
|
||||
$TAILSCALED_PATH \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket=$SOCKET_PATH \
|
||||
>/dev/null 2>&1 &
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
|
||||
|
||||
if [ -n "$CUSTOM_SERVER" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
||||
fi
|
||||
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
||||
fi
|
||||
|
||||
if [ "$ACCEPT_DNS" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
|
||||
fi
|
||||
|
||||
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||
fi
|
||||
|
||||
# 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 kernel-networking (root) mode the
|
||||
# host must forward packets between the tailnet and the LAN, so enable IP
|
||||
# forwarding. Routes must still be approved in the Tailscale admin console.
|
||||
if [ -n "$ADVERTISE_ROUTES" ]; then
|
||||
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
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --advertise-routes=$ADVERTISE_ROUTES"
|
||||
fi
|
||||
|
||||
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
||||
# (re-)authentication, `up` blocks until the user logs in; backgrounding it
|
||||
# ensures the status publisher below keeps running so the UI can surface the
|
||||
# login URL (tailscaled reports BackendState=NeedsLogin + AuthURL while waiting).
|
||||
# NOTE: `up` runs synchronously *inside* this backgrounded block so its real exit
|
||||
# code is captured directly. We must NOT background `up` separately and `wait`
|
||||
# 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.
|
||||
{
|
||||
eval "$TAILSCALE_CMD"
|
||||
up_exit=$?
|
||||
if [ "$up_exit" -eq 0 ]; then
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running (root mode)"
|
||||
# Auth succeeded with a one-time auth key — signal param_bridge to clear it
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
: > "$STATE_DIR/authkey_clear"
|
||||
fi
|
||||
else
|
||||
logger -t "Tailscale_VPN" "ERROR: tailscale up failed (exit $up_exit)"
|
||||
fi
|
||||
} &
|
||||
TAILSCALE_UP_PID=$!
|
||||
|
||||
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||
# TailscaleIPs / AuthURL) instead of scraping syslog. Served statically at
|
||||
# /local/Tailscale_VPN/status.json.
|
||||
STATUS_FILE="$APP_DIR/html/status.json"
|
||||
|
||||
publish_status() {
|
||||
if "$TAILSCALE_PATH" --socket="$SOCKET_PATH" status --json > "$STATUS_FILE.tmp" 2>/dev/null; then
|
||||
mv "$STATUS_FILE.tmp" "$STATUS_FILE" 2>/dev/null
|
||||
chmod 644 "$STATUS_FILE" 2>/dev/null
|
||||
else
|
||||
rm -f "$STATUS_FILE.tmp" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
status_loop() {
|
||||
while true; do
|
||||
publish_status
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
status_loop &
|
||||
STATUS_LOOP_PID=$!
|
||||
|
||||
# Clean up the status writer, up watcher, daemon and published status on
|
||||
# stop/restart so param_bridge (which signals this script) leaves no orphans or
|
||||
# stale state.
|
||||
cleanup() {
|
||||
[ -n "$STATUS_LOOP_PID" ] && kill "$STATUS_LOOP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALE_UP_PID" ] && kill "$TAILSCALE_UP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALED_PID" ] && kill "$TAILSCALED_PID" 2>/dev/null
|
||||
rm -f "$STATUS_FILE" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap cleanup TERM INT
|
||||
|
||||
wait $TAILSCALED_PID
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,507 +0,0 @@
|
||||
// Copyright (C) 2024 Mo3he
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* ACAP parameter bridge for Tailscale VPN (ROOT / kernel networking variant).
|
||||
* Same structure as regular param_bridge.c but without proxy port params.
|
||||
*/
|
||||
|
||||
#include <axsdk/axparameter.h>
|
||||
#include <glib-unix.h>
|
||||
#include <gio/gio.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>
|
||||
#include <signal.h>
|
||||
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define SENTINEL_FILE "/usr/local/packages/Tailscale_VPN/localdata/authkey_clear"
|
||||
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
static pid_t child_pid = -1;
|
||||
static guint reload_timer_id = 0;
|
||||
|
||||
static char *cfg_custom_server = NULL;
|
||||
static char *cfg_auth_key = NULL;
|
||||
static char *cfg_accept_dns = NULL;
|
||||
static char *cfg_accept_routes = NULL;
|
||||
static char *cfg_advertise_routes = NULL;
|
||||
|
||||
static void cache_set(char **field, const char *value) {
|
||||
if (!value) return;
|
||||
free(*field);
|
||||
*field = strdup(value);
|
||||
}
|
||||
|
||||
static const char *cache_get(char **field, const char *fallback) {
|
||||
return (*field && **field) ? *field : fallback;
|
||||
}
|
||||
|
||||
/* Ensure a parameter exists in the device parameter database. On in-place ACAP
|
||||
* upgrades a newly introduced manifest parameter is not always auto-registered,
|
||||
* 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) {
|
||||
GError *err = NULL;
|
||||
if (!ax_parameter_add(handle, name, def, "string", &err)) {
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_child(void) {
|
||||
if (child_pid <= 0)
|
||||
return;
|
||||
kill(child_pid, SIGTERM);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int status;
|
||||
if (waitpid(child_pid, &status, WNOHANG) == child_pid) {
|
||||
child_pid = -1;
|
||||
return;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
syslog(LOG_WARNING, "child did not exit in 3 s, sending SIGKILL");
|
||||
kill(child_pid, SIGKILL);
|
||||
waitpid(child_pid, NULL, 0);
|
||||
child_pid = -1;
|
||||
}
|
||||
|
||||
static void start_child(void) {
|
||||
stop_child();
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
syslog(LOG_ERR, "fork failed: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
if (pid == 0) {
|
||||
execl(RUN_SCRIPT, RUN_SCRIPT, NULL);
|
||||
syslog(LOG_ERR, "execl %s failed: %s", RUN_SCRIPT, strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
child_pid = pid;
|
||||
syslog(LOG_INFO, "started %s (pid %d)", RUN_SCRIPT, child_pid);
|
||||
}
|
||||
|
||||
static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
||||
if (child_pid > 0) {
|
||||
int status;
|
||||
pid_t ret = waitpid(child_pid, &status, WNOHANG);
|
||||
if (ret == child_pid) {
|
||||
int exit_code = WEXITSTATUS(status);
|
||||
syslog(LOG_WARNING, "child exited (status %d), restarting", exit_code);
|
||||
child_pid = -1;
|
||||
/* If child exited 0, auth succeeded — clear AuthKey via axparameter */
|
||||
if (exit_code == 0 && g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
start_child();
|
||||
}
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* The run script drops SENTINEL_FILE after a successful `tailscale up` that
|
||||
* used a one-time auth key. Clear the stored AuthKey so it is not reused and
|
||||
* 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) {
|
||||
if (access(SENTINEL_FILE, F_OK) != 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
||||
if (g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth (sentinel)");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
unlink(SENTINEL_FILE);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static void load_config_cache(AXParameter *handle) {
|
||||
GError *error = NULL;
|
||||
gchar *val = NULL;
|
||||
|
||||
#define LOAD(name, field) \
|
||||
val = NULL; error = NULL; \
|
||||
if (ax_parameter_get(handle, name, &val, &error)) { \
|
||||
free(field); field = val ? strdup(val) : strdup(""); \
|
||||
g_free(val); val = NULL; \
|
||||
} else { \
|
||||
syslog(LOG_WARNING, "ax_parameter_get %s failed: %s", name, \
|
||||
error ? error->message : "unknown"); \
|
||||
if (error) { g_error_free(error); error = NULL; } \
|
||||
}
|
||||
|
||||
LOAD("CustomServer", cfg_custom_server)
|
||||
LOAD("AuthKey", cfg_auth_key)
|
||||
LOAD("AcceptDNS", cfg_accept_dns)
|
||||
LOAD("AcceptRoutes", cfg_accept_routes)
|
||||
LOAD("AdvertiseRoutes", cfg_advertise_routes)
|
||||
#undef LOAD
|
||||
}
|
||||
|
||||
static void write_config_file(void) {
|
||||
FILE *f = fopen(CONFIG_FILE, "w");
|
||||
if (!f) {
|
||||
syslog(LOG_ERR, "cannot open config file %s: %s",
|
||||
CONFIG_FILE, strerror(errno));
|
||||
return;
|
||||
}
|
||||
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
|
||||
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
||||
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
|
||||
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
|
||||
fprintf(f, "ADVERTISE_ROUTES=%s\n", cache_get(&cfg_advertise_routes, ""));
|
||||
fclose(f);
|
||||
chmod(CONFIG_FILE, 0600);
|
||||
syslog(LOG_INFO, "config updated: server=%s",
|
||||
cache_get(&cfg_custom_server, "(default)"));
|
||||
}
|
||||
|
||||
static gboolean debounced_restart(gpointer G_GNUC_UNUSED data) {
|
||||
reload_timer_id = 0;
|
||||
if (g_ax_handle)
|
||||
load_config_cache(g_ax_handle);
|
||||
write_config_file();
|
||||
syslog(LOG_INFO, "restarting with new config");
|
||||
stop_child();
|
||||
start_child();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void parameter_changed(const gchar *name, const gchar *value,
|
||||
gpointer G_GNUC_UNUSED handle_void_ptr) {
|
||||
const char *dot = strrchr(name, '.');
|
||||
const char *short_name = dot ? dot + 1 : name;
|
||||
syslog(LOG_INFO, "parameter changed: %s", short_name);
|
||||
|
||||
if (strcmp(short_name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||
else if (strcmp(short_name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||
else if (strcmp(short_name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||
else if (strcmp(short_name, "AdvertiseRoutes") == 0) cache_set(&cfg_advertise_routes, value);
|
||||
|
||||
if (reload_timer_id)
|
||||
g_source_remove(reload_timer_id);
|
||||
reload_timer_id = g_timeout_add(300, debounced_restart, NULL);
|
||||
}
|
||||
|
||||
/* ── embedded settings HTTP server (reverse-proxy fallback) ──────────────────
|
||||
* Some AXIS device classes (e.g. recorders/NVRs) do not expose the legacy
|
||||
* /axis-cgi/param.cgi VAPIX endpoint, so the web UI cannot load or save
|
||||
* 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
|
||||
|
||||
static const char *http_param_names[] = {
|
||||
"CustomServer", "AuthKey", "AcceptDNS", "AcceptRoutes", "AdvertiseRoutes"
|
||||
};
|
||||
|
||||
static void cache_set_by_name(const char *name, const char *value) {
|
||||
if (strcmp(name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||
else if (strcmp(name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||
else if (strcmp(name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||
else if (strcmp(name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||
else if (strcmp(name, "AdvertiseRoutes") == 0) cache_set(&cfg_advertise_routes, value);
|
||||
}
|
||||
|
||||
static int http_is_known_param(const char *name) {
|
||||
for (size_t i = 0; i < G_N_ELEMENTS(http_param_names); i++)
|
||||
if (strcmp(name, http_param_names[i]) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void http_json_append_escaped(GString *out, const char *s) {
|
||||
for (const char *p = s; *p; p++) {
|
||||
switch (*p) {
|
||||
case '"': g_string_append(out, "\\\""); break;
|
||||
case '\\': g_string_append(out, "\\\\"); break;
|
||||
case '\n': g_string_append(out, "\\n"); break;
|
||||
case '\r': g_string_append(out, "\\r"); break;
|
||||
case '\t': g_string_append(out, "\\t"); break;
|
||||
default:
|
||||
if ((unsigned char)*p < 0x20)
|
||||
g_string_append_printf(out, "\\u%04x", (unsigned char)*p);
|
||||
else
|
||||
g_string_append_c(out, *p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static gchar *http_build_settings_json(AXParameter *handle) {
|
||||
GString *out = g_string_new("{");
|
||||
for (size_t i = 0; i < G_N_ELEMENTS(http_param_names); i++) {
|
||||
gchar *val = NULL;
|
||||
GError *err = NULL;
|
||||
if (!ax_parameter_get(handle, http_param_names[i], &val, &err)) {
|
||||
if (err) g_error_free(err);
|
||||
val = g_strdup("");
|
||||
}
|
||||
if (i) g_string_append_c(out, ',');
|
||||
g_string_append_printf(out, "\"%s\":\"", http_param_names[i]);
|
||||
http_json_append_escaped(out, val ? val : "");
|
||||
g_string_append_c(out, '"');
|
||||
g_free(val);
|
||||
}
|
||||
g_string_append_c(out, '}');
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
static gchar *http_url_decode(const char *s, size_t len) {
|
||||
GString *out = g_string_new(NULL);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = s[i];
|
||||
if (c == '+') {
|
||||
g_string_append_c(out, ' ');
|
||||
} else if (c == '%' && i + 2 < len &&
|
||||
g_ascii_isxdigit(s[i + 1]) && g_ascii_isxdigit(s[i + 2])) {
|
||||
int hi = g_ascii_xdigit_value(s[i + 1]);
|
||||
int lo = g_ascii_xdigit_value(s[i + 2]);
|
||||
g_string_append_c(out, (char)((hi << 4) | lo));
|
||||
i += 2;
|
||||
} else {
|
||||
g_string_append_c(out, c);
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
/* Apply an application/x-www-form-urlencoded body of shortName=value pairs to
|
||||
* the parameter store. Returns the number of parameters successfully set. */
|
||||
static int http_apply_settings(AXParameter *handle, const char *body, size_t len) {
|
||||
int applied = 0;
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= len; i++) {
|
||||
if (i == len || body[i] == '&') {
|
||||
size_t seg_len = i - start;
|
||||
if (seg_len > 0) {
|
||||
const char *seg = body + start;
|
||||
const char *eq = memchr(seg, '=', seg_len);
|
||||
if (eq) {
|
||||
size_t nlen = (size_t)(eq - seg);
|
||||
gchar *name = g_strndup(seg, nlen);
|
||||
gchar *value = http_url_decode(eq + 1, seg_len - nlen - 1);
|
||||
if (http_is_known_param(name)) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(handle, name, value, TRUE, &err)) {
|
||||
cache_set_by_name(name, value);
|
||||
applied++;
|
||||
} else {
|
||||
syslog(LOG_WARNING, "http set %s failed: %s",
|
||||
name, err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
g_free(name);
|
||||
g_free(value);
|
||||
}
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
static size_t http_parse_content_length(const char *hdr, size_t hlen) {
|
||||
const char *key = "content-length:";
|
||||
size_t klen = strlen(key);
|
||||
for (size_t i = 0; i + klen <= hlen; i++) {
|
||||
if (g_ascii_strncasecmp(hdr + i, key, klen) == 0) {
|
||||
i += klen;
|
||||
while (i < hlen && (hdr[i] == ' ' || hdr[i] == '\t')) i++;
|
||||
return (size_t)strtoul(hdr + i, NULL, 10);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void http_send(GOutputStream *out, const char *status,
|
||||
const char *ctype, const char *body) {
|
||||
gchar *resp = g_strdup_printf(
|
||||
"HTTP/1.1 %s\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Content-Length: %zu\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
status, ctype, strlen(body), body);
|
||||
g_output_stream_write_all(out, resp, strlen(resp), NULL, NULL, NULL);
|
||||
g_free(resp);
|
||||
}
|
||||
|
||||
static gboolean http_on_incoming(GSocketService *service G_GNUC_UNUSED,
|
||||
GSocketConnection *connection,
|
||||
GObject *source G_GNUC_UNUSED,
|
||||
gpointer user_data) {
|
||||
AXParameter *handle = (AXParameter *)user_data;
|
||||
GInputStream *in = g_io_stream_get_input_stream(G_IO_STREAM(connection));
|
||||
GOutputStream *out = g_io_stream_get_output_stream(G_IO_STREAM(connection));
|
||||
|
||||
GString *req = g_string_new(NULL);
|
||||
char buf[2048];
|
||||
int have_headers = 0;
|
||||
size_t header_end = 0;
|
||||
size_t content_length = 0;
|
||||
|
||||
while (1) {
|
||||
gssize n = g_input_stream_read(in, buf, sizeof(buf), NULL, NULL);
|
||||
if (n <= 0) break;
|
||||
g_string_append_len(req, buf, n);
|
||||
if (!have_headers) {
|
||||
char *p = g_strstr_len(req->str, req->len, "\r\n\r\n");
|
||||
if (p) {
|
||||
have_headers = 1;
|
||||
header_end = (size_t)(p - req->str) + 4;
|
||||
content_length = http_parse_content_length(req->str, header_end);
|
||||
}
|
||||
}
|
||||
if (have_headers && req->len - header_end >= content_length) break;
|
||||
if (req->len > 262144) break; /* safety cap */
|
||||
}
|
||||
|
||||
int is_get = 0, is_post = 0, is_settings = 0;
|
||||
if (have_headers) {
|
||||
if (g_str_has_prefix(req->str, "GET ")) is_get = 1;
|
||||
if (g_str_has_prefix(req->str, "POST ")) is_post = 1;
|
||||
const char *sp1 = strchr(req->str, ' ');
|
||||
if (sp1) {
|
||||
const char *path = sp1 + 1;
|
||||
const char *sp2 = strchr(path, ' ');
|
||||
size_t plen = sp2 ? (size_t)(sp2 - path) : strlen(path);
|
||||
const char *q = memchr(path, '?', plen);
|
||||
size_t match_len = q ? (size_t)(q - path) : plen;
|
||||
if (match_len >= 8 &&
|
||||
g_ascii_strncasecmp(path + match_len - 8, "settings", 8) == 0)
|
||||
is_settings = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_settings && is_get) {
|
||||
gchar *json = http_build_settings_json(handle);
|
||||
http_send(out, "200 OK", "application/json", json);
|
||||
g_free(json);
|
||||
} else if (is_settings && is_post) {
|
||||
const char *body = req->str + header_end;
|
||||
size_t body_len = req->len - header_end;
|
||||
if (body_len > content_length) body_len = content_length;
|
||||
int applied = http_apply_settings(handle, body, body_len);
|
||||
syslog(LOG_INFO, "settings http: applied %d parameter(s)", applied);
|
||||
if (reload_timer_id) g_source_remove(reload_timer_id);
|
||||
reload_timer_id = g_timeout_add(300, debounced_restart, NULL);
|
||||
http_send(out, "200 OK", "text/plain", "OK");
|
||||
} else {
|
||||
http_send(out, "404 Not Found", "text/plain", "Not found");
|
||||
}
|
||||
|
||||
g_string_free(req, TRUE);
|
||||
g_io_stream_close(G_IO_STREAM(connection), NULL, NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void http_server_start(AXParameter *handle) {
|
||||
GError *err = NULL;
|
||||
GSocketService *service = g_socket_service_new();
|
||||
GInetAddress *addr = g_inet_address_new_from_string("127.0.0.1");
|
||||
GSocketAddress *saddr = g_inet_socket_address_new(addr, HTTP_PORT);
|
||||
|
||||
if (!g_socket_listener_add_address(G_SOCKET_LISTENER(service), saddr,
|
||||
G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_TCP,
|
||||
NULL, NULL, &err)) {
|
||||
syslog(LOG_WARNING, "settings http: bind 127.0.0.1:%d failed: %s",
|
||||
HTTP_PORT, err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
g_object_unref(service);
|
||||
} else {
|
||||
g_signal_connect(service, "incoming", G_CALLBACK(http_on_incoming), handle);
|
||||
g_socket_service_start(service);
|
||||
syslog(LOG_INFO, "settings http server listening on 127.0.0.1:%d", HTTP_PORT);
|
||||
}
|
||||
g_object_unref(addr);
|
||||
g_object_unref(saddr);
|
||||
}
|
||||
|
||||
static gboolean signal_handler(gpointer loop) {
|
||||
syslog(LOG_INFO, "stopping");
|
||||
stop_child();
|
||||
g_main_loop_quit((GMainLoop *)loop);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
GError *error = NULL;
|
||||
|
||||
openlog(APP_NAME, LOG_PID, LOG_USER);
|
||||
syslog(LOG_INFO, "starting (root mode)");
|
||||
|
||||
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
|
||||
|
||||
/* Drop any stale auth-key sentinel from a previous run so we don't clear a
|
||||
* freshly configured key before it has been used. */
|
||||
unlink(SENTINEL_FILE);
|
||||
|
||||
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||
if (!handle) {
|
||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||
error ? error->message : "unknown");
|
||||
if (error) g_error_free(error);
|
||||
return 1;
|
||||
}
|
||||
g_ax_handle = handle;
|
||||
|
||||
ensure_param(handle, "AdvertiseRoutes", "");
|
||||
|
||||
load_config_cache(handle);
|
||||
write_config_file();
|
||||
start_child();
|
||||
|
||||
const char *params[] = { "CustomServer", "AuthKey", "AcceptDNS", "AcceptRoutes", "AdvertiseRoutes" };
|
||||
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
||||
if (!ax_parameter_register_callback(handle, params[i],
|
||||
parameter_changed, handle, &error)) {
|
||||
syslog(LOG_WARNING, "register callback %s: %s",
|
||||
params[i], error ? error->message : "unknown");
|
||||
if (error) { g_error_free(error); error = NULL; }
|
||||
}
|
||||
}
|
||||
|
||||
http_server_start(handle);
|
||||
|
||||
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
|
||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
||||
g_timeout_add_seconds(5, authkey_sentinel_cb, NULL);
|
||||
|
||||
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||
g_main_loop_run(loop);
|
||||
|
||||
g_main_loop_unref(loop);
|
||||
ax_parameter_free(handle);
|
||||
return 0;
|
||||
}
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
ARG ARCH=armv7hf
|
||||
ARG VERSION=1.15.1
|
||||
ARG UBUNTU_VERSION=22.04
|
||||
ARG VERSION=12.10.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/
|
||||
COPY common/app /opt/app/
|
||||
COPY arm/app/manifest.json /opt/app/manifest.json
|
||||
WORKDIR /opt/app
|
||||
ENV EXTRA_CFLAGS=-DHAS_PROXY_PORTS
|
||||
RUN arm-linux-gnueabihf-strip -s lib/tailscale lib/tailscaled
|
||||
RUN . /opt/axis/acapsdk/environment-setup* && acap-build -a Tailscale_VPN_run ./
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,14 +0,0 @@
|
||||
PROG = Tailscale_VPN
|
||||
SRCS = param_bridge.c
|
||||
PKGS = axparameter glib-2.0 gio-2.0
|
||||
CFLAGS += $(shell pkg-config --cflags $(PKGS))
|
||||
LDADD = $(shell pkg-config --libs $(PKGS))
|
||||
|
||||
all: $(PROG)
|
||||
chmod +x Tailscale_VPN_run
|
||||
|
||||
$(PROG): $(SRCS)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDADD)
|
||||
|
||||
clean:
|
||||
rm -f $(PROG)
|
||||
@@ -1,151 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Tailscale VPN run script — called by the param_bridge C binary.
|
||||
# Config is sourced from $STATE_DIR/params.conf (written by param_bridge).
|
||||
killall tailscaled 2>/dev/null || true
|
||||
|
||||
APP_DIR="/usr/local/packages/Tailscale_VPN"
|
||||
STATE_DIR="$APP_DIR/localdata"
|
||||
TAILSCALED_PATH="$APP_DIR/lib/tailscaled"
|
||||
TAILSCALE_PATH="$APP_DIR/lib/tailscale"
|
||||
SOCKET_PATH="$STATE_DIR/tailscaled.sock"
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
chmod 755 $TAILSCALED_PATH
|
||||
chmod 755 $TAILSCALE_PATH
|
||||
|
||||
# Defaults — overridden by sourcing params.conf written by param_bridge
|
||||
CUSTOM_SERVER=""
|
||||
AUTH_KEY=""
|
||||
CONF_HTTP="8080"
|
||||
CONF_SOCKS="1080"
|
||||
ACCEPT_DNS="false"
|
||||
ACCEPT_ROUTES="false"
|
||||
ADVERTISE_ROUTES=""
|
||||
|
||||
if [ -f "$STATE_DIR/params.conf" ]; then
|
||||
. "$STATE_DIR/params.conf"
|
||||
fi
|
||||
|
||||
logger -t "Tailscale_VPN" "Starting: http_port=$CONF_HTTP socks_port=$CONF_SOCKS custom_server=${CUSTOM_SERVER:-(default)}"
|
||||
|
||||
# Check whether a TCP port is already bound
|
||||
is_port_in_use() {
|
||||
local port=$1
|
||||
local hex_port
|
||||
hex_port=$(printf '%04X' "$port")
|
||||
grep -q ":${hex_port} " /proc/net/tcp 2>/dev/null && return 0
|
||||
grep -q ":${hex_port} " /proc/net/tcp6 2>/dev/null && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
if is_port_in_use "$CONF_HTTP"; then
|
||||
logger -t "Tailscale_VPN" "ERROR: HTTP proxy port $CONF_HTTP is already in use. Change it in Settings."
|
||||
exit 1
|
||||
fi
|
||||
if is_port_in_use "$CONF_SOCKS"; then
|
||||
logger -t "Tailscale_VPN" "ERROR: SOCKS5 port $CONF_SOCKS is already in use. Change it in Settings."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
|
||||
$TAILSCALED_PATH \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket=$SOCKET_PATH \
|
||||
--socks5-server=localhost:$CONF_SOCKS \
|
||||
--outbound-http-proxy-listen=localhost:$CONF_HTTP \
|
||||
--tun=userspace-networking \
|
||||
>/dev/null 2>&1 &
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
|
||||
|
||||
if [ -n "$CUSTOM_SERVER" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
||||
fi
|
||||
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
||||
fi
|
||||
|
||||
if [ "$ACCEPT_DNS" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
|
||||
fi
|
||||
|
||||
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||
fi
|
||||
|
||||
# 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
|
||||
# tailscaled netstack forwards tailnet traffic to these subnets, so no kernel IP
|
||||
# forwarding is required. Routes must still be approved in the Tailscale admin
|
||||
# console before peers can use them.
|
||||
if [ -n "$ADVERTISE_ROUTES" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --advertise-routes=$ADVERTISE_ROUTES"
|
||||
fi
|
||||
|
||||
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
||||
# (re-)authentication, `up` blocks until the user logs in; backgrounding it
|
||||
# ensures the status publisher below keeps running so the UI can surface the
|
||||
# login URL (tailscaled reports BackendState=NeedsLogin + AuthURL while waiting).
|
||||
# NOTE: `up` runs synchronously *inside* this backgrounded block so its real exit
|
||||
# code is captured directly. We must NOT background `up` separately and `wait`
|
||||
# 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.
|
||||
{
|
||||
eval "$TAILSCALE_CMD"
|
||||
up_exit=$?
|
||||
if [ "$up_exit" -eq 0 ]; then
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
# Auth succeeded with a one-time auth key — signal param_bridge to clear it
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
: > "$STATE_DIR/authkey_clear"
|
||||
fi
|
||||
else
|
||||
logger -t "Tailscale_VPN" "ERROR: tailscale up failed (exit $up_exit)"
|
||||
fi
|
||||
} &
|
||||
TAILSCALE_UP_PID=$!
|
||||
|
||||
logger -t "Tailscale_VPN" "HTTP/HTTPS proxy: http://127.0.0.1:$CONF_HTTP"
|
||||
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:$CONF_SOCKS"
|
||||
|
||||
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||
# TailscaleIPs / AuthURL) instead of scraping syslog. Served statically at
|
||||
# /local/Tailscale_VPN/status.json.
|
||||
STATUS_FILE="$APP_DIR/html/status.json"
|
||||
|
||||
publish_status() {
|
||||
if "$TAILSCALE_PATH" --socket="$SOCKET_PATH" status --json > "$STATUS_FILE.tmp" 2>/dev/null; then
|
||||
mv "$STATUS_FILE.tmp" "$STATUS_FILE" 2>/dev/null
|
||||
chmod 644 "$STATUS_FILE" 2>/dev/null
|
||||
else
|
||||
rm -f "$STATUS_FILE.tmp" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
status_loop() {
|
||||
while true; do
|
||||
publish_status
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
status_loop &
|
||||
STATUS_LOOP_PID=$!
|
||||
|
||||
# Clean up the status writer, up watcher, daemon and published status on
|
||||
# stop/restart so param_bridge (which signals this script) leaves no orphans or
|
||||
# stale state.
|
||||
cleanup() {
|
||||
[ -n "$STATUS_LOOP_PID" ] && kill "$STATUS_LOOP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALE_UP_PID" ] && kill "$TAILSCALE_UP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALED_PID" ] && kill "$TAILSCALED_PID" 2>/dev/null
|
||||
rm -f "$STATUS_FILE" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap cleanup TERM INT
|
||||
|
||||
wait $TAILSCALED_PID
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -1,15 +1,20 @@
|
||||
{
|
||||
"schemaVersion": "1.7.0",
|
||||
"schemaVersion": "2.0.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"vendorId": "5741c1fb91",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.98.8",
|
||||
"architecture": "armv7hf"
|
||||
"architecture": "armv7hf",
|
||||
"runMode": "respawn",
|
||||
"compatibleOsVersions": [
|
||||
{
|
||||
"max": "13"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
|
||||
@@ -1,549 +0,0 @@
|
||||
// Copyright (C) 2024 Mo3he
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* ACAP parameter bridge for Tailscale VPN (userspace variant).
|
||||
*
|
||||
* Responsibilities:
|
||||
* 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.
|
||||
*
|
||||
* Runs as the unprivileged 'sdk' ACAP user — no root required.
|
||||
*/
|
||||
|
||||
#include <axsdk/axparameter.h>
|
||||
#include <glib-unix.h>
|
||||
#include <gio/gio.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>
|
||||
#include <signal.h>
|
||||
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define SENTINEL_FILE "/usr/local/packages/Tailscale_VPN/localdata/authkey_clear"
|
||||
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
static pid_t child_pid = -1;
|
||||
static guint reload_timer_id = 0;
|
||||
|
||||
static char *cfg_custom_server = NULL;
|
||||
static char *cfg_auth_key = NULL;
|
||||
static char *cfg_http_proxy_port = NULL;
|
||||
static char *cfg_socks5_port = NULL;
|
||||
static char *cfg_accept_dns = NULL;
|
||||
static char *cfg_accept_routes = NULL;
|
||||
static char *cfg_advertise_routes = NULL;
|
||||
|
||||
static void cache_set(char **field, const char *value) {
|
||||
if (!value) return;
|
||||
free(*field);
|
||||
*field = strdup(value);
|
||||
}
|
||||
|
||||
static const char *cache_get(char **field, const char *fallback) {
|
||||
return (*field && **field) ? *field : fallback;
|
||||
}
|
||||
|
||||
/* Ensure a parameter exists in the device parameter database. On in-place ACAP
|
||||
* upgrades a newly introduced manifest parameter is not always auto-registered,
|
||||
* 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) {
|
||||
GError *err = NULL;
|
||||
if (!ax_parameter_add(handle, name, def, "string", &err)) {
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── child process management ──────────────────────────────────────────── */
|
||||
|
||||
static void stop_child(void) {
|
||||
if (child_pid <= 0)
|
||||
return;
|
||||
kill(child_pid, SIGTERM);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int status;
|
||||
if (waitpid(child_pid, &status, WNOHANG) == child_pid) {
|
||||
child_pid = -1;
|
||||
return;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
syslog(LOG_WARNING, "child did not exit in 3 s, sending SIGKILL");
|
||||
kill(child_pid, SIGKILL);
|
||||
waitpid(child_pid, NULL, 0);
|
||||
child_pid = -1;
|
||||
}
|
||||
|
||||
static void start_child(void) {
|
||||
stop_child();
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
syslog(LOG_ERR, "fork failed: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
if (pid == 0) {
|
||||
execl(RUN_SCRIPT, RUN_SCRIPT, NULL);
|
||||
syslog(LOG_ERR, "execl %s failed: %s", RUN_SCRIPT, strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
child_pid = pid;
|
||||
syslog(LOG_INFO, "started %s (pid %d)", RUN_SCRIPT, child_pid);
|
||||
}
|
||||
|
||||
/* ── watchdog ────────────────────────────────────────────────────────────── */
|
||||
|
||||
static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
||||
if (child_pid > 0) {
|
||||
int status;
|
||||
pid_t ret = waitpid(child_pid, &status, WNOHANG);
|
||||
if (ret == child_pid) {
|
||||
int exit_code = WEXITSTATUS(status);
|
||||
syslog(LOG_WARNING, "child exited (status %d), restarting", exit_code);
|
||||
child_pid = -1;
|
||||
/* If child exited 0, auth succeeded — clear AuthKey via axparameter */
|
||||
if (exit_code == 0 && g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
start_child();
|
||||
}
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* ── auth-key sentinel ───────────────────────────────────────────────────── */
|
||||
|
||||
/* The run script drops SENTINEL_FILE after a successful `tailscale up` that
|
||||
* used a one-time auth key. Clear the stored AuthKey so it is not reused and
|
||||
* 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) {
|
||||
if (access(SENTINEL_FILE, F_OK) != 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
||||
if (g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth (sentinel)");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
unlink(SENTINEL_FILE);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* ── config file ─────────────────────────────────────────────────────────── */
|
||||
|
||||
static void load_config_cache(AXParameter *handle) {
|
||||
GError *error = NULL;
|
||||
gchar *val = NULL;
|
||||
|
||||
#define LOAD(name, field) \
|
||||
val = NULL; error = NULL; \
|
||||
if (ax_parameter_get(handle, name, &val, &error)) { \
|
||||
free(field); field = val ? strdup(val) : strdup(""); \
|
||||
g_free(val); val = NULL; \
|
||||
} else { \
|
||||
syslog(LOG_WARNING, "ax_parameter_get %s failed: %s", name, \
|
||||
error ? error->message : "unknown"); \
|
||||
if (error) { g_error_free(error); error = NULL; } \
|
||||
}
|
||||
|
||||
LOAD("CustomServer", cfg_custom_server)
|
||||
LOAD("AuthKey", cfg_auth_key)
|
||||
LOAD("HttpProxyPort", cfg_http_proxy_port)
|
||||
LOAD("Socks5Port", cfg_socks5_port)
|
||||
LOAD("AcceptDNS", cfg_accept_dns)
|
||||
LOAD("AcceptRoutes", cfg_accept_routes)
|
||||
LOAD("AdvertiseRoutes", cfg_advertise_routes)
|
||||
#undef LOAD
|
||||
}
|
||||
|
||||
static void write_config_file(void) {
|
||||
FILE *f = fopen(CONFIG_FILE, "w");
|
||||
if (!f) {
|
||||
syslog(LOG_ERR, "cannot open config file %s: %s",
|
||||
CONFIG_FILE, strerror(errno));
|
||||
return;
|
||||
}
|
||||
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
|
||||
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
||||
fprintf(f, "CONF_HTTP=%s\n", cache_get(&cfg_http_proxy_port, "8080"));
|
||||
fprintf(f, "CONF_SOCKS=%s\n", cache_get(&cfg_socks5_port, "1080"));
|
||||
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
|
||||
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
|
||||
fprintf(f, "ADVERTISE_ROUTES=%s\n", cache_get(&cfg_advertise_routes, ""));
|
||||
fclose(f);
|
||||
chmod(CONFIG_FILE, 0600);
|
||||
syslog(LOG_INFO, "config updated: http=%s socks=%s server=%s",
|
||||
cache_get(&cfg_http_proxy_port, "8080"),
|
||||
cache_get(&cfg_socks5_port, "1080"),
|
||||
cache_get(&cfg_custom_server, "(default)"));
|
||||
}
|
||||
|
||||
/* ── ACAP parameter callback ─────────────────────────────────────────────── */
|
||||
|
||||
static gboolean debounced_restart(gpointer G_GNUC_UNUSED data) {
|
||||
reload_timer_id = 0;
|
||||
if (g_ax_handle)
|
||||
load_config_cache(g_ax_handle);
|
||||
write_config_file();
|
||||
syslog(LOG_INFO, "restarting with new config");
|
||||
stop_child();
|
||||
start_child();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void parameter_changed(const gchar *name, const gchar *value,
|
||||
gpointer G_GNUC_UNUSED handle_void_ptr) {
|
||||
const char *dot = strrchr(name, '.');
|
||||
const char *short_name = dot ? dot + 1 : name;
|
||||
|
||||
syslog(LOG_INFO, "parameter changed: %s", short_name);
|
||||
|
||||
if (strcmp(short_name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||
else if (strcmp(short_name, "HttpProxyPort") == 0) cache_set(&cfg_http_proxy_port, value);
|
||||
else if (strcmp(short_name, "Socks5Port") == 0) cache_set(&cfg_socks5_port, value);
|
||||
else if (strcmp(short_name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||
else if (strcmp(short_name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||
else if (strcmp(short_name, "AdvertiseRoutes") == 0) cache_set(&cfg_advertise_routes, value);
|
||||
|
||||
if (reload_timer_id)
|
||||
g_source_remove(reload_timer_id);
|
||||
reload_timer_id = g_timeout_add(300, debounced_restart, NULL);
|
||||
}
|
||||
|
||||
/* ── embedded settings HTTP server (reverse-proxy fallback) ──────────────────
|
||||
* Some AXIS device classes (e.g. recorders/NVRs) do not expose the legacy
|
||||
* /axis-cgi/param.cgi VAPIX endpoint, so the web UI cannot load or save
|
||||
* 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
|
||||
|
||||
static const char *http_param_names[] = {
|
||||
"CustomServer", "AuthKey", "HttpProxyPort", "Socks5Port",
|
||||
"AcceptDNS", "AcceptRoutes", "AdvertiseRoutes"
|
||||
};
|
||||
|
||||
static void cache_set_by_name(const char *name, const char *value) {
|
||||
if (strcmp(name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||
else if (strcmp(name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||
else if (strcmp(name, "HttpProxyPort") == 0) cache_set(&cfg_http_proxy_port, value);
|
||||
else if (strcmp(name, "Socks5Port") == 0) cache_set(&cfg_socks5_port, value);
|
||||
else if (strcmp(name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||
else if (strcmp(name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||
else if (strcmp(name, "AdvertiseRoutes") == 0) cache_set(&cfg_advertise_routes, value);
|
||||
}
|
||||
|
||||
static int http_is_known_param(const char *name) {
|
||||
for (size_t i = 0; i < G_N_ELEMENTS(http_param_names); i++)
|
||||
if (strcmp(name, http_param_names[i]) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void http_json_append_escaped(GString *out, const char *s) {
|
||||
for (const char *p = s; *p; p++) {
|
||||
switch (*p) {
|
||||
case '"': g_string_append(out, "\\\""); break;
|
||||
case '\\': g_string_append(out, "\\\\"); break;
|
||||
case '\n': g_string_append(out, "\\n"); break;
|
||||
case '\r': g_string_append(out, "\\r"); break;
|
||||
case '\t': g_string_append(out, "\\t"); break;
|
||||
default:
|
||||
if ((unsigned char)*p < 0x20)
|
||||
g_string_append_printf(out, "\\u%04x", (unsigned char)*p);
|
||||
else
|
||||
g_string_append_c(out, *p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static gchar *http_build_settings_json(AXParameter *handle) {
|
||||
GString *out = g_string_new("{");
|
||||
for (size_t i = 0; i < G_N_ELEMENTS(http_param_names); i++) {
|
||||
gchar *val = NULL;
|
||||
GError *err = NULL;
|
||||
if (!ax_parameter_get(handle, http_param_names[i], &val, &err)) {
|
||||
if (err) g_error_free(err);
|
||||
val = g_strdup("");
|
||||
}
|
||||
if (i) g_string_append_c(out, ',');
|
||||
g_string_append_printf(out, "\"%s\":\"", http_param_names[i]);
|
||||
http_json_append_escaped(out, val ? val : "");
|
||||
g_string_append_c(out, '"');
|
||||
g_free(val);
|
||||
}
|
||||
g_string_append_c(out, '}');
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
static gchar *http_url_decode(const char *s, size_t len) {
|
||||
GString *out = g_string_new(NULL);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = s[i];
|
||||
if (c == '+') {
|
||||
g_string_append_c(out, ' ');
|
||||
} else if (c == '%' && i + 2 < len &&
|
||||
g_ascii_isxdigit(s[i + 1]) && g_ascii_isxdigit(s[i + 2])) {
|
||||
int hi = g_ascii_xdigit_value(s[i + 1]);
|
||||
int lo = g_ascii_xdigit_value(s[i + 2]);
|
||||
g_string_append_c(out, (char)((hi << 4) | lo));
|
||||
i += 2;
|
||||
} else {
|
||||
g_string_append_c(out, c);
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
/* Apply an application/x-www-form-urlencoded body of shortName=value pairs to
|
||||
* the parameter store. Returns the number of parameters successfully set. */
|
||||
static int http_apply_settings(AXParameter *handle, const char *body, size_t len) {
|
||||
int applied = 0;
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= len; i++) {
|
||||
if (i == len || body[i] == '&') {
|
||||
size_t seg_len = i - start;
|
||||
if (seg_len > 0) {
|
||||
const char *seg = body + start;
|
||||
const char *eq = memchr(seg, '=', seg_len);
|
||||
if (eq) {
|
||||
size_t nlen = (size_t)(eq - seg);
|
||||
gchar *name = g_strndup(seg, nlen);
|
||||
gchar *value = http_url_decode(eq + 1, seg_len - nlen - 1);
|
||||
if (http_is_known_param(name)) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(handle, name, value, TRUE, &err)) {
|
||||
cache_set_by_name(name, value);
|
||||
applied++;
|
||||
} else {
|
||||
syslog(LOG_WARNING, "http set %s failed: %s",
|
||||
name, err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
g_free(name);
|
||||
g_free(value);
|
||||
}
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
static size_t http_parse_content_length(const char *hdr, size_t hlen) {
|
||||
const char *key = "content-length:";
|
||||
size_t klen = strlen(key);
|
||||
for (size_t i = 0; i + klen <= hlen; i++) {
|
||||
if (g_ascii_strncasecmp(hdr + i, key, klen) == 0) {
|
||||
i += klen;
|
||||
while (i < hlen && (hdr[i] == ' ' || hdr[i] == '\t')) i++;
|
||||
return (size_t)strtoul(hdr + i, NULL, 10);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void http_send(GOutputStream *out, const char *status,
|
||||
const char *ctype, const char *body) {
|
||||
gchar *resp = g_strdup_printf(
|
||||
"HTTP/1.1 %s\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Content-Length: %zu\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
status, ctype, strlen(body), body);
|
||||
g_output_stream_write_all(out, resp, strlen(resp), NULL, NULL, NULL);
|
||||
g_free(resp);
|
||||
}
|
||||
|
||||
static gboolean http_on_incoming(GSocketService *service G_GNUC_UNUSED,
|
||||
GSocketConnection *connection,
|
||||
GObject *source G_GNUC_UNUSED,
|
||||
gpointer user_data) {
|
||||
AXParameter *handle = (AXParameter *)user_data;
|
||||
GInputStream *in = g_io_stream_get_input_stream(G_IO_STREAM(connection));
|
||||
GOutputStream *out = g_io_stream_get_output_stream(G_IO_STREAM(connection));
|
||||
|
||||
GString *req = g_string_new(NULL);
|
||||
char buf[2048];
|
||||
int have_headers = 0;
|
||||
size_t header_end = 0;
|
||||
size_t content_length = 0;
|
||||
|
||||
while (1) {
|
||||
gssize n = g_input_stream_read(in, buf, sizeof(buf), NULL, NULL);
|
||||
if (n <= 0) break;
|
||||
g_string_append_len(req, buf, n);
|
||||
if (!have_headers) {
|
||||
char *p = g_strstr_len(req->str, req->len, "\r\n\r\n");
|
||||
if (p) {
|
||||
have_headers = 1;
|
||||
header_end = (size_t)(p - req->str) + 4;
|
||||
content_length = http_parse_content_length(req->str, header_end);
|
||||
}
|
||||
}
|
||||
if (have_headers && req->len - header_end >= content_length) break;
|
||||
if (req->len > 262144) break; /* safety cap */
|
||||
}
|
||||
|
||||
int is_get = 0, is_post = 0, is_settings = 0;
|
||||
if (have_headers) {
|
||||
if (g_str_has_prefix(req->str, "GET ")) is_get = 1;
|
||||
if (g_str_has_prefix(req->str, "POST ")) is_post = 1;
|
||||
const char *sp1 = strchr(req->str, ' ');
|
||||
if (sp1) {
|
||||
const char *path = sp1 + 1;
|
||||
const char *sp2 = strchr(path, ' ');
|
||||
size_t plen = sp2 ? (size_t)(sp2 - path) : strlen(path);
|
||||
const char *q = memchr(path, '?', plen);
|
||||
size_t match_len = q ? (size_t)(q - path) : plen;
|
||||
if (match_len >= 8 &&
|
||||
g_ascii_strncasecmp(path + match_len - 8, "settings", 8) == 0)
|
||||
is_settings = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_settings && is_get) {
|
||||
gchar *json = http_build_settings_json(handle);
|
||||
http_send(out, "200 OK", "application/json", json);
|
||||
g_free(json);
|
||||
} else if (is_settings && is_post) {
|
||||
const char *body = req->str + header_end;
|
||||
size_t body_len = req->len - header_end;
|
||||
if (body_len > content_length) body_len = content_length;
|
||||
int applied = http_apply_settings(handle, body, body_len);
|
||||
syslog(LOG_INFO, "settings http: applied %d parameter(s)", applied);
|
||||
if (reload_timer_id) g_source_remove(reload_timer_id);
|
||||
reload_timer_id = g_timeout_add(300, debounced_restart, NULL);
|
||||
http_send(out, "200 OK", "text/plain", "OK");
|
||||
} else {
|
||||
http_send(out, "404 Not Found", "text/plain", "Not found");
|
||||
}
|
||||
|
||||
g_string_free(req, TRUE);
|
||||
g_io_stream_close(G_IO_STREAM(connection), NULL, NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void http_server_start(AXParameter *handle) {
|
||||
GError *err = NULL;
|
||||
GSocketService *service = g_socket_service_new();
|
||||
GInetAddress *addr = g_inet_address_new_from_string("127.0.0.1");
|
||||
GSocketAddress *saddr = g_inet_socket_address_new(addr, HTTP_PORT);
|
||||
|
||||
if (!g_socket_listener_add_address(G_SOCKET_LISTENER(service), saddr,
|
||||
G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_TCP,
|
||||
NULL, NULL, &err)) {
|
||||
syslog(LOG_WARNING, "settings http: bind 127.0.0.1:%d failed: %s",
|
||||
HTTP_PORT, err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
g_object_unref(service);
|
||||
} else {
|
||||
g_signal_connect(service, "incoming", G_CALLBACK(http_on_incoming), handle);
|
||||
g_socket_service_start(service);
|
||||
syslog(LOG_INFO, "settings http server listening on 127.0.0.1:%d", HTTP_PORT);
|
||||
}
|
||||
g_object_unref(addr);
|
||||
g_object_unref(saddr);
|
||||
}
|
||||
|
||||
/* ── signal handler ──────────────────────────────────────────────────────── */
|
||||
|
||||
static gboolean signal_handler(gpointer loop) {
|
||||
syslog(LOG_INFO, "stopping");
|
||||
stop_child();
|
||||
g_main_loop_quit((GMainLoop *)loop);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/* ── main ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
int main(void) {
|
||||
GError *error = NULL;
|
||||
|
||||
openlog(APP_NAME, LOG_PID, LOG_USER);
|
||||
syslog(LOG_INFO, "starting");
|
||||
|
||||
/* Ensure localdata dir exists */
|
||||
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
|
||||
|
||||
/* Drop any stale auth-key sentinel from a previous run so we don't clear a
|
||||
* freshly configured key before it has been used. */
|
||||
unlink(SENTINEL_FILE);
|
||||
|
||||
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||
if (!handle) {
|
||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||
error ? error->message : "unknown");
|
||||
if (error) g_error_free(error);
|
||||
return 1;
|
||||
}
|
||||
g_ax_handle = handle;
|
||||
|
||||
ensure_param(handle, "AdvertiseRoutes", "");
|
||||
|
||||
load_config_cache(handle);
|
||||
write_config_file();
|
||||
start_child();
|
||||
|
||||
const char *params[] = {
|
||||
"CustomServer", "AuthKey", "HttpProxyPort", "Socks5Port",
|
||||
"AcceptDNS", "AcceptRoutes", "AdvertiseRoutes"
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
||||
if (!ax_parameter_register_callback(handle, params[i],
|
||||
parameter_changed, handle, &error)) {
|
||||
syslog(LOG_WARNING, "register callback %s: %s",
|
||||
params[i], error ? error->message : "unknown");
|
||||
if (error) { g_error_free(error); error = NULL; }
|
||||
}
|
||||
}
|
||||
|
||||
http_server_start(handle);
|
||||
|
||||
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
|
||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
||||
g_timeout_add_seconds(5, authkey_sentinel_cb, NULL);
|
||||
|
||||
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||
g_main_loop_run(loop);
|
||||
|
||||
g_main_loop_unref(loop);
|
||||
ax_parameter_free(handle);
|
||||
return 0;
|
||||
}
|
||||
+2
-1
@@ -7,7 +7,8 @@ ARG SDK=acap-native-sdk
|
||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||
|
||||
# Building the ACAP application
|
||||
COPY ./app /opt/app/
|
||||
COPY common/app /opt/app/
|
||||
COPY arm_ROOT/app/manifest.json /opt/app/manifest.json
|
||||
WORKDIR /opt/app
|
||||
RUN arm-linux-gnueabihf-strip -s lib/tailscale lib/tailscaled
|
||||
RUN . /opt/axis/acapsdk/environment-setup* && acap-build -a Tailscale_VPN_run ./
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,14 +0,0 @@
|
||||
PROG = Tailscale_VPN
|
||||
SRCS = param_bridge.c
|
||||
PKGS = axparameter glib-2.0 gio-2.0
|
||||
CFLAGS += $(shell pkg-config --cflags $(PKGS))
|
||||
LDADD = $(shell pkg-config --libs $(PKGS))
|
||||
|
||||
all: $(PROG)
|
||||
chmod +x Tailscale_VPN_run
|
||||
|
||||
$(PROG): $(SRCS)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDADD)
|
||||
|
||||
clean:
|
||||
rm -f $(PROG)
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Tailscale VPN run script (ROOT / kernel networking variant).
|
||||
# Sources config from params.conf written by param_bridge.
|
||||
killall tailscaled 2>/dev/null || true
|
||||
|
||||
APP_DIR="/usr/local/packages/Tailscale_VPN"
|
||||
STATE_DIR="$APP_DIR/localdata"
|
||||
TAILSCALED_PATH="$APP_DIR/lib/tailscaled"
|
||||
TAILSCALE_PATH="$APP_DIR/lib/tailscale"
|
||||
SOCKET_PATH="$STATE_DIR/tailscaled.sock"
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
chmod 755 $TAILSCALED_PATH
|
||||
chmod 755 $TAILSCALE_PATH
|
||||
|
||||
CUSTOM_SERVER=""
|
||||
AUTH_KEY=""
|
||||
ACCEPT_DNS="false"
|
||||
ACCEPT_ROUTES="false"
|
||||
ADVERTISE_ROUTES=""
|
||||
|
||||
if [ -f "$STATE_DIR/params.conf" ]; then
|
||||
. "$STATE_DIR/params.conf"
|
||||
fi
|
||||
|
||||
logger -t "Tailscale_VPN" "Starting (root mode): custom_server=${CUSTOM_SERVER:-(default)}"
|
||||
|
||||
$TAILSCALED_PATH \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket=$SOCKET_PATH \
|
||||
>/dev/null 2>&1 &
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
|
||||
|
||||
if [ -n "$CUSTOM_SERVER" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
||||
fi
|
||||
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
||||
fi
|
||||
|
||||
if [ "$ACCEPT_DNS" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
|
||||
fi
|
||||
|
||||
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||
fi
|
||||
|
||||
# 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 kernel-networking (root) mode the
|
||||
# host must forward packets between the tailnet and the LAN, so enable IP
|
||||
# forwarding. Routes must still be approved in the Tailscale admin console.
|
||||
if [ -n "$ADVERTISE_ROUTES" ]; then
|
||||
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
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --advertise-routes=$ADVERTISE_ROUTES"
|
||||
fi
|
||||
|
||||
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
||||
# (re-)authentication, `up` blocks until the user logs in; backgrounding it
|
||||
# ensures the status publisher below keeps running so the UI can surface the
|
||||
# login URL (tailscaled reports BackendState=NeedsLogin + AuthURL while waiting).
|
||||
# NOTE: `up` runs synchronously *inside* this backgrounded block so its real exit
|
||||
# code is captured directly. We must NOT background `up` separately and `wait`
|
||||
# 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.
|
||||
{
|
||||
eval "$TAILSCALE_CMD"
|
||||
up_exit=$?
|
||||
if [ "$up_exit" -eq 0 ]; then
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running (root mode)"
|
||||
# Auth succeeded with a one-time auth key — signal param_bridge to clear it
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
: > "$STATE_DIR/authkey_clear"
|
||||
fi
|
||||
else
|
||||
logger -t "Tailscale_VPN" "ERROR: tailscale up failed (exit $up_exit)"
|
||||
fi
|
||||
} &
|
||||
TAILSCALE_UP_PID=$!
|
||||
|
||||
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||
# TailscaleIPs / AuthURL) instead of scraping syslog. Served statically at
|
||||
# /local/Tailscale_VPN/status.json.
|
||||
STATUS_FILE="$APP_DIR/html/status.json"
|
||||
|
||||
publish_status() {
|
||||
if "$TAILSCALE_PATH" --socket="$SOCKET_PATH" status --json > "$STATUS_FILE.tmp" 2>/dev/null; then
|
||||
mv "$STATUS_FILE.tmp" "$STATUS_FILE" 2>/dev/null
|
||||
chmod 644 "$STATUS_FILE" 2>/dev/null
|
||||
else
|
||||
rm -f "$STATUS_FILE.tmp" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
status_loop() {
|
||||
while true; do
|
||||
publish_status
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
status_loop &
|
||||
STATUS_LOOP_PID=$!
|
||||
|
||||
# Clean up the status writer, up watcher, daemon and published status on
|
||||
# stop/restart so param_bridge (which signals this script) leaves no orphans or
|
||||
# stale state.
|
||||
cleanup() {
|
||||
[ -n "$STATUS_LOOP_PID" ] && kill "$STATUS_LOOP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALE_UP_PID" ] && kill "$TAILSCALE_UP_PID" 2>/dev/null
|
||||
[ -n "$TAILSCALED_PID" ] && kill "$TAILSCALED_PID" 2>/dev/null
|
||||
rm -f "$STATUS_FILE" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap cleanup TERM INT
|
||||
|
||||
wait $TAILSCALED_PID
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -1,507 +0,0 @@
|
||||
// Copyright (C) 2024 Mo3he
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* ACAP parameter bridge for Tailscale VPN (ROOT / kernel networking variant).
|
||||
* Same structure as regular param_bridge.c but without proxy port params.
|
||||
*/
|
||||
|
||||
#include <axsdk/axparameter.h>
|
||||
#include <glib-unix.h>
|
||||
#include <gio/gio.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>
|
||||
#include <signal.h>
|
||||
|
||||
#define APP_NAME "Tailscale_VPN"
|
||||
#define CONFIG_FILE "/usr/local/packages/Tailscale_VPN/localdata/params.conf"
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define SENTINEL_FILE "/usr/local/packages/Tailscale_VPN/localdata/authkey_clear"
|
||||
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
static pid_t child_pid = -1;
|
||||
static guint reload_timer_id = 0;
|
||||
|
||||
static char *cfg_custom_server = NULL;
|
||||
static char *cfg_auth_key = NULL;
|
||||
static char *cfg_accept_dns = NULL;
|
||||
static char *cfg_accept_routes = NULL;
|
||||
static char *cfg_advertise_routes = NULL;
|
||||
|
||||
static void cache_set(char **field, const char *value) {
|
||||
if (!value) return;
|
||||
free(*field);
|
||||
*field = strdup(value);
|
||||
}
|
||||
|
||||
static const char *cache_get(char **field, const char *fallback) {
|
||||
return (*field && **field) ? *field : fallback;
|
||||
}
|
||||
|
||||
/* Ensure a parameter exists in the device parameter database. On in-place ACAP
|
||||
* upgrades a newly introduced manifest parameter is not always auto-registered,
|
||||
* 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) {
|
||||
GError *err = NULL;
|
||||
if (!ax_parameter_add(handle, name, def, "string", &err)) {
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_child(void) {
|
||||
if (child_pid <= 0)
|
||||
return;
|
||||
kill(child_pid, SIGTERM);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int status;
|
||||
if (waitpid(child_pid, &status, WNOHANG) == child_pid) {
|
||||
child_pid = -1;
|
||||
return;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
syslog(LOG_WARNING, "child did not exit in 3 s, sending SIGKILL");
|
||||
kill(child_pid, SIGKILL);
|
||||
waitpid(child_pid, NULL, 0);
|
||||
child_pid = -1;
|
||||
}
|
||||
|
||||
static void start_child(void) {
|
||||
stop_child();
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
syslog(LOG_ERR, "fork failed: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
if (pid == 0) {
|
||||
execl(RUN_SCRIPT, RUN_SCRIPT, NULL);
|
||||
syslog(LOG_ERR, "execl %s failed: %s", RUN_SCRIPT, strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
child_pid = pid;
|
||||
syslog(LOG_INFO, "started %s (pid %d)", RUN_SCRIPT, child_pid);
|
||||
}
|
||||
|
||||
static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
||||
if (child_pid > 0) {
|
||||
int status;
|
||||
pid_t ret = waitpid(child_pid, &status, WNOHANG);
|
||||
if (ret == child_pid) {
|
||||
int exit_code = WEXITSTATUS(status);
|
||||
syslog(LOG_WARNING, "child exited (status %d), restarting", exit_code);
|
||||
child_pid = -1;
|
||||
/* If child exited 0, auth succeeded — clear AuthKey via axparameter */
|
||||
if (exit_code == 0 && g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
start_child();
|
||||
}
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/* The run script drops SENTINEL_FILE after a successful `tailscale up` that
|
||||
* used a one-time auth key. Clear the stored AuthKey so it is not reused and
|
||||
* 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) {
|
||||
if (access(SENTINEL_FILE, F_OK) != 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
||||
if (g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
free(cfg_auth_key); cfg_auth_key = strdup("");
|
||||
syslog(LOG_INFO, "AuthKey cleared after successful auth (sentinel)");
|
||||
} else {
|
||||
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
|
||||
err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
unlink(SENTINEL_FILE);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static void load_config_cache(AXParameter *handle) {
|
||||
GError *error = NULL;
|
||||
gchar *val = NULL;
|
||||
|
||||
#define LOAD(name, field) \
|
||||
val = NULL; error = NULL; \
|
||||
if (ax_parameter_get(handle, name, &val, &error)) { \
|
||||
free(field); field = val ? strdup(val) : strdup(""); \
|
||||
g_free(val); val = NULL; \
|
||||
} else { \
|
||||
syslog(LOG_WARNING, "ax_parameter_get %s failed: %s", name, \
|
||||
error ? error->message : "unknown"); \
|
||||
if (error) { g_error_free(error); error = NULL; } \
|
||||
}
|
||||
|
||||
LOAD("CustomServer", cfg_custom_server)
|
||||
LOAD("AuthKey", cfg_auth_key)
|
||||
LOAD("AcceptDNS", cfg_accept_dns)
|
||||
LOAD("AcceptRoutes", cfg_accept_routes)
|
||||
LOAD("AdvertiseRoutes", cfg_advertise_routes)
|
||||
#undef LOAD
|
||||
}
|
||||
|
||||
static void write_config_file(void) {
|
||||
FILE *f = fopen(CONFIG_FILE, "w");
|
||||
if (!f) {
|
||||
syslog(LOG_ERR, "cannot open config file %s: %s",
|
||||
CONFIG_FILE, strerror(errno));
|
||||
return;
|
||||
}
|
||||
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
|
||||
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
||||
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
|
||||
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
|
||||
fprintf(f, "ADVERTISE_ROUTES=%s\n", cache_get(&cfg_advertise_routes, ""));
|
||||
fclose(f);
|
||||
chmod(CONFIG_FILE, 0600);
|
||||
syslog(LOG_INFO, "config updated: server=%s",
|
||||
cache_get(&cfg_custom_server, "(default)"));
|
||||
}
|
||||
|
||||
static gboolean debounced_restart(gpointer G_GNUC_UNUSED data) {
|
||||
reload_timer_id = 0;
|
||||
if (g_ax_handle)
|
||||
load_config_cache(g_ax_handle);
|
||||
write_config_file();
|
||||
syslog(LOG_INFO, "restarting with new config");
|
||||
stop_child();
|
||||
start_child();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void parameter_changed(const gchar *name, const gchar *value,
|
||||
gpointer G_GNUC_UNUSED handle_void_ptr) {
|
||||
const char *dot = strrchr(name, '.');
|
||||
const char *short_name = dot ? dot + 1 : name;
|
||||
syslog(LOG_INFO, "parameter changed: %s", short_name);
|
||||
|
||||
if (strcmp(short_name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||
else if (strcmp(short_name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||
else if (strcmp(short_name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||
else if (strcmp(short_name, "AdvertiseRoutes") == 0) cache_set(&cfg_advertise_routes, value);
|
||||
|
||||
if (reload_timer_id)
|
||||
g_source_remove(reload_timer_id);
|
||||
reload_timer_id = g_timeout_add(300, debounced_restart, NULL);
|
||||
}
|
||||
|
||||
/* ── embedded settings HTTP server (reverse-proxy fallback) ──────────────────
|
||||
* Some AXIS device classes (e.g. recorders/NVRs) do not expose the legacy
|
||||
* /axis-cgi/param.cgi VAPIX endpoint, so the web UI cannot load or save
|
||||
* 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
|
||||
|
||||
static const char *http_param_names[] = {
|
||||
"CustomServer", "AuthKey", "AcceptDNS", "AcceptRoutes", "AdvertiseRoutes"
|
||||
};
|
||||
|
||||
static void cache_set_by_name(const char *name, const char *value) {
|
||||
if (strcmp(name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||
else if (strcmp(name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||
else if (strcmp(name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||
else if (strcmp(name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||
else if (strcmp(name, "AdvertiseRoutes") == 0) cache_set(&cfg_advertise_routes, value);
|
||||
}
|
||||
|
||||
static int http_is_known_param(const char *name) {
|
||||
for (size_t i = 0; i < G_N_ELEMENTS(http_param_names); i++)
|
||||
if (strcmp(name, http_param_names[i]) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void http_json_append_escaped(GString *out, const char *s) {
|
||||
for (const char *p = s; *p; p++) {
|
||||
switch (*p) {
|
||||
case '"': g_string_append(out, "\\\""); break;
|
||||
case '\\': g_string_append(out, "\\\\"); break;
|
||||
case '\n': g_string_append(out, "\\n"); break;
|
||||
case '\r': g_string_append(out, "\\r"); break;
|
||||
case '\t': g_string_append(out, "\\t"); break;
|
||||
default:
|
||||
if ((unsigned char)*p < 0x20)
|
||||
g_string_append_printf(out, "\\u%04x", (unsigned char)*p);
|
||||
else
|
||||
g_string_append_c(out, *p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static gchar *http_build_settings_json(AXParameter *handle) {
|
||||
GString *out = g_string_new("{");
|
||||
for (size_t i = 0; i < G_N_ELEMENTS(http_param_names); i++) {
|
||||
gchar *val = NULL;
|
||||
GError *err = NULL;
|
||||
if (!ax_parameter_get(handle, http_param_names[i], &val, &err)) {
|
||||
if (err) g_error_free(err);
|
||||
val = g_strdup("");
|
||||
}
|
||||
if (i) g_string_append_c(out, ',');
|
||||
g_string_append_printf(out, "\"%s\":\"", http_param_names[i]);
|
||||
http_json_append_escaped(out, val ? val : "");
|
||||
g_string_append_c(out, '"');
|
||||
g_free(val);
|
||||
}
|
||||
g_string_append_c(out, '}');
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
static gchar *http_url_decode(const char *s, size_t len) {
|
||||
GString *out = g_string_new(NULL);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = s[i];
|
||||
if (c == '+') {
|
||||
g_string_append_c(out, ' ');
|
||||
} else if (c == '%' && i + 2 < len &&
|
||||
g_ascii_isxdigit(s[i + 1]) && g_ascii_isxdigit(s[i + 2])) {
|
||||
int hi = g_ascii_xdigit_value(s[i + 1]);
|
||||
int lo = g_ascii_xdigit_value(s[i + 2]);
|
||||
g_string_append_c(out, (char)((hi << 4) | lo));
|
||||
i += 2;
|
||||
} else {
|
||||
g_string_append_c(out, c);
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
/* Apply an application/x-www-form-urlencoded body of shortName=value pairs to
|
||||
* the parameter store. Returns the number of parameters successfully set. */
|
||||
static int http_apply_settings(AXParameter *handle, const char *body, size_t len) {
|
||||
int applied = 0;
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= len; i++) {
|
||||
if (i == len || body[i] == '&') {
|
||||
size_t seg_len = i - start;
|
||||
if (seg_len > 0) {
|
||||
const char *seg = body + start;
|
||||
const char *eq = memchr(seg, '=', seg_len);
|
||||
if (eq) {
|
||||
size_t nlen = (size_t)(eq - seg);
|
||||
gchar *name = g_strndup(seg, nlen);
|
||||
gchar *value = http_url_decode(eq + 1, seg_len - nlen - 1);
|
||||
if (http_is_known_param(name)) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(handle, name, value, TRUE, &err)) {
|
||||
cache_set_by_name(name, value);
|
||||
applied++;
|
||||
} else {
|
||||
syslog(LOG_WARNING, "http set %s failed: %s",
|
||||
name, err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
}
|
||||
}
|
||||
g_free(name);
|
||||
g_free(value);
|
||||
}
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
static size_t http_parse_content_length(const char *hdr, size_t hlen) {
|
||||
const char *key = "content-length:";
|
||||
size_t klen = strlen(key);
|
||||
for (size_t i = 0; i + klen <= hlen; i++) {
|
||||
if (g_ascii_strncasecmp(hdr + i, key, klen) == 0) {
|
||||
i += klen;
|
||||
while (i < hlen && (hdr[i] == ' ' || hdr[i] == '\t')) i++;
|
||||
return (size_t)strtoul(hdr + i, NULL, 10);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void http_send(GOutputStream *out, const char *status,
|
||||
const char *ctype, const char *body) {
|
||||
gchar *resp = g_strdup_printf(
|
||||
"HTTP/1.1 %s\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Content-Length: %zu\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
status, ctype, strlen(body), body);
|
||||
g_output_stream_write_all(out, resp, strlen(resp), NULL, NULL, NULL);
|
||||
g_free(resp);
|
||||
}
|
||||
|
||||
static gboolean http_on_incoming(GSocketService *service G_GNUC_UNUSED,
|
||||
GSocketConnection *connection,
|
||||
GObject *source G_GNUC_UNUSED,
|
||||
gpointer user_data) {
|
||||
AXParameter *handle = (AXParameter *)user_data;
|
||||
GInputStream *in = g_io_stream_get_input_stream(G_IO_STREAM(connection));
|
||||
GOutputStream *out = g_io_stream_get_output_stream(G_IO_STREAM(connection));
|
||||
|
||||
GString *req = g_string_new(NULL);
|
||||
char buf[2048];
|
||||
int have_headers = 0;
|
||||
size_t header_end = 0;
|
||||
size_t content_length = 0;
|
||||
|
||||
while (1) {
|
||||
gssize n = g_input_stream_read(in, buf, sizeof(buf), NULL, NULL);
|
||||
if (n <= 0) break;
|
||||
g_string_append_len(req, buf, n);
|
||||
if (!have_headers) {
|
||||
char *p = g_strstr_len(req->str, req->len, "\r\n\r\n");
|
||||
if (p) {
|
||||
have_headers = 1;
|
||||
header_end = (size_t)(p - req->str) + 4;
|
||||
content_length = http_parse_content_length(req->str, header_end);
|
||||
}
|
||||
}
|
||||
if (have_headers && req->len - header_end >= content_length) break;
|
||||
if (req->len > 262144) break; /* safety cap */
|
||||
}
|
||||
|
||||
int is_get = 0, is_post = 0, is_settings = 0;
|
||||
if (have_headers) {
|
||||
if (g_str_has_prefix(req->str, "GET ")) is_get = 1;
|
||||
if (g_str_has_prefix(req->str, "POST ")) is_post = 1;
|
||||
const char *sp1 = strchr(req->str, ' ');
|
||||
if (sp1) {
|
||||
const char *path = sp1 + 1;
|
||||
const char *sp2 = strchr(path, ' ');
|
||||
size_t plen = sp2 ? (size_t)(sp2 - path) : strlen(path);
|
||||
const char *q = memchr(path, '?', plen);
|
||||
size_t match_len = q ? (size_t)(q - path) : plen;
|
||||
if (match_len >= 8 &&
|
||||
g_ascii_strncasecmp(path + match_len - 8, "settings", 8) == 0)
|
||||
is_settings = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_settings && is_get) {
|
||||
gchar *json = http_build_settings_json(handle);
|
||||
http_send(out, "200 OK", "application/json", json);
|
||||
g_free(json);
|
||||
} else if (is_settings && is_post) {
|
||||
const char *body = req->str + header_end;
|
||||
size_t body_len = req->len - header_end;
|
||||
if (body_len > content_length) body_len = content_length;
|
||||
int applied = http_apply_settings(handle, body, body_len);
|
||||
syslog(LOG_INFO, "settings http: applied %d parameter(s)", applied);
|
||||
if (reload_timer_id) g_source_remove(reload_timer_id);
|
||||
reload_timer_id = g_timeout_add(300, debounced_restart, NULL);
|
||||
http_send(out, "200 OK", "text/plain", "OK");
|
||||
} else {
|
||||
http_send(out, "404 Not Found", "text/plain", "Not found");
|
||||
}
|
||||
|
||||
g_string_free(req, TRUE);
|
||||
g_io_stream_close(G_IO_STREAM(connection), NULL, NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void http_server_start(AXParameter *handle) {
|
||||
GError *err = NULL;
|
||||
GSocketService *service = g_socket_service_new();
|
||||
GInetAddress *addr = g_inet_address_new_from_string("127.0.0.1");
|
||||
GSocketAddress *saddr = g_inet_socket_address_new(addr, HTTP_PORT);
|
||||
|
||||
if (!g_socket_listener_add_address(G_SOCKET_LISTENER(service), saddr,
|
||||
G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_TCP,
|
||||
NULL, NULL, &err)) {
|
||||
syslog(LOG_WARNING, "settings http: bind 127.0.0.1:%d failed: %s",
|
||||
HTTP_PORT, err ? err->message : "unknown");
|
||||
if (err) g_error_free(err);
|
||||
g_object_unref(service);
|
||||
} else {
|
||||
g_signal_connect(service, "incoming", G_CALLBACK(http_on_incoming), handle);
|
||||
g_socket_service_start(service);
|
||||
syslog(LOG_INFO, "settings http server listening on 127.0.0.1:%d", HTTP_PORT);
|
||||
}
|
||||
g_object_unref(addr);
|
||||
g_object_unref(saddr);
|
||||
}
|
||||
|
||||
static gboolean signal_handler(gpointer loop) {
|
||||
syslog(LOG_INFO, "stopping");
|
||||
stop_child();
|
||||
g_main_loop_quit((GMainLoop *)loop);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
GError *error = NULL;
|
||||
|
||||
openlog(APP_NAME, LOG_PID, LOG_USER);
|
||||
syslog(LOG_INFO, "starting (root mode)");
|
||||
|
||||
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
|
||||
|
||||
/* Drop any stale auth-key sentinel from a previous run so we don't clear a
|
||||
* freshly configured key before it has been used. */
|
||||
unlink(SENTINEL_FILE);
|
||||
|
||||
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||
if (!handle) {
|
||||
syslog(LOG_ERR, "ax_parameter_new: %s",
|
||||
error ? error->message : "unknown");
|
||||
if (error) g_error_free(error);
|
||||
return 1;
|
||||
}
|
||||
g_ax_handle = handle;
|
||||
|
||||
ensure_param(handle, "AdvertiseRoutes", "");
|
||||
|
||||
load_config_cache(handle);
|
||||
write_config_file();
|
||||
start_child();
|
||||
|
||||
const char *params[] = { "CustomServer", "AuthKey", "AcceptDNS", "AcceptRoutes", "AdvertiseRoutes" };
|
||||
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
||||
if (!ax_parameter_register_callback(handle, params[i],
|
||||
parameter_changed, handle, &error)) {
|
||||
syslog(LOG_WARNING, "register callback %s: %s",
|
||||
params[i], error ? error->message : "unknown");
|
||||
if (error) { g_error_free(error); error = NULL; }
|
||||
}
|
||||
}
|
||||
|
||||
http_server_start(handle);
|
||||
|
||||
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
|
||||
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||
g_timeout_add_seconds(60, watchdog_cb, NULL);
|
||||
g_timeout_add_seconds(5, authkey_sentinel_cb, NULL);
|
||||
|
||||
syslog(LOG_INFO, "running — watching for parameter changes");
|
||||
g_main_loop_run(loop);
|
||||
|
||||
g_main_loop_unref(loop);
|
||||
ax_parameter_free(handle);
|
||||
return 0;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ FROM axisecp/acap-sdk:3.5-armv7hf-ubuntu${UBUNTU_VERSION}
|
||||
RUN apt-get update -qq && apt-get install -y --no-install-recommends upx-ucl && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY ./app /opt/app/
|
||||
COPY arm_acap3/app /opt/app/
|
||||
WORKDIR /opt/app
|
||||
|
||||
# Rename the shell startup script (the ELF launcher will take the Tailscale_VPN name)
|
||||
|
||||
@@ -2,6 +2,7 @@ PROG = Tailscale_VPN
|
||||
SRCS = param_bridge.c
|
||||
PKGS = axparameter glib-2.0 gio-2.0
|
||||
CFLAGS += $(shell pkg-config --cflags $(PKGS))
|
||||
CFLAGS += $(EXTRA_CFLAGS)
|
||||
LDADD = $(shell pkg-config --libs $(PKGS))
|
||||
|
||||
all: $(PROG)
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/bin/sh
|
||||
# Tailscale VPN run script — called by the param_bridge C binary.
|
||||
# Config is sourced from $STATE_DIR/params.conf (written by param_bridge).
|
||||
# $1 selects the variant: "standard" (userspace networking + local proxies)
|
||||
# or "root" (kernel networking, no local proxy). Defaults to "standard".
|
||||
VARIANT="${1:-standard}"
|
||||
|
||||
killall tailscaled 2>/dev/null || true
|
||||
|
||||
APP_DIR="/usr/local/packages/Tailscale_VPN"
|
||||
@@ -26,35 +30,46 @@ if [ -f "$STATE_DIR/params.conf" ]; then
|
||||
. "$STATE_DIR/params.conf"
|
||||
fi
|
||||
|
||||
logger -t "Tailscale_VPN" "Starting: http_port=$CONF_HTTP socks_port=$CONF_SOCKS custom_server=${CUSTOM_SERVER:-(default)}"
|
||||
if [ "$VARIANT" = "root" ]; then
|
||||
logger -t "Tailscale_VPN" "Starting (root mode): custom_server=${CUSTOM_SERVER:-(default)}"
|
||||
else
|
||||
logger -t "Tailscale_VPN" "Starting: http_port=$CONF_HTTP socks_port=$CONF_SOCKS custom_server=${CUSTOM_SERVER:-(default)}"
|
||||
|
||||
# Check whether a TCP port is already bound
|
||||
is_port_in_use() {
|
||||
local port=$1
|
||||
local hex_port
|
||||
hex_port=$(printf '%04X' "$port")
|
||||
grep -q ":${hex_port} " /proc/net/tcp 2>/dev/null && return 0
|
||||
grep -q ":${hex_port} " /proc/net/tcp6 2>/dev/null && return 0
|
||||
return 1
|
||||
}
|
||||
# Check whether a TCP port is already bound
|
||||
is_port_in_use() {
|
||||
local port=$1
|
||||
local hex_port
|
||||
hex_port=$(printf '%04X' "$port")
|
||||
grep -q ":${hex_port} " /proc/net/tcp 2>/dev/null && return 0
|
||||
grep -q ":${hex_port} " /proc/net/tcp6 2>/dev/null && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
if is_port_in_use "$CONF_HTTP"; then
|
||||
logger -t "Tailscale_VPN" "ERROR: HTTP proxy port $CONF_HTTP is already in use. Change it in Settings."
|
||||
exit 1
|
||||
fi
|
||||
if is_port_in_use "$CONF_SOCKS"; then
|
||||
logger -t "Tailscale_VPN" "ERROR: SOCKS5 port $CONF_SOCKS is already in use. Change it in Settings."
|
||||
exit 1
|
||||
if is_port_in_use "$CONF_HTTP"; then
|
||||
logger -t "Tailscale_VPN" "ERROR: HTTP proxy port $CONF_HTTP is already in use. Change it in Settings."
|
||||
exit 1
|
||||
fi
|
||||
if is_port_in_use "$CONF_SOCKS"; then
|
||||
logger -t "Tailscale_VPN" "ERROR: SOCKS5 port $CONF_SOCKS is already in use. Change it in Settings."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
|
||||
$TAILSCALED_PATH \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket=$SOCKET_PATH \
|
||||
--socks5-server=localhost:$CONF_SOCKS \
|
||||
--outbound-http-proxy-listen=localhost:$CONF_HTTP \
|
||||
--tun=userspace-networking \
|
||||
>/dev/null 2>&1 &
|
||||
if [ "$VARIANT" = "root" ]; then
|
||||
$TAILSCALED_PATH \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket=$SOCKET_PATH \
|
||||
>/dev/null 2>&1 &
|
||||
else
|
||||
$TAILSCALED_PATH \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket=$SOCKET_PATH \
|
||||
--socks5-server=localhost:$CONF_SOCKS \
|
||||
--outbound-http-proxy-listen=localhost:$CONF_HTTP \
|
||||
--tun=userspace-networking \
|
||||
>/dev/null 2>&1 &
|
||||
fi
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
sleep 2
|
||||
@@ -80,9 +95,14 @@ fi
|
||||
# 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
|
||||
# tailscaled netstack forwards tailnet traffic to these subnets, so no kernel IP
|
||||
# forwarding is required. Routes must still be approved in the Tailscale admin
|
||||
# console before peers can use them.
|
||||
# forwarding is required. In kernel-networking (root) mode the host must
|
||||
# forward packets between the tailnet and the LAN, so enable IP forwarding.
|
||||
# Routes must still be approved in the Tailscale admin console either way.
|
||||
if [ -n "$ADVERTISE_ROUTES" ]; then
|
||||
if [ "$VARIANT" = "root" ]; then
|
||||
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
|
||||
fi
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --advertise-routes=$ADVERTISE_ROUTES"
|
||||
fi
|
||||
|
||||
@@ -98,7 +118,11 @@ fi
|
||||
eval "$TAILSCALE_CMD"
|
||||
up_exit=$?
|
||||
if [ "$up_exit" -eq 0 ]; then
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
if [ "$VARIANT" = "root" ]; then
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running (root mode)"
|
||||
else
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
fi
|
||||
# Auth succeeded with a one-time auth key — signal param_bridge to clear it
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
: > "$STATE_DIR/authkey_clear"
|
||||
@@ -109,8 +133,10 @@ fi
|
||||
} &
|
||||
TAILSCALE_UP_PID=$!
|
||||
|
||||
logger -t "Tailscale_VPN" "HTTP/HTTPS proxy: http://127.0.0.1:$CONF_HTTP"
|
||||
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:$CONF_SOCKS"
|
||||
if [ "$VARIANT" != "root" ]; then
|
||||
logger -t "Tailscale_VPN" "HTTP/HTTPS proxy: http://127.0.0.1:$CONF_HTTP"
|
||||
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:$CONF_SOCKS"
|
||||
fi
|
||||
|
||||
# Publish tailscale's real backend state as JSON for the web UI to consume.
|
||||
# This is the authoritative connection signal (BackendState / Self.Online /
|
||||
@@ -403,8 +403,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proxy Info (always visible) -->
|
||||
<div class="card">
|
||||
<!-- Proxy Info (hidden on ROOT builds, which have no local proxy) -->
|
||||
<div class="card" id="proxy-info-card" style="display:none;">
|
||||
<div class="card-title">Proxy Configuration</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
@@ -432,12 +432,12 @@
|
||||
<input class="settings-input" id="input-authkey" type="text" autocomplete="off" placeholder="tskey-auth-... (leave blank to use browser login)">
|
||||
<span class="settings-hint">One-time use. Cleared automatically after first successful connection.</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-row" id="http-port-row" style="display:none;">
|
||||
<label class="settings-label" for="input-http-port">HTTP Proxy Port</label>
|
||||
<input class="settings-input" id="input-http-port" type="text" autocomplete="off" placeholder="8080">
|
||||
<span class="settings-hint">Port for the outbound HTTP/HTTPS proxy. Default: 8080.</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-row" id="socks-port-row" style="display:none;">
|
||||
<label class="settings-label" for="input-socks-port">SOCKS5 Proxy Port</label>
|
||||
<input class="settings-input" id="input-socks-port" type="text" autocomplete="off" placeholder="1080">
|
||||
<span class="settings-hint">Port for the SOCKS5 proxy. Default: 1080.</span>
|
||||
@@ -886,6 +886,22 @@
|
||||
// exposed through the manifest reverseProxy mapping at API_URL.
|
||||
var API_URL = '/local/' + APP + '/api/settings';
|
||||
|
||||
// Whether this build exposes local HTTP/SOCKS5 proxies (absent on ROOT
|
||||
// builds, which use kernel networking directly). Detected from whichever
|
||||
// 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;
|
||||
|
||||
function toggleProxyUi(visible) {
|
||||
hasProxyPorts = visible;
|
||||
var display = visible ? '' : 'none';
|
||||
document.getElementById('proxy-info-card').style.display = display;
|
||||
document.getElementById('http-port-row').style.display = display;
|
||||
document.getElementById('socks-port-row').style.display = display;
|
||||
}
|
||||
|
||||
function updateProxyDisplay(httpPort, socksPort) {
|
||||
if (httpPort) { cacheSet('http-port', httpPort); document.getElementById('ts-http-proxy').textContent = 'http://127.0.0.1:' + httpPort; }
|
||||
if (socksPort) { cacheSet('socks-port', socksPort); document.getElementById('ts-socks-proxy').textContent = '127.0.0.1:' + socksPort; }
|
||||
@@ -909,6 +925,7 @@
|
||||
if (dm) acceptDnsInput.checked = dm[1].trim() === 'true';
|
||||
if (rm) acceptRoutesInput.checked = rm[1].trim() === 'true';
|
||||
if (avm) advertiseRoutesInput.value = avm[1].trim();
|
||||
toggleProxyUi(!!hm && !!km);
|
||||
updateProxyDisplay(hm ? hm[1].trim() : null, km ? km[1].trim() : null);
|
||||
return true;
|
||||
}
|
||||
@@ -921,6 +938,7 @@
|
||||
if (typeof obj.AcceptDNS === 'string') acceptDnsInput.checked = obj.AcceptDNS === 'true';
|
||||
if (typeof obj.AcceptRoutes === 'string') acceptRoutesInput.checked = obj.AcceptRoutes === 'true';
|
||||
if (typeof obj.AdvertiseRoutes === 'string') advertiseRoutesInput.value = obj.AdvertiseRoutes;
|
||||
toggleProxyUi(typeof obj.HttpProxyPort === 'string' && typeof obj.Socks5Port === 'string');
|
||||
updateProxyDisplay(obj.HttpProxyPort, obj.Socks5Port);
|
||||
}
|
||||
|
||||
@@ -947,8 +965,8 @@
|
||||
function saveViaFallback(httpPort, socksPort) {
|
||||
var body = 'CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
||||
'&AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
||||
'&HttpProxyPort=' + encodeURIComponent(httpPort) +
|
||||
'&Socks5Port=' + encodeURIComponent(socksPort) +
|
||||
(hasProxyPorts ? '&HttpProxyPort=' + encodeURIComponent(httpPort) : '') +
|
||||
(hasProxyPorts ? '&Socks5Port=' + encodeURIComponent(socksPort) : '') +
|
||||
'&AcceptDNS=' + (acceptDnsInput.checked ? 'true' : 'false') +
|
||||
'&AcceptRoutes=' + (acceptRoutesInput.checked ? 'true' : 'false') +
|
||||
'&AdvertiseRoutes=' + encodeURIComponent(advertiseRoutesInput.value.trim());
|
||||
@@ -978,8 +996,8 @@
|
||||
var params = 'action=update' +
|
||||
'&root.' + APP + '.CustomServer=' + encodeURIComponent(serverInput.value.trim()) +
|
||||
'&root.' + APP + '.AuthKey=' + encodeURIComponent(authInput.value.trim()) +
|
||||
'&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) +
|
||||
'&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort) +
|
||||
(hasProxyPorts ? '&root.' + APP + '.HttpProxyPort=' + encodeURIComponent(httpPort) : '') +
|
||||
(hasProxyPorts ? '&root.' + APP + '.Socks5Port=' + encodeURIComponent(socksPort) : '') +
|
||||
'&root.' + APP + '.AcceptDNS=' + (acceptDnsInput.checked ? 'true' : 'false') +
|
||||
'&root.' + APP + '.AcceptRoutes=' + (acceptRoutesInput.checked ? 'true' : 'false') +
|
||||
'&root.' + APP + '.AdvertiseRoutes=' + encodeURIComponent(advertiseRoutesInput.value.trim());
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* ACAP parameter bridge for Tailscale VPN (userspace variant).
|
||||
* ACAP parameter bridge for Tailscale VPN.
|
||||
*
|
||||
* Responsibilities:
|
||||
* 1. Read Tailscale parameters from the ACAP parameter store (axparameter).
|
||||
@@ -13,7 +13,10 @@
|
||||
* Rapid changes within 300 ms are coalesced into a single restart.
|
||||
* 5. Watchdog: if the child exits unexpectedly, restart it.
|
||||
*
|
||||
* Runs as the unprivileged 'sdk' ACAP user — no root required.
|
||||
* 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>
|
||||
@@ -37,14 +40,22 @@
|
||||
#define RUN_SCRIPT "/usr/local/packages/Tailscale_VPN/Tailscale_VPN_run"
|
||||
#define SENTINEL_FILE "/usr/local/packages/Tailscale_VPN/localdata/authkey_clear"
|
||||
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
#define RUN_SCRIPT_VARIANT "standard"
|
||||
#else
|
||||
#define RUN_SCRIPT_VARIANT "root"
|
||||
#endif
|
||||
|
||||
static AXParameter *g_ax_handle = NULL;
|
||||
static pid_t child_pid = -1;
|
||||
static guint reload_timer_id = 0;
|
||||
|
||||
static char *cfg_custom_server = NULL;
|
||||
static char *cfg_auth_key = NULL;
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
static char *cfg_http_proxy_port = NULL;
|
||||
static char *cfg_socks5_port = NULL;
|
||||
#endif
|
||||
static char *cfg_accept_dns = NULL;
|
||||
static char *cfg_accept_routes = NULL;
|
||||
static char *cfg_advertise_routes = NULL;
|
||||
@@ -98,7 +109,7 @@ static void start_child(void) {
|
||||
return;
|
||||
}
|
||||
if (pid == 0) {
|
||||
execl(RUN_SCRIPT, RUN_SCRIPT, NULL);
|
||||
execl(RUN_SCRIPT, RUN_SCRIPT, RUN_SCRIPT_VARIANT, NULL);
|
||||
syslog(LOG_ERR, "execl %s failed: %s", RUN_SCRIPT, strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
@@ -178,8 +189,10 @@ static void load_config_cache(AXParameter *handle) {
|
||||
|
||||
LOAD("CustomServer", cfg_custom_server)
|
||||
LOAD("AuthKey", cfg_auth_key)
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
LOAD("HttpProxyPort", cfg_http_proxy_port)
|
||||
LOAD("Socks5Port", cfg_socks5_port)
|
||||
#endif
|
||||
LOAD("AcceptDNS", cfg_accept_dns)
|
||||
LOAD("AcceptRoutes", cfg_accept_routes)
|
||||
LOAD("AdvertiseRoutes", cfg_advertise_routes)
|
||||
@@ -195,17 +208,24 @@ static void write_config_file(void) {
|
||||
}
|
||||
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
|
||||
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
fprintf(f, "CONF_HTTP=%s\n", cache_get(&cfg_http_proxy_port, "8080"));
|
||||
fprintf(f, "CONF_SOCKS=%s\n", cache_get(&cfg_socks5_port, "1080"));
|
||||
#endif
|
||||
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
|
||||
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
|
||||
fprintf(f, "ADVERTISE_ROUTES=%s\n", cache_get(&cfg_advertise_routes, ""));
|
||||
fclose(f);
|
||||
chmod(CONFIG_FILE, 0600);
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
syslog(LOG_INFO, "config updated: http=%s socks=%s server=%s",
|
||||
cache_get(&cfg_http_proxy_port, "8080"),
|
||||
cache_get(&cfg_socks5_port, "1080"),
|
||||
cache_get(&cfg_custom_server, "(default)"));
|
||||
#else
|
||||
syslog(LOG_INFO, "config updated: server=%s",
|
||||
cache_get(&cfg_custom_server, "(default)"));
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── ACAP parameter callback ─────────────────────────────────────────────── */
|
||||
@@ -230,8 +250,10 @@ static void parameter_changed(const gchar *name, const gchar *value,
|
||||
|
||||
if (strcmp(short_name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||
else if (strcmp(short_name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
else if (strcmp(short_name, "HttpProxyPort") == 0) cache_set(&cfg_http_proxy_port, value);
|
||||
else if (strcmp(short_name, "Socks5Port") == 0) cache_set(&cfg_socks5_port, value);
|
||||
#endif
|
||||
else if (strcmp(short_name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||
else if (strcmp(short_name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||
else if (strcmp(short_name, "AdvertiseRoutes") == 0) cache_set(&cfg_advertise_routes, value);
|
||||
@@ -251,15 +273,20 @@ static void parameter_changed(const gchar *name, const gchar *value,
|
||||
#define HTTP_PORT 2201
|
||||
|
||||
static const char *http_param_names[] = {
|
||||
"CustomServer", "AuthKey", "HttpProxyPort", "Socks5Port",
|
||||
"CustomServer", "AuthKey",
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
"HttpProxyPort", "Socks5Port",
|
||||
#endif
|
||||
"AcceptDNS", "AcceptRoutes", "AdvertiseRoutes"
|
||||
};
|
||||
|
||||
static void cache_set_by_name(const char *name, const char *value) {
|
||||
if (strcmp(name, "CustomServer") == 0) cache_set(&cfg_custom_server, value);
|
||||
else if (strcmp(name, "AuthKey") == 0) cache_set(&cfg_auth_key, value);
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
else if (strcmp(name, "HttpProxyPort") == 0) cache_set(&cfg_http_proxy_port, value);
|
||||
else if (strcmp(name, "Socks5Port") == 0) cache_set(&cfg_socks5_port, value);
|
||||
#endif
|
||||
else if (strcmp(name, "AcceptDNS") == 0) cache_set(&cfg_accept_dns, value);
|
||||
else if (strcmp(name, "AcceptRoutes") == 0) cache_set(&cfg_accept_routes, value);
|
||||
else if (strcmp(name, "AdvertiseRoutes") == 0) cache_set(&cfg_advertise_routes, value);
|
||||
@@ -304,7 +331,12 @@ static gchar *http_build_settings_json(AXParameter *handle) {
|
||||
g_free(val);
|
||||
}
|
||||
g_string_append_c(out, '}');
|
||||
return g_string_free(out, FALSE);
|
||||
/* g_string_free(out, FALSE) is inlined by glib >= 2.76 headers into a call
|
||||
* to g_string_free_and_steal(), which doesn't exist in older glib runtimes
|
||||
* (e.g. AXIS OS 11.x). Copy out and fully free instead to stay portable. */
|
||||
gchar *json_result = g_strdup(out->str);
|
||||
g_string_free(out, TRUE);
|
||||
return json_result;
|
||||
}
|
||||
|
||||
static gchar *http_url_decode(const char *s, size_t len) {
|
||||
@@ -323,7 +355,9 @@ static gchar *http_url_decode(const char *s, size_t len) {
|
||||
g_string_append_c(out, c);
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
gchar *decoded_result = g_strdup(out->str);
|
||||
g_string_free(out, TRUE);
|
||||
return decoded_result;
|
||||
}
|
||||
|
||||
/* Apply an application/x-www-form-urlencoded body of shortName=value pairs to
|
||||
@@ -520,7 +554,10 @@ int main(void) {
|
||||
start_child();
|
||||
|
||||
const char *params[] = {
|
||||
"CustomServer", "AuthKey", "HttpProxyPort", "Socks5Port",
|
||||
"CustomServer", "AuthKey",
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
"HttpProxyPort", "Socks5Port",
|
||||
#endif
|
||||
"AcceptDNS", "AcceptRoutes", "AdvertiseRoutes"
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
||||
Reference in New Issue
Block a user