🤞 Sync 2026-10-06 13:52:46

This commit is contained in:
github-actions[bot] committed 2026-10-06 13:52:46 +08:00
1 parent 4d2d69bed9
commit 947eaea2ee
42 files changed
+2681 -1582

No files matched your search

+10 -7
View File
@@ -1,19 +1,22 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-flowproxy
PKG_VERSION:=1.0.4
PKG_RELEASE:=4
PKG_VERSION:=1.0.3
PKG_RELEASE:=5
LUCI_TITLE:=LuCI Support for FlowProxy Traffic Diversion
LUCI_DEPENDS:=+luci-base +nftables +kmod-nft-core +ip-full
LUCI_DEPENDS:=+luci-base +nftables +ip-full
LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/conffiles
/etc/config/flowproxy
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
[ -f /etc/uci-defaults/99_flowproxy ] && ( . /etc/uci-defaults/99_flowproxy ) && rm -f /etc/uci-defaults/99_flowproxy
(sleep 1; /etc/init.d/rpcd reload) >/dev/null 2>&1 &
fi
[ -n "$${IPKG_INSTROOT}" ] || {
/etc/init.d/rpcd restart
}
exit 0
endef
@@ -40,18 +40,6 @@ var callRestartService = rpc.declare({
method: 'restart_service'
});
var callGetDatasetInfo = rpc.declare({
object: 'luci.flowproxy',
method: 'get_dataset_info',
params: ['file_path']
});
var callUpdateDataset = rpc.declare({
object: 'luci.flowproxy',
method: 'update_dataset',
params: ['url', 'file_path']
});
return L.view.extend({
load: function() {
return Promise.all([
@@ -108,12 +96,6 @@ return L.view.extend({
}, [ _('Restart') ]));
} else {
content.push(E('span', { 'style': 'color: red; font-weight: bold;' }, [ _('stopped') ]));
content.push(' ');
content.push(E('button', {
'class': 'btn cbi-button cbi-button-apply',
'style': 'margin-left: 10px;',
'click': ui.createHandlerFn(this, 'handleRestart')
}, [ _('Restart') ]));
}
L.dom.content(statusEl, content);
}
@@ -207,54 +189,11 @@ return L.view.extend({
s.taboption('settings', form.Flag, 'dns_proxy_enabled', _('Force upstream DNS to proxy server')).rmempty = false;
o = s.taboption('settings', form.Value, 'proxy_server_ip_addr', _('Proxy server IP address'));
o.datatype = 'ip4addr'; o.rmempty = false;
o.placeholder = '192.168.1.254';
o.rmempty = true;
o.validate = function(section_id, value) {
var isEnabled = (s.formvalue(section_id, 'enabled') === '1');
// 1. 如果值为空:仅当启用了服务尝试保存时才提示必填;未启用时允许留空,绝不展示刺眼红框
if (!value || value.trim() === '') {
if (isEnabled) {
return _('Proxy server IP address is required when FlowProxy is enabled.');
}
return true;
}
value = value.trim();
// 2. 基础 IPv4 格式校验
if (!value.match(/^([0-9]{1,3}\.){3}[0-9]{1,3}$/)) {
return _('Invalid IPv4 address format');
}
var octets = value.split('.');
for (var oi = 0; oi < 4; oi++) {
var oct = parseInt(octets[oi], 10);
if (isNaN(oct) || oct < 0 || oct > 255) {
return _('Invalid IPv4 address format');
}
}
if (value === '127.0.0.1' || value === '0.0.0.0') {
return _('Invalid proxy server IP address.');
}
// 3. 拦截本路由器自身的 IP 地址,彻底避免产生自指路由环路导致网络崩溃
var allLocalIps = [];
for (var k = 0; k < ifaces.length; k++) {
var addrs = ifaces[k].getIPAddrs() || [];
for (var a = 0; a < addrs.length; a++) {
var pureIp = addrs[a].split('/')[0];
if (pureIp && allLocalIps.indexOf(pureIp) === -1) {
allLocalIps.push(pureIp);
}
}
}
if (allLocalIps.indexOf(value) !== -1) {
return _('The proxy server IP address cannot be the local router IP (%s).').format(value);
}
var ipInCidr = function(ipStr, cidrStr) {
if (!cidrStr || cidrStr.indexOf(':') !== -1 || !ipStr || ipStr.indexOf(':') !== -1) return null;
if (!cidrStr || cidrStr.indexOf(':') !== -1 || !ipStr || ipStr.indexOf(':') !== -1) return false;
var parts = cidrStr.split('/');
var cidrIp = parts[0];
var maskBits = parseInt(parts[1] || '32', 10);
@@ -270,19 +209,15 @@ return L.view.extend({
var ipNum = ipToNum(ipStr);
var cidrNum = ipToNum(cidrIp);
if (maskBits === 0) return { inSubnet: true, isBcastOrNet: false };
if (maskBits === 0) return true;
var mask = (0xffffffff << (32 - maskBits)) >>> 0;
var inSubnet = ((ipNum & mask) === (cidrNum & mask));
var netNum = (cidrNum & mask) >>> 0;
var bcastNum = (netNum | (~mask >>> 0)) >>> 0;
var isBcastOrNet = (maskBits < 31 && (ipNum === netNum || ipNum === bcastNum));
return { inSubnet: inSubnet, isBcastOrNet: isBcastOrNet };
return (ipNum & mask) === (cidrNum & mask);
};
var selected_interface = s.formvalue(section_id, 'interface');
if (!selected_interface || selected_interface === 'auto') return true;
if (!value || !selected_interface) return true;
var target_iface = null;
for (var i = 0; i < ifaces.length; i++) {
@@ -299,12 +234,9 @@ return L.view.extend({
if (!ips || ips.length === 0) return true;
var inSubnet = false;
var isBcastOrNet = false;
for (var j = 0; j < ips.length; j++) {
var res = ipInCidr(value, ips[j]);
if (res && res.inSubnet) {
if (ipInCidr(value, ips[j])) {
inSubnet = true;
if (res.isBcastOrNet) isBcastOrNet = true;
break;
}
}
@@ -313,61 +245,19 @@ return L.view.extend({
return _('The proxy server IP address must be within the subnet of the selected interface (%s).').format(ips.join(', '));
}
if (isBcastOrNet) {
return _('The proxy server IP address cannot be the network address or broadcast address.');
}
return true;
};
o = s.taboption('settings', form.Value, 'proxy_server_dns_port', _('Proxy server DNS port'));
o.datatype = 'port'; o.default = '5353'; o.rmempty = false;
var iface_opt = s.taboption('settings', form.ListValue, 'interface', _('Proxy server interface'));
iface_opt.value('auto', _('Auto (Detect automatically)'));
devices.forEach(function(d) { iface_opt.value(d.getName(), d.getName()); });
iface_opt.default = 'auto';
iface_opt.onchange = function(ev, sid, val) {
var ip_opt = m.lookupOption('proxy_server_ip_addr', sid)[0];
if (ip_opt) {
var currVal = ip_opt.formvalue(sid);
// 仅当输入框已有非空输入时才触发重新比对子网,避免在空值时派发失焦导致误报红框
if (currVal && currVal.trim() !== '') {
var el = document.getElementById(ip_opt.cbid(sid));
if (el) {
var inputEl = el.tagName === 'INPUT' ? el : el.querySelector('input');
if (inputEl) {
inputEl.dispatchEvent(new CustomEvent('input', { bubbles: true }));
}
}
}
}
};
o = s.taboption('settings', form.ListValue, 'interface', _('Proxy server interface'));
devices.forEach(function(d) { o.value(d.getName(), d.getName()); });
o.default = 'br-lan';
o = s.taboption('settings', form.Value, 'traffic_mark', _('Traffic Mark'));
o.datatype = 'or(uinteger, hex)';
o.default = '0x666';
o.validate = function(section_id, value) {
if (!value) return true;
var num = parseInt(value, (typeof value === 'string' && value.indexOf('0x') === 0) ? 16 : 10);
if (isNaN(num) || num <= 0) return _('Traffic mark cannot be 0.');
return true;
};
o = s.taboption('settings', form.Value, 'routing_table', _('Routing Table ID'));
o.datatype = 'and(uinteger,range(1, 4294967295))';
o.default = '888';
o.validate = function(section_id, value) {
var num = parseInt(value, 10);
if (num === 0 || num === 253 || num === 254 || num === 255) {
return _('Routing table ID %d is reserved by the system (0, 253, 254, 255). Please use another ID.').format(num);
}
return true;
};
o = s.taboption('settings', form.Value, 'rule_priority', _('Rule Priority'));
o.datatype = 'and(uinteger,range(10, 32765))';
o.default = '1000';
s.taboption('settings', form.Value, 'traffic_mark', _('Traffic Mark')).datatype = 'or(uinteger, hex)';
s.taboption('settings', form.Value, 'routing_table', _('Routing Table ID')).datatype = 'and(uinteger,range(1, 4294967295))';
s.taboption('settings', form.Value, 'rule_priority', _('Rule Priority')).datatype = 'and(uinteger,range(0, 4294967295))';
// --- Rules ---
var nftsets = uci.sections('flowproxy', 'nftset').map(function(ss) { return '@' + ss['.name']; });
@@ -529,14 +419,16 @@ return L.view.extend({
}
return true;
};
// 联动:当 match_type 改变时,仅在已有输入内容时重新校验 match_value
// 联动:当 match_type 改变时,强制触发 match_value 的校验
match_type.onchange = function(ev, sid, val) {
var value_input = m.lookupOption('match_value', sid)[0];
if (value_input) setTimeout(function() {
var el = document.getElementById(value_input.cbid(sid));
if (el) {
var inputEl = el.tagName === 'INPUT' ? el : el.querySelector('input');
if (inputEl && inputEl.value && inputEl.value.trim() !== '') {
if (inputEl) {
inputEl.dispatchEvent(new CustomEvent('change', { bubbles: true }));
inputEl.dispatchEvent(new CustomEvent('blur', { bubbles: true }));
inputEl.dispatchEvent(new CustomEvent('input', { bubbles: true }));
}
}
@@ -568,15 +460,7 @@ return L.view.extend({
var path = uci.get('flowproxy', sid, 'file_path') || '/usr/share/flowproxy/chnroute.txt';
var resNode = E('span', { 'style': 'margin-left: 10px; font-weight: bold; color: #444;', 'id': 'line-count-status' }, [ '...' ]);
var input = node.querySelector('input'); if (input) input.parentNode.appendChild(resNode);
callGetDatasetInfo(path).then(function(res) {
if (res && res.exists) {
L.dom.content(resNode, [ _('(%d lines)').format(res.lines) ]);
} else {
L.dom.content(resNode, [ _('(file not found)') ]);
}
}).catch(function() {
L.dom.content(resNode, [ _('(n/a)') ]);
});
fs.exec('/usr/bin/wc', ['-l', path]).then(function(res) { L.dom.content(resNode, (res.code === 0) ? [ _('(%d lines)').format(res.stdout.trim().split(' ')[0]) ] : [ _('(n/a)') ]); });
return node;
});
};
@@ -590,21 +474,14 @@ return L.view.extend({
if (!path) path = '/usr/share/flowproxy/chnroute.txt';
if (!url) { ui.addNotification(null, E('p', _('Please set download_url first')), 'error'); return; }
ui.showModal(null, [ E('p', { 'class': 'spinning', 'id': 'download-msg' }, [ _('Downloading chnroute data...') ]) ]);
return callUpdateDataset(url, path).then(function(res) {
return fs.exec('/usr/bin/wget', ['-q', '-O', path, url, '--timeout=10', '--no-check-certificate']).then(function(res) {
var msgEl = document.getElementById('download-msg');
if (res && res.success) {
if (res.code === 0) {
if (msgEl) { msgEl.classList.remove('spinning'); L.dom.content(msgEl, [ _('Updated successfully.') ]); }
var cn = document.getElementById('line-count-status');
if (cn) L.dom.content(cn, [ _('(%d lines)').format(res.lines || 0) ]);
var cn = document.getElementById('line-count-status'); if (cn) fs.exec('/usr/bin/wc', ['-l', path]).then(function(r) { L.dom.content(cn, (r.code === 0) ? [ _('(%d lines)').format(r.stdout.trim().split(' ')[0]) ] : [ _('(n/a)') ]); });
setTimeout(ui.hideModal, 1500);
} else {
ui.hideModal();
ui.addNotification(null, E('p', _('Download failed: %s').format(res ? res.error : 'Unknown')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(e.message || String(e))), 'error');
});
} else { ui.hideModal(); ui.addNotification(null, E('p', _('Download failed')), 'error'); }
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', _('Error: %s').format(e.message)), 'error'); });
};
var sg = s.taboption('lists', form.SectionValue, '_list_custom', form.GridSection, 'nftset', _('Custom nftables sets'));
+1 -50
View File
@@ -118,7 +118,7 @@ msgstr ""
msgid "Match Value"
msgstr ""
msgid "Proxy server interface"
msgid "Network interface"
msgstr ""
msgid "Please set download_url first"
@@ -145,9 +145,6 @@ msgstr ""
msgid "Routing Table ID"
msgstr ""
msgid "Rule Priority"
msgstr ""
msgid "Rules"
msgstr ""
@@ -229,49 +226,3 @@ msgstr ""
msgid "this will delete ALL current %s rules and generate default templates. are you sure?"
msgstr ""
msgid "(file not found)"
msgstr ""
msgid "Download failed: %s"
msgstr ""
msgid "The proxy server IP address must be within the subnet of the selected interface (%s)."
msgstr ""
msgid "Invalid proxy server IP address."
msgstr ""
msgid "The proxy server IP address cannot be the local router IP (%s)."
msgstr ""
msgid "The proxy server IP address cannot be the network address or broadcast address."
msgstr ""
msgid "Traffic mark cannot be 0."
msgstr ""
msgid "Routing table ID %d is reserved by the system (0, 253, 254, 255). Please use another ID."
msgstr ""
msgid "Restart"
msgstr ""
msgid "Restarting FlowProxy service..."
msgstr ""
msgid "FlowProxy service restarted successfully."
msgstr ""
msgid "Failed to restart FlowProxy service."
msgstr ""
msgid "RPC error: %s"
msgstr ""
msgid "Proxy server IP address is required when FlowProxy is enabled."
msgstr ""
msgid "Invalid IPv4 address format"
msgstr ""
msgid "Auto (Detect automatically)"
msgstr ""
@@ -250,31 +250,3 @@ msgstr "RPC 错误: %s"
msgid "The proxy server IP address must be within the subnet of the selected interface (%s)."
msgstr "代理服务器 IP 地址必须在所选网络接口的子网网段之内 (%s)。"
msgid "(file not found)"
msgstr "(文件不存在)"
msgid "Download failed: %s"
msgstr "下载失败: %s"
msgid "Invalid proxy server IP address."
msgstr "无效的代理服务器 IP 地址。"
msgid "The proxy server IP address cannot be the local router IP (%s)."
msgstr "代理服务器 IP 地址不能为本路由器自身 IP (%s)。"
msgid "The proxy server IP address cannot be the network address or broadcast address."
msgstr "代理服务器 IP 地址不能为网络地址或广播地址。"
msgid "Traffic mark cannot be 0."
msgstr "流量标记不能为 0。"
msgid "Routing table ID %d is reserved by the system (0, 253, 254, 255). Please use another ID."
msgstr "路由表 ID %d 为系统保留路由表 (0, 253, 254, 255),请使用其他 ID。"
msgid "Proxy server IP address is required when FlowProxy is enabled."
msgstr "启用代理分流时,代理服务器 IP 地址为必填项。"
msgid "Invalid IPv4 address format"
msgstr "无效的 IPv4 地址格式"
msgid "Auto (Detect automatically)"
msgstr "自动探测"
+1 -1
View File
@@ -3,7 +3,7 @@ config flowproxy 'global'
option tcp_enabled '1'
option udp_enabled '1'
option proxy_server_ip_addr ''
option interface 'auto'
option interface 'br-lan'
option traffic_mark '0x666'
option routing_table '888'
option rule_priority '1000'
@@ -1,26 +0,0 @@
#!/bin/sh
# Hotplug trigger for flowproxy to restore policy routing on interface reload
[ "$ACTION" = "ifup" ] || exit 0
enabled=$(uci -q get flowproxy.global.enabled || echo 0)
[ "$enabled" = "1" ] || exit 0
proxy_ip=$(uci -q get flowproxy.global.proxy_server_ip_addr)
[ -n "$proxy_ip" ] || exit 0
target_iface=""
if [ -f "/var/run/flowproxy.mark" ]; then
target_iface=$(cut -d':' -f4 /var/run/flowproxy.mark 2>/dev/null)
fi
if [ -z "$target_iface" ]; then
target_iface=$(uci -q get flowproxy.global.interface)
if [ -z "$target_iface" ] || [ "$target_iface" = "auto" ]; then
target_iface=$(ip route get "$proxy_ip" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); exit}}')
fi
fi
target_iface="${target_iface:-br-lan}"
if [ "$DEVICE" = "$target_iface" ] || [ "$INTERFACE" = "$target_iface" ]; then
[ -x /etc/init.d/flowproxy ] && /etc/init.d/flowproxy add_route
fi
+77 -192
View File
@@ -4,8 +4,6 @@
START=99
STOP=10
USE_PROCD=1
EXTRA_COMMANDS="add_route"
EXTRA_HELP=" add_route Add routing rules and policy"
NAME="flowproxy"
UCI_CONFIG="flowproxy"
@@ -59,62 +57,9 @@ check_env() {
check_config() {
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
[ -z "$proxy_server_ip_addr" ] && { log_msg error "proxy_server_ip_addr not configured"; return 1; }
if [ "$proxy_server_ip_addr" = "127.0.0.1" ] || [ "$proxy_server_ip_addr" = "0.0.0.0" ]; then
log_msg error "Invalid proxy_server_ip_addr: $proxy_server_ip_addr"; return 1;
fi
# 纵深防御:严禁将代理服务器设为本路由器自身的任一 IP,防止策略路由自指死循环导致网络崩溃
if ip -4 -o addr show 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | grep -Fxq "$proxy_server_ip_addr"; then
log_msg error "proxy_server_ip_addr ($proxy_server_ip_addr) is a local IP of this router, aborting to prevent routing loop"
return 1
fi
# 检查路由表 ID 是否为系统保留表 (0, 253, 254, 255)
local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888")
if [ "$routing_table" = "0" ] || [ "$routing_table" = "253" ] || [ "$routing_table" = "254" ] || [ "$routing_table" = "255" ]; then
log_msg error "routing_table ($routing_table) is a reserved system table (0, 253, 254, 255), aborting"
return 1
fi
return 0
}
probe_proxy_server() {
local proxy_ip="$1"
local proxy_dns_port="$2"
local interface="$3"
log_msg info "Probing proxy server $proxy_ip on interface $interface..."
# 1. 检查 ARP 缓存或快速唤醒以获取 MAC 地址
local mac=""
if [ -f "/proc/net/arp" ]; then
mac=$(awk -v ip="$proxy_ip" '$1 == ip { print $4; exit }' /proc/net/arp)
fi
if [ -n "$mac" ] && [ "$mac" != "00:00:00:00:00:00" ]; then
log_msg info "Proxy server is REACHABLE: IP=$proxy_ip, MAC=$mac (found in ARP cache)"
else
# 快速单发 ping 1 秒唤醒 ARP 学习
ping -c 1 -W 1 "$proxy_ip" >/dev/null 2>&1
if [ -f "/proc/net/arp" ]; then
mac=$(awk -v ip="$proxy_ip" '$1 == ip { print $4; exit }' /proc/net/arp)
fi
if [ -n "$mac" ] && [ "$mac" != "00:00:00:00:00:00" ]; then
log_msg info "Proxy server is REACHABLE: IP=$proxy_ip, MAC=$mac"
else
log_msg warn "Proxy server $proxy_ip is NOT responding to ARP yet (it may be booting or offline, traffic will still be routed via policy table)"
fi
fi
# 2. 若启用了上游 DNS 代理,在日志中记录目标地址与端口
local dns_proxy=$(uci -q get "$UCI_CONFIG.global.dns_proxy_enabled" || echo 0)
if [ "$dns_proxy" = "1" ]; then
log_msg info "Upstream DNS redirection target configured: $proxy_ip#$proxy_dns_port"
fi
}
apply_nft_rules() {
log_msg info "Generating nftables configuration..."
local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info")
@@ -126,14 +71,10 @@ apply_nft_rules() {
FLOWPROXY_LOG_LEVEL="$conf_level" "$GENERATE_SCRIPT" >/dev/null 2> "$err_file"
local ret=$?
# 将脚本的输出记录到日志:成功时按 debug 输出,失败时按 error 输出
# 将脚本的输出记录到日志
if [ -s "$err_file" ]; then
while read -r line; do
if [ $ret -eq 0 ]; then
log_msg debug "Script: $line"
else
log_msg error "Generator: $line"
fi
log_msg debug "Script: $line"
done < "$err_file"
rm -f "$err_file"
fi
@@ -142,16 +83,14 @@ apply_nft_rules() {
log_msg info "Applying nftables rules from $NFT_CONF"
# 确保内核支持 inet 表
nft delete table inet flowproxy 2>/dev/null
local nft_err
nft_err=$(nft -f "$NFT_CONF" 2>&1)
if [ $? -eq 0 ]; then
if nft -f "$NFT_CONF"; then
return 0
else
log_msg error "nftables rules application failed: $nft_err"
log_msg error "nftables rules application failed (syntax error)"
return 1
fi
else
log_msg error "Failed to generate nftables config (exit code: $ret)"
log_msg error "Failed to generate nftables config"
return 1
fi
else
@@ -160,66 +99,33 @@ apply_nft_rules() {
fi
}
get_proxy_interface() {
local proxy_ip="$1"
local cfg_iface=$(uci -q get "$UCI_CONFIG.global.interface")
if [ -n "$cfg_iface" ] && [ "$cfg_iface" != "auto" ]; then
echo "$cfg_iface"
return
fi
local auto_dev=""
if [ -n "$proxy_ip" ]; then
auto_dev=$(ip route get "$proxy_ip" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); exit}}')
fi
echo "${auto_dev:-br-lan}"
}
add_route() {
check_config || return 1
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
local interface=$(get_proxy_interface "$proxy_server_ip_addr")
local traffic_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark")
traffic_mark="${traffic_mark:-0x666}"
local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table")
routing_table="${routing_table:-888}"
local rule_priority=$(uci -q get "$UCI_CONFIG.global.rule_priority")
rule_priority="${rule_priority:-1000}"
local interface=$(uci -q get "$UCI_CONFIG.global.interface" || echo "br-lan")
local traffic_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666")
local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888")
local rule_priority=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000")
# 1. 先尝试用旧记录清理一次,防止由于配置变更导致的残留
delete_route "quiet"
log_msg info "Setting up routing table $routing_table (dev: $interface) and rule for mark $traffic_mark with priority $rule_priority..."
log_msg info "Setting up routing table $routing_table and rule for mark $traffic_mark with priority $rule_priority..."
# 2. 记录当前使用的标记值、路由表 ID、优先级和实际出口网卡
echo "$traffic_mark:$routing_table:$rule_priority:$interface" > "/var/run/flowproxy.mark"
# 2. 记录当前使用的标记值、路由表 ID 和优先级
echo "$traffic_mark:$routing_table:$rule_priority" > "/var/run/flowproxy.mark"
# 3. 再次确保新标记和表的规则是干净的
while ip rule del fwmark "$traffic_mark" lookup "$routing_table" 2>/dev/null; do :; done
ip route flush table "$routing_table" 2>/dev/null
# 4. 添加路由和策略
local route_err rule_err
route_err=$(ip route add default via "$proxy_server_ip_addr" dev "$interface" table "$routing_table" 2>&1)
if [ $? -ne 0 ]; then
log_msg error "Failed to add default route to table $routing_table via $proxy_server_ip_addr (dev: $interface): $route_err"
log_msg error "Proxy server IP $proxy_server_ip_addr is unreachable or has no valid route on interface $interface"
return 1
fi
rule_err=$(ip rule add fwmark "$traffic_mark" lookup "$routing_table" priority "$rule_priority" 2>&1)
local err
ip route add default via "$proxy_server_ip_addr" dev "$interface" table "$routing_table" 2>/dev/null
err=$(ip rule add fwmark "$traffic_mark" lookup "$routing_table" priority "$rule_priority" 2>&1)
if [ $? -eq 0 ]; then
log_msg info "Route added: via $proxy_server_ip_addr dev $interface table $routing_table for mark $traffic_mark with priority $rule_priority"
else
log_msg error "Failed to add ip rule: $rule_err"
return 1
fi
# 5. 禁用目标网卡的 ICMP 重定向通告,防止客户端绕行或 TCP 连接挂起
if [ -d "/proc/sys/net/ipv4/conf/$interface" ]; then
sysctl -w "net.ipv4.conf.$interface.send_redirects=0" >/dev/null 2>&1
log_msg debug "Disabled send_redirects for $interface"
log_msg error "Failed to add ip rule: $err"
fi
}
@@ -228,23 +134,18 @@ delete_route() {
local quiet="$1"
# 1. 获取两组清理目标:旧值(记录文件) 和 新值(当前UCI)
local old_mark="" old_table="" old_prio="" old_iface=""
local old_mark="" old_table="" old_prio=""
if [ -f "/var/run/flowproxy.mark" ]; then
local saved_val=$(cat "/var/run/flowproxy.mark")
old_mark=$(echo "$saved_val" | cut -d':' -f1)
old_table=$(echo "$saved_val" | cut -d':' -f2)
old_prio=$(echo "$saved_val" | cut -d':' -f3)
old_iface=$(echo "$saved_val" | cut -d':' -f4)
[ "$quiet" != "quiet" ] && rm -f "/var/run/flowproxy.mark"
fi
local curr_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark")
curr_mark="${curr_mark:-0x666}"
local curr_table=$(uci -q get "$UCI_CONFIG.global.routing_table")
curr_table="${curr_table:-888}"
local curr_prio=$(uci -q get "$UCI_CONFIG.global.rule_priority")
curr_prio="${curr_prio:-1000}"
local curr_iface=$(get_proxy_interface "$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")")
local curr_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666")
local curr_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888")
local curr_prio=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000")
[ "$quiet" != "quiet" ] && log_msg info "Cleaning up FlowProxy routes (Atomic Mode)..."
@@ -252,10 +153,6 @@ delete_route() {
safe_del_rule() {
local m="$1" t="$2" p="$3"
[ -z "$m" ] || [ -z "$t" ] && return
if [ "$t" = "0" ] || [ "$t" = "253" ] || [ "$t" = "254" ] || [ "$t" = "255" ]; then
log_msg error "Refusing to flush reserved system routing table $t"
return
fi
log_msg debug "Attempting cleanup: mark=$m, table=$t, priority=${p:-any}"
# 精准清理 (带优先级)
[ -n "$p" ] && while ip rule del fwmark "$m" lookup "$t" priority "$p" 2>/dev/null; do
@@ -274,12 +171,6 @@ delete_route() {
safe_del_rule "$old_mark" "$old_table" "$old_prio"
[ "$old_mark" != "$curr_mark" ] || [ "$old_table" != "$curr_table" ] && \
safe_del_rule "$curr_mark" "$curr_table" "$curr_prio"
# 恢复 ICMP 重定向通告
[ -n "$old_iface" ] && [ -d "/proc/sys/net/ipv4/conf/$old_iface" ] && \
sysctl -w "net.ipv4.conf.$old_iface.send_redirects=1" >/dev/null 2>&1
[ -n "$curr_iface" ] && [ "$curr_iface" != "$old_iface" ] && [ -d "/proc/sys/net/ipv4/conf/$curr_iface" ] && \
sysctl -w "net.ipv4.conf.$curr_iface.send_redirects=1" >/dev/null 2>&1
}
delete_nft() {
@@ -301,46 +192,68 @@ setup_dns() {
[ -z "$proxy_server_ip_addr" ] && { log_msg error "proxy_server_ip_addr not configured, cannot setup DNS"; return 1; }
log_msg info "Setting up DNS redirection to $proxy_server_ip_addr#$proxy_server_dns_port via drop-in config..."
log_msg info "Setting up DNS redirection to $proxy_server_ip_addr#$proxy_server_dns_port..."
mkdir -p /tmp/dnsmasq.d
cat > /tmp/dnsmasq.d/flowproxy.conf << EOF
# Generated by FlowProxy - DO NOT EDIT MANUALLY
no-resolv
server=$proxy_server_ip_addr#$proxy_server_dns_port
EOF
# 同步分发至各个可能存在的 dnsmasq 实例目录
for dir in /tmp/dnsmasq*.d; do
if [ -d "$dir" ] && [ "$dir" != "/tmp/dnsmasq.d" ]; then
cp -f /tmp/dnsmasq.d/flowproxy.conf "$dir/flowproxy.conf"
fi
done
# 历史遗留自愈:若早期版本存在残留的 dns_backup 节,静默移除避免脏数据
if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
uci -q delete "$UCI_CONFIG.dns_backup"
uci -q commit "$UCI_CONFIG"
# Persistent backup in UCI
if ! uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
local old_noresolv=$(uci -q get dhcp.@dnsmasq[0].noresolv)
local old_dns_redirect=$(uci -q get dhcp.@dnsmasq[0].dns_redirect)
local old_servers=$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ',' | sed 's/,$//')
log_msg debug "Backing up DNS: noresolv=${old_noresolv:-empty}, redirect=${old_dns_redirect:-empty}, servers=${old_servers:-empty}"
uci set "$UCI_CONFIG.dns_backup=dns_backup"
[ -n "$old_noresolv" ] && uci set "$UCI_CONFIG.dns_backup.noresolv=$old_noresolv"
[ -n "$old_dns_redirect" ] && uci set "$UCI_CONFIG.dns_backup.dns_redirect=$old_dns_redirect"
[ -n "$old_servers" ] && uci set "$UCI_CONFIG.dns_backup.servers=$old_servers"
uci commit "$UCI_CONFIG"
fi
log_msg debug "Applying new DNS: noresolv=1, redirect=0, server=$proxy_server_ip_addr#$proxy_server_dns_port"
uci set dhcp.@dnsmasq[0].noresolv='1'
uci set dhcp.@dnsmasq[0].dns_redirect='0'
# Delete current servers and add our proxy
uci -q delete dhcp.@dnsmasq[0].server
uci add_list dhcp.@dnsmasq[0].server="$proxy_server_ip_addr#$proxy_server_dns_port"
uci commit dhcp
/etc/init.d/dnsmasq restart
log_msg info "DNS drop-in configuration applied successfully"
}
cleanup_dns() {
log_msg info "Cleaning up DNS redirection drop-in configuration..."
rm -f /tmp/dnsmasq.d/flowproxy.conf
for dir in /tmp/dnsmasq*.d; do
[ -d "$dir" ] && rm -f "$dir/flowproxy.conf"
done
# 历史遗留自愈
if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
uci -q delete "$UCI_CONFIG.dns_backup"
uci -q commit "$UCI_CONFIG"
fi
log_msg info "Restoring original DNS configuration..."
local noresolv=$(uci -q get "$UCI_CONFIG.dns_backup.noresolv")
local dns_redirect=$(uci -q get "$UCI_CONFIG.dns_backup.dns_redirect")
local servers=$(uci -q get "$UCI_CONFIG.dns_backup.servers")
log_msg debug "Restoring DNS values: noresolv=${noresolv:-0}, redirect=${dns_redirect:-1}, servers=${servers:-empty}"
/etc/init.d/dnsmasq restart
# Restore noresolv (default to 0 if not backed up)
[ -n "$noresolv" ] && uci set dhcp.@dnsmasq[0].noresolv="$noresolv" || uci set dhcp.@dnsmasq[0].noresolv="0"
# Restore dns_redirect (only restore if it originally existed, avoid polluting config)
if [ -n "$dns_redirect" ]; then
uci set dhcp.@dnsmasq[0].dns_redirect="$dns_redirect"
else
uci -q delete dhcp.@dnsmasq[0].dns_redirect
fi
# Restore servers correctly splitting comma-separated list
uci -q delete dhcp.@dnsmasq[0].server
if [ -n "$servers" ]; then
for s in $(echo "$servers" | tr ',' ' '); do
[ -n "$s" ] && uci add_list dhcp.@dnsmasq[0].server="$s"
done
fi
uci commit dhcp
uci delete "$UCI_CONFIG.dns_backup"
uci commit "$UCI_CONFIG"
/etc/init.d/dnsmasq restart
fi
}
start_service() {
@@ -348,42 +261,23 @@ start_service() {
if [ "$enabled" -eq 0 ]; then
log_msg info "FlowProxy is disabled, ensuring service is stopped"
[ -x /etc/init.d/flowproxy ] && /etc/init.d/flowproxy disable 2>/dev/null
stop_service
return 0
fi
# 状态自愈:当 enabled=1 时,确保服务已在 rc.d 中 enable,防止 luci-reload 因未启用而静默跳过执行
[ -x /etc/init.d/flowproxy ] && /etc/init.d/flowproxy enable 2>/dev/null
log_msg info "Starting FlowProxy..."
check_env || return 1
check_config || return 1
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
local proxy_server_dns_port=$(uci -q get "$UCI_CONFIG.global.proxy_server_dns_port" || echo 5353)
local interface=$(get_proxy_interface "$proxy_server_ip_addr")
probe_proxy_server "$proxy_server_ip_addr" "$proxy_server_dns_port" "$interface"
apply_nft_rules || return 1
if ! add_route; then
log_msg error "Routing setup failed, aborting and cleaning up service..."
stop_service
return 1
fi
add_route
setup_dns
touch "$STATE_FILE"
log_msg info "FlowProxy service started successfully"
}
stop_service() {
log_msg info "Stopping FlowProxy..."
local enabled=$(uci -q get "$UCI_CONFIG.global.enabled" || echo 0)
if [ "$enabled" -eq 0 ]; then
[ -x /etc/init.d/flowproxy ] && /etc/init.d/flowproxy disable 2>/dev/null
fi
# 强制先移除标记,防止状态误报
rm -f "$STATE_FILE"
delete_route
@@ -392,15 +286,13 @@ stop_service() {
log_msg info "FlowProxy service stopped"
}
reload_service() {
log_msg info "Reloading FlowProxy..."
restart_service() {
stop_service
start_service
}
restart_service() {
stop_service
start_service
reload_service() {
restart_service
}
service_triggers() {
@@ -408,11 +300,4 @@ service_triggers() {
procd_add_reload_trigger "$UCI_CONFIG"
# 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复
procd_add_reload_trigger "firewall"
# 监听网络接口事件(procd trigger 需接收 UCI 逻辑网络名,如 lan/wan)
local dev=$(get_proxy_interface "$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")")
local net_name=""
if command -v ubus >/dev/null 2>&1 && [ -n "$dev" ]; then
net_name=$(ubus call network.interface dump 2>/dev/null | jsonfilter -e "@.interface[@.device=\"$dev\"].interface" -e "@.interface[@.l3_device=\"$dev\"].interface" 2>/dev/null | head -n 1)
fi
procd_add_reload_interface_trigger "${net_name:-lan}"
}
@@ -1,23 +0,0 @@
#!/bin/sh
[ ! -f "/usr/share/ucitrack/luci-app-flowproxy.json" ] && {
cat > /usr/share/ucitrack/luci-app-flowproxy.json << EEOF
{
"config": "flowproxy",
"init": "flowproxy"
}
EEOF
}
uci -q batch <<-EOF >/dev/null
delete ucitrack.@flowproxy[-1]
add ucitrack flowproxy
set ucitrack.@flowproxy[-1].init=flowproxy
commit ucitrack
EOF
/etc/init.d/flowproxy enabled || /etc/init.d/flowproxy enable 2>/dev/null
rm -f /tmp/luci-indexcache* 2>/dev/null
rm -f /tmp/luci-modulecache/* 2>/dev/null
exit 0
@@ -44,15 +44,20 @@ gen_set_definition() {
[ -n "$uci_elems" ] && { [ -n "$elems" ] && elems="${elems}, "; elems="${elems}${uci_elems}"; }
file_path=$(uci -q get "$CONFIG.$section.file_path")
if [ -n "$file_path" ]; then
if [ -f "$file_path" ]; then
local file_elems=$(get_file_elements "$file_path")
if [ -n "$file_elems" ]; then
[ -n "$elems" ] && elems="${elems}, "
elems="${elems}${file_elems}"
fi
else
log_debug "Set file $file_path not found, continuing without file elements"
download_url=$(uci -q get "$CONFIG.$section.download_url")
if [ -n "$download_url" ] && [ -n "$file_path" ]; then
if [ ! -f "$file_path" ]; then
echo "Downloading $section data from $download_url to $file_path..." >&2
wget -q -O "$file_path" "$download_url" --timeout=10 --no-check-certificate
[ $? -ne 0 ] && { echo "Failed to download $section data" >&2; rm -f "$file_path"; }
fi
fi
if [ -n "$file_path" ] && [ -f "$file_path" ]; then
local file_elems=$(get_file_elements "$file_path")
if [ -n "$file_elems" ]; then
[ -n "$elems" ] && elems="${elems}, "
elems="${elems}${file_elems}"
fi
fi
@@ -178,68 +183,15 @@ TCP_ENABLED=$(uci -q get "$CONFIG.global.tcp_enabled" || echo "1")
UDP_ENABLED=$(uci -q get "$CONFIG.global.udp_enabled" || echo "1")
TRAFFIC_MARK=$(uci -q get "$CONFIG.global.traffic_mark" || echo "0x666")
PROXY_SERVER_IP_ADDR=$(uci -q get "$CONFIG.global.proxy_server_ip_addr")
get_lan_devices() {
local dev="" devs="" nets=""
local z_idx=0
while :; do
local z_name=$(uci -q get firewall.@zone[$z_idx].name)
[ -z "$z_name" ] && break
if [ "$z_name" = "lan" ]; then
nets=$(uci -q get firewall.@zone[$z_idx].network)
break
fi
z_idx=$((z_idx + 1))
done
[ -z "$nets" ] && nets="lan"
for net in $nets; do
dev=""
if command -v ubus >/dev/null 2>&1; then
dev=$(ubus call network.interface."$net" status 2>/dev/null | jsonfilter -e "@.l3_device" 2>/dev/null)
[ -z "$dev" ] && dev=$(ubus call network.interface."$net" status 2>/dev/null | jsonfilter -e "@.device" 2>/dev/null)
fi
[ -z "$dev" ] && dev=$(uci -q get network."$net".device || uci -q get network."$net".device)
if [ -n "$dev" ]; then
case " $devs " in
*" $dev "*) ;;
*) devs="${devs}${devs:+ }$dev" ;;
esac
fi
done
echo "${devs:-br-lan}"
}
render_nft_lan_filter() {
local devs="$1"
set -- $devs
local count=$#
if [ "$count" -le 1 ]; then
echo " iifname != \"$1\" return"
else
local set_str=""
for d in "$@"; do
[ -n "$set_str" ] && set_str="${set_str}, "
set_str="${set_str}\"${d}\""
done
echo " iifname != { $set_str } return"
fi
}
LAN_DEVICES=$(get_lan_devices)
LAN_NFT_FILTER=$(render_nft_lan_filter "$LAN_DEVICES")
if [ "$TCP_ENABLED" = "1" ]; then
cat >> "$OUTPUT_FILE" << EOF
chain LAN_MARKFLOW_TCP {
type filter hook prerouting priority mangle; policy accept;
$LAN_NFT_FILTER
fib daddr type { unspec, local, anycast, multicast } counter return
EOF
if [ -n "$PROXY_SERVER_IP_ADDR" ]; then
echo " ip saddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE"
echo " ip daddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE"
echo " ip saddr $PROXY_SERVER_IP_ADDR ip protocol tcp counter return" >> "$OUTPUT_FILE"
fi
for s in $SECTIONS_TCP; do process_rule "$s" "tcp" >> "$OUTPUT_FILE"; done
echo " ip protocol tcp counter meta mark set $TRAFFIC_MARK" >> "$OUTPUT_FILE"
@@ -250,12 +202,10 @@ if [ "$UDP_ENABLED" = "1" ]; then
cat >> "$OUTPUT_FILE" << EOF
chain LAN_MARKFLOW_UDP {
type filter hook prerouting priority mangle; policy accept;
$LAN_NFT_FILTER
fib daddr type { unspec, local, anycast, multicast } counter return
EOF
if [ -n "$PROXY_SERVER_IP_ADDR" ]; then
echo " ip saddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE"
echo " ip daddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE"
echo " ip saddr $PROXY_SERVER_IP_ADDR ip protocol udp counter return" >> "$OUTPUT_FILE"
fi
for s in $SECTIONS_UDP; do process_rule "$s" "udp" >> "$OUTPUT_FILE"; done
echo " ip protocol udp counter meta mark set $TRAFFIC_MARK" >> "$OUTPUT_FILE"
@@ -13,6 +13,11 @@
},
"write": {
"uci": [ "flowproxy" ],
"file": {
"/usr/bin/wget": [ "exec" ],
"/usr/bin/wc": [ "exec" ],
"/usr/share/flowproxy/generate_nft.sh": [ "exec" ]
},
"ubus": {
"luci.flowproxy": [ "*" ]
}
@@ -9,7 +9,6 @@ const STATE_FILE = '/var/run/flowproxy.running';
const IP_CACHE = '/tmp/flowproxy/lan_ip.cache';
const LOG_FILE = '/var/log/flowproxy.log';
const NFT_TABLE = 'flowproxy';
const DNS_DROPIN_FILE = '/tmp/dnsmasq.d/flowproxy.conf';
// 辅助函数:安全 trim
function safe_trim(str) {
@@ -19,10 +18,10 @@ function safe_trim(str) {
const methods = {
get_status: {
call: function() {
// 1. 快速探测标记文件
// 1. 快速探测
let is_running = access(STATE_FILE, 'f');
// 2. 自愈:检查内核状态 (nft 链与策略路由)
// 2. 自愈:检查内核状态并同步标记文件
let tcp_active = (system('nft list chain inet ' + NFT_TABLE + ' LAN_MARKFLOW_TCP >/dev/null 2>&1') === 0);
let udp_active = (system('nft list chain inet ' + NFT_TABLE + ' LAN_MARKFLOW_UDP >/dev/null 2>&1') === 0);
let real_running = (tcp_active || udp_active);
@@ -41,6 +40,7 @@ const methods = {
if (ubus_conn) {
let ifaces = ubus_conn.call('network.interface', 'dump');
if (type(ifaces?.interface) === 'array') {
// 优先寻找名为 'lan' 的接口,或者寻找拥有 ipv4 地址的第一个非回环接口
let fallback_ip = '';
for (let iface in ifaces.interface) {
if (iface.interface === 'lan' && iface['ipv4-address']?.[0]?.address) {
@@ -64,8 +64,9 @@ const methods = {
}
let uci_cursor = cursor();
let proxy_server_ip_addr = uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_ip_addr') || '';
let dns_running = access(DNS_DROPIN_FILE, 'f') ? 1 : 0;
let dns_running = (uci_cursor.get(UCI_CONFIG, 'dns_backup') ? 1 : 0);
return {
running: real_running ? 1 : 0,
@@ -113,67 +114,6 @@ const methods = {
}
},
get_dataset_info: {
args: { file_path: 'string' },
call: function(request) {
let file_path = request.args?.file_path || '/usr/share/flowproxy/chnroute.txt';
if (!match(file_path, /^\/(usr\/share\/flowproxy|tmp\/flowproxy)\/[a-zA-Z0-9_.-]+$/)) {
return { error: 'Invalid path' };
}
if (!access(file_path, 'r')) {
return { exists: false, lines: 0, size: 0 };
}
let s = stat(file_path);
let lines = 0;
let fd = popen(sprintf("wc -l '%s' 2>/dev/null", file_path));
if (fd) {
let out = fd.read('all');
fd.close();
if (out) {
let parts = split(trim(out), /\s+/);
lines = int(parts[0]) || 0;
}
}
return { exists: true, lines: lines, size: s ? s.size : 0 };
}
},
update_dataset: {
args: { url: 'string', file_path: 'string' },
call: function(request) {
let url = request.args?.url;
let file_path = request.args?.file_path || '/usr/share/flowproxy/chnroute.txt';
if (!url || !match(url, /^https?:\/\/[A-Za-z0-9_.\/:~%#?&=+-]+$/)) {
return { success: false, error: 'Invalid URL' };
}
if (!match(file_path, /^\/(usr\/share\/flowproxy|tmp\/flowproxy)\/[a-zA-Z0-9_.-]+$/)) {
return { success: false, error: 'Invalid target file path' };
}
let tmp_file = file_path + '.tmp';
let cmd = sprintf("wget -q -O '%s' '%s' --timeout=15 --no-check-certificate 2>&1", tmp_file, url);
let rc = system(cmd);
if (rc === 0 && access(tmp_file, 'f')) {
let s = stat(tmp_file);
if (s && s.size > 0) {
system(sprintf("mv -f '%s' '%s'", tmp_file, file_path));
let lines = 0;
let fd = popen(sprintf("wc -l '%s' 2>/dev/null", file_path));
if (fd) {
let out = fd.read('all');
fd.close();
if (out) {
let parts = split(trim(out), /\s+/);
lines = int(parts[0]) || 0;
}
}
return { success: true, lines: lines };
}
}
system(sprintf("rm -f '%s'", tmp_file));
return { success: false, error: 'Download failed or file empty' };
}
},
generate_nft_config: {
call: function() {
system('mkdir -p /tmp/flowproxy');
@@ -185,18 +125,19 @@ const methods = {
get_runtime_config: {
call: function() {
let res_file = '/tmp/flowproxy/runtime.tmp';
let dns_file = '/tmp/flowproxy/dns_runtime.tmp';
system('mkdir -p /tmp/flowproxy');
system('/usr/share/flowproxy/generate_nft.sh runtime > ' + res_file + ' 2>&1');
let nft_runtime = readfile(res_file) || "(failed)";
system('rm -f ' + res_file);
let dns_runtime = "\n--- [ DNSmasq Drop-in Runtime Configuration ] ---\n";
let dns_content = readfile(DNS_DROPIN_FILE);
if (dns_content) {
dns_runtime += dns_content;
} else {
dns_runtime += "(no active dnsmasq drop-in overrides)";
}
system('uci show dhcp.@dnsmasq[0] | grep -E "noresolv|dns_redirect|server" | sed -E "s/^dhcp\\.[^.]*\\.([^=]*)=(.*)/\\1: \\2/; s/\\x27//g; s/#/:/g" > ' + dns_file + ' 2>&1');
let dns_out = readfile(dns_file);
system('rm -f ' + dns_file);
let dns_runtime = "\n--- [ DNSmasq Runtime Configuration ] ---\n";
dns_runtime += (dns_out || "(no active dnsmasq overrides)");
return { runtime: safe_trim(nft_runtime + dns_runtime) };
}
@@ -208,6 +149,7 @@ const methods = {
try {
let ubus_conn = connect();
if (ubus_conn) {
// 使用 network.device status 以获取所有物理设备 (包括 eth0 等 bridge 成员)
let dev_status = ubus_conn.call('network.device', 'status');
if (dev_status) {
for (let name in dev_status) {