Compare commits

...
229 Commits
Author SHA1 Message Date
Weston Blieden 0cbe14b8cf Trim redundant comments and correct stale ones 2026-09-25 09:59:40 +02:00
Weston Blieden 230a9f4861 Stop executing Tailscale settings as shell code
param_bridge wrote params.conf unquoted and the run script sourced it and ran tailscale up through eval, so a setting containing $(...) executed (as root in the ROOT variant). Values are now single-quoted shell literals and the up command is built as an argument list. AdvertiseRoutes also tolerates spaces after commas, which previously dropped every route. Verified on a P3288-LV (OS 12.11).
2026-09-25 08:49:21 +02:00
Weston Blieden 1997941965 ci: match the SDK approval rule regardless of registry prefix
matchPackageNames listed only axisecp/acap-native-sdk, but repos with ARG REPO=docker.io/axisecp resolve the dep as docker.io/axisecp/acap-native-sdk, which never matched. The approval gate was silently bypassed in Axis_Cam_OpenVPN and Axis_Cam_WireGuard, where SDK 12.11.0 was queued for automatic PR creation.
2026-09-21 10:27:19 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8b70caaf70 Update actions/checkout action to v7 (#38)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-21 08:32:00 +02:00
Weston Blieden 08f791ed24 ci: maintain build.yml in this repo instead of generating it
acap-ci.sh rendered this workflow from a shared template and renovate.json excluded it from updates. The generator was only ever run by hand, so the file is owned here now and Renovate maintains its action pins.
2026-09-21 08:28:08 +02:00
Weston Blieden 7d8d3a04cb ci: align Renovate config with the Go repos 2026-09-18 10:12:07 +02:00
Weston Blieden ccd670591e ci: scan UI JavaScript with CodeQL advanced setup 2026-09-18 09:07:56 +02:00
Weston Blieden 7dffcd1f58 ci: require approval for ACAP SDK bumps 2026-09-18 08:40:41 +02:00
Weston Blieden e23272fedd ci: adopt Renovate for dependency updates 2026-09-17 08:38:24 +02:00
Weston Blieden 559fe42294 Run tests/run.sh in CI when a repo has one
Regenerated from Axis_Cam_Template/ci/build.yml.tmpl via acap-ci.sh.
Repos without a tests/run.sh skip the step.
2026-09-15 12:46:05 +02:00
github-actions[bot] 3413d3a992 Update to 1.102.4 2026-09-11 09:27:07 +00:00
Weston Blieden f526196d99 Sync apply-version.sh: apply go.mod pins declared in .acap.json 2026-08-22 13:32:27 +02:00
Weston Blieden 14518ffe16 Strip inside the SDK container so every build matches CI
Stripping relied on host cross-binutils and was best-effort, so a machine without
them silently shipped a 68 MB package instead of 45 MB under the same version
number. Running the SDK's $STRIP in a container removes the host dependency.
Uses create/cp rather than a bind mount because podman machine on macOS only
mounts $HOME.
2026-08-22 13:22:01 +02:00
Weston Blieden d615191fc2 Count packages a build writes straight into releases/ 2026-08-22 08:27:52 +02:00
Weston Blieden d2f2cd13a4 Keep the UI update check in sync with the packaged version 2026-08-21 15:48:39 +02:00
Weston Blieden 96f5a7c909 Group Dependabot action updates into a single PR 2026-08-21 15:27:23 +02:00
Weston Blieden 86309e1291 Bump GitHub Actions to checkout v7, upload-artifact v7, download-artifact v8 2026-08-21 15:18:29 +02:00
Weston Blieden ed0f6d23fe Add Dependabot; never target an already-published version 2026-08-21 14:50:11 +02:00
Weston Blieden 474d783f48 Add Dependabot for GitHub Actions 2026-08-21 14:47:47 +02:00
Weston Blieden 9c1bec5043 Mirror upstream only when it is ahead of our version line 2026-08-21 14:37:23 +02:00
Weston Blieden 15ac2c3395 Only mention unsigned variants a release actually ships 2026-08-21 13:58:25 +02:00
Weston Blieden 437f7f9e40 Keep build output out of the version bump commit 2026-08-21 13:24:33 +02:00
Weston Blieden 53240ed2ec Add upstream-tracking release pipeline; extract build.sh 2026-08-21 13:07:28 +02:00
github-actions[bot] 492392d175 Update Tailscale to v1.102.3 2026-08-21 01:59:18 +00:00
github-actions[bot] e17549fef0 Update Tailscale to v1.102.2 2026-08-05 03:36:20 +00:00
github-actions[bot] 3a71aba342 Update Tailscale to v1.102.1 2026-08-04 03:35:42 +00:00
github-actions[bot] dce8beaa1b Update Tailscale to v1.98.10 2026-07-29 06:32:18 +00:00
Weston Blieden b70d664f91 fix: use Mo3he vendor name in ROOT manifests to pass schema validation 2026-07-29 08:15:03 +02:00
Weston Blieden 9ea88ccd0d docs: extend verified OS range to 13 (intro + roadmap) 2026-07-24 13:06:32 +02:00
Weston Blieden f79f631d95 docs: note verified on AXIS OS 13 (13.0.0, aarch64) 2026-07-24 13:03:13 +02:00
Weston Blieden 835ccff82f docs: add Advertise Routes (Subnet Router) to README config table 2026-07-21 19:44:51 +02:00
Weston Blieden 92262bd16c Update vendor to moshe@mohome.net and vendorId for ACAP signing
Packages are now signed with the Axis ACAP signing service. Document the
signing change and the upgrade steps (back up config and uninstall the old
version to avoid the "Vendor ID in manifest does not match" install error)
in the README and changelog.
2026-07-21 19:35:05 +02:00
github-actions[bot] 948c201ea3 Update Tailscale to v1.98.9 2026-07-21 03:39:55 +00:00
Weston Blieden 3730609abe docs: standardize README (badges, disclaimer, compatibility, sections, links, AXIS OS naming) 2026-07-17 10:11:52 +02:00
Weston BliedenandGitHub b06dbc0059 Update sponsorship links and badge styles in README 2026-07-16 13:32:38 +02:00
Weston Blieden 7d32827567 Fix invalid MD060 config value (disable rule) 2026-07-08 10:04:45 +02:00
Weston Blieden 970055051d Point super-linter at repo .markdownlint.yaml (honor shared config) 2026-07-08 09:47:50 +02:00
Weston Blieden 216ae1ebf9 Add super-linter (markdown/yaml/json/dockerfile/shell); fix lint findings 2026-07-08 09:40:14 +02:00
Weston Blieden e2daee6519 Add CHANGELOG.md generated from release history
Keep a Changelog-style summary of every released version with its date and a
link to the full GitHub release notes.
2026-07-07 20:56:09 +02:00
Weston Blieden 2a8dccfc05 Add CONTRIBUTING.md and shared lint configs
Add a contributor guide, and copy the shared .clang-format and .markdownlint.yaml
so linting is consistent across the ACAP repos.
2026-07-07 20:44:46 +02:00
Weston Blieden 02dda93272 Fix markdownlint MD034/MD040 in notices and security docs
Wrap bare URLs and the contact email in angle brackets, and add a language to
the license-text code fences, so the shared markdown lint passes.
2026-07-07 20:35:24 +02:00
Weston Blieden 0331f8d8c5 Keep LICENSE as pure BSD 3-Clause text for license detection
Move the third-party pointer out of LICENSE (it lives in THIRD_PARTY_NOTICES.md)
so GitHub's license detection recognizes the BSD 3-Clause license.
2026-07-07 20:02:57 +02:00
Weston Blieden f3d649aa92 Update security contact email to moshe@mohome.net 2026-07-07 19:53:15 +02:00
Weston Blieden bddcdf7de8 Standardize licensing and repo hygiene
- Relicense wrapper code to BSD 3-Clause (SPDX-detectable); the previous
  LICENSE combined the wrapper license with third-party notices.
- Move upstream attributions into THIRD_PARTY_NOTICES.md (nothing dropped).
- Add SECURITY.md with a private disclosure process.
2026-07-07 19:47:25 +02:00
Weston Blieden 47c3894002 Consolidate variants into common/app and prepare for AXIS OS 13
Much of this work is AXIS OS 13 preparation. Of the OS 13 breaking
changes, all are now addressed except one: recompiled against the
updated SDK for 64-bit time (Y2038), migrated to Manifest Schema v2
with declared OS compatibility, audited all binaries for executable
stack (all clean, GNU_STACK rw-), and verified the web UI end to end
over HTTPS. The only outstanding item is signing through the Axis
ACAP Portal, pending a registered vendorId.

The four ACAP 4 variants (aarch64, armv7hf, and their ROOT versions)
carried byte-identical copies of the C bridge, run script, web UI, and
Makefile per architecture, diverging only between standard and ROOT.
Merge them into a single common/app/ tree:

- param_bridge.c: proxy-port parameters gated behind -DHAS_PROXY_PORTS
  (set via EXTRA_CFLAGS in the standard Dockerfiles); ROOT builds omit
  them as before
- Tailscale_VPN_run: variant passed as $1 ("standard"/"root") selects
  userspace vs kernel networking, port-collision checks, and IP
  forwarding for advertised routes
- index.html: detects proxy support at runtime from the settings
  response, hiding the proxy card and keeping the params out of save
  requests on ROOT builds (fixes ROOT UI always showing proxy fields
  and falsely reporting save errors)

Standard variants move to ACAP Native SDK 12.10.0 and Manifest Schema
v2 (vendorId, compatibleOsVersions); verified installable and working
on OS 10.12, 11.11, and 12.10, so OS 13 readiness costs no backward
compatibility. ROOT variants intentionally stay on SDK 1.15.1 since
OS 12+ never runs root apps.

All builds (including arm_acap3) now use the repository root as build
context with -f <variant>/Dockerfile; CI updated accordingly and a
.dockerignore added to keep the context lean. Tailscale binaries are
no longer tracked in git; *.eap outputs are now gitignored.

README: correct the standard variant's floor to OS 10.12+ and ROOT to
10.12-11.x (both live-verified), update build/update instructions for
the shared tree, and check off completed OS 13 readiness items.
2026-07-03 10:38:14 +02:00
Weston Blieden 3ed71ccae3 Add subnet routing and param.cgi-less settings fallback
- Advertise routes (subnet router) support wired through run scripts and params
- Settings UI tries param.cgi first, then falls back to an app-hosted endpoint
  exposed via manifest reverseProxy, so devices without param.cgi (recorder/NVR
  class) can load and save settings without a reinstall
- Embedded GSocketService HTTP server in param_bridge serves the fallback
- Adds gio-2.0 dependency; correct aarch64 tailscale binaries
2026-07-01 09:02:28 +02:00
github-actions[bot] c0ef4852ae Update Tailscale to v1.98.8 2026-06-30 04:23:01 +00:00
Weston Blieden 741062bcef fix: status.json connection detection, auth-key auto-clear, IP display
Replace process-liveness/log-scraping heuristics with Tailscale's
authoritative backend state published as status.json.

- UI now uses BackendState + Self.Online so "no Internet" no longer
  shows Connected; surfaces the real Tailscale IP, node and tailnet.
- Auth key is auto-cleared from the UI after a successful keyed login
  via a sentinel file picked up by param_bridge (non-acap3 variants).
- Run scripts background `tailscale up` and publish status every 5s so
  re-auth (NeedsLogin + AuthURL) surfaces without starving the loop.
- Ported across aarch64, aarch64_ROOT, arm, arm_ROOT, and arm_acap3
  (acap3 uses the status.json detection; it has no auth-key param).
- Bump bundled Tailscale binaries to 1.98.4 for all variants.
- Fix CONTRIBUTING.md issue/discussion links to this repo.
- Add packaging/wrapper copyright to LICENSE.
2026-06-16 08:59:00 +02:00
Weston BliedenandGitHub c4ac8ab601 Clarify reverse-SSH tunnel requirements in README
Updated the section on reverse-SSH tunnel to clarify root requirements and added details about using a non-root SSH user.
2026-06-10 13:12:21 +02:00
Weston Blieden fd5cec50bb feat: add Accept DNS and Accept Routes toggles to all ACAP 4 variants
Restores the toggle implementation that shipped in the v1.96.4-dns-routes
release but was never committed to main, so weekly auto-builds (v1.98.x)
regressed and dropped the feature.

- manifest.json: register AcceptDNS / AcceptRoutes parameters
- param_bridge.c: cache, load, persist and live-reload the new params
- Tailscale_VPN_run: append --accept-dns / --accept-routes when enabled; add --reset
- html/index.html: add the Settings toggles
2026-06-10 12:24:09 +02:00
Weston Blieden 9a35f4e584 docs: add section on accessing tailnet services from the camera 2026-06-10 12:23:17 +02:00
github-actions[bot] 896fe380ff Update Tailscale to v1.98.4 2026-06-02 05:00:37 +00:00
github-actions[bot] b398b5498c Update Tailscale to v1.98.3 2026-05-22 04:30:26 +00:00
github-actions[bot] 08a2140d68 Update Tailscale to v1.98.2 2026-05-19 04:28:36 +00:00
Weston Blieden 1a5d2c1a24 docs: update README with Accept DNS and Accept Routes settings 2026-05-12 14:11:18 +02:00
Weston Blieden 4066273b3f Add accept-routes toggle to roadmap 2026-05-05 09:53:10 +02:00
Weston Blieden 924f04c44a Add DNS toggle and tiny-tailscale to roadmap 2026-05-05 08:32:08 +02:00
Weston Blieden 674f1c4853 Add Roadmap section with AXIS OS 13 preparation items 2026-05-05 08:00:09 +02:00
Weston BliedenandGitHub 93855e5762 Update README.md to remove oosmetrics badge
Removed outdated oosmetrics badge and adjusted spacing.
2026-05-02 21:07:29 +02:00
Weston BliedenandGitHub 889db273ec Update README with additional badges 2026-05-02 14:35:40 +02:00
Weston Blieden 52572fc61c Update homepage: remove custom variant, add ACAP3 card, rotating hero word, device language 2026-04-21 08:57:56 +02:00
Weston BliedenandGitHub 76138394e1 Update Buy Me A Coffee badge in README 2026-04-19 14:59:22 +02:00
Weston BliedenandGitHub db24028d61 Simplify description of Headscale compatibility 2026-04-17 22:01:28 +02:00
Weston Blieden f5a30122a3 Fix password manager prompt: change auth key input from type=password to type=text 2026-04-17 19:56:04 +02:00
Weston Blieden 768a859c1f Fix password manager prompt on auth key input: use autocomplete=new-password 2026-04-17 19:51:12 +02:00
Weston Blieden f395d91de8 Fix auto-focus on settings inputs: add autocomplete=off 2026-04-17 19:42:25 +02:00
Weston Blieden 41e2a508c0 v1.96.4-r3 - Remove icon from header, change accent color to #2e2d2d 2026-04-17 19:25:04 +02:00
Weston Blieden 256aaa9d23 Fix param_bridge.c build errors: move g_ax_handle declaration before watchdog_cb, add missing TRUE arg to ax_parameter_set 2026-04-17 18:31:19 +02:00
Mo3heandClaude Sonnet 4.6 3cc3cd4804 Add Buy Me a Coffee link to homepage nav and footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:26:08 +02:00
Weston BliedenandGitHub 2c7ce1dc52 Add 'Buy Me A Coffee' badge to README
Added a 'Buy Me A Coffee' badge to the README.
2026-04-17 16:20:58 +02:00
Weston BliedenandGitHub 6bacc9e92b Remove BuyMeACoffee entry from FUNDING.yml
Removed BuyMeACoffee funding option.
2026-04-17 16:20:44 +02:00
Weston BliedenandGitHub 281fed0aa0 Fix Buy Me a Coffee username case
Updated Buy Me a Coffee username to match GitHub format.
2026-04-17 16:18:51 +02:00
Weston BliedenandGitHub 988adfc83a Change Buy Me a Coffee username to lowercase
Updated Buy Me a Coffee username to lowercase.
2026-04-17 16:16:19 +02:00
Weston BliedenandGitHub 0761b6e959 Add Buy Me a Coffee funding option 2026-04-17 16:12:37 +02:00
Weston Blieden ab2f4c050e docs: ROOT variants only supported on 11.11-11.x, OS 12 removed root access 2026-04-17 14:29:25 +02:00
Weston Blieden c396f00d0b docs: fix min Axis OS to 11.11+ for all variants including ROOT 2026-04-17 14:28:21 +02:00
Weston Blieden ced340453c v1.96.4-r2: C param bridge, configurable proxy ports, SDK 1.15.1, custom variants folded in, UI fixes 2026-04-17 14:05:09 +02:00
Weston BliedenandGitHub 4afd79167e Remove redundant phrase in disclaimer section 2026-04-15 10:00:55 +02:00
Weston BliedenandGitHub e9919e069d Refine disclaimer wording in README.md
Updated disclaimer to remove redundant wording.
2026-04-15 09:56:24 +02:00
Weston Blieden 732dacc5ba ci: add acap3 variant support to build workflow 2026-04-15 08:36:03 +02:00
Weston Blieden 27e26c65e3 docs: remove emoji from README 2026-04-15 08:35:19 +02:00
Weston Blieden 8a070bf5bf docs: add arm_acap3 legacy variant to README compatibility table and feature list 2026-04-14 21:07:22 +02:00
Weston Blieden de58375eb3 chore(acap3): add remaining app source files (Makefile, launcher.c, package.conf) 2026-04-14 21:04:24 +02:00
Weston Blieden 86c6ecb385 fix(acap3): fetch tailscaled.log for status details, fix connecting→connected for all variants
- arm_acap3 start.sh: log IP, version and auth URL to syslog via 'tailscale ip/version'
- arm_acap3 index.html: fetch tailscaled.log (symlinked into html/) in addition to
  syslog so IP, version, tailnet and -> Running state are always available
- arm_acap3 Dockerfile: bake html/tailscaled.log symlink into .eap
- all variants index.html: upgrade 'connecting' (without auth URL) to 'connected'
  when VAPIX list.cgi confirms Status=Running (was only upgrading from 'disconnected')
2026-04-14 21:04:16 +02:00
Weston BliedenandGitHub 7bb541fa2a Format disclaimer section in README.md 2026-04-14 19:18:24 +02:00
Weston Blieden 3e87803625 fix: always check app status API so UI shows Stopped when ACAP is not running 2026-04-14 15:43:18 +02:00
Weston Blieden 8bd18ffc63 fix: use syslog header as primary node name source (prevents stale acap-tailscale_vpn) 2026-04-14 15:36:13 +02:00
Weston Blieden c2b15fb7ec fix: pass --hostname=$(hostname) to tailscale up so node name matches camera hostname 2026-04-14 15:29:20 +02:00
Weston Blieden b07e4f1f61 fix: use cross-strip in Dockerfiles to reduce .eap size (32M -> 16M) 2026-04-14 15:22:11 +02:00
Weston Blieden 75f9cfd4b7 fix: show auth URL even when stale Running entry exists in syslog (reinstall/re-login) 2026-04-14 15:12:19 +02:00
Weston Blieden 96e8a8fd97 fix: correct flag name --outbound-http-proxy-listen (remove trailing 'er') 2026-04-14 15:02:06 +02:00
Weston Blieden 8c9072b876 Fix README: proxy available on all non-ROOT variants, not just custom 2026-04-14 13:53:03 +02:00
Weston Blieden cf84c3cdb6 Add HTTP CONNECT proxy (8080) to standard aarch64 and arm variants 2026-04-14 13:49:24 +02:00
Weston Blieden db05725432 Add HTTP CONNECT proxy (8080) and SOCKS5 (1055) to custom variants 2026-04-14 13:44:31 +02:00
Weston Blieden ef7d73ca0f Remove --verbose=1 from tailscaled to reduce syslog noise 2026-04-14 08:13:51 +02:00
Mo3heandClaude Sonnet 4.6 5080ec39fb Fix BSD 3-Clause license compliance across all variants
- Set vendor to "Mo3he" and vendorUrl to GitHub repo in all manifest.json
  files to avoid implying Tailscale Inc. endorsement (Clause 3)
- Add Tailscale third-party notice to README.md
- Add attribution/disclaimer footer to docs/index.html

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 21:19:20 +02:00
Weston BliedenandGitHub 219ff70c84 Update disclaimer text in index.html 2026-04-13 16:59:13 +02:00
Weston BliedenandGitHub 7a7ecec47c Update README.md 2026-04-13 16:57:22 +02:00
Weston Blieden 3ab196b7b5 docs: add disclaimer to homepage hero 2026-04-13 16:34:16 +02:00
Weston BliedenandGitHub 6be4d7d8ad Format disclaimer section in README 2026-04-13 16:25:46 +02:00
Weston BliedenandGitHub 64ee3e49ab Add disclaimer to README
Added a disclaimer about the project's independence from Axis Communications.
2026-04-13 16:22:51 +02:00
Weston Blieden 4df48e8ae6 fix: node name, account, version and IP persist after syslog rotation 2026-04-13 14:33:16 +02:00
Weston Blieden 806c3309b6 CI: strip debug info from Tailscale binaries to reduce .eap size ~30% 2026-04-11 21:17:55 +02:00
Weston Blieden a4deaeacae Fix empty syslog: pipe tailscaled output via --verbose=1 to logger, add app list API fallback to all variants 2026-04-11 20:59:35 +02:00
Weston Blieden 6ba1c1a1a1 Fix UI: cache connection details in localStorage to survive syslog rotation, detect running state from health/derp/c2n patterns 2026-04-11 18:04:22 +02:00
Weston BliedenandGitHub 25df94e489 Fix README formatting and remove duplicate link
Removed duplicate homepage link and updated formatting.
2026-04-11 12:45:05 +02:00
Weston Blieden a72987391c Update Tailscale binaries to v1.96.4, fix UI parsing (last-match), add Check for Updates button, direct download links on homepage 2026-04-11 12:41:40 +02:00
Weston Blieden 04907d55bf Redesign ACAP UI: dark/light theme, version check, better log viewer 2026-04-11 09:39:17 +02:00
Weston Blieden 54a1859f14 Fix mobile layout for nav, hero, downloads, and footer 2026-04-11 07:05:55 +02:00
Weston Blieden 81e38c28b6 Add sponsor link to nav bar 2026-04-11 06:28:45 +02:00
Weston Blieden 8643e99833 Add Open Graph meta tags and social sharing image 2026-04-11 05:43:41 +02:00
Weston Blieden 95e4a9aec2 Add homepage link to README 2026-04-11 05:40:27 +02:00
Weston Blieden fb67b6dff3 Add GitHub Pages homepage with light/dark theme 2026-04-11 05:37:53 +02:00
Weston BliedenandGitHub 77fc186d0f Update sponsorship wording in README 2026-04-05 20:53:18 +02:00
Weston BliedenandGitHub b789d2e78e Update README to improve wording 2026-04-05 20:51:09 +02:00
Weston BliedenandGitHub 937b8ede3e Update project title in README.md 2026-04-05 20:49:44 +02:00
Weston BliedenandGitHub 49898008f6 Update installation instructions for clarity 2026-04-05 20:48:39 +02:00
github-actions[bot] 5b09a40231 Update Tailscale to v1.96.4 2026-03-28 02:52:43 +00:00
Weston BliedenandGitHub 98918ca6f5 Update README.md 2026-03-26 10:18:16 +01:00
Weston Blieden 08b0fdc081 Create LICENSE 2026-03-19 16:39:22 +01:00
Weston Blieden f3c6dd54f3 Update build.yml 2026-03-19 16:26:26 +01:00
Weston Blieden c3cbf8d433 Improve all variants: new UI, respawn mode, arm_custom, binary gitignore 2026-03-19 16:23:44 +01:00
github-actions[bot] 39a5a42def Update Tailscale to v1.96.2 2026-03-19 02:55:58 +00:00
github-actions[bot] 713550d1c0 Update Tailscale to v1.94.2 2026-02-26 02:46:36 +00:00
github-actions[bot] 14db783425 Update Tailscale to v1.94.1 2026-01-28 02:26:05 +00:00
github-actions[bot] c02f0aa498 Update Tailscale to v1.92.5 2026-01-07 02:22:01 +00:00
github-actions[bot] 96477cdb0d Update Tailscale to v1.92.3 2025-12-17 08:38:44 +00:00
Weston BliedenandGitHub 4e0eabeec3 Update version in manifest.json to 1.92.1 2025-12-17 09:36:08 +01:00
Weston BliedenandGitHub 59161ccd91 Downgrade version from 1.92.3 to 1.92.1 2025-12-17 09:35:26 +01:00
Weston BliedenandGitHub e1b2ac3490 Downgrade version from 1.92.3 to 1.92.1 2025-12-17 09:35:04 +01:00
Weston BliedenandGitHub 5f03ac918f Update version in manifest.json to 1.92.1 2025-12-17 09:34:45 +01:00
Weston BliedenandGitHub 2db9f27181 Update version in manifest.json to 1.92.1 2025-12-17 09:34:26 +01:00
Weston Blieden 396b450415 Store state in localdata
The tailscaled.state file in localdata will now persist across updates, so your camera stays connected to Tailscale without re-authentication
2025-12-17 09:31:34 +01:00
github-actions[bot] 2d1ff5e8a6 Update Tailscale to v1.92.3 2025-12-17 07:36:38 +00:00
Weston BliedenandGitHub 7655028477 Update cron schedule for build workflow 2025-12-17 08:33:36 +01:00
Mo3he f890d8df1a Re add lib folders after housekeeping 2025-12-15 15:48:21 +01:00
Weston BliedenandGitHub df0756a64d Update .gitignore to exclude release and Tailscale files
Added entries to ignore release artifacts and Tailscale files.
2025-12-15 12:25:06 +01:00
Weston BliedenandGitHub df2cf84c6c Modify build.yml to limit git add to manifest files
Only commit manifest files and ignore release artifacts.
2025-12-15 12:23:57 +01:00
github-actions[bot] f420e7d24c Update Tailscale to v1.92.1 2025-12-15 11:02:54 +00:00
Weston BliedenandGitHub 3dc49f1254 Enhance Tailscale version retrieval logic
Updated the workflow to check for ARM build availability and fallback to the latest version if not found.
2025-12-15 11:58:09 +01:00
github-actions[bot] 2005ba32db Update Tailscale to v1.90.9 2025-11-26 15:08:35 +00:00
github-actions[bot] 1f9c6a9e45 Update Tailscale to v1.90.8 2025-11-20 14:26:56 +00:00
github-actions[bot] 6148fc3298 Update Tailscale to v1.90.6 2025-11-03 03:52:08 +00:00
github-actions[bot] bd60c24b5b Update Tailscale to v1.90.3 2025-10-28 07:40:27 +00:00
github-actions[bot] 7926c58b3d Update Tailscale to v1.90.2 2025-10-27 03:53:33 +00:00
github-actions[bot] 6bdca7eadd Update Tailscale to v1.90.1 2025-10-23 20:07:52 +00:00
Weston BliedenandGitHub 8b941b08a5 Update README for Tailscale ACAP user space mode
Removed limitation note for Tailscale ACAP user space mode.
2025-10-22 21:36:54 +02:00
Weston BliedenandGitHub d157a91bae Fix build_needed flag logic in workflow 2025-10-22 21:31:29 +02:00
Weston BliedenandGitHub b35ed437b5 Fix build_needed condition in build workflow 2025-10-22 21:25:51 +02:00
Weston BliedenandGitHub cb390384a1 Update tailscaled command to include SOCKS5 server 2025-10-22 21:18:43 +02:00
Weston BliedenandGitHub 533fc53040 Update Tailscaled command to include SOCKS5 server 2025-10-22 21:17:11 +02:00
Weston BliedenandGitHub 0a85c286b4 Merge pull request #26 from kaleaht/SOCKS5-proxy
feat: ability to route traffic to other tailnet nodes using SOCKS5 proxy
2025-10-22 21:13:05 +02:00
Ahti Kalervo d7e7b63952 feat: ability to route traffic to other tailnet nodes using SOCKS5 proxy 2025-10-22 21:55:10 +03:00
Weston BliedenandGitHub ed7643b2eb Update README.md 2025-09-29 10:53:27 +02:00
Weston BliedenandGitHub c4b24aee40 Update README.md 2025-09-29 10:51:24 +02:00
Weston BliedenandGitHub bec473f0a8 Update README.md 2025-09-29 09:35:34 +02:00
Weston BliedenandGitHub 5278677098 Update README.md 2025-09-29 09:33:20 +02:00
github-actions[bot] c21f640622 Update Tailscale to v1.88.3 2025-09-29 03:38:36 +00:00
Weston BliedenandGitHub 83e108bb05 Update README.md 2025-09-15 13:49:03 +02:00
Weston BliedenandGitHub 38d7af6c86 Update build.yml 2025-09-15 07:58:02 +02:00
github-actions[bot] 51ead4a708 Update Tailscale to v1.88.1 2025-09-15 03:41:03 +00:00
Weston BliedenandGitHub 123906c98b Update README.md 2025-08-28 12:15:15 +02:00
Weston BliedenandGitHub f6999311b7 Update build.yml 2025-08-28 12:09:50 +02:00
Weston BliedenandGitHub a95415d687 Update build.yml 2025-08-28 12:04:18 +02:00
Weston BliedenandGitHub 7d584d1a22 Update build.yml 2025-08-28 11:50:02 +02:00
Weston BliedenandGitHub e27f0cdb34 Update build.yml 2025-08-28 11:45:27 +02:00
Weston BliedenandGitHub e5954eac52 Update build.yml 2025-08-28 11:41:17 +02:00
Weston BliedenandGitHub 91a45be400 Update build.yml 2025-08-28 11:35:04 +02:00
Weston BliedenandGitHub 9b06b49fef Update build.yml 2025-08-28 11:26:37 +02:00
Weston BliedenandGitHub 811aa9e2f5 Update README.md 2025-08-27 17:14:07 +02:00
Weston BliedenandGitHub f69a1971a2 Update README.md 2025-08-27 17:12:09 +02:00
github-actions[bot] e01a2ab95f Update Tailscale to v1.86.2 2025-08-27 15:08:38 +00:00
Weston BliedenandGitHub 5b87555295 Delete all directory 2025-08-27 17:04:51 +02:00
Weston BliedenandGitHub 4a4618344a Create build.yml 2025-08-27 17:04:08 +02:00
Weston BliedenandGitHub de739dec13 Update README.md 2025-05-26 18:25:20 +02:00
Weston BliedenandGitHub ef0cb156be Update README.md 2025-05-26 18:24:37 +02:00
Weston Blieden f3556d3e76 Merge branch 'main' of https://github.com/Mo3he/Axis_Cam_Tailscale 2025-05-26 15:18:51 +02:00
Weston Blieden d5e2be837f Update To Version 1.84.0
Updated Tailscale Binaries to version 1.84.0
2025-05-26 15:18:47 +02:00
Weston BliedenandGitHub 10f5cec1dd Update README.md 2025-04-30 08:59:39 +02:00
Weston BliedenandGitHub eb46f0f201 Update README.md 2025-04-30 08:56:54 +02:00
Weston BliedenandGitHub 5a6aada216 Update README.md 2025-04-30 08:54:08 +02:00
Weston BliedenandGitHub 65509f037e Update README.md 2025-04-30 08:40:22 +02:00
Weston BliedenandGitHub 5e27f2f6ca Update README.md 2025-04-30 08:20:02 +02:00
Weston Blieden f27769f415 Added "ROOT" versions 2025-04-25 13:34:46 +02:00
Weston BliedenandGitHub b8038797f3 Update README.md 2025-04-16 19:59:46 +02:00
Weston BliedenandGitHub 07f73648bf Update README.md 2025-04-16 18:41:39 +02:00
Weston BliedenandGitHub 108ea203b4 Update README.md 2025-04-16 18:40:32 +02:00
Weston BliedenandGitHub 4b282e81e6 Update README.md 2025-04-16 18:38:50 +02:00
Weston Blieden 34943f62b9 Added custom version 2025-04-16 18:35:38 +02:00
Weston Blieden 3d12e28a1f Update binaries to 1.82.0 2025-04-11 09:51:39 +02:00
Weston BliedenandGitHub b453bcb6f2 Update README.md 2025-03-24 10:02:25 +01:00
Weston Blieden 616d270350 Update Tailscale binaries to v1.80.3 2025-03-24 10:01:38 +01:00
Weston Blieden 17c2ae705c Update Tailscale binaries to v1.78.1 2025-01-13 08:19:57 +01:00
Weston Blieden 54be4dd84f V 1.76.1
Updated Tailscale binaries to version 1.76.1
2024-10-24 08:03:20 +02:00
Weston Blieden 452f7d54d8 V1.72.1
Updated tailscale binaries to v1.72.1
2024-08-26 08:33:38 +02:00
Weston BliedenandGitHub b444c964f6 Update README.md 2024-08-08 18:34:06 +02:00
Weston BliedenandGitHub e0178b531e Update README.md 2024-08-08 18:27:54 +02:00
Weston BliedenandGitHub 46f7a524c0 Merge pull request #15 from MicroTechnology-Services/allarch
Add all arch build
2024-08-08 18:25:38 +02:00
Wes Malone f5ab42fd3f Add all arch 2024-08-07 10:32:02 -05:00
Weston BliedenandGitHub 019beb3c73 Update README.md 2024-06-27 10:10:42 +02:00
Weston BliedenandGitHub db343b4447 Update README.md 2024-06-27 07:08:30 +02:00
Weston Blieden f3163cae45 Updated to V1.68.1
Updated tailscale binaries and removed need for root to run
2024-06-27 07:04:56 +02:00
Weston Blieden b7391743d6 Good news, everyone! 2024-06-24 08:17:36 +02:00
Weston Blieden 2b373227d2 Fixed index for auto update versions 2024-02-28 08:11:23 +01:00
Weston BliedenandGitHub fa4e6edde1 Update README.md 2024-02-21 09:36:45 +01:00
Weston BliedenandGitHub c26643adf0 Update README.md 2024-02-21 09:27:03 +01:00
Weston Blieden 39bc29e1ab Merge branch 'main' of https://github.com/Mo3he/Axis_Cam_Tailscale 2024-02-21 08:46:31 +01:00
Weston Blieden af3813135a Updated to version 1.60.0, add Auto Update version
Updated Tailscale binaries to 1.60.0 and added version that automatically keeps tailscale up to date.
2024-02-21 08:46:15 +01:00
Weston BliedenandGitHub 5fe5a01f59 Update README.md 2024-02-15 08:06:29 +01:00
Weston BliedenandGitHub 080b3bb90e Update README.md 2024-02-02 11:19:18 +01:00
Weston Blieden 41b9bd04b5 Update to V1.56.1
Updated Tailscale Static Binaries to 1.56.1
2024-01-17 12:19:57 +01:00
Weston Blieden 86d902d289 V1.54.0 Update
Updated to Tailscale Version 1.54.0
2023-11-28 09:05:17 +01:00
Weston Blieden f4b64b7f3a Updated Tailscale to Version 1.52.0 2023-11-01 16:30:44 +01:00
Weston BliedenandGitHub d00a45effb Update README.md 2023-10-16 10:22:30 +02:00
Weston Blieden 74af48b024 Update Tailscale version to 1.50.1 2023-10-16 10:20:05 +02:00
Weston BliedenandGitHub 0e89c01546 Update README.md 2023-10-02 08:21:34 +02:00
Weston BliedenandGitHub 87de22b8a1 Update CONTRIBUTING.md 2023-09-13 14:27:20 +02:00
Weston BliedenandGitHub 0ddd3c1ee2 Delete CODEOWNERS 2023-09-13 14:25:56 +02:00
Weston BliedenandGitHub 0e6227149f Update README.md 2023-09-13 14:14:56 +02:00
Weston BliedenandGitHub 7943176695 Update README.md 2023-09-13 14:14:07 +02:00
Weston Blieden ad5e52aff3 Updated binaries to 1.48.2 2023-09-13 14:10:31 +02:00
Weston Blieden 70e4e8d49d Updated to 1.44.0 2023-07-04 11:01:59 +02:00
Weston Blieden b2aec2cbf8 Removed offending material 2023-05-03 13:31:44 +02:00
Weston BliedenandGitHub 343fe09e1f Merge pull request #4 from tris/cleanup
Remove build artifacts
2023-04-20 08:54:11 +02:00
Weston BliedenandGitHub 852a7546b8 Merge pull request #5 from tris/arch
Remove arch from manifest.json
2023-04-20 08:53:35 +02:00
Tristan Horn 5ab1a3e063 chore: typos/whitespace 2023-04-18 21:31:57 -07:00
Tristan Horn e283930aaf chore: remove arch from manifest.json (schema 1.3 generates automatically) 2023-04-18 21:22:02 -07:00
Tristan Horn 1062a63513 chore: remove (most) build artifacts 2023-04-18 18:13:02 -07:00
Weston BliedenandGitHub 0d2e5da139 Update README.md 2023-04-17 10:04:20 +02:00
80 changed files with 5046 additions and 459 deletions
+22
View File
@@ -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"
]
}
}
+102
View File
@@ -0,0 +1,102 @@
---
Language: Cpp
# BasedOnStyle: LLVM
AccessModifierOffset: -2
AlignAfterOpenBracket: AlwaysBreak
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Right
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: MultiLine
BinPackArguments: false
BinPackParameters: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
- Regex: '.*'
Priority: 1
IncludeIsMainRegex: '(Test)?$'
IndentCaseLabels: false
IndentPPDirectives: None
IndentWidth: 4
IndentWrappedFunctionNames: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
ReflowComments: true
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 8
UseTab: Never
...
+12
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
DEFAULT_BRANCH=origin/main
LINTER_RULES_PATH=/
VALIDATE_ALL_CODEBASE=true
IGNORE_GITIGNORED_FILES=true
YAML_CONFIG_FILE=.yamllint.yaml
MARKDOWN_CONFIG_FILE=.markdownlint.yaml
VALIDATE_DOCKERFILE_HADOLINT=true
VALIDATE_JSON=true
VALIDATE_MARKDOWN=true
VALIDATE_SHELL_SHFMT=true
VALIDATE_YAML=true
+168
View File
@@ -0,0 +1,168 @@
---
# 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 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:
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
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
- name: Apply version and upstream pins
run: ./ci/apply-version.sh "$VERSION" "$UPSTREAM_VERSION"
# Repos without a tests/run.sh simply skip this.
- name: Run unit tests
run: |
set -euo pipefail
if [ -x tests/run.sh ] || [ -f tests/run.sh ]; then
sudo apt-get update
sudo apt-get install -y --no-install-recommends pkg-config libglib2.0-dev
sh tests/run.sh
else
echo "no tests/run.sh, skipping"
fi
- name: Build packages
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
for package in "${packages[@]}"; do
echo "== $package"
tar tzf "$package" >/dev/null
done
- uses: actions/upload-artifact@v7
with:
name: packages
path: releases/*.eap
if-no-files-found: error
# 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
# 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
+41
View File
@@ -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 }}"
+22
View File
@@ -0,0 +1,22 @@
---
name: Lint
on: push
jobs:
Build:
name: Lint code base
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Environment
run: cat .github/super-linter.env >> "$GITHUB_ENV"
- name: Lint code base
uses: super-linter/super-linter/slim@v8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+18 -1
View File
@@ -1 +1,18 @@
.DS_Store **/.DS_Store
**/build
# Do not track release artifacts (local .eap build outputs stay untracked anywhere in the tree)
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
# Tailscale binaries - downloaded fresh by CI at build time, not stored in git
*/app/lib/tailscale
*/app/lib/tailscaled
+8
View File
@@ -0,0 +1,8 @@
---
# Enforce error-level Dockerfile correctness. Warnings/info are advisory: the
# ACAP cross-compile Dockerfiles use accepted patterns (cd in RUN, ARG-templated
# FROM tags hadolint cannot resolve, optional pipefail).
failure-threshold: error
ignored:
# Pin versions in 'apt-get install' - the SDK base image is already pinned.
- DL3008
+11
View File
@@ -0,0 +1,11 @@
---
# Line length (disabled: long lines in tables, URLs and prose are acceptable)
MD013: false
# Allow inline HTML (e.g. <img> logos and badges in READMEs)
MD033: false
# Allow blank lines inside blockquotes
MD028: false
# First line in a file should be a top-level heading
MD041: false
# Table column style (disabled; the previous "padded" value was invalid)
MD060: false
+2
View File
@@ -0,0 +1,2 @@
rules:
line-length: disable
+157
View File
@@ -0,0 +1,157 @@
# Changelog
All notable changes to this project are documented here. Each version
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
normally on AXIS OS 12.10 and later.
- Vendor updated to `moshe@mohome.net` with the registered vendor ID.
- `root` and `acap3` variants remain unsigned (manifest schema v1.x).
- Upgrading from an earlier unsigned version can fail with "Couldn't
install: app" (device log: "Vendor ID in manifest does not match the
vendor ID of the previous version"). Back up your config, uninstall the
old version, then install this one.
## [1.98.8-2] - 2026-07-03 - Tailscale VPN 1.98.8-2
## [1.98.8-subnet-routing] - 2026-07-01 - Tailscale VPN 1.98.8 - Subnet Routing
## [1.98.8] - 2026-06-30 - Tailscale VPN 1.98.8
## [1.98.4-statusfix] - 2026-06-16
## [1.98.4-dns-routes] - 2026-06-10 - Tailscale VPN v1.98.4 - Accept DNS & Routes toggles
## [1.98.4] - 2026-06-02 - Tailscale VPN 1.98.4
## [1.98.3] - 2026-05-22 - Tailscale VPN 1.98.3
## [1.98.2] - 2026-05-19 - Tailscale VPN 1.98.2
## [1.96.4-dns-routes] - 2026-05-12 - Tailscale VPN v1.96.4 - Accept DNS & Routes toggles
## [1.96.4-r3] - 2026-04-17 - Tailscale VPN v1.96.4-r3
## [1.96.4-r2] - 2026-04-17
## [1.96.4-proxy] - 2026-04-14 - Tailscale VPN 1.96.4 - Proxy Support
## [1.96.4] - 2026-03-28 - Tailscale VPN 1.96.4
## [1.96.2] - 2026-03-19 - Tailscale VPN 1.96.2
## [1.94.2] - 2026-02-26 - Tailscale VPN 1.94.2
## [1.94.1] - 2026-01-28 - Tailscale VPN 1.94.1
## [1.92.5] - 2026-01-07 - Tailscale VPN 1.92.5
## [1.92.3] - 2025-12-17 - Tailscale VPN 1.92.3
## [1.92.1] - 2025-12-15 - Tailscale VPN 1.92.1
## [1.90.9] - 2025-11-26 - Tailscale VPN 1.90.9
## [1.90.8] - 2025-11-20 - Tailscale VPN 1.90.8
## [1.90.6] - 2025-11-03 - Tailscale VPN 1.90.6
## [1.90.3] - 2025-10-28 - Tailscale VPN 1.90.3
## [1.90.2] - 2025-10-27 - Tailscale VPN 1.90.2
## [1.90.1] - 2025-10-23 - Tailscale VPN 1.90.1
## [1.88.3] - 2025-09-29 - Tailscale VPN 1.88.3
## [1.88.1] - 2025-09-15 - Tailscale VPN 1.88.1
## [1.86.2] - 2025-08-27 - Tailscale VPN 1.86.2
## [1.84.0] - 2025-05-26
## [1.82.0] - 2025-04-11
## [1.80.3] - 2025-03-24
## [1.78.1] - 2025-01-13
## [1.76.1] - 2024-10-24
## [1.72.1] - 2024-08-26
## [1.68.1] - 2024-06-27
## [1.62.0] - 2024-03-23
## [1.60.0] - 2024-02-21
## [1.56.1] - 2024-01-17
## [1.54.0] - 2023-11-28
## [1.52.0] - 2023-11-01
## [1.50.1] - 2023-10-16
## [148.2] - 2023-09-13 - Version 1.48.2
## [1.44.0] - 2023-07-04
## [138.4] - 2023-04-17 - V1.38.4
## [1.34.0] - 2022-12-19
[1.98.8-2]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.98.8-2
[1.98.8-subnet-routing]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.98.8-subnet-routing
[1.98.8]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.98.8
[1.98.4-statusfix]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.98.4-statusfix
[1.98.4-dns-routes]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.98.4-dns-routes
[1.98.4]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.98.4
[1.98.3]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.98.3
[1.98.2]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.98.2
[1.96.4-dns-routes]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.96.4-dns-routes
[1.96.4-r3]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.96.4-r3
[1.96.4-r2]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.96.4-r2
[1.96.4-proxy]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.96.4-proxy
[1.96.4]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.96.4
[1.96.2]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.96.2
[1.94.2]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.94.2
[1.94.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.94.1
[1.92.5]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.92.5
[1.92.3]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.92.3
[1.92.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.92.1
[1.90.9]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.90.9
[1.90.8]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.90.8
[1.90.6]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.90.6
[1.90.3]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.90.3
[1.90.2]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.90.2
[1.90.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.90.1
[1.88.3]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.88.3
[1.88.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.88.1
[1.86.2]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.86.2
[1.84.0]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.84.0
[1.82.0]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.82.0
[1.80.3]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.80.3
[1.78.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.78.1
[1.76.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.76.1
[1.72.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.72.1
[1.68.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.68.1
[1.62.0]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.62.0
[1.60.0]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.60.0
[1.56.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.56.1
[1.54.0]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.54.0
[1.52.0]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.52.0
[1.50.1]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.50.1
[148.2]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.148.2
[1.44.0]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.1.44.0
[138.4]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v.138.4
[1.34.0]: https://github.com/Mo3he/Axis_Cam_Tailscale/releases/tag/v1.34.0
-3
View File
@@ -1,3 +0,0 @@
@Mo3he
@pandosme
@johanssonfrans
+6 -5
View File
@@ -6,6 +6,7 @@ All types of contributions are encouraged and valued. See the [Table of contents
> - Tweet about it > - Tweet about it
> - Refer this project in your project's readme > - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues > - Mention the project at local meetups and tell your friends/colleagues
> - A sponsorship of any amount is always appreciated
<!-- omit in toc --> <!-- omit in toc -->
## Table of contents ## Table of contents
@@ -125,9 +126,9 @@ Before opening a Pull Request (PR), please consider the following guidelines:
And finally when you are satisfied with your changes, open a new PR. And finally when you are satisfied with your changes, open a new PR.
<!-- markdownlint-disable MD034 --> <!-- markdownlint-disable MD034 -->
[issues]: https://github.com/AxisCommunications/tailscale-acap/issues [issues]: https://github.com/Mo3he/Axis_Cam_Tailscale/issues
[issues_new]: https://github.com/AxisCommunications/tailscale-acap/issues/new [issues_new]: https://github.com/Mo3he/Axis_Cam_Tailscale/issues/new
[issues_bugs]: https://github.com/AxisCommunications/tailscale-acap/issues?q=label%3Abug [issues_bugs]: https://github.com/Mo3he/Axis_Cam_Tailscale/issues?q=label%3Abug
[discussions]: https://github.com/AxisCommunications/tailscale-acap/discussions [discussions]: https://github.com/Mo3he/Axis_Cam_Tailscale/discussions
[discussions_new]: https://github.com/AxisCommunications/tailscale-acap/discussions/new [discussions_new]: https://github.com/Mo3he/Axis_Cam_Tailscale/discussions/new
<!-- markdownlint-enable MD034 --> <!-- markdownlint-enable MD034 -->
+1 -2
View File
@@ -1,7 +1,6 @@
BSD 3-Clause License BSD 3-Clause License
Copyright (c) 2020 Tailscale & AUTHORS. Copyright (c) 2022, Weston Blieden
All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:
+223 -49
View File
@@ -1,81 +1,255 @@
# The Tailscale installer ACAP # Tailscale ACAP for Axis Cameras
This ACAP packages the scripts and files required to install the Tailscale VPN client on Axis Cameras. [![Release](https://img.shields.io/github/v/release/Mo3he/Axis_Cam_Tailscale?style=flat)](https://github.com/Mo3he/Axis_Cam_Tailscale/releases)
[![License](https://img.shields.io/github/license/Mo3he/Axis_Cam_Tailscale?style=flat)](LICENSE)
[![Downloads](https://img.shields.io/github/downloads/Mo3he/Axis_Cam_Tailscale/total?label=Downloads&color=blue&style=flat)](https://github.com/Mo3he/Axis_Cam_Tailscale/releases)
[![Build](https://github.com/Mo3he/Axis_Cam_Tailscale/actions/workflows/build.yml/badge.svg)](https://github.com/Mo3he/Axis_Cam_Tailscale/actions/workflows/build.yml)
[![Super-Linter](https://github.com/Mo3he/Axis_Cam_Tailscale/actions/workflows/super-linter.yml/badge.svg)](https://github.com/Mo3he/Axis_Cam_Tailscale/actions/workflows/super-linter.yml)
[![Sponsor](https://img.shields.io/badge/Sponsor%20My%20Work-EA4AAA?style=flat&logo=github&logoColor=white)](https://github.com/sponsors/Mo3he)
[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/mo3he)
Current version 1.34.0 This repository provides an **ACAP package** that installs the
[Tailscale VPN client](https://tailscale.com/) on Axis cameras, for secure remote
access without extra hardware or complex network configuration.
## Purpose **[Visit the Homepage](https://mo3he.github.io/Axis_Cam_Tailscale/)**
Adding a VPN client directly to the camera allows secure remote access to the device without requiring any other equipment or network configuration. > **Disclaimer:** Independent, community-developed ACAP package. Not an official
Tailscale achieves this in a secure, simple to setup and easy to use way. > Axis product and not affiliated with, endorsed by, or supported by Axis
Tailscale is based on WireGuard VPN tunneling technology. > Communications AB or Tailscale Inc. Use at your own risk.
https://tailscale.com/blog/how-tailscale-works/ > **Tailscale Notice:** Tailscale is a product of Tailscale Inc. This package
> independently redistributes the Tailscale binaries under the
> [BSD 3-Clause License](LICENSE) and is not affiliated with, endorsed by, or
> supported by Tailscale Inc. For the official Tailscale client, visit
> [tailscale.com](https://tailscale.com).
## Links ## Table of Contents
https://tailscale.com/ - [Overview](#overview)
- [Compatibility](#compatibility)
- [Installation](#installation)
- [Configuration](#configuration)
- [Ports & security](#ports--security)
- [Accessing Tailnet services from the camera](#accessing-tailnet-services-from-the-camera)
- [Updating Tailscale](#updating-tailscale)
- [Build from source](#build-from-source)
- [Roadmap](#roadmap)
- [Links](#links)
- [License](#license)
https://github.com/tailscale/tailscale ## Overview
https://www.wireguard.com/ Adding a VPN client directly to the camera enables secure remote access without
additional hardware or complex network configuration, through Tailscale's
lightweight WireGuard-based tunnel.
https://www.axis.com/ - Secure remote access to cameras.
- Easy to install via EAP package.
- Works on **AXIS OS 10.12+** (non-root version, verified across 10.12–13).
- Works on **legacy AXIS OS 9.x / 10.x** via the ACAP 3 variant.
- Based on **WireGuard VPN** technology.
Tailscale ACAP runs **without root privileges** in userspace networking mode,
making it compatible with AXIS OS 10.12+. For **full kernel networking**, use the
**ROOT** version (AXIS OS 10.12–11.x only; AXIS OS 12 and later removed root
access for third-party applications). Learn more:
[How Tailscale Works](https://tailscale.com/blog/how-tailscale-works/).
## Compatibility ## Compatibility
The Tailscale ACAP is compatable with Axis cameras with arm and aarch64 based Soc's. | Build | AXIS OS | Architecture | Notes |
|---|---|---|---|
| ACAP 4 (native SDK) | 10.12 – 13 | aarch64 | Standard, userspace networking |
| ACAP 4 (native SDK) | 10.12 – 13 | armv7hf | Standard, userspace networking |
| ACAP 4 root | 10.12 – 11.x | aarch64 | Full kernel networking (not on OS 12+) |
| ACAP 4 root | 10.12 – 11.x | armv7hf | Full kernel networking (not on OS 12+) |
| ACAP 3 (legacy SDK) | 9.x – 10.x | armv7hf | Legacy cameras |
``` > Most cameras use the standard **ACAP 4** build. The **root** builds add
curl --anyauth "*" -u <username>:<password> <device ip>/axis-cgi/basicdeviceinfo.cgi --data "{\"apiVersion\":\"1.0\",\"context\":\"Client defined request ID\",\"method\":\"getAllProperties\"}" > kernel-level networking but only run on AXIS OS 10.12–11.x (AXIS OS 12+ removed
> root access for ACAPs). Use the **ACAP 3** build only on legacy cameras that
> don't support ACAP 4 (AXIS OS 9–10).
**Verified on AXIS OS 13** (13.0.0, aarch64).
## Installation
> **Signed packages:** Release `.eap` files are signed with the Axis ACAP
> signing service and install normally on AXIS OS 12.10 and later.
>
> **Upgrading from an earlier version?** The signing vendor changed, so
> installing over a previously installed unsigned build can fail with
> **"Couldn't install: app"** (device log: *"Vendor ID in manifest does not
> match the vendor ID of the previous version"*). To upgrade: back up your app
> configuration, **uninstall** the old version, then install the signed one.
Get the **prebuilt `.eap` file** from the
[Releases page](https://github.com/Mo3he/Axis_Cam_Tailscale/releases).
1. Log into your Axis camera.
2. Go to **Apps -> Add App**.
3. Upload the `.eap` file.
Once installed:
- Start the app.
- Click **Open** to view logs and get your Tailscale authentication URL.
- On uninstall, all changes/files are removed.
> You'll need a [Tailscale account](https://tailscale.com/) to authenticate.
## Configuration
The app runs a C-based parameter bridge that reads settings from the ACAP
parameter store and launches Tailscale. View logs and connection status via the
**Open** button in the app, and authenticate using the provided URL or by
pre-entering an auth key in **Settings**. Parameter changes (ports, server URL,
auth key) are applied automatically without reinstalling the app.
All parameters are configurable via the web UI (**Open -> Settings** card) and
take effect immediately:
| Parameter | Default | Description |
|---|---|---|
| Custom Server URL | *(empty)* | Control server URL for [Headscale](https://headscale.net/) or other self-hosted servers. Leave blank to use Tailscale's official servers. |
| Auth Key | *(empty)* | Pre-authentication key (`tskey-auth-...`). Cleared automatically after first successful connection. Leave blank to authenticate via browser. |
| HTTP Proxy Port | `8080` | Port for the outbound HTTP/HTTPS proxy. |
| SOCKS5 Proxy Port | `1080` | Port for the outbound SOCKS5 proxy. |
| Accept DNS | `off` | Passes `--accept-dns=true` to `tailscale up`. Allows the tailnet to push DNS settings to the camera. Not available on `armv7hf_acap3`. |
| Accept Routes | `off` | Passes `--accept-routes=true` to `tailscale up`. Allows the camera to use subnet routes advertised by other nodes. Not available on `armv7hf_acap3`. |
| Advertise Routes (Subnet Router) | *(empty)* | Comma-separated CIDRs (e.g. `192.168.1.0/24,10.0.0.0/8`) this camera will route for the tailnet, turning it into a subnet router. Approve the routes in the Tailscale admin console after saving. Leave blank to disable. |
## Ports & security
All non-ROOT variants expose two local proxy endpoints that route outbound
traffic through the Tailscale tunnel. The ports are configurable via **Settings
-> HTTP Proxy Port / SOCKS5 Proxy Port**.
| Proxy | Default address | Routes |
|---|---|---|
| HTTP CONNECT | `http://127.0.0.1:8080` | HTTP and HTTPS traffic |
| SOCKS5 | `127.0.0.1:1080` | Any SOCKS5-aware app or service |
Set the HTTP CONNECT proxy wherever an HTTP/HTTPS proxy field is available on the
camera (System -> Network -> Global proxies; System -> MQTT -> Broker). For
SOCKS5-aware apps, set their proxy to `127.0.0.1:<port>`.
> **Security:** the proxies bind to **loopback only** (`127.0.0.1`), so they are
> not exposed on the camera's network interface, the least-exposed of the VPN
> ACAPs. The active proxy addresses are shown in the **Proxy Configuration** card
> of the web UI. If you change a port that is already in use, the app logs an
> error and exits rather than silently falling back.
## Accessing Tailnet services from the camera
There is an important asymmetry. Making the camera **reachable from** the tailnet
(browsing to it, VAPIX, SSH from another tailnet node) works on every build. The
harder direction is the camera **reaching out to** a tailnet peer, for example
mounting an SMB/CIFS share hosted on another node. How well this works depends on
the build:
| Build | Networking mode | Camera-initiated access to tailnet peers |
|---|---|---|
| Non-root (`aarch64`, `armv7hf`) and `armv7hf_acap3` | `--tun=userspace-networking` (no kernel `tailscale0`) | Only through the local **SOCKS5 / HTTP proxies**, and only for **proxy-aware** apps. Firmware system services (SMB client, NTP, etc.) are proxy-unaware and **cannot** reach a peer's `100.x` IP directly. |
| **ROOT** (`aarch64_root`, `armv7hf_root`) | Kernel networking with a real `tailscale0` interface | Peer `100.x` IPs are routable at the OS level, so firmware services **can** connect directly. Enable **Accept Routes** to also reach subnets behind other nodes. |
### Plan B: reverse-SSH tunnel
> **Requires root on the camera.** Port 445 is privileged, so binding it needs a
> root-capable build (e.g. developer certificates installed).
If you cannot use the ROOT build but still need the camera to mount a share on a
machine that is on your tailnet, make the remote share appear **local** to the
camera with a reverse SSH tunnel. Because the destination becomes `127.0.0.1`, the
proxy-unaware SMB client never has to route over the tailnet.
```bash
# Forward the camera's local port 445 back to the SMB share on this machine
ssh -R 445:localhost:445 root@<camera-tailscale-ip>
``` ```
where `<device ip>` is the IP address of the Axis device, `<username>` is the root username and `<password>` is the root password. Please Then, in **System -> Storage -> Add network share**, use `127.0.0.1` as the share
note that you need to enclose your password with quotes (`'`) if it contains special characters. host and connect.
## Installing ## Updating Tailscale
The recommended way to install this ACAP is to use the pre built eap file. - New `.eap` files are auto-built and released **weekly** (if a new Tailscale
Go to "Apps" on the camera and click "Add app". version is available).
Once installed it will look like below. - To update, simply install the new `.eap` over the existing one.
![alt text](https://github.com/Mo3he/Axis_Cam_Tailscale/blob/main/images/ACAP.png) ### Manual update (advanced)
Replace the binaries in `common/app/lib/` (shared by `aarch64`, `armv7hf`, and
their ROOT variants) or `arm_acap3/app/lib/` (legacy variant, kept separate):
## Using the Tailscale ACAP - `tailscale`
- `tailscaled`
The Tailscale ACAP will run a script on startup that sets the required permissions and starts the service and app. Download the latest versions:
Once started click "Open" to see the output of the logs for further instructions and obtain the authetication URL. [Tailscale static builds](https://pkgs.tailscale.com/stable/#static).
When uninstalling the ACAP, all changes and files are removed from the camera. ## Build from source
You will need a tailscale.com account to use the ACAP The Tailscale binaries are not stored in git, so first download them (see
[Manual update](#manual-update-advanced)) and place them in `common/app/lib/`, or
`arm_acap3/app/lib/` for the legacy variant.
## Updating Tailscale version All variants build from the **repository root**, pointing at the variant's own
`Dockerfile`:
The eap files will be updated from time to time and simply installing the new version over the old will update all files. ```bash
docker build -f aarch64/Dockerfile --tag <package_name> .
It's also possible to build and use a locally built image as all necesary files are provided. docker cp $(docker create <package_name>):/opt/app ./build
Replace binaries "tailscale" and "tailscaled" in lib folder with new versions.
Make sure you use the files for the correct Soc.
Latest versions can be found at
https://pkgs.tailscale.com/stable/#static
To build,
From main directory of the version you want (arm/aarch64)
```
docker build --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`.)
## Roadmap
### AXIS OS 13 Preparation
AXIS OS 13 (scheduled for September 2026) introduces several breaking changes that
affect all ACAP applications. See the full
[AXIS OS 13 breaking changes](https://www.axis.com/for-developers/news/AXIS-OS-13-breaking-changes)
announcement for details.
- [x] **Recompile for 64-bit time (Y2038)** - 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.
- [x] **Migrate to Manifest Schema v2** - Done for `aarch64`/`armv7hf` (schema
2.0.0, `compatibleOsVersions` declared); verified installability on OS
10.12–13.
- [x] **Audit for executable stack usage** - All compiled binaries report
`flags rw-` (no executable stack) on every architecture and variant.
- [x] **Verify web UI works over HTTPS** - Verified live; the UI only issues
relative-path requests, so it inherits the page's protocol with no
mixed-content risk.
- [x] **Sign the ACAP via the Axis ACAP Portal** - Done; `aarch64`/`armv7hf`
packages are signed with the Axis ACAP signing service. The `root` and
`acap3` variants use manifest schema v1.x and are distributed unsigned.
### General Improvements
- [x] **Accept DNS from tailnet toggle** - Opt-in setting passing
`--accept-dns=true` to `tailscale up` (defaults off).
- [x] **Accept routes toggle** - Opt-in setting passing `--accept-routes=true`.
- [ ] **Switch to tiny-tailscale binaries** - Evaluate replacing the bundled
`tailscale`/`tailscaled` with [tiny-tailscale](https://github.com/iamromulan/tiny-tailscale)
builds (single binary, ~43% smaller).
## Links
- [Tailscale](https://tailscale.com/)
- [Tailscale GitHub](https://github.com/tailscale/tailscale)
- [WireGuard](https://www.wireguard.com/)
- [Axis Communications](https://www.axis.com/)
## License
The packaging code in this repository is licensed under BSD 3-Clause (see
[LICENSE](LICENSE)); this also covers the redistributed Tailscale binaries
(upstream Tailscale is BSD 3-Clause). Bundled upstream components are listed in
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
+23
View File
@@ -0,0 +1,23 @@
# Security Policy
This is an independent, community-developed ACAP package, provided on a
best-effort basis. It is not an official Axis Communications product.
## Reporting a vulnerability
Please report security issues privately rather than in a public issue:
- Use GitHub's "Report a vulnerability" (Security > Advisories) to open a
private advisory, or
- email <moshe@mohome.net>.
Include the affected version (or `.eap` filename), camera model / Axis OS
version, a description and its impact, and reproduction steps if available. You
can expect an acknowledgement within a reasonable time; please avoid public
disclosure until a fix is released.
## Scope
Reports about this ACAP's own wrapper code, configuration handling, and default
settings are in scope. Vulnerabilities in bundled upstream projects should also
be reported to their respective upstream projects.
@@ -1,8 +1,20 @@
BSD 3-Clause License # Third-Party Notices
Copyright (c) 2020 Tailscale & AUTHORS. This ACAP package redistributes the Tailscale client. The ACAP's own wrapper
All rights reserved. code is licensed separately (see `LICENSE`, BSD 3-Clause).
## Tailscale
- Copyright (c) 2020 Tailscale & AUTHORS
- Project: <https://github.com/tailscale/tailscale>
- License: BSD 3-Clause
Tailscale is a product of Tailscale Inc. This package independently
redistributes the Tailscale binaries and is not affiliated with, endorsed by, or
supported by Tailscale Inc. For the official Tailscale client, visit
<https://tailscale.com>.
```text
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:
@@ -13,9 +25,9 @@ modification, are permitted provided that the following conditions are met:
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its 3. Neither the name of the copyright holder nor the names of its contributors
contributors may be used to endorse or promote products derived from may be used to endorse or promote products derived from this software
this software without specific prior written permission. without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
@@ -27,3 +39,4 @@ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 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 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. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
+7 -5
View File
@@ -1,12 +1,14 @@
ARG ARCH=aarch64 ARG ARCH=aarch64
ARG VERSION=1.3 ARG VERSION=12.10.0
ARG UBUNTU_VERSION=22.04 ARG UBUNTU_VERSION=24.04
ARG REPO=axisecp ARG REPO=axisecp
ARG SDK=acap-native-sdk ARG SDK=acap-native-sdk
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION} FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
# Building the ACAP application COPY common/app /opt/app/
COPY ./app /opt/app/ COPY aarch64/app/manifest.json /opt/app/manifest.json
WORKDIR /opt/app WORKDIR /opt/app
RUN . /opt/axis/acapsdk/environment-setup* && acap-build ./ 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 ./
-1
View File
@@ -1 +0,0 @@
nop:
-10
View File
@@ -1,10 +0,0 @@
#!/bin/sh
echo "Starting Service"
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
/usr/local/packages/Tailscale_VPN/lib/tailscaled &
echo "Service Started"
echo "Scroll to Bottom for link"
/usr/local/packages/Tailscale_VPN/lib/tailscale up
wait
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<head>
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
<script>
function refreshIFrame() {
var x = document.getElementById("*Your_iframe_id*");
x.contentWindow.location.reload();
var t = setTimeout(refreshIFrame, 5000);
}
</script>
</head>
<body onload="refreshIFrame()">
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
</body>
</body>
</html>
+57 -13
View File
@@ -1,23 +1,67 @@
{ {
"schemaVersion": "1.3", "schemaVersion": "2.0.0",
"acapPackageConf": { "acapPackageConf": {
"setup": { "setup": {
"architecture": "aarch64",
"appName": "Tailscale_VPN", "appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Axis Labs", "vendor": "moshe@mohome.net",
"embeddedSdkVersion": "3.0", "vendorId": "70ee172dd9",
"user": { "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"group": "root", "version": "1.102.4",
"username": "root" "architecture": "aarch64",
}, "runMode": "respawn",
"vendorUrl": "https://www.tailscale.com", "compatibleOsVersions": [
"runMode": "once", {
"version": "1.38.4" "max": "13"
}
]
}, },
"configuration": { "configuration": {
"settingPage": "index.html" "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"
}
]
} }
} }
}
-1
View File
@@ -1 +0,0 @@
nop:
-10
View File
@@ -1,10 +0,0 @@
#!/bin/sh
echo "Starting Service"
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
/usr/local/packages/Tailscale_VPN/lib/tailscaled &
echo "Service Started"
echo "Scroll to Bottom for link"
/usr/local/packages/Tailscale_VPN/lib/tailscale up
wait
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<head>
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
<script>
function refreshIFrame() {
var x = document.getElementById("*Your_iframe_id*");
x.contentWindow.location.reload();
var t = setTimeout(refreshIFrame, 5000);
}
</script>
</head>
<body onload="refreshIFrame()">
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
</body>
</body>
</html>
Binary file not shown.
Binary file not shown.
-23
View File
@@ -1,23 +0,0 @@
{
"schemaVersion": "1.3",
"acapPackageConf": {
"setup": {
"architecture": "aarch64",
"appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Axis Labs",
"embeddedSdkVersion": "3.0",
"user": {
"group": "root",
"username": "root"
},
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"version": "1.38.4"
},
"configuration": {
"settingPage": "index.html"
}
}
}
-22
View File
@@ -1,22 +0,0 @@
PACKAGENAME="Tailscale VPN"
APPTYPE="aarch64"
APPNAME="Tailscale_VPN"
APPID=""
LICENSENAME="Available"
LICENSEPAGE="none"
VENDOR="Tailscale - Packaged by Axis Labs"
REQEMBDEVVERSION="3.0"
APPMAJORVERSION="1"
APPMINORVERSION="38"
APPMICROVERSION="4"
APPGRP="root"
APPUSR="root"
APPOPTS=""
OTHERFILES=""
SETTINGSPAGEFILE="index.html"
SETTINGSPAGETEXT=""
VENDORHOMEPAGELINK='<a href="https://www.tailscale.com" target="_blank">www.tailscale.com</a>'
PREUPGRADESCRIPT=""
POSTINSTALLSCRIPT=""
STARTMODE="once"
HTTPCGIPATHS=""
-13
View File
@@ -1,13 +0,0 @@
APPTYPE="aarch64"
APPNAME="Tailscale_VPN"
PACKAGENAME="Tailscale VPN"
VENDOR="Tailscale - Packaged by Axis Labs"
REQEMBDEVVERSION="3.0"
APPGRP="root"
APPUSR="root"
VENDORHOMEPAGELINK='<a href="https://www.tailscale.com" target="_blank">www.tailscale.com</a>'
STARTMODE="once"
APPMAJORVERSION="1"
APPMINORVERSION="38"
APPMICROVERSION="4"
SETTINGSPAGEFILE="index.html"
+13
View File
@@ -0,0 +1,13 @@
ARG ARCH=aarch64
ARG VERSION=1.15.1
ARG UBUNTU_VERSION=22.04
ARG REPO=axisecp
ARG SDK=acap-native-sdk
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
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 ./
+5
View File
@@ -0,0 +1,5 @@
To build, from main directory
docker build --tag aarch64 .
docker cp $(docker create aarch64):/opt/app ./build
+56
View File
@@ -0,0 +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.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"
}
]
}
}
}
+7 -5
View File
@@ -1,12 +1,14 @@
ARG ARCH=armv7hf ARG ARCH=armv7hf
ARG VERSION=1.3 ARG VERSION=12.10.0
ARG UBUNTU_VERSION=22.04 ARG UBUNTU_VERSION=24.04
ARG REPO=axisecp ARG REPO=axisecp
ARG SDK=acap-native-sdk ARG SDK=acap-native-sdk
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION} FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
# Building the ACAP application COPY common/app /opt/app/
COPY ./app /opt/app/ COPY arm/app/manifest.json /opt/app/manifest.json
WORKDIR /opt/app WORKDIR /opt/app
RUN . /opt/axis/acapsdk/environment-setup* && acap-build ./ 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 ./
-29
View File
@@ -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
View File
@@ -1 +0,0 @@
nop:
-10
View File
@@ -1,10 +0,0 @@
#!/bin/sh
echo "Starting Service"
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
/usr/local/packages/Tailscale_VPN/lib/tailscaled &
echo "Service Started"
echo "Scroll to Bottom for link"
/usr/local/packages/Tailscale_VPN/lib/tailscale up
wait
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<head>
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
<script>
function refreshIFrame() {
var x = document.getElementById("*Your_iframe_id*");
x.contentWindow.location.reload();
var t = setTimeout(refreshIFrame, 5000);
}
</script>
</head>
<body onload="refreshIFrame()">
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
</body>
</body>
</html>
+57 -13
View File
@@ -1,23 +1,67 @@
{ {
"schemaVersion": "1.3", "schemaVersion": "2.0.0",
"acapPackageConf": { "acapPackageConf": {
"setup": { "setup": {
"architecture": "armv7hf",
"appName": "Tailscale_VPN", "appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Axis Axis Labs", "vendor": "moshe@mohome.net",
"embeddedSdkVersion": "3.0", "vendorId": "70ee172dd9",
"user": { "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"group": "root", "version": "1.102.4",
"username": "root" "architecture": "armv7hf",
}, "runMode": "respawn",
"vendorUrl": "https://www.tailscale.com", "compatibleOsVersions": [
"runMode": "once", {
"version": "1.38.4" "max": "13"
}
]
}, },
"configuration": { "configuration": {
"settingPage": "index.html" "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"
}
]
} }
} }
}
-29
View File
@@ -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
View File
@@ -1 +0,0 @@
nop:
-10
View File
@@ -1,10 +0,0 @@
#!/bin/sh
echo "Starting Service"
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
/usr/local/packages/Tailscale_VPN/lib/tailscaled &
echo "Service Started"
echo "Scroll to Bottom for link"
/usr/local/packages/Tailscale_VPN/lib/tailscale up
wait
@@ -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.
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<head>
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
<script>
function refreshIFrame() {
var x = document.getElementById("*Your_iframe_id*");
x.contentWindow.location.reload();
var t = setTimeout(refreshIFrame, 5000);
}
</script>
</head>
<body onload="refreshIFrame()">
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
</body>
</body>
</html>
Binary file not shown.
Binary file not shown.
-23
View File
@@ -1,23 +0,0 @@
{
"schemaVersion": "1.3",
"acapPackageConf": {
"setup": {
"architecture": "armv7hf",
"appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Axis Axis Labs",
"embeddedSdkVersion": "3.0",
"user": {
"group": "root",
"username": "root"
},
"vendorUrl": "https://www.tailscale.com",
"runMode": "once",
"version": "1.38.4"
},
"configuration": {
"settingPage": "index.html"
}
}
}
-22
View File
@@ -1,22 +0,0 @@
PACKAGENAME="Tailscale VPN"
APPTYPE="armv7hf"
APPNAME="Tailscale_VPN"
APPID=""
LICENSENAME="Available"
LICENSEPAGE="none"
VENDOR="Tailscale - Packaged by Axis Axis Labs"
REQEMBDEVVERSION="3.0"
APPMAJORVERSION="1"
APPMINORVERSION="38"
APPMICROVERSION="4"
APPGRP="root"
APPUSR="root"
APPOPTS=""
OTHERFILES=""
SETTINGSPAGEFILE="index.html"
SETTINGSPAGETEXT=""
VENDORHOMEPAGELINK='<a href="https://www.tailscale.com" target="_blank">www.tailscale.com</a>'
PREUPGRADESCRIPT=""
POSTINSTALLSCRIPT=""
STARTMODE="once"
HTTPCGIPATHS=""
-13
View File
@@ -1,13 +0,0 @@
APPTYPE="armv7hf"
APPNAME="Tailscale_VPN"
PACKAGENAME="Tailscale VPN"
VENDOR="Tailscale - Packaged by Axis Axis Labs"
REQEMBDEVVERSION="3.0"
APPGRP="root"
APPUSR="root"
VENDORHOMEPAGELINK='<a href="https://www.tailscale.com" target="_blank">www.tailscale.com</a>'
STARTMODE="once"
APPMAJORVERSION="1"
APPMINORVERSION="38"
APPMICROVERSION="4"
SETTINGSPAGEFILE="index.html"
+13
View File
@@ -0,0 +1,13 @@
ARG ARCH=armv7hf
ARG VERSION=1.15.1
ARG UBUNTU_VERSION=22.04
ARG REPO=axisecp
ARG SDK=acap-native-sdk
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
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 ./
+5
View File
@@ -0,0 +1,5 @@
To build from main directory
docker build --tag arm .
docker cp $(docker create arm):/opt/app ./build
+56
View File
@@ -0,0 +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.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"
}
]
}
}
}
+40
View File
@@ -0,0 +1,40 @@
ARG UBUNTU_VERSION=20.04
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 arm_acap3/app /opt/app/
WORKDIR /opt/app
# The ELF launcher takes over the Tailscale_VPN name.
RUN mv Tailscale_VPN start.sh && chmod +x start.sh
# 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
# 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
# ACAP 3 firmware expects the settings page at the app root, not in html/
RUN cp html/index.html index.html
# Runtime files live in localdata/; symlink them so the web UI can fetch them.
RUN ln -sf ../localdata/tailscaled.log html/tailscaled.log
RUN ln -sf ../localdata/status.json html/status.json
RUN . /opt/axis/acapsdk/environment-setup* && create-package.sh ./
# 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" && \
(cd "$tmpdir" && tar czf "/opt/app/$eap" .) && \
rm -rf "$tmpdir"; \
done
+1
View File
@@ -0,0 +1 @@
nop:
+91
View File
@@ -0,0 +1,91 @@
#!/bin/sh
APP_DIR="/usr/local/packages/Tailscale_VPN"
STATE_DIR="$APP_DIR/localdata"
logger -t "Tailscale_VPN" "Starting Tailscale VPN service (userspace networking)"
mkdir -p "$STATE_DIR"
chmod 755 "$APP_DIR/lib/tailscale"
chmod 755 "$APP_DIR/lib/tailscaled"
# Kill any leftover daemon from a previous run
killall tailscaled 2>/dev/null || true
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
# Log to file (not piped through logger) -- avoids extra logger process holding
# tailscaled stdout open, which prevents our wait loop from detecting exit
"$APP_DIR/lib/tailscaled" \
--state="$STATE_DIR/tailscaled.state" \
--socket="$STATE_DIR/tailscaled.sock" \
--socks5-server=localhost:1055 \
--outbound-http-proxy-listen=localhost:8080 \
--tun=userspace-networking \
>>"$STATE_DIR/tailscaled.log" 2>&1 &
TAILSCALED_PID=$!
# Wait for socket to appear (up to 15 seconds)
i=0
while [ $i -lt 15 ] && [ ! -S "$STATE_DIR/tailscaled.sock" ]; do
sleep 1
i=$((i + 1))
done
logger -t "Tailscale_VPN" "Connecting to Tailscale network"
# --timeout=10s: tailscale up exits promptly after connecting (or giving up),
# preventing two large Go binaries running simultaneously and causing OOM on
# cameras with limited RAM (e.g. 222 MB).
# Capture output so we can extract auth URL and log it to syslog for the web UI.
UP_OUT=$("$APP_DIR/lib/tailscale" \
--socket="$STATE_DIR/tailscaled.sock" \
up --hostname="$(hostname)" --timeout=10s 2>&1) || true
echo "$UP_OUT" >>"$STATE_DIR/tailscaled.log"
# If an auth URL was returned, log it so the web UI can show it
AUTH_URL=$(echo "$UP_OUT" | grep -o 'https://login\.tailscale\.com/[^ ]*' | head -1)
[ -n "$AUTH_URL" ] && logger -t "Tailscale_VPN" "Auth required: $AUTH_URL"
# Log IP and version into syslog so the web UI details panel can populate
TS_IP=$("$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" ip -4 2>/dev/null | head -1)
TS_VER=$("$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" version 2>/dev/null | head -1)
[ -n "$TS_IP" ] && logger -t "Tailscale_VPN" "Tailscale IP: $TS_IP"
[ -n "$TS_VER" ] && logger -t "Tailscale_VPN" "Tailscale version: $TS_VER"
logger -t "Tailscale_VPN" "Tailscale VPN is running"
logger -t "Tailscale_VPN" "HTTP/HTTPS proxy: http://127.0.0.1:8080"
logger -t "Tailscale_VPN" "SOCKS5 proxy: 127.0.0.1:1055"
# 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 logs, which otherwise reports
# "connected" whenever the launcher keeps the process alive (e.g. no Internet).
# Written to localdata and exposed at html/status.json via a build-time symlink.
STATUS_FILE="$STATE_DIR/status.json"
publish_status() {
if "$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" 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
}
# Remove stale status on stop so the UI does not show a connected node after exit.
cleanup() {
rm -f "$STATUS_FILE" 2>/dev/null
[ -n "$TAILSCALED_PID" ] && kill "$TAILSCALED_PID" 2>/dev/null
exit 0
}
trap cleanup TERM INT
# Monitoring loop: stay alive while tailscaled is running and keep the published
# status fresh. This keeps the parent Tailscale_VPN (C launcher) in the process
# table so pidof finds it and the camera web UI shows "Running" instead of "Stopped".
while kill -0 "$TAILSCALED_PID" 2>/dev/null; do
publish_status
sleep 5
done
rm -f "$STATUS_FILE" 2>/dev/null
logger -t "Tailscale_VPN" "tailscaled exited"
+739
View File
@@ -0,0 +1,739 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tailscale VPN</title>
<style>
:root {
--bg: #0f1117;
--surface: #181b23;
--surface2: #1e2230;
--border: #262a35;
--text: #e4e6ed;
--muted: #8b8fa3;
--accent: #2e2d2d;
--green: #22c55e;
--yellow: #f59e0b;
--red: #ef4444;
--radius: 10px;
--mono: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
}
[data-theme="light"] {
--bg: #f5f6f8;
--surface: #ffffff;
--surface2: #f0f1f4;
--border: #e0e3e8;
--text: #1a1a2e;
--muted: #6b7084;
--accent: #2e2d2d;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
padding: 20px;
font-size: 14px;
max-width: 720px;
margin: 0 auto;
line-height: 1.5;
}
/* Header */
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.header-left {
display: flex;
align-items: center;
gap: 10px;
}
.header h1 {
font-size: 18px;
font-weight: 700;
}
.theme-btn {
background: var(--surface);
border: 1px solid var(--border);
color: var(--muted);
cursor: pointer;
border-radius: 8px;
padding: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.theme-btn:hover { color: var(--text); border-color: var(--muted); }
.theme-btn svg { width: 16px; height: 16px; }
/* Cards */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-bottom: 14px;
}
.card-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--muted);
margin-bottom: 14px;
}
/* Status */
.status-banner {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.status-banner.connected { background: rgba(34,197,94,0.1); border: 1px solid rgba(34,197,94,0.2); }
.status-banner.connecting { background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.2); }
.status-banner.disconnected { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); }
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.status-banner.connected .dot { background: var(--green); box-shadow: 0 0 0 3px rgba(34,197,94,0.2); }
.status-banner.connecting .dot { background: var(--yellow); box-shadow: 0 0 0 3px rgba(245,158,11,0.2); animation: pulse 1.5s infinite; }
.status-banner.disconnected .dot { background: var(--red); box-shadow: 0 0 0 3px rgba(239,68,68,0.2); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.status-text {
font-size: 14px;
font-weight: 600;
}
.status-banner.connected .status-text { color: var(--green); }
.status-banner.connecting .status-text { color: var(--yellow); }
.status-banner.disconnected .status-text { color: var(--red); }
.status-time {
margin-left: auto;
font-size: 12px;
color: var(--muted);
font-family: var(--mono);
}
/* Auth block */
.auth-block {
background: rgba(245,158,11,0.08);
border: 1px solid rgba(245,158,11,0.2);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.auth-block p {
font-size: 13px;
color: var(--muted);
margin-bottom: 12px;
}
.auth-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--accent);
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 13px;
padding: 8px 18px;
border-radius: 6px;
margin-bottom: 8px;
}
.auth-btn:hover { opacity: 0.9; }
.auth-url {
display: block;
font-size: 11px;
color: var(--muted);
word-break: break-all;
font-family: var(--mono);
}
/* Info grid */
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.info-item {
background: var(--surface2);
border-radius: 8px;
padding: 12px 14px;
}
.info-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
color: var(--muted);
margin-bottom: 4px;
}
.info-value {
font-size: 14px;
font-weight: 600;
font-family: var(--mono);
word-break: break-all;
}
.info-value.dim { color: var(--muted); font-weight: 400; }
/* Log viewer */
.log-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.log-badge {
font-size: 11px;
color: var(--muted);
font-family: var(--mono);
}
.log-toggle {
font-size: 12px;
color: var(--accent);
background: none;
border: none;
cursor: pointer;
font-weight: 600;
}
.log-toggle:hover { text-decoration: underline; }
.log-box {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
max-height: 400px;
overflow-y: auto;
font-family: var(--mono);
font-size: 11.5px;
line-height: 1.7;
color: var(--muted);
white-space: pre-wrap;
word-break: break-all;
}
.log-box .log-line { display: block; }
.log-box .log-line:hover { background: rgba(46,45,45,0.06); }
.log-line .ts { color: var(--muted); opacity: 0.6; }
.log-line .msg-info { color: var(--accent); }
.log-line .msg-warn { color: var(--yellow); }
.log-line .msg-err { color: var(--red); }
.log-line .msg-ok { color: var(--green); }
/* Refresh indicator */
.refresh-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px;
font-size: 11px;
color: var(--muted);
}
/* Update banner */
.update-banner {
display: none;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 14px;
background: rgba(46,45,45,0.1);
border: 1px solid rgba(46,45,45,0.2);
}
.update-banner.visible { display: flex; }
.update-banner .update-text {
flex: 1;
font-size: 13px;
color: var(--text);
}
.update-banner .update-text strong { color: var(--accent); }
.update-btn {
display: inline-flex;
align-items: center;
gap: 5px;
background: var(--accent);
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 12px;
padding: 6px 14px;
border-radius: 6px;
white-space: nowrap;
}
.update-btn:hover { opacity: 0.9; }
@media (max-width: 480px) {
body { padding: 14px; }
.info-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<h1>Tailscale VPN</h1>
</div>
<button class="theme-btn" id="themeToggle" aria-label="Toggle theme">
<svg id="iconSun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
<svg id="iconMoon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
</div>
<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>
<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">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Download
</a>
</div>
<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">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
Open Login Page
</a>
<span id="auth-url-text" class="auth-url"></span>
</div>
<div class="card" id="info-card" style="display:none;">
<div class="card-title">Connection Details</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Tailscale IP</div>
<div class="info-value" id="ts-ip">-</div>
</div>
<div class="info-item">
<div class="info-label">Node Name</div>
<div class="info-value" id="ts-node">-</div>
</div>
<div class="info-item">
<div class="info-label">Account</div>
<div class="info-value" id="ts-tailnet">-</div>
</div>
<div class="info-item">
<div class="info-label">Version</div>
<div class="info-value" id="ts-version">-</div>
</div>
</div>
<div style="margin-top:14px;text-align:right;">
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
</div>
</div>
<div class="card">
<div class="log-controls">
<div class="card-title" style="margin-bottom:0;">Service Log</div>
<div style="display:flex;gap:10px;align-items:center;">
<span id="log-count" class="log-badge"></span>
<button class="log-toggle" id="log-scroll-btn">Scroll to bottom</button>
</div>
</div>
<div class="log-box" id="log-box">Loading logs...</div>
</div>
<div class="refresh-bar">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
<span>Auto-refresh every 5s</span>
</div>
<script>
(function() {
var APP = 'Tailscale_VPN';
var LOG_URL = '/axis-cgi/admin/systemlog.cgi?appname=' + APP;
var logBox = document.getElementById('log-box');
var autoScroll = true;
// Theme
var toggle = document.getElementById('themeToggle');
var sun = document.getElementById('iconSun');
var moon = document.getElementById('iconMoon');
var root = document.documentElement;
function applyTheme(t) {
if (t === 'light') {
root.setAttribute('data-theme', 'light');
sun.style.display = 'none';
moon.style.display = 'block';
} else {
root.removeAttribute('data-theme');
sun.style.display = 'block';
moon.style.display = 'none';
}
}
var stored = localStorage.getItem('ts-acap-theme');
if (stored) applyTheme(stored);
else if (window.matchMedia('(prefers-color-scheme: light)').matches) applyTheme('light');
toggle.addEventListener('click', function() {
var next = root.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
localStorage.setItem('ts-acap-theme', next);
applyTheme(next);
});
// Log scroll
document.getElementById('log-scroll-btn').addEventListener('click', function() {
logBox.scrollTop = logBox.scrollHeight;
autoScroll = true;
});
logBox.addEventListener('scroll', function() {
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
});
// Cache helpers - survive syslog rotation
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
function parse(txt) {
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
var ipMatch = txt.match(/peerapi: serving on http:\/\/(100\.[\d.]+):/g);
var tsIP = null;
if (ipMatch) {
var last = ipMatch[ipMatch.length - 1];
var m = last.match(/http:\/\/(100\.[\d.]+):/);
if (m) tsIP = m[1];
}
if (!tsIP) {
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
if (nmSelf) tsIP = nmSelf[1];
}
if (!tsIP) {
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
}
// 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; may contain a stale acap-tailscale_vpn name.
if (!node) {
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
if (nodeMatches) {
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
if (nm) node = nm[1];
}
}
var loginMatches = txt.match(/active login:\s+\S+/g);
var tailnet = null;
if (loginMatches) {
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
if (lm) tailnet = lm[1];
}
if (!tailnet) {
// Fallback: extract from periodic netmap lines "u=user@email.com"
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
if (userMatches) {
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
if (um) tailnet = um[1];
}
}
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
var version = null;
if (versionMatches) {
var last = versionMatches[versionMatches.length - 1];
var vm = last.match(/v(\d+\.\d+\.\d+)/);
if (vm) version = vm[1];
}
if (!version) {
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
if (peersMatches) {
var lp = peersMatches[peersMatches.length - 1];
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
if (pm) version = pm[1];
}
}
if (!version) {
// ACAP3: logged by start.sh as "Tailscale version: 1.96.4"
var acap3ver = txt.match(/Tailscale version: (\d+\.\d+\.\d+)/);
if (acap3ver) version = acap3ver[1];
}
// ACAP3: extract IP from "Tailscale IP: 100.x.x.x" logged by start.sh
if (!tsIP) {
var acap3ip = txt.match(/Tailscale IP: (100\.[\d.]+)/);
if (acap3ip) tsIP = acap3ip[1];
}
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
tsIP = tsIP || cacheGet('ip');
node = node || cacheGet('node');
tailnet = tailnet || cacheGet('tailnet');
version = version || cacheGet('version');
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
var isRunning = /-> Running/.test(lastState);
if (!isRunning && stateLines.length === 0) {
isRunning = /Tailscale VPN is running/.test(txt) ||
/health\(warnable=[^)]+\): ok/.test(txt) ||
/derp-\d+ connected/.test(txt) ||
/c2n: GET/.test(txt) ||
/localapi:/.test(txt);
}
// If an auth URL appears AFTER the last Running state, re-auth is needed
if (isRunning && latestUrl) {
var lastRunIdx = txt.lastIndexOf('-> Running');
var lastRunningMsgIdx = txt.lastIndexOf('Tailscale VPN is running');
if (lastRunningMsgIdx > lastRunIdx) lastRunIdx = lastRunningMsgIdx;
var urlSnippet = latestUrl.substring(0, 60);
var lastUrlIdx = -1, upos = 0, uidx;
while ((uidx = txt.indexOf(urlSnippet, upos)) !== -1) { lastUrlIdx = uidx; upos = uidx + 1; }
if (lastUrlIdx > lastRunIdx) isRunning = false;
}
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) return { state: 'connecting', url: null, ip: null, node: null, tailnet: null, version: version };
return { state: 'disconnected', url: null, ip: null, node: null, tailnet: null, version: version };
}
function classifyLine(msg) {
if (/error|fail|panic|fatal/i.test(msg)) return 'msg-err';
if (/warn|timeout|retry/i.test(msg)) return 'msg-warn';
if (/connected|running|logged in|success/i.test(msg)) return 'msg-ok';
if (/starting|auth|login|switching/i.test(msg)) return 'msg-info';
return '';
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function renderLogs(txt) {
var lines = txt.split('\n').filter(function(l) { return l.trim(); });
document.getElementById('log-count').textContent = lines.length + ' lines';
var h = '';
for (var i = 0; i < lines.length; i++) {
var parts = lines[i].match(/^(\S+\s+\d+\s+[\d:]+)\s+(.*)/);
var cls = classifyLine(lines[i]);
if (parts) {
h += '<span class="log-line"><span class="ts">' + escHtml(parts[1]) + '</span> <span class="' + cls + '">' + escHtml(parts[2]) + '</span></span>\n';
} else {
h += '<span class="log-line"><span class="' + cls + '">' + escHtml(lines[i]) + '</span></span>\n';
}
}
logBox.innerHTML = h;
if (autoScroll) logBox.scrollTop = logBox.scrollHeight;
}
function render(r) {
var banner = document.getElementById('status-banner');
var statusText = document.getElementById('status-text');
var auth = document.getElementById('auth-block');
var info = document.getElementById('info-card');
banner.className = 'status-banner ' + r.state;
var labels = { connected: 'Connected', connecting: 'Connecting...', disconnected: 'Stopped' };
statusText.textContent = labels[r.state];
if (r.state === 'connecting' && r.url) {
document.getElementById('auth-link').href = r.url;
document.getElementById('auth-url-text').textContent = r.url;
auth.style.display = '';
} else {
auth.style.display = 'none';
}
if (r.state === 'connected') {
document.getElementById('ts-ip').textContent = r.ip || '-';
document.getElementById('ts-ip').className = 'info-value' + (r.ip ? '' : ' dim');
document.getElementById('ts-node').textContent = r.node || '-';
document.getElementById('ts-node').className = 'info-value' + (r.node ? '' : ' dim');
document.getElementById('ts-tailnet').textContent = r.tailnet || '-';
document.getElementById('ts-tailnet').className = 'info-value' + (r.tailnet ? '' : ' dim');
document.getElementById('ts-version').textContent = r.version || '-';
document.getElementById('ts-version').className = 'info-value' + (r.version ? '' : ' dim');
info.style.display = '';
if (r.version) checkForUpdate(r.version);
} else {
info.style.display = 'none';
}
var now = new Date();
document.getElementById('status-time').textContent =
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
}
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
function checkAppRunning() {
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(xml) {
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
return m && m[1] === 'Running';
})
.catch(function() { return false; });
}
// ACAP3: tailscaled's own log has the IP, version, tailnet and Running state.
var DAEMON_LOG_URL = 'tailscaled.log';
var STATUS_URL = 'status.json';
// Ground truth published by start.sh from `tailscale status --json`.
function fetchStatus() {
return fetch(STATUS_URL + '?t=' + Date.now(), { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.ok ? r.json() : null; })
.catch(function() { return null; });
}
// Apply Tailscale's authoritative backend state onto the result object.
function applyStatus(result, st) {
var self = st.Self || {};
var ips = self.TailscaleIPs || st.TailscaleIPs || [];
var ip4 = null;
for (var i = 0; i < ips.length; i++) { if (/^100\./.test(ips[i])) { ip4 = ips[i]; break; } }
var bs = st.BackendState;
if (st.Version) result.version = String(st.Version).split('-')[0];
if (bs === 'Running' && self.Online === true) {
result.state = 'connected';
result.url = null;
result.ip = ip4 || result.ip;
result.node = self.HostName || result.node;
result.tailnet = (st.CurrentTailnet && st.CurrentTailnet.Name) || result.tailnet;
cacheSet('ip', result.ip); cacheSet('node', result.node);
cacheSet('tailnet', result.tailnet); cacheSet('version', result.version);
} else if (bs === 'NeedsLogin' || bs === 'NeedsMachineAuth') {
result.state = 'connecting';
result.url = st.AuthURL || result.url;
} else if (bs === 'Running') {
// 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') {
result.state = 'disconnected';
result.url = null;
} else {
// NoState / Starting / unknown
result.state = 'connecting';
result.url = st.AuthURL || result.url;
}
}
function refresh() {
var syslogFetch = fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); }).catch(function() { return ''; });
var daemonFetch = fetch(DAEMON_LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
.then(function(r) { return r.text(); }).catch(function() { return ''; });
Promise.all([syslogFetch, daemonFetch, fetchStatus()]).then(function(res) {
// Syslog provides Axis timestamp headers (node name) and start/stop events.
// tailscaled.log provides IP, version, tailnet, and -> Running state.
var txt = res[0] + '\n' + res[1];
var st = res[2];
var result = parse(txt);
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';
} else if (st && st.BackendState) {
// Authoritative: Tailscale's own backend state
applyStatus(result, st);
} else if (!result.url && result.state !== 'connected') {
// Fallback to log heuristic when status.json is unavailable
result.state = 'connected';
result.ip = result.ip || cacheGet('ip');
result.node = result.node || cacheGet('node');
result.tailnet = result.tailnet || cacheGet('tailnet');
result.version = result.version || cacheGet('version');
}
render(result);
});
}).catch(function() {
document.getElementById('status-text').textContent = 'Unable to fetch logs';
});
}
refresh();
setInterval(refresh, 5000);
// Check for updates from GitHub
var installedVersion = null;
var autoChecked = false;
function checkForUpdate(currentVersion, manual) {
if (!currentVersion) return;
installedVersion = currentVersion;
if (!manual && autoChecked) return;
if (!manual) autoChecked = true;
var btn = document.getElementById('check-update-btn');
if (manual && btn) btn.textContent = 'Checking...';
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
.then(function(r) { return r.json(); })
.then(function(data) {
var tag = (data.tag_name || '').replace(/^v/, '');
if (!tag) return;
if (compareVersions(tag, currentVersion) > 0) {
document.getElementById('update-version').textContent = 'v' + tag;
document.getElementById('update-banner').classList.add('visible');
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
if (btn) btn.textContent = 'Update Available';
} else {
if (manual && btn) btn.textContent = 'Up to date';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
}
})
.catch(function() {
if (manual && btn) btn.textContent = 'Check failed';
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
});
}
document.getElementById('check-update-btn').addEventListener('click', function() {
if (installedVersion) checkForUpdate(installedVersion, true);
});
function compareVersions(a, b) {
var pa = a.split('.').map(Number);
var pb = b.split('.').map(Number);
for (var i = 0; i < 3; i++) {
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
}
return 0;
}
})();
</script>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
/*
* 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;
static volatile pid_t g_child = -1;
static void sig_forward(int sig) {
g_stop = 1;
if (g_child > 0)
kill(g_child, sig);
}
int main(void)
{
signal(SIGTERM, sig_forward);
signal(SIGINT, sig_forward);
signal(SIGCHLD, SIG_DFL);
while (!g_stop) {
pid_t pid = fork();
if (pid == 0) {
signal(SIGTERM, SIG_DFL);
signal(SIGINT, SIG_DFL);
execl("/usr/local/packages/Tailscale_VPN/start.sh",
"/usr/local/packages/Tailscale_VPN/start.sh", (char *)0);
_exit(127);
}
if (pid < 0) {
sleep(5);
continue;
}
g_child = pid;
int status;
pid_t ret;
do {
ret = waitpid(pid, &status, 0);
} while (ret == -1 && errno == EINTR && !g_stop);
g_child = -1;
if (!g_stop) {
sleep(3);
}
}
return 0;
}
+14
View File
@@ -0,0 +1,14 @@
PACKAGENAME=Tailscale_VPN
MENUNAME="Tailscale VPN"
VENDOR="Mo3he"
APPMAJORVERSION=1
APPMINORVERSION=102
APPMICROVERSION=4
APPTYPE=armv7hf
APPNAME=Tailscale_VPN
APPOPTS=""
STARTMODE=respawn
OTHERFILES="lib html index.html LICENSE start.sh"
SETTINGSPAGEFILE=index.html
GRPNAME=
USERNAME=root
Executable
+141
View File
@@ -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
+113
View File
@@ -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
+43
View File
@@ -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/
+67
View File
@@ -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
+171
View File
@@ -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
+21
View File
@@ -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
+15
View File
@@ -0,0 +1,15 @@
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)
chmod +x Tailscale_VPN_run
$(PROG): $(SRCS)
$(CC) $(CFLAGS) -o $@ $^ $(LDADD)
clean:
rm -f $(PROG)
+182
View File
@@ -0,0 +1,182 @@
#!/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"
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
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
}
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"
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
# 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
set -- "$@" --login-server "$CUSTOM_SERVER"
fi
if [ -n "$AUTH_KEY" ]; then
set -- "$@" --authkey "$AUTH_KEY"
fi
if [ "$ACCEPT_DNS" = "true" ]; then
set -- "$@" --accept-dns=true
fi
if [ "$ACCEPT_ROUTES" = "true" ]; then
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
# 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
set -- "$@" --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.
{
"$TAILSCALE_PATH" "$@"
up_exit=$?
if [ "$up_exit" -eq 0 ]; then
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"
fi
else
logger -t "Tailscale_VPN" "ERROR: tailscale up failed (exit $up_exit)"
fi
} &
TAILSCALE_UP_PID=$!
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 /
# 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
+577
View File
@@ -0,0 +1,577 @@
// Copyright (C) 2024 Mo3he
// SPDX-License-Identifier: GPL-3.0-or-later
/*
* 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>
#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"
#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;
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;
}
/* 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)) {
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, RUN_SCRIPT_VARIANT, 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;
/* 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)) {
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 `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;
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)
#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)
#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) {
syslog(LOG_ERR, "cannot open config file %s: %s",
CONFIG_FILE, strerror(errno));
return;
}
write_var(f, "CUSTOM_SERVER", cache_get(&cfg_custom_server, ""));
write_var(f, "AUTH_KEY", cache_get(&cfg_auth_key, ""));
#ifdef HAS_PROXY_PORTS
write_var(f, "CONF_HTTP", cache_get(&cfg_http_proxy_port, "8080"));
write_var(f, "CONF_SOCKS", cache_get(&cfg_socks5_port, "1080"));
#endif
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
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 ─────────────────────────────────────────────── */
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);
#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);
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) ──────────────────
* 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
static const char *http_param_names[] = {
"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);
}
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, '}');
/* 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;
}
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);
}
}
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
* 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");
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
/* A stale sentinel would clear a freshly configured key before use. */
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",
#ifdef HAS_PROXY_PORTS
"HttpProxyPort", "Socks5Port",
#endif
"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;
}
+582
View File
@@ -0,0 +1,582 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tailscale VPN for Axis Devices</title>
<meta name="description" content="Install Tailscale VPN directly on your Axis device. Secure remote access with WireGuard, no extra hardware needed.">
<!-- Open Graph / LinkedIn -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mo3he.github.io/Axis_Cam_Tailscale/">
<meta property="og:title" content="Tailscale VPN for Axis Devices">
<meta property="og:description" content="Secure remote access to your Axis devices over WireGuard. No extra hardware, no complex network config - just install and connect.">
<meta property="og:image" content="https://mo3he.github.io/Axis_Cam_Tailscale/og-image.png">
<meta property="og:image:width" content="1340">
<meta property="og:image:height" content="724">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Tailscale VPN for Axis Devices">
<meta name="twitter:description" content="Secure remote access to your Axis devices over WireGuard. No extra hardware, no complex network config - just install and connect.">
<meta name="twitter:image" content="https://mo3he.github.io/Axis_Cam_Tailscale/og-image.png">
<style>
:root {
--bg: #0f1117;
--surface: #181b23;
--border: #262a35;
--text: #e4e6ed;
--muted: #8b8fa3;
--accent: #4f8ff7;
--accent-hover: #6ba1ff;
--green: #34d399;
--radius: 12px;
}
[data-theme="light"] {
--bg: #f8f9fb;
--surface: #ffffff;
--border: #e0e3e8;
--text: #1a1d27;
--muted: #5f6577;
--accent: #2563eb;
--accent-hover: #1d4ed8;
--green: #059669;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }
/* Nav */
nav {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1100px;
margin: 0 auto;
padding: 1.5rem 2rem;
}
nav .logo {
font-size: 1.2rem;
font-weight: 700;
color: var(--text);
}
nav .links { display: flex; gap: 1.5rem; align-items: center; }
nav .links a { color: var(--muted); font-size: 0.95rem; }
nav .links a:hover { color: var(--text); }
.theme-toggle {
background: none;
border: 1px solid var(--border);
color: var(--muted);
cursor: pointer;
border-radius: 8px;
padding: 0.4rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
width: 34px;
height: 34px;
}
.theme-toggle:hover { border-color: var(--muted); color: var(--text); }
.theme-toggle svg { width: 18px; height: 18px; }
/* Hero */
.hero {
text-align: center;
padding: 5rem 2rem 4rem;
max-width: 800px;
margin: 0 auto;
}
.hero .badge {
display: inline-block;
background: rgba(79, 143, 247, 0.12);
color: var(--accent);
padding: 0.35rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 1.5rem;
border: 1px solid rgba(79, 143, 247, 0.2);
}
.hero h1 {
font-size: 3rem;
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.15;
margin-bottom: 1.25rem;
}
.hero h1 span { color: var(--accent); }
.hero-word {
display: inline-block;
color: var(--accent);
transition: opacity 0.25s, transform 0.25s;
}
.hero p {
font-size: 1.2rem;
color: var(--muted);
max-width: 600px;
margin: 0 auto 2rem;
}
.hero-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
transition: all 0.2s;
}
.btn-primary {
background: var(--accent);
color: #fff;
}
.btn-primary:hover { background: var(--accent-hover); color: #fff; }
.btn-outline {
border: 1px solid var(--border);
color: var(--text);
background: transparent;
}
.btn-outline:hover { border-color: var(--muted); color: var(--text); }
/* Features */
.features {
max-width: 1100px;
margin: 0 auto;
padding: 3rem 2rem 4rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.25rem;
}
.feature-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.75rem;
}
.feature-card .icon {
font-size: 1.6rem;
margin-bottom: 0.75rem;
}
.feature-card h3 {
font-size: 1.05rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.feature-card p {
color: var(--muted);
font-size: 0.92rem;
line-height: 1.55;
}
/* Downloads */
.downloads {
max-width: 1100px;
margin: 0 auto;
padding: 3rem 2rem 4rem;
}
.downloads h2 {
text-align: center;
font-size: 2rem;
font-weight: 800;
margin-bottom: 0.5rem;
}
.downloads .subtitle {
text-align: center;
color: var(--muted);
margin-bottom: 2.5rem;
font-size: 1.05rem;
}
.download-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.25rem;
}
.download-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.75rem;
display: flex;
flex-direction: column;
}
.download-card .tag {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
margin-bottom: 0.75rem;
width: fit-content;
}
.tag-recommended { background: rgba(52, 211, 153, 0.15); color: var(--green); }
.tag-acap3 { background: rgba(168, 85, 247, 0.15); color: #a855f7; }
.tag-root { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
.download-card h3 {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.4rem;
}
.download-card p {
color: var(--muted);
font-size: 0.9rem;
margin-bottom: 1.25rem;
flex: 1;
}
.arch-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.arch-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.55rem 1rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
border: 1px solid var(--border);
color: var(--text);
background: transparent;
transition: all 0.2s;
}
.arch-btn:hover { border-color: var(--accent); color: var(--accent); }
.arch-btn svg { width: 16px; height: 16px; }
/* Install */
.install {
max-width: 700px;
margin: 0 auto;
padding: 3rem 2rem 4rem;
text-align: center;
}
.install h2 {
font-size: 2rem;
font-weight: 800;
margin-bottom: 2rem;
}
.steps {
text-align: left;
display: flex;
flex-direction: column;
gap: 1rem;
}
.step {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.step-num {
min-width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(79, 143, 247, 0.12);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.9rem;
border: 1px solid rgba(79, 143, 247, 0.2);
}
.step-text { padding-top: 0.35rem; }
.step-text strong { display: block; margin-bottom: 0.15rem; }
.step-text span { color: var(--muted); font-size: 0.92rem; }
/* Footer */
footer {
border-top: 1px solid var(--border);
text-align: center;
padding: 2rem;
color: var(--muted);
font-size: 0.85rem;
max-width: 1100px;
margin: 2rem auto 0;
}
footer .footer-links {
display: flex;
gap: 1.5rem;
justify-content: center;
margin-bottom: 0.75rem;
}
@media (max-width: 640px) {
.hero h1 { font-size: 2rem; }
.hero { padding: 3rem 1.25rem 2.5rem; }
.features, .downloads, .install { padding-left: 1.25rem; padding-right: 1.25rem; }
nav { padding: 1rem 1.25rem; flex-wrap: wrap; gap: 0.75rem; }
nav .logo { font-size: 1.05rem; }
nav .links { gap: 1rem; flex-wrap: wrap; justify-content: center; }
nav .links a { font-size: 0.85rem; }
.hero-buttons { flex-direction: column; align-items: center; }
.download-grid { grid-template-columns: 1fr; }
footer .footer-links { flex-wrap: wrap; gap: 1rem; }
}
</style>
</head>
<body>
<nav>
<div class="logo">Tailscale VPN + Axis</div>
<div class="links">
<a href="#features">Features</a>
<a href="#downloads">Downloads</a>
<a href="#install">Install</a>
<a href="https://github.com/Mo3he/Axis_Cam_Tailscale" target="_blank" rel="noopener">GitHub</a>
<a href="https://github.com/sponsors/Mo3he" target="_blank" rel="noopener" style="color: #db61a2;">Sponsor</a>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
<svg id="iconSun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
<svg id="iconMoon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
</div>
</nav>
<section class="hero">
<div class="badge">Open Source &middot; ACAP Package</div>
<h1>Tailscale VPN for<br>Axis <span class="hero-word" id="heroWord">Cameras</span></h1>
<p>Secure remote access to your Axis devices over WireGuard. No extra hardware, no complex network config - just install and connect.</p>
<div class="hero-buttons">
<a href="#downloads" class="btn btn-primary">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Download EAP
</a>
<a href="https://github.com/Mo3he/Axis_Cam_Tailscale" class="btn btn-outline" target="_blank" rel="noopener">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.387.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.756-1.333-1.756-1.09-.745.083-.73.083-.73 1.205.085 1.838 1.237 1.838 1.237 1.07 1.834 2.807 1.304 3.492.997.108-.775.418-1.305.762-1.604-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.468-2.382 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23a11.5 11.5 0 0 1 3.003-.404c1.02.005 2.047.138 3.006.404 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.838 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.694.825.576C20.565 21.795 24 17.295 24 12c0-6.63-5.37-12-12-12z"/></svg>
View on GitHub
</a>
</div>
</section>
<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>
This is an independent, community-developed ACAP package and is <strong style="color:var(--text);">not an official Axis Communications product</strong>.
It is not affiliated with, endorsed by, or supported by Axis Communications AB.
Use at your own risk. For official Axis software, visit <a href="https://www.axis.com" target="_blank" rel="noopener">axis.com</a>.
</div>
</div>
<section id="features" class="features">
<div class="feature-card">
<div class="icon">🔒</div>
<h3>WireGuard Encryption</h3>
<p>Built on Tailscale's WireGuard-based tunnel for fast, modern, and auditable encryption on every connection.</p>
</div>
<div class="feature-card">
<div class="icon">⚡</div>
<h3>No Root Required</h3>
<p>Runs in user-space networking mode on Axis OS 12+. No need to enable root access on your device.</p>
</div>
<div class="feature-card">
<div class="icon">📦</div>
<h3>Simple EAP Install</h3>
<p>Upload the .eap file through your device's web interface. Start the app and authenticate - done.</p>
</div>
<div class="feature-card">
<div class="icon">🔄</div>
<h3>Weekly Auto-Updates</h3>
<p>New EAP packages are automatically built and released every week when a new Tailscale version is available.</p>
</div>
<div class="feature-card">
<div class="icon">🌐</div>
<h3>Headscale Compatible</h3>
<p>Supports self-hosted Headscale servers with configurable server URL and auth key, built into every variant.</p>
</div>
<div class="feature-card">
<div class="icon">🔀</div>
<h3>Outbound Proxy</h3>
<p>Allows the device to route its own outbound traffic through Tailscale via a local HTTP/HTTPS and SOCKS5 proxy.</p>
</div>
</section>
<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">
<div class="download-card">
<div class="tag tag-recommended">Recommended</div>
<h3>Standard (Non-Root)</h3>
<p>For Axis OS 12 and newer. Runs in user-space networking mode without root privileges.</p>
<div class="arch-buttons">
<a class="arch-btn" data-asset="Tailscale_VPN" data-arch="aarch64" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
AARCH64
</a>
<a class="arch-btn" data-asset="Tailscale_VPN" data-arch="armv7hf" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
ARM
</a>
</div>
</div>
<div class="download-card">
<div class="tag tag-root">Root &middot; Legacy</div>
<h3>Root (Full Networking)</h3>
<p>For Axis OS older than 12. Requires root access but provides full Tailscale networking features.</p>
<div class="arch-buttons">
<a class="arch-btn" data-asset="ROOT" data-arch="aarch64" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
AARCH64
</a>
<a class="arch-btn" data-asset="ROOT" data-arch="armv7hf" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
ARM
</a>
</div>
</div>
<div class="download-card">
<div class="tag tag-acap3">ACAP3 &middot; Legacy</div>
<h3>ACAP3 (Older Axis OS)</h3>
<p>For devices running Axis OS versions that do not support ACAP4. ARM only.</p>
<div class="arch-buttons">
<a class="arch-btn" data-asset="acap3" data-arch="armv7hf" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
ARM
</a>
</div>
</div>
</div>
</section>
<section id="install" class="install">
<h2>Installation</h2>
<div class="steps">
<div class="step">
<div class="step-num">1</div>
<div class="step-text">
<strong>Download the EAP</strong>
<span>Grab the right .eap file for your device architecture from the releases page above.</span>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-text">
<strong>Upload to your device</strong>
<span>Log into your Axis device web interface and go to <strong>Apps &rarr; Add App</strong>. Upload the .eap file.</span>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-text">
<strong>Start &amp; authenticate</strong>
<span>Start the app, click <strong>Open</strong> to view logs and get your Tailscale authentication URL. Sign in and you're connected.</span>
</div>
</div>
</div>
</section>
<footer>
<div class="footer-links">
<a href="https://github.com/Mo3he/Axis_Cam_Tailscale" target="_blank" rel="noopener">GitHub</a>
<a href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases" target="_blank" rel="noopener">Releases</a>
<a href="https://github.com/sponsors/Mo3he" target="_blank" rel="noopener">Sponsor</a>
<a href="https://buymeacoffee.com/mo3he" target="_blank" rel="noopener">Buy Me a Coffee</a>
<a href="https://tailscale.com/" target="_blank" rel="noopener">Tailscale</a>
<a href="https://www.axis.com/" target="_blank" rel="noopener">Axis</a>
</div>
<p>&copy; 2025 Mo3he &middot; MIT License</p>
<p style="font-size:0.75rem;opacity:0.5;margin-top:0.5rem;">Tailscale is a product of Tailscale Inc. This is an independent, unofficial package that redistributes Tailscale binaries under the <a href="https://github.com/Mo3he/Axis_Cam_Tailscale/blob/main/LICENSE" target="_blank" rel="noopener" style="color:inherit;">BSD 3-Clause License</a>. Not affiliated with or endorsed by Tailscale Inc. or Axis Communications AB.</p>
</footer>
<script>
(function() {
var toggle = document.getElementById('themeToggle');
var sun = document.getElementById('iconSun');
var moon = document.getElementById('iconMoon');
var html = document.documentElement;
function applyTheme(theme) {
if (theme === 'light') {
html.setAttribute('data-theme', 'light');
sun.style.display = 'none';
moon.style.display = 'block';
} else {
html.removeAttribute('data-theme');
sun.style.display = 'block';
moon.style.display = 'none';
}
}
var stored = localStorage.getItem('theme');
if (stored) {
applyTheme(stored);
} else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
applyTheme('light');
}
toggle.addEventListener('click', function() {
var isLight = html.getAttribute('data-theme') === 'light';
var next = isLight ? 'dark' : 'light';
localStorage.setItem('theme', next);
applyTheme(next);
});
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'light' : 'dark');
}
});
var heroWord = document.getElementById('heroWord');
var devices = ['Cameras', 'Door Stations', 'Intercoms', 'Speakers', 'Radars', 'Encoders'];
var wordIdx = 0;
function cycleWord() {
wordIdx = (wordIdx + 1) % devices.length;
heroWord.style.opacity = '0';
heroWord.style.transform = 'translateY(8px)';
setTimeout(function() {
heroWord.textContent = devices[wordIdx];
heroWord.style.opacity = '1';
heroWord.style.transform = 'translateY(0)';
}, 250);
}
setInterval(cycleWord, 3000);
// Rewrite download links to point directly to latest release assets
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
.then(function(r) { return r.json(); })
.then(function(data) {
var assets = data.assets || [];
var buttons = document.querySelectorAll('a[data-asset]');
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
var type = btn.getAttribute('data-asset');
var arch = btn.getAttribute('data-arch');
for (var j = 0; j < assets.length; j++) {
var name = assets[j].name.toLowerCase();
var archMatch = name.indexOf(arch) !== -1;
var typeMatch = false;
if (type === 'Tailscale_VPN') {
typeMatch = name.indexOf('root') === -1 && name.indexOf('acap3') === -1;
} else if (type === 'acap3') {
typeMatch = name.indexOf('acap3') !== -1;
} else if (type === 'ROOT') {
typeMatch = name.indexOf('root') !== -1;
}
if (archMatch && typeMatch) {
btn.href = assets[j].browser_download_url;
btn.removeAttribute('target');
break;
}
}
}
})
.catch(function() {});
})();
</script>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

-1
View File
@@ -1 +0,0 @@
+29
View File
@@ -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"
}
]
}