mirror of
https://github.com/Mo3he/Axis_Cam_Tailscale.git
synced 2026-10-01 03:55:40 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cbe14b8cf | ||
|
|
230a9f4861 | ||
|
|
1997941965 | ||
|
|
8b70caaf70 | ||
|
|
08f791ed24 | ||
|
|
7d8d3a04cb | ||
|
|
ccd670591e | ||
|
|
7dffcd1f58 | ||
|
|
e23272fedd | ||
|
|
559fe42294 | ||
|
|
3413d3a992 | ||
|
|
f526196d99 | ||
|
|
14518ffe16 | ||
|
|
d615191fc2 | ||
|
|
d2f2cd13a4 | ||
|
|
96f5a7c909 | ||
|
|
86309e1291 | ||
|
|
ed0f6d23fe | ||
|
|
474d783f48 | ||
|
|
9c1bec5043 | ||
|
|
15ac2c3395 | ||
|
|
437f7f9e40 | ||
|
|
53240ed2ec | ||
|
|
492392d175 |
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"app": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"versionPolicy": "mirror",
|
||||
"upstream": {
|
||||
"type": "script",
|
||||
"script": "ci/upstream-version.sh",
|
||||
"name": "Tailscale",
|
||||
"changesUrl": "https://tailscale.com/changelog"
|
||||
},
|
||||
"build": {
|
||||
"command": "./build.sh",
|
||||
"env": {}
|
||||
},
|
||||
"pins": [],
|
||||
"signing": {
|
||||
"skip": [
|
||||
"*_acap3.eap",
|
||||
"*_root.eap"
|
||||
]
|
||||
}
|
||||
}
|
||||
+149
-186
@@ -1,205 +1,168 @@
|
||||
name: Auto Build & Release Tailscale ACAP
|
||||
---
|
||||
# Upstream release -> build -> DRAFT release holding unsigned .eap files.
|
||||
# Signing is manual (Axis has no signing API); ../acap-sign.sh uploads the
|
||||
# signed packages and publishes the release. The acap-ops repo notifies.
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # Every Monday at 03:00 UTC
|
||||
- cron: "0 3 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to build. Empty resolves from upstream."
|
||||
required: false
|
||||
force:
|
||||
description: "Rebuild and re-cut the draft even if unchanged."
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: acap-release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
build: ${{ steps.decide.outputs.build }}
|
||||
release: ${{ steps.decide.outputs.release }}
|
||||
version: ${{ steps.decide.outputs.version }}
|
||||
upstream: ${{ steps.decide.outputs.upstream }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- id: decide
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_FORCE: ${{ github.event.inputs.force }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: ./ci/resolve-version.sh
|
||||
|
||||
# 1. Checkout repo
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
persist-credentials: true
|
||||
fetch-depth: 0
|
||||
build:
|
||||
needs: check
|
||||
if: needs.check.outputs.build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.check.outputs.version }}
|
||||
UPSTREAM_VERSION: ${{ needs.check.outputs.upstream }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# 2. Get latest Tailscale version
|
||||
- name: Get latest Tailscale version
|
||||
id: tailscale_version
|
||||
run: |
|
||||
# 1. Start from GitHub latest
|
||||
GH_TAG=$(curl -s https://api.github.com/repos/tailscale/tailscale/releases/latest | jq -r .tag_name)
|
||||
GH_VERSION=${GH_TAG#v}
|
||||
- name: Apply version and upstream pins
|
||||
run: ./ci/apply-version.sh "$VERSION" "$UPSTREAM_VERSION"
|
||||
|
||||
echo "GitHub latest: $GH_VERSION"
|
||||
|
||||
# 2. See if static ARM build exists for that version
|
||||
if curl -sfI "https://pkgs.tailscale.com/stable/tailscale_${GH_VERSION}_arm.tgz" > /dev/null; then
|
||||
VERSION="$GH_VERSION"
|
||||
echo "Using GitHub latest (has ARM package): $VERSION"
|
||||
else
|
||||
echo "No ARM package for $GH_VERSION, falling back to latest version on pkgs.tailscale.com"
|
||||
# 3. Derive latest version that actually has an ARM tarball
|
||||
VERSION=$(
|
||||
curl -s https://pkgs.tailscale.com/stable/ \
|
||||
| grep -o 'tailscale_[0-9.]*_arm\.tgz' \
|
||||
| sed -E 's/^tailscale_([0-9.]+)_arm\.tgz$/\1/' \
|
||||
| sort -V | tail -n1
|
||||
)
|
||||
echo "Fallback version: $VERSION"
|
||||
fi
|
||||
|
||||
echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current repo version
|
||||
id: current
|
||||
run: |
|
||||
CURRENT=$(find . -path "*/app/manifest.json" -exec jq -r '.acapPackageConf.setup.version' {} \; | sort -u | head -n1)
|
||||
echo "CURRENT_VERSION=$CURRENT" >> $GITHUB_ENV
|
||||
echo "Current repo version: $CURRENT"
|
||||
|
||||
- name: Compare versions
|
||||
id: compare
|
||||
run: |
|
||||
echo "Repo version: $CURRENT_VERSION"
|
||||
echo "Latest Tailscale version: $RELEASE_VERSION"
|
||||
echo "Trigger: ${{ github.event_name }}"
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "build_needed=true" >> $GITHUB_ENV
|
||||
echo "Manual trigger — building regardless of version."
|
||||
elif [ "$CURRENT_VERSION" = "$RELEASE_VERSION" ]; then
|
||||
echo "build_needed=false" >> $GITHUB_ENV
|
||||
echo "Already up to date. Skipping build."
|
||||
else
|
||||
echo "build_needed=true" >> $GITHUB_ENV
|
||||
echo "New version detected. Will build."
|
||||
fi
|
||||
|
||||
# 3. Download Tailscale binaries
|
||||
- name: Download Tailscale binaries
|
||||
if: env.build_needed == 'true'
|
||||
run: |
|
||||
mkdir -p tailscale_bins
|
||||
curl -L "https://pkgs.tailscale.com/stable/tailscale_${RELEASE_VERSION}_arm.tgz" -o tailscale_arm.tgz
|
||||
tar -xzf tailscale_arm.tgz -C tailscale_bins --strip-components=1
|
||||
mv tailscale_bins/tailscale tailscale_bins/tailscale_arm
|
||||
mv tailscale_bins/tailscaled tailscale_bins/tailscaled_arm
|
||||
|
||||
curl -L "https://pkgs.tailscale.com/stable/tailscale_${RELEASE_VERSION}_arm64.tgz" -o tailscale_arm64.tgz
|
||||
tar -xzf tailscale_arm64.tgz -C tailscale_bins --strip-components=1
|
||||
mv tailscale_bins/tailscale tailscale_bins/tailscale_arm64
|
||||
mv tailscale_bins/tailscaled tailscale_bins/tailscaled_arm64
|
||||
|
||||
# 4. Strip binaries to reduce package size
|
||||
- name: Strip binaries
|
||||
if: env.build_needed == 'true'
|
||||
run: |
|
||||
# Install cross-architecture strip tools
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y binutils-aarch64-linux-gnu binutils-arm-linux-gnueabihf
|
||||
|
||||
# Strip debug info and symbol tables (zero runtime/memory cost)
|
||||
aarch64-linux-gnu-strip -s tailscale_bins/tailscale_arm64
|
||||
aarch64-linux-gnu-strip -s tailscale_bins/tailscaled_arm64
|
||||
arm-linux-gnueabihf-strip -s tailscale_bins/tailscale_arm
|
||||
arm-linux-gnueabihf-strip -s tailscale_bins/tailscaled_arm
|
||||
|
||||
ls -lh tailscale_bins/
|
||||
|
||||
# 5. Build each folder, update manifest, and copy .eap files
|
||||
- name: Build all folders
|
||||
if: env.build_needed == 'true'
|
||||
run: |
|
||||
mkdir -p build
|
||||
rm -rf releases
|
||||
mkdir -p releases
|
||||
|
||||
for folder in */ ; do
|
||||
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 "$APP_LIB_DIR/tailscale"
|
||||
cp tailscale_bins/tailscaled_arm "$APP_LIB_DIR/tailscaled"
|
||||
# Repos without a tests/run.sh simply skip this.
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -x tests/run.sh ] || [ -f tests/run.sh ]; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends pkg-config libglib2.0-dev
|
||||
sh tests/run.sh
|
||||
else
|
||||
cp tailscale_bins/tailscale_arm64 "$APP_LIB_DIR/tailscale"
|
||||
cp tailscale_bins/tailscaled_arm64 "$APP_LIB_DIR/tailscaled"
|
||||
echo "no tests/run.sh, skipping"
|
||||
fi
|
||||
|
||||
# Detect variant suffix for .eap naming
|
||||
if [[ "$FOLDER_NAME" == *_ROOT ]]; then
|
||||
VARIANT="_root"
|
||||
elif [[ "$FOLDER_NAME" == *_acap3 ]]; then
|
||||
VARIANT="_acap3"
|
||||
else
|
||||
VARIANT=""
|
||||
- name: Build packages
|
||||
run: ./ci/build-packages.sh
|
||||
|
||||
- name: Verify packages
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
packages=(releases/*.eap)
|
||||
if [ ${#packages[@]} -eq 0 ]; then
|
||||
echo "no .eap produced" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update version — manifest.json for ACAP 4, package.conf for ACAP 3
|
||||
if [[ -f "$folder/app/manifest.json" ]]; then
|
||||
sed -i "s/\"version\": \".*\"/\"version\": \"${RELEASE_VERSION}\"/" "$folder/app/manifest.json"
|
||||
elif [[ -f "$folder/app/package.conf" ]]; then
|
||||
IFS='.' read -r MAJOR MINOR MICRO <<< "${RELEASE_VERSION}"
|
||||
sed -i "s/^APPMAJORVERSION=.*/APPMAJORVERSION=${MAJOR}/" "$folder/app/package.conf"
|
||||
sed -i "s/^APPMINORVERSION=.*/APPMINORVERSION=${MINOR}/" "$folder/app/package.conf"
|
||||
sed -i "s/^APPMICROVERSION=.*/APPMICROVERSION=${MICRO}/" "$folder/app/package.conf"
|
||||
fi
|
||||
|
||||
# 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" .
|
||||
|
||||
# Extract .eap files into build folder
|
||||
EAP_OUTPUT="./build/${TAG_NAME}"
|
||||
mkdir -p "$EAP_OUTPUT"
|
||||
CID=$(docker create "$TAG_NAME")
|
||||
docker cp "$CID":/opt/app "$EAP_OUTPUT"
|
||||
docker rm "$CID" >/dev/null
|
||||
|
||||
# Move all .eap files to releases folder, append variant if needed
|
||||
find "$EAP_OUTPUT" -type f -name "*.eap" | while read eap; do
|
||||
BASENAME=$(basename "$eap" .eap)
|
||||
if [[ -n "$VARIANT" ]]; then
|
||||
mv "$eap" "releases/${BASENAME}${VARIANT}.eap"
|
||||
else
|
||||
mv "$eap" "releases/${BASENAME}.eap"
|
||||
fi
|
||||
for package in "${packages[@]}"; do
|
||||
echo "== $package"
|
||||
tar tzf "$package" >/dev/null
|
||||
done
|
||||
done
|
||||
|
||||
# Clean up
|
||||
rm -rf build tailscale_bins *.tgz
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: packages
|
||||
path: releases/*.eap
|
||||
if-no-files-found: error
|
||||
|
||||
# 6. Commit updated manifests and .eap files directly to main
|
||||
- name: Commit updates to main
|
||||
if: env.build_needed == 'true'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
# Only commit manifests and ACAP 3 package.conf; do not track release artifacts
|
||||
git add */app/manifest.json arm_acap3/app/package.conf
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "Update Tailscale to v${RELEASE_VERSION}"
|
||||
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Mo3he/Axis_Cam_Tailscale.git main
|
||||
fi
|
||||
# Unstripped binaries for symbolising a crash from a shipped (stripped)
|
||||
# package. Not a release asset: they are only useful while debugging.
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: debug-symbols
|
||||
path: debug/
|
||||
if-no-files-found: ignore
|
||||
|
||||
# 7. Create GitHub Release with all new .eap files
|
||||
- name: Create GitHub Release
|
||||
if: env.build_needed == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: v${{ env.RELEASE_VERSION }}
|
||||
name: "Tailscale VPN ${{ env.RELEASE_VERSION }}"
|
||||
files: releases/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Only after a successful build, so a failed upstream jump leaves main clean.
|
||||
- name: Commit version bump
|
||||
if: needs.check.outputs.release == 'true' && github.event_name != 'pull_request'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
# Packages are already uploaded as an artifact; removing them here
|
||||
# keeps build output out of the commit regardless of .gitignore.
|
||||
rm -rf releases
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "nothing to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update to $VERSION"
|
||||
# The remote can move while a long build runs, so rebase and retry.
|
||||
for attempt in 1 2 3; do
|
||||
if git push; then
|
||||
exit 0
|
||||
fi
|
||||
echo "push rejected, rebasing (attempt $attempt)"
|
||||
git pull --rebase --autostash origin main
|
||||
done
|
||||
echo "could not push the version bump" >&2
|
||||
exit 1
|
||||
|
||||
release:
|
||||
needs: [check, build]
|
||||
if: needs.check.outputs.release == 'true' && github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.check.outputs.version }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: packages
|
||||
path: releases
|
||||
|
||||
# Stays a DRAFT: unsigned packages must never reach users, and an
|
||||
# already-published release must never be overwritten with unsigned ones.
|
||||
- name: Create or refresh draft release
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="v$VERSION"
|
||||
./ci/release-notes.sh "$VERSION" "${{ needs.check.outputs.upstream }}" > /tmp/notes.md
|
||||
cat /tmp/notes.md
|
||||
if gh release view "$tag" --json isDraft --jq '.isDraft' 2>/dev/null | grep -qx true; then
|
||||
gh release upload "$tag" releases/*.eap --clobber
|
||||
gh release edit "$tag" --notes-file /tmp/notes.md
|
||||
elif gh release view "$tag" >/dev/null 2>&1; then
|
||||
echo "release $tag is already published; refusing to touch it" >&2
|
||||
exit 1
|
||||
else
|
||||
gh release create "$tag" releases/*.eap \
|
||||
--draft \
|
||||
--title "Tailscale VPN $VERSION" \
|
||||
--notes-file /tmp/notes.md
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
# Advanced setup: default setup cannot select javascript-typescript here
|
||||
# because the UI scripts live inside index.html rather than a .js file.
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "24 4 * * 1"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze ${{ matrix.language }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [c-cpp, javascript-typescript]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: none
|
||||
|
||||
- name: Perform CodeQL analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ releases/
|
||||
build/
|
||||
*.eap
|
||||
|
||||
# Unstripped binaries kept for crash symbolisation, uploaded as a CI artifact
|
||||
debug/
|
||||
|
||||
# Do not track downloaded Tailscale tarballs and temp bins
|
||||
tailscale_bins/
|
||||
*.tgz
|
||||
|
||||
@@ -5,6 +5,10 @@ links to its full release notes on GitHub.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## 1.102.4 - 2026-09-11
|
||||
|
||||
- Update to upstream 1.102.4.
|
||||
|
||||
## [1.98.9-Signed] - 2026-07-21 - Tailscale VPN 1.98.9 (Signed)
|
||||
|
||||
- Packages are now signed with the Axis ACAP signing service and install
|
||||
|
||||
@@ -6,7 +6,6 @@ ARG SDK=acap-native-sdk
|
||||
|
||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||
|
||||
# Building the ACAP application
|
||||
COPY common/app /opt/app/
|
||||
COPY aarch64/app/manifest.json /opt/app/manifest.json
|
||||
WORKDIR /opt/app
|
||||
|
||||
+63
-63
@@ -1,67 +1,67 @@
|
||||
{
|
||||
"schemaVersion": "2.0.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "moshe@mohome.net",
|
||||
"vendorId": "70ee172dd9",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"version": "1.102.2",
|
||||
"architecture": "aarch64",
|
||||
"runMode": "respawn",
|
||||
"compatibleOsVersions": [
|
||||
{
|
||||
"max": "13"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
"reverseProxy": [
|
||||
{
|
||||
"apiPath": "api",
|
||||
"target": "http://localhost:2201/",
|
||||
"access": "admin"
|
||||
}
|
||||
],
|
||||
"paramConfig": [
|
||||
{
|
||||
"name": "CustomServer",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AuthKey",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "HttpProxyPort",
|
||||
"default": "8080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Socks5Port",
|
||||
"default": "1080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptDNS",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptRoutes",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AdvertiseRoutes",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
"schemaVersion": "2.0.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "moshe@mohome.net",
|
||||
"vendorId": "70ee172dd9",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"version": "1.102.4",
|
||||
"architecture": "aarch64",
|
||||
"runMode": "respawn",
|
||||
"compatibleOsVersions": [
|
||||
{
|
||||
"max": "13"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
"reverseProxy": [
|
||||
{
|
||||
"apiPath": "api",
|
||||
"target": "http://localhost:2201/",
|
||||
"access": "admin"
|
||||
}
|
||||
],
|
||||
"paramConfig": [
|
||||
{
|
||||
"name": "CustomServer",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AuthKey",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "HttpProxyPort",
|
||||
"default": "8080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Socks5Port",
|
||||
"default": "1080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptDNS",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptRoutes",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AdvertiseRoutes",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ ARG SDK=acap-native-sdk
|
||||
|
||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||
|
||||
# Building the ACAP application
|
||||
COPY common/app /opt/app/
|
||||
COPY aarch64_ROOT/app/manifest.json /opt/app/manifest.json
|
||||
WORKDIR /opt/app
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
{
|
||||
"schemaVersion": "1.7.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"user": {
|
||||
"group": "root",
|
||||
"username": "root"
|
||||
},
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.102.2",
|
||||
"architecture": "aarch64"
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
"reverseProxy": [
|
||||
{
|
||||
"apiPath": "api",
|
||||
"target": "http://localhost:2201/",
|
||||
"access": "admin"
|
||||
}
|
||||
],
|
||||
"paramConfig": [
|
||||
{
|
||||
"name": "CustomServer",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AuthKey",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptDNS",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptRoutes",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AdvertiseRoutes",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
"schemaVersion": "1.7.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"user": {
|
||||
"group": "root",
|
||||
"username": "root"
|
||||
},
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.102.4",
|
||||
"architecture": "aarch64"
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
"reverseProxy": [
|
||||
{
|
||||
"apiPath": "api",
|
||||
"target": "http://localhost:2201/",
|
||||
"access": "admin"
|
||||
}
|
||||
],
|
||||
"paramConfig": [
|
||||
{
|
||||
"name": "CustomServer",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AuthKey",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptDNS",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptRoutes",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AdvertiseRoutes",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ ARG SDK=acap-native-sdk
|
||||
|
||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||
|
||||
# Building the ACAP application
|
||||
COPY common/app /opt/app/
|
||||
COPY arm/app/manifest.json /opt/app/manifest.json
|
||||
WORKDIR /opt/app
|
||||
|
||||
+63
-63
@@ -1,67 +1,67 @@
|
||||
{
|
||||
"schemaVersion": "2.0.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "moshe@mohome.net",
|
||||
"vendorId": "70ee172dd9",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"version": "1.102.2",
|
||||
"architecture": "armv7hf",
|
||||
"runMode": "respawn",
|
||||
"compatibleOsVersions": [
|
||||
{
|
||||
"max": "13"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
"reverseProxy": [
|
||||
{
|
||||
"apiPath": "api",
|
||||
"target": "http://localhost:2201/",
|
||||
"access": "admin"
|
||||
}
|
||||
],
|
||||
"paramConfig": [
|
||||
{
|
||||
"name": "CustomServer",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AuthKey",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "HttpProxyPort",
|
||||
"default": "8080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Socks5Port",
|
||||
"default": "1080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptDNS",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptRoutes",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AdvertiseRoutes",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
"schemaVersion": "2.0.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "moshe@mohome.net",
|
||||
"vendorId": "70ee172dd9",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"version": "1.102.4",
|
||||
"architecture": "armv7hf",
|
||||
"runMode": "respawn",
|
||||
"compatibleOsVersions": [
|
||||
{
|
||||
"max": "13"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
"reverseProxy": [
|
||||
{
|
||||
"apiPath": "api",
|
||||
"target": "http://localhost:2201/",
|
||||
"access": "admin"
|
||||
}
|
||||
],
|
||||
"paramConfig": [
|
||||
{
|
||||
"name": "CustomServer",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AuthKey",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "HttpProxyPort",
|
||||
"default": "8080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Socks5Port",
|
||||
"default": "1080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptDNS",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptRoutes",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AdvertiseRoutes",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ ARG SDK=acap-native-sdk
|
||||
|
||||
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||
|
||||
# Building the ACAP application
|
||||
COPY common/app /opt/app/
|
||||
COPY arm_ROOT/app/manifest.json /opt/app/manifest.json
|
||||
WORKDIR /opt/app
|
||||
|
||||
+52
-52
@@ -1,56 +1,56 @@
|
||||
{
|
||||
"schemaVersion": "1.7.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"user": {
|
||||
"group": "root",
|
||||
"username": "root"
|
||||
},
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.102.2",
|
||||
"architecture": "armv7hf"
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
"reverseProxy": [
|
||||
{
|
||||
"apiPath": "api",
|
||||
"target": "http://localhost:2201/",
|
||||
"access": "admin"
|
||||
}
|
||||
],
|
||||
"paramConfig": [
|
||||
{
|
||||
"name": "CustomServer",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AuthKey",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptDNS",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptRoutes",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AdvertiseRoutes",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
"schemaVersion": "1.7.0",
|
||||
"acapPackageConf": {
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"user": {
|
||||
"group": "root",
|
||||
"username": "root"
|
||||
},
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.102.4",
|
||||
"architecture": "armv7hf"
|
||||
},
|
||||
"configuration": {
|
||||
"settingPage": "index.html",
|
||||
"reverseProxy": [
|
||||
{
|
||||
"apiPath": "api",
|
||||
"target": "http://localhost:2201/",
|
||||
"access": "admin"
|
||||
}
|
||||
],
|
||||
"paramConfig": [
|
||||
{
|
||||
"name": "CustomServer",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AuthKey",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptDNS",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AcceptRoutes",
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "AdvertiseRoutes",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-14
@@ -7,18 +7,15 @@ RUN apt-get update -qq && apt-get install -y --no-install-recommends upx-ucl &&
|
||||
COPY arm_acap3/app /opt/app/
|
||||
WORKDIR /opt/app
|
||||
|
||||
# Rename the shell startup script (the ELF launcher will take the Tailscale_VPN name)
|
||||
# The ELF launcher takes over the Tailscale_VPN name.
|
||||
RUN mv Tailscale_VPN start.sh && chmod +x start.sh
|
||||
|
||||
# Compile a minimal ELF launcher as APPNAME.
|
||||
# ACAP 3 elflibcheck requires an ELF binary and uses pidof(APPNAME) for status.
|
||||
# We fork+exec start.sh so the parent "Tailscale_VPN" process stays resident,
|
||||
# making pidof find it and the camera UI correctly show Running/Stopped.
|
||||
# ACAP 3 elflibcheck needs APPNAME to be ELF and uses pidof(APPNAME) for status.
|
||||
RUN . /opt/axis/acapsdk/environment-setup* && \
|
||||
${CC} -o Tailscale_VPN launcher.c && \
|
||||
${STRIP} -s Tailscale_VPN
|
||||
|
||||
# Strip then UPX-compress the Tailscale binaries so they fit on flash
|
||||
# UPX so the binaries fit on flash
|
||||
RUN . /opt/axis/acapsdk/environment-setup* && \
|
||||
${STRIP} -s lib/tailscale lib/tailscaled 2>/dev/null || true && \
|
||||
upx --best lib/tailscale lib/tailscaled
|
||||
@@ -26,20 +23,15 @@ RUN . /opt/axis/acapsdk/environment-setup* && \
|
||||
# ACAP 3 firmware expects the settings page at the app root, not in html/
|
||||
RUN cp html/index.html index.html
|
||||
|
||||
# Symlink tailscaled.log into html/ so the web UI can fetch it via HTTP.
|
||||
# The log is written at runtime to localdata/ (resolved path at runtime).
|
||||
# Runtime files live in localdata/; symlink them so the web UI can fetch them.
|
||||
RUN ln -sf ../localdata/tailscaled.log html/tailscaled.log
|
||||
|
||||
# Symlink the runtime status.json (written by start.sh from `tailscale status
|
||||
# --json`) into html/ so the web UI can read Tailscale's authoritative state.
|
||||
RUN ln -sf ../localdata/status.json html/status.json
|
||||
|
||||
# Build and package
|
||||
RUN . /opt/axis/acapsdk/environment-setup* && create-package.sh ./
|
||||
|
||||
# Patch STARTMODE: create-package.sh hardcodes "never" unless RESTRICTION_STARTMODE is set,
|
||||
# but the ACAP 3 SDK does not honour our package.conf's STARTMODE=respawn without it.
|
||||
# Repack the .eap with the corrected value.
|
||||
# create-package.sh hardcodes STARTMODE="never" (ignoring package.conf) unless
|
||||
# RESTRICTION_STARTMODE is set, so repack the .eap with respawn.
|
||||
RUN for eap in *.eap; do \
|
||||
tmpdir=$(mktemp -d) && tar xf "$eap" -C "$tmpdir" && \
|
||||
sed -i 's/STARTMODE="never"/STARTMODE="respawn"/' "$tmpdir/package.conf" && \
|
||||
|
||||
@@ -300,14 +300,12 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div id="status-banner" class="status-banner connecting">
|
||||
<span class="dot"></span>
|
||||
<span id="status-text" class="status-text">Checking...</span>
|
||||
<span id="status-time" class="status-time"></span>
|
||||
</div>
|
||||
|
||||
<!-- Update available -->
|
||||
<div id="update-banner" class="update-banner">
|
||||
<div class="update-text">Update available: <strong id="update-version"></strong></div>
|
||||
<a id="update-link" class="update-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
@@ -316,7 +314,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Auth (hidden by default) -->
|
||||
<div id="auth-block" class="auth-block" style="display:none;">
|
||||
<p>Authenticate this device to connect to your Tailscale network:</p>
|
||||
<a id="auth-link" class="auth-btn" href="#" target="_blank">
|
||||
@@ -326,7 +323,6 @@
|
||||
<span id="auth-url-text" class="auth-url"></span>
|
||||
</div>
|
||||
|
||||
<!-- Connection Info -->
|
||||
<div class="card" id="info-card" style="display:none;">
|
||||
<div class="card-title">Connection Details</div>
|
||||
<div class="info-grid">
|
||||
@@ -352,7 +348,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="card">
|
||||
<div class="log-controls">
|
||||
<div class="card-title" style="margin-bottom:0;">Service Log</div>
|
||||
@@ -436,11 +431,11 @@
|
||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||
}
|
||||
|
||||
// Primary: extract hostname from Axis syslog header (always the real device hostname)
|
||||
// The syslog header always carries the real device hostname.
|
||||
var node = null;
|
||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||
if (hostLine) node = hostLine[1];
|
||||
// Fallback: popBrowserAuthNow/StartLoginInteractiveAs (may contain stale acap-tailscale_vpn)
|
||||
// Fallback; may contain a stale acap-tailscale_vpn name.
|
||||
if (!node) {
|
||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||
@@ -493,7 +488,6 @@
|
||||
if (acap3ip) tsIP = acap3ip[1];
|
||||
}
|
||||
|
||||
// Cache when found, restore from cache when missing
|
||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||
tsIP = tsIP || cacheGet('ip');
|
||||
node = node || cacheGet('node');
|
||||
@@ -609,10 +603,8 @@
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
// ACAP3: also fetch the raw tailscaled.log (symlinked into html/) so the parser
|
||||
// can find IP, version, tailnet and Running state from tailscaled's own output.
|
||||
// ACAP3: tailscaled's own log has the IP, version, tailnet and Running state.
|
||||
var DAEMON_LOG_URL = 'tailscaled.log';
|
||||
// Authoritative backend state published by start.sh (symlinked into html/).
|
||||
var STATUS_URL = 'status.json';
|
||||
|
||||
// Ground truth published by start.sh from `tailscale status --json`.
|
||||
@@ -633,7 +625,6 @@
|
||||
if (st.Version) result.version = String(st.Version).split('-')[0];
|
||||
|
||||
if (bs === 'Running' && self.Online === true) {
|
||||
// Genuinely connected and reachable on the tailnet
|
||||
result.state = 'connected';
|
||||
result.url = null;
|
||||
result.ip = ip4 || result.ip;
|
||||
@@ -645,9 +636,8 @@
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Running') {
|
||||
// Backend running but node not online: transient network drop or the
|
||||
// node was removed/expired and needs re-auth. Not connected. Keep any
|
||||
// login URL the log parser found so the login button still appears.
|
||||
// Not online: transient drop or node removed/expired. Keep the log
|
||||
// parser's URL so the login button still appears.
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Stopped') {
|
||||
@@ -672,8 +662,8 @@
|
||||
var txt = res[0] + '\n' + res[1];
|
||||
var st = res[2];
|
||||
var result = parse(txt);
|
||||
renderLogs(res[0]); // show syslog in log panel; daemon log is too verbose
|
||||
// Always verify with the app status API - logs can have stale entries
|
||||
renderLogs(res[0]); // daemon log is too verbose to show
|
||||
// Logs can have stale entries; confirm the app is running.
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
|
||||
@@ -4,19 +4,9 @@
|
||||
#include <signal.h>
|
||||
|
||||
/*
|
||||
* ACAP 3 supervisor launcher for Tailscale_VPN.
|
||||
*
|
||||
* elflibcheck.sh requires APPNAME to be an ELF binary.
|
||||
* acap-startstop / respawnd / list.cgi all use pidof(APPNAME) for status.
|
||||
*
|
||||
* This binary NEVER exits voluntarily — it loops restarting start.sh if it
|
||||
* dies, so:
|
||||
* - pidof Tailscale_VPN always finds this process → UI shows "Running"
|
||||
* - respawnd never triggers (it only fires when APPNAME exits)
|
||||
* - If tailscaled OOMs and start.sh exits, we cleanly restart it
|
||||
*
|
||||
* To stop the app, acap-startstop calls stop_daemon which sends SIGTERM here.
|
||||
* We forward SIGTERM/SIGINT to the child and then exit.
|
||||
* ACAP 3 launcher: elflibcheck needs APPNAME to be ELF, and acap-startstop,
|
||||
* respawnd and list.cgi use pidof(APPNAME). So this stays resident, restarts
|
||||
* start.sh whenever it dies, and only exits on SIGTERM/SIGINT.
|
||||
*/
|
||||
|
||||
static volatile int g_stop = 0;
|
||||
@@ -37,7 +27,6 @@ int main(void)
|
||||
while (!g_stop) {
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
/* child: reset signals and exec start.sh */
|
||||
signal(SIGTERM, SIG_DFL);
|
||||
signal(SIGINT, SIG_DFL);
|
||||
execl("/usr/local/packages/Tailscale_VPN/start.sh",
|
||||
@@ -59,7 +48,6 @@ int main(void)
|
||||
g_child = -1;
|
||||
|
||||
if (!g_stop) {
|
||||
/* start.sh died unexpectedly — wait before restarting */
|
||||
sleep(3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ MENUNAME="Tailscale VPN"
|
||||
VENDOR="Mo3he"
|
||||
APPMAJORVERSION=1
|
||||
APPMINORVERSION=102
|
||||
APPMICROVERSION=2
|
||||
APPMICROVERSION=4
|
||||
APPTYPE=armv7hf
|
||||
APPNAME=Tailscale_VPN
|
||||
APPOPTS=""
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env sh
|
||||
# Build the Tailscale ACAP variants.
|
||||
#
|
||||
# ./build.sh # build every variant
|
||||
# ./build.sh aarch64 arm # build only the named variant folders
|
||||
#
|
||||
# Variant folders map to release .eap suffixes: *_ROOT -> _root, *_acap3 -> _acap3.
|
||||
# RUNTIME=docker|podman forces a container runtime; TAILSCALE_VERSION overrides
|
||||
# the version resolved by ci/upstream-version.sh.
|
||||
set -eu
|
||||
|
||||
REPO_ROOT=$(cd -P "$(dirname "$0")" && pwd)
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if [ -z "${RUNTIME:-}" ]; then
|
||||
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
||||
RUNTIME=docker
|
||||
elif command -v podman >/dev/null 2>&1; then
|
||||
RUNTIME=podman
|
||||
else
|
||||
echo 'Error: neither docker nor podman found in PATH' >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "==> Using container runtime: ${RUNTIME}"
|
||||
|
||||
VERSION="${TAILSCALE_VERSION:-$(sh ci/upstream-version.sh)}"
|
||||
[ -n "$VERSION" ] || {
|
||||
echo 'Error: could not resolve a Tailscale version' >&2
|
||||
exit 1
|
||||
}
|
||||
echo "==> Tailscale version: ${VERSION}"
|
||||
|
||||
# --- fetch and strip upstream binaries ---------------------------------------
|
||||
|
||||
BINS="${REPO_ROOT}/tailscale_bins"
|
||||
rm -rf "$BINS"
|
||||
rm -rf "${REPO_ROOT}/debug"
|
||||
mkdir -p "$BINS"
|
||||
|
||||
fetch_arch() {
|
||||
tgz_arch=$1
|
||||
suffix=$2
|
||||
echo "==> Downloading tailscale ${VERSION} (${tgz_arch})"
|
||||
curl -fsSL "https://pkgs.tailscale.com/stable/tailscale_${VERSION}_${tgz_arch}.tgz" \
|
||||
-o "${BINS}/ts_${suffix}.tgz"
|
||||
tar -xzf "${BINS}/ts_${suffix}.tgz" -C "$BINS" --strip-components=1
|
||||
mv "${BINS}/tailscale" "${BINS}/tailscale_${suffix}"
|
||||
mv "${BINS}/tailscaled" "${BINS}/tailscaled_${suffix}"
|
||||
rm -f "${BINS}/ts_${suffix}.tgz"
|
||||
}
|
||||
|
||||
fetch_arch arm arm
|
||||
fetch_arch arm64 arm64
|
||||
|
||||
# Upstream ships unstripped binaries; stripping saves ~23 MB per package. Do it
|
||||
# inside the SDK container: without host cross-binutils it silently did nothing.
|
||||
SDK_IMAGE=axisecp/acap-native-sdk:12.10.0
|
||||
SDK_UBUNTU=ubuntu24.04
|
||||
|
||||
strip_arch() {
|
||||
sdk_arch=$1
|
||||
suffix=$2
|
||||
echo "==> Stripping ${suffix} binaries"
|
||||
# Unstripped copies for symbolising crash dumps; never shipped.
|
||||
mkdir -p "${REPO_ROOT}/debug"
|
||||
cp "${BINS}/tailscale_${suffix}" "${REPO_ROOT}/debug/tailscale-${suffix}.unstripped"
|
||||
cp "${BINS}/tailscaled_${suffix}" "${REPO_ROOT}/debug/tailscaled-${suffix}.unstripped"
|
||||
# SC2016: $STRIP must expand inside the container, not on the host.
|
||||
# shellcheck disable=SC2016
|
||||
cid=$("$RUNTIME" create "${SDK_IMAGE}-${sdk_arch}-${SDK_UBUNTU}" sh -c \
|
||||
'. /opt/axis/acapsdk/environment-setup* >/dev/null 2>&1 && "${STRIP:?SDK environment did not set STRIP}" /tmp/tailscale /tmp/tailscaled')
|
||||
"$RUNTIME" cp "${BINS}/tailscale_${suffix}" "${cid}:/tmp/tailscale"
|
||||
"$RUNTIME" cp "${BINS}/tailscaled_${suffix}" "${cid}:/tmp/tailscaled"
|
||||
"$RUNTIME" start -a "$cid"
|
||||
"$RUNTIME" cp "${cid}:/tmp/tailscale" "${BINS}/tailscale_${suffix}"
|
||||
"$RUNTIME" cp "${cid}:/tmp/tailscaled" "${BINS}/tailscaled_${suffix}"
|
||||
"$RUNTIME" rm "$cid" >/dev/null
|
||||
}
|
||||
strip_arch aarch64 arm64
|
||||
strip_arch armv7hf arm
|
||||
|
||||
# --- build variants -----------------------------------------------------------
|
||||
|
||||
echo '==> Cleaning old .eap files...'
|
||||
rm -f "${REPO_ROOT}"/*.eap
|
||||
rm -rf "${REPO_ROOT}/build"
|
||||
|
||||
build_variant() {
|
||||
folder=${1%/}
|
||||
[ -d "${folder}/app" ] || return 0
|
||||
[ "$folder" = common ] && return 0
|
||||
|
||||
# aarch64/arm/aarch64_ROOT/arm_ROOT share sources via common/app; only
|
||||
# arm_acap3 carries its own self-contained app tree.
|
||||
case "$folder" in
|
||||
aarch64 | arm | aarch64_ROOT | arm_ROOT) lib_dir="common/app/lib" ;;
|
||||
*) lib_dir="${folder}/app/lib" ;;
|
||||
esac
|
||||
mkdir -p "$lib_dir"
|
||||
|
||||
case "$folder" in
|
||||
arm*) src=arm ;;
|
||||
*) src=arm64 ;;
|
||||
esac
|
||||
cp "${BINS}/tailscale_${src}" "${lib_dir}/tailscale"
|
||||
cp "${BINS}/tailscaled_${src}" "${lib_dir}/tailscaled"
|
||||
|
||||
case "$folder" in
|
||||
*_ROOT) variant="_root" ;;
|
||||
*_acap3) variant="_acap3" ;;
|
||||
*) variant="" ;;
|
||||
esac
|
||||
|
||||
tag=$(echo "$folder" | tr '[:upper:]' '[:lower:]' | tr '/ ' '__')
|
||||
echo "==> Building ${folder}"
|
||||
"$RUNTIME" build -f "${folder}/Dockerfile" --tag "$tag" .
|
||||
|
||||
out="${REPO_ROOT}/build/${tag}"
|
||||
mkdir -p "$out"
|
||||
cid=$("$RUNTIME" create "$tag")
|
||||
"$RUNTIME" cp "${cid}:/opt/app" "$out"
|
||||
"$RUNTIME" rm "$cid" >/dev/null
|
||||
|
||||
find "$out" -type f -name '*.eap' | while read -r eap; do
|
||||
base=$(basename "$eap" .eap)
|
||||
mv "$eap" "${REPO_ROOT}/${base}${variant}.eap"
|
||||
done
|
||||
}
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set -- */
|
||||
fi
|
||||
for v in "$@"; do
|
||||
build_variant "$v"
|
||||
done
|
||||
|
||||
rm -rf "${REPO_ROOT}/build" "$BINS"
|
||||
|
||||
echo '==> Done!'
|
||||
ls -lh "${REPO_ROOT}"/*.eap 2>/dev/null || true
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Write a version into every place this repo records it and refresh the
|
||||
# upstream pins declared in .acap.json.
|
||||
#
|
||||
# Usage: ci/apply-version.sh <version> [upstream-version]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION=${1:?version required}
|
||||
UPSTREAM=${2:-}
|
||||
|
||||
CONFIG=.acap.json
|
||||
cfg() { jq -r "$1" "$CONFIG"; }
|
||||
|
||||
IFS='.' read -r MAJOR MINOR MICRO <<<"$VERSION"
|
||||
|
||||
while IFS= read -r manifest; do
|
||||
[ -n "$manifest" ] || continue
|
||||
tmp=$(mktemp)
|
||||
jq --arg v "$VERSION" '.acapPackageConf.setup.version = $v' "$manifest" >"$tmp"
|
||||
mv "$tmp" "$manifest"
|
||||
echo "version $VERSION -> $manifest"
|
||||
done < <(find . -path '*/app/manifest.json' -not -path './node_modules/*' | sort)
|
||||
|
||||
while IFS= read -r conf; do
|
||||
[ -n "$conf" ] || continue
|
||||
sed -i.bak -E \
|
||||
-e "s/^APPMAJORVERSION=.*/APPMAJORVERSION=${MAJOR}/" \
|
||||
-e "s/^APPMINORVERSION=.*/APPMINORVERSION=${MINOR}/" \
|
||||
-e "s/^APPMICROVERSION=.*/APPMICROVERSION=${MICRO}/" \
|
||||
-e "s/^VERSION=.*/VERSION=${VERSION}/" \
|
||||
"$conf"
|
||||
rm -f "$conf.bak"
|
||||
echo "version $VERSION -> $conf"
|
||||
done < <(find . -path '*/app/package.conf' | sort)
|
||||
|
||||
pin_count=$(cfg '.pins | length')
|
||||
for ((i = 0; i < pin_count; i++)); do
|
||||
file=$(cfg ".pins[$i].file")
|
||||
arg=$(cfg ".pins[$i].arg")
|
||||
prefix=$(cfg ".pins[$i].prefix // empty")
|
||||
sha_url=$(cfg ".pins[$i].sha256Url // empty")
|
||||
gomodule=$(cfg ".pins[$i].goModule // empty")
|
||||
[ -f "$file" ] || {
|
||||
echo "pin target missing: $file" >&2
|
||||
continue
|
||||
}
|
||||
|
||||
# A go.mod pin has no ARG to substitute, and go refuses a bare "0.77.1", so
|
||||
# the declared prefix has to be applied here.
|
||||
if [ -n "$gomodule" ]; then
|
||||
(cd "$(dirname "$file")" && go get "${gomodule}@${prefix}${UPSTREAM:-$VERSION}" && go mod tidy)
|
||||
echo "go module ${gomodule}@${prefix}${UPSTREAM:-$VERSION} -> $file"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -n "$sha_url" ]; then
|
||||
# Checksum pins track the version pin, so the tarball is fetched and
|
||||
# hashed rather than substituted.
|
||||
url=${sha_url//\$\{VERSION\}/${UPSTREAM:-$VERSION}}
|
||||
echo "hashing $url"
|
||||
value=$(curl -fsSL "$url" | sha256sum | awk '{print $1}')
|
||||
else
|
||||
value="${prefix}${UPSTREAM:-$VERSION}"
|
||||
fi
|
||||
|
||||
sed -i.bak -E "s|^ARG ${arg}=.*|ARG ${arg}=${value}|" "$file"
|
||||
rm -f "$file.bak"
|
||||
echo "pin ${arg}=${value} -> $file"
|
||||
done
|
||||
|
||||
module=$(cfg '.upstream.module // empty')
|
||||
gomod=$(cfg '.upstream.goMod // empty')
|
||||
if [ -n "$module" ] && [ -n "$UPSTREAM" ] && [ -f "$gomod" ]; then
|
||||
(cd "$(dirname "$gomod")" && go get "${module}@${UPSTREAM}" && go mod tidy)
|
||||
echo "go module ${module}@${UPSTREAM}"
|
||||
fi
|
||||
|
||||
# Web UIs compare the installed version against the latest GitHub release. The
|
||||
# literal is marked so it cannot drift out of sync with the manifest.
|
||||
while IFS= read -r page; do
|
||||
[ -n "$page" ] || continue
|
||||
sed -i.bak -E "s|'[0-9]+\.[0-9]+\.[0-9]+'( /\* acap:installed-version \*/)|'${VERSION}'\1|g" "$page"
|
||||
rm -f "$page.bak"
|
||||
echo "installed-version $VERSION -> $page"
|
||||
done < <(grep -rl 'acap:installed-version' --include='*.html' . 2>/dev/null || true)
|
||||
|
||||
if [ -f CHANGELOG.md ] && ! grep -qE "^## \[?${VERSION}\]?" CHANGELOG.md; then
|
||||
first_heading=$(grep -n -m1 '^## ' CHANGELOG.md | cut -d: -f1 || true)
|
||||
tmp=$(mktemp)
|
||||
{
|
||||
if [ -n "$first_heading" ]; then
|
||||
head -n "$((first_heading - 1))" CHANGELOG.md
|
||||
else
|
||||
cat CHANGELOG.md
|
||||
echo
|
||||
fi
|
||||
echo "## ${VERSION} - $(date +%Y-%m-%d)"
|
||||
echo
|
||||
if [ -n "$UPSTREAM" ]; then
|
||||
echo "- Update to upstream ${UPSTREAM}."
|
||||
else
|
||||
echo "- Release ${VERSION}."
|
||||
fi
|
||||
echo
|
||||
[ -n "$first_heading" ] && tail -n +"$first_heading" CHANGELOG.md
|
||||
} >"$tmp"
|
||||
mv "$tmp" CHANGELOG.md
|
||||
echo "changelog entry added for $VERSION"
|
||||
fi
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Run the repo's build and collect every .eap into releases/.
|
||||
# The build command and extra env come from .acap.json.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
CONFIG=.acap.json
|
||||
cfg() { jq -r "$1" "$CONFIG"; }
|
||||
|
||||
COMMAND=$(cfg '.build.command')
|
||||
|
||||
while IFS=$'\t' read -r key value; do
|
||||
[ -n "$key" ] || continue
|
||||
value=${value//\$\{VERSION\}/${VERSION:-}}
|
||||
export "$key=$value"
|
||||
echo "env $key=$value"
|
||||
done < <(cfg '.build.env | to_entries[]? | [.key, .value] | @tsv')
|
||||
|
||||
rm -rf releases
|
||||
mkdir -p releases
|
||||
|
||||
echo "== $COMMAND"
|
||||
eval "$COMMAND"
|
||||
|
||||
# Repos drop packages in the root, build/, build_<arch>/ or straight into
|
||||
# releases/ depending on the repo, so gather any strays and then count what
|
||||
# actually ended up in releases/.
|
||||
while IFS= read -r package; do
|
||||
[ -n "$package" ] || continue
|
||||
mv "$package" releases/
|
||||
done < <(find . -name '*.eap' -not -path './releases/*' -not -path './.git/*')
|
||||
|
||||
found=$(find releases -name '*.eap' | wc -l | tr -d ' ')
|
||||
[ "$found" -gt 0 ] || {
|
||||
echo "no .eap produced" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "collected $found package(s):"
|
||||
ls -lh releases/
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Generate release notes for a draft release.
|
||||
#
|
||||
# Usage: ci/release-notes.sh <version> [upstream-version] > notes.md
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION=${1:?version required}
|
||||
UPSTREAM=${2:-}
|
||||
|
||||
CONFIG=.acap.json
|
||||
cfg() { jq -r "$1" "$CONFIG"; }
|
||||
|
||||
FRIENDLY=$(cfg '.friendlyName')
|
||||
UPSTREAM_NAME=$(cfg '.upstream.name // .upstream.repo // .upstream.module // empty')
|
||||
CHANGES_URL=$(cfg '.upstream.changesUrl // empty')
|
||||
CHANGES_URL=${CHANGES_URL//\$\{UPSTREAM\}/$UPSTREAM}
|
||||
|
||||
# Previous tag, so the compare link points somewhere useful.
|
||||
PREVIOUS=$(git tag --list 'v*' --sort=-v:refname | grep -v "^v${VERSION}$" | head -1 || true)
|
||||
|
||||
printf '%s %s\n\n' "$FRIENDLY" "$VERSION"
|
||||
|
||||
if [ -n "$UPSTREAM" ] && [ -n "$UPSTREAM_NAME" ]; then
|
||||
printf 'Packages **%s `%s`**.\n\n' "$UPSTREAM_NAME" "$UPSTREAM"
|
||||
fi
|
||||
|
||||
if [ -n "$CHANGES_URL" ]; then
|
||||
printf '### Upstream changes\n\n%s\n\n' "$CHANGES_URL"
|
||||
fi
|
||||
|
||||
if [ -f CHANGELOG.md ]; then
|
||||
# Pull just this version's section out of the changelog.
|
||||
section=$(awk -v v="$VERSION" '
|
||||
$0 ~ "^## \\[?" v "\\]?" { found = 1; next }
|
||||
found && /^## / { exit }
|
||||
found { print }
|
||||
' CHANGELOG.md | sed '/^[[:space:]]*$/d')
|
||||
if [ -n "$section" ]; then
|
||||
printf '### Changes\n\n%s\n\n' "$section"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '### Packages\n\n'
|
||||
printf 'Install the `signed_*.eap` matching your device architecture.\n\n'
|
||||
|
||||
# Only explain the unsigned variants when this release actually ships them.
|
||||
unsigned_note=''
|
||||
if compgen -G 'releases/*_acap3.eap' >/dev/null 2>&1; then
|
||||
unsigned_note='`_acap3`'
|
||||
fi
|
||||
if compgen -G 'releases/*_root.eap' >/dev/null 2>&1; then
|
||||
[ -n "$unsigned_note" ] && unsigned_note="${unsigned_note} and "
|
||||
unsigned_note="${unsigned_note}\`_root\`"
|
||||
fi
|
||||
if [ -n "$unsigned_note" ]; then
|
||||
printf 'Packages ending %s are published unsigned by design:\n' "$unsigned_note"
|
||||
printf 'they use manifest schema 1.x, which the Axis signing service does not accept.\n\n'
|
||||
fi
|
||||
|
||||
if [ -n "$PREVIOUS" ] && [ -n "${GITHUB_REPOSITORY:-}" ]; then
|
||||
printf '**Full changelog**: https://github.com/%s/compare/%s...v%s\n' \
|
||||
"$GITHUB_REPOSITORY" "$PREVIOUS" "$VERSION"
|
||||
fi
|
||||
Executable
+171
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Decide which version this repo should build, from .acap.json.
|
||||
# Writes build/release/version/upstream to GITHUB_OUTPUT under CI, and always
|
||||
# prints the decision so it can be run locally to preview.
|
||||
#
|
||||
# Policies:
|
||||
# mirror the ACAP version follows the upstream version exactly.
|
||||
# patch upstream is tracked through a pin; our own last digit is bumped.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
CONFIG=.acap.json
|
||||
[ -f "$CONFIG" ] || {
|
||||
echo "missing $CONFIG" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cfg() { jq -r "$1" "$CONFIG"; }
|
||||
|
||||
POLICY=$(cfg '.versionPolicy')
|
||||
UPSTREAM_TYPE=$(cfg '.upstream.type')
|
||||
EVENT_NAME=${EVENT_NAME:-manual}
|
||||
INPUT_VERSION=${INPUT_VERSION:-}
|
||||
INPUT_FORCE=${INPUT_FORCE:-false}
|
||||
|
||||
current_version() {
|
||||
local manifest conf
|
||||
manifest=$(find . -path '*/app/manifest.json' -not -path './node_modules/*' | sort | head -1)
|
||||
if [ -n "$manifest" ]; then
|
||||
jq -r '.acapPackageConf.setup.version' "$manifest"
|
||||
return
|
||||
fi
|
||||
conf=$(find . -path '*/app/package.conf' | sort | head -1)
|
||||
[ -n "$conf" ] && sed -n 's/^VERSION=//p' "$conf" | head -1
|
||||
}
|
||||
|
||||
# Current value of the first pin, used by "patch" to detect upstream movement.
|
||||
pin_value() {
|
||||
local file arg gomod module
|
||||
file=$(cfg '.pins[0].file // empty')
|
||||
arg=$(cfg '.pins[0].arg // empty')
|
||||
if [ -n "$file" ] && [ -n "$arg" ] && [ -f "$file" ]; then
|
||||
sed -n "s/^ARG ${arg}=//p" "$file" | head -1
|
||||
return
|
||||
fi
|
||||
gomod=$(cfg '.upstream.goMod // empty')
|
||||
module=$(cfg '.upstream.module // empty')
|
||||
if [ -n "$gomod" ] && [ -f "$gomod" ]; then
|
||||
# The module may appear as "require mod ver" or as "mod ver" inside a
|
||||
# require block, so take the field after the module name wherever it is.
|
||||
awk -v m="$module" '{ for (i = 1; i < NF; i++) if ($i == m) { print $(i + 1); exit } }' "$gomod"
|
||||
fi
|
||||
}
|
||||
|
||||
# FFmpeg and openvpn3 publish no releases, and their tag lists contain names
|
||||
# that are not versions, hence the explicit pattern per repo.
|
||||
upstream_version() {
|
||||
case "$UPSTREAM_TYPE" in
|
||||
github-release)
|
||||
local tag
|
||||
tag=$(gh api "repos/$(cfg '.upstream.repo')/releases/latest" --jq '.tag_name')
|
||||
[ "$(cfg '.upstream.stripV // false')" = true ] && tag=${tag#v}
|
||||
printf '%s\n' "$tag"
|
||||
;;
|
||||
github-tag)
|
||||
gh api "repos/$(cfg '.upstream.repo')/tags?per_page=100" --paginate --jq '.[].name' |
|
||||
grep -E "$(cfg '.upstream.tagPattern')" |
|
||||
sed "s|^$(cfg '.upstream.strip // empty')||" |
|
||||
sort -V | tail -1
|
||||
;;
|
||||
go-module)
|
||||
curl -fsSL "https://proxy.golang.org/$(cfg '.upstream.module')/@latest" | jq -r '.Version'
|
||||
;;
|
||||
script)
|
||||
bash "$(cfg '.upstream.script')"
|
||||
;;
|
||||
*)
|
||||
echo ''
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
bump_patch() {
|
||||
local major minor patch
|
||||
IFS='.' read -r major minor patch <<<"$1"
|
||||
printf '%s.%s.%s\n' "${major:-0}" "${minor:-0}" "$((${patch:-0} + 1))"
|
||||
}
|
||||
|
||||
# True when $1 is a strictly higher version than $2.
|
||||
version_gt() {
|
||||
[ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ]
|
||||
}
|
||||
|
||||
CURRENT=$(current_version)
|
||||
UPSTREAM=$(upstream_version || true)
|
||||
|
||||
BUILD=false
|
||||
RELEASE=false
|
||||
TARGET="$CURRENT"
|
||||
|
||||
if [ -n "$INPUT_VERSION" ]; then
|
||||
TARGET=${INPUT_VERSION#v}
|
||||
BUILD=true
|
||||
RELEASE=true
|
||||
elif [ "$POLICY" = mirror ]; then
|
||||
if [ -n "$UPSTREAM" ] && [ "$UPSTREAM" != "$CURRENT" ]; then
|
||||
if version_gt "$UPSTREAM" "$CURRENT"; then
|
||||
# Upstream is ahead: adopt its version.
|
||||
TARGET="$UPSTREAM"
|
||||
BUILD=true
|
||||
RELEASE=true
|
||||
elif [ "$UPSTREAM" != "$(pin_value)" ]; then
|
||||
# Our line already ran past upstream, so keep moving forward on it
|
||||
# rather than emitting a lower version that clashes with old tags.
|
||||
TARGET=$(bump_patch "$CURRENT")
|
||||
BUILD=true
|
||||
RELEASE=true
|
||||
fi
|
||||
fi
|
||||
elif [ "$POLICY" = patch ]; then
|
||||
if [ -n "$UPSTREAM" ] && [ "$UPSTREAM" != "$(pin_value)" ]; then
|
||||
TARGET=$(bump_patch "$CURRENT")
|
||||
BUILD=true
|
||||
RELEASE=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Pull requests build for validation but never release.
|
||||
if [ "$EVENT_NAME" = pull_request ]; then
|
||||
BUILD=true
|
||||
RELEASE=false
|
||||
fi
|
||||
|
||||
if [ "$INPUT_FORCE" = true ]; then
|
||||
BUILD=true
|
||||
RELEASE=true
|
||||
fi
|
||||
|
||||
# Never aim at a version that is already published. Stepping forward here means
|
||||
# a long build is not wasted only to be rejected by the release job. An explicit
|
||||
# version input is respected as given.
|
||||
if [ "$RELEASE" = true ] && [ -z "$INPUT_VERSION" ] && command -v gh >/dev/null 2>&1; then
|
||||
attempts=0
|
||||
while [ "$attempts" -lt 20 ] &&
|
||||
gh release view "v$TARGET" --json isDraft --jq '.isDraft' 2>/dev/null | grep -qx false; do
|
||||
echo "v$TARGET is already published; stepping forward"
|
||||
TARGET=$(bump_patch "$TARGET")
|
||||
attempts=$((attempts + 1))
|
||||
done
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
policy : $POLICY
|
||||
current : $CURRENT
|
||||
upstream : ${UPSTREAM:-n/a}
|
||||
target : $TARGET
|
||||
build : $BUILD
|
||||
release : $RELEASE
|
||||
EOF
|
||||
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
{
|
||||
echo "build=$BUILD"
|
||||
echo "release=$RELEASE"
|
||||
echo "version=$TARGET"
|
||||
echo "upstream=$UPSTREAM"
|
||||
} >>"$GITHUB_OUTPUT"
|
||||
fi
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env sh
|
||||
# Resolve the Tailscale version to package.
|
||||
#
|
||||
# Tailscale's GitHub "latest" release sometimes lands before the static ARM
|
||||
# tarballs are published, so fall back to the newest version that actually has
|
||||
# an ARM package on pkgs.tailscale.com.
|
||||
set -eu
|
||||
|
||||
GH_VERSION=$(curl -fsS https://api.github.com/repos/tailscale/tailscale/releases/latest |
|
||||
sed -n 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/p' | head -1)
|
||||
|
||||
if [ -n "${GH_VERSION}" ] &&
|
||||
curl -sfI "https://pkgs.tailscale.com/stable/tailscale_${GH_VERSION}_arm.tgz" >/dev/null 2>&1; then
|
||||
printf '%s\n' "${GH_VERSION}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
curl -fsS https://pkgs.tailscale.com/stable/ |
|
||||
grep -o 'tailscale_[0-9.]*_arm\.tgz' |
|
||||
sed -E 's/^tailscale_([0-9.]+)_arm\.tgz$/\1/' |
|
||||
sort -V | tail -1
|
||||
@@ -74,24 +74,29 @@ TAILSCALED_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
|
||||
# Arguments for `tailscale up`, built as a list so no setting is ever re-parsed
|
||||
# by the shell.
|
||||
set -- --socket="$SOCKET_PATH" up --reset --hostname="$(hostname)"
|
||||
|
||||
if [ -n "$CUSTOM_SERVER" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
||||
set -- "$@" --login-server "$CUSTOM_SERVER"
|
||||
fi
|
||||
|
||||
if [ -n "$AUTH_KEY" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
||||
set -- "$@" --authkey "$AUTH_KEY"
|
||||
fi
|
||||
|
||||
if [ "$ACCEPT_DNS" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
|
||||
set -- "$@" --accept-dns=true
|
||||
fi
|
||||
|
||||
if [ "$ACCEPT_ROUTES" = "true" ]; then
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
|
||||
set -- "$@" --accept-routes=true
|
||||
fi
|
||||
|
||||
# "192.168.1.0/24, 10.0.0.0/24" is a natural way to type the list.
|
||||
ADVERTISE_ROUTES=$(printf '%s' "$ADVERTISE_ROUTES" | tr -d ' \t\r\n')
|
||||
|
||||
# Advertise LAN subnets so this camera acts as a subnet router. Comma-separated
|
||||
# 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
|
||||
@@ -103,7 +108,7 @@ if [ -n "$ADVERTISE_ROUTES" ]; then
|
||||
echo 1 >/proc/sys/net/ipv4/ip_forward 2>/dev/null || true
|
||||
echo 1 >/proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || true
|
||||
fi
|
||||
TAILSCALE_CMD="$TAILSCALE_CMD --advertise-routes=$ADVERTISE_ROUTES"
|
||||
set -- "$@" --advertise-routes="$ADVERTISE_ROUTES"
|
||||
fi
|
||||
|
||||
# Run `tailscale up` in the background and act on its outcome. If the node needs
|
||||
@@ -115,7 +120,7 @@ fi
|
||||
# for it from here, because in POSIX sh `wait` only works on children of the
|
||||
# current shell — a subshell waiting on the parent's child returns 127.
|
||||
{
|
||||
eval "$TAILSCALE_CMD"
|
||||
"$TAILSCALE_PATH" "$@"
|
||||
up_exit=$?
|
||||
if [ "$up_exit" -eq 0 ]; then
|
||||
if [ "$VARIANT" = "root" ]; then
|
||||
|
||||
+20
-44
@@ -351,14 +351,12 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div id="status-banner" class="status-banner connecting">
|
||||
<span class="dot"></span>
|
||||
<span id="status-text" class="status-text">Checking...</span>
|
||||
<span id="status-time" class="status-time"></span>
|
||||
</div>
|
||||
|
||||
<!-- Update available -->
|
||||
<div id="update-banner" class="update-banner">
|
||||
<div class="update-text">Update available: <strong id="update-version"></strong></div>
|
||||
<a id="update-link" class="update-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
@@ -367,7 +365,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Auth (hidden by default) -->
|
||||
<div id="auth-block" class="auth-block" style="display:none;">
|
||||
<p>Authenticate this device to connect to your Tailscale network:</p>
|
||||
<a id="auth-link" class="auth-btn" href="#" target="_blank">
|
||||
@@ -377,7 +374,6 @@
|
||||
<span id="auth-url-text" class="auth-url"></span>
|
||||
</div>
|
||||
|
||||
<!-- Connection Info -->
|
||||
<div class="card" id="info-card" style="display:none;">
|
||||
<div class="card-title">Connection Details</div>
|
||||
<div class="info-grid">
|
||||
@@ -403,7 +399,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proxy Info (hidden on ROOT builds, which have no local proxy) -->
|
||||
<!-- Hidden on ROOT builds, which have no local proxy -->
|
||||
<div class="card" id="proxy-info-card" style="display:none;">
|
||||
<div class="card-title">Proxy Configuration</div>
|
||||
<div class="info-grid">
|
||||
@@ -418,7 +414,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="card">
|
||||
<div class="card-title">Settings</div>
|
||||
<div class="settings-form">
|
||||
@@ -474,7 +469,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="card">
|
||||
<div class="log-controls">
|
||||
<div class="card-title" style="margin-bottom:0;">Service Log</div>
|
||||
@@ -559,11 +553,11 @@
|
||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||
}
|
||||
|
||||
// Primary: extract hostname from Axis syslog header (always the real device hostname)
|
||||
// The syslog header always carries the real device hostname.
|
||||
var node = null;
|
||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||
if (hostLine) node = hostLine[1];
|
||||
// Fallback: popBrowserAuthNow/StartLoginInteractiveAs (may contain stale acap-tailscale_vpn)
|
||||
// Fallback; may contain a stale acap-tailscale_vpn name.
|
||||
if (!node) {
|
||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||
@@ -605,7 +599,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Parse proxy ports from log — use last match so old entries don't win
|
||||
// Use the last match so older log entries don't win.
|
||||
var httpPort = null;
|
||||
var httpProxyMatches = txt.match(/HTTP\/HTTPS proxy: http:\/\/127\.0\.0\.1:(\d+)/g);
|
||||
if (httpProxyMatches) { var m = httpProxyMatches[httpProxyMatches.length - 1].match(/:(\d+)$/); if (m) httpPort = m[1]; }
|
||||
@@ -613,7 +607,6 @@
|
||||
var socksProxyMatches = txt.match(/SOCKS5 proxy:\s+127\.0\.0\.1:(\d+)/g);
|
||||
if (socksProxyMatches) { var ms = socksProxyMatches[socksProxyMatches.length - 1].match(/:(\d+)$/); if (ms) socksPort = ms[1]; }
|
||||
|
||||
// Cache when found, restore from cache when missing
|
||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||
cacheSet('http-port', httpPort); cacheSet('socks-port', socksPort);
|
||||
tsIP = tsIP || cacheGet('ip');
|
||||
@@ -627,9 +620,8 @@
|
||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||
var isRunning = /-> Running/.test(lastState);
|
||||
|
||||
// Fallbacks only when syslog has rotated and no state transitions are visible.
|
||||
// If we CAN see state lines (e.g. "-> NeedsLogin"), trust them over our own
|
||||
// "Tailscale VPN is running" message which stays in syslog indefinitely.
|
||||
// Only when syslog rotated away all state lines: our own "Tailscale VPN
|
||||
// is running" line lingers in syslog and must not override them.
|
||||
if (!isRunning && stateLines.length === 0) {
|
||||
isRunning = /Tailscale VPN is running/.test(txt) ||
|
||||
/health\(warnable=[^)]+\): ok/.test(txt) ||
|
||||
@@ -638,12 +630,11 @@
|
||||
/localapi:/.test(txt);
|
||||
}
|
||||
|
||||
// If an auth URL appears AFTER the last Running state, re-auth is needed
|
||||
// (handles stale Running entries in syslog after reinstall or token expiry)
|
||||
// An auth URL after the last Running state means re-auth is needed
|
||||
// (stale Running entries after reinstall or token expiry).
|
||||
if (isRunning && latestUrl) {
|
||||
// Use the LATEST of '-> Running' (tailscaled state) or 'Tailscale VPN is running'
|
||||
// (our shell log). The shell log is written AFTER auth completes, so it correctly
|
||||
// post-dates the auth URL when connection succeeds.
|
||||
// Our shell log line is written after auth completes, so it post-dates
|
||||
// the auth URL on success.
|
||||
var lastRunIdx = txt.lastIndexOf('-> Running');
|
||||
var lastRunningMsgIdx = txt.lastIndexOf('Tailscale VPN is running');
|
||||
if (lastRunningMsgIdx > lastRunIdx) lastRunIdx = lastRunningMsgIdx;
|
||||
@@ -707,7 +698,6 @@
|
||||
auth.style.display = 'none';
|
||||
}
|
||||
|
||||
// Proxy card is always visible — update ports whenever known
|
||||
if (r.httpPort) document.getElementById('ts-http-proxy').textContent = 'http://127.0.0.1:' + r.httpPort;
|
||||
if (r.socksPort) document.getElementById('ts-socks-proxy').textContent = '127.0.0.1:' + r.socksPort;
|
||||
|
||||
@@ -761,7 +751,6 @@
|
||||
if (st.Version) result.version = String(st.Version).split('-')[0];
|
||||
|
||||
if (bs === 'Running' && self.Online === true) {
|
||||
// Genuinely connected and reachable on the tailnet
|
||||
result.state = 'connected';
|
||||
result.url = null;
|
||||
result.ip = ip4 || result.ip;
|
||||
@@ -773,11 +762,8 @@
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Running') {
|
||||
// Backend running but node not online: either a transient network
|
||||
// drop (no action needed) or the node was removed/expired and needs
|
||||
// re-auth. Not connected. Keep any login URL the log parser found
|
||||
// (status.json's AuthURL lags during the `tailscale up` re-auth
|
||||
// window) so the login button still appears when re-auth is needed.
|
||||
// Not online: transient drop or node removed/expired. Keep the log
|
||||
// parser's URL; status.json's AuthURL lags during re-auth.
|
||||
result.state = 'connecting';
|
||||
result.url = st.AuthURL || result.url;
|
||||
} else if (bs === 'Stopped') {
|
||||
@@ -801,7 +787,7 @@
|
||||
var st = arr[1];
|
||||
var result = parse(txt || '');
|
||||
if (txt) renderLogs(txt);
|
||||
// Verify the app is actually running - status.json can be stale if stopped
|
||||
// status.json goes stale when the app is stopped.
|
||||
checkAppRunning().then(function(running) {
|
||||
if (!running) {
|
||||
result.state = 'disconnected';
|
||||
@@ -869,7 +855,7 @@
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Settings — load current param values and save on submit
|
||||
// Settings
|
||||
var PARAM_URL = '/axis-cgi/param.cgi';
|
||||
var serverInput = document.getElementById('input-server');
|
||||
var authInput = document.getElementById('input-authkey');
|
||||
@@ -881,17 +867,11 @@
|
||||
var saveBtn = document.getElementById('save-btn');
|
||||
var saveStatus = document.getElementById('save-status');
|
||||
|
||||
// param.cgi is used when available; on devices that lack it (e.g. some
|
||||
// recorder/NVR-class devices) we fall back to the app's own endpoint,
|
||||
// exposed through the manifest reverseProxy mapping at API_URL.
|
||||
// Fallback for devices without param.cgi (e.g. recorders).
|
||||
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.
|
||||
// False on ROOT builds (no local proxy). Also keeps the ports out of saves:
|
||||
// param.cgi fails the whole update on a name the manifest never registered.
|
||||
var hasProxyPorts = false;
|
||||
|
||||
function toggleProxyUi(visible) {
|
||||
@@ -915,8 +895,7 @@
|
||||
var dm = txt.match(/root\.\S+\.AcceptDNS=(.*)/);
|
||||
var rm = txt.match(/root\.\S+\.AcceptRoutes=(.*)/);
|
||||
var avm = txt.match(/root\.\S+\.AdvertiseRoutes=(.*)/);
|
||||
// If none of the expected keys are present the endpoint isn't param.cgi
|
||||
// (e.g. a generic 404 page); signal the caller to use the fallback.
|
||||
// No expected keys means this isn't param.cgi output (e.g. a 404 page).
|
||||
if (!sm && !hm && !km) return false;
|
||||
if (sm) serverInput.value = sm[1].trim();
|
||||
if (am) authInput.value = am[1].trim();
|
||||
@@ -979,8 +958,7 @@
|
||||
.then(function(r) { return r.ok ? r.text() : Promise.reject(); })
|
||||
.then(function(txt) {
|
||||
if (/OK/.test(txt)) {
|
||||
// The app applies the change and restarts its tunnel itself,
|
||||
// so no separate control.cgi restart is needed here.
|
||||
// The app restarts its tunnel itself; no control.cgi call needed.
|
||||
setStatus('Saved. Restarting...', 'ok');
|
||||
} else {
|
||||
setStatus('Error saving settings', 'err');
|
||||
@@ -1011,15 +989,13 @@
|
||||
.then(function(txt) {
|
||||
if (/^OK/.test(txt.trim())) {
|
||||
setStatus('Saved. Restarting...', 'ok');
|
||||
// Restart the app so new settings take effect
|
||||
return fetch('/axis-cgi/applications/control.cgi?action=restart&package=' + APP,
|
||||
{ method: 'POST', credentials: 'same-origin' });
|
||||
}
|
||||
// param.cgi reachable but rejected the update — surface the error.
|
||||
setStatus('Error: ' + txt.trim(), 'err');
|
||||
})
|
||||
.catch(function() {
|
||||
// param.cgi unavailable (e.g. recorder-class device) — use the fallback.
|
||||
// param.cgi unavailable (e.g. recorder-class device).
|
||||
return saveViaFallback(httpPort, socksPort);
|
||||
})
|
||||
.then(function() { saveBtn.disabled = false; })
|
||||
|
||||
+34
-43
@@ -1,22 +1,10 @@
|
||||
// Copyright (C) 2024 Mo3he
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* ACAP parameter bridge for Tailscale VPN.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
/*
|
||||
* ACAP parameter bridge for Tailscale VPN: mirrors axparameter values into
|
||||
* CONFIG_FILE, runs Tailscale_VPN_run as a child and restarts it on changes.
|
||||
* Build with -DHAS_PROXY_PORTS for the userspace variants; ROOT has no proxy.
|
||||
*/
|
||||
|
||||
#include <axsdk/axparameter.h>
|
||||
@@ -70,10 +58,8 @@ 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. */
|
||||
/* In-place upgrades don't always register new manifest params (param.cgi then
|
||||
* 404s); ax_parameter_add fails harmlessly if the param already exists. */
|
||||
static void ensure_param(AXParameter *handle, const char *name, const char *def) {
|
||||
GError *err = NULL;
|
||||
if (!ax_parameter_add(handle, name, def, "string", &err)) {
|
||||
@@ -127,7 +113,7 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
||||
int exit_code = WEXITSTATUS(status);
|
||||
syslog(LOG_WARNING, "child exited (status %d), restarting", exit_code);
|
||||
child_pid = -1;
|
||||
/* If child exited 0, auth succeeded — clear AuthKey via axparameter */
|
||||
/* Legacy AuthKey clear: the run script only exits 0 from its TERM/INT trap. */
|
||||
if (exit_code == 0 && g_ax_handle && cfg_auth_key && *cfg_auth_key) {
|
||||
GError *err = NULL;
|
||||
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
|
||||
@@ -147,10 +133,8 @@ static gboolean watchdog_cb(gpointer G_GNUC_UNUSED data) {
|
||||
|
||||
/* ── auth-key sentinel ───────────────────────────────────────────────────── */
|
||||
|
||||
/* 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. */
|
||||
/* The run script drops SENTINEL_FILE after `tailscale up` used the auth key.
|
||||
* The exit-code-0 path in watchdog_cb rarely fires since the child stays up. */
|
||||
static gboolean authkey_sentinel_cb(gpointer G_GNUC_UNUSED data) {
|
||||
if (access(SENTINEL_FILE, F_OK) != 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
@@ -199,6 +183,19 @@ static void load_config_cache(AXParameter *handle) {
|
||||
#undef LOAD
|
||||
}
|
||||
|
||||
/* The run script sources this file, so every value must be a single-quoted
|
||||
* shell literal or it would be executed. */
|
||||
static void write_var(FILE *f, const char *name, const char *value) {
|
||||
fprintf(f, "%s='", name);
|
||||
for (const char *p = value; *p; p++) {
|
||||
if (*p == '\'')
|
||||
fputs("'\\''", f);
|
||||
else
|
||||
fputc(*p, f);
|
||||
}
|
||||
fputs("'\n", f);
|
||||
}
|
||||
|
||||
static void write_config_file(void) {
|
||||
FILE *f = fopen(CONFIG_FILE, "w");
|
||||
if (!f) {
|
||||
@@ -206,15 +203,15 @@ static void write_config_file(void) {
|
||||
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, ""));
|
||||
write_var(f, "CUSTOM_SERVER", cache_get(&cfg_custom_server, ""));
|
||||
write_var(f, "AUTH_KEY", 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"));
|
||||
write_var(f, "CONF_HTTP", cache_get(&cfg_http_proxy_port, "8080"));
|
||||
write_var(f, "CONF_SOCKS", 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, ""));
|
||||
write_var(f, "ACCEPT_DNS", cache_get(&cfg_accept_dns, "false"));
|
||||
write_var(f, "ACCEPT_ROUTES", cache_get(&cfg_accept_routes, "false"));
|
||||
write_var(f, "ADVERTISE_ROUTES", cache_get(&cfg_advertise_routes, ""));
|
||||
fclose(f);
|
||||
chmod(CONFIG_FILE, 0600);
|
||||
#ifdef HAS_PROXY_PORTS
|
||||
@@ -264,11 +261,8 @@ static void parameter_changed(const gchar *name, const gchar *value,
|
||||
}
|
||||
|
||||
/* ── embedded settings HTTP server (reverse-proxy fallback) ──────────────────
|
||||
* 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. */
|
||||
* For devices without /axis-cgi/param.cgi (e.g. recorders); reached via the
|
||||
* manifest reverseProxy at /local/Tailscale_VPN/api/settings. */
|
||||
|
||||
#define HTTP_PORT 2201
|
||||
|
||||
@@ -331,9 +325,8 @@ static gchar *http_build_settings_json(AXParameter *handle) {
|
||||
g_free(val);
|
||||
}
|
||||
g_string_append_c(out, '}');
|
||||
/* 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. */
|
||||
/* Not g_string_free(out, FALSE): glib >= 2.76 headers turn it into
|
||||
* g_string_free_and_steal(), missing from older runtimes (AXIS OS 11.x). */
|
||||
gchar *json_result = g_strdup(out->str);
|
||||
g_string_free(out, TRUE);
|
||||
return json_result;
|
||||
@@ -531,11 +524,9 @@ int main(void) {
|
||||
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. */
|
||||
/* A stale sentinel would clear a freshly configured key before use. */
|
||||
unlink(SENTINEL_FILE);
|
||||
|
||||
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
|
||||
|
||||
@@ -346,7 +346,6 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="badge">Open Source · ACAP Package</div>
|
||||
<h1>Tailscale VPN for<br>Axis <span class="hero-word" id="heroWord">Cameras</span></h1>
|
||||
@@ -363,7 +362,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Disclaimer -->
|
||||
<div style="max-width:720px;margin:0 auto;padding:0 1.5rem 1.5rem;">
|
||||
<div style="background:rgba(79,143,247,0.07);border:1px solid rgba(79,143,247,0.18);border-radius:10px;padding:12px 18px;font-size:12.5px;color:var(--muted);line-height:1.6;">
|
||||
<strong style="color:var(--text);">Disclaimer:</strong>
|
||||
@@ -373,7 +371,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<section id="features" class="features">
|
||||
<div class="feature-card">
|
||||
<div class="icon">🔒</div>
|
||||
@@ -407,12 +404,10 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Downloads -->
|
||||
<section id="downloads" class="downloads">
|
||||
<h2>Download</h2>
|
||||
<p class="subtitle">Pick the right variant for your device and Axis OS version.</p>
|
||||
<div class="download-grid">
|
||||
<!-- Standard -->
|
||||
<div class="download-card">
|
||||
<div class="tag tag-recommended">Recommended</div>
|
||||
<h3>Standard (Non-Root)</h3>
|
||||
@@ -428,7 +423,6 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ROOT -->
|
||||
<div class="download-card">
|
||||
<div class="tag tag-root">Root · Legacy</div>
|
||||
<h3>Root (Full Networking)</h3>
|
||||
@@ -444,7 +438,6 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ACAP3 -->
|
||||
<div class="download-card">
|
||||
<div class="tag tag-acap3">ACAP3 · Legacy</div>
|
||||
<h3>ACAP3 (Older Axis OS)</h3>
|
||||
@@ -459,7 +452,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Install -->
|
||||
<section id="install" class="install">
|
||||
<h2>Installation</h2>
|
||||
<div class="steps">
|
||||
@@ -487,7 +479,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/Mo3he/Axis_Cam_Tailscale" target="_blank" rel="noopener">GitHub</a>
|
||||
@@ -540,7 +531,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Rotate hero word
|
||||
var heroWord = document.getElementById('heroWord');
|
||||
var devices = ['Cameras', 'Door Stations', 'Intercoms', 'Speakers', 'Radars', 'Encoders'];
|
||||
var wordIdx = 0;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"dependencyDashboard": true,
|
||||
"prConcurrentLimit": 3,
|
||||
"prHourlyLimit": 0,
|
||||
"schedule": ["before 5am on Monday"],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "An SDK bump raises the package's minimum AXIS OS, so decide it deliberately.",
|
||||
"matchDatasources": ["docker"],
|
||||
"matchPackageNames": [
|
||||
"axisecp/acap-native-sdk",
|
||||
"axisecp/acap-sdk",
|
||||
"docker.io/axisecp/acap-native-sdk",
|
||||
"docker.io/axisecp/acap-sdk"
|
||||
],
|
||||
"dependencyDashboardApproval": true
|
||||
},
|
||||
{
|
||||
"description": "Transitive Go modules are off by default; enable so CVE fixes can land.",
|
||||
"matchManagers": ["gomod"],
|
||||
"matchDepTypes": ["indirect"],
|
||||
"enabled": true,
|
||||
"groupName": "Go indirect dependencies",
|
||||
"groupSlug": "go-indirect-dependencies"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user