mirror of
https://github.com/Mo3he/Axis_Cam_Tailscale.git
synced 2026-10-01 12:05:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b09a40231 | ||
|
|
98918ca6f5 | ||
|
|
08b0fdc081 | ||
|
|
f3c6dd54f3 | ||
|
|
c3cbf8d433 | ||
|
|
39a5a42def | ||
|
|
713550d1c0 | ||
|
|
14db783425 | ||
|
|
c02f0aa498 | ||
|
|
96477cdb0d | ||
|
|
4e0eabeec3 | ||
|
|
59161ccd91 | ||
|
|
e1b2ac3490 | ||
|
|
5f03ac918f | ||
|
|
2db9f27181 | ||
|
|
396b450415 | ||
|
|
2d1ff5e8a6 | ||
|
|
7655028477 | ||
|
|
f890d8df1a | ||
|
|
df0756a64d | ||
|
|
df2cf84c6c | ||
|
|
f420e7d24c | ||
|
|
3dc49f1254 | ||
|
|
2005ba32db | ||
|
|
1f9c6a9e45 | ||
|
|
6148fc3298 | ||
|
|
bd60c24b5b | ||
|
|
7926c58b3d | ||
|
|
6bdca7eadd | ||
|
|
8b941b08a5 | ||
|
|
d157a91bae | ||
|
|
b35ed437b5 | ||
|
|
cb390384a1 | ||
|
|
533fc53040 | ||
|
|
0a85c286b4 | ||
|
|
d7e7b63952 | ||
|
|
ed7643b2eb | ||
|
|
c4b24aee40 | ||
|
|
bec473f0a8 | ||
|
|
5278677098 | ||
|
|
c21f640622 | ||
|
|
83e108bb05 | ||
|
|
38d7af6c86 | ||
|
|
51ead4a708 | ||
|
|
123906c98b | ||
|
|
f6999311b7 | ||
|
|
a95415d687 | ||
|
|
7d584d1a22 | ||
|
|
e27f0cdb34 | ||
|
|
e5954eac52 | ||
|
|
91a45be400 | ||
|
|
9b06b49fef | ||
|
|
811aa9e2f5 | ||
|
|
f69a1971a2 |
+56
-11
@@ -1,14 +1,9 @@
|
|||||||
name: Auto Build & Release Tailscale ACAP
|
name: Auto Build & Release Tailscale ACAP
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
force:
|
|
||||||
description: "Force build even if version exists"
|
|
||||||
required: false
|
|
||||||
default: "false"
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 6 * * *' # optional: daily at 06:00 UTC
|
- cron: "0 0 * * *" # Every Monday at 03:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release:
|
build-and-release:
|
||||||
@@ -25,13 +20,59 @@ jobs:
|
|||||||
- name: Get latest Tailscale version
|
- name: Get latest Tailscale version
|
||||||
id: tailscale_version
|
id: tailscale_version
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(curl -s https://api.github.com/repos/tailscale/tailscale/releases/latest | jq -r .tag_name)
|
# 1. Start from GitHub latest
|
||||||
VERSION=${VERSION#v} # remove leading 'v'
|
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 "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
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
|
# 3. Download Tailscale binaries
|
||||||
- name: Download Tailscale binaries
|
- name: Download Tailscale binaries
|
||||||
|
if: env.build_needed == 'true'
|
||||||
run: |
|
run: |
|
||||||
mkdir -p tailscale_bins
|
mkdir -p tailscale_bins
|
||||||
curl -L "https://pkgs.tailscale.com/stable/tailscale_${RELEASE_VERSION}_arm.tgz" -o tailscale_arm.tgz
|
curl -L "https://pkgs.tailscale.com/stable/tailscale_${RELEASE_VERSION}_arm.tgz" -o tailscale_arm.tgz
|
||||||
@@ -46,8 +87,10 @@ jobs:
|
|||||||
|
|
||||||
# 4. Build each folder, update manifest, and copy .eap files
|
# 4. Build each folder, update manifest, and copy .eap files
|
||||||
- name: Build all folders
|
- name: Build all folders
|
||||||
|
if: env.build_needed == 'true'
|
||||||
run: |
|
run: |
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
|
rm -rf releases
|
||||||
mkdir -p releases
|
mkdir -p releases
|
||||||
|
|
||||||
for folder in */ ; do
|
for folder in */ ; do
|
||||||
@@ -102,11 +145,12 @@ jobs:
|
|||||||
|
|
||||||
# 5. Commit updated manifests and .eap files directly to main
|
# 5. Commit updated manifests and .eap files directly to main
|
||||||
- name: Commit updates to main
|
- name: Commit updates to main
|
||||||
|
if: env.build_needed == 'true'
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
# Only commit manifests; do not track release artifacts
|
||||||
git add */app/manifest.json releases/*
|
git add */app/manifest.json
|
||||||
if git diff --cached --quiet; then
|
if git diff --cached --quiet; then
|
||||||
echo "No changes to commit"
|
echo "No changes to commit"
|
||||||
else
|
else
|
||||||
@@ -116,6 +160,7 @@ jobs:
|
|||||||
|
|
||||||
# 6. Create GitHub Release with all new .eap files
|
# 6. Create GitHub Release with all new .eap files
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
|
if: env.build_needed == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ env.RELEASE_VERSION }}
|
tag_name: v${{ env.RELEASE_VERSION }}
|
||||||
|
|||||||
+12
@@ -1,2 +1,14 @@
|
|||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
**/build
|
**/build
|
||||||
|
|
||||||
|
# Do not track release artifacts
|
||||||
|
releases/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -1,117 +1,145 @@
|
|||||||
# The Tailscale installer ACAP
|
# Tailscale Installer ACAP for Axis Cameras
|
||||||
|
|
||||||
This ACAP packages the scripts and files required to install the Tailscale VPN client on Axis Cameras.
|
This repository contains an **ACAP package** that installs the [Tailscale VPN client](https://tailscale.com/) on Axis cameras.
|
||||||
|
|
||||||
Current version 1.84.0
|
- ✅ Secure remote access to cameras
|
||||||
|
- ✅ Easy to install via EAP package
|
||||||
|
- ✅ Works on **Axis OS 12+** (non-root version)
|
||||||
|
- ✅ Based on **WireGuard VPN** technology
|
||||||
|
|
||||||
https://tailscale.com/changelog/
|
[](https://github.com/Mo3he/Axis_Cam_Tailscale/releases)
|
||||||
|
[](LICENSE)
|
||||||
|

|
||||||
|
[](https://github.com/sponsors/Mo3he)
|
||||||
|
|
||||||
The "ROOT" versions require root privileges on the camera and will not work on OS 12 up.
|
|
||||||
|
|
||||||
If you like my work and want to help me to keep maintaining it, a sponsor would be amazing, every bit helps.
|
---
|
||||||
|
|
||||||
[:dollar: Sponsor](https://github.com/sponsors/Mo3he)
|
## 📑 Table of Contents
|
||||||
|
|
||||||
## Testers needed
|
- [📥 Installation](#-installation)
|
||||||
|
- [🚀 Usage](#-usage)
|
||||||
|
- [🔄 Updating Tailscale](#-updating-tailscale)
|
||||||
|
- [🧪 Testers Needed](#-testers-needed)
|
||||||
|
- [🎉 Good News](#-good-news)
|
||||||
|
- [🎯 Purpose](#-purpose)
|
||||||
|
- [🔗 Useful Links](#-useful-links)
|
||||||
|
- [🖥️ Compatibility](#️-compatibility)
|
||||||
|
- [⭐ Star History](#-star-history)
|
||||||
|
- [💖 Support](#-support)
|
||||||
|
|
||||||
I have added a new "custom" version (Tailscale_VPN_Custom_1_84_0_aarch64.eap) which allows you to set a custom server and auth key for use of headscale.
|
---
|
||||||
|
|
||||||
Click the three dots then "settings" to add your details.
|
## 📥 Installation
|
||||||
If you get a tls error restart the app.
|
|
||||||
|
|
||||||
Please give it a try and let me know if it works well or if you have issues.
|
The recommended way is to use the **prebuilt `.eap` file** from the [Releases page](https://github.com/Mo3he/Axis_Cam_Tailscale/releases).
|
||||||
|
|
||||||
## Good news, everyone!
|
1. Log into your Axis camera.
|
||||||
|
2. Go to **Apps → Add App**.
|
||||||
|
3. Upload the `.eap` file.
|
||||||
|
|
||||||
We have found a way to run the Tailscale ACAP without root privileges allowing it to run on Axis OS 12.
|
Once installed:
|
||||||
|
- Start the app.
|
||||||
|
- Click **Open** to view logs and get your Tailscale authentication URL.
|
||||||
|
- On uninstall, all changes/files are removed.
|
||||||
|
|
||||||
Please note this new version runs in user space networking mode and therefore has some limitations, most notably the camera will not be able to connect out to other Tailscale nodes.
|
> ⚠️ You’ll need a [Tailscale account](https://tailscale.com/) to authenticate.
|
||||||
This does not affect the normal use case of using Tailscale to connect to the camera.
|
|
||||||
If you require full functionality, please do not upgrade to Axis OS 12 and use the version marked "ROOT".
|
|
||||||
|
|
||||||
The changes are implemented from version 1.68.1 of the acap.
|
---
|
||||||
|
|
||||||
Thank you for your continued support.
|
## 🚀 Usage
|
||||||
|
|
||||||
## Purpose
|
- Runs a startup script to set permissions and launch Tailscale.
|
||||||
|
- View logs via the **Open** button in the app.
|
||||||
|
- Authenticate using the provided URL.
|
||||||
|
|
||||||
Adding a VPN client directly to the camera allows secure remote access to the device without requiring any other equipment or network configuration.
|
---
|
||||||
Tailscale achieves this in a secure, simple to setup and easy to use way.
|
|
||||||
Tailscale is based on WireGuard VPN tunneling technology.
|
|
||||||
|
|
||||||
https://tailscale.com/blog/how-tailscale-works/
|
## 🔄 Updating Tailscale
|
||||||
|
|
||||||
## Links
|
- New `.eap` files are auto-built and released **weekly** (if a new Tailscale version is available).
|
||||||
|
- To update, simply install the new `.eap` over the existing one.
|
||||||
|
|
||||||
https://tailscale.com/
|
### Manual update (advanced)
|
||||||
|
|
||||||
https://github.com/tailscale/tailscale
|
Replace the binaries in the `lib/` folder:
|
||||||
|
- `tailscale`
|
||||||
|
- `tailscaled`
|
||||||
|
|
||||||
https://www.wireguard.com/
|
Download the latest versions: [Tailscale static builds](https://pkgs.tailscale.com/stable/#static)
|
||||||
|
|
||||||
https://www.axis.com/
|
#### Build locally
|
||||||
|
|
||||||
## Compatibility
|
From the main directory of the version you want (`arm` / `aarch64`):
|
||||||
|
|
||||||
The Tailscale ACAP is compatable with Axis cameras with arm and aarch64 based Soc's.
|
```bash
|
||||||
|
docker build --tag <package_name> .
|
||||||
```
|
docker cp $(docker create <package_name>):/opt/app ./build
|
||||||
curl --anyauth "*" -u <username>:<password> <device ip>/axis-cgi/basicdeviceinfo.cgi --data "{\"apiVersion\":\"1.0\",\"context\":\"Client defined request ID\",\"method\":\"getAllProperties\"}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
where `<device ip>` is the IP address of the Axis device, `<username>` is the root username and `<password>` is the root password. Please
|
---
|
||||||
note that you need to enclose your password with quotes (`'`) if it contains special characters.
|
|
||||||
|
|
||||||
## Installing
|
## 🧪 Testers Needed
|
||||||
|
|
||||||
The recommended way to install this ACAP is to use the pre built eap file.
|
A new **custom** version is available:
|
||||||
Go to "Apps" on the camera and click "Add app".
|
- Allows setting a custom server and auth key (for [Headscale](https://headscale.net/)).
|
||||||
|
- Go to **Settings (⋮ → Settings)** to add your details.
|
||||||
|
|
||||||
|
Please give it a try and share your feedback!
|
||||||
|
|
||||||
## Using the Tailscale ACAP
|
---
|
||||||
|
|
||||||
The Tailscale ACAP will run a script on startup that sets the required permissions and starts the service and app.
|
## 🎉 Good News
|
||||||
Once started click "Open" to see the output of the logs for further instructions and obtain the authetication URL.
|
|
||||||
|
|
||||||
When uninstalling the ACAP, all changes and files are removed from the camera.
|
Tailscale ACAP can now run **without root privileges**, making it compatible with **Axis OS 12+**.
|
||||||
|
|
||||||
You will need a tailscale.com account to use the ACAP
|
- Runs in **user space networking mode**.
|
||||||
|
|
||||||
## Updating Tailscale version
|
For **full networking features**, use the **ROOT** version on Axis OS < 12.
|
||||||
|
|
||||||
The eap files will be updated from time to time and simply installing the new version over the old will update all files.
|
---
|
||||||
|
|
||||||
It's also possible to build and use a locally built image as all necesary files are provided.
|
## 🎯 Purpose
|
||||||
|
|
||||||
Replace binaries "tailscale" and "tailscaled" in lib folder with new versions.
|
Adding a VPN client directly to the camera enables:
|
||||||
Make sure you use the files for the correct Soc.
|
- Secure remote access without additional hardware or complex network configuration.
|
||||||
|
- Easy setup through Tailscale’s lightweight WireGuard-based tunnel.
|
||||||
|
|
||||||
Latest versions can be found at
|
🔗 Learn more: [How Tailscale Works](https://tailscale.com/blog/how-tailscale-works/)
|
||||||
|
|
||||||
https://pkgs.tailscale.com/stable/#static
|
---
|
||||||
|
|
||||||
|
## 🔗 Useful Links
|
||||||
|
|
||||||
To build,
|
- [Tailscale](https://tailscale.com/)
|
||||||
From main directory of the version you want (arm/aarch64)
|
- [Tailscale GitHub](https://github.com/tailscale/tailscale)
|
||||||
|
- [WireGuard](https://www.wireguard.com/)
|
||||||
|
- [Axis Communications](https://www.axis.com/)
|
||||||
|
|
||||||
```
|
---
|
||||||
docker build --tag <package name> .
|
|
||||||
```
|
## 🖥️ Compatibility
|
||||||
```
|
|
||||||
docker cp $(docker create <package name>):/opt/app ./build
|
The Tailscale ACAP is compatible with Axis cameras with **ARM** and **AARCH64**-based SoCs.
|
||||||
|
|
||||||
|
You can verify your device details using the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --anyauth "*" -u <username>:<password> <device_ip>/axis-cgi/basicdeviceinfo.cgi --data '{"apiVersion":"1.0","context":"Client defined request ID","method":"getAllProperties"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Thanks to wesQ3 there is now also a version in the "all" folder with a script that simplifies the build process with the following changes.
|
> Replace `<device_ip>`, `<username>`, and `<password>` with your device credentials.
|
||||||
|
> Enclose your password in quotes `' '` if it contains special characters.
|
||||||
|
|
||||||
1. Architecture specific build directories are not required, target arch is now an argument
|
---
|
||||||
|
|
||||||
2. manifest files are templated as part of the build
|
## ⭐ Star History
|
||||||
|
|
||||||
3. Tailscale binaries are downloaded as part of the build
|
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
|
||||||
|
|
||||||
[](https://www.star-history.com/#Mo3he/Axis_Cam_Tailscale&Date)
|
[](https://www.star-history.com/#Mo3he/Axis_Cam_Tailscale&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💖 Support
|
||||||
|
|
||||||
|
If you like this project and want to support future updates:
|
||||||
|
👉 [Sponsor Me](https://github.com/sponsors/Mo3he)
|
||||||
|
|||||||
@@ -1,10 +1,30 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
echo "Starting Service"
|
APP_DIR="/usr/local/packages/Tailscale_VPN"
|
||||||
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
|
STATE_DIR="$APP_DIR/localdata"
|
||||||
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
|
|
||||||
/usr/local/packages/Tailscale_VPN/lib/tailscaled --tun=userspace-networking --socket=/usr/local/packages/Tailscale_VPN/tailscaled.sock &
|
logger -t "Tailscale_VPN" "Starting Tailscale VPN service"
|
||||||
echo "Service Started"
|
|
||||||
echo "Scroll to Bottom for link"
|
mkdir -p "$STATE_DIR"
|
||||||
/usr/local/packages/Tailscale_VPN/lib/tailscale --socket=/usr/local/packages/Tailscale_VPN/tailscaled.sock up
|
chmod 755 "$APP_DIR/lib/tailscale"
|
||||||
wait
|
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 (userspace networking)"
|
||||||
|
"$APP_DIR/lib/tailscaled" \
|
||||||
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
|
--socket="$STATE_DIR/tailscaled.sock" \
|
||||||
|
--socks5-server=localhost:1055 \
|
||||||
|
--tun=userspace-networking &
|
||||||
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
||||||
|
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up
|
||||||
|
|
||||||
|
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||||
|
wait $TAILSCALED_PID
|
||||||
|
logger -t "Tailscale_VPN" "tailscaled exited"
|
||||||
|
|||||||
+252
-13
@@ -1,19 +1,258 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<body>
|
|
||||||
<head>
|
<head>
|
||||||
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
|
<meta charset="UTF-8">
|
||||||
<script>
|
<title>Tailscale VPN</title>
|
||||||
function refreshIFrame() {
|
<style>
|
||||||
var x = document.getElementById("*Your_iframe_id*");
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
x.contentWindow.location.reload();
|
|
||||||
var t = setTimeout(refreshIFrame, 5000);
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
||||||
|
background: #f5f6f8;
|
||||||
|
color: #1a1a2e;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header svg { flex-shrink: 0; }
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status row ── */
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
|
||||||
|
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
|
||||||
|
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auth URL block ── */
|
||||||
|
#auth-block {
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-block p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #92400e;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link {
|
||||||
|
display: inline-block;
|
||||||
|
background: #0166ff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link:hover { background: #0052cc; }
|
||||||
|
|
||||||
|
#auth-url-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Info grid (connected state) ── */
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 6px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid .label { color: #888; }
|
||||||
|
.info-grid .value { font-weight: 500; font-family: monospace; }
|
||||||
|
|
||||||
|
/* ── Log section ── */
|
||||||
|
.log-header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid #e8e8ec;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body onload="refreshIFrame()">
|
<body>
|
||||||
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
|
|
||||||
</body>
|
<header>
|
||||||
|
<!-- Tailscale wordmark icon -->
|
||||||
|
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="128" height="128" rx="28" fill="#0166FF"/>
|
||||||
|
<circle cx="64" cy="64" r="18" fill="white"/>
|
||||||
|
<circle cx="64" cy="22" r="10" fill="white"/>
|
||||||
|
<circle cx="64" cy="106" r="10" fill="white"/>
|
||||||
|
<circle cx="22" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="106" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="93" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="93" r="10" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Tailscale VPN</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Status card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="status-row">
|
||||||
|
<span id="dot" class="dot connecting"></span>
|
||||||
|
<span id="status-label" class="status-label">Checking…</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when waiting for auth -->
|
||||||
|
<div id="auth-block" style="display:none;">
|
||||||
|
<p>Open this link in a browser to authenticate this device:</p>
|
||||||
|
<a id="auth-link" href="#" target="_blank">Authenticate →</a>
|
||||||
|
<span id="auth-url-text"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when connected -->
|
||||||
|
<div id="info-block" style="display:none;" class="info-grid">
|
||||||
|
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">—</span>
|
||||||
|
<span class="label">Node</span><span class="value" id="ts-node">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="log-header">Service Log</div>
|
||||||
|
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN';
|
||||||
|
|
||||||
|
function parse(txt) {
|
||||||
|
// Use the LAST occurrence of each pattern so stale log entries don't win
|
||||||
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
|
|
||||||
|
// Tailscale IP — 100.x.x.x range
|
||||||
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
|
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
|
|
||||||
|
// Node name from "Logged into tailnet" line or "acap-" username pattern
|
||||||
|
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||||
|
var node = nodeMatch ? nodeMatch[1] : null;
|
||||||
|
|
||||||
|
// Determine state from the LAST state-transition log line
|
||||||
|
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
|
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
return { state: 'connected', url: null, ip: tsIP, node: node };
|
||||||
|
}
|
||||||
|
if (latestUrl) {
|
||||||
|
return { state: 'connecting', url: latestUrl, ip: null, node: null };
|
||||||
|
}
|
||||||
|
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
|
||||||
|
return { state: 'connecting', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
return { state: 'disconnected', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(r) {
|
||||||
|
var dot = document.getElementById('dot');
|
||||||
|
var label = document.getElementById('status-label');
|
||||||
|
var auth = document.getElementById('auth-block');
|
||||||
|
var info = document.getElementById('info-block');
|
||||||
|
|
||||||
|
dot.className = 'dot ' + r.state;
|
||||||
|
|
||||||
|
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
|
||||||
|
label.textContent = labels[r.state];
|
||||||
|
|
||||||
|
// Auth block
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info block
|
||||||
|
if (r.state === 'connected') {
|
||||||
|
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
|
||||||
|
document.getElementById('ts-node').textContent = r.node || '\u2014';
|
||||||
|
info.style.display = '';
|
||||||
|
} else {
|
||||||
|
info.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
// Reload the log iframe (same-origin)
|
||||||
|
var frame = document.getElementById('log-frame');
|
||||||
|
try { frame.contentWindow.location.reload(); } catch(e) {}
|
||||||
|
|
||||||
|
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
|
.then(function(r) { return r.text(); })
|
||||||
|
.then(function(txt) { render(parse(txt)); })
|
||||||
|
.catch(function() {
|
||||||
|
document.getElementById('status-label').textContent = 'Unknown';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 5000);
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -7,12 +7,12 @@
|
|||||||
"vendor": "Tailscale - Packaged by Mo3he",
|
"vendor": "Tailscale - Packaged by Mo3he",
|
||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://www.tailscale.com",
|
||||||
"runMode": "once",
|
"runMode": "respawn",
|
||||||
"version": "1.86.2",
|
"version": "1.96.4",
|
||||||
"architecture": "aarch64"
|
"architecture": "aarch64"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"settingPage": "index.html"
|
"settingPage": "index.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
echo "Starting Service"
|
APP_DIR="/usr/local/packages/Tailscale_VPN"
|
||||||
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
|
STATE_DIR="$APP_DIR/localdata"
|
||||||
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
|
|
||||||
/usr/local/packages/Tailscale_VPN/lib/tailscaled &
|
logger -t "Tailscale_VPN" "Starting Tailscale VPN service (root mode)"
|
||||||
echo "Service Started"
|
|
||||||
echo "Scroll to Bottom for link"
|
mkdir -p "$STATE_DIR"
|
||||||
/usr/local/packages/Tailscale_VPN/lib/tailscale up --accept-routes
|
chmod 755 "$APP_DIR/lib/tailscale"
|
||||||
wait
|
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"
|
||||||
|
"$APP_DIR/lib/tailscaled" \
|
||||||
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
|
--socket="$STATE_DIR/tailscaled.sock" &
|
||||||
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
||||||
|
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes
|
||||||
|
|
||||||
|
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||||
|
wait $TAILSCALED_PID
|
||||||
|
logger -t "Tailscale_VPN" "tailscaled exited"
|
||||||
|
|||||||
@@ -1,19 +1,258 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<body>
|
|
||||||
<head>
|
<head>
|
||||||
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
|
<meta charset="UTF-8">
|
||||||
<script>
|
<title>Tailscale VPN</title>
|
||||||
function refreshIFrame() {
|
<style>
|
||||||
var x = document.getElementById("*Your_iframe_id*");
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
x.contentWindow.location.reload();
|
|
||||||
var t = setTimeout(refreshIFrame, 5000);
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
||||||
|
background: #f5f6f8;
|
||||||
|
color: #1a1a2e;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header svg { flex-shrink: 0; }
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status row ── */
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
|
||||||
|
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
|
||||||
|
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auth URL block ── */
|
||||||
|
#auth-block {
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-block p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #92400e;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link {
|
||||||
|
display: inline-block;
|
||||||
|
background: #0166ff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link:hover { background: #0052cc; }
|
||||||
|
|
||||||
|
#auth-url-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Info grid (connected state) ── */
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 6px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid .label { color: #888; }
|
||||||
|
.info-grid .value { font-weight: 500; font-family: monospace; }
|
||||||
|
|
||||||
|
/* ── Log section ── */
|
||||||
|
.log-header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid #e8e8ec;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body onload="refreshIFrame()">
|
<body>
|
||||||
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
|
|
||||||
</body>
|
<header>
|
||||||
|
<!-- Tailscale wordmark icon -->
|
||||||
|
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="128" height="128" rx="28" fill="#0166FF"/>
|
||||||
|
<circle cx="64" cy="64" r="18" fill="white"/>
|
||||||
|
<circle cx="64" cy="22" r="10" fill="white"/>
|
||||||
|
<circle cx="64" cy="106" r="10" fill="white"/>
|
||||||
|
<circle cx="22" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="106" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="93" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="93" r="10" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Tailscale VPN</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Status card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="status-row">
|
||||||
|
<span id="dot" class="dot connecting"></span>
|
||||||
|
<span id="status-label" class="status-label">Checking…</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when waiting for auth -->
|
||||||
|
<div id="auth-block" style="display:none;">
|
||||||
|
<p>Open this link in a browser to authenticate this device:</p>
|
||||||
|
<a id="auth-link" href="#" target="_blank">Authenticate →</a>
|
||||||
|
<span id="auth-url-text"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when connected -->
|
||||||
|
<div id="info-block" style="display:none;" class="info-grid">
|
||||||
|
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">—</span>
|
||||||
|
<span class="label">Node</span><span class="value" id="ts-node">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="log-header">Service Log</div>
|
||||||
|
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN';
|
||||||
|
|
||||||
|
function parse(txt) {
|
||||||
|
// Use the LAST occurrence of each pattern so stale log entries don't win
|
||||||
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
|
|
||||||
|
// Tailscale IP — 100.x.x.x range
|
||||||
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
|
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
|
|
||||||
|
// Node name from "Logged into tailnet" line or "acap-" username pattern
|
||||||
|
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||||
|
var node = nodeMatch ? nodeMatch[1] : null;
|
||||||
|
|
||||||
|
// Determine state from the LAST state-transition log line
|
||||||
|
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
|
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
return { state: 'connected', url: null, ip: tsIP, node: node };
|
||||||
|
}
|
||||||
|
if (latestUrl) {
|
||||||
|
return { state: 'connecting', url: latestUrl, ip: null, node: null };
|
||||||
|
}
|
||||||
|
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
|
||||||
|
return { state: 'connecting', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
return { state: 'disconnected', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(r) {
|
||||||
|
var dot = document.getElementById('dot');
|
||||||
|
var label = document.getElementById('status-label');
|
||||||
|
var auth = document.getElementById('auth-block');
|
||||||
|
var info = document.getElementById('info-block');
|
||||||
|
|
||||||
|
dot.className = 'dot ' + r.state;
|
||||||
|
|
||||||
|
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
|
||||||
|
label.textContent = labels[r.state];
|
||||||
|
|
||||||
|
// Auth block
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info block
|
||||||
|
if (r.state === 'connected') {
|
||||||
|
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
|
||||||
|
document.getElementById('ts-node').textContent = r.node || '\u2014';
|
||||||
|
info.style.display = '';
|
||||||
|
} else {
|
||||||
|
info.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
// Reload the log iframe (same-origin)
|
||||||
|
var frame = document.getElementById('log-frame');
|
||||||
|
try { frame.contentWindow.location.reload(); } catch(e) {}
|
||||||
|
|
||||||
|
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
|
.then(function(r) { return r.text(); })
|
||||||
|
.then(function(txt) { render(parse(txt)); })
|
||||||
|
.catch(function() {
|
||||||
|
document.getElementById('status-label').textContent = 'Unknown';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 5000);
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -11,12 +11,12 @@
|
|||||||
"username": "root"
|
"username": "root"
|
||||||
},
|
},
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://www.tailscale.com",
|
||||||
"runMode": "once",
|
"runMode": "respawn",
|
||||||
"version": "1.86.2",
|
"version": "1.96.4",
|
||||||
"architecture": "aarch64"
|
"architecture": "aarch64"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"settingPage": "index.html"
|
"settingPage": "index.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
#define APP_NAME "serverconfig"
|
#define APP_NAME "serverconfig"
|
||||||
#define CONFIG_FILE "/usr/local/packages/serverconfig/config.txt"
|
#define APP_DIR "/usr/local/packages/serverconfig"
|
||||||
|
#define STATE_DIR APP_DIR "/localdata"
|
||||||
|
#define CONFIG_FILE STATE_DIR "/config.txt"
|
||||||
#define SCRIPT_PATH "/usr/local/packages/serverconfig/start_tailscale.sh"
|
#define SCRIPT_PATH "/usr/local/packages/serverconfig/start_tailscale.sh"
|
||||||
#define SCRIPT_SOURCE "/usr/local/packages/serverconfig/lib/start_tailscale.sh"
|
#define SCRIPT_SOURCE "/usr/local/packages/serverconfig/lib/start_tailscale.sh"
|
||||||
|
|
||||||
@@ -27,6 +29,19 @@ static gboolean signal_handler(gpointer loop) {
|
|||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create localdata directory
|
||||||
|
static void ensure_localdata_exists(void) {
|
||||||
|
struct stat st = {0};
|
||||||
|
|
||||||
|
if (stat(STATE_DIR, &st) == -1) {
|
||||||
|
if (mkdir(STATE_DIR, 0755) != 0) {
|
||||||
|
syslog(LOG_ERR, "Failed to create localdata directory: %s", strerror(errno));
|
||||||
|
} else {
|
||||||
|
syslog(LOG_INFO, "Created localdata directory: %s", STATE_DIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Copy script from lib folder to main directory
|
// Copy script from lib folder to main directory
|
||||||
static void copy_script_file(void) {
|
static void copy_script_file(void) {
|
||||||
char buffer[4096];
|
char buffer[4096];
|
||||||
@@ -109,6 +124,9 @@ static void update_config_file(AXParameter* handle) {
|
|||||||
gchar* key_value = NULL;
|
gchar* key_value = NULL;
|
||||||
FILE* file;
|
FILE* file;
|
||||||
|
|
||||||
|
// Ensure localdata directory exists
|
||||||
|
ensure_localdata_exists();
|
||||||
|
|
||||||
// Get parameter values
|
// Get parameter values
|
||||||
if (!ax_parameter_get(handle, "CustomServer", &server_value, &error)) {
|
if (!ax_parameter_get(handle, "CustomServer", &server_value, &error)) {
|
||||||
syslog(LOG_ERR, "Failed to get CustomServer: %s",
|
syslog(LOG_ERR, "Failed to get CustomServer: %s",
|
||||||
@@ -125,7 +143,7 @@ static void update_config_file(AXParameter* handle) {
|
|||||||
key_value = g_strdup("");
|
key_value = g_strdup("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to config file
|
// Write to config file in localdata
|
||||||
file = fopen(CONFIG_FILE, "w");
|
file = fopen(CONFIG_FILE, "w");
|
||||||
if (file) {
|
if (file) {
|
||||||
fprintf(file, "custom_server=%s\n", server_value ? server_value : "");
|
fprintf(file, "custom_server=%s\n", server_value ? server_value : "");
|
||||||
@@ -135,12 +153,12 @@ static void update_config_file(AXParameter* handle) {
|
|||||||
// Set permissions to ensure the file is readable
|
// Set permissions to ensure the file is readable
|
||||||
chmod(CONFIG_FILE, 0644);
|
chmod(CONFIG_FILE, 0644);
|
||||||
|
|
||||||
syslog(LOG_INFO, "Updated configuration file: custom_server=%s",
|
syslog(LOG_INFO, "Updated configuration file in local custom_server=%s",
|
||||||
server_value ? server_value : "");
|
server_value ? server_value : "");
|
||||||
syslog(LOG_INFO, "Updated configuration file: auth_key=%s",
|
syslog(LOG_INFO, "Updated configuration file in local auth_key=%s",
|
||||||
key_value && strlen(key_value) > 0 ? "(set)" : "(empty)");
|
key_value && strlen(key_value) > 0 ? "(set)" : "(empty)");
|
||||||
} else {
|
} else {
|
||||||
syslog(LOG_ERR, "Failed to open config file for writing");
|
syslog(LOG_ERR, "Failed to open config file for writing: %s", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
@@ -185,6 +203,9 @@ int main(void) {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure localdata directory exists
|
||||||
|
ensure_localdata_exists();
|
||||||
|
|
||||||
// Ensure script is copied from lib folder
|
// Ensure script is copied from lib folder
|
||||||
copy_script_file();
|
copy_script_file();
|
||||||
|
|
||||||
@@ -229,4 +250,4 @@ int main(void) {
|
|||||||
ax_parameter_free(handle);
|
ax_parameter_free(handle);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,258 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<body>
|
|
||||||
<head>
|
<head>
|
||||||
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
|
<meta charset="UTF-8">
|
||||||
<script>
|
<title>Tailscale VPN</title>
|
||||||
function refreshIFrame() {
|
<style>
|
||||||
var x = document.getElementById("*Your_iframe_id*");
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
x.contentWindow.location.reload();
|
|
||||||
var t = setTimeout(refreshIFrame, 5000);
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
||||||
|
background: #f5f6f8;
|
||||||
|
color: #1a1a2e;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header svg { flex-shrink: 0; }
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status row ── */
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
|
||||||
|
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
|
||||||
|
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auth URL block ── */
|
||||||
|
#auth-block {
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-block p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #92400e;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link {
|
||||||
|
display: inline-block;
|
||||||
|
background: #0166ff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link:hover { background: #0052cc; }
|
||||||
|
|
||||||
|
#auth-url-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Info grid (connected state) ── */
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 6px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid .label { color: #888; }
|
||||||
|
.info-grid .value { font-weight: 500; font-family: monospace; }
|
||||||
|
|
||||||
|
/* ── Log section ── */
|
||||||
|
.log-header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid #e8e8ec;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body onload="refreshIFrame()">
|
<body>
|
||||||
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=serverconfig" style="height:600px;width:100%;border: none;" ></iframe>
|
|
||||||
</body>
|
<header>
|
||||||
|
<!-- Tailscale wordmark icon -->
|
||||||
|
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="128" height="128" rx="28" fill="#0166FF"/>
|
||||||
|
<circle cx="64" cy="64" r="18" fill="white"/>
|
||||||
|
<circle cx="64" cy="22" r="10" fill="white"/>
|
||||||
|
<circle cx="64" cy="106" r="10" fill="white"/>
|
||||||
|
<circle cx="22" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="106" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="93" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="93" r="10" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Tailscale VPN</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Status card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="status-row">
|
||||||
|
<span id="dot" class="dot connecting"></span>
|
||||||
|
<span id="status-label" class="status-label">Checking…</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when waiting for auth -->
|
||||||
|
<div id="auth-block" style="display:none;">
|
||||||
|
<p>Open this link in a browser to authenticate this device:</p>
|
||||||
|
<a id="auth-link" href="#" target="_blank">Authenticate →</a>
|
||||||
|
<span id="auth-url-text"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when connected -->
|
||||||
|
<div id="info-block" style="display:none;" class="info-grid">
|
||||||
|
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">—</span>
|
||||||
|
<span class="label">Node</span><span class="value" id="ts-node">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="log-header">Service Log</div>
|
||||||
|
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=serverconfig"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=serverconfig';
|
||||||
|
|
||||||
|
function parse(txt) {
|
||||||
|
// Use the LAST occurrence of each pattern so stale log entries don't win
|
||||||
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
|
|
||||||
|
// Tailscale IP — 100.x.x.x range
|
||||||
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
|
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
|
|
||||||
|
// Node name from "Logged into tailnet" line or "acap-" username pattern
|
||||||
|
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||||
|
var node = nodeMatch ? nodeMatch[1] : null;
|
||||||
|
|
||||||
|
// Determine state from the LAST state-transition log line
|
||||||
|
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
|
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
return { state: 'connected', url: null, ip: tsIP, node: node };
|
||||||
|
}
|
||||||
|
if (latestUrl) {
|
||||||
|
return { state: 'connecting', url: latestUrl, ip: null, node: null };
|
||||||
|
}
|
||||||
|
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
|
||||||
|
return { state: 'connecting', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
return { state: 'disconnected', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(r) {
|
||||||
|
var dot = document.getElementById('dot');
|
||||||
|
var label = document.getElementById('status-label');
|
||||||
|
var auth = document.getElementById('auth-block');
|
||||||
|
var info = document.getElementById('info-block');
|
||||||
|
|
||||||
|
dot.className = 'dot ' + r.state;
|
||||||
|
|
||||||
|
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
|
||||||
|
label.textContent = labels[r.state];
|
||||||
|
|
||||||
|
// Auth block
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info block
|
||||||
|
if (r.state === 'connected') {
|
||||||
|
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
|
||||||
|
document.getElementById('ts-node').textContent = r.node || '\u2014';
|
||||||
|
info.style.display = '';
|
||||||
|
} else {
|
||||||
|
info.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
// Reload the log iframe (same-origin)
|
||||||
|
var frame = document.getElementById('log-frame');
|
||||||
|
try { frame.contentWindow.location.reload(); } catch(e) {}
|
||||||
|
|
||||||
|
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
|
.then(function(r) { return r.text(); })
|
||||||
|
.then(function(txt) { render(parse(txt)); })
|
||||||
|
.catch(function() {
|
||||||
|
document.getElementById('status-label').textContent = 'Unknown';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 5000);
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,13 +2,18 @@
|
|||||||
# Make sure this script terminates any existing Tailscale processes before starting new ones
|
# Make sure this script terminates any existing Tailscale processes before starting new ones
|
||||||
|
|
||||||
# Kill any existing tailscaled processes
|
# Kill any existing tailscaled processes
|
||||||
pkill -f tailscaled || true
|
killall tailscaled 2>/dev/null || true
|
||||||
|
|
||||||
# Simple script to start Tailscale with custom configuration
|
# Simple script to start Tailscale with custom configuration
|
||||||
CONFIG_FILE="/usr/local/packages/serverconfig/config.txt"
|
APP_DIR="/usr/local/packages/serverconfig"
|
||||||
TAILSCALED_PATH="/usr/local/packages/serverconfig/lib/tailscaled"
|
STATE_DIR="$APP_DIR/localdata"
|
||||||
TAILSCALE_PATH="/usr/local/packages/serverconfig/lib/tailscale"
|
CONFIG_FILE="$STATE_DIR/config.txt"
|
||||||
SOCKET_PATH="/usr/local/packages/serverconfig/tailscaled.sock"
|
TAILSCALED_PATH="$APP_DIR/lib/tailscaled"
|
||||||
|
TAILSCALE_PATH="$APP_DIR/lib/tailscale"
|
||||||
|
SOCKET_PATH="$STATE_DIR/tailscaled.sock"
|
||||||
|
|
||||||
|
# Create localdata directory if it doesn't exist
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
# Log to syslog
|
# Log to syslog
|
||||||
logger -t "tailscale_script" "Starting Tailscale VPN service"
|
logger -t "tailscale_script" "Starting Tailscale VPN service"
|
||||||
@@ -31,9 +36,13 @@ if [ -f "$CONFIG_FILE" ]; then
|
|||||||
done < "$CONFIG_FILE"
|
done < "$CONFIG_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start tailscaled
|
# Start tailscaled with state stored in localdata
|
||||||
logger -t "tailscale_script" "Starting tailscaled daemon"
|
logger -t "tailscale_script" "Starting tailscaled daemon"
|
||||||
$TAILSCALED_PATH --tun=userspace-networking --socket=$SOCKET_PATH &
|
$TAILSCALED_PATH \
|
||||||
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
|
--socket=$SOCKET_PATH \
|
||||||
|
--socks5-server=localhost:1055 \
|
||||||
|
--tun=userspace-networking &
|
||||||
TAILSCALED_PID=$!
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
# Wait for tailscaled to initialize
|
# Wait for tailscaled to initialize
|
||||||
@@ -55,10 +64,18 @@ fi
|
|||||||
# Connect to Tailscale network
|
# Connect to Tailscale network
|
||||||
logger -t "tailscale_script" "Running: $TAILSCALE_CMD"
|
logger -t "tailscale_script" "Running: $TAILSCALE_CMD"
|
||||||
eval $TAILSCALE_CMD
|
eval $TAILSCALE_CMD
|
||||||
|
UP_EXIT=$?
|
||||||
|
|
||||||
|
# Clear auth key from config after first use — Tailscale auth keys are single-use
|
||||||
|
# and the node identity is persisted in tailscaled.state, so the key is no longer needed.
|
||||||
|
if [ -n "$AUTH_KEY" ] && [ "$UP_EXIT" -eq 0 ] && [ -f "$CONFIG_FILE" ]; then
|
||||||
|
logger -t "tailscale_script" "Clearing auth key from config after successful authentication"
|
||||||
|
printf 'custom_server=%s\nauth_key=\n' "$CUSTOM_SERVER" > "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
# Keep the script running to maintain the tailscaled process
|
# Keep the script running to maintain the tailscaled process
|
||||||
logger -t "tailscale_script" "Tailscale VPN is running"
|
logger -t "tailscale_script" "Tailscale VPN is running"
|
||||||
logger -t "tailscale_script" "To change settings, modify parameters in ACAP web interface"
|
logger -t "tailscale_script" "To change settings, modify parameters in ACAP web interface"
|
||||||
|
|
||||||
# Wait for tailscaled process to exit
|
# Wait for tailscaled process to exit
|
||||||
wait $TAILSCALED_PID
|
wait $TAILSCALED_PID
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -8,7 +8,7 @@
|
|||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://www.tailscale.com",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.86.2"
|
"version": "1.96.4"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"settingPage": "index.html",
|
"settingPage": "index.html",
|
||||||
@@ -26,4 +26,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-8
@@ -1,10 +1,30 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
echo "Starting Service"
|
APP_DIR="/usr/local/packages/Tailscale_VPN"
|
||||||
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
|
STATE_DIR="$APP_DIR/localdata"
|
||||||
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
|
|
||||||
/usr/local/packages/Tailscale_VPN/lib/tailscaled --tun=userspace-networking --socket=/usr/local/packages/Tailscale_VPN/tailscaled.sock &
|
logger -t "Tailscale_VPN" "Starting Tailscale VPN service"
|
||||||
echo "Service Started"
|
|
||||||
echo "Scroll to Bottom for link"
|
mkdir -p "$STATE_DIR"
|
||||||
/usr/local/packages/Tailscale_VPN/lib/tailscale --socket=/usr/local/packages/Tailscale_VPN/tailscaled.sock up
|
chmod 755 "$APP_DIR/lib/tailscale"
|
||||||
wait
|
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 (userspace networking)"
|
||||||
|
"$APP_DIR/lib/tailscaled" \
|
||||||
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
|
--socket="$STATE_DIR/tailscaled.sock" \
|
||||||
|
--socks5-server=localhost:1055 \
|
||||||
|
--tun=userspace-networking &
|
||||||
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
||||||
|
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up
|
||||||
|
|
||||||
|
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||||
|
wait $TAILSCALED_PID
|
||||||
|
logger -t "Tailscale_VPN" "tailscaled exited"
|
||||||
|
|||||||
+252
-13
@@ -1,19 +1,258 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<body>
|
|
||||||
<head>
|
<head>
|
||||||
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
|
<meta charset="UTF-8">
|
||||||
<script>
|
<title>Tailscale VPN</title>
|
||||||
function refreshIFrame() {
|
<style>
|
||||||
var x = document.getElementById("*Your_iframe_id*");
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
x.contentWindow.location.reload();
|
|
||||||
var t = setTimeout(refreshIFrame, 5000);
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
||||||
|
background: #f5f6f8;
|
||||||
|
color: #1a1a2e;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header svg { flex-shrink: 0; }
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status row ── */
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
|
||||||
|
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
|
||||||
|
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auth URL block ── */
|
||||||
|
#auth-block {
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-block p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #92400e;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link {
|
||||||
|
display: inline-block;
|
||||||
|
background: #0166ff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link:hover { background: #0052cc; }
|
||||||
|
|
||||||
|
#auth-url-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Info grid (connected state) ── */
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 6px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid .label { color: #888; }
|
||||||
|
.info-grid .value { font-weight: 500; font-family: monospace; }
|
||||||
|
|
||||||
|
/* ── Log section ── */
|
||||||
|
.log-header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid #e8e8ec;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body onload="refreshIFrame()">
|
<body>
|
||||||
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
|
|
||||||
</body>
|
<header>
|
||||||
|
<!-- Tailscale wordmark icon -->
|
||||||
|
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="128" height="128" rx="28" fill="#0166FF"/>
|
||||||
|
<circle cx="64" cy="64" r="18" fill="white"/>
|
||||||
|
<circle cx="64" cy="22" r="10" fill="white"/>
|
||||||
|
<circle cx="64" cy="106" r="10" fill="white"/>
|
||||||
|
<circle cx="22" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="106" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="93" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="93" r="10" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Tailscale VPN</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Status card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="status-row">
|
||||||
|
<span id="dot" class="dot connecting"></span>
|
||||||
|
<span id="status-label" class="status-label">Checking…</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when waiting for auth -->
|
||||||
|
<div id="auth-block" style="display:none;">
|
||||||
|
<p>Open this link in a browser to authenticate this device:</p>
|
||||||
|
<a id="auth-link" href="#" target="_blank">Authenticate →</a>
|
||||||
|
<span id="auth-url-text"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when connected -->
|
||||||
|
<div id="info-block" style="display:none;" class="info-grid">
|
||||||
|
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">—</span>
|
||||||
|
<span class="label">Node</span><span class="value" id="ts-node">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="log-header">Service Log</div>
|
||||||
|
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN';
|
||||||
|
|
||||||
|
function parse(txt) {
|
||||||
|
// Use the LAST occurrence of each pattern so stale log entries don't win
|
||||||
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
|
|
||||||
|
// Tailscale IP — 100.x.x.x range
|
||||||
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
|
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
|
|
||||||
|
// Node name from "Logged into tailnet" line or "acap-" username pattern
|
||||||
|
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||||
|
var node = nodeMatch ? nodeMatch[1] : null;
|
||||||
|
|
||||||
|
// Determine state from the LAST state-transition log line
|
||||||
|
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
|
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
return { state: 'connected', url: null, ip: tsIP, node: node };
|
||||||
|
}
|
||||||
|
if (latestUrl) {
|
||||||
|
return { state: 'connecting', url: latestUrl, ip: null, node: null };
|
||||||
|
}
|
||||||
|
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
|
||||||
|
return { state: 'connecting', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
return { state: 'disconnected', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(r) {
|
||||||
|
var dot = document.getElementById('dot');
|
||||||
|
var label = document.getElementById('status-label');
|
||||||
|
var auth = document.getElementById('auth-block');
|
||||||
|
var info = document.getElementById('info-block');
|
||||||
|
|
||||||
|
dot.className = 'dot ' + r.state;
|
||||||
|
|
||||||
|
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
|
||||||
|
label.textContent = labels[r.state];
|
||||||
|
|
||||||
|
// Auth block
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info block
|
||||||
|
if (r.state === 'connected') {
|
||||||
|
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
|
||||||
|
document.getElementById('ts-node').textContent = r.node || '\u2014';
|
||||||
|
info.style.display = '';
|
||||||
|
} else {
|
||||||
|
info.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
// Reload the log iframe (same-origin)
|
||||||
|
var frame = document.getElementById('log-frame');
|
||||||
|
try { frame.contentWindow.location.reload(); } catch(e) {}
|
||||||
|
|
||||||
|
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
|
.then(function(r) { return r.text(); })
|
||||||
|
.then(function(txt) { render(parse(txt)); })
|
||||||
|
.catch(function() {
|
||||||
|
document.getElementById('status-label').textContent = 'Unknown';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 5000);
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -7,12 +7,12 @@
|
|||||||
"vendor": "Tailscale - Packaged by Mo3he",
|
"vendor": "Tailscale - Packaged by Mo3he",
|
||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://www.tailscale.com",
|
||||||
"runMode": "once",
|
"runMode": "respawn",
|
||||||
"version": "1.86.2",
|
"version": "1.96.4",
|
||||||
"architecture": "armv7hf"
|
"architecture": "armv7hf"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"settingPage": "index.html"
|
"settingPage": "index.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
echo "Starting Service"
|
APP_DIR="/usr/local/packages/Tailscale_VPN"
|
||||||
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscale
|
STATE_DIR="$APP_DIR/localdata"
|
||||||
chmod 777 /usr/local/packages/Tailscale_VPN/lib/tailscaled
|
|
||||||
/usr/local/packages/Tailscale_VPN/lib/tailscaled &
|
logger -t "Tailscale_VPN" "Starting Tailscale VPN service (root mode)"
|
||||||
echo "Service Started"
|
|
||||||
echo "Scroll to Bottom for link"
|
mkdir -p "$STATE_DIR"
|
||||||
/usr/local/packages/Tailscale_VPN/lib/tailscale up --accept-routes
|
chmod 755 "$APP_DIR/lib/tailscale"
|
||||||
wait
|
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"
|
||||||
|
"$APP_DIR/lib/tailscaled" \
|
||||||
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
|
--socket="$STATE_DIR/tailscaled.sock" &
|
||||||
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
||||||
|
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes
|
||||||
|
|
||||||
|
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||||
|
wait $TAILSCALED_PID
|
||||||
|
logger -t "Tailscale_VPN" "tailscaled exited"
|
||||||
|
|||||||
+252
-13
@@ -1,19 +1,258 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<body>
|
|
||||||
<head>
|
<head>
|
||||||
<h1 style="color:red;">Scroll to the bottom to find authentication URL</h1>
|
<meta charset="UTF-8">
|
||||||
<script>
|
<title>Tailscale VPN</title>
|
||||||
function refreshIFrame() {
|
<style>
|
||||||
var x = document.getElementById("*Your_iframe_id*");
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
x.contentWindow.location.reload();
|
|
||||||
var t = setTimeout(refreshIFrame, 5000);
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
||||||
|
background: #f5f6f8;
|
||||||
|
color: #1a1a2e;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header svg { flex-shrink: 0; }
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status row ── */
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
|
||||||
|
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
|
||||||
|
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auth URL block ── */
|
||||||
|
#auth-block {
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-block p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #92400e;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link {
|
||||||
|
display: inline-block;
|
||||||
|
background: #0166ff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link:hover { background: #0052cc; }
|
||||||
|
|
||||||
|
#auth-url-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Info grid (connected state) ── */
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 6px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid .label { color: #888; }
|
||||||
|
.info-grid .value { font-weight: 500; font-family: monospace; }
|
||||||
|
|
||||||
|
/* ── Log section ── */
|
||||||
|
.log-header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid #e8e8ec;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body onload="refreshIFrame()">
|
<body>
|
||||||
<iframe id="*Your_iframe_id*" src= "/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN" style="height:600px;width:100%;border: none;" ></iframe>
|
|
||||||
</body>
|
<header>
|
||||||
|
<!-- Tailscale wordmark icon -->
|
||||||
|
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="128" height="128" rx="28" fill="#0166FF"/>
|
||||||
|
<circle cx="64" cy="64" r="18" fill="white"/>
|
||||||
|
<circle cx="64" cy="22" r="10" fill="white"/>
|
||||||
|
<circle cx="64" cy="106" r="10" fill="white"/>
|
||||||
|
<circle cx="22" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="106" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="93" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="93" r="10" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Tailscale VPN</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Status card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="status-row">
|
||||||
|
<span id="dot" class="dot connecting"></span>
|
||||||
|
<span id="status-label" class="status-label">Checking…</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when waiting for auth -->
|
||||||
|
<div id="auth-block" style="display:none;">
|
||||||
|
<p>Open this link in a browser to authenticate this device:</p>
|
||||||
|
<a id="auth-link" href="#" target="_blank">Authenticate →</a>
|
||||||
|
<span id="auth-url-text"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when connected -->
|
||||||
|
<div id="info-block" style="display:none;" class="info-grid">
|
||||||
|
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">—</span>
|
||||||
|
<span class="label">Node</span><span class="value" id="ts-node">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="log-header">Service Log</div>
|
||||||
|
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=Tailscale_VPN';
|
||||||
|
|
||||||
|
function parse(txt) {
|
||||||
|
// Use the LAST occurrence of each pattern so stale log entries don't win
|
||||||
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
|
|
||||||
|
// Tailscale IP — 100.x.x.x range
|
||||||
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
|
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
|
|
||||||
|
// Node name from "Logged into tailnet" line or "acap-" username pattern
|
||||||
|
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||||
|
var node = nodeMatch ? nodeMatch[1] : null;
|
||||||
|
|
||||||
|
// Determine state from the LAST state-transition log line
|
||||||
|
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
|
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
return { state: 'connected', url: null, ip: tsIP, node: node };
|
||||||
|
}
|
||||||
|
if (latestUrl) {
|
||||||
|
return { state: 'connecting', url: latestUrl, ip: null, node: null };
|
||||||
|
}
|
||||||
|
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
|
||||||
|
return { state: 'connecting', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
return { state: 'disconnected', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(r) {
|
||||||
|
var dot = document.getElementById('dot');
|
||||||
|
var label = document.getElementById('status-label');
|
||||||
|
var auth = document.getElementById('auth-block');
|
||||||
|
var info = document.getElementById('info-block');
|
||||||
|
|
||||||
|
dot.className = 'dot ' + r.state;
|
||||||
|
|
||||||
|
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
|
||||||
|
label.textContent = labels[r.state];
|
||||||
|
|
||||||
|
// Auth block
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info block
|
||||||
|
if (r.state === 'connected') {
|
||||||
|
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
|
||||||
|
document.getElementById('ts-node').textContent = r.node || '\u2014';
|
||||||
|
info.style.display = '';
|
||||||
|
} else {
|
||||||
|
info.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
// Reload the log iframe (same-origin)
|
||||||
|
var frame = document.getElementById('log-frame');
|
||||||
|
try { frame.contentWindow.location.reload(); } catch(e) {}
|
||||||
|
|
||||||
|
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
|
.then(function(r) { return r.text(); })
|
||||||
|
.then(function(txt) { render(parse(txt)); })
|
||||||
|
.catch(function() {
|
||||||
|
document.getElementById('status-label').textContent = 'Unknown';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 5000);
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -11,12 +11,12 @@
|
|||||||
"username": "root"
|
"username": "root"
|
||||||
},
|
},
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://www.tailscale.com",
|
||||||
"runMode": "once",
|
"runMode": "respawn",
|
||||||
"version": "1.86.2",
|
"version": "1.96.4",
|
||||||
"architecture": "armv7hf"
|
"architecture": "armv7hf"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"settingPage": "index.html"
|
"settingPage": "index.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
ARG ARCH=armv7hf
|
||||||
|
ARG VERSION=12.3.0
|
||||||
|
ARG UBUNTU_VERSION=24.04
|
||||||
|
ARG REPO=axisecp
|
||||||
|
ARG SDK=acap-native-sdk
|
||||||
|
|
||||||
|
FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION}
|
||||||
|
|
||||||
|
# Building the ACAP application
|
||||||
|
COPY ./app /opt/app/
|
||||||
|
WORKDIR /opt/app
|
||||||
|
RUN . /opt/axis/acapsdk/environment-setup* && acap-build .
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
PROGS = serverconfig
|
||||||
|
SRCS = config_updater.c
|
||||||
|
OBJS = $(SRCS:.c=.o)
|
||||||
|
|
||||||
|
PKGS = glib-2.0 gio-2.0 axparameter
|
||||||
|
|
||||||
|
CFLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --cflags $(PKGS))
|
||||||
|
LDLIBS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --libs $(PKGS))
|
||||||
|
|
||||||
|
CFLAGS += -Wall \
|
||||||
|
-Wextra \
|
||||||
|
-Wformat=2 \
|
||||||
|
-Wpointer-arith \
|
||||||
|
-Wbad-function-cast \
|
||||||
|
-Wstrict-prototypes \
|
||||||
|
-Wmissing-prototypes \
|
||||||
|
-Winline \
|
||||||
|
-Wdisabled-optimization \
|
||||||
|
-Wfloat-equal \
|
||||||
|
-W \
|
||||||
|
-Werror
|
||||||
|
|
||||||
|
all: $(PROGS)
|
||||||
|
|
||||||
|
$(PROGS): $(OBJS)
|
||||||
|
$(CC) $(LDFLAGS) $^ $(LIBS) $(LDLIBS) -o $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(PROGS) *.o *.eap* *_LICENSE.txt package.conf* param.conf tmp*
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* Simple file-based configuration updater for Tailscale
|
||||||
|
* This avoids the AXParameter system and just writes directly to a config file
|
||||||
|
*/
|
||||||
|
#include <axsdk/axparameter.h>
|
||||||
|
#include <glib-unix.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>
|
||||||
|
|
||||||
|
#define APP_NAME "serverconfig"
|
||||||
|
#define APP_DIR "/usr/local/packages/serverconfig"
|
||||||
|
#define STATE_DIR APP_DIR "/localdata"
|
||||||
|
#define CONFIG_FILE STATE_DIR "/config.txt"
|
||||||
|
#define SCRIPT_PATH "/usr/local/packages/serverconfig/start_tailscale.sh"
|
||||||
|
#define SCRIPT_SOURCE "/usr/local/packages/serverconfig/lib/start_tailscale.sh"
|
||||||
|
|
||||||
|
static gboolean signal_handler(gpointer loop) {
|
||||||
|
g_main_loop_quit((GMainLoop*)loop);
|
||||||
|
syslog(LOG_INFO, "Configuration updater stopping.");
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create localdata directory
|
||||||
|
static void ensure_localdata_exists(void) {
|
||||||
|
struct stat st = {0};
|
||||||
|
|
||||||
|
if (stat(STATE_DIR, &st) == -1) {
|
||||||
|
if (mkdir(STATE_DIR, 0755) != 0) {
|
||||||
|
syslog(LOG_ERR, "Failed to create localdata directory: %s", strerror(errno));
|
||||||
|
} else {
|
||||||
|
syslog(LOG_INFO, "Created localdata directory: %s", STATE_DIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy script from lib folder to main directory
|
||||||
|
static void copy_script_file(void) {
|
||||||
|
char buffer[4096];
|
||||||
|
ssize_t bytes_read, bytes_written;
|
||||||
|
int source_fd, dest_fd;
|
||||||
|
|
||||||
|
syslog(LOG_INFO, "Copying script from %s to %s", SCRIPT_SOURCE, SCRIPT_PATH);
|
||||||
|
|
||||||
|
// Open source file
|
||||||
|
source_fd = open(SCRIPT_SOURCE, O_RDONLY);
|
||||||
|
if (source_fd < 0) {
|
||||||
|
syslog(LOG_ERR, "Failed to open source script: %s", strerror(errno));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open destination file (create if doesn't exist, truncate if exists)
|
||||||
|
dest_fd = open(SCRIPT_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0755);
|
||||||
|
if (dest_fd < 0) {
|
||||||
|
syslog(LOG_ERR, "Failed to open destination script: %s", strerror(errno));
|
||||||
|
close(source_fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file
|
||||||
|
while ((bytes_read = read(source_fd, buffer, sizeof(buffer))) > 0) {
|
||||||
|
bytes_written = write(dest_fd, buffer, bytes_read);
|
||||||
|
if (bytes_written != bytes_read) {
|
||||||
|
syslog(LOG_ERR, "Error writing to destination file: %s", strerror(errno));
|
||||||
|
close(source_fd);
|
||||||
|
close(dest_fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close file descriptors
|
||||||
|
close(source_fd);
|
||||||
|
close(dest_fd);
|
||||||
|
|
||||||
|
// Make the script executable
|
||||||
|
if (chmod(SCRIPT_PATH, 0755) != 0) {
|
||||||
|
syslog(LOG_ERR, "Failed to make script executable: %s", strerror(errno));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syslog(LOG_INFO, "Script copied and made executable successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the Tailscale script
|
||||||
|
static void start_tailscale(void) {
|
||||||
|
syslog(LOG_INFO, "Starting Tailscale VPN script");
|
||||||
|
|
||||||
|
// Check if script exists, if not, copy it
|
||||||
|
struct stat st;
|
||||||
|
if (stat(SCRIPT_PATH, &st) != 0) {
|
||||||
|
syslog(LOG_INFO, "Script not found at %s, copying from lib folder", SCRIPT_PATH);
|
||||||
|
copy_script_file();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fork and execute the script
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
syslog(LOG_ERR, "Failed to fork for Tailscale script: %s", strerror(errno));
|
||||||
|
return;
|
||||||
|
} else if (pid == 0) {
|
||||||
|
// Child process - execute the script
|
||||||
|
execl(SCRIPT_PATH, "start_tailscale.sh", NULL);
|
||||||
|
|
||||||
|
// If we get here, execl failed
|
||||||
|
syslog(LOG_ERR, "Failed to execute Tailscale script: %s", strerror(errno));
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
syslog(LOG_INFO, "Tailscale script started with PID: %d", pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the configuration file with current parameter values
|
||||||
|
static void update_config_file(AXParameter* handle) {
|
||||||
|
GError* error = NULL;
|
||||||
|
gchar* server_value = NULL;
|
||||||
|
gchar* key_value = NULL;
|
||||||
|
FILE* file;
|
||||||
|
|
||||||
|
// Ensure localdata directory exists
|
||||||
|
ensure_localdata_exists();
|
||||||
|
|
||||||
|
// Get parameter values
|
||||||
|
if (!ax_parameter_get(handle, "CustomServer", &server_value, &error)) {
|
||||||
|
syslog(LOG_ERR, "Failed to get CustomServer: %s",
|
||||||
|
error ? error->message : "unknown error");
|
||||||
|
if (error) g_error_free(error);
|
||||||
|
error = NULL;
|
||||||
|
server_value = g_strdup("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ax_parameter_get(handle, "AuthKey", &key_value, &error)) {
|
||||||
|
syslog(LOG_ERR, "Failed to get AuthKey: %s",
|
||||||
|
error ? error->message : "unknown error");
|
||||||
|
if (error) g_error_free(error);
|
||||||
|
key_value = g_strdup("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to config file in localdata
|
||||||
|
file = fopen(CONFIG_FILE, "w");
|
||||||
|
if (file) {
|
||||||
|
fprintf(file, "custom_server=%s\n", server_value ? server_value : "");
|
||||||
|
fprintf(file, "auth_key=%s\n", key_value ? key_value : "");
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
// Set permissions to ensure the file is readable
|
||||||
|
chmod(CONFIG_FILE, 0644);
|
||||||
|
|
||||||
|
syslog(LOG_INFO, "Updated configuration file in local custom_server=%s",
|
||||||
|
server_value ? server_value : "");
|
||||||
|
syslog(LOG_INFO, "Updated configuration file in local auth_key=%s",
|
||||||
|
key_value && strlen(key_value) > 0 ? "(set)" : "(empty)");
|
||||||
|
} else {
|
||||||
|
syslog(LOG_ERR, "Failed to open config file for writing: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
g_free(server_value);
|
||||||
|
g_free(key_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle parameter changes
|
||||||
|
static void parameter_changed(const gchar* name, const gchar* value, gpointer handle_void_ptr) {
|
||||||
|
AXParameter* handle = handle_void_ptr;
|
||||||
|
|
||||||
|
// Extract simple parameter name from the fully qualified name
|
||||||
|
const char* simple_name = name;
|
||||||
|
const char* prefix = "root." APP_NAME ".";
|
||||||
|
if (strncmp(name, prefix, strlen(prefix)) == 0) {
|
||||||
|
simple_name = name + strlen(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
syslog(LOG_INFO, "Parameter changed: %s = %s", simple_name, value);
|
||||||
|
|
||||||
|
// Update config file whenever any parameter changes
|
||||||
|
update_config_file(handle);
|
||||||
|
|
||||||
|
// Restart Tailscale to apply the new settings
|
||||||
|
start_tailscale();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
GError* error = NULL;
|
||||||
|
GMainLoop* loop = NULL;
|
||||||
|
|
||||||
|
// Open syslog for logging
|
||||||
|
openlog(APP_NAME, LOG_PID, LOG_USER);
|
||||||
|
syslog(LOG_INFO, "Config updater starting");
|
||||||
|
|
||||||
|
// Initialize parameter handling
|
||||||
|
AXParameter* handle = ax_parameter_new(APP_NAME, &error);
|
||||||
|
if (handle == NULL) {
|
||||||
|
syslog(LOG_ERR, "Failed to initialize parameters: %s",
|
||||||
|
error ? error->message : "unknown error");
|
||||||
|
if (error) g_error_free(error);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure localdata directory exists
|
||||||
|
ensure_localdata_exists();
|
||||||
|
|
||||||
|
// Ensure script is copied from lib folder
|
||||||
|
copy_script_file();
|
||||||
|
|
||||||
|
// Create initial config file
|
||||||
|
update_config_file(handle);
|
||||||
|
|
||||||
|
// Start Tailscale VPN script
|
||||||
|
start_tailscale();
|
||||||
|
|
||||||
|
// Register for parameter changes
|
||||||
|
if (!ax_parameter_register_callback(handle, "CustomServer", parameter_changed, handle, &error)) {
|
||||||
|
syslog(LOG_ERR, "Failed to register CustomServer callback: %s",
|
||||||
|
error ? error->message : "unknown error");
|
||||||
|
if (error) g_error_free(error);
|
||||||
|
error = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ax_parameter_register_callback(handle, "AuthKey", parameter_changed, handle, &error)) {
|
||||||
|
syslog(LOG_ERR, "Failed to register AuthKey callback: %s",
|
||||||
|
error ? error->message : "unknown error");
|
||||||
|
if (error) g_error_free(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register for parameter changes with fully qualified names as fallback
|
||||||
|
if (!ax_parameter_register_callback(handle, "root." APP_NAME ".CustomServer", parameter_changed, handle, NULL)) {
|
||||||
|
syslog(LOG_INFO, "Fallback CustomServer registration failed (this may be normal)");
|
||||||
|
}
|
||||||
|
if (!ax_parameter_register_callback(handle, "root." APP_NAME ".AuthKey", parameter_changed, handle, NULL)) {
|
||||||
|
syslog(LOG_INFO, "Fallback AuthKey registration failed (this may be normal)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up main loop
|
||||||
|
loop = g_main_loop_new(NULL, FALSE);
|
||||||
|
g_unix_signal_add(SIGTERM, signal_handler, loop);
|
||||||
|
g_unix_signal_add(SIGINT, signal_handler, loop);
|
||||||
|
|
||||||
|
syslog(LOG_INFO, "Config updater running. Waiting for parameter changes...");
|
||||||
|
g_main_loop_run(loop);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
g_main_loop_unref(loop);
|
||||||
|
ax_parameter_free(handle);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Tailscale VPN</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
||||||
|
background: #f5f6f8;
|
||||||
|
color: #1a1a2e;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header svg { flex-shrink: 0; }
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status row ── */
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.connected { background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2); }
|
||||||
|
.dot.connecting { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,.2); }
|
||||||
|
.dot.disconnected { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.2); }
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auth URL block ── */
|
||||||
|
#auth-block {
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-block p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #92400e;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link {
|
||||||
|
display: inline-block;
|
||||||
|
background: #0166ff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-link:hover { background: #0052cc; }
|
||||||
|
|
||||||
|
#auth-url-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Info grid (connected state) ── */
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 6px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid .label { color: #888; }
|
||||||
|
.info-grid .value { font-weight: 500; font-family: monospace; }
|
||||||
|
|
||||||
|
/* ── Log section ── */
|
||||||
|
.log-header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid #e8e8ec;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<!-- Tailscale wordmark icon -->
|
||||||
|
<svg width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="128" height="128" rx="28" fill="#0166FF"/>
|
||||||
|
<circle cx="64" cy="64" r="18" fill="white"/>
|
||||||
|
<circle cx="64" cy="22" r="10" fill="white"/>
|
||||||
|
<circle cx="64" cy="106" r="10" fill="white"/>
|
||||||
|
<circle cx="22" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="106" cy="64" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="35" r="10" fill="white"/>
|
||||||
|
<circle cx="35" cy="93" r="10" fill="white"/>
|
||||||
|
<circle cx="93" cy="93" r="10" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Tailscale VPN</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Status card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="status-row">
|
||||||
|
<span id="dot" class="dot connecting"></span>
|
||||||
|
<span id="status-label" class="status-label">Checking…</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when waiting for auth -->
|
||||||
|
<div id="auth-block" style="display:none;">
|
||||||
|
<p>Open this link in a browser to authenticate this device:</p>
|
||||||
|
<a id="auth-link" href="#" target="_blank">Authenticate →</a>
|
||||||
|
<span id="auth-url-text"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown when connected -->
|
||||||
|
<div id="info-block" style="display:none;" class="info-grid">
|
||||||
|
<span class="label">Tailscale IP</span><span class="value" id="ts-ip">—</span>
|
||||||
|
<span class="label">Node</span><span class="value" id="ts-node">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="log-header">Service Log</div>
|
||||||
|
<iframe id="log-frame" src="/axis-cgi/admin/systemlog.cgi?appname=serverconfig"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var LOG = '/axis-cgi/admin/systemlog.cgi?appname=serverconfig';
|
||||||
|
|
||||||
|
function parse(txt) {
|
||||||
|
// Use the LAST occurrence of each pattern so stale log entries don't win
|
||||||
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
|
|
||||||
|
// Tailscale IP — 100.x.x.x range
|
||||||
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
|
var tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
|
|
||||||
|
// Node name from "Logged into tailnet" line or "acap-" username pattern
|
||||||
|
var nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||||
|
var node = nodeMatch ? nodeMatch[1] : null;
|
||||||
|
|
||||||
|
// Determine state from the LAST state-transition log line
|
||||||
|
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
|
var isRunning = /-> Running/.test(lastState) || /Logged into tailnet/.test(txt);
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
return { state: 'connected', url: null, ip: tsIP, node: node };
|
||||||
|
}
|
||||||
|
if (latestUrl) {
|
||||||
|
return { state: 'connecting', url: latestUrl, ip: null, node: null };
|
||||||
|
}
|
||||||
|
if (/Starting Tailscale|tailscaled.*start|logtail started/.test(txt)) {
|
||||||
|
return { state: 'connecting', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
return { state: 'disconnected', url: null, ip: null, node: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(r) {
|
||||||
|
var dot = document.getElementById('dot');
|
||||||
|
var label = document.getElementById('status-label');
|
||||||
|
var auth = document.getElementById('auth-block');
|
||||||
|
var info = document.getElementById('info-block');
|
||||||
|
|
||||||
|
dot.className = 'dot ' + r.state;
|
||||||
|
|
||||||
|
var labels = { connected: 'Connected', connecting: 'Connecting\u2026', disconnected: 'Stopped' };
|
||||||
|
label.textContent = labels[r.state];
|
||||||
|
|
||||||
|
// Auth block
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info block
|
||||||
|
if (r.state === 'connected') {
|
||||||
|
document.getElementById('ts-ip').textContent = r.ip || '\u2014';
|
||||||
|
document.getElementById('ts-node').textContent = r.node || '\u2014';
|
||||||
|
info.style.display = '';
|
||||||
|
} else {
|
||||||
|
info.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
// Reload the log iframe (same-origin)
|
||||||
|
var frame = document.getElementById('log-frame');
|
||||||
|
try { frame.contentWindow.location.reload(); } catch(e) {}
|
||||||
|
|
||||||
|
fetch(LOG, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
|
.then(function(r) { return r.text(); })
|
||||||
|
.then(function(txt) { render(parse(txt)); })
|
||||||
|
.catch(function() {
|
||||||
|
document.getElementById('status-label').textContent = 'Unknown';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 5000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Make sure this script terminates any existing Tailscale processes before starting new ones
|
||||||
|
|
||||||
|
# Kill any existing tailscaled processes
|
||||||
|
killall tailscaled 2>/dev/null || true
|
||||||
|
|
||||||
|
# Simple script to start Tailscale with custom configuration
|
||||||
|
APP_DIR="/usr/local/packages/serverconfig"
|
||||||
|
STATE_DIR="$APP_DIR/localdata"
|
||||||
|
CONFIG_FILE="$STATE_DIR/config.txt"
|
||||||
|
TAILSCALED_PATH="$APP_DIR/lib/tailscaled"
|
||||||
|
TAILSCALE_PATH="$APP_DIR/lib/tailscale"
|
||||||
|
SOCKET_PATH="$STATE_DIR/tailscaled.sock"
|
||||||
|
|
||||||
|
# Create localdata directory if it doesn't exist
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
# Log to syslog
|
||||||
|
logger -t "tailscale_script" "Starting Tailscale VPN service"
|
||||||
|
|
||||||
|
# Set execute permissions
|
||||||
|
chmod 755 $TAILSCALED_PATH
|
||||||
|
chmod 755 $TAILSCALE_PATH
|
||||||
|
|
||||||
|
# Read configuration (if exists)
|
||||||
|
CUSTOM_SERVER=""
|
||||||
|
AUTH_KEY=""
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
logger -t "tailscale_script" "Reading configuration from $CONFIG_FILE"
|
||||||
|
# Read values from config file
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
case "$key" in
|
||||||
|
"custom_server") CUSTOM_SERVER="$value" ;;
|
||||||
|
"auth_key") AUTH_KEY="$value" ;;
|
||||||
|
esac
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start tailscaled with state stored in localdata
|
||||||
|
logger -t "tailscale_script" "Starting tailscaled daemon"
|
||||||
|
$TAILSCALED_PATH \
|
||||||
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
|
--socket=$SOCKET_PATH \
|
||||||
|
--socks5-server=localhost:1055 \
|
||||||
|
--tun=userspace-networking &
|
||||||
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
|
# Wait for tailscaled to initialize
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Build up the command based on available parameters
|
||||||
|
TAILSCALE_CMD="$TAILSCALE_PATH --socket=$SOCKET_PATH up"
|
||||||
|
|
||||||
|
if [ -n "$CUSTOM_SERVER" ]; then
|
||||||
|
logger -t "tailscale_script" "Using custom server: $CUSTOM_SERVER"
|
||||||
|
TAILSCALE_CMD="$TAILSCALE_CMD --login-server $CUSTOM_SERVER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$AUTH_KEY" ]; then
|
||||||
|
logger -t "tailscale_script" "Using authentication key"
|
||||||
|
TAILSCALE_CMD="$TAILSCALE_CMD --authkey $AUTH_KEY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Connect to Tailscale network
|
||||||
|
logger -t "tailscale_script" "Running: $TAILSCALE_CMD"
|
||||||
|
eval $TAILSCALE_CMD
|
||||||
|
UP_EXIT=$?
|
||||||
|
|
||||||
|
# Clear auth key from config after first use — Tailscale auth keys are single-use
|
||||||
|
# and the node identity is persisted in tailscaled.state, so the key is no longer needed.
|
||||||
|
if [ -n "$AUTH_KEY" ] && [ "$UP_EXIT" -eq 0 ] && [ -f "$CONFIG_FILE" ]; then
|
||||||
|
logger -t "tailscale_script" "Clearing auth key from config after successful authentication"
|
||||||
|
printf 'custom_server=%s\nauth_key=\n' "$CUSTOM_SERVER" > "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep the script running to maintain the tailscaled process
|
||||||
|
logger -t "tailscale_script" "Tailscale VPN is running"
|
||||||
|
logger -t "tailscale_script" "To change settings, modify parameters in ACAP web interface"
|
||||||
|
|
||||||
|
# Wait for tailscaled process to exit
|
||||||
|
wait $TAILSCALED_PID
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.7.3",
|
||||||
|
"acapPackageConf": {
|
||||||
|
"setup": {
|
||||||
|
"friendlyName": "Tailscale VPN",
|
||||||
|
"appName": "serverconfig",
|
||||||
|
"vendor": "Tailscale - Packaged by Mo3he",
|
||||||
|
"embeddedSdkVersion": "3.0",
|
||||||
|
"vendorUrl": "https://www.tailscale.com",
|
||||||
|
"runMode": "respawn",
|
||||||
|
"version": "1.96.4",
|
||||||
|
"architecture": "armv7hf"
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"settingPage": "index.html",
|
||||||
|
"paramConfig": [
|
||||||
|
{
|
||||||
|
"name": "CustomServer",
|
||||||
|
"default": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AuthKey",
|
||||||
|
"default": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user