mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 09:54:06 +08:00
⛅ Sync 2026-10-03 02:37:08
This commit is contained in:
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
@@ -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
|
||||
//
|
||||
Reference in new issue
Block a user