🐤 Sync 2026-10-03 00:12:36

This commit is contained in:
github-actions[bot] committed 2026-10-03 00:12:36 +08:00
1 parent ce3df55b24
commit cb1d978497
58 files changed
+1627 -1018

No files matched your search

+11 -3
View File
@@ -1,13 +1,21 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-flowproxy
PKG_VERSION:=1.0.3
PKG_RELEASE:=1
PKG_VERSION:=1.0.4
PKG_RELEASE:=2
LUCI_TITLE:=LuCI Support for FlowProxy Traffic Diversion
LUCI_DEPENDS:=+luci-base
LUCI_DEPENDS:=+luci-base +nftables +kmod-nft-core +ip-full
LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/postinst
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
/etc/init.d/rpcd restart
fi
exit 0
endef
define Package/$(PKG_NAME)/prerm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
@@ -40,6 +40,18 @@ 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([
@@ -189,11 +201,54 @@ 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 false;
if (!cidrStr || cidrStr.indexOf(':') !== -1 || !ipStr || ipStr.indexOf(':') !== -1) return null;
var parts = cidrStr.split('/');
var cidrIp = parts[0];
var maskBits = parseInt(parts[1] || '32', 10);
@@ -209,15 +264,19 @@ return L.view.extend({
var ipNum = ipToNum(ipStr);
var cidrNum = ipToNum(cidrIp);
if (maskBits === 0) return true;
if (maskBits === 0) return { inSubnet: true, isBcastOrNet: false };
var mask = (0xffffffff << (32 - maskBits)) >>> 0;
return (ipNum & mask) === (cidrNum & mask);
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 };
};
var selected_interface = s.formvalue(section_id, 'interface');
if (!value || !selected_interface) return true;
if (!selected_interface) return true;
var target_iface = null;
for (var i = 0; i < ifaces.length; i++) {
@@ -234,9 +293,12 @@ 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++) {
if (ipInCidr(value, ips[j])) {
var res = ipInCidr(value, ips[j]);
if (res && res.inSubnet) {
inSubnet = true;
if (res.isBcastOrNet) isBcastOrNet = true;
break;
}
}
@@ -245,19 +307,60 @@ 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;
o = s.taboption('settings', form.ListValue, 'interface', _('Proxy server interface'));
devices.forEach(function(d) { o.value(d.getName(), d.getName()); });
o.default = 'br-lan';
var iface_opt = s.taboption('settings', form.ListValue, 'interface', _('Proxy server interface'));
devices.forEach(function(d) { iface_opt.value(d.getName(), d.getName()); });
iface_opt.default = 'br-lan';
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 }));
}
}
}
}
};
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))';
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';
// --- Rules ---
var nftsets = uci.sections('flowproxy', 'nftset').map(function(ss) { return '@' + ss['.name']; });
@@ -419,16 +522,14 @@ 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.dispatchEvent(new CustomEvent('change', { bubbles: true }));
inputEl.dispatchEvent(new CustomEvent('blur', { bubbles: true }));
if (inputEl && inputEl.value && inputEl.value.trim() !== '') {
inputEl.dispatchEvent(new CustomEvent('input', { bubbles: true }));
}
}
@@ -460,7 +561,15 @@ 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);
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)') ]); });
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)') ]);
});
return node;
});
};
@@ -474,14 +583,21 @@ 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 fs.exec('/usr/bin/wget', ['-q', '-O', path, url, '--timeout=10', '--no-check-certificate']).then(function(res) {
return callUpdateDataset(url, path).then(function(res) {
var msgEl = document.getElementById('download-msg');
if (res.code === 0) {
if (res && res.success) {
if (msgEl) { msgEl.classList.remove('spinning'); L.dom.content(msgEl, [ _('Updated successfully.') ]); }
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)') ]); });
var cn = document.getElementById('line-count-status');
if (cn) L.dom.content(cn, [ _('(%d lines)').format(res.lines || 0) ]);
setTimeout(ui.hideModal, 1500);
} 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'); });
} 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');
});
};
var sg = s.taboption('lists', form.SectionValue, '_list_custom', form.GridSection, 'nftset', _('Custom nftables sets'));
+52 -1
View File
@@ -118,7 +118,7 @@ msgstr ""
msgid "Match Value"
msgstr ""
msgid "Network interface"
msgid "Proxy server interface"
msgstr ""
msgid "Please set download_url first"
@@ -145,6 +145,9 @@ msgstr ""
msgid "Routing Table ID"
msgstr ""
msgid "Rule Priority"
msgstr ""
msgid "Rules"
msgstr ""
@@ -226,3 +229,51 @@ 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 ""
@@ -250,3 +250,32 @@ 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 地址格式"
@@ -102,3 +102,4 @@ config nftset 'no_proxy_dst_tcp_ports'
config nftset 'no_proxy_dst_udp_ports'
option enabled '1'
option type 'inet_service'
list elements '123'
+13
View File
@@ -0,0 +1,13 @@
#!/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
target_iface=$(uci -q get flowproxy.global.interface || echo "br-lan")
if [ "$DEVICE" = "$target_iface" ] || [ "$INTERFACE" = "$target_iface" ]; then
[ -x /etc/init.d/flowproxy ] && /etc/init.d/flowproxy add_route
fi
+135 -60
View File
@@ -4,6 +4,8 @@
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"
@@ -57,9 +59,62 @@ 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")
@@ -100,11 +155,17 @@ apply_nft_rules() {
}
add_route() {
check_config || return 1
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
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")
local interface=$(uci -q get "$UCI_CONFIG.global.interface")
interface="${interface:-br-lan}"
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}"
# 1. 先尝试用旧记录清理一次,防止由于配置变更导致的残留
delete_route "quiet"
@@ -127,6 +188,12 @@ add_route() {
else
log_msg error "Failed to add ip rule: $err"
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"
fi
}
delete_route() {
@@ -143,9 +210,14 @@ delete_route() {
[ "$quiet" != "quiet" ] && rm -f "/var/run/flowproxy.mark"
fi
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")
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=$(uci -q get "$UCI_CONFIG.global.interface")
curr_iface="${curr_iface:-br-lan}"
[ "$quiet" != "quiet" ] && log_msg info "Cleaning up FlowProxy routes (Atomic Mode)..."
@@ -153,6 +225,10 @@ 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
@@ -171,6 +247,11 @@ 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 重定向通告
if [ -d "/proc/sys/net/ipv4/conf/$curr_iface" ]; then
sysctl -w "net.ipv4.conf.$curr_iface.send_redirects=1" >/dev/null 2>&1
fi
}
delete_nft() {
@@ -192,64 +273,46 @@ 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..."
log_msg info "Setting up DNS redirection to $proxy_server_ip_addr#$proxy_server_dns_port via drop-in 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"
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"
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
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}"
# 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 (default to 1 as requested for OpenWrt typical modded builds)
[ -n "$dns_redirect" ] && uci set dhcp.@dnsmasq[0].dns_redirect="$dns_redirect" || uci set dhcp.@dnsmasq[0].dns_redirect="1"
# Restore servers
uci -q delete dhcp.@dnsmasq[0].server
if [ -n "$servers" ]; then
for s in $servers; 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
uci -q delete "$UCI_CONFIG.dns_backup"
uci -q commit "$UCI_CONFIG"
fi
/etc/init.d/dnsmasq restart
}
start_service() {
@@ -265,6 +328,12 @@ start_service() {
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=$(uci -q get "$UCI_CONFIG.global.interface" || echo "br-lan")
probe_proxy_server "$proxy_server_ip_addr" "$proxy_server_dns_port" "$interface"
apply_nft_rules || return 1
add_route
setup_dns
@@ -272,6 +341,7 @@ start_service() {
touch "$STATE_FILE"
log_msg info "FlowProxy service started successfully"
}
stop_service() {
log_msg info "Stopping FlowProxy..."
# 强制先移除标记,防止状态误报
@@ -282,6 +352,12 @@ stop_service() {
log_msg info "FlowProxy service stopped"
}
reload_service() {
log_msg info "Reloading FlowProxy..."
stop_service
start_service
}
restart_service() {
stop_service
start_service
@@ -292,8 +368,7 @@ service_triggers() {
procd_add_reload_trigger "$UCI_CONFIG"
# 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复
procd_add_reload_trigger "firewall"
}
reload_service() {
restart
# 监听网络接口事件
local iface=$(uci -q get "$UCI_CONFIG.global.interface" || echo "br-lan")
procd_add_reload_interface_trigger "$iface"
}
@@ -44,20 +44,15 @@ gen_set_definition() {
[ -n "$uci_elems" ] && { [ -n "$elems" ] && elems="${elems}, "; elems="${elems}${uci_elems}"; }
file_path=$(uci -q get "$CONFIG.$section.file_path")
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}"
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"
fi
fi
@@ -183,15 +178,19 @@ 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")
INTERFACE=$(uci -q get "$CONFIG.global.interface")
[ -z "$INTERFACE" ] && INTERFACE="br-lan"
if [ "$TCP_ENABLED" = "1" ]; then
cat >> "$OUTPUT_FILE" << EOF
chain LAN_MARKFLOW_TCP {
type filter hook prerouting priority mangle; policy accept;
iifname != "$INTERFACE" return
fib daddr type { unspec, local, anycast, multicast } counter return
EOF
if [ -n "$PROXY_SERVER_IP_ADDR" ]; then
echo " ip saddr $PROXY_SERVER_IP_ADDR ip protocol tcp counter return" >> "$OUTPUT_FILE"
echo " ip saddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE"
echo " ip daddr $PROXY_SERVER_IP_ADDR 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"
@@ -202,10 +201,12 @@ if [ "$UDP_ENABLED" = "1" ]; then
cat >> "$OUTPUT_FILE" << EOF
chain LAN_MARKFLOW_UDP {
type filter hook prerouting priority mangle; policy accept;
iifname != "$INTERFACE" return
fib daddr type { unspec, local, anycast, multicast } counter return
EOF
if [ -n "$PROXY_SERVER_IP_ADDR" ]; then
echo " ip saddr $PROXY_SERVER_IP_ADDR ip protocol udp counter return" >> "$OUTPUT_FILE"
echo " ip saddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE"
echo " ip daddr $PROXY_SERVER_IP_ADDR 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,11 +13,6 @@
},
"write": {
"uci": [ "flowproxy" ],
"file": {
"/usr/bin/wget": [ "exec" ],
"/usr/bin/wc": [ "exec" ],
"/usr/share/flowproxy/generate_nft.sh": [ "exec" ]
},
"ubus": {
"luci.flowproxy": [ "*" ]
}
@@ -1,6 +1,6 @@
'use strict';
import { access, readfile, writefile, stat } from 'fs';
import { access, readfile, writefile, stat, popen } from 'fs';
import { cursor } from 'uci';
import { connect } from 'ubus';
@@ -9,6 +9,7 @@ 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) {
@@ -18,10 +19,10 @@ function safe_trim(str) {
const methods = {
get_status: {
call: function() {
// 1. 快速探测
// 1. 快速探测标记文件
let is_running = access(STATE_FILE, 'f');
// 2. 自愈:检查内核状态并同步标记文件
// 2. 自愈:检查内核状态 (nft 链与策略路由)
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);
@@ -40,7 +41,6 @@ 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,9 +64,8 @@ const methods = {
}
let uci_cursor = cursor();
let proxy_server_ip_addr = uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_ip_addr') || '';
let dns_running = (uci_cursor.get(UCI_CONFIG, 'dns_backup') ? 1 : 0);
let dns_running = access(DNS_DROPIN_FILE, 'f') ? 1 : 0;
return {
running: real_running ? 1 : 0,
@@ -114,6 +113,67 @@ 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');
@@ -125,19 +185,18 @@ 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);
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)");
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)";
}
return { runtime: safe_trim(nft_runtime + dns_runtime) };
}
@@ -149,7 +208,6 @@ 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) {