mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 01:44:16 +08:00
🔥 Sync 2026-10-24 23:19:23
This commit is contained in:
1 parent
9886197604
commit
37e1475e87
59 files changed
+5167
-1483
No files matched your search
@@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
# Background worker for the "Auto Configure Firewall" action.
|
||||
#
|
||||
# Kept outside the rpcd plugin on purpose: the RPC must return before the
|
||||
# network is reloaded, otherwise a request coming through the network being
|
||||
# reloaded (reverse proxy, or LuCI over Tailscale itself) dies with an XHR
|
||||
# abort and the user cannot tell whether anything happened.
|
||||
#
|
||||
# The status file is line based rather than JSON so that a message containing
|
||||
# a backslash, a quote or a tab cannot corrupt it: whoever reads it builds the
|
||||
# JSON itself. Line 4 is the run id handed over by the RPC, so a result from a
|
||||
# previous run can never be mistaken for this one.
|
||||
#
|
||||
# Usage: tailscale-setup-firewall <run-id>
|
||||
|
||||
STATE_FILE=/var/run/tailscale-fwsetup.status
|
||||
LOCK_FILE=/var/run/tailscale-fwsetup.lock
|
||||
|
||||
RUN_ID="$1"
|
||||
|
||||
write_state() {
|
||||
{
|
||||
printf '%s\n' "$1"
|
||||
printf '%s\n' "$2"
|
||||
# one line only: the reader takes fixed line numbers, so a message that
|
||||
# wrapped would shift the run id off the last line. tr cannot strip a
|
||||
# real newline, sed can.
|
||||
if [ -n "$3" ]; then
|
||||
printf '%s' "$3" | sed -e 's/\r//g' -e ':a' -e 'N' -e '$!ba' -e 's/\n/ /g'
|
||||
fi
|
||||
printf '\n'
|
||||
printf '%s\n' "$RUN_ID"
|
||||
} > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||
}
|
||||
|
||||
# Only one worker at a time: a second click while a reload is running would
|
||||
# queue another network reload on top of an unfinished one. mkdir is atomic and
|
||||
# is the only portable primitive available here; a test-then-write pair lets
|
||||
# two workers through at the same moment. Stale locks are recognised by age so
|
||||
# a recycled pid or a killed worker cannot block forever.
|
||||
LOCK_DIR="$LOCK_FILE.d"
|
||||
lock_age() {
|
||||
local ts now
|
||||
ts=$(cat "$LOCK_DIR/ts" 2>/dev/null)
|
||||
case "$ts" in ''|*[!0-9]*) echo 999999; return ;; esac
|
||||
now=$(date +%s)
|
||||
echo $((now - ts))
|
||||
}
|
||||
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
if [ "$(lock_age)" -gt 300 ]; then
|
||||
# the holder is gone; take the lock over
|
||||
rm -rf "$LOCK_DIR"
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
|
||||
else
|
||||
# someone else is already reloading: say so instead of leaving the
|
||||
# status file at "queued" and letting the UI time out
|
||||
write_state busy locked "Another firewall reload is already running"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "$$" > "$LOCK_DIR/pid"
|
||||
date +%s > "$LOCK_DIR/ts"
|
||||
|
||||
trap 'write_state failed aborted "Worker terminated unexpectedly"; rm -rf "$LOCK_DIR"' EXIT HUP INT TERM
|
||||
|
||||
write_state running network "Reloading network"
|
||||
|
||||
if ! /etc/init.d/network reload >/dev/null 2>&1; then
|
||||
write_state failed network "Network reload failed"
|
||||
trap - EXIT HUP INT TERM
|
||||
rm -rf "$LOCK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_state running firewall "Reloading firewall"
|
||||
|
||||
if ! /etc/init.d/firewall reload >/dev/null 2>&1; then
|
||||
write_state failed firewall "Firewall reload failed"
|
||||
trap - EXIT HUP INT TERM
|
||||
rm -rf "$LOCK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_state done done "Firewall configuration applied"
|
||||
trap - EXIT HUP INT TERM
|
||||
rm -rf "$LOCK_DIR"
|
||||
+4
-2
@@ -7,7 +7,8 @@
|
||||
"get_status",
|
||||
"get_settings",
|
||||
"get_subroutes",
|
||||
"get_logs"
|
||||
"get_logs",
|
||||
"get_firewall_status"
|
||||
]
|
||||
},
|
||||
"uci": [ "tailscale" ]
|
||||
@@ -17,7 +18,8 @@
|
||||
"tailscale": [
|
||||
"do_login",
|
||||
"do_logout",
|
||||
"setup_firewall"
|
||||
"setup_firewall",
|
||||
"set_service_enabled"
|
||||
]
|
||||
},
|
||||
"uci": [ "tailscale" ]
|
||||
|
||||
@@ -149,7 +149,6 @@ methods.do_login = {
|
||||
if (status.status != 'logout') {
|
||||
return { error: 'Tailscale is already logged in and running.' };
|
||||
}
|
||||
|
||||
// --- 1. Prepare and Run Login Command (Once) ---
|
||||
const loginserver = trim(form_data.loginserver) || '';
|
||||
const loginserver_authkey = trim(form_data.loginserver_authkey) || '';
|
||||
@@ -177,7 +176,7 @@ methods.do_login = {
|
||||
let parts = split(trline, ' ');
|
||||
for (let part in parts) {
|
||||
if (index(part, 'http') != -1) {
|
||||
return { url: part };
|
||||
return { success: true, url: part };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,7 +198,7 @@ methods.do_logout = {
|
||||
if (logout_result.code != 0) {
|
||||
return { error: 'Failed to logout: ' + logout_result.stderr };
|
||||
}
|
||||
return { success: true };
|
||||
return { success: true, message: 'Logged out.' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,6 +240,7 @@ methods.setup_firewall = {
|
||||
|
||||
let changed_network = false;
|
||||
let changed_firewall = false;
|
||||
let run_id = 0;
|
||||
|
||||
// 1. config Network Interface
|
||||
let net_ts = uci.get('network', 'tailscale');
|
||||
@@ -335,24 +335,57 @@ methods.setup_firewall = {
|
||||
changed_firewall = true;
|
||||
}
|
||||
|
||||
// 4. save
|
||||
// 4. save and hand the reload to a background worker
|
||||
//
|
||||
// Reloading network/firewall synchronously here was the reason the
|
||||
// button failed through a reverse proxy: the reload tears down the
|
||||
// connection carrying this very request. The worker records its
|
||||
// progress in /var/run/tailscale-fwsetup.status and the UI polls it.
|
||||
if (changed_network) {
|
||||
uci.save('network');
|
||||
uci.commit('network');
|
||||
exec('/etc/init.d/network reload');
|
||||
}
|
||||
|
||||
if (changed_firewall) {
|
||||
uci.save('firewall');
|
||||
uci.commit('firewall');
|
||||
exec('/etc/init.d/firewall reload');
|
||||
}
|
||||
|
||||
if (changed_network || changed_firewall) {
|
||||
if (!access('/usr/libexec/tailscale-setup-firewall')) {
|
||||
return { error: 'Firewall worker /usr/libexec/tailscale-setup-firewall is missing. Reinstall the package.' };
|
||||
}
|
||||
|
||||
// The run id ties the polled result to this request, and the
|
||||
// queued state replaces any previous result before the worker
|
||||
// gets a chance to overwrite it. Without both, a poll arriving
|
||||
// while the worker is still starting would read the previous
|
||||
// run's "done" and report success for work that has not run.
|
||||
run_id = time();
|
||||
let st = writefile('/var/run/tailscale-fwsetup.status.tmp',
|
||||
'queued\nstart\n\n' + run_id + '\n');
|
||||
if (st == null || rename('/var/run/tailscale-fwsetup.status.tmp', '/var/run/tailscale-fwsetup.status') == null) {
|
||||
// Without a writable status file the poll can never see a
|
||||
// result, so say so instead of letting the UI wait it out.
|
||||
return { error: 'Cannot write the firewall status file in /var/run. Configuration was not changed.' };
|
||||
}
|
||||
|
||||
// The background shell exits immediately, so its status only
|
||||
// tells us whether sh itself started. Anything the worker
|
||||
// reports afterwards lands in the status file.
|
||||
let spawn = exec('/bin/sh -c ' + shell_quote('/usr/libexec/tailscale-setup-firewall ' + run_id + ' >/dev/null 2>&1 &'));
|
||||
if (spawn.code != 0) {
|
||||
return { error: 'Failed to start the firewall worker: ' + join('\n', spawn.stdout) };
|
||||
}
|
||||
}
|
||||
|
||||
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.'
|
||||
job: (changed_network || changed_firewall) ? 'running' : 'idle',
|
||||
run_id: run_id,
|
||||
message: (changed_network || changed_firewall) ? 'Configuration committed, reloading network and firewall in the background.' : 'Tailscale firewall/interface already configured.'
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
@@ -361,6 +394,75 @@ methods.setup_firewall = {
|
||||
}
|
||||
};
|
||||
|
||||
// Record that the user asked to disable the service.
|
||||
//
|
||||
// The actual stop happens in /etc/init.d/tailscale-settings on the reload
|
||||
// trigger, which runs right after rpcd commits and *before* it arms the LuCI
|
||||
// rollback timer. Stopping here would not survive a rollback: rpcd restores
|
||||
// the config files but never restarts a daemon it did not stop, so the user
|
||||
// would end up with service_enabled=1 and a dead tunnel. The request file is
|
||||
// dropped by the reload handler as soon as the config says enabled again.
|
||||
methods.set_service_enabled = {
|
||||
args: { enabled: '' },
|
||||
call: function(request) {
|
||||
let enabled = request?.args?.enabled;
|
||||
if (enabled != '0' && enabled != '1') {
|
||||
return { error: 'Invalid enabled value, expected 0 or 1.' };
|
||||
}
|
||||
|
||||
if (enabled == '1') {
|
||||
let r = exec('rm -f /var/run/tailscale-settings/disable-requested');
|
||||
if (r.code != 0) {
|
||||
return { error: 'Failed to clear the disable request: ' + join('\n', r.stdout) };
|
||||
}
|
||||
return { success: true, armed: false };
|
||||
}
|
||||
|
||||
// No daemon probe here: get_status shells out to 'tailscale status'
|
||||
// and would delay the apply for nothing. A request armed on a box
|
||||
// without the tailscale package is simply ignored by the reload
|
||||
// handler, which bails out when the init script is missing.
|
||||
let r = exec('mkdir -p /var/run/tailscale-settings && touch /var/run/tailscale-settings/disable-requested');
|
||||
if (r.code != 0) {
|
||||
return { error: 'Failed to arm the disable request: ' + join('\n', r.stdout) };
|
||||
}
|
||||
|
||||
return { success: true, armed: true };
|
||||
}
|
||||
};
|
||||
|
||||
// Result of the last background firewall setup job.
|
||||
//
|
||||
// Read here rather than in the browser so the ACL needs no file rights: the
|
||||
// ucode plugin runs as root and rpcd only enforces path ACLs on the file
|
||||
// ubus object.
|
||||
methods.get_firewall_status = {
|
||||
call: function() {
|
||||
let path = '/var/run/tailscale-fwsetup.status';
|
||||
if (!access(path)) {
|
||||
return { success: true, state: 'idle', step: '', message: '', run_id: 0 };
|
||||
}
|
||||
try {
|
||||
let raw = readfile(path);
|
||||
if (raw == null) {
|
||||
return { success: true, state: 'idle', step: '', message: '', run_id: 0 };
|
||||
}
|
||||
let lines = split(rtrim(raw, '\n'), '\n');
|
||||
return {
|
||||
success: true,
|
||||
state: lines[0] || 'unknown',
|
||||
step: lines[1] || '',
|
||||
message: lines[2] || '',
|
||||
// the run id is always the last line, so a message that somehow
|
||||
// still contains a newline cannot shift it into line 3
|
||||
run_id: int(lines[length(lines) - 1]) || 0
|
||||
};
|
||||
} catch (e) {
|
||||
return { success: true, state: 'unknown', step: '', message: 'Unreadable job state: ' + e, run_id: 0 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
methods.get_logs = {
|
||||
args: { lines: 200 },
|
||||
call: function(request) {
|
||||
@@ -368,9 +470,9 @@ methods.get_logs = {
|
||||
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 { success: true, logs: result.stdout };
|
||||
}
|
||||
return { logs: [], error: join(' ', result.stderr) };
|
||||
return { success: false, logs: [], error: join(' ', result.stderr) };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user