⛅ Sync 2026-10-03 02:37:08

This commit is contained in:
github-actions[bot] committed 2026-10-03 02:37:08 +08:00
1 parent 1b126bfea3
commit a5bb91e6ed
24 files changed
+5536 -990

No files matched your search

@@ -0,0 +1,24 @@
#!/bin/sh
# When a ZeroTier device appears or disappears, converge the firewall
# zone's explicit device list onto the interfaces currently present
# (/etc/init.d/luci-zerotier sync). Events must not be dropped while a
# previous sync still holds the lock (fw4 reload), so WAIT for the lock —
# BusyBox flock has no -w, hence the bounded -n retry loop. If the lock
# never frees up, run anyway: sync is idempotent and self-verifying.
[ "$ACTION" = "add" ] || [ "$ACTION" = "remove" ] || exit 0
case "$DEVICENAME" in zt??*) ;; *) exit 0 ;; esac
(
_i=0
while ! flock -n 9; do
_i=$((_i + 1))
[ "$_i" -ge 20 ] && break
sleep 1
done
sleep 1
/etc/init.d/luci-zerotier sync >/dev/null 2>&1
) 9>/var/run/luci-zerotier-hotplug.lock &
exit 0
+276
View File
@@ -0,0 +1,276 @@
#!/bin/sh /etc/rc.common
START=99
STOP=98
PROG=/etc/init.d/zerotier
FW4_LOCK=/var/run/fw4.lock
EXTRA_COMMANDS="sync"
EXTRA_HELP=" sync Re-sync the firewall zone device list from the
currently present ZeroTier interfaces (also driven by
hotplug on zt* device add/remove)"
get_config() {
config_get_bool enabled $1 enabled 0
config_get_bool nat $1 nat 0
}
log() {
echo "$@"
logger -t luci-zerotier "$@"
}
validate_path() {
# $2 = kind: "file" (local_conf_path) additionally allows the upstream
# package's conventional single-file location /etc/zerotier.conf and
# volatile /tmp (harmless there — the daemon only uses it if present);
# "dir" (config_path) requires a persistent location: the daemon
# refuses to start when config_path is missing, so /tmp would leave
# it dead after every reboot.
case "$2:$1" in
file:/etc/zerotier.conf|file:/etc/zerotier/*|file:/tmp|file:/tmp/*)
return 0
;;
dir:/etc/zerotier|dir:/etc/zerotier/*)
return 0
;;
esac
log "Invalid path rejected: $1"
return 1
}
# fw4 holds /var/run/fw4.lock exclusively for the entire start/reload run,
# INCLUDING the execution of script includes. Calling "firewall reload" from
# such a context re-enters fw4 and deadlocks on that lock until the include
# is SIGKILLed after 30s — and the orphaned nested fw4 process then starts
# a self-sustaining reload cascade (reproduced on 24.10.7: ~36s cycles,
# growing process backlog, zone flapping). So: only reload when the lock
# is free. If the lock is held, whoever holds it has already rendered the
# ruleset from the uci state we just committed, so skipping is safe.
fw_reload() {
: >> "$FW4_LOCK" 2>/dev/null
if flock -n "$FW4_LOCK" true 2>/dev/null; then
/etc/init.d/firewall reload
else
log "fw4 lock is held (reload/include in progress), skipping explicit reload"
fi
}
any_rules_present() {
local s
for s in zerotier zerotier_lan lan_zerotier zerotier_wan; do
[ -n "$(uci -q get "firewall.$s")" ] && return 0
done
return 1
}
# Zone membership design (r31): the zone lists the EXPLICIT ZeroTier
# device names present on the system, enumerated straight from the kernel
# (/sys/class/net/zt*) — no zerotier-cli dependency (its port file can go
# missing after service restarts), no netifd involvement (r29 proved that
# netifd claiming zerotier-managed devices flushes the daemon-assigned
# IPv4 on 24.10 — do not reintroduce that). The pre-r23 design already
# used explicit devices but updated them from a firewall include with a
# 20s readiness wait, which re-entered fw4's lock and deadlocked; the
# update trigger is now the net hotplug hook instead (fires exactly when
# a zt device appears or vanishes — no waiting, no include).
#
# Trade-off vs the r23 'zt+' glob, kept as documented residue: with real
# device names LuCI's zone editor shows them and saves preserve them (a
# glob gets flagged "Absent Interface" and was manually "fixed" away on
# production boxes). The fw4 glob semantics are partially retained in the
# matching below — absent devices are simply dropped from the list until
# they reappear (fw4 would skip them anyway).
# Enumerate the ZeroTier interfaces currently present in the kernel.
zt_devices() {
local d
for d in /sys/class/net/zt*; do
[ -e "$d" ] && basename "$d"
done
}
# Parse the zone's device list out of `uci show` output. List options are
# packed onto a single line as device='v1' 'v2' — split the quoted values
# apart (r29's outage was rooted in parsing this wrong).
zone_devices() {
uci -q show firewall.zerotier 2>/dev/null \
| sed -n "s/^firewall\.zerotier\.device='\(.*\)'\$/\1/p" \
| tr "'" '\n' \
| sed '/^[[:space:]]*$/d; s/^[[:space:]]*//; s/[[:space:]]*$//'
}
rules_present() {
[ "$(uci -q get firewall.zerotier)" = "zone" ] || return 1
[ "$(uci -q get firewall.zerotier.name)" = "zerotier" ] || return 1
[ "$(uci -q get firewall.zerotier_lan)" = "forwarding" ] || return 1
[ "$(uci -q get firewall.lan_zerotier)" = "forwarding" ] || return 1
[ "$(uci -q get firewall.zerotier_wan)" = "forwarding" ] || return 1
return 0
}
state_ok() {
rules_present || return 1
[ "$(zt_devices | sort)" = "$(zone_devices | sort)" ]
}
# NOTE on transaction structure: every "uci commit firewall" fires procd's
# config trigger, which makes fw4 re-render ASYNCHRONOUSLY. When a previous
# commit's render is still in flight while we issue our next batch of uci
# commands, individual uci CLI invocations can fail intermittently ("uci:
# Invalid argument") and committed state silently loses options (observed on
# 24.10.7: the zone's 'name' vanished, fw4 then skipped the zone and all
# forwardings — NAT silently broken). Therefore:
# 1. All edits of one phase happen in a SINGLE "uci batch" invocation
# (one delta load/save cycle — no inter-command window).
# 2. The committed result is verified afterwards; the whole transaction is
# retried on failure. Intermediate async renders may still read a partial
# state, but the render triggered by our final commit always sees the
# complete state (eventual consistency), and the committed state itself
# is guaranteed intact by the verify-retry loop.
# Apply the desired state in ONE uci transaction: zone (with the current
# explicit device list) and forwardings are committed together, verified,
# and retried. The fw4 reload only happens after the committed state is
# complete (never a half-applied zone — that severs management access,
# see the r29 incident notes).
apply_state() {
if state_ok; then
log "ZeroTier firewall state already in sync, nothing to do"
return 0
fi
local attempt dev batch
for attempt in 1 2 3; do
batch="delete firewall.zerotier
set firewall.zerotier=zone
set firewall.zerotier.name='zerotier'
set firewall.zerotier.input='ACCEPT'
set firewall.zerotier.output='ACCEPT'
set firewall.zerotier.forward='ACCEPT'
set firewall.zerotier.masq='1'
"
for dev in $(zt_devices); do
batch="${batch}add_list firewall.zerotier.device='$dev'
"
done
# zerotier -> lan: 让 zerotier 客户端访问局域网
# lan -> zerotier: 让局域网设备通过 zerotier 互访
# zerotier -> wan: 让 zerotier 客户端通过本机 WAN 上网(网关模式)
batch="${batch}delete firewall.zerotier_lan
set firewall.zerotier_lan=forwarding
set firewall.zerotier_lan.src='zerotier'
set firewall.zerotier_lan.dest='lan'
delete firewall.lan_zerotier
set firewall.lan_zerotier=forwarding
set firewall.lan_zerotier.src='lan'
set firewall.lan_zerotier.dest='zerotier'
delete firewall.zerotier_wan
set firewall.zerotier_wan=forwarding
set firewall.zerotier_wan.src='zerotier'
set firewall.zerotier_wan.dest='wan'
"
printf '%s' "$batch" | uci -q batch
uci -q commit firewall
if state_ok; then
fw_reload
log "ZeroTier firewall zone applied ($(zt_devices | wc -l) devices: $(zt_devices | tr '\n' ' '))"
return 0
fi
log "uci transaction incomplete (attempt $attempt), retrying..."
sleep 1
done
log "ERROR: failed to apply ZeroTier firewall state after 3 attempts"
return 1
}
delete_rules() {
uci -q batch <<-'EOF'
delete firewall.zerotier
delete firewall.zerotier_lan
delete firewall.lan_zerotier
delete firewall.zerotier_wan
EOF
}
remove_rules() {
local attempt
any_rules_present || return 0
for attempt in 1 2 3; do
delete_rules
uci commit firewall
if ! any_rules_present; then
fw_reload
log "Removed uci firewall rules for ZeroTier"
return 0
fi
log "uci delete incomplete (attempt $attempt), retrying..."
sleep 1
done
log "ERROR: failed to remove firewall rules after 3 attempts"
return 1
}
start() {
config_load zerotier
config_foreach get_config zerotier
# Server-side path validation (defense-in-depth, mirrors general.js regex)
_local_conf=$(uci -q get zerotier.global.local_conf_path 2>/dev/null)
_config_path=$(uci -q get zerotier.global.config_path 2>/dev/null)
[ -n "$_local_conf" ] && { validate_path "$_local_conf" file || return 1; }
[ -n "$_config_path" ] && { validate_path "$_config_path" dir || return 1; }
# Disabled or NAT off: make sure no stale rules are left behind
# (previously these paths returned early without cleanup). The :-0
# defaults keep this fail-closed when the UCI config is missing
# entirely (unset vars would otherwise error both guards and fall
# through to add_rules).
[ "${enabled:-0}" -eq 0 ] && { remove_rules; log "Service disabled in config."; return 0; }
[ "${nat:-0}" -eq 0 ] && { remove_rules; log "NAT disabled in config."; return 0; }
# The zone lists the currently present devices, so there is no need to
# gate rule creation on daemon/interface readiness — the hotplug hook
# re-syncs the list the moment a zt device appears. Warn only, and
# install the rules unconditionally.
$PROG running 2>/dev/null || log "Warning: zerotier service is not running; installing rules anyway"
apply_state
}
# Hotplug/manual entrypoint: converge the zone device list onto the
# currently present ZeroTier interfaces (idempotent).
sync() {
config_load zerotier
config_foreach get_config zerotier
[ "${enabled:-0}" -eq 0 ] && { remove_rules; log "Service disabled in config."; return 0; }
[ "${nat:-0}" -eq 0 ] && { remove_rules; log "NAT disabled in config."; return 0; }
apply_state
}
# rc.common's default reload() is "restart" (stop+start): stop() runs the
# runtime-state sync and tears the rules down, so every LuCI Save&Apply
# (ucitrack fires reload on each committed change) churned uci, the
# firewall and the flash twice over (ucitrack + the explicit RPC reload in
# general.js). Defining reload_service() does NOT help here — that hook is
# only honored for procd scripts (USE_PROCD=1); for a plain rc.common
# script the function to override is reload() itself. start() is the
# idempotent no-op this service was always meant to reload with.
reload() {
start
}
stop() {
/usr/bin/zerotier-sync.sh 2>/dev/null
remove_rules
}
# Invoked via the K-link at system shutdown (rc.common's default shutdown()
# would call stop(), churning uci and the firewall for nothing) — only the
# runtime-state sync matters here.
shutdown() {
/usr/bin/zerotier-sync.sh 2>/dev/null
}
@@ -1,12 +0,0 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
delete ucitrack.@zerotier[-1]
commit ucitrack
delete firewall.zerotier
commit firewall
EOF
rm -f /tmp/luci-indexcache
exit 0
@@ -0,0 +1,91 @@
#!/bin/sh
[ ! -f "/usr/share/ucitrack/luci-app-registration.json" ] && {
cat > /usr/share/ucitrack/luci-app-registration.json << EEOF
{
"config": "registration",
"init": "registration"
}
EEOF
}
# Migrate the misnamed option written by <= r22: the LuCI form stored the
# local.conf path in `zerotier.global.local_conf`, but the zerotier package
# init script only honors `local_conf_path` — the old value was silently
# ignored. Rename it (keeping an explicitly set new value if both exist).
_old_lc=$(uci -q get zerotier.global.local_conf)
if [ -n "$_old_lc" ]; then
if [ -z "$(uci -q get zerotier.global.local_conf_path)" ]; then
uci set zerotier.global.local_conf_path="$_old_lc"
fi
uci delete zerotier.global.local_conf
uci commit zerotier
fi
# NOTE: reload-on-apply is handled by /usr/share/ucitrack/luci-app-zerotier.json
# (procd config trigger -> /etc/init.d/luci-zerotier reload). The old uci-based
# ucitrack registration (`uci add ucitrack ...`) is obsolete: modern ucitrack
# only reads /usr/share/ucitrack/*.json, and `uci add` fails on systems where
# /etc/config/ucitrack does not exist.
# NOTE on firewall integration (r31+): the zerotier zone lists the
# EXPLICIT device names of the currently present zt* interfaces,
# maintained by /etc/init.d/luci-zerotier sync (run at service start and
# from the net hotplug hook on zt* device add/remove). Earlier designs
# are deliberately gone: the pre-r23 include re-entered fw4's lock and
# deadlocked; the r23 'zt+' glob was flagged "Absent Interface" by LuCI's
# DeviceSelect and got manually edited out of zones; r29's netifd
# interface takeover flushed the daemon-assigned IPv4 on 24.10. Nothing
# needs to be re-run on firewall reloads; the old include registration is
# still cleaned up below (including the pre-r22 legacy name collision
# where it was registered as `firewall.zerotier` type=include).
# Only commit when something was actually deleted — every firewall commit
# triggers an asynchronous fw4 re-render.
_fw_changed=0
uci -q delete firewall.zerotier_include && _fw_changed=1
[ "$(uci -q get firewall.zerotier)" = "include" ] && { uci -q delete firewall.zerotier && _fw_changed=1; }
[ "$_fw_changed" -eq 1 ] && uci commit firewall
# Set default persistent config path if not already configured
if ! uci -q get zerotier.global.config_path >/dev/null; then
uci set zerotier.global.config_path='/etc/zerotier'
uci set zerotier.global.copy_config_path='1'
uci commit zerotier
fi
# Seed local.conf from template if neither exists — but not when
# local_conf_path is set: a seeded file in config_path would shadow it
# (upstream links with plain `ln -s`, which fails when the target exists;
# sync_config's import step handles the precedence instead).
if [ -z "$(uci -q get zerotier.global.local_conf_path)" ] && [ ! -f /etc/zerotier/local.conf ]; then
if [ -f /etc/zerotier/local.conf.template ]; then
# Strip JSON comments (lines starting with //) to create valid JSON
grep -v '^[[:space:]]*//' /etc/zerotier/local.conf.template | grep -v '^$' > /etc/zerotier/local.conf
fi
fi
# r26 removed the periodic sync: backups run on demand (LuCI "Backup Now"
# button) and from lifecycle hooks (init.d stop/shutdown, restart_service
# RPC). Remove the legacy cron entry on upgrade.
( crontab -l 2>/dev/null | grep -v 'zerotier-sync' ) | crontab -
# Ensure scripts have execute permissions
chmod +x /usr/bin/zerotier-sync.sh 2>/dev/null
[ -f /usr/libexec/rpcd/luci-zerotier ] && chmod +x /usr/libexec/rpcd/luci-zerotier
# Enable at rc level so ZeroTier firewall rules are (re)applied at boot out of
# the box. Safe since r23: the service no longer re-enters fw4 from include
# contexts, and start() is idempotent (skips the firewall reload entirely when
# the desired rules are already in place). This must precede the ucitrack
# restart below, because ucitrack only registers triggers for enabled inits.
/etc/init.d/luci-zerotier enable
# Register the procd config trigger from our ucitrack JSON without a reboot
[ -x /etc/init.d/ucitrack ] && /etc/init.d/ucitrack restart
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd restart
# No uhttpd restart: it would kill the very LuCI session performing this
# install/upgrade; rpcd restart + dropping the index cache suffice for the
# new ACLs, RPC object and menu entries to be picked up.
rm -f /tmp/luci-indexcache
exit 0
@@ -0,0 +1,33 @@
{
"settings": {
"forceTcpRelay": false
}
}
// ZeroTier local.conf - Local Configuration
//
// This file configures ZeroTier's behavior when operating in restricted
// networks (e.g., behind corporate firewalls or in mainland China).
//
// NOTE: the UDP port is managed by the app's "Port" option (passed to the
// daemon as -p). Set it there, not here — a primaryPort entry in this file
// would compete with it.
//
// Settings:
// tcpFallbackRelay: TCP relay server for when UDP is blocked
// Format: "IP_ADDRESS/PORT"
// Example: "10.10.10.10/443"
// Replace with your own relay server IP or a public one
// forceTcpRelay: Set to true to force TCP mode (slower but works
// through most firewalls). Set to false to prefer UDP
// with TCP as fallback.
//
// To enable TCP fallback (default disabled, prefer UDP):
// 1. Add a tcpFallbackRelay line inside the "settings" object above,
// e.g. "tcpFallbackRelay": "10.10.10.10/443"
// (replace 10.10.10.10 with your relay server's IP)
// 2. Set forceTcpRelay to true if UDP is fully blocked in your network
// 3. All // comment lines are stripped automatically at install time
// (JSON itself does not support comments)
// 4. Refer to https://docs.zerotier.com/config#local-configuration-options
//