mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 21:13:56 +08:00
🗽 Sync 2026-09-29 04:49:38
This commit is contained in:
1 parent
1ba92ebff1
commit
cec9f6bff2
87 files changed
+25281
No files matched your search
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Tailscale table 52 route recovery via tailscaled restart.
|
||||
#
|
||||
# Trigger: netifd brings tailscale0 back up (ifup) after an external
|
||||
# interface rebuild (e.g. network reload, firewall reload).
|
||||
#
|
||||
# tailscaled maintains an in-memory route state that diverges from the
|
||||
# kernel after netifd flushes tailscale0 routes. The only reliable
|
||||
# fix is a full tailscaled restart, which re-initialises from a clean
|
||||
# state and re-applies all table 52 routes from the current NetworkMap.
|
||||
#
|
||||
# Atomicity: a timestamp lock file (30 s TTL) prevents the recursive
|
||||
# loop that would otherwise arise because the restart itself causes
|
||||
# tailscale0 to be torn down and rebuilt, firing another ifup event.
|
||||
|
||||
[ "$DEVICE" = "tailscale0" ] || exit 0
|
||||
[ "$ACTION" = "ifup" ] || exit 0
|
||||
|
||||
# Check if Tailscale service is enabled
|
||||
service_enabled=$(uci -q get tailscale.settings.service_enabled 2>/dev/null || echo 0)
|
||||
[ "$service_enabled" = "1" ] || exit 0
|
||||
|
||||
LOCK=/tmp/tailscale_hotplug.lock
|
||||
|
||||
# Timestamp-based lock: bail out if another restart fired within 30 s.
|
||||
if [ -f "$LOCK" ]; then
|
||||
ts=$(cat "$LOCK" 2>/dev/null)
|
||||
[ -n "$ts" ] && [ $(( $(date +%s) - ts )) -lt 30 ] && exit 0
|
||||
fi
|
||||
|
||||
# Skip if tailscaled is not yet ready (normal first ifup during boot).
|
||||
tailscale status >/dev/null 2>&1 || exit 0
|
||||
|
||||
# Record lock timestamp only after confirming service readiness
|
||||
date +%s > "$LOCK"
|
||||
|
||||
logger -t tailscale-hotplug \
|
||||
"tailscale0 ifup: restarting tailscaled to restore table 52 routes"
|
||||
|
||||
/etc/init.d/tailscale restart
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
STOP=10
|
||||
|
||||
NAME=tailscale-settings
|
||||
EXTRA_COMMANDS="clean_firewall clean_firewall_rules"
|
||||
|
||||
# Compare version strings: return 0 (true) if $1 >= $2
|
||||
version_gte() {
|
||||
awk -v a="${1%%-*}" -v b="${2%%-*}" 'BEGIN {
|
||||
split(a, av, ".")
|
||||
split(b, bv, ".")
|
||||
for (i=1; i<=3; i++) {
|
||||
ai = int(av[i]+0); bi = int(bv[i]+0)
|
||||
if (ai > bi) { exit 0 }
|
||||
if (ai < bi) { exit 1 }
|
||||
}
|
||||
exit 0
|
||||
}'
|
||||
return $?
|
||||
}
|
||||
|
||||
# Add/remove DNS forward entry in dnsmasq for MagicDNS via OpenWrt
|
||||
setup_tailscale_dns_forward() {
|
||||
if [ ! -x /usr/sbin/tailscale ] && [ ! -x /usr/bin/tailscale ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local entry="/.ts.net/100.100.100.100"
|
||||
local count
|
||||
count=$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null | tr ' ' '\n' | grep -Fxc "$entry" 2>/dev/null)
|
||||
|
||||
if [ "$count" -gt 1 ]; then
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="$entry"
|
||||
uci add_list dhcp.@dnsmasq[0].server="$entry"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq reload 2>/dev/null || /etc/init.d/dnsmasq restart 2>/dev/null
|
||||
logger -t "$NAME" "Collapsed duplicate DNS forward entries"
|
||||
elif [ "$count" -eq 0 ]; then
|
||||
uci add_list dhcp.@dnsmasq[0].server="$entry"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq reload 2>/dev/null || /etc/init.d/dnsmasq restart 2>/dev/null
|
||||
logger -t "$NAME" "Added DNS forward: $entry"
|
||||
fi
|
||||
}
|
||||
|
||||
remove_tailscale_dns_forward() {
|
||||
local entry="/.ts.net/100.100.100.100"
|
||||
local idx=0
|
||||
local removed=0
|
||||
while true; do
|
||||
local val
|
||||
val=$(uci -q get dhcp.@dnsmasq[0].server."${idx}" 2>/dev/null)
|
||||
[ -z "$val" ] && break
|
||||
if [ "$val" = "$entry" ]; then
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="$entry"
|
||||
removed=1
|
||||
break
|
||||
fi
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
if [ "$removed" = "1" ]; then
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq reload 2>/dev/null || /etc/init.d/dnsmasq restart 2>/dev/null
|
||||
logger -t "$NAME" "Removed Tailscale DNS forward entry: $entry"
|
||||
fi
|
||||
}
|
||||
|
||||
check_zone() {
|
||||
local cfg="$1"
|
||||
local name
|
||||
config_get name "$cfg" name
|
||||
if [ "$name" = "tailscale" ]; then
|
||||
FOUND_TS_ZONE="$cfg"
|
||||
fi
|
||||
}
|
||||
|
||||
check_forwarding() {
|
||||
local cfg="$1"
|
||||
local src dst
|
||||
config_get src "$cfg" src
|
||||
config_get dst "$cfg" dest
|
||||
[ "$src" = "lan" ] && [ "$dst" = "tailscale" ] && FWD_LAN_TS=1
|
||||
[ "$src" = "tailscale" ] && [ "$dst" = "lan" ] && FWD_TS_LAN=1
|
||||
[ "$src" = "tailscale" ] && [ "$dst" = "wan" ] && FWD_TS_WAN=1
|
||||
}
|
||||
|
||||
setup_firewall_rules() {
|
||||
local changed_net=0
|
||||
local changed_fw=0
|
||||
|
||||
# 1. Interface (set both device and ifname for broad OpenWrt version compatibility)
|
||||
local dev ifname
|
||||
dev=$(uci -q get network.tailscale.device)
|
||||
ifname=$(uci -q get network.tailscale.device)
|
||||
|
||||
if [ -z "$dev" ] && [ -z "$ifname" ]; then
|
||||
uci -q set network.tailscale=interface
|
||||
uci -q set network.tailscale.proto='none'
|
||||
uci -q set network.tailscale.device='tailscale0'
|
||||
uci -q set network.tailscale.device='tailscale0'
|
||||
changed_net=1
|
||||
else
|
||||
if [ -z "$dev" ]; then
|
||||
uci -q set network.tailscale.device='tailscale0'
|
||||
changed_net=1
|
||||
fi
|
||||
if [ -z "$ifname" ]; then
|
||||
uci -q set network.tailscale.device='tailscale0'
|
||||
changed_net=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Zone
|
||||
local tunnel_snat target_masq="0"
|
||||
tunnel_snat=$(uci -q get tailscale.settings.tunnel_snat)
|
||||
[ "$tunnel_snat" = "1" ] && target_masq="1"
|
||||
|
||||
config_load firewall
|
||||
FOUND_TS_ZONE=""
|
||||
config_foreach check_zone zone
|
||||
|
||||
if [ -z "$FOUND_TS_ZONE" ]; then
|
||||
local zid
|
||||
zid=$(uci -q add firewall zone)
|
||||
uci -q set firewall."$zid".name='tailscale'
|
||||
uci -q set firewall."$zid".input='ACCEPT'
|
||||
uci -q set firewall."$zid".output='ACCEPT'
|
||||
uci -q set firewall."$zid".forward='ACCEPT'
|
||||
uci -q set firewall."$zid".masq="$target_masq"
|
||||
uci -q set firewall."$zid".mtu_fix='1'
|
||||
uci -q add_list firewall."$zid".network='tailscale'
|
||||
changed_fw=1
|
||||
else
|
||||
local cur_masq
|
||||
cur_masq=$(uci -q get firewall."$FOUND_TS_ZONE".masq)
|
||||
if [ "$cur_masq" != "$target_masq" ]; then
|
||||
uci -q set firewall."$FOUND_TS_ZONE".masq="$target_masq"
|
||||
changed_fw=1
|
||||
fi
|
||||
|
||||
local has_net=0
|
||||
local net
|
||||
for net in $(uci -q get firewall."$FOUND_TS_ZONE".network); do
|
||||
[ "$net" = "tailscale" ] && has_net=1
|
||||
done
|
||||
if [ "$has_net" = "0" ]; then
|
||||
uci -q add_list firewall."$FOUND_TS_ZONE".network='tailscale'
|
||||
changed_fw=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Forwardings
|
||||
FWD_LAN_TS=0
|
||||
FWD_TS_LAN=0
|
||||
FWD_TS_WAN=0
|
||||
config_foreach check_forwarding forwarding
|
||||
|
||||
if [ "$FWD_LAN_TS" = "0" ]; then
|
||||
local fid
|
||||
fid=$(uci -q add firewall forwarding)
|
||||
uci -q set firewall."$fid".src='lan'
|
||||
uci -q set firewall."$fid".dest='tailscale'
|
||||
changed_fw=1
|
||||
fi
|
||||
if [ "$FWD_TS_LAN" = "0" ]; then
|
||||
local fid=$(uci -q add firewall forwarding)
|
||||
uci -q set firewall."$fid".src='tailscale'
|
||||
uci -q set firewall."$fid".dest='lan'
|
||||
changed_fw=1
|
||||
fi
|
||||
if [ "$FWD_TS_WAN" = "0" ]; then
|
||||
local fid=$(uci -q add firewall forwarding)
|
||||
uci -q set firewall."$fid".src='tailscale'
|
||||
uci -q set firewall."$fid".dest='wan'
|
||||
changed_fw=1
|
||||
fi
|
||||
|
||||
if [ "$changed_net" = "1" ]; then
|
||||
uci -q commit network
|
||||
logger -t "$NAME" "Auto configured tailscale network interface (tailscale0)"
|
||||
fi
|
||||
|
||||
if [ "$changed_fw" = "1" ]; then
|
||||
uci -q commit firewall
|
||||
/etc/init.d/firewall reload 2>/dev/null
|
||||
logger -t "$NAME" "Auto configured tailscale firewall zone and forwarding rules"
|
||||
fi
|
||||
}
|
||||
|
||||
clean_firewall_rules() {
|
||||
local changed_net=0
|
||||
local changed_fw=0
|
||||
|
||||
# 1. Remove forwarding rules referencing tailscale
|
||||
config_load firewall
|
||||
local fwds_to_del=""
|
||||
delete_ts_fwd() {
|
||||
local cfg="$1"
|
||||
local src dst
|
||||
config_get src "$cfg" src
|
||||
config_get dst "$cfg" dest
|
||||
if [ "$src" = "tailscale" ] || [ "$dst" = "tailscale" ]; then
|
||||
fwds_to_del="$fwds_to_del $cfg"
|
||||
fi
|
||||
}
|
||||
config_foreach delete_ts_fwd forwarding
|
||||
|
||||
for fwd in $fwds_to_del; do
|
||||
uci -q delete firewall."$fwd"
|
||||
changed_fw=1
|
||||
done
|
||||
|
||||
# 2. Remove tailscale zone
|
||||
local zones_to_del=""
|
||||
find_ts_zone() {
|
||||
local cfg="$1"
|
||||
local name
|
||||
config_get name "$cfg" name
|
||||
if [ "$name" = "tailscale" ]; then
|
||||
zones_to_del="$zones_to_del $cfg"
|
||||
fi
|
||||
}
|
||||
config_foreach find_ts_zone zone
|
||||
|
||||
for z in $zones_to_del; do
|
||||
uci -q delete firewall."$z"
|
||||
changed_fw=1
|
||||
done
|
||||
|
||||
# 3. Remove network interface
|
||||
if uci -q get network.tailscale >/dev/null; then
|
||||
uci -q delete network.tailscale
|
||||
changed_net=1
|
||||
fi
|
||||
|
||||
# 4. Remove DNS forward entry in dnsmasq
|
||||
remove_tailscale_dns_forward
|
||||
|
||||
if [ "$changed_net" = "1" ]; then
|
||||
uci -q commit network
|
||||
logger -t "$NAME" "Cleaned tailscale network interface"
|
||||
fi
|
||||
|
||||
if [ "$changed_fw" = "1" ]; then
|
||||
uci -q commit firewall
|
||||
/etc/init.d/firewall reload 2>/dev/null
|
||||
logger -t "$NAME" "Cleaned tailscale firewall zone and forwarding rules"
|
||||
fi
|
||||
}
|
||||
|
||||
clean_firewall() {
|
||||
clean_firewall_rules
|
||||
}
|
||||
|
||||
apply_settings() {
|
||||
setup_firewall_rules
|
||||
|
||||
local ts_bin
|
||||
if [ -x /usr/sbin/tailscale ]; then
|
||||
ts_bin=/usr/sbin/tailscale
|
||||
elif [ -x /usr/bin/tailscale ]; then
|
||||
ts_bin=/usr/bin/tailscale
|
||||
else
|
||||
logger -t "$NAME" "tailscale binary not found, skipping settings apply"
|
||||
return 0
|
||||
fi
|
||||
|
||||
config_load tailscale
|
||||
|
||||
local accept_routes advertise_exit_node exit_node exit_node_allow_lan_access
|
||||
local ssh shields_up runwebclient tunnel_snat hostname dns_mode
|
||||
local enable_relay relay_server_port
|
||||
|
||||
config_get accept_routes settings accept_routes '0'
|
||||
config_get advertise_exit_node settings advertise_exit_node '0'
|
||||
config_get exit_node settings exit_node ''
|
||||
config_get exit_node_allow_lan_access settings exit_node_allow_lan_access '0'
|
||||
config_get ssh settings ssh '0'
|
||||
config_get dns_mode settings dns_mode 'disabled'
|
||||
config_get shields_up settings shields_up '0'
|
||||
config_get runwebclient settings runwebclient '0'
|
||||
config_get tunnel_snat settings tunnel_snat '0'
|
||||
config_get hostname settings hostname ''
|
||||
config_get enable_relay settings enable_relay '0'
|
||||
config_get relay_server_port settings relay_server_port '40000'
|
||||
|
||||
# Collect advertise_routes UCI list into comma-separated string
|
||||
local routes=""
|
||||
append_route() {
|
||||
if [ -z "$routes" ]; then
|
||||
routes="$1"
|
||||
else
|
||||
routes="$routes,$1"
|
||||
fi
|
||||
}
|
||||
config_list_foreach settings advertise_routes append_route
|
||||
|
||||
# Build argument list for 'tailscale set'
|
||||
set -- set
|
||||
|
||||
# --accept-routes
|
||||
[ "$accept_routes" = "1" ] \
|
||||
&& set -- "$@" --accept-routes=true \
|
||||
|| set -- "$@" --accept-routes=false
|
||||
|
||||
# --advertise-exit-node (only when no exit node is selected)
|
||||
[ "$advertise_exit_node" = "1" ] && [ -z "$exit_node" ] \
|
||||
&& set -- "$@" --advertise-exit-node=true \
|
||||
|| set -- "$@" --advertise-exit-node=false
|
||||
|
||||
# --exit-node-allow-lan-access (only when no exit node is selected)
|
||||
if [ -z "$exit_node" ]; then
|
||||
[ "$exit_node_allow_lan_access" = "1" ] \
|
||||
&& set -- "$@" --exit-node-allow-lan-access=true \
|
||||
|| set -- "$@" --exit-node-allow-lan-access=false
|
||||
fi
|
||||
|
||||
# --ssh
|
||||
[ "$ssh" = "1" ] \
|
||||
&& set -- "$@" --ssh=true \
|
||||
|| set -- "$@" --ssh=false
|
||||
|
||||
# --accept-dns based on dns_mode
|
||||
case "$dns_mode" in
|
||||
magicdns)
|
||||
set -- "$@" --accept-dns=true
|
||||
remove_tailscale_dns_forward
|
||||
;;
|
||||
openwrt_forward)
|
||||
set -- "$@" --accept-dns=false
|
||||
setup_tailscale_dns_forward
|
||||
;;
|
||||
*)
|
||||
# disabled (default)
|
||||
set -- "$@" --accept-dns=false
|
||||
remove_tailscale_dns_forward
|
||||
;;
|
||||
esac
|
||||
|
||||
# --shields-up
|
||||
[ "$shields_up" = "1" ] \
|
||||
&& set -- "$@" --shields-up=true \
|
||||
|| set -- "$@" --shields-up=false
|
||||
|
||||
# --webclient
|
||||
[ "$runwebclient" = "1" ] \
|
||||
&& set -- "$@" --webclient=true \
|
||||
|| set -- "$@" --webclient=false
|
||||
|
||||
# --snat-subnet-routes (aligned with tunnel_snat)
|
||||
[ "$tunnel_snat" = "1" ] \
|
||||
&& set -- "$@" --snat-subnet-routes=true \
|
||||
|| set -- "$@" --snat-subnet-routes=false
|
||||
|
||||
# --advertise-routes (comma-separated, empty clears routes)
|
||||
set -- "$@" "--advertise-routes=$routes"
|
||||
|
||||
# --exit-node (empty clears the exit node)
|
||||
set -- "$@" "--exit-node=$exit_node"
|
||||
|
||||
# when an exit node is configured, always allow LAN access
|
||||
[ -n "$exit_node" ] && set -- "$@" --exit-node-allow-lan-access=true
|
||||
|
||||
# --hostname (only set if non-empty)
|
||||
[ -n "$hostname" ] && set -- "$@" "--hostname=$hostname"
|
||||
|
||||
# --relay-server-port (requires tailscale >= 1.90.5)
|
||||
local ts_ver
|
||||
ts_ver=$("$ts_bin" version 2>/dev/null | head -n1)
|
||||
if version_gte "$ts_ver" "1.90.5"; then
|
||||
if [ "$enable_relay" = "1" ]; then
|
||||
set -- "$@" "--relay-server-port=${relay_server_port:-40000}"
|
||||
else
|
||||
set -- "$@" "--relay-server-port="
|
||||
fi
|
||||
fi
|
||||
|
||||
# Always enforce netfilter-mode=off so OpenWrt manages all firewall rules cleanly
|
||||
set -- "$@" --netfilter-mode=off
|
||||
|
||||
# Wait for tailscaled daemon to be ready (up to 5 seconds)
|
||||
local i=0
|
||||
local daemon_ready=0
|
||||
while [ $i -lt 5 ]; do
|
||||
if "$ts_bin" status >/dev/null 2>&1; then
|
||||
daemon_ready=1
|
||||
break
|
||||
fi
|
||||
if ! "$ts_bin" status 2>&1 | grep -q "failed to connect to local tailscaled"; then
|
||||
daemon_ready=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
if [ "$daemon_ready" = "0" ]; then
|
||||
logger -t "$NAME" "Tailscale daemon is not running or socket is unreachable, skipping settings apply"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check logged-in status. Skip applying settings if logged out to prevent command error.
|
||||
local status_out
|
||||
status_out=$("$ts_bin" status 2>&1)
|
||||
if echo "$status_out" | grep -qE "Logged out|NeedsLogin|NeedsCard"; then
|
||||
logger -t "$NAME" "Tailscale is logged out, saving settings locally but skipping immediate apply"
|
||||
return 0
|
||||
fi
|
||||
|
||||
logger -t "$NAME" "Applying settings via tailscale set"
|
||||
"$ts_bin" "$@"
|
||||
local ret=$?
|
||||
if [ $ret -eq 0 ]; then
|
||||
logger -t "$NAME" "Settings applied successfully"
|
||||
else
|
||||
logger -t "$NAME" "Failed to apply settings (exit code: $ret)"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
handle_service_state() {
|
||||
config_load tailscale
|
||||
local service_enabled
|
||||
config_get service_enabled settings service_enabled '0'
|
||||
|
||||
if [ "$service_enabled" = "0" ]; then
|
||||
logger -t "$NAME" "Service is disabled, stopping tailscale and cleaning up"
|
||||
/etc/init.d/tailscale stop 2>/dev/null
|
||||
/etc/init.d/tailscale disable 2>/dev/null
|
||||
clean_firewall_rules
|
||||
killall -9 tailscaled 2>/dev/null || true
|
||||
else
|
||||
logger -t "$NAME" "Service is enabled, ensuring tailscale is running"
|
||||
/etc/init.d/tailscale enable 2>/dev/null
|
||||
if ! /usr/sbin/tailscale status >/dev/null 2>&1 && ! /usr/bin/tailscale status >/dev/null 2>&1; then
|
||||
/etc/init.d/tailscale start 2>/dev/null
|
||||
fi
|
||||
apply_settings
|
||||
fi
|
||||
}
|
||||
|
||||
start_service() {
|
||||
handle_service_state
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "tailscale"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
handle_service_state
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/vpn/tailscale": {
|
||||
"title": "Tailscale",
|
||||
"order": 90,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "tailscale"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-tailscale" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"luci-app-tailscale": {
|
||||
"description": "Allow user access to tailscale",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"tailscale": [
|
||||
"get_status",
|
||||
"get_settings",
|
||||
"get_subroutes",
|
||||
"get_logs"
|
||||
]
|
||||
},
|
||||
"uci": [ "tailscale" ]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"tailscale": [
|
||||
"do_login",
|
||||
"do_logout",
|
||||
"restart",
|
||||
"setup_firewall",
|
||||
"reload_settings"
|
||||
]
|
||||
},
|
||||
"uci": [ "tailscale" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { access, popen, readfile, writefile, unlink, stat } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const uci = cursor();
|
||||
|
||||
function exec(command) {
|
||||
let stdout_content = '';
|
||||
let p = popen(command, 'r');
|
||||
if (p == null) {
|
||||
return { code: -1, stdout: '', stderr: `Failed to execute: ${command}` };
|
||||
}
|
||||
for (let line = p.read('line'); length(line); line = p.read('line')) {
|
||||
stdout_content = stdout_content+line;
|
||||
}
|
||||
stdout_content = rtrim(stdout_content);
|
||||
stdout_content = split(stdout_content, '\n');
|
||||
|
||||
let exit_code = p.close();
|
||||
let stderr_content = '';
|
||||
if (exit_code != 0) {
|
||||
stderr_content = stdout_content;
|
||||
}
|
||||
return { code: exit_code, stdout: stdout_content, stderr: stderr_content };
|
||||
}
|
||||
|
||||
function shell_quote(s) {
|
||||
if (s == null || s == '') return "''";
|
||||
return "'" + replace(s, "'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
function get_connectivity_data() {
|
||||
let ts_cmd = access('/usr/sbin/tailscale') ? '/usr/sbin/tailscale' : '/usr/bin/tailscale';
|
||||
if (!access(ts_cmd)) return null;
|
||||
|
||||
const cache_file = '/tmp/tailscale_netcheck.json';
|
||||
const lock_file = '/tmp/tailscale_netcheck.lock';
|
||||
|
||||
let cached_data = null;
|
||||
if (access(cache_file)) {
|
||||
try {
|
||||
let content = readfile(cache_file);
|
||||
if (content != null && content != '') {
|
||||
cached_data = json(content);
|
||||
}
|
||||
} catch(e) { /* ignore */ }
|
||||
}
|
||||
|
||||
let cache_st = access(cache_file) ? stat(cache_file) : null;
|
||||
let now = time();
|
||||
if (!cache_st || (now - cache_st.mtime) > 60) {
|
||||
let lock_st = access(lock_file) ? stat(lock_file) : null;
|
||||
if (!lock_st || (now - lock_st.mtime) > 30) {
|
||||
writefile(lock_file, '' + now);
|
||||
system('(' + ts_cmd + ' netcheck --format=json > /tmp/tailscale_netcheck.tmp 2>/dev/null && mv /tmp/tailscale_netcheck.tmp /tmp/tailscale_netcheck.json; rm -f ' + lock_file + ') >/dev/null 2>&1 &');
|
||||
}
|
||||
}
|
||||
|
||||
return cached_data;
|
||||
}
|
||||
|
||||
function get_cached_version() {
|
||||
const cache_file = '/tmp/tailscale_version.cache';
|
||||
const ts_bin = access('/usr/sbin/tailscale') ? '/usr/sbin/tailscale' : '/usr/bin/tailscale';
|
||||
if (!access(ts_bin)) return 'Unknown';
|
||||
|
||||
let bin_st = stat(ts_bin);
|
||||
let cache_st = access(cache_file) ? stat(cache_file) : null;
|
||||
|
||||
if (cache_st && cache_st.size > 0 && bin_st && cache_st.mtime >= bin_st.mtime) {
|
||||
let cached = trim(readfile(cache_file) || '');
|
||||
if (cached != '') return cached;
|
||||
}
|
||||
|
||||
let ver_res = exec(ts_bin + ' version 2>/dev/null');
|
||||
if (ver_res.code == 0 && length(ver_res.stdout) > 0) {
|
||||
let ver_line = trim(ver_res.stdout[0]);
|
||||
if (ver_line != '') {
|
||||
writefile(cache_file, ver_line + '\n');
|
||||
return ver_line;
|
||||
}
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const methods = {};
|
||||
|
||||
methods.get_status = {
|
||||
call: function() {
|
||||
let data = {
|
||||
status: '',
|
||||
version: '',
|
||||
TUNMode: '',
|
||||
health: '',
|
||||
ipv4: null,
|
||||
ipv6: null,
|
||||
domain_name: '',
|
||||
peers: [],
|
||||
connectivity: null
|
||||
};
|
||||
|
||||
if (!access('/usr/sbin/tailscale') && !access('/usr/bin/tailscale')) {
|
||||
data.status = 'not_installed';
|
||||
return data;
|
||||
}
|
||||
|
||||
data.version = get_cached_version();
|
||||
|
||||
let status_out = exec('tailscale status --json 2>/dev/null');
|
||||
let peer_map = {};
|
||||
|
||||
if (status_out.code == 0 && length(status_out.stdout) > 0) {
|
||||
try {
|
||||
let status_data = json(join('', status_out.stdout));
|
||||
if (status_data?.Version) {
|
||||
data.version = status_data.Version;
|
||||
}
|
||||
data.health = status_data?.Health || '';
|
||||
data.TUNMode = (status_data?.TUN == false ? 'false' : 'true');
|
||||
if (status_data?.BackendState == 'NeedsLogin' || status_data?.AuthURL || status_data?.BackendState == 'NoState' || (status_data?.HaveNodeKey == false && status_data?.BackendState != 'Running')) {
|
||||
data.status = 'logout';
|
||||
} else if (status_data?.BackendState == 'Running' && status_data?.HaveNodeKey == true) {
|
||||
data.status = 'running';
|
||||
} else if (status_data?.HaveNodeKey == true) {
|
||||
data.status = 'stopped';
|
||||
} else {
|
||||
data.status = (status_data?.BackendState == 'Stopped') ? 'stopped' : 'logout';
|
||||
}
|
||||
|
||||
if (status_data?.Self?.TailscaleIPs) {
|
||||
data.ipv4 = status_data.Self.TailscaleIPs[0] || null;
|
||||
data.ipv6 = status_data.Self.TailscaleIPs[1] || null;
|
||||
}
|
||||
|
||||
let account_str = '';
|
||||
if (status_data?.CurrentTailnet?.Name) {
|
||||
account_str = status_data.CurrentTailnet.Name;
|
||||
}
|
||||
if (status_data?.Self?.UserID && status_data?.User) {
|
||||
let uid = '' + status_data.Self.UserID;
|
||||
let uinfo = status_data.User[uid];
|
||||
if (uinfo) {
|
||||
let login_name = uinfo?.LoginName || uinfo?.Username || '';
|
||||
let display_name = uinfo?.DisplayName || '';
|
||||
if (login_name != '' && display_name != '' && login_name != display_name) {
|
||||
account_str = login_name + ' (' + display_name + ')';
|
||||
} else if (login_name != '') {
|
||||
account_str = login_name;
|
||||
} else if (display_name != '') {
|
||||
account_str = display_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (account_str == '' && status_data?.Self?.DNSName) {
|
||||
account_str = status_data.Self.DNSName;
|
||||
}
|
||||
data.account = account_str;
|
||||
data.domain_name = account_str;
|
||||
|
||||
if (status_data?.Peer) {
|
||||
for (let id, p in status_data.Peer) {
|
||||
let ips = '';
|
||||
if (p?.TailscaleIPs) {
|
||||
ips = join('<br>', p.TailscaleIPs);
|
||||
}
|
||||
let hostname = p?.DNSName || '';
|
||||
if (hostname != '') {
|
||||
let parts = split(hostname, '.');
|
||||
hostname = parts[0];
|
||||
}
|
||||
|
||||
peer_map[id] = {
|
||||
ip: ips,
|
||||
hostname: hostname,
|
||||
ostype: p?.OS,
|
||||
online: p?.Online || false,
|
||||
linkadress: (!p?.CurAddr || p?.CurAddr == '') ? p?.Relay : p?.CurAddr,
|
||||
lastseen: p?.LastSeen,
|
||||
exit_node: !(!p?.ExitNode),
|
||||
exit_node_option: !(!p?.ExitNodeOption),
|
||||
tx: p?.TxBytes || '',
|
||||
rx: p?.RxBytes || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (data.status == 'running') {
|
||||
let netcheck = get_connectivity_data();
|
||||
let derp_lat_str = '';
|
||||
let pref_derp = netcheck?.PreferredDERP || status_data?.Self?.Relay || 0;
|
||||
if (pref_derp != 0 && pref_derp != '' && netcheck?.DERPLatencies) {
|
||||
let lat = netcheck.DERPLatencies['' + pref_derp];
|
||||
if (lat) {
|
||||
derp_lat_str = sprintf('%d ms', lat * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
let ep_list = [];
|
||||
if (status_data?.Self?.Addrs && length(status_data.Self.Addrs) > 0) {
|
||||
ep_list = status_data.Self.Addrs;
|
||||
} else if (status_data?.Self?.Endpoints && length(status_data.Self.Endpoints) > 0) {
|
||||
ep_list = status_data.Self.Endpoints;
|
||||
} else if (netcheck) {
|
||||
if (netcheck.GlobalV4) push(ep_list, netcheck.GlobalV4);
|
||||
if (netcheck.GlobalV6) push(ep_list, netcheck.GlobalV6);
|
||||
if (netcheck.GlobalAddrs && length(netcheck.GlobalAddrs) > 0) {
|
||||
for (let a in netcheck.GlobalAddrs) push(ep_list, a);
|
||||
}
|
||||
}
|
||||
|
||||
data.connectivity = {
|
||||
varies: (netcheck?.MappingVariesByDestIP == true) ? 'Yes' : 'No',
|
||||
ipv4: (netcheck?.IPv4 == true || (ep_list && length(ep_list) > 0)) ? 'Yes' : 'No',
|
||||
ipv6: (netcheck?.IPv6 == true) ? 'Yes' : 'No',
|
||||
udp: (netcheck?.UDP == true || (ep_list && length(ep_list) > 0)) ? 'Yes' : 'No',
|
||||
upnp: (netcheck?.UPnP == true) ? 'Yes' : 'No',
|
||||
pcp: (netcheck?.PCP == true) ? 'Yes' : 'No',
|
||||
pmp: (netcheck?.PMP == true) ? 'Yes' : 'No',
|
||||
hairpinning: (netcheck?.HairPinning == true) ? 'Yes' : 'No',
|
||||
preferred_derp: pref_derp,
|
||||
derp_latency: derp_lat_str,
|
||||
endpoints: ep_list
|
||||
};
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
uci.load('tailscale');
|
||||
let state_file_path = uci.get('tailscale', 'settings', 'state_file') || "/etc/tailscale/tailscaled.state";
|
||||
let has_state = false;
|
||||
if (access(state_file_path) && stat(state_file_path)?.size > 0) {
|
||||
has_state = true;
|
||||
} else if (access('/var/lib/tailscale/tailscaled.state') && stat('/var/lib/tailscale/tailscaled.state')?.size > 0) {
|
||||
has_state = true;
|
||||
}
|
||||
data.status = has_state ? 'stopped' : 'logout';
|
||||
}
|
||||
|
||||
data.peers = peer_map;
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
methods.get_settings = {
|
||||
call: function() {
|
||||
let settings = {};
|
||||
uci.load('tailscale');
|
||||
let state_file_path = uci.get('tailscale', 'settings', 'state_file') || "/etc/tailscale/tailscaled.state";
|
||||
if (access(state_file_path)) {
|
||||
try {
|
||||
let state_content = readfile(state_file_path);
|
||||
if (state_content != null) {
|
||||
let state_data = json(state_content);
|
||||
let profiles_b64 = state_data?._profiles;
|
||||
if (!profiles_b64) return settings;
|
||||
|
||||
let profiles_data = json(b64dec(profiles_b64));
|
||||
let profiles_key = null;
|
||||
for (let key in profiles_data) {
|
||||
profiles_key = key;
|
||||
break;
|
||||
}
|
||||
profiles_key = 'profile-'+profiles_key;
|
||||
|
||||
let status_data = json(b64dec(state_data?.[profiles_key]));
|
||||
if (status_data != null) {
|
||||
settings.accept_routes = status_data?.RouteAll || false;
|
||||
settings.advertise_exit_node = status_data?.AdvertiseExitNode || false;
|
||||
settings.advertise_routes = status_data?.AdvertiseRoutes || [];
|
||||
settings.exit_node = status_data?.ExitNodeID || "";
|
||||
settings.exit_node_allow_lan_access = status_data?.ExitNodeAllowLANAccess || false;
|
||||
settings.shields_up = status_data?.ShieldsUp || false;
|
||||
settings.ssh = status_data?.RunSSH || false;
|
||||
settings.runwebclient = status_data?.RunWebClient || false;
|
||||
settings.tunnel_snat = uci.get('tailscale', 'settings', 'tunnel_snat') || '0';
|
||||
settings.dns_mode = uci.get('tailscale', 'settings', 'dns_mode') || 'disabled';
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
methods.do_login = {
|
||||
args: { form_data: {} },
|
||||
call: function(request) {
|
||||
const form_data = request.args.form_data;
|
||||
if (form_data == null || length(form_data) == 0) {
|
||||
return { error: 'Missing or invalid form_data parameter. Please provide login data.' };
|
||||
}
|
||||
|
||||
let status = methods.get_status.call();
|
||||
if (status.status == 'running') {
|
||||
return { error: 'Tailscale is already logged in and running.' };
|
||||
}
|
||||
|
||||
let status_test = exec('tailscale status >/dev/null 2>&1');
|
||||
if (status_test.code != 0) {
|
||||
exec('/etc/init.d/tailscale start >/dev/null 2>&1');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
sleep(1000);
|
||||
let res = exec('tailscale status >/dev/null 2>&1');
|
||||
if (res.code == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 1. Prepare and Run Login Command (Once) ---
|
||||
let loginargs = [];
|
||||
const loginserver = trim(form_data.loginserver) || '';
|
||||
const loginserver_authkey = trim(form_data.loginserver_authkey) || '';
|
||||
|
||||
if (loginserver != '') {
|
||||
push(loginargs, '--login-server ' + shell_quote(loginserver));
|
||||
if (loginserver_authkey != '') {
|
||||
push(loginargs, '--auth-key ' + shell_quote(loginserver_authkey));
|
||||
}
|
||||
}
|
||||
|
||||
// Run the command in the background using /bin/sh -c to handle the '&' correctly
|
||||
let login_cmd = 'tailscale login ' + join(' ', loginargs);
|
||||
popen('/bin/sh -c ' + shell_quote(login_cmd + ' &'), 'r');
|
||||
|
||||
// --- 2. Loop to Check Status for URL ---
|
||||
let max_attempts = 15;
|
||||
let interval = 2000;
|
||||
|
||||
for (let i = 0; i < max_attempts; i++) {
|
||||
let tresult = exec('tailscale status 2>&1');
|
||||
for (let line in tresult.stdout) {
|
||||
let trline = trim(line);
|
||||
if (index(trline, 'http') != -1) {
|
||||
let parts = split(trline, ' ');
|
||||
for (let part in parts) {
|
||||
if (index(part, 'http') != -1) {
|
||||
return { url: part };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sleep(interval);
|
||||
}
|
||||
return { error: 'Could not retrieve login URL from tailscale command after 30 seconds.' };
|
||||
}
|
||||
};
|
||||
|
||||
methods.do_logout = {
|
||||
call: function() {
|
||||
let status = methods.get_status.call();
|
||||
if (status.status == 'not_installed') {
|
||||
return { error: 'Tailscale is not installed.' };
|
||||
}
|
||||
if (status.status == 'logout') {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
uci.load('tailscale');
|
||||
let state_file_path = uci.get('tailscale', 'settings', 'state_file') || "/etc/tailscale/tailscaled.state";
|
||||
|
||||
if (status.status == 'running') {
|
||||
exec('tailscale logout 2>/dev/null');
|
||||
} else {
|
||||
exec('/etc/init.d/tailscale start >/dev/null 2>&1');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
sleep(1000);
|
||||
let res = exec('tailscale logout 2>/dev/null');
|
||||
if (res.code == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
exec('/etc/init.d/tailscale stop >/dev/null 2>&1');
|
||||
|
||||
if (access(state_file_path)) {
|
||||
unlink(state_file_path);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
||||
methods.get_subroutes = {
|
||||
call: function() {
|
||||
try {
|
||||
let cmd = 'ip -j route';
|
||||
let result = exec(cmd);
|
||||
let subnets = [];
|
||||
|
||||
if (result.code == 0 && length(result.stdout) > 0) {
|
||||
let routes_json = json(join('',result.stdout));
|
||||
|
||||
for (let route in routes_json) {
|
||||
// We need to filter out local subnets
|
||||
// 1. 'dst' (target address) is not' default' (default gateway)
|
||||
// 2. 'scope' is' link' (indicating directly connected network)
|
||||
// 3. It is an IPv4 address (simple judgment: including'.')
|
||||
if (route?.dst && route.dst != 'default' && route?.scope == 'link' && index(route.dst,'.') != -1) {
|
||||
push(subnets,route.dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { routes: subnets };
|
||||
}
|
||||
catch(e) {
|
||||
return { routes: [] };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
methods.setup_firewall = {
|
||||
call: function() {
|
||||
try {
|
||||
uci.load('tailscale');
|
||||
|
||||
uci.load('network');
|
||||
uci.load('firewall');
|
||||
|
||||
let changed_network = false;
|
||||
let changed_firewall = false;
|
||||
|
||||
// 1. config Network Interface
|
||||
let net_ts = uci.get('network', 'tailscale');
|
||||
if (net_ts == null) {
|
||||
uci.set('network', 'tailscale', 'interface');
|
||||
uci.set('network', 'tailscale', 'proto', 'none');
|
||||
uci.set('network', 'tailscale', 'device', 'tailscale0');
|
||||
changed_network = true;
|
||||
} else {
|
||||
let current_dev = uci.get('network', 'tailscale', 'device');
|
||||
if (current_dev != 'tailscale0') {
|
||||
uci.set('network', 'tailscale', 'device', 'tailscale0');
|
||||
changed_network = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. config Firewall Zone
|
||||
let ts_zone_section = null;
|
||||
let fwd_lan_to_ts = false;
|
||||
let fwd_ts_to_lan = false;
|
||||
|
||||
uci.foreach('firewall', 'zone', function(s) {
|
||||
if (s['name'] == 'tailscale')
|
||||
ts_zone_section = s['.name'];
|
||||
});
|
||||
uci.foreach('firewall', 'forwarding', function(s) {
|
||||
if (s['src'] == 'lan' && s['dest'] == 'tailscale') fwd_lan_to_ts = true;
|
||||
if (s['src'] == 'tailscale' && s['dest'] == 'lan') fwd_ts_to_lan = true;
|
||||
});
|
||||
|
||||
let tunnel_snat = uci.get('tailscale', 'settings', 'tunnel_snat') || '0';
|
||||
let target_masq = (tunnel_snat == '1') ? '1' : '0';
|
||||
|
||||
if (ts_zone_section == null) {
|
||||
let zid = uci.add('firewall', 'zone');
|
||||
uci.set('firewall', zid, 'name', 'tailscale');
|
||||
uci.set('firewall', zid, 'input', 'ACCEPT');
|
||||
uci.set('firewall', zid, 'output', 'ACCEPT');
|
||||
uci.set('firewall', zid, 'forward', 'ACCEPT');
|
||||
uci.set('firewall', zid, 'masq', target_masq);
|
||||
uci.set('firewall', zid, 'mtu_fix', '1');
|
||||
uci.set('firewall', zid, 'network', ['tailscale']);
|
||||
changed_firewall = true;
|
||||
} else {
|
||||
let cur_masq = uci.get('firewall', ts_zone_section, 'masq');
|
||||
if (cur_masq != target_masq) {
|
||||
uci.set('firewall', ts_zone_section, 'masq', target_masq);
|
||||
changed_firewall = true;
|
||||
}
|
||||
let nets = uci.get('firewall', ts_zone_section, 'network');
|
||||
let net_list = [];
|
||||
let has_ts_net = false;
|
||||
|
||||
if (type(nets) == 'array') {
|
||||
net_list = nets;
|
||||
} else if (type(nets) == 'string') {
|
||||
net_list = [nets];
|
||||
}
|
||||
|
||||
// check if 'tailscale' is already in the list
|
||||
for (let n in net_list) {
|
||||
if (n == 'tailscale') {
|
||||
has_ts_net = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_ts_net) {
|
||||
push(net_list, 'tailscale');
|
||||
uci.set('firewall', ts_zone_section, 'network', net_list);
|
||||
changed_firewall = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. config Forwarding
|
||||
if (!fwd_lan_to_ts) {
|
||||
let fid = uci.add('firewall', 'forwarding');
|
||||
uci.set('firewall', fid, 'src', 'lan');
|
||||
uci.set('firewall', fid, 'dest', 'tailscale');
|
||||
changed_firewall = true;
|
||||
}
|
||||
|
||||
if (!fwd_ts_to_lan) {
|
||||
let fid = uci.add('firewall', 'forwarding');
|
||||
uci.set('firewall', fid, 'src', 'tailscale');
|
||||
uci.set('firewall', fid, 'dest', 'lan');
|
||||
changed_firewall = true;
|
||||
}
|
||||
|
||||
// Exit node requires WAN <- tailscale forwarding
|
||||
let fwd_ts_to_wan = false;
|
||||
uci.foreach('firewall', 'forwarding', function(s) {
|
||||
if (s['src'] == 'tailscale' && s['dest'] == 'wan') fwd_ts_to_wan = true;
|
||||
});
|
||||
|
||||
if (!fwd_ts_to_wan) {
|
||||
let fid = uci.add('firewall', 'forwarding');
|
||||
uci.set('firewall', fid, 'src', 'tailscale');
|
||||
uci.set('firewall', fid, 'dest', 'wan');
|
||||
changed_firewall = true;
|
||||
}
|
||||
|
||||
// 4. save
|
||||
if (changed_network) {
|
||||
uci.save('network');
|
||||
uci.commit('network');
|
||||
}
|
||||
|
||||
if (changed_firewall) {
|
||||
uci.save('firewall');
|
||||
uci.commit('firewall');
|
||||
exec('/etc/init.d/firewall reload');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
changed_network: changed_network,
|
||||
changed_firewall: changed_firewall,
|
||||
message: (changed_network || changed_firewall) ? 'Tailscale firewall/interface configuration applied.' : 'Tailscale firewall/interface already configured.'
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
return { error: 'Exception in setup_firewall: ' + e + '\nStack: ' + (e.stacktrace || '') };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
methods.get_logs = {
|
||||
args: { lines: 200 },
|
||||
call: function(request) {
|
||||
let lines = int(request?.args?.lines) || 200;
|
||||
let cmd = 'logread -l ' + lines + ' 2>/dev/null | grep -i -E "tailscale" || true';
|
||||
let result = exec(cmd);
|
||||
if (result.code == 0) {
|
||||
return { logs: result.stdout };
|
||||
}
|
||||
return { logs: [], error: join(' ', result.stderr) };
|
||||
}
|
||||
};
|
||||
|
||||
methods.reload_settings = {
|
||||
call: function() {
|
||||
exec('/etc/init.d/tailscale-settings reload');
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
||||
methods.restart = {
|
||||
call: function() {
|
||||
writefile('/tmp/tailscale_hotplug.lock', '' + time() + '\n');
|
||||
exec('/etc/init.d/tailscale restart');
|
||||
exec('/etc/init.d/tailscale-settings reload');
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
||||
return { 'tailscale': methods };
|
||||
Reference in new issue
Block a user