🎄 Sync 2026-10-04 22:59:03

This commit is contained in:
github-actions[bot] committed 2026-10-04 22:59:03 +08:00
1 parent c745b7a707
commit 8473506f3d
15 files changed
+176 -40

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.51
PKG_RELEASE:=59
PKG_RELEASE:=60
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
+1 -1
View File
@@ -29,7 +29,7 @@ LuCI **Firewall Live View** — client-side JS view polling `ubus fwlive poll` (
| `root/usr/libexec/fwlive-adaptive-cap.sh` | Layer 1 adaptive poll cap (sourced by `rpcd/fwlive`) |
| `root/usr/libexec/fwlive-logging.sh` | WAN zone logging helpers |
| `root/lib/upgrade/keep.d/luci-app-fwlive` | Keep `/etc/fwlive/` across sysupgrade |
| `/etc/fwlive/wan-log-baseline` | Written on first **Enable logging** (and best-effort on already-on enable); `prerm` restores it on uninstall. Restore is skipped (file kept; `fwlive` syslog only) when there is no WAN zone, the lock is unavailable, firewall changes are pending, the commit does not verify, reload fails, or the value after reload does not match the baseline. Disable of a pre-existing/foreign log bit is not snapshotted, so uninstall will not restore that bit. |
| `/etc/fwlive/wan-log-baseline` | Written on first **Enable logging** (and best-effort on already-on enable); `prerm` restores it on uninstall. A successful disable/reload, including an already-disabled retry, drops the marker when no later fwlive toggle has taken ownership; this preserves operator changes to other log-mask bits, including staged edits. If logging is already off but the marker remains, Disable retries `fw4 reload`; pending UCI changes return `firewall_changes_pending` without reloading, and reload failure returns `firewall_reload_failed` while retaining the marker. Lock, read, or generation mismatch also keeps it for uninstall recovery. Restore is skipped (file kept; `fwlive` syslog only) when there is no WAN zone, the lock is unavailable, firewall changes are pending, the commit does not verify, reload fails, or the value after reload does not match the baseline. Disable of a pre-existing/foreign log bit is not snapshotted, so uninstall will not restore that bit. |
| `root/usr/libexec/fwlive-log-filter.sh` | Server-side firewall-only filter (`isFirewallEvent` parity) |
| `root/usr/libexec/fwlive-is-firewall-event.sh` | Shared filter logic (sourced by filter + tests) |
| `root/usr/libexec/fwlive-is-firewall-event.awk` | **Generated** standalone classifier from `CLASSIFY_SPEC`; loaded by `fwlive-is-firewall-event.sh` |
@@ -1029,6 +1029,12 @@ commit_wan_log_change() {
report_wan_log_after_commit() {
_rc="$1"
zone_json="$2"
_drop_baseline=0
# The optional eighth argument opts in only when its value is exactly 1.
# Argument count alone must never authorize deleting recovery state.
if [ "${8:-0}" = 1 ]; then
_drop_baseline=1
fi
if [ "$_rc" -eq 1 ]; then
return 0
fi
@@ -1038,11 +1044,16 @@ report_wan_log_after_commit() {
wan_log_error_json "$zone_json" firewall_reload_failed
return 0
fi
if [ "$_drop_baseline" -eq 1 ]; then
drop_wan_log_baseline_after_disable "$zone" "$WAN_LOG_COMMIT_GENERATION"
fi
wan_log_error_json "$zone_json" firewall_commit_raced
return 0
fi
shift 2
reload_and_report_wan_log "$@" "$zone_json" "$WAN_LOG_COMMIT_GENERATION"
# Forward only the five documented operation fields plus an explicit flag;
# unrelated trailing caller arguments must not shift into the flag position.
reload_and_report_wan_log "$3" "$4" "$5" "$6" "$7" \
"$zone_json" "$WAN_LOG_COMMIT_GENERATION" "$_drop_baseline"
}
# Firewall reload + best-effort UCI rollback on reload failure. The reload
@@ -1065,6 +1076,10 @@ reload_and_report_wan_log() {
success_msg="$5"
zone_json="$6"
committed_generation="$7"
_drop_baseline=0
if [ "${8:-0}" = 1 ]; then
_drop_baseline=1
fi
if ! reload_firewall; then
# Re-acquire the logging lock so the rollback decision is atomic
@@ -1097,11 +1112,43 @@ reload_and_report_wan_log() {
wan_log_error_json "$zone_json" firewall_reload_failed
return 0
fi
if [ "$_drop_baseline" -eq 1 ]; then
drop_wan_log_baseline_after_disable "$zone" "$committed_generation"
fi
logger -t fwlive "$success_msg" 2>/dev/null || true
printf '{"ok":true,"changed":true,"wan_zone":%s}' "$zone_json"
return 0
}
# After a successful disable/reload, discard the uninstall marker unless a
# later fwlive toggle has taken ownership again. Pending external UCI edits
# are preserved by retiring the stale marker too; a valid `uci show` confirms
# the config is readable before unlinking it.
drop_wan_log_baseline_after_disable() {
drop_zone="$1"
drop_generation="$2"
_drop_path="$(wan_log_baseline_path)"
[ -f "$_drop_path" ] && [ ! -L "$_drop_path" ] || return 0
if ! acquire_wan_log_lock; then
return 0
fi
if ! uci -q show "firewall.${drop_zone}" >/dev/null 2>&1; then
release_wan_log_lock
return 0
fi
_drop_current_generation=$(wan_log_generation_read 2>/dev/null) || {
release_wan_log_lock
return 0
}
if [ -z "$drop_generation" ] || [ "$drop_generation" = unavailable ] \
|| [ "$_drop_current_generation" != "$drop_generation" ]; then
release_wan_log_lock
return 0
fi
rm -f "$_drop_path" 2>/dev/null || true
release_wan_log_lock
}
# $1=enable checks nf_log before the lock. Success leaves the lock held
# and zone / zone_json set. Failure prints JSON and returns non-zero.
wan_log_toggle_preamble() {
@@ -1182,12 +1229,27 @@ disable_wan_logging() {
current=$(wan_zone_log_value "$zone")
if [ -z "$current" ] || ! wan_filter_log_enabled "$current"; then
if ! wan_log_generation_bump >/dev/null; then
_disable_generation=$(wan_log_generation_bump) || {
release_wan_log_lock
wan_log_tracking_failed_json "$zone_json"
return 0
}
_baseline_path="$(wan_log_baseline_path)"
if [ -f "$_baseline_path" ] && [ ! -L "$_baseline_path" ] \
&& firewall_changes_pending; then
release_wan_log_lock
wan_log_error_json "$zone_json" firewall_changes_pending
return 0
fi
release_wan_log_lock
if [ -f "$_baseline_path" ] && [ ! -L "$_baseline_path" ]; then
if ! reload_firewall; then
logger -t fwlive "Firewall reload failed after already-disabled request" 2>/dev/null || true
wan_log_error_json "$zone_json" firewall_reload_failed
return 0
fi
drop_wan_log_baseline_after_disable "$zone" "$_disable_generation"
fi
printf '{"ok":true,"changed":false,"wan_zone":%s}' "$zone_json"
return 0
fi
@@ -1203,7 +1265,7 @@ disable_wan_logging() {
report_wan_log_after_commit "$_commit_rc" "$zone_json" \
"$zone" "$current" "$target" \
'Firewall reload failed after disable; reverted UCI WAN log' \
'WAN zone logging disabled'
'WAN zone logging disabled' 1
return 0
}
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.3
PKG_RELEASE:=312
PKG_VERSION:=26.10.4
PKG_RELEASE:=313
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -429,6 +429,16 @@ if singbox_tags:find("with_quic") then
o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps"))
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_stream_recv_win", translate("QUIC stream receive window"))
o.datatype = "uinteger"
o.placeholder = "8388608"
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_conn_recv_win", translate("QUIC connection receive window"))
o.datatype = "uinteger"
o.placeholder = "20971520"
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o:depends({ protocol = "hysteria2"})
@@ -361,6 +361,16 @@ o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps"))
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_stream_recv_win", translate("QUIC stream receive window"))
o.datatype = "uinteger"
o.placeholder = "8388608"
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_conn_recv_win", translate("QUIC connection receive window"))
o.datatype = "uinteger"
o.placeholder = "20971520"
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o:depends({ protocol = "hysteria2"})
@@ -109,10 +109,14 @@ o.rewrite_option = o.option
o = s:option(Value, "down_mbps", translate("Max download Mbps"))
o.rewrite_option = o.option
o = s:option(Value, "recv_window", translate("QUIC stream receive window"))
o = s:option(Value, "stream_recv_win", translate("QUIC stream receive window"))
o.datatype = "uinteger"
o.placeholder = "8388608"
o.rewrite_option = o.option
o = s:option(Value, "recv_window_conn", translate("QUIC connection receive window"))
o = s:option(Value, "conn_recv_win", translate("QUIC connection receive window"))
o.datatype = "uinteger"
o.placeholder = "20971520"
o.rewrite_option = o.option
o = s:option(Value, "idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
@@ -144,8 +144,8 @@ function gen_config(var)
ech = (node.ech == "1") and node.ech_config or nil
},
quic = {
initStreamReceiveWindow = (node.hysteria2_recv_window) and tonumber(node.hysteria2_recv_window) or nil,
initConnReceiveWindow = (node.hysteria2_recv_window_conn) and tonumber(node.hysteria2_recv_window_conn) or nil,
initStreamReceiveWindow = node.hysteria2_stream_recv_win and tonumber(node.hysteria2_stream_recv_win) or nil,
initConnReceiveWindow = node.hysteria2_conn_recv_win and tonumber(node.hysteria2_conn_recv_win) or nil,
maxIdleTimeout = (function(t)
t = tonumber(tostring(t or "30"):match("^%d+"))
return (t and t >= 4 and t <= 120) and t .. "s" or "30s"
@@ -623,7 +623,9 @@ function gen_outbound(flag, node, tag, proxy_table)
t = tonumber(tostring(t or "0"):match("^%d+"))
return (t and t >= 2 and t <= 60) and t .. "s" or nil
end)(node.hysteria2_keep_alive_period),
disable_path_mtu_discovery = version_ge_1_14_0 and (tonumber(node.hysteria2_disable_mtu_discovery) == 1) or nil,
stream_receive_window = tonumber(node.hysteria2_stream_recv_win) and tonumber(node.hysteria2_stream_recv_win)/1048576 .. " MB" or nil,
connection_receive_window = tonumber(node.hysteria2_conn_recv_win) and tonumber(node.hysteria2_conn_recv_win)/1048576 .. " MB" or nil,
disable_path_mtu_discovery = version_ge_1_14_0 and (node.hysteria2_disable_mtu_discovery == "1") or nil,
tls = tls,
realm = node.hysteria2_realms and (function()
result.server = nil
@@ -364,7 +364,9 @@ function gen_outbound(flag, node, tag, proxy_table)
t = tonumber(tostring(t or "0"):match("^%d+"))
return (t and t >= 2 and t <= 60) and t or nil
end)(node.hysteria2_keep_alive_period),
disablePathMTUDiscovery = tonumber(node.hysteria2_disable_mtu_discovery) == 1
disablePathMTUDiscovery = node.hysteria2_disable_mtu_discovery == "1",
initStreamReceiveWindow = node.hysteria2_stream_recv_win and tonumber(node.hysteria2_stream_recv_win) or nil,
initConnectionReceiveWindow = node.hysteria2_conn_recv_win and tonumber(node.hysteria2_conn_recv_win) or nil,
}
end
if fragment and fragment_table and ({raw=1, ws=1, httpupgrade=1, grpc=1, xhttp=1})[TP] then
+1 -1
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI Status Pages
LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket
PKG_RELEASE:=25
PKG_RELEASE:=26
PKG_BUILD_DEPENDS:=iwinfo
PKG_LICENSE:=Apache-2.0
@@ -28,7 +28,7 @@ function parseIwDevMap(stdout) {
return Object.entries(byPhy).reduce((map, entry) => {
const phy = entry[0];
const ifaces = entry[1];
const preferred = ifaces.find((ifname) => !/^wifi\d+$/.test(ifname)) || ifaces[0];
const preferred = ifaces.find((ifname) => !/^(wifi\d+$|tmpsta\d+$|tmp\.)/.test(ifname)) || ifaces.find((ifname) => /^wifi\d+$/.test(ifname)) || ifaces[0];
const temp = `tmpsta${phy.replace(/^phy/, '')}`;
ifaces.forEach((ifname) => {
@@ -374,7 +374,19 @@ return view.extend({
.finally(L.bind(this.cleanupTemporaryScanIface, this, tempIfname));
},
isQcaRadio(radioDev) {
return ['qcawifi', 'qcawificfg80211'].includes(radioDev.get('type'));
},
resolveScanDevice(radioDev) {
if (this.isQcaRadio(radioDev)) {
// QCA netifd may expose logical wifiN.networkM names instead of VAPs.
const radio = this.iwDevMap?.[radioDev.getName()];
const ifname = radio?.preferred;
if (!ifname || ifname == radioDev.getName())
return Promise.reject(new Error(_('No active wireless interface found for %s').format(radioDev.getName())));
return Promise.resolve(ifname);
}
return radioDev.getWifiNetworks().then((networks) => {
const ap = networks.find(net => net.getMode() == 'ap') || networks[0];
return ap?.getIfname() || radioDev.getName();
@@ -386,6 +398,15 @@ return view.extend({
return radioDev.getScanList();
return this.resolveScanDevice(radioDev).then((scanIfname) => {
if (this.isQcaRadio(radioDev)) {
// QCA STA teardown also stops AP VAPs through the repeater state machine.
// Scan the existing VAP without changing the radio topology.
return fs.exec('/usr/sbin/iw', [ 'dev', scanIfname, 'scan', 'ap-force' ]).then((res) => {
if (res.code !== 0 || /scan aborted!/i.test(res.stdout || ''))
throw new Error(res.stderr?.trim() || res.stdout?.trim() || _('Wireless scan failed'));
return parseIwScan(res.stdout);
});
}
const iwDev = this.iwDevMap?.[scanIfname] || this.iwDevMap?.[radioDev.getName()];
const scanTasks = [];
@@ -793,24 +814,19 @@ return view.extend({
return Promise.all([
this.loadSVG(L.resource('svg/channel_analysis.svg')),
L.resolveDefault(fs.exec('/usr/sbin/iw', [ 'dev' ]), null),
network.getWifiDevices().then(L.bind(function(data) {
const tasks = [], ret = [];
network.getWifiDevices()
]).then(([svg, iwDevs, devices]) => {
this.iwDevMap = parseIwDevMap(iwDevs?.stdout);
const ret = {};
for (let d of data) {
ret[d.getName()] = { dev : d };
tasks.push(this.callFrequencyList(d.getName())
.then(L.bind(function(radio, data) {
ret[radio.getName()].freq = data;
}, this, d)));
}
return Promise.all(tasks).then(function() { return ret; })
}, this))
]).then((data) => {
this.iwDevMap = parseIwDevMap(data[1]?.stdout);
return [ data[0], data[2] ];
return Promise.all(devices.map((dev) => {
const name = dev.getName();
// Query an existing QCA VAP; querying wifiN creates a temporary VAP.
const target = this.isQcaRadio(dev) ? this.iwDevMap[name]?.preferred : name;
const freqs = target && (!this.isQcaRadio(dev) || target != name)
? this.callFrequencyList(target) : Promise.resolve([]);
return freqs.then((freq) => { ret[name] = { dev, freq }; });
})).then(() => [svg, ret]);
});
},
+35 -3
View File
@@ -2,9 +2,15 @@ const assert = require('assert');
const fs = require('fs');
const path = require('path');
const source = fs.readFileSync(path.join(__dirname, '../htdocs/luci-static/resources/view/status/channel_analysis.js'), 'utf8');
const view = new Function('view', 'rpc', 'L', 'fs', source)(
const execCalls = [];
let devices = [];
const frequencies = [];
let execResult = {code: 0, stdout: ''};
const view = new Function('view', 'rpc', 'L', 'fs', 'network', source)(
{extend: o => o}, {declare: () => () => {}},
{resolveDefault: (p, fallback) => p.catch(() => fallback)}, {}
{resolveDefault: (p, fallback) => p.catch(() => fallback), resource: p => p},
{exec: async (cmd, args) => {execCalls.push([cmd, args]); return execResult;}},
{getWifiDevices: async () => devices}
);
view.updateScanButton = () => {};
const dev = (name, type) => ({getName: () => name, get: () => type});
@@ -12,9 +18,35 @@ const dev = (name, type) => ({getName: () => name, get: () => type});
const mt = dev('ra', 'mt_dbdc');
mt.getScanList = async () => [{ssid:'mtwifi'}];
assert.deepEqual(await view.getScanResultsForRadio(mt), [{ssid:'mtwifi'}]);
String.prototype.format = function(...args) {let i=0;return this.replace(/%s/g,()=>args[i++]);};
global._ = s => s;
const qca = dev('wifi0', 'qcawificfg80211');
qca.getWifiNetworks = async () => [{getMode:()=> 'ap',getIfname:()=> 'ath0'}];
view.iwDevMap = {wifi0: {phy:'phy0', preferred:'ath0', temp:'tmpsta0'}};
qca.getWifiNetworks = async () => [{getMode:()=> 'ap',getIfname:()=> 'wifi0.network1'}];
assert.equal(await view.resolveScanDevice(qca), 'ath0');
assert.deepEqual(await view.getScanResultsForRadio(qca), []);
assert.deepEqual(execCalls, [['/usr/sbin/iw', ['dev','ath0','scan','ap-force']]], 'QCA scans a real VAP without creating a temporary STA');
execResult = {code:237, stderr:'No such device'};
await assert.rejects(view.getScanResultsForRadio(qca), /No such device/);
execResult = {code:0, stdout:'scan aborted!'};
await assert.rejects(view.getScanResultsForRadio(qca), /scan aborted/);
await assert.rejects(view.resolveScanDevice(dev('wifi2', 'qcawifi')), /No active wireless interface/);
execResult = {code:0, stdout:'BSS 00:11:22:33:44:55(on ath0)\n\tfreq: 2412\n\tsignal: -42.00 dBm\n\tSSID: test\n'};
const scan = await view.getScanResultsForRadio(qca);
assert.equal(scan[0].band,2);
assert.equal(scan[0].signal,-42);
// Frequency queries must also use real VAPs, including a third QCA radio.
devices = [qca,dev('wifi2','qcawifi'),dev('radio0','mac80211')];
execResult = {code:0,stdout:'phy#7\n\tInterface tmpsta7\n\tInterface ath2\n\tInterface wifi2\nphy#0\n\tInterface ath0\n\tInterface wifi0\n'};
view.loadSVG = async () => 'svg';
view.callFrequencyList = async name => {frequencies.push(name);return [];};
await view.load();
assert.deepEqual(frequencies, ['ath0','ath2','radio0']);
assert.equal(await view.resolveScanDevice(dev('wifi2','qcawifi')), 'ath2');
const generic=dev('radio0','mac80211');
generic.getWifiNetworks=async()=>[{getMode:()=> 'ap',getIfname:()=> 'wlan0'}];
assert.equal(await view.resolveScanDevice(generic),'wlan0');
let calls = [], release;
view.getScanResultsForRadio = d => {
calls.push(d.getName());
@@ -6,8 +6,6 @@
{% include('header') %}
<h2 name="content">{{ _('Status') }}</h2>
<div id="view">
<div class="spinning">{{ _('Loading view…') }}</div>
</div>
+2 -2
View File
@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=msd_lite
PKG_RELEASE:=4
PKG_RELEASE:=5
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/rozhuk-im/msd_lite.git
PKG_SOURCE_DATE:=2026-07-21
PKG_SOURCE_VERSION:=null
PKG_SOURCE_VERSION:=fa68e131343fb58c67ad77b2d26f2cb7c49a2c95
PKG_MIRROR_HASH:=skip
PKG_MAINTAINER:=Tianling Shen <cnsztl@immrotalwrt.org>