Compare commits

...
188 Commits
Author SHA1 Message Date
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
51 changed files with 4495 additions and 180 deletions
+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
+205
View File
@@ -0,0 +1,205 @@
name: Auto Build & Release Tailscale ACAP
on:
schedule:
- cron: "0 0 * * *" # Every Monday at 03:00 UTC
workflow_dispatch:
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
# 1. Checkout repo
- uses: actions/checkout@v3
with:
persist-credentials: true
fetch-depth: 0
# 2. Get latest Tailscale version
- name: Get latest Tailscale version
id: tailscale_version
run: |
# 1. Start from GitHub latest
GH_TAG=$(curl -s https://api.github.com/repos/tailscale/tailscale/releases/latest | jq -r .tag_name)
GH_VERSION=${GH_TAG#v}
echo "GitHub latest: $GH_VERSION"
# 2. See if static ARM build exists for that version
if curl -sfI "https://pkgs.tailscale.com/stable/tailscale_${GH_VERSION}_arm.tgz" > /dev/null; then
VERSION="$GH_VERSION"
echo "Using GitHub latest (has ARM package): $VERSION"
else
echo "No ARM package for $GH_VERSION, falling back to latest version on pkgs.tailscale.com"
# 3. Derive latest version that actually has an ARM tarball
VERSION=$(
curl -s https://pkgs.tailscale.com/stable/ \
| grep -o 'tailscale_[0-9.]*_arm\.tgz' \
| sed -E 's/^tailscale_([0-9.]+)_arm\.tgz$/\1/' \
| sort -V | tail -n1
)
echo "Fallback version: $VERSION"
fi
echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Get current repo version
id: current
run: |
CURRENT=$(find . -path "*/app/manifest.json" -exec jq -r '.acapPackageConf.setup.version' {} \; | sort -u | head -n1)
echo "CURRENT_VERSION=$CURRENT" >> $GITHUB_ENV
echo "Current repo version: $CURRENT"
- name: Compare versions
id: compare
run: |
echo "Repo version: $CURRENT_VERSION"
echo "Latest Tailscale version: $RELEASE_VERSION"
echo "Trigger: ${{ github.event_name }}"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "build_needed=true" >> $GITHUB_ENV
echo "Manual trigger — building regardless of version."
elif [ "$CURRENT_VERSION" = "$RELEASE_VERSION" ]; then
echo "build_needed=false" >> $GITHUB_ENV
echo "Already up to date. Skipping build."
else
echo "build_needed=true" >> $GITHUB_ENV
echo "New version detected. Will build."
fi
# 3. Download Tailscale binaries
- name: Download Tailscale binaries
if: env.build_needed == 'true'
run: |
mkdir -p tailscale_bins
curl -L "https://pkgs.tailscale.com/stable/tailscale_${RELEASE_VERSION}_arm.tgz" -o tailscale_arm.tgz
tar -xzf tailscale_arm.tgz -C tailscale_bins --strip-components=1
mv tailscale_bins/tailscale tailscale_bins/tailscale_arm
mv tailscale_bins/tailscaled tailscale_bins/tailscaled_arm
curl -L "https://pkgs.tailscale.com/stable/tailscale_${RELEASE_VERSION}_arm64.tgz" -o tailscale_arm64.tgz
tar -xzf tailscale_arm64.tgz -C tailscale_bins --strip-components=1
mv tailscale_bins/tailscale tailscale_bins/tailscale_arm64
mv tailscale_bins/tailscaled tailscale_bins/tailscaled_arm64
# 4. Strip binaries to reduce package size
- name: Strip binaries
if: env.build_needed == 'true'
run: |
# Install cross-architecture strip tools
sudo apt-get update
sudo apt-get install -y binutils-aarch64-linux-gnu binutils-arm-linux-gnueabihf
# Strip debug info and symbol tables (zero runtime/memory cost)
aarch64-linux-gnu-strip -s tailscale_bins/tailscale_arm64
aarch64-linux-gnu-strip -s tailscale_bins/tailscaled_arm64
arm-linux-gnueabihf-strip -s tailscale_bins/tailscale_arm
arm-linux-gnueabihf-strip -s tailscale_bins/tailscaled_arm
ls -lh tailscale_bins/
# 5. Build each folder, update manifest, and copy .eap files
- name: Build all folders
if: env.build_needed == 'true'
run: |
mkdir -p build
rm -rf releases
mkdir -p releases
for folder in */ ; do
FOLDER_NAME="${folder%/}" # remove trailing slash
[[ "$FOLDER_NAME" == "common" ]] && continue
[[ ! -d "$folder/app" ]] && continue
echo "Processing folder $FOLDER_NAME"
# aarch64/arm/aarch64_ROOT/arm_ROOT share their C source, run script,
# HTML, and Makefile via common/app/ (see Dockerfile COPY layers);
# only arm_acap3 still carries its own self-contained app/ tree.
case "$FOLDER_NAME" in
aarch64|arm|aarch64_ROOT|arm_ROOT) APP_LIB_DIR="common/app/lib" ;;
*) APP_LIB_DIR="$folder/app/lib" ;;
esac
mkdir -p "$APP_LIB_DIR"
# Detect architecture
if [[ "$FOLDER_NAME" == arm* ]]; then
cp tailscale_bins/tailscale_arm "$APP_LIB_DIR/tailscale"
cp tailscale_bins/tailscaled_arm "$APP_LIB_DIR/tailscaled"
else
cp tailscale_bins/tailscale_arm64 "$APP_LIB_DIR/tailscale"
cp tailscale_bins/tailscaled_arm64 "$APP_LIB_DIR/tailscaled"
fi
# Detect variant suffix for .eap naming
if [[ "$FOLDER_NAME" == *_ROOT ]]; then
VARIANT="_root"
elif [[ "$FOLDER_NAME" == *_acap3 ]]; then
VARIANT="_acap3"
else
VARIANT=""
fi
# Update version — manifest.json for ACAP 4, package.conf for ACAP 3
if [[ -f "$folder/app/manifest.json" ]]; then
sed -i "s/\"version\": \".*\"/\"version\": \"${RELEASE_VERSION}\"/" "$folder/app/manifest.json"
elif [[ -f "$folder/app/package.conf" ]]; then
IFS='.' read -r MAJOR MINOR MICRO <<< "${RELEASE_VERSION}"
sed -i "s/^APPMAJORVERSION=.*/APPMAJORVERSION=${MAJOR}/" "$folder/app/package.conf"
sed -i "s/^APPMINORVERSION=.*/APPMINORVERSION=${MINOR}/" "$folder/app/package.conf"
sed -i "s/^APPMICROVERSION=.*/APPMICROVERSION=${MICRO}/" "$folder/app/package.conf"
fi
# Docker build
TAG_NAME=$(echo "$FOLDER_NAME" | tr '[:upper:]' '[:lower:]' | tr '/ ' '_') # lowercase and clean
echo "Building $TAG_NAME"
docker build -f "$folder/Dockerfile" --tag "$TAG_NAME" .
# Extract .eap files into build folder
EAP_OUTPUT="./build/${TAG_NAME}"
mkdir -p "$EAP_OUTPUT"
CID=$(docker create "$TAG_NAME")
docker cp "$CID":/opt/app "$EAP_OUTPUT"
docker rm "$CID" >/dev/null
# Move all .eap files to releases folder, append variant if needed
find "$EAP_OUTPUT" -type f -name "*.eap" | while read eap; do
BASENAME=$(basename "$eap" .eap)
if [[ -n "$VARIANT" ]]; then
mv "$eap" "releases/${BASENAME}${VARIANT}.eap"
else
mv "$eap" "releases/${BASENAME}.eap"
fi
done
done
# Clean up
rm -rf build tailscale_bins *.tgz
# 6. Commit updated manifests and .eap files directly to main
- name: Commit updates to main
if: env.build_needed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Only commit manifests and ACAP 3 package.conf; do not track release artifacts
git add */app/manifest.json arm_acap3/app/package.conf
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "Update Tailscale to v${RELEASE_VERSION}"
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Mo3he/Axis_Cam_Tailscale.git main
fi
# 7. Create GitHub Release with all new .eap files
- name: Create GitHub Release
if: env.build_needed == 'true'
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ env.RELEASE_VERSION }}
name: "Tailscale VPN ${{ env.RELEASE_VERSION }}"
files: releases/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+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@v6
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 }}
+13
View File
@@ -1,2 +1,15 @@
**/.DS_Store **/.DS_Store
**/build **/build
# Do not track release artifacts (local .eap build outputs stay untracked anywhere in the tree)
releases/
build/
*.eap
# Do not track downloaded Tailscale tarballs and temp bins
tailscale_bins/
*.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
+142
View File
@@ -0,0 +1,142 @@
# 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.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
+20 -19
View File
@@ -6,19 +6,20 @@ 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
- [I have a question](#i-have-a-question) - [I have a question](#i-have-a-question)
- [I want to contribute](#i-want-to-contribute) - [I want to contribute](#i-want-to-contribute)
- [Reporting bugs](#reporting-bugs) - [Reporting bugs](#reporting-bugs)
- [Before submitting a bug report](#before-submitting-a-bug-report) - [Before submitting a bug report](#before-submitting-a-bug-report)
- [How do I submit a good bug report?](#how-do-i-submit-a-good-bug-report) - [How do I submit a good bug report?](#how-do-i-submit-a-good-bug-report)
- [Suggesting enhancements](#suggesting-enhancements) - [Suggesting enhancements](#suggesting-enhancements)
- [Before Submitting an Enhancement](#before-submitting-an-enhancement) - [Before Submitting an Enhancement](#before-submitting-an-enhancement)
- [How do I submit a good enhancement suggestion?](#how-do-i-submit-a-good-enhancement-suggestion) - [How do I submit a good enhancement suggestion?](#how-do-i-submit-a-good-enhancement-suggestion)
- [Your first code contribution](#your-first-code-contribution) - [Your first code contribution](#your-first-code-contribution)
## I have a question ## I have a question
@@ -45,13 +46,13 @@ A good bug report shouldn't leave others needing to chase you up for more inform
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker][issues_bugs]. - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker][issues_bugs].
- Also make sure to search the internet to see if users outside of the GitHub community have discussed the issue. - Also make sure to search the internet to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug: - Collect information about the bug:
- Axis device model - Axis device model
- Axis device firmware version - Axis device firmware version
- Stack trace - Stack trace
- OS and version (Windows, Linux, macOS, x86, ARM) - OS and version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant
- Possibly your input and the output - Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions? - Can you reliably reproduce the issue? And can you also reproduce it with older versions?
#### How do I submit a good bug report? #### How do I submit a good bug report?
@@ -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 -->
+2 -3
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:
@@ -26,4 +25,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 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.
+210 -46
View File
@@ -1,78 +1,242 @@
# 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.48.2 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–12.10).
- 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).
## Installation
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`. |
## 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).
- To update, simply install the new `.eap` over the existing one.
### Manual update (advanced)
## Using the Tailscale ACAP Replace the binaries in `common/app/lib/` (shared by `aarch64`, `armv7hf`, and
their ROOT variants) or `arm_acap3/app/lib/` (legacy variant, kept separate):
The Tailscale ACAP will run a script on startup that sets the required permissions and starts the service and app. - `tailscale`
Once started click "Open" to see the output of the logs for further instructions and obtain the authetication URL. - `tailscaled`
When uninstalling the ACAP, all changes and files are removed from the camera. Download the latest versions:
[Tailscale static builds](https://pkgs.tailscale.com/stable/#static).
You will need a tailscale.com account to use the ACAP ## Build from source
## Updating Tailscale version 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.
The eap files will be updated from time to time and simply installing the new version over the old will update all files. All variants build from the **repository root**, pointing at the variant's own
`Dockerfile`:
It's also possible to build and use a locally built image as all necesary files are provided. ```bash
docker build -f aarch64/Dockerfile --tag <package_name> .
Replace binaries "tailscale" and "tailscaled" in lib folder with new versions. docker cp $(docker create <package_name>):/opt/app ./build
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–12.10.
- [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.
- [ ] **Sign the ACAP via the Axis ACAP Portal** - Deferred; the manifest's
`vendorId` is a placeholder value, not yet portal-registered.
### 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.
+42
View File
@@ -0,0 +1,42 @@
# Third-Party Notices
This ACAP package redistributes the Tailscale client. The ACAP's own wrapper
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
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.
```
+7 -4
View File
@@ -1,12 +1,15 @@
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 # Building the ACAP application
COPY ./app /opt/app/ COPY common/app /opt/app/
COPY aarch64/app/manifest.json /opt/app/manifest.json
WORKDIR /opt/app 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 ./
+2 -2
View File
@@ -1,5 +1,5 @@
To build, from main directory To build, from main directory
docker build --tag aarch64 . docker build --tag aarch64 .
docker cp $(docker create aarch64):/opt/app ./build docker cp $(docker create aarch64):/opt/app ./build
-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>
+64 -18
View File
@@ -1,21 +1,67 @@
{ {
"schemaVersion": "1.3", "schemaVersion": "2.0.0",
"acapPackageConf": { "acapPackageConf": {
"setup": { "setup": {
"appName": "Tailscale_VPN", "appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Mo3he", "vendor": "Mo3he",
"embeddedSdkVersion": "3.0", "vendorId": "5741c1fb91",
"user": { "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"group": "root", "version": "1.98.9",
"username": "root" "architecture": "aarch64",
}, "runMode": "respawn",
"vendorUrl": "https://www.tailscale.com", "compatibleOsVersions": [
"runMode": "once", {
"version": "1.48.2" "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"
}
]
} }
} }
} }
+14
View File
@@ -0,0 +1,14 @@
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}
# Building the ACAP application
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.98.9",
"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 -4
View File
@@ -1,12 +1,15 @@
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 # Building the ACAP application
COPY ./app /opt/app/ COPY common/app /opt/app/
COPY arm/app/manifest.json /opt/app/manifest.json
WORKDIR /opt/app 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 ./
+2 -2
View File
@@ -1,5 +1,5 @@
To build from main directory To build from main directory
docker build --tag arm . docker build --tag arm .
docker cp $(docker create arm):/opt/app ./build docker cp $(docker create arm):/opt/app ./build
-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>
+64 -18
View File
@@ -1,21 +1,67 @@
{ {
"schemaVersion": "1.3", "schemaVersion": "2.0.0",
"acapPackageConf": { "acapPackageConf": {
"setup": { "setup": {
"appName": "Tailscale_VPN", "appName": "Tailscale_VPN",
"friendlyName": "Tailscale VPN", "friendlyName": "Tailscale VPN",
"vendor": "Tailscale - Packaged by Mo3he", "vendor": "Mo3he",
"embeddedSdkVersion": "3.0", "vendorId": "5741c1fb91",
"user": { "vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
"group": "root", "version": "1.98.9",
"username": "root" "architecture": "armv7hf",
}, "runMode": "respawn",
"vendorUrl": "https://www.tailscale.com", "compatibleOsVersions": [
"runMode": "once", {
"version": "1.48.2" "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"
}
]
} }
} }
} }
+14
View File
@@ -0,0 +1,14 @@
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}
# Building the ACAP application
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.98.9",
"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"
}
]
}
}
}
+48
View File
@@ -0,0 +1,48 @@
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
# Rename the shell startup script (the ELF launcher will take the Tailscale_VPN name)
RUN mv Tailscale_VPN start.sh && chmod +x start.sh
# Compile a minimal ELF launcher as APPNAME.
# ACAP 3 elflibcheck requires an ELF binary and uses pidof(APPNAME) for status.
# We fork+exec start.sh so the parent "Tailscale_VPN" process stays resident,
# making pidof find it and the camera UI correctly show Running/Stopped.
RUN . /opt/axis/acapsdk/environment-setup* && \
${CC} -o Tailscale_VPN launcher.c && \
${STRIP} -s Tailscale_VPN
# Strip then UPX-compress the Tailscale binaries so they fit on flash
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
# Symlink tailscaled.log into html/ so the web UI can fetch it via HTTP.
# The log is written at runtime to localdata/ (resolved path at runtime).
RUN ln -sf ../localdata/tailscaled.log html/tailscaled.log
# Symlink the runtime status.json (written by start.sh from `tailscale status
# --json`) into html/ so the web UI can read Tailscale's authoritative state.
RUN ln -sf ../localdata/status.json html/status.json
# Build and package
RUN . /opt/axis/acapsdk/environment-setup* && create-package.sh ./
# Patch STARTMODE: create-package.sh hardcodes "never" unless RESTRICTION_STARTMODE is set,
# but the ACAP 3 SDK does not honour our package.conf's STARTMODE=respawn without it.
# Repack the .eap with the corrected value.
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"
+749
View File
@@ -0,0 +1,749 @@
<!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>
<!-- Status -->
<div id="status-banner" class="status-banner connecting">
<span class="dot"></span>
<span id="status-text" class="status-text">Checking...</span>
<span id="status-time" class="status-time"></span>
</div>
<!-- Update available -->
<div id="update-banner" class="update-banner">
<div class="update-text">Update available: <strong id="update-version"></strong></div>
<a id="update-link" class="update-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
<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>
<!-- Auth (hidden by default) -->
<div id="auth-block" class="auth-block" style="display:none;">
<p>Authenticate this device to connect to your Tailscale network:</p>
<a id="auth-link" class="auth-btn" href="#" target="_blank">
<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>
<!-- Connection Info -->
<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>
<!-- Logs -->
<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;
}
// Primary: extract hostname from Axis syslog header (always the real device hostname)
var node = null;
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
if (hostLine) node = hostLine[1];
// Fallback: popBrowserAuthNow/StartLoginInteractiveAs (may contain stale acap-tailscale_vpn)
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];
}
// Cache when found, restore from cache when missing
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
tsIP = tsIP || cacheGet('ip');
node = node || cacheGet('node');
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: also fetch the raw tailscaled.log (symlinked into html/) so the parser
// can find IP, version, tailnet and Running state from tailscaled's own output.
var DAEMON_LOG_URL = 'tailscaled.log';
// Authoritative backend state published by start.sh (symlinked into html/).
var STATUS_URL = 'status.json';
// Ground truth published by start.sh from `tailscale status --json`.
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) {
// Genuinely connected and reachable on the tailnet
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') {
// Backend running but node not online: transient network drop or the
// node was removed/expired and needs re-auth. Not connected. Keep any
// login URL the log parser found so the login button still appears.
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]); // show syslog in log panel; daemon log is too verbose
// Always verify with the app status API - logs can have stale entries
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>
+67
View File
@@ -0,0 +1,67 @@
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
/*
* ACAP 3 supervisor launcher for Tailscale_VPN.
*
* elflibcheck.sh requires APPNAME to be an ELF binary.
* acap-startstop / respawnd / list.cgi all use pidof(APPNAME) for status.
*
* This binary NEVER exits voluntarily — it loops restarting start.sh if it
* dies, so:
* - pidof Tailscale_VPN always finds this process → UI shows "Running"
* - respawnd never triggers (it only fires when APPNAME exits)
* - If tailscaled OOMs and start.sh exits, we cleanly restart it
*
* To stop the app, acap-startstop calls stop_daemon which sends SIGTERM here.
* We forward SIGTERM/SIGINT to the child and then exit.
*/
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) {
/* child: reset signals and exec start.sh */
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) {
/* start.sh died unexpectedly — wait before restarting */
sleep(3);
}
}
return 0;
}
View File
+14
View File
@@ -0,0 +1,14 @@
PACKAGENAME=Tailscale_VPN
MENUNAME="Tailscale VPN"
VENDOR="Mo3he"
APPMAJORVERSION=1
APPMINORVERSION=98
APPMICROVERSION=9
APPTYPE=armv7hf
APPNAME=Tailscale_VPN
APPOPTS=""
STARTMODE=respawn
OTHERFILES="lib html index.html LICENSE start.sh"
SETTINGSPAGEFILE=index.html
GRPNAME=
USERNAME=root
+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)
+177
View File
@@ -0,0 +1,177 @@
#!/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
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up --reset --hostname=$(hostname)"
if [ -n "$CUSTOM_SERVER" ]; then
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
fi
if [ -n "$AUTH_KEY" ]; then
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
fi
if [ "$ACCEPT_DNS" = "true" ]; then
TAILSCALE_CMD="$TAILSCALE_CMD --accept-dns=true"
fi
if [ "$ACCEPT_ROUTES" = "true" ]; then
TAILSCALE_CMD="$TAILSCALE_CMD --accept-routes=true"
fi
# Advertise LAN subnets so this camera acts as a subnet router. Comma-separated
# CIDRs (e.g. 192.168.1.0/24,10.0.0.0/8). In userspace-networking mode the
# tailscaled netstack forwards tailnet traffic to these subnets, so no kernel IP
# forwarding is required. In kernel-networking (root) mode the host must
# forward packets between the tailnet and the LAN, so enable IP forwarding.
# Routes must still be approved in the Tailscale admin console either way.
if [ -n "$ADVERTISE_ROUTES" ]; then
if [ "$VARIANT" = "root" ]; then
echo 1 >/proc/sys/net/ipv4/ip_forward 2>/dev/null || true
echo 1 >/proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || true
fi
TAILSCALE_CMD="$TAILSCALE_CMD --advertise-routes=$ADVERTISE_ROUTES"
fi
# Run `tailscale up` in the background and act on its outcome. If the node needs
# (re-)authentication, `up` blocks until the user logs in; backgrounding it
# ensures the status publisher below keeps running so the UI can surface the
# login URL (tailscaled reports BackendState=NeedsLogin + AuthURL while waiting).
# NOTE: `up` runs synchronously *inside* this backgrounded block so its real exit
# code is captured directly. We must NOT background `up` separately and `wait`
# for it from here, because in POSIX sh `wait` only works on children of the
# current shell — a subshell waiting on the parent's child returns 127.
{
eval "$TAILSCALE_CMD"
up_exit=$?
if [ "$up_exit" -eq 0 ]; then
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
View File
+586
View File
@@ -0,0 +1,586 @@
// Copyright (C) 2024 Mo3he
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* ACAP parameter bridge for Tailscale VPN.
*
* Responsibilities:
* 1. Read Tailscale parameters from the ACAP parameter store (axparameter).
* 2. Write them to CONFIG_FILE so the shell script can source them.
* 3. Launch the shell script (Tailscale_VPN_run) as a child process.
* 4. On any parameter change: rewrite CONFIG_FILE and do a full stop+restart
* of the child so the new config is picked up.
* Rapid changes within 300 ms are coalesced into a single restart.
* 5. Watchdog: if the child exits unexpectedly, restart it.
*
* Shared across the userspace-networking variants (unprivileged 'sdk' ACAP
* user) and the ROOT / kernel-networking variant. Build with -DHAS_PROXY_PORTS
* for the userspace variants, which exposes the HTTP/SOCKS5 proxy port
* parameters; the ROOT variant omits them since it has no local proxy.
*/
#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;
}
/* Ensure a parameter exists in the device parameter database. On in-place ACAP
* upgrades a newly introduced manifest parameter is not always auto-registered,
* which makes param.cgi return a 404 when the web UI tries to set it. Creating
* it here is idempotent: if it already exists, ax_parameter_add fails harmlessly. */
static void ensure_param(AXParameter *handle, const char *name, const char *def) {
GError *err = NULL;
if (!ax_parameter_add(handle, name, def, "string", &err)) {
if (err) g_error_free(err);
}
}
/* ── child process management ──────────────────────────────────────────── */
static void stop_child(void) {
if (child_pid <= 0)
return;
kill(child_pid, SIGTERM);
for (int i = 0; i < 30; i++) {
int status;
if (waitpid(child_pid, &status, WNOHANG) == child_pid) {
child_pid = -1;
return;
}
usleep(100000);
}
syslog(LOG_WARNING, "child did not exit in 3 s, sending SIGKILL");
kill(child_pid, SIGKILL);
waitpid(child_pid, NULL, 0);
child_pid = -1;
}
static void start_child(void) {
stop_child();
pid_t pid = fork();
if (pid < 0) {
syslog(LOG_ERR, "fork failed: %s", strerror(errno));
return;
}
if (pid == 0) {
execl(RUN_SCRIPT, RUN_SCRIPT, 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;
/* If child exited 0, auth succeeded — clear AuthKey via axparameter */
if (exit_code == 0 && g_ax_handle && cfg_auth_key && *cfg_auth_key) {
GError *err = NULL;
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
free(cfg_auth_key); cfg_auth_key = strdup("");
syslog(LOG_INFO, "AuthKey cleared after successful auth");
} else {
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
err ? err->message : "unknown");
if (err) g_error_free(err);
}
}
start_child();
}
}
return G_SOURCE_CONTINUE;
}
/* ── auth-key sentinel ───────────────────────────────────────────────────── */
/* The run script drops SENTINEL_FILE after a successful `tailscale up` that
* used a one-time auth key. Clear the stored AuthKey so it is not reused and
* disappears from the settings UI. This replaces the old exit-code-0 path,
* which never fired because tailscaled keeps the child alive indefinitely. */
static gboolean authkey_sentinel_cb(gpointer G_GNUC_UNUSED data) {
if (access(SENTINEL_FILE, F_OK) != 0)
return G_SOURCE_CONTINUE;
if (g_ax_handle && cfg_auth_key && *cfg_auth_key) {
GError *err = NULL;
if (ax_parameter_set(g_ax_handle, "AuthKey", "", TRUE, &err)) {
free(cfg_auth_key); cfg_auth_key = strdup("");
syslog(LOG_INFO, "AuthKey cleared after successful auth (sentinel)");
} else {
syslog(LOG_WARNING, "failed to clear AuthKey: %s",
err ? err->message : "unknown");
if (err) g_error_free(err);
}
}
unlink(SENTINEL_FILE);
return G_SOURCE_CONTINUE;
}
/* ── config file ─────────────────────────────────────────────────────────── */
static void load_config_cache(AXParameter *handle) {
GError *error = NULL;
gchar *val = NULL;
#define LOAD(name, field) \
val = NULL; error = NULL; \
if (ax_parameter_get(handle, name, &val, &error)) { \
free(field); field = val ? strdup(val) : strdup(""); \
g_free(val); val = NULL; \
} else { \
syslog(LOG_WARNING, "ax_parameter_get %s failed: %s", name, \
error ? error->message : "unknown"); \
if (error) { g_error_free(error); error = NULL; } \
}
LOAD("CustomServer", cfg_custom_server)
LOAD("AuthKey", cfg_auth_key)
#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
}
static void write_config_file(void) {
FILE *f = fopen(CONFIG_FILE, "w");
if (!f) {
syslog(LOG_ERR, "cannot open config file %s: %s",
CONFIG_FILE, strerror(errno));
return;
}
fprintf(f, "CUSTOM_SERVER=%s\n", cache_get(&cfg_custom_server, ""));
fprintf(f, "AUTH_KEY=%s\n", cache_get(&cfg_auth_key, ""));
#ifdef HAS_PROXY_PORTS
fprintf(f, "CONF_HTTP=%s\n", cache_get(&cfg_http_proxy_port, "8080"));
fprintf(f, "CONF_SOCKS=%s\n", cache_get(&cfg_socks5_port, "1080"));
#endif
fprintf(f, "ACCEPT_DNS=%s\n", cache_get(&cfg_accept_dns, "false"));
fprintf(f, "ACCEPT_ROUTES=%s\n", cache_get(&cfg_accept_routes, "false"));
fprintf(f, "ADVERTISE_ROUTES=%s\n", cache_get(&cfg_advertise_routes, ""));
fclose(f);
chmod(CONFIG_FILE, 0600);
#ifdef HAS_PROXY_PORTS
syslog(LOG_INFO, "config updated: http=%s socks=%s server=%s",
cache_get(&cfg_http_proxy_port, "8080"),
cache_get(&cfg_socks5_port, "1080"),
cache_get(&cfg_custom_server, "(default)"));
#else
syslog(LOG_INFO, "config updated: server=%s",
cache_get(&cfg_custom_server, "(default)"));
#endif
}
/* ── ACAP parameter callback ─────────────────────────────────────────────── */
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) ──────────────────
* Some AXIS device classes (e.g. recorders/NVRs) do not expose the legacy
* /axis-cgi/param.cgi VAPIX endpoint, so the web UI cannot load or save
* settings through it. This tiny HTTP server, reached through the manifest
* reverseProxy mapping at /local/Tailscale_VPN/api/settings, lets the web UI
* fall back to reading and writing the parameters directly. */
#define HTTP_PORT 2201
static const char *http_param_names[] = {
"CustomServer", "AuthKey",
#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, '}');
/* g_string_free(out, FALSE) is inlined by glib >= 2.76 headers into a call
* to g_string_free_and_steal(), which doesn't exist in older glib runtimes
* (e.g. AXIS OS 11.x). Copy out and fully free instead to stay portable. */
gchar *json_result = g_strdup(out->str);
g_string_free(out, TRUE);
return json_result;
}
static gchar *http_url_decode(const char *s, size_t len) {
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");
/* Ensure localdata dir exists */
mkdir("/usr/local/packages/Tailscale_VPN/localdata", 0755);
/* Drop any stale auth-key sentinel from a previous run so we don't clear a
* freshly configured key before it has been used. */
unlink(SENTINEL_FILE);
AXParameter *handle = ax_parameter_new(APP_NAME, &error);
if (!handle) {
syslog(LOG_ERR, "ax_parameter_new: %s",
error ? error->message : "unknown");
if (error) g_error_free(error);
return 1;
}
g_ax_handle = handle;
ensure_param(handle, "AdvertiseRoutes", "");
load_config_cache(handle);
write_config_file();
start_child();
const char *params[] = {
"CustomServer", "AuthKey",
#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;
}
+592
View File
@@ -0,0 +1,592 @@
<!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>
<!-- Hero -->
<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>
<!-- Disclaimer -->
<div style="max-width:720px;margin:0 auto;padding:0 1.5rem 1.5rem;">
<div style="background:rgba(79,143,247,0.07);border:1px solid rgba(79,143,247,0.18);border-radius:10px;padding:12px 18px;font-size:12.5px;color:var(--muted);line-height:1.6;">
<strong style="color:var(--text);">Disclaimer:</strong>
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>
<!-- Features -->
<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>
<!-- Downloads -->
<section id="downloads" class="downloads">
<h2>Download</h2>
<p class="subtitle">Pick the right variant for your device and Axis OS version.</p>
<div class="download-grid">
<!-- Standard -->
<div class="download-card">
<div class="tag tag-recommended">Recommended</div>
<h3>Standard (Non-Root)</h3>
<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>
<!-- ROOT -->
<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>
<!-- ACAP3 -->
<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>
<!-- Install -->
<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 -->
<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');
}
});
// Rotate hero word
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

-1
View File
@@ -1 +0,0 @@