mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 00:35:13 +08:00
🤞 Sync 2026-10-06 13:52:46
This commit is contained in:
1 parent
4d2d69bed9
commit
947eaea2ee
42 files changed
+2681
-1582
No files matched your search
@@ -1,12 +1,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=lua-periphery
|
||||
PKG_VERSION:=2.5.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=2.5.1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/vsergeev/lua-periphery.git
|
||||
PKG_SOURCE_VERSION:=e91d515ebbf04bd4cfc10a3f4c7e1f12736350af
|
||||
PKG_SOURCE_VERSION:=6c966f76a1ba280d71871d737f4802c34d72ff55
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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 "自动探测"
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-ipsec-server
|
||||
PKG_VERSION:=20211223
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=20260815
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_MAINTAINER:=Lienol <lawlienol@gmail.com>
|
||||
|
||||
LUCI_TITLE:=LuCI support for IPSec VPN Server
|
||||
LUCI_DEPENDS:=+kmod-tun +luci-lib-jsonc +strongswan +strongswan-minimal +strongswan-mod-kernel-libipsec +strongswan-mod-openssl +strongswan-mod-xauth-generic +xl2tpd
|
||||
LUCI_DEPENDS:=+kmod-tun +luci-lib-jsonc +strongswan +strongswan-minimal +strongswan-mod-kernel-libipsec +strongswan-mod-openssl +strongswan-mod-xauth-generic +strongswan-mod-eap-mschapv2 +strongswan-mod-eap-identity +strongswan-mod-pem +strongswan-mod-pkcs1 +strongswan-mod-pkcs8 +strongswan-mod-dhcp +strongswan-mod-farp
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
conn ikev2-EAP
|
||||
keyexchange=ikev2
|
||||
auto=add
|
||||
fragmentation=yes
|
||||
forceencaps=yes
|
||||
|
||||
left=%defaultroute
|
||||
leftid=@ns.miaogongzi.cc
|
||||
leftauth=pubkey
|
||||
leftcert=full.pem
|
||||
leftsendcert=always
|
||||
leftsubnet=0.0.0.0/0
|
||||
|
||||
right=%any
|
||||
rightid=%any
|
||||
rightauth=eap-mschapv2
|
||||
eap_identity=%identity
|
||||
rightsourceip=10.0.20.0/24
|
||||
rightdns=10.0.0.1
|
||||
|
||||
ike=aes256-sha256-modp1024,aes256-sha1-modp1024,aes128-sha256-modp1024,aes128-sha1-modp1024,aes256-sha256-modp2048,aes256-sha1-modp2048
|
||||
esp=aes256-sha256,aes128-sha256,aes256-sha1,aes128-sha1
|
||||
dpdaction=clear
|
||||
dpddelay=30s
|
||||
rekey=no
|
||||
compress=no
|
||||
@@ -11,14 +11,12 @@ function index()
|
||||
entry({"admin", "vpn", "ipsec-server", "settings"}, cbi("ipsec-server/settings"), _("General Settings"), 10).leaf = true
|
||||
entry({"admin", "vpn", "ipsec-server", "users"}, cbi("ipsec-server/users"), _("Users Manager"), 20).leaf = true
|
||||
entry({"admin", "vpn", "ipsec-server", "l2tp_user"}, cbi("ipsec-server/l2tp_user")).leaf = true
|
||||
entry({"admin", "vpn", "ipsec-server", "online"}, cbi("ipsec-server/online"), _("L2TP Online Users"), 30).leaf = true
|
||||
entry({"admin", "vpn", "ipsec-server", "status"}, call("act_status")).leaf = true
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local e = {}
|
||||
e["ipsec_status"] = luci.sys.call("/usr/bin/pgrep ipsec >/dev/null") == 0
|
||||
e["l2tp_status"] = luci.sys.call("top -bn1 | grep -v grep | grep '/var/etc/xl2tpd' >/dev/null") == 0
|
||||
e["ipsec_status"] = luci.sys.call("pidof charon >/dev/null 2>&1") == 0
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
@@ -1,10 +1,10 @@
|
||||
local d = require "luci.dispatcher"
|
||||
local sys = require "luci.sys"
|
||||
local fs = require "nixio.fs"
|
||||
|
||||
m = Map("luci-app-ipsec-server", "L2TP/IPSec PSK " .. translate("Users Manager"))
|
||||
m.redirect = d.build_url("admin", "vpn", "ipsec-server", "users")
|
||||
|
||||
if sys.call("command -v xl2tpd > /dev/null") == 0 then
|
||||
if fs.access("/usr/sbin/xl2tpd") then
|
||||
s = m:section(NamedSection, arg[1], "l2tp_users", "")
|
||||
s.addremove = false
|
||||
s.anonymous = true
|
||||
@@ -19,6 +19,7 @@ if sys.call("command -v xl2tpd > /dev/null") == 0 then
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.placeholder = translate("Password")
|
||||
o.password = true
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "ipaddress", translate("IP address"))
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
local o = require "luci.dispatcher"
|
||||
local fs = require "nixio.fs"
|
||||
local jsonc = require "luci.jsonc"
|
||||
|
||||
local sessions = {}
|
||||
local session_path = "/var/etc/xl2tpd/session"
|
||||
if fs.access(session_path) then
|
||||
for filename in fs.dir(session_path) do
|
||||
local session_file = session_path .. "/" .. filename
|
||||
local file = io.open(session_file, "r")
|
||||
local t = jsonc.parse(file:read("*a"))
|
||||
if t then
|
||||
t.session_file = session_file
|
||||
sessions[#sessions + 1] = t
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
|
||||
local blacklist = {}
|
||||
local firewall_user_path = "/etc/firewall.user"
|
||||
if fs.access(firewall_user_path) then
|
||||
for line in io.lines(firewall_user_path) do
|
||||
local m = line:match('xl2tpd%-blacklist%-([^\n]+)')
|
||||
if m then
|
||||
local t = {}
|
||||
t.ip = m
|
||||
blacklist[#blacklist + 1] = t
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
f = SimpleForm("processes")
|
||||
f.reset = false
|
||||
f.submit = false
|
||||
|
||||
t = f:section(Table, sessions, translate("L2TP Online Users"))
|
||||
t:option(DummyValue, "username", translate("Username"))
|
||||
t:option(DummyValue, "interface", translate("Interface"))
|
||||
t:option(DummyValue, "ip", translate("Client IP"))
|
||||
t:option(DummyValue, "remote_ip", translate("IP address"))
|
||||
t:option(DummyValue, "login_time", translate("Login Time"))
|
||||
|
||||
_blacklist = t:option(Button, "_blacklist", translate("Blacklist"))
|
||||
function _blacklist.render(e, t, a)
|
||||
e.title = translate("Add to Blacklist")
|
||||
e.inputstyle = "remove"
|
||||
Button.render(e, t, a)
|
||||
end
|
||||
function _blacklist.write(t, s)
|
||||
local e = t.map:get(s, "remote_ip")
|
||||
luci.util.execi("echo 'iptables -I INPUT -s %s -p udp -m multiport --dports 500,4500,1701 -j DROP ## xl2tpd-blacklist-%s' >> /etc/firewall.user" % {e, e})
|
||||
luci.util.execi("iptables -I INPUT -s %s -p udp -m multiport --dports 500,4500,1701 -j DROP" % {e})
|
||||
luci.util.execi("rm -f " .. t.map:get(s, "session_file"))
|
||||
null, t.tag_error[s] = luci.sys.process.signal(t.map:get(s, "pid"), 9)
|
||||
luci.http.redirect(o.build_url("admin/vpn/ipsec-server/online"))
|
||||
end
|
||||
|
||||
_kill = t:option(Button, "_kill", translate("Forced offline"))
|
||||
_kill.inputstyle = "remove"
|
||||
function _kill.write(t, s)
|
||||
luci.util.execi("rm -f " .. t.map:get(s, "session_file"))
|
||||
null, t.tag_error[t] = luci.sys.process.signal(t.map:get(s, "pid"), 9)
|
||||
luci.http.redirect(o.build_url("admin/vpn/ipsec-server/online"))
|
||||
end
|
||||
|
||||
t = f:section(Table, blacklist, translate("Blacklist"))
|
||||
t:option(DummyValue, "ip", translate("IP address"))
|
||||
|
||||
_blacklist2 = t:option(Button, "_blacklist2", translate("Blacklist"))
|
||||
function _blacklist2.render(e, t, a)
|
||||
e.title = translate("Remove from Blacklist")
|
||||
e.inputstyle = "apply"
|
||||
Button.render(e, t, a)
|
||||
end
|
||||
function _blacklist2.write(t, s)
|
||||
local e = t.map:get(s, "ip")
|
||||
luci.util.execi("sed -i -e '/## xl2tpd-blacklist-%s/d' /etc/firewall.user" % {e})
|
||||
luci.util.execi("iptables -D INPUT -s %s -p udp -m multiport --dports 500,4500,1701 -j DROP" % {e})
|
||||
luci.http.redirect(o.build_url("admin/vpn/ipsec-server/online"))
|
||||
end
|
||||
|
||||
return f
|
||||
@@ -1,5 +1,3 @@
|
||||
local sys = require "luci.sys"
|
||||
|
||||
m = Map("luci-app-ipsec-server", translate("IPSec VPN Server"))
|
||||
m.template = "ipsec-server/ipsec-server_status"
|
||||
|
||||
@@ -9,56 +7,53 @@ s.anonymous = true
|
||||
o = s:option(DummyValue, "ipsec-server_status", translate("Current Condition"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
return '<font class="ipsec-server_status"></font>'
|
||||
return '<span class="ipsec-server-status is-loading" role="status" aria-live="polite">' .. translate("Checking...") .. '</span>'
|
||||
end
|
||||
|
||||
enabled = s:option(Flag, "enabled", translate("Enable"))
|
||||
enabled.description = translate("Use a client that supports IPSec Xauth PSK (iOS or Android) to connect to this server.")
|
||||
enabled.description = translate("Enable IPsec access for IKEv2 PSK, IKEv1 XAuth PSK, and IKEv2 EAP clients.")
|
||||
enabled.default = 0
|
||||
enabled.rmempty = false
|
||||
|
||||
clientip = s:option(Value, "clientip", translate("VPN Client IP"))
|
||||
clientip.description = translate("VPN Client reserved started IP addresses with the same subnet mask, such as: 192.168.100.10/24")
|
||||
clientip.description = translate("VPN client address pool in CIDR notation, such as: 10.0.10.0/24")
|
||||
clientip.datatype = "ip4addr"
|
||||
clientip.optional = false
|
||||
clientip.rmempty = false
|
||||
clientip.placeholder = "10.0.10.0/24"
|
||||
|
||||
serverip = s:option(Value, "serverip", translate("VPN Server IP"))
|
||||
serverip.description = translate("VPN server address and subnet in CIDR notation, such as: 10.0.0.1/24")
|
||||
serverip.datatype = "ip4addr"
|
||||
serverip.optional = false
|
||||
serverip.rmempty = false
|
||||
serverip.placeholder = "10.0.0.1/24"
|
||||
|
||||
domain = s:option(Value, "domain", translate("VPN Domain"))
|
||||
domain.description = translate("Domain name used for IKEv2 certificate and Remote ID, such as: ns.miaogongzi.cc")
|
||||
domain.datatype = "host"
|
||||
domain.optional = false
|
||||
domain.rmempty = false
|
||||
domain.placeholder = "ns.miaogongzi.cc"
|
||||
domain.default = "ns.miaogongzi.cc"
|
||||
|
||||
certs_name = s:option(Value, "certs_name", translate("Certificate File Name"))
|
||||
certs_name.description = translate("Server certificate file name under /etc/ipsec.d/certs/, such as: server.pem")
|
||||
certs_name.optional = false
|
||||
certs_name.rmempty = false
|
||||
certs_name.placeholder = "server.pem"
|
||||
certs_name.default = "server.pem"
|
||||
|
||||
private_key = s:option(Value, "private_key", translate("Private Key File Name"))
|
||||
private_key.description = translate("Server private key file name under /etc/ipsec.d/private/, such as: server_key.pem")
|
||||
private_key.optional = false
|
||||
private_key.rmempty = false
|
||||
private_key.placeholder = "server_key.pem"
|
||||
private_key.default = "server_key.pem"
|
||||
|
||||
secret = s:option(Value, "secret", translate("Secret Pre-Shared Key"))
|
||||
secret.description = translate("Used by IKEv2 PSK and IKEv1 XAuth PSK clients.")
|
||||
secret.password = true
|
||||
|
||||
if sys.call("command -v xl2tpd > /dev/null") == 0 then
|
||||
o = s:option(DummyValue, "l2tp_status", "L2TP " .. translate("Current Condition"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
return '<font class="l2tp_status"></font>'
|
||||
end
|
||||
|
||||
o = s:option(Flag, "l2tp_enable", "L2TP " .. translate("Enable"))
|
||||
o.description = translate("Use a client that supports L2TP over IPSec PSK to connect to this server.")
|
||||
o.default = 0
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "l2tp_localip", "L2TP " .. translate("Server IP"))
|
||||
o.description = translate("VPN Server IP address, such as: 192.168.101.1")
|
||||
o.datatype = "ip4addr"
|
||||
o.rmempty = true
|
||||
o.default = "192.168.101.1"
|
||||
o.placeholder = o.default
|
||||
|
||||
o = s:option(Value, "l2tp_remoteip", "L2TP " .. translate("Client IP"))
|
||||
o.description = translate("VPN Client IP address range, such as: 192.168.101.10-20")
|
||||
o.rmempty = true
|
||||
o.default = "192.168.101.10-20"
|
||||
o.placeholder = o.default
|
||||
|
||||
if sys.call("ls -L /usr/lib/ipsec/libipsec* 2>/dev/null >/dev/null") == 0 then
|
||||
o = s:option(DummyValue, "_o", " ")
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
return string.format('<a style="color: red">%s</a>', translate("L2TP/IPSec is not compatible with kernel-libipsec, which will disable this module."))
|
||||
end
|
||||
o:depends("l2tp_enable", true)
|
||||
end
|
||||
end
|
||||
secret.rmempty = false
|
||||
|
||||
return m
|
||||
@@ -1,10 +1,10 @@
|
||||
local d = require "luci.dispatcher"
|
||||
local sys = require "luci.sys"
|
||||
local fs = require "nixio.fs"
|
||||
|
||||
m = Map("luci-app-ipsec-server")
|
||||
|
||||
s = m:section(TypedSection, "ipsec_users", "IPSec Xauth PSK " .. translate("Users Manager"))
|
||||
s.description = translate("Use a client that supports IPSec Xauth PSK (iOS or Android) to connect to this server.")
|
||||
s = m:section(TypedSection, "ipsec_users", "IPSec Xauth PSK/EAP " .. translate("Users Manager"))
|
||||
s.description = translate("Use a client that supports IPSec Xauth PSK or IPSec EAP (iOS, Android, macOS, or Windows) to connect to this server.")
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
s.template = "cbi/tblsection"
|
||||
@@ -19,9 +19,10 @@ o.rmempty = false
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.placeholder = translate("Password")
|
||||
o.password = true
|
||||
o.rmempty = false
|
||||
|
||||
if sys.call("command -v xl2tpd > /dev/null") == 0 then
|
||||
if fs.access("/usr/sbin/xl2tpd") then
|
||||
s = m:section(TypedSection, "l2tp_users", "L2TP/IPSec PSK " .. translate("Users Manager"))
|
||||
s.description = translate("Use a client that supports L2TP over IPSec PSK to connect to this server.")
|
||||
s.addremove = true
|
||||
@@ -31,7 +32,7 @@ if sys.call("command -v xl2tpd > /dev/null") == 0 then
|
||||
function s.create(e, t)
|
||||
t = TypedSection.create(e, t)
|
||||
luci.http.redirect(e.extedit:format(t))
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Flag, "enabled", translate("Enabled"))
|
||||
o.default = 1
|
||||
@@ -43,6 +44,7 @@ if sys.call("command -v xl2tpd > /dev/null") == 0 then
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.placeholder = translate("Password")
|
||||
o.password = true
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "ipaddress", translate("IP address"))
|
||||
|
||||
@@ -1,21 +1,61 @@
|
||||
<style>
|
||||
.cbi-map {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.ipsec-server-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ipsec-server-status::before {
|
||||
width: .65em;
|
||||
height: .65em;
|
||||
margin-right: .55em;
|
||||
border-radius: 50%;
|
||||
background: #6b7280;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.ipsec-server-status.is-running::before {
|
||||
background: #198754;
|
||||
}
|
||||
|
||||
.ipsec-server-status.is-stopped::before {
|
||||
background: #c0392b;
|
||||
}
|
||||
</style>
|
||||
|
||||
<% include("cbi/map") %>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
XHR.poll(2, '<%=luci.dispatcher.build_url("admin", "vpn", "ipsec-server", "status")%>', null,
|
||||
function(x, result)
|
||||
{
|
||||
var ipsec_status = document.getElementsByClassName('ipsec-server_status')[0];
|
||||
if (ipsec_status) {
|
||||
ipsec_status.setAttribute("style","font-weight:bold;");
|
||||
ipsec_status.setAttribute("color",result.ipsec_status ? "green":"red");
|
||||
ipsec_status.innerHTML = result.ipsec_status?'<%=translate("RUNNING")%>':'<%=translate("NOT RUNNING")%>';
|
||||
}
|
||||
var l2tp_status = document.getElementsByClassName('l2tp_status')[0];
|
||||
if (l2tp_status) {
|
||||
l2tp_status.setAttribute("style","font-weight:bold;");
|
||||
l2tp_status.setAttribute("color",result.l2tp_status ? "green":"red");
|
||||
l2tp_status.innerHTML = result.l2tp_status?'<%=translate("RUNNING")%>':'<%=translate("NOT RUNNING")%>';
|
||||
}
|
||||
(function() {
|
||||
var status = document.getElementsByClassName('ipsec-server-status')[0];
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
)
|
||||
|
||||
XHR.poll(5, '<%=luci.dispatcher.build_url("admin", "vpn", "ipsec-server", "status")%>', null,
|
||||
function(x, result) {
|
||||
var className;
|
||||
var label;
|
||||
|
||||
if (!result || typeof result.ipsec_status !== 'boolean') {
|
||||
className = 'ipsec-server-status is-unknown';
|
||||
label = '<%=translate("Status unavailable")%>';
|
||||
}
|
||||
else {
|
||||
className = 'ipsec-server-status ' + (result.ipsec_status ? 'is-running' : 'is-stopped');
|
||||
label = result.ipsec_status ? '<%=translate("RUNNING")%>' : '<%=translate("NOT RUNNING")%>';
|
||||
}
|
||||
|
||||
if (status.className !== className) {
|
||||
status.className = className;
|
||||
status.textContent = label;
|
||||
}
|
||||
}
|
||||
);
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,38 +1,56 @@
|
||||
msgid "IPSec VPN Server"
|
||||
msgstr "IPSec VPN 服务器"
|
||||
|
||||
msgid "Use a client that supports IPSec Xauth PSK (iOS or Android) to connect to this server."
|
||||
msgstr "使用支持 IPSec Xauth PSK(iOS 或 Android)的客户端连接到此服务端。"
|
||||
msgid "Enable IPsec access for IKEv2 PSK, IKEv1 XAuth PSK, and IKEv2 EAP clients."
|
||||
msgstr "启用 IKEv2 PSK、IKEv1 XAuth PSK 和 IKEv2 EAP 客户端接入。"
|
||||
|
||||
msgid "Use a client that supports L2TP over IPSec PSK to connect to this server."
|
||||
msgstr "使用支持 L2TP over IPSec PSK 的客户端连接到此服务端。"
|
||||
msgid "Use a client that supports IPSec Xauth PSK or IPSec EAP (iOS, Android, macOS, or Windows) to connect to this server."
|
||||
msgstr "请使用支持 IPSec Xauth PSK 或 IPSec EAP 的客户端(iOS、Android、macOS、Windows)连接到此服务器。"
|
||||
|
||||
msgid "Current Condition"
|
||||
msgstr "当前状态"
|
||||
|
||||
msgid "General settings"
|
||||
msgid "Checking..."
|
||||
msgstr "正在检查..."
|
||||
|
||||
msgid "Status unavailable"
|
||||
msgstr "状态不可用"
|
||||
|
||||
msgid "General Settings"
|
||||
msgstr "基本设置"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "VPN Client IP"
|
||||
msgstr "VPN客户端地址段"
|
||||
msgstr "VPN 客户端地址池"
|
||||
|
||||
msgid "VPN Client reserved started IP addresses with the same subnet mask, such as: 192.168.100.10/24"
|
||||
msgstr "VPN客户端获取IP的起始地址,例如:192.168.100.10/24"
|
||||
msgid "VPN Server IP"
|
||||
msgstr "VPN 服务端地址"
|
||||
|
||||
msgid "VPN client address pool in CIDR notation, such as: 10.0.10.0/24"
|
||||
msgstr "VPN 客户端地址池(CIDR 格式),例如:10.0.10.0/24"
|
||||
|
||||
msgid "VPN server address and subnet in CIDR notation, such as: 10.0.0.1/24"
|
||||
msgstr "VPN 服务端地址和子网(CIDR 格式),例如:10.0.0.1/24"
|
||||
|
||||
msgid "Domain name used for IKEv2 certificate and Remote ID, such as: ns.miaogongzi.cc"
|
||||
msgstr "用于 IKEv2 证书和远程 ID 的域名,例如:ns.miaogongzi.cc"
|
||||
|
||||
msgid "Server private key file name under /etc/ipsec.d/private/, such as: server_key.pem"
|
||||
msgstr "位于 /etc/ipsec.d/private/ 的服务器私钥文件名,例如:server_key.pem"
|
||||
|
||||
msgid "Server certificate file name under /etc/ipsec.d/certs/, such as: server.pem"
|
||||
msgstr "位于 /etc/ipsec.d/certs/ 的服务器证书文件名,例如:server.pem"
|
||||
|
||||
msgid "Secret Pre-Shared Key"
|
||||
msgstr "PSK密钥"
|
||||
msgstr "PSK 密钥"
|
||||
|
||||
msgid "VPN Server IP address, such as: 192.168.101.1"
|
||||
msgstr "VPN服务端IP地址,例如:192.168.101.1"
|
||||
|
||||
msgid "VPN Client IP address range, such as: 192.168.101.10-20"
|
||||
msgstr "VPN客户端获取IP范围,例如:192.168.101.10-20"
|
||||
|
||||
msgid "L2TP/IPSec is not compatible with kernel-libipsec, which will disable this module."
|
||||
msgstr "L2TP/IPSec不兼容kernel-libipsec,开启将会禁用此模块。"
|
||||
msgid "Used by IKEv2 PSK and IKEv1 XAuth PSK clients."
|
||||
msgstr "供 IKEv2 PSK 和 IKEv1 XAuth PSK 客户端使用。"
|
||||
|
||||
msgid "Users Manager"
|
||||
msgstr "用户管理"
|
||||
@@ -49,29 +67,23 @@ msgstr "IP 地址"
|
||||
msgid "Automatically"
|
||||
msgstr "自动分配"
|
||||
|
||||
msgid "Online Users"
|
||||
msgstr "在线用户"
|
||||
msgid "Static Routes"
|
||||
msgstr "静态路由"
|
||||
|
||||
msgid "L2TP Online Users"
|
||||
msgstr "L2TP 在线用户"
|
||||
|
||||
msgid "Login Time"
|
||||
msgstr "登录时间"
|
||||
|
||||
msgid "Blacklist"
|
||||
msgstr "黑名单"
|
||||
|
||||
msgid "Add to Blacklist"
|
||||
msgstr "加入黑名单"
|
||||
|
||||
msgid "Remove from Blacklist"
|
||||
msgstr "移出黑名单"
|
||||
|
||||
msgid "Forced offline"
|
||||
msgstr "强制下线"
|
||||
msgid "Use a client that supports L2TP over IPSec PSK to connect to this server."
|
||||
msgstr "使用支持 L2TP over IPSec PSK 的客户端连接到此服务器。"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "Certificate File Name"
|
||||
msgstr "证书文件名"
|
||||
|
||||
msgid "Private Key File Name"
|
||||
msgstr "私钥文件名"
|
||||
|
||||
msgid "VPN Domain"
|
||||
msgstr "VPN 域名"
|
||||
@@ -2,6 +2,9 @@
|
||||
config service 'ipsec'
|
||||
option enabled '0'
|
||||
option secret 'ipsec'
|
||||
option clientip '192.168.100.10/24'
|
||||
|
||||
option clientip '10.0.10.0/24'
|
||||
option serverip '10.0.0.1/24'
|
||||
option certs_name 'server.pem'
|
||||
option private_key 'server_key.pem'
|
||||
option domain 'ns.miaogongzi.cc'
|
||||
|
||||
@@ -13,6 +13,10 @@ L2TP_OPTIONS_FILE=${L2TP_PATH}/options.xl2tpd
|
||||
L2TP_LOG_FILE=${L2TP_PATH}/xl2tpd.log
|
||||
|
||||
vt_clientip=$(uci -q get ${CONFIG}.@service[0].clientip)
|
||||
vt_serverip=$(uci -q get ${CONFIG}.@service[0].serverip)
|
||||
certs_name=$(uci -q get ${CONFIG}.@service[0].certs_name)
|
||||
private_key=$(uci -q get ${CONFIG}.@service[0].private_key)
|
||||
vpn_domain=$(uci -q get ${CONFIG}.@service[0].domain)
|
||||
l2tp_enabled=$(uci -q get ${CONFIG}.@service[0].l2tp_enable)
|
||||
l2tp_localip=$(uci -q get ${CONFIG}.@service[0].l2tp_localip)
|
||||
|
||||
@@ -24,8 +28,8 @@ get_enabled_anonymous_secs() {
|
||||
|
||||
ipt_rule() {
|
||||
if [ "$1" = "add" ]; then
|
||||
iptables -t nat -I POSTROUTING -s ${vt_clientip} -m comment --comment "${ipt_flag}" -j MASQUERADE 2>/dev/null
|
||||
iptables -I forwarding_rule -s ${vt_clientip} -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
|
||||
iptables -t nat -I POSTROUTING -s ${vt_serverip} -m comment --comment "${ipt_flag}" -j MASQUERADE 2>/dev/null
|
||||
iptables -I forwarding_rule -s ${vt_serverip} -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
|
||||
iptables -I forwarding_rule -m policy --dir in --pol ipsec --proto esp -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
|
||||
iptables -I forwarding_rule -m policy --dir out --pol ipsec --proto esp -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
|
||||
iptables -I INPUT -p udp -m multiport --dports 500,4500 -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
|
||||
@@ -70,9 +74,9 @@ gen_include() {
|
||||
|
||||
start() {
|
||||
local vt_enabled=$(uci -q get ${CONFIG}.@service[0].enabled)
|
||||
[ "$vt_enabled" = 0 ] && return 1
|
||||
[ "$vt_enabled" = 1 ] || return 0
|
||||
|
||||
local vt_gateway="${vt_clientip%.*}.1"
|
||||
local vt_gateway="${vt_serverip%.*}.1"
|
||||
local vt_secret=$(uci -q get ${CONFIG}.@service[0].secret)
|
||||
|
||||
local l2tp_enabled=$(uci -q get ${CONFIG}.@service[0].l2tp_enable)
|
||||
@@ -185,8 +189,8 @@ start() {
|
||||
|
||||
conn Non-L2TP
|
||||
leftsubnet=0.0.0.0/0
|
||||
rightsubnet=${vt_clientip}
|
||||
rightsourceip=${vt_clientip}
|
||||
rightsubnet=${vt_serverip}
|
||||
rightsourceip=${vt_serverip}
|
||||
rightdns=${vt_gateway}
|
||||
ike=aes128-sha1-modp2048,aes128-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1536
|
||||
esp=aes128-sha1,3des-sha1
|
||||
@@ -198,6 +202,52 @@ start() {
|
||||
leftauth=psk
|
||||
rightauth=psk
|
||||
rightauth2=xauth
|
||||
|
||||
conn IKEv2-PSK
|
||||
keyexchange=ikev2
|
||||
auto=add
|
||||
left=%defaultroute
|
||||
leftauth=psk
|
||||
leftsubnet=0.0.0.0/0
|
||||
right=%any
|
||||
rightauth=psk
|
||||
rightsourceip=${vt_clientip}
|
||||
|
||||
ike=aes128-sha1-modp2048,aes128-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1536
|
||||
esp=aes128-sha1,3des-sha1
|
||||
dpdaction=clear
|
||||
dpddelay=30s
|
||||
rekey=no
|
||||
compress=no
|
||||
|
||||
conn ikev2-EAP
|
||||
keyexchange=ikev2
|
||||
auto=add
|
||||
fragmentation=yes
|
||||
forceencaps=yes
|
||||
|
||||
left=%defaultroute
|
||||
leftid=@$vpn_domain
|
||||
leftauth=pubkey
|
||||
leftcert=${certs_name}
|
||||
leftsendcert=always
|
||||
leftsubnet=0.0.0.0/0
|
||||
|
||||
right=%any
|
||||
rightid=%any
|
||||
rightauth=eap-mschapv2
|
||||
eap_identity=%identity
|
||||
rightsourceip=${vt_clientip}
|
||||
rightdns=10.0.0.1
|
||||
|
||||
ike=aes256-sha256-modp1024,aes256-sha1-modp1024,aes128-sha256-modp1024,aes128-sha1-modp1024,aes256-sha256-modp2048,aes256-sha1-modp2048
|
||||
esp=aes256-sha256,aes128-sha256,aes256-sha1,aes128-sha1
|
||||
dpdaction=clear
|
||||
dpddelay=30s
|
||||
rekey=no
|
||||
compress=no
|
||||
|
||||
|
||||
|
||||
$ipsec_l2tp_config
|
||||
EOF
|
||||
@@ -205,6 +255,7 @@ start() {
|
||||
cat > /etc/ipsec.secrets <<-EOF
|
||||
# /etc/ipsec.secrets - strongSwan IPsec secrets file
|
||||
: PSK "$vt_secret"
|
||||
: RSA /etc/ipsec.d/private/$private_key
|
||||
EOF
|
||||
|
||||
local ipsec_users=$(get_enabled_anonymous_secs "@ipsec_users")
|
||||
@@ -220,6 +271,7 @@ start() {
|
||||
[ -n "${u_password}" ] || continue
|
||||
|
||||
echo "${u_username} : XAUTH '${u_password}'" >> ${IPSEC_SECRETS_FILE}
|
||||
echo "${u_username} : EAP '${u_password}'" >> ${IPSEC_SECRETS_FILE}
|
||||
done
|
||||
}
|
||||
unset user
|
||||
@@ -230,7 +282,7 @@ start() {
|
||||
gen_include
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.ipsec_server.ipaddr="${vt_clientip%.*}.1"
|
||||
set network.ipsec_server.ipaddr="${vt_serverip%.*}.1"
|
||||
commit network
|
||||
EOF
|
||||
ifup ipsec_server > /dev/null 2>&1
|
||||
@@ -254,7 +306,7 @@ gen_iface_and_firewall() {
|
||||
set network.ipsec_server.device="ipsec0"
|
||||
set network.ipsec_server.device="ipsec0"
|
||||
set network.ipsec_server.proto="static"
|
||||
set network.ipsec_server.ipaddr="${vt_clientip%.*}.1"
|
||||
set network.ipsec_server.ipaddr="${vt_serverip%.*}.1"
|
||||
set network.ipsec_server.netmask="255.255.255.0"
|
||||
commit network
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
}
|
||||
EEOF
|
||||
}
|
||||
rm -f /usr/share/ucitrack/luci-app-luci-app-ipsec-server.json
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete firewall.luci_app_ipsec_server
|
||||
@@ -14,6 +15,7 @@ uci -q batch <<-EOF >/dev/null
|
||||
set firewall.luci_app_ipsec_server.type=script
|
||||
set firewall.luci_app_ipsec_server.path=/var/etc/ipsecvpn.include
|
||||
set firewall.luci_app_ipsec_server.reload=1
|
||||
commit firewall
|
||||
EOF
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
_LOGOUT_TIME="$(date "+%Y-%m-%d %H:%M:%S")"
|
||||
CONFIG="luci-app-ipsec-server"
|
||||
L2TP_PATH=/var/etc/xl2tpd
|
||||
L2TP_SESSION_PATH=${L2TP_PATH}/session
|
||||
|
||||
_USERNAME=${PEERNAME}
|
||||
_IFACE=${1}
|
||||
_TTY=${2}
|
||||
_SPEED=${3}
|
||||
_LOCALIP=${4}
|
||||
_PEERIP=${5}
|
||||
_REMOTEIP=${6}
|
||||
_BYTES_SENT=${BYTES_SENT}
|
||||
_BYTES_RCVD=${BYTES_RCVD}
|
||||
_CONNECT_TIME=${CONNECT_TIME}
|
||||
|
||||
rm -f ${L2TP_SESSION_PATH}/${_USERNAME}.${_IFACE}
|
||||
rm -f /var/run/${_IFACE}.pid
|
||||
|
||||
#可根据退出的账号自定义脚本,如静态路由表,组网等。
|
||||
SCRIPT="/usr/share/xl2tpd/ip-down.d/${_USERNAME}"
|
||||
[ -s "$SCRIPT" ] && {
|
||||
[ ! -x "$SCRIPT" ] && chmod 0755 "$SCRIPT"
|
||||
"$SCRIPT" "$@"
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
_LOGIN_TIME="$(date "+%Y-%m-%d %H:%M:%S")"
|
||||
CONFIG="luci-app-ipsec-server"
|
||||
L2TP_PATH=/var/etc/xl2tpd
|
||||
L2TP_SESSION_PATH=${L2TP_PATH}/session
|
||||
|
||||
_USERNAME=${PEERNAME}
|
||||
_IFACE=${1}
|
||||
_TTY=${2}
|
||||
_SPEED=${3}
|
||||
_LOCALIP=${4}
|
||||
_PEERIP=${5}
|
||||
|
||||
_PID=$(cat /var/run/${_IFACE}.pid 2>/dev/null)
|
||||
_REMOTEIP=$(cat /var/etc/xl2tpd/xl2tpd.log 2>/dev/null | grep "PID: ${_PID}" | grep -o -E '([0-9]{1,3}[\.]){3}[0-9]{1,3}')
|
||||
|
||||
mkdir -p ${L2TP_SESSION_PATH}
|
||||
|
||||
cat <<-EOF > ${L2TP_SESSION_PATH}/${_USERNAME}.${_IFACE}
|
||||
{
|
||||
"username": "${_USERNAME}",
|
||||
"interface": "${_IFACE}",
|
||||
"tty": "${_TTY}",
|
||||
"speed": "${_SPEED}",
|
||||
"ip": "${_PEERIP}",
|
||||
"remote_ip": "${_REMOTEIP}",
|
||||
"pid": "${_PID}",
|
||||
"login_time": "${_LOGIN_TIME}"
|
||||
}
|
||||
EOF
|
||||
|
||||
#只能单用户使用
|
||||
cfgid=$(uci show ${CONFIG} | grep "@l2tp_users" | grep "\.username='${_USERNAME}'" | cut -d '.' -sf 2)
|
||||
[ -n "$cfgid" ] && {
|
||||
HAS_LOGIN=$(ls ${L2TP_SESSION_PATH} | grep "^${_USERNAME}\.ppp" | grep -v "${_IFACE}")
|
||||
[ -n "$HAS_LOGIN" ] && {
|
||||
#踢出之前的用户
|
||||
KO_IFACE=$(echo $HAS_LOGIN | awk -F '.' '{print $2}')
|
||||
KO_PID=$(cat /var/run/${KO_IFACE}.pid 2>/dev/null)
|
||||
[ -n "$KO_PID" ] && kill -9 ${KO_PID} >/dev/null 2>&1
|
||||
rm -f ${L2TP_SESSION_PATH}/${HAS_LOGIN}
|
||||
rm -f /var/run/${KO_IFACE}.pid
|
||||
}
|
||||
routes=$(uci -q get ${CONFIG}.${cfgid}.routes)
|
||||
[ -n "$routes" ] && {
|
||||
for router in ${routes}; do
|
||||
route add -net ${router} dev ${_IFACE} >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
}
|
||||
|
||||
#可根据登录的账号自定义脚本,如组网、日志、限速、权限等特殊待遇。
|
||||
SCRIPT="/usr/share/xl2tpd/ip-up.d/${_USERNAME}"
|
||||
[ -s "$SCRIPT" ] && {
|
||||
[ ! -x "$SCRIPT" ] && chmod 0755 "$SCRIPT"
|
||||
"$SCRIPT" "$@"
|
||||
}
|
||||
+8
-3
@@ -8,8 +8,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mwan3-nft
|
||||
PKG_VERSION:=3.6.9
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=3.6.12
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_MAINTAINER:=Florian Eckert <fe@dev.tdt.de>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
@@ -35,7 +35,8 @@ define Package/$(PKG_NAME)
|
||||
+ucode-mod-uci \
|
||||
+ucode-mod-ubus \
|
||||
+ucode-mod-fs \
|
||||
+ucode-mod-socket
|
||||
+ucode-mod-socket \
|
||||
+ucode-mod-log
|
||||
CONFLICTS:=mwan3
|
||||
TITLE:=Multiwan hotplug script with connection tracking support (ucode rtmon)
|
||||
MAINTAINER:=Florian Eckert <fe@dev.tdt.de>
|
||||
@@ -220,6 +221,10 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_BIN) ./files/usr/share/rpcd/ucode/mwan3 \
|
||||
$(1)/usr/share/rpcd/ucode/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/ucode/mwan3
|
||||
$(INSTALL_DATA) ./files/usr/share/ucode/mwan3/common.uc \
|
||||
$(1)/usr/share/ucode/mwan3/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/usr/sbin/mwan3 \
|
||||
$(1)/usr/sbin/
|
||||
|
||||
+721
-179
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
# For full documentation of mwan3 configuration:
|
||||
# https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3#mwan3_configuration
|
||||
# https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3-nft#configuration
|
||||
|
||||
config globals 'globals'
|
||||
option mmx_mask '0x3F00'
|
||||
|
||||
@@ -52,9 +52,24 @@ LOG notice "Execute $ACTION event on interface $INTERFACE (${DEVICE:-unknown})"
|
||||
|
||||
case "$ACTION" in
|
||||
connected)
|
||||
# A tracker recovery must restore the member's routing before the policy
|
||||
# re-selects it, so the member is routable the instant it is marked, then
|
||||
# snap new flows off the worse-metric backups back toward it. The policy
|
||||
# rebuild commits as one nft transaction so no packet classifies against a
|
||||
# half-rebuilt policy chain. The snap-back stays outside that batch on
|
||||
# purpose: a recovery's worse-metric pins point at backups that are still
|
||||
# up, so a source re-pinned in the window before the snap reaches a live
|
||||
# member, not a dead one, unlike a teardown whose pins must be flushed in
|
||||
# the same transaction because they point at the departing member.
|
||||
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "online"
|
||||
mwan3_create_iface_nft $INTERFACE $DEVICE
|
||||
mwan3_create_iface_rules $INTERFACE $DEVICE
|
||||
mwan3_create_iface_route $INTERFACE $DEVICE
|
||||
mwan3_nft_batch_start
|
||||
mwan3_set_policies_nft
|
||||
mwan3_nft_batch_commit
|
||||
mwan3_flush_worse_metric_sticky $INTERFACE
|
||||
mwan3_flush_unreplied_conntrack
|
||||
;;
|
||||
ifup)
|
||||
@@ -66,24 +81,52 @@ case "$ACTION" in
|
||||
if [ "$MWAN3_STARTUP" != "init" ]; then
|
||||
mwan3_set_general_rules
|
||||
if [ "$status" = "online" ]; then
|
||||
|
||||
# Same atomic policy rebuild and out-of-batch snap-back as the
|
||||
# connected arm above; see that comment for why the snap-back is
|
||||
# not folded into the batch. The batch sits inside the online
|
||||
# guard so the init path never opens one.
|
||||
|
||||
mwan3_nft_batch_start
|
||||
mwan3_set_policies_nft
|
||||
mwan3_nft_batch_commit
|
||||
mwan3_flush_worse_metric_sticky $INTERFACE
|
||||
mwan3_flush_unreplied_conntrack
|
||||
fi
|
||||
fi
|
||||
[ "$ACTION" = ifup ] && procd_running mwan3 "track_$INTERFACE" && procd_send_signal mwan3 "track_$INTERFACE" USR2
|
||||
;;
|
||||
disconnected)
|
||||
# A soft tracker failure drops the member from the policy; flush its
|
||||
# sticky pins after the rebuild so a pinned source re-evaluates to the
|
||||
# survivor, the same treatment a hard ifdown gets. The policy rebuild
|
||||
# and the sticky flush commit as one nft transaction so no packet
|
||||
# classifies against a half-rebuilt policy. The member's marked flows
|
||||
# are cleared by the conntrack flush at the handler tail, which is
|
||||
# kernel state outside nftables and stays a separate operation.
|
||||
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "offline"
|
||||
mwan3_nft_batch_start
|
||||
mwan3_set_policies_nft
|
||||
mwan3_delete_iface_map_entries $INTERFACE
|
||||
mwan3_nft_batch_commit
|
||||
;;
|
||||
ifdown)
|
||||
# Drop the member from the policy before tearing down its routing, so the
|
||||
# policy never marks for a member whose rules are already gone; flush its
|
||||
# sticky sets after the rebuild so the rebuild cannot re-seed them. The
|
||||
# policy rebuild and the sticky flush commit as one nft transaction; the
|
||||
# routing teardown and the conntrack flush follow as separate steps.
|
||||
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "offline"
|
||||
mwan3_nft_batch_start
|
||||
mwan3_set_policies_nft
|
||||
mwan3_delete_iface_map_entries $INTERFACE
|
||||
mwan3_nft_batch_commit
|
||||
mwan3_delete_iface_rules $INTERFACE
|
||||
mwan3_delete_iface_route $INTERFACE
|
||||
mwan3_delete_iface_nft $INTERFACE
|
||||
procd_running mwan3 "track_$INTERFACE" && procd_send_signal mwan3 "track_$INTERFACE" USR1
|
||||
mwan3_set_policies_nft
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ start_service() {
|
||||
mwan3_set_connected_sets
|
||||
mwan3_set_custom_sets
|
||||
mwan3_set_general_rules
|
||||
mwan3_set_member_backstops
|
||||
config_foreach mwan3_ifup interface "init"
|
||||
wait $hotplug_pids
|
||||
mwan3_set_general_nft
|
||||
@@ -134,11 +135,13 @@ stop_service() {
|
||||
|
||||
# Delete ip rules created by this mwan3 instance. A rule must pass
|
||||
# both gates before it is deleted:
|
||||
# priority gate -- the priority lies in one of mwan3's three
|
||||
# configured rule-base ranges (iif, fwmark, unreachable). The
|
||||
# priority gate -- the priority lies in one of mwan3's four
|
||||
# rule-base ranges (iif, fwmark, unreachable, source). The
|
||||
# fwmark range extends to fwmark_base + MM_UNREACHABLE to cover
|
||||
# the global blackhole and unreachable rules at fwmark_base +
|
||||
# MM_BLACKHOLE / MM_UNREACHABLE.
|
||||
# MM_BLACKHOLE / MM_UNREACHABLE. The source band is not
|
||||
# configurable; it is derived from the unreachable base and
|
||||
# sits immediately above the unreachable band.
|
||||
# content gate -- the line either references a mwan3-owned routing
|
||||
# table (lookup <N> with N in 1..MWAN3_INTERFACE_MAX) or carries
|
||||
# an fwmark masked by mwan3's MMX_MASK. The priority gate alone
|
||||
@@ -149,13 +152,15 @@ stop_service() {
|
||||
# /var/run/mwan3/mmx_mask and /var/state/mwan3 (set at start_service
|
||||
# time), so they reflect what the running instance created, not
|
||||
# whatever is currently in /etc/config/mwan3.
|
||||
local line pref tbl iif_min iif_max fw_min fw_max ur_min ur_max mask
|
||||
local line pref tbl iif_min iif_max fw_min fw_max ur_min ur_max src_min src_max mask
|
||||
iif_min=$((MWAN3_IIF_RULE_BASE + 1))
|
||||
iif_max=$((MWAN3_IIF_RULE_BASE + MWAN3_INTERFACE_MAX))
|
||||
fw_min=$((MWAN3_FWMARK_RULE_BASE + 1))
|
||||
fw_max=$((MWAN3_FWMARK_RULE_BASE + MM_UNREACHABLE))
|
||||
ur_min=$((MWAN3_UNREACHABLE_RULE_BASE + 1))
|
||||
ur_max=$((MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX))
|
||||
src_min=$((MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX + 2))
|
||||
src_max=$((MWAN3_UNREACHABLE_RULE_BASE + 2 * MWAN3_INTERFACE_MAX + 1))
|
||||
# ip rule list prints fwmark masks lowercase; normalise for matching
|
||||
mask=$(echo "$MMX_MASK" | tr 'A-Z' 'a-z')
|
||||
$IP rule list | while read -r line; do
|
||||
@@ -166,7 +171,8 @@ stop_service() {
|
||||
# Priority gate
|
||||
if ! { { [ "$pref" -ge "$iif_min" ] && [ "$pref" -le "$iif_max" ]; } || \
|
||||
{ [ "$pref" -ge "$fw_min" ] && [ "$pref" -le "$fw_max" ]; } || \
|
||||
{ [ "$pref" -ge "$ur_min" ] && [ "$pref" -le "$ur_max" ]; }; }; then
|
||||
{ [ "$pref" -ge "$ur_min" ] && [ "$pref" -le "$ur_max" ]; } || \
|
||||
{ [ "$pref" -ge "$src_min" ] && [ "$pref" -le "$src_max" ]; }; }; then
|
||||
continue
|
||||
fi
|
||||
# Content gate (a): carries an fwmark masked by mwan3's MMX_MASK
|
||||
@@ -190,40 +196,9 @@ stop_service() {
|
||||
done
|
||||
done
|
||||
|
||||
# Flush all mwan3 chains (rules removed, chains stay as skeletons)
|
||||
for chain_name in $($NFT list chains inet 2>/dev/null | grep "chain mwan3_" | awk '{print $2}'); do
|
||||
$NFT flush chain inet mwan3 "$chain_name" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Delete dynamic per-interface and per-policy chains (not the skeleton ones from 10-mwan3.nft)
|
||||
for chain_name in $($NFT list chains inet 2>/dev/null | grep "chain mwan3_" | awk '{print $2}'); do
|
||||
case "$chain_name" in
|
||||
mwan3_prerouting|mwan3_output|mwan3_postrouting|mwan3_ifaces_in|mwan3_rules|mwan3_connected|mwan3_custom|mwan3_dynamic)
|
||||
# These are skeleton chains from the static .nft file; keep them
|
||||
;;
|
||||
*)
|
||||
$NFT delete chain inet mwan3 "$chain_name" >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Final safety flush of skeleton chains (handles any transient failures above)
|
||||
for chain_name in mwan3_prerouting mwan3_output mwan3_postrouting mwan3_ifaces_in mwan3_rules mwan3_connected mwan3_custom mwan3_dynamic; do
|
||||
$NFT flush chain inet mwan3 "$chain_name" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Flush all mwan3 sets
|
||||
for setname in $($NFT list sets inet mwan3 2>/dev/null | grep "set mwan3_" | awk '{print $2}'); do
|
||||
$NFT flush set inet mwan3 "$setname" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Delete dynamic maps (sticky maps)
|
||||
for mapname in $($NFT list maps inet mwan3 2>/dev/null | grep "map mwan3_" | awk '{print $2}'); do
|
||||
$NFT flush map inet mwan3 "$mapname" >/dev/null 2>&1
|
||||
$NFT delete map inet mwan3 "$mapname" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Remove the table entirely so a stopped mwan3 leaves no nft residue
|
||||
# Remove the table entirely. The single atomic delete drops every
|
||||
# chain, set and map mwan3 created in one kernel transaction, so a
|
||||
# stopped mwan3 leaves no nft residue.
|
||||
$NFT delete table inet mwan3 >/dev/null 2>&1
|
||||
|
||||
rm -rf $MWAN3_STATUS_DIR $MWAN3TRACK_STATUS_DIR
|
||||
@@ -233,6 +208,23 @@ stop_service() {
|
||||
reload_service() {
|
||||
mwan3_init
|
||||
|
||||
# The nft table is created only by the skeleton load in start_service,
|
||||
# which is skipped when the service starts with every interface
|
||||
# disabled. In that state the table is absent while the status
|
||||
# directory (created by mwan3_init) exists, so the service reports
|
||||
# running and a config change drives a reload, not a start. Rebuilding
|
||||
# the ruleset here would batch every add against a missing table, the
|
||||
# atomic commit would fail, and this function would return at the commit
|
||||
# check before the tracker-mismatch restart below. Recover with a full
|
||||
# stop and start, which reloads the skeleton and recreates the table.
|
||||
|
||||
if ! $NFT list table inet mwan3 >/dev/null 2>&1; then
|
||||
LOG notice "reload escalated to restart as table inet mwan3 not initialised"
|
||||
stop
|
||||
start
|
||||
return
|
||||
fi
|
||||
|
||||
# Atomically rebuild the entire mwan3 nft ruleset in a single kernel
|
||||
# transaction. mwan3_nft_reload_start opens the outermost batch level
|
||||
# (MWAN3_BATCH_DEPTH 0->1) so all subsequent batch_start/commit calls
|
||||
@@ -256,6 +248,7 @@ reload_service() {
|
||||
# update them after the nft commit so the table is already consistent.
|
||||
mwan3_update_iface_to_table
|
||||
mwan3_set_general_rules
|
||||
mwan3_set_member_backstops
|
||||
config_foreach mwan3_rebuild_iface_rules interface
|
||||
|
||||
# Signal mwan3rtmon to reload UCI config and re-sync the custom sets.
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
# $DEVICE Physical device name of the interface the action relates to (e.g. "eth0" or "wwan0").
|
||||
# Note: On an ifdown event, $DEVICE is not available, use $INTERFACE instead.
|
||||
#
|
||||
# Further documentation can be found here:
|
||||
# https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3#alertsnotifications
|
||||
# Further documentation can be found at:
|
||||
# https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3-nft#hotplug_events_mwan3user
|
||||
@@ -4,6 +4,7 @@
|
||||
import * as rtnl from "rtnl";
|
||||
import * as uci from "uci";
|
||||
import * as ubus from "ubus";
|
||||
import { log_open, log_msg } from 'mwan3.common';
|
||||
|
||||
const RTM_GETROUTE = rtnl.const.RTM_GETROUTE;
|
||||
const RTM_NEWROUTE = rtnl.const.RTM_NEWROUTE;
|
||||
@@ -56,12 +57,14 @@ let tid = 0;
|
||||
cur.foreach("mwan3", "interface", function(s) {
|
||||
tid++;
|
||||
let fam = s.family ?? "ipv4";
|
||||
let enabled = +(s.enabled ?? "1");
|
||||
let enabled = +(s.enabled ?? "0");
|
||||
if (enabled && fam == family_name)
|
||||
name_tid[s[".name"]] = tid;
|
||||
});
|
||||
cur.unload("mwan3");
|
||||
|
||||
log_open("mwan3-create-iface-route");
|
||||
|
||||
let dev_table_map = {};
|
||||
let uconn = ubus.connect();
|
||||
if (uconn) {
|
||||
@@ -117,5 +120,5 @@ for (let route in source_routes) {
|
||||
rtnl.request(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_REPLACE, r);
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-create-iface-route: table %d: %s\n", table_id, err));
|
||||
log_msg("err", sprintf("table %d: %s", table_id, err));
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
'use strict';
|
||||
|
||||
import * as rtnl from "rtnl";
|
||||
import { is_default_equivalent } from 'mwan3.common';
|
||||
|
||||
const RTM_GETROUTE = rtnl.const.RTM_GETROUTE;
|
||||
const NLM_F_DUMP = rtnl.const.NLM_F_DUMP;
|
||||
@@ -18,12 +19,6 @@ function is_cidr_route(route, family_num) {
|
||||
return (family_num == AF_INET) ? (prefix_len < 32) : (prefix_len < 128);
|
||||
}
|
||||
|
||||
function is_default_route(route) {
|
||||
return (route.dst == null ||
|
||||
route.dst == "0.0.0.0/0" ||
|
||||
route.dst == "::/0");
|
||||
}
|
||||
|
||||
function is_linklocal_route(route) {
|
||||
return (route.dst != null &&
|
||||
(match(route.dst, /^fe80::\//) != null ||
|
||||
@@ -40,7 +35,7 @@ let seen = {};
|
||||
for (let route in routes) {
|
||||
if (route.table != table_num) continue;
|
||||
if (!is_cidr_route(route, family_num)) continue;
|
||||
if (is_default_route(route)) continue;
|
||||
if (is_default_equivalent(route)) continue;
|
||||
if (is_linklocal_route(route)) continue;
|
||||
if (family_num == AF_INET) {
|
||||
let first_octet = +split(route.dst, ".")[0];
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
'use strict';
|
||||
|
||||
import * as rtnl from "rtnl";
|
||||
import { log_open, log_msg } from 'mwan3.common';
|
||||
|
||||
const RTM_GETRULE = rtnl.const.RTM_GETRULE;
|
||||
const RTM_DELRULE = rtnl.const.RTM_DELRULE;
|
||||
const RTM_NEWRULE = rtnl.const.RTM_NEWRULE;
|
||||
const RTM_GETROUTE = rtnl.const.RTM_GETROUTE;
|
||||
const RTM_GETADDR = rtnl.const.RTM_GETADDR;
|
||||
const NLM_F_DUMP = rtnl.const.NLM_F_DUMP;
|
||||
const NLM_F_CREATE = rtnl.const.NLM_F_CREATE;
|
||||
const AF_INET = rtnl.const.AF_INET;
|
||||
@@ -22,6 +24,8 @@ function is_default_route(route) {
|
||||
route.dst == "::/0");
|
||||
}
|
||||
|
||||
log_open("mwan3-manage-rules");
|
||||
|
||||
let mode = ARGV[0];
|
||||
|
||||
if (mode == "check") {
|
||||
@@ -59,20 +63,33 @@ if (mode == "check") {
|
||||
let id = +ARGV[1];
|
||||
let iif_base = +ARGV[2];
|
||||
let fwmark_base = +ARGV[3];
|
||||
let unreachable_base = +ARGV[4];
|
||||
let mmx_mask = +ARGV[5];
|
||||
let mmx_mask = +ARGV[4];
|
||||
let src_prio = +ARGV[5];
|
||||
|
||||
let iif_prio = iif_base + id;
|
||||
let fwmark_prio = fwmark_base + id;
|
||||
let unreachable_prio = unreachable_base + id;
|
||||
|
||||
for (let family in [AF_INET, AF_INET6]) {
|
||||
let rules = rtnl.request(RTM_GETRULE, NLM_F_DUMP, { family: family }) ?? [];
|
||||
let fwmark_val = null;
|
||||
|
||||
for (let rule in rules) {
|
||||
if (rule.table != id) continue;
|
||||
if (rule.action != FR_ACT_TO_TBL) continue;
|
||||
|
||||
if (rule.src != null && rule.priority == src_prio) {
|
||||
rtnl.request(RTM_DELRULE, 0, {
|
||||
family: family,
|
||||
priority: rule.priority,
|
||||
src: rule.src,
|
||||
table: rule.table,
|
||||
action: rule.action
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
log_msg("err", sprintf("delete src rule failed: %s", err));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.src != null || rule.dst != null) continue;
|
||||
|
||||
if (rule.fwmark == null && rule.priority == iif_prio) {
|
||||
@@ -84,9 +101,8 @@ if (mode == "check") {
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: delete iif rule failed: %s\n", err));
|
||||
log_msg("err", sprintf("delete iif rule failed: %s", err));
|
||||
} else if (rule.fwmark != null && rule.priority == fwmark_prio && rule.fwmask == mmx_mask) {
|
||||
fwmark_val = rule.fwmark;
|
||||
rtnl.request(RTM_DELRULE, 0, {
|
||||
family: family,
|
||||
priority: rule.priority,
|
||||
@@ -97,27 +113,7 @@ if (mode == "check") {
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: delete fwmark rule failed: %s\n", err));
|
||||
}
|
||||
}
|
||||
|
||||
if (fwmark_val != null) {
|
||||
for (let rule in rules) {
|
||||
if (rule.fwmark != fwmark_val) continue;
|
||||
if (rule.action != FR_ACT_UNREACHABLE) continue;
|
||||
if (rule.priority != unreachable_prio) continue;
|
||||
if (rule.fwmask != mmx_mask) continue;
|
||||
if (rule.src != null || rule.dst != null) continue;
|
||||
rtnl.request(RTM_DELRULE, 0, {
|
||||
family: family,
|
||||
priority: rule.priority,
|
||||
fwmark: rule.fwmark,
|
||||
fwmask: rule.fwmask,
|
||||
action: rule.action
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: delete unreachable rule failed: %s\n", err));
|
||||
log_msg("err", sprintf("delete fwmark rule failed: %s", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +140,7 @@ if (mode == "check") {
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: add blackhole rule failed: %s\n", err));
|
||||
log_msg("err", sprintf("add blackhole rule failed: %s", err));
|
||||
}
|
||||
|
||||
if (!existing[ur_prio]) {
|
||||
@@ -157,6 +153,55 @@ if (mode == "check") {
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: add unreachable rule failed: %s\n", err));
|
||||
log_msg("err", sprintf("add unreachable rule failed: %s", err));
|
||||
}
|
||||
|
||||
} else if (mode == "add-backstop") {
|
||||
let family_num = (ARGV[1] == "6") ? AF_INET6 : AF_INET;
|
||||
let prio = +ARGV[2], mark = +ARGV[3];
|
||||
let mask = +ARGV[4];
|
||||
|
||||
let rules = rtnl.request(RTM_GETRULE, NLM_F_DUMP, { family: family_num }) ?? [];
|
||||
let existing = {};
|
||||
for (let r in rules)
|
||||
if (r.priority != null)
|
||||
existing[r.priority] = true;
|
||||
|
||||
if (!existing[prio]) {
|
||||
rtnl.request(RTM_NEWRULE, NLM_F_CREATE, {
|
||||
family: family_num,
|
||||
priority: prio,
|
||||
fwmark: mark,
|
||||
fwmask: mask,
|
||||
action: FR_ACT_UNREACHABLE
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
log_msg("err", sprintf("add backstop rule failed: %s", err));
|
||||
}
|
||||
|
||||
} else if (mode == "add-src") {
|
||||
// IPv4 only
|
||||
|
||||
let device = ARGV[1];
|
||||
let table_id = +ARGV[2];
|
||||
let prio = +ARGV[3];
|
||||
|
||||
let addrs = rtnl.request(RTM_GETADDR, NLM_F_DUMP, { family: AF_INET }) ?? [];
|
||||
for (let a in addrs) {
|
||||
if (a.dev != device) continue;
|
||||
if (a.scope != 0) continue;
|
||||
let addr = split(a.local ?? a.address ?? "", "/")[0];
|
||||
if (!addr) continue;
|
||||
rtnl.request(RTM_NEWRULE, NLM_F_CREATE, {
|
||||
family: AF_INET,
|
||||
priority: prio,
|
||||
src: addr,
|
||||
table: table_id,
|
||||
action: FR_ACT_TO_TBL
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
log_msg("err", sprintf("add src rule failed: %s", err));
|
||||
}
|
||||
}
|
||||
+336
-100
@@ -266,7 +266,8 @@ _mwan3_render_one_ipset()
|
||||
|
||||
set_decl="type ${addr_type}; flags interval"
|
||||
[ "$timeout" -gt 0 ] && set_decl="$set_decl, timeout"
|
||||
set_decl="$set_decl; auto-merge;"
|
||||
set_decl="$set_decl;"
|
||||
[ "$timeout" -eq 0 ] && set_decl="$set_decl auto-merge;"
|
||||
[ "$counters" -eq 1 ] && set_decl="$set_decl counter;"
|
||||
[ "$timeout" -gt 0 ] && set_decl="$set_decl timeout ${timeout}s;"
|
||||
[ "$maxelem" -gt 0 ] && set_decl="$set_decl size ${maxelem};"
|
||||
@@ -291,14 +292,35 @@ _mwan3_render_one_ipset()
|
||||
fi
|
||||
|
||||
# Collect all elements (inline list + loadfile) before entering batch.
|
||||
# Every element is validated against the nft set element grammar (IPv4 and
|
||||
# IPv6 addresses, CIDR prefixes and hyphen ranges) so a crafted value
|
||||
# cannot close the element braces early and inject nft statements into the
|
||||
# batch that is later committed with nft -f. Anything carrying a character
|
||||
# outside that class is dropped.
|
||||
|
||||
local elements="" line
|
||||
_add_entry() { elements="${elements:+$elements, }$1"; }
|
||||
_add_entry() {
|
||||
case "$1" in
|
||||
""|*[!0-9A-Fa-f:./-]*)
|
||||
LOG warn "config ipset '$name': dropping invalid entry"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
elements="${elements:+$elements, }$1"
|
||||
}
|
||||
config_list_foreach "$section" entry _add_entry
|
||||
if [ -n "$loadfile" ] && [ -f "$loadfile" ]; then
|
||||
while IFS= read -r line; do
|
||||
line="${line%%#*}"
|
||||
line=$(echo "$line" | xargs 2>/dev/null)
|
||||
[ -n "$line" ] && elements="${elements:+$elements, }$line"
|
||||
[ -n "$line" ] || continue
|
||||
case "$line" in
|
||||
*[!0-9A-Fa-f:./-]*)
|
||||
LOG warn "config ipset '$name': dropping invalid loadfile element"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
elements="${elements:+$elements, }$line"
|
||||
done < "$loadfile"
|
||||
fi
|
||||
|
||||
@@ -466,6 +488,41 @@ mwan3_set_general_rules()
|
||||
"$MMX_MASK"
|
||||
}
|
||||
|
||||
# Install a static unreachable ip rule for every configured interface mark, in
|
||||
# the per-member unreachable priority band. These are created at service start
|
||||
# and reload and removed only at stop, so a packet carrying a member mark whose
|
||||
# fwmark rule is currently absent (the member is down, disabled, or briefly
|
||||
# between a delete and re-add) is rejected instead of leaking to the main
|
||||
# routing table. One rule per member is required because an fwmark rule matches
|
||||
# an exact value under the mask, not "any member mark"; the reserved last_resort
|
||||
# marks are never enumerated here, so last_resort behaviour is unaffected.
|
||||
|
||||
mwan3_set_member_backstops()
|
||||
{
|
||||
mwan3_add_backstop()
|
||||
{
|
||||
local id family fam
|
||||
|
||||
config_get family "$1" family ipv4
|
||||
mwan3_get_iface_id id "$1"
|
||||
[ -n "$id" ] || return 0
|
||||
|
||||
if [ "$family" = "ipv4" ]; then
|
||||
fam=4
|
||||
elif [ "$family" = "ipv6" ] && [ $NO_IPV6 -eq 0 ]; then
|
||||
fam=6
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
|
||||
${MWAN3_MANAGE_RULES} add-backstop "$fam" \
|
||||
"$((id+MWAN3_UNREACHABLE_RULE_BASE))" \
|
||||
"$(mwan3_id2mask id MMX_MASK)" "$MMX_MASK"
|
||||
}
|
||||
|
||||
config_foreach mwan3_add_backstop interface
|
||||
}
|
||||
|
||||
mwan3_set_general_nft()
|
||||
{
|
||||
local chain_exists restore_vmap save_vmap all_marks
|
||||
@@ -843,16 +900,112 @@ mwan3_delete_iface_map_entries()
|
||||
# Sticky scheme: one set per (rule, family, iface_id) holding
|
||||
# only saddrs (no value side). Removing an interface invalidates every
|
||||
# such set whose name ends in "_<id>"; we flush rather than delete since
|
||||
# rule chains may still reference the set name.
|
||||
# rule chains may still reference the set name. The flush lines are pushed
|
||||
# into the caller's open nft batch so they commit in the same transaction
|
||||
# as the policy rebuild; the set enumeration reads committed kernel state
|
||||
# and so stays outside the batch. Call only within an open nft batch.
|
||||
|
||||
for setname in $($NFT list sets inet 2>/dev/null | \
|
||||
awk '$1=="set" && $2 ~ /^mwan3_sticky_v[46]_/ { print $2 }'); do
|
||||
case "$setname" in
|
||||
*_"$id") $NFT flush set inet mwan3 "$setname" 2>/dev/null ;;
|
||||
*_"$id") mwan3_nft_push "flush set inet mwan3 $setname" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
mwan3_flush_worse_metric_sticky()
|
||||
{
|
||||
# On recovery of a better-metric member, snap new flows off the worse-metric
|
||||
# backups it is now preferred over. For each policy the recovered interface
|
||||
# participates in, flush the per-rule sticky sets of that policy's same-family
|
||||
# members whose metric is strictly worse, scoped to the rules that use the
|
||||
# policy, so pinned sources re-evaluate to the recovered member on their next
|
||||
# new flow while established flows ride their conntrack marks to completion
|
||||
# undisturbed. The scope is per policy and per rule by construction: each set
|
||||
# name is built from the rule name and the member id rather than flushing
|
||||
# every set ending in an id, so a member that is also a deliberate-stickiness
|
||||
# member of a separate load-balance policy keeps those pins.
|
||||
|
||||
local recovered="$1" recovered_id rec_family rec_fam_short
|
||||
local _snap_pairs=""
|
||||
|
||||
mwan3_get_iface_id recovered_id "$recovered"
|
||||
[ -n "$recovered_id" ] || return 0
|
||||
|
||||
# mwan3 runs an independent metric contest per address family, so only
|
||||
# same-family members compete with the recovered one; resolve its family once
|
||||
# and snap only that family's backups.
|
||||
|
||||
config_get rec_family "$recovered" family ipv4
|
||||
if [ "$rec_family" = "ipv6" ]; then
|
||||
rec_fam_short="v6"
|
||||
else
|
||||
rec_fam_short="v4"
|
||||
fi
|
||||
|
||||
# Record one "policy=worse-id" pair per strictly-worse-metric same-family
|
||||
# member of each policy the recovered interface is in. The recovered
|
||||
# interface's own id is excluded so its own pins are never cleared.
|
||||
|
||||
_mwan3_snap_collect() {
|
||||
local policy="$1" rec_metric=""
|
||||
|
||||
# The recovered interface's metric in this policy sets the threshold;
|
||||
# take the lowest if it appears more than once.
|
||||
|
||||
_mwan3_snap_rec_metric() {
|
||||
local m_iface m_metric
|
||||
config_get m_iface "$1" interface
|
||||
[ "$m_iface" = "$recovered" ] || return
|
||||
config_get m_metric "$1" metric 1
|
||||
{ [ -z "$rec_metric" ] || [ "$m_metric" -lt "$rec_metric" ]; } && rec_metric="$m_metric"
|
||||
}
|
||||
config_list_foreach "$policy" use_member _mwan3_snap_rec_metric
|
||||
|
||||
# Policy does not include the recovered interface: nothing to snap.
|
||||
|
||||
[ -n "$rec_metric" ] || return
|
||||
|
||||
_mwan3_snap_worse() {
|
||||
local m_iface m_metric m_id m_family
|
||||
config_get m_iface "$1" interface
|
||||
[ -n "$m_iface" ] || return
|
||||
config_get m_family "$m_iface" family ipv4
|
||||
[ "$m_family" = "$rec_family" ] || return
|
||||
config_get m_metric "$1" metric 1
|
||||
[ "$m_metric" -gt "$rec_metric" ] || return
|
||||
mwan3_get_iface_id m_id "$m_iface"
|
||||
[ -n "$m_id" ] || return
|
||||
[ "$m_id" = "$recovered_id" ] && return
|
||||
case " $_snap_pairs " in
|
||||
*" ${policy}=${m_id} "*) ;;
|
||||
*) _snap_pairs="$_snap_pairs ${policy}=${m_id}" ;;
|
||||
esac
|
||||
}
|
||||
config_list_foreach "$policy" use_member _mwan3_snap_worse
|
||||
}
|
||||
config_foreach _mwan3_snap_collect policy
|
||||
|
||||
[ -n "$_snap_pairs" ] || return 0
|
||||
|
||||
# For every rule, flush the worse-metric members' per-rule sticky sets of the
|
||||
# policy that rule uses, in the recovered member's family. A set name that
|
||||
# does not exist is a harmless no-op.
|
||||
|
||||
_mwan3_snap_flush() {
|
||||
local r_policy pair p_name p_id
|
||||
config_get r_policy "$1" use_policy
|
||||
[ -n "$r_policy" ] || return
|
||||
for pair in $_snap_pairs; do
|
||||
p_name="${pair%=*}"
|
||||
p_id="${pair##*=}"
|
||||
[ "$p_name" = "$r_policy" ] || continue
|
||||
$NFT flush set inet mwan3 "mwan3_sticky_${rec_fam_short}_${1}_${p_id}" 2>/dev/null
|
||||
done
|
||||
}
|
||||
config_foreach _mwan3_snap_flush rule
|
||||
}
|
||||
|
||||
mwan3_create_iface_route()
|
||||
{
|
||||
local family id source_routing _fam_num
|
||||
@@ -904,7 +1057,8 @@ mwan3_create_iface_rules()
|
||||
|
||||
$IP rule add pref $((id+MWAN3_IIF_RULE_BASE)) iif "$2" lookup "$id" 2>/dev/null
|
||||
$IP rule add pref $((id+MWAN3_FWMARK_RULE_BASE)) fwmark "$(mwan3_id2mask id MMX_MASK)/$MMX_MASK" lookup "$id" 2>/dev/null
|
||||
$IP rule add pref $((id+MWAN3_UNREACHABLE_RULE_BASE)) fwmark "$(mwan3_id2mask id MMX_MASK)/$MMX_MASK" unreachable 2>/dev/null
|
||||
[ "$family" = "ipv4" ] && ${MWAN3_MANAGE_RULES} add-src "$2" "$id" \
|
||||
"$((id + MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX + 1))"
|
||||
}
|
||||
|
||||
mwan3_delete_iface_rules()
|
||||
@@ -915,13 +1069,13 @@ mwan3_delete_iface_rules()
|
||||
[ -n "$id" ] || return 0
|
||||
|
||||
${MWAN3_MANAGE_RULES} delete-iface "$id" \
|
||||
"$MWAN3_IIF_RULE_BASE" "$MWAN3_FWMARK_RULE_BASE" \
|
||||
"$MWAN3_UNREACHABLE_RULE_BASE" "$MMX_MASK"
|
||||
"$MWAN3_IIF_RULE_BASE" "$MWAN3_FWMARK_RULE_BASE" "$MMX_MASK" \
|
||||
"$((id + MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX + 1))"
|
||||
}
|
||||
|
||||
mwan3_set_policy()
|
||||
{
|
||||
local id iface family metric weight device is_lowest is_offline
|
||||
local id iface family metric weight is_lowest is_offline
|
||||
|
||||
is_lowest=0
|
||||
config_get iface "$1" interface
|
||||
@@ -929,7 +1083,6 @@ mwan3_set_policy()
|
||||
config_get weight "$1" weight 1
|
||||
|
||||
[ -n "$iface" ] || return 0
|
||||
network_get_device device "$iface"
|
||||
[ "$metric" -gt $DEFAULT_LOWEST_METRIC ] && LOG warn "Member interface $iface has >$DEFAULT_LOWEST_METRIC metric. Not appending to policy" && return 0
|
||||
|
||||
mwan3_get_iface_id id "$iface"
|
||||
@@ -976,30 +1129,28 @@ mwan3_set_policy()
|
||||
|
||||
if [ $is_offline -eq 0 ]; then
|
||||
|
||||
# Accumulate members per family: "iface_name:id:weight" tuples
|
||||
# Accumulate members per family: "iface_name:id:weight" tuples.
|
||||
# The branches mirror the metric contest above, so a member only
|
||||
# accumulates through its interface family's contest: an IPv6
|
||||
# member is excluded on a kernel without IPv6, and an
|
||||
# unrecognised family value is never accumulated.
|
||||
|
||||
if [ "$family" = "ipv4" ]; then
|
||||
policy_members_v4="$policy_members_v4 $iface:$id:$weight"
|
||||
else
|
||||
elif [ "$family" = "ipv6" ] && [ $NO_IPV6 -eq 0 ]; then
|
||||
policy_members_v6="$policy_members_v6 $iface:$id:$weight"
|
||||
fi
|
||||
elif [ -n "$device" ]; then
|
||||
|
||||
# Offline interface with device: record for fallback out-device rule
|
||||
|
||||
policy_offline_devices="$policy_offline_devices $iface:$device"
|
||||
fi
|
||||
}
|
||||
|
||||
mwan3_create_policies_nft()
|
||||
{
|
||||
local last_resort lowest_metric_v4 lowest_metric_v6 total_weight_v4 total_weight_v6
|
||||
local policy policy_members_v4 policy_members_v6 policy_offline_devices
|
||||
local policy policy_members_v4 policy_members_v6
|
||||
|
||||
policy="$1"
|
||||
policy_members_v4=""
|
||||
policy_members_v6=""
|
||||
policy_offline_devices=""
|
||||
|
||||
config_get last_resort "$1" last_resort unreachable
|
||||
|
||||
@@ -1097,22 +1248,6 @@ mwan3_create_policies_nft()
|
||||
done
|
||||
fi
|
||||
|
||||
# Add offline device fallback rules
|
||||
|
||||
local dev_entry offline_iface offline_device
|
||||
|
||||
# Only add if no online members
|
||||
|
||||
if [ "$total_weight" -eq 0 ]; then
|
||||
for dev_entry in $policy_offline_devices; do
|
||||
offline_iface="${dev_entry%%:*}"
|
||||
offline_device="${dev_entry#*:}"
|
||||
mwan3_nft_exec add rule inet mwan3 "mwan3_policy_$policy" \
|
||||
oifname "$offline_device" meta mark \& "$MMX_MASK" == 0 \
|
||||
"$(mwan3_nft_mark_expr $MMX_DEFAULT $MMX_MASK)"
|
||||
done
|
||||
fi
|
||||
|
||||
# Add last resort rule
|
||||
|
||||
case "$last_resort" in
|
||||
@@ -1214,7 +1349,7 @@ mwan3_set_user_nft_rule()
|
||||
local ipset_name ipset_src family proto policy src_ip src_port src_iface src_dev
|
||||
local sticky dest_ip dest_port use_policy timeout policy
|
||||
local global_logging rule_logging loglevel rule_policy rule ipv
|
||||
local enabled fwmark fwmask _check_set _set_info
|
||||
local enabled fwmark fwmask _check_set _set_info _fam_anchored
|
||||
|
||||
config_get_bool enabled "$1" enabled 1
|
||||
[ "$enabled" -eq 1 ] || return
|
||||
@@ -1262,18 +1397,30 @@ mwan3_set_user_nft_rule()
|
||||
[ "$family" = "ipv4" ] && [ "$ipv" = "ipv6" ] && return
|
||||
[ "$family" = "ipv6" ] && [ "$ipv" = "ipv4" ] && return
|
||||
|
||||
# family=any rules whose nft expression has no IP-version-specific element
|
||||
# (no src_ip/dest_ip/ipset/ipset_src) generate identical output on both the
|
||||
# ipv4 and ipv6 passes. Skip the ipv6 pass to avoid pushing a duplicate
|
||||
# rule. The ipv4 pass output already matches IPv6 traffic at runtime
|
||||
# because the match operates on family-agnostic fields (meta mark, l4proto,
|
||||
# port, iifname). Exception: proto=icmp requires both passes because ICMP
|
||||
# (protocol 1) and ICMPv6 (protocol 58) are distinct L4 protocols.
|
||||
# A family-anchoring element (src_ip/dest_ip/ipset/ipset_src) ties the
|
||||
# rendered expression to one address family per pass. This list must
|
||||
# cover every matcher whose nft form differs by address family; the
|
||||
# de-duplication guard and the sticky block both key on the flag.
|
||||
|
||||
_fam_anchored=0
|
||||
if [ -n "$src_ip" ] || [ -n "$dest_ip" ] || \
|
||||
[ -n "$ipset_name" ] || [ -n "$ipset_src" ]; then
|
||||
_fam_anchored=1
|
||||
fi
|
||||
|
||||
# family=any rules with no family-anchoring element render once, on
|
||||
# the ipv4 pass: the emitted line matches on family-agnostic fields
|
||||
# (meta mark, l4proto, port, iifname, ether saddr) and carries no
|
||||
# nfproto guard, so it covers both families at its config position,
|
||||
# and the sticky block builds the per-rule chain complete for both
|
||||
# families on that pass. Skip the ipv6 pass to avoid a duplicate
|
||||
# line. Exception: proto=icmp requires both passes because ICMP
|
||||
# (protocol 1) and ICMPv6 (protocol 58) are distinct L4 protocols;
|
||||
# the ipv6 pass then emits its own line but must not rebuild the
|
||||
# sticky chain.
|
||||
|
||||
if [ "$family" = "any" ] && [ "$ipv" = "ipv6" ] && \
|
||||
[ -z "$src_ip" ] && [ -z "$dest_ip" ] && \
|
||||
[ -z "$ipset_name" ] && [ -z "$ipset_src" ] && \
|
||||
[ "$proto" != "icmp" ]; then
|
||||
[ $_fam_anchored -eq 0 ] && [ "$proto" != "icmp" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -1300,7 +1447,13 @@ mwan3_set_user_nft_rule()
|
||||
# the src_ip/dest_ip address validation above: a set of type ipv4_addr
|
||||
# cannot appear in an "ip6 daddr @set" expression, and vice versa.
|
||||
|
||||
local _ipset_name_uci_type="" _ipset_src_uci_type=""
|
||||
local _ipset_name_uci_type="" _ipset_src_uci_type="" _pass_addrtype
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
_pass_addrtype="ipv4_addr"
|
||||
else
|
||||
_pass_addrtype="ipv6_addr"
|
||||
fi
|
||||
|
||||
for _check_set in "$ipset_name" "$ipset_src"; do
|
||||
[ -z "$_check_set" ] && continue
|
||||
local _set_info
|
||||
@@ -1339,6 +1492,25 @@ mwan3_set_user_nft_rule()
|
||||
[ "$ipv" = "ipv6" ] && [ "$_uci_addrtype" = "ipv4_addr" ] && return
|
||||
continue
|
||||
fi
|
||||
|
||||
# An earlier rule in this render may already have pre-created
|
||||
# the set into the open batch, which the kernel query above
|
||||
# cannot see. Consult the render's own record instead: the
|
||||
# first pre-creation establishes the set's type for the whole
|
||||
# render, and every later reference is treated exactly as if
|
||||
# the kernel held a set of that type.
|
||||
|
||||
case " $MWAN3_RENDER_SETS " in
|
||||
*" ${_check_set}=${_pass_addrtype} "*)
|
||||
continue
|
||||
;;
|
||||
*" ${_check_set}="*)
|
||||
[ "$family" = "any" ] && return
|
||||
LOG warn "Rule $rule: set '$_check_set' pre-created as the other address family by an earlier rule, incompatible with family $family"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
# Not UCI-managed: apply existing guard for external sets.
|
||||
# For family=any, skip the ipv6 pass; the ipv4 pass will pre-create
|
||||
# the set as ipv4_addr until an external creator establishes its type.
|
||||
@@ -1439,15 +1611,20 @@ mwan3_set_user_nft_rule()
|
||||
# set is missing, which would kill ALL user rules.
|
||||
# UCI-managed sets are already added to the batch by mwan3_render_config_ipsets;
|
||||
# skip the kernel check and pre-creation for those.
|
||||
# A set this render has already pre-created is in the record and is
|
||||
# never added twice: two adds of one name in a batch are rejected
|
||||
# when the types differ, and redundant when they match.
|
||||
|
||||
if [ -z "$_ipset_name_uci_type" ] && \
|
||||
! $NFT list set inet mwan3 "$ipset_name" &>/dev/null; then
|
||||
LOG notice "Creating missing nft set '$ipset_name' for rule $rule"
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_name { type ipv4_addr; flags interval; auto-merge; }"
|
||||
else
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_name { type ipv6_addr; flags interval; auto-merge; }"
|
||||
fi
|
||||
case " $MWAN3_RENDER_SETS " in
|
||||
*" ${ipset_name}=${_pass_addrtype} "*) ;;
|
||||
*)
|
||||
LOG notice "Creating missing nft set '$ipset_name' for rule $rule"
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_name { type ${_pass_addrtype}; flags interval; auto-merge; }"
|
||||
MWAN3_RENDER_SETS="$MWAN3_RENDER_SETS ${ipset_name}=${_pass_addrtype}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
nft_match="$nft_match ip daddr @$ipset_name"
|
||||
@@ -1461,12 +1638,14 @@ mwan3_set_user_nft_rule()
|
||||
if [ -n "$ipset_src" ]; then
|
||||
if [ -z "$_ipset_src_uci_type" ] && \
|
||||
! $NFT list set inet mwan3 "$ipset_src" &>/dev/null; then
|
||||
LOG notice "Creating missing nft set '$ipset_src' for rule $rule"
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_src { type ipv4_addr; flags interval; auto-merge; }"
|
||||
else
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_src { type ipv6_addr; flags interval; auto-merge; }"
|
||||
fi
|
||||
case " $MWAN3_RENDER_SETS " in
|
||||
*" ${ipset_src}=${_pass_addrtype} "*) ;;
|
||||
*)
|
||||
LOG notice "Creating missing nft set '$ipset_src' for rule $rule"
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_src { type ${_pass_addrtype}; flags interval; auto-merge; }"
|
||||
MWAN3_RENDER_SETS="$MWAN3_RENDER_SETS ${ipset_src}=${_pass_addrtype}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
nft_match="$nft_match ip saddr @$ipset_src"
|
||||
@@ -1509,7 +1688,7 @@ mwan3_set_user_nft_rule()
|
||||
# like default_rule (family ipv4, no saddr/daddr) generates a bare
|
||||
# "meta mark ... jump policy" that matches IPv6 traffic too.
|
||||
|
||||
if [ -z "$src_ip" ] && [ -z "$dest_ip" ] && [ -z "$ipset_name" ] && [ -z "$ipset_src" ]; then
|
||||
if [ $_fam_anchored -eq 0 ]; then
|
||||
if [ "$family" = "ipv4" ]; then
|
||||
nft_match="${nft_match:+$nft_match }meta nfproto ipv4"
|
||||
elif [ "$family" = "ipv6" ]; then
|
||||
@@ -1549,49 +1728,86 @@ mwan3_set_user_nft_rule()
|
||||
# bit. The save side mirrors this with per-member "update @set" rules
|
||||
# guarded on (meta mark & MMX) == <member_mark>.
|
||||
|
||||
# The per-rule chain is shared by every mwan3_rules line that
|
||||
# jumps to it, so it is built exactly once, complete for every
|
||||
# family whose traffic can reach it. A family=any rule with no
|
||||
# family-anchoring element serves both families from the single
|
||||
# line emitted on the ipv4 pass, so that pass builds both
|
||||
# families' arms; for proto=icmp the ipv6 pass also reaches this
|
||||
# block and only points its own line at the finished chain. The
|
||||
# layout is load-bearing: every lookup precedes the single
|
||||
# fall-through, so a pinned source ORs its member mark into the
|
||||
# packet before the fall-through can hand it to the policy, and
|
||||
# the update rules follow.
|
||||
|
||||
local _policy_member_marks _entry _m_id _m_mark _setname
|
||||
local _fam_short _saddr_kw _addr_type
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
_fam_short="v4"; _saddr_kw="ip saddr"; _addr_type="ipv4_addr"
|
||||
else
|
||||
_fam_short="v6"; _saddr_kw="ip6 saddr"; _addr_type="ipv6_addr"
|
||||
local _fam_short _saddr_kw _addr_type _sticky_families _sfam
|
||||
|
||||
_sticky_families="$ipv"
|
||||
if [ "$family" = "any" ] && [ $_fam_anchored -eq 0 ]; then
|
||||
if [ "$ipv" = "ipv6" ]; then
|
||||
_sticky_families=""
|
||||
elif [ $NO_IPV6 -eq 0 ]; then
|
||||
_sticky_families="ipv4 ipv6"
|
||||
fi
|
||||
fi
|
||||
|
||||
mwan3_get_policy_members_for_family "$use_policy" "$ipv"
|
||||
_sticky_fam_vars() {
|
||||
if [ "$1" = "ipv4" ]; then
|
||||
_fam_short="v4"; _saddr_kw="ip saddr"; _addr_type="ipv4_addr"
|
||||
else
|
||||
_fam_short="v6"; _saddr_kw="ip6 saddr"; _addr_type="ipv6_addr"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create sticky rule chain if it doesn't exist yet. The chain was
|
||||
# already flushed in the preamble of mwan3_set_user_rules, so both
|
||||
# ipv4 and ipv6 passes can add their rules without interference.
|
||||
if [ -n "$_sticky_families" ]; then
|
||||
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_rule_$1"
|
||||
# Create sticky rule chain if it doesn't exist yet. The chain
|
||||
# was already flushed in the preamble of mwan3_set_user_rules.
|
||||
|
||||
# Per-member sticky sets and lookup rules.
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_rule_$1"
|
||||
|
||||
for _entry in $_policy_member_marks; do
|
||||
_m_id="${_entry%%:*}"
|
||||
_m_mark="${_entry##*:}"
|
||||
_setname="mwan3_sticky_${_fam_short}_${rule}_${_m_id}"
|
||||
# Per-member sticky sets and lookup rules, every served
|
||||
# family's lookups ahead of the fall-through.
|
||||
|
||||
$NFT list set inet mwan3 "$_setname" &>/dev/null || \
|
||||
mwan3_nft_push "add set inet mwan3 $_setname { type ${_addr_type}; flags timeout; timeout ${timeout}s; }"
|
||||
for _sfam in $_sticky_families; do
|
||||
_sticky_fam_vars "$_sfam"
|
||||
mwan3_get_policy_members_for_family "$use_policy" "$_sfam"
|
||||
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 ${_saddr_kw} @${_setname} jump mwan3_or_meta_$(mwan3_or_chain_suffix "$_m_mark")"
|
||||
done
|
||||
for _entry in $_policy_member_marks; do
|
||||
_m_id="${_entry%%:*}"
|
||||
_m_mark="${_entry##*:}"
|
||||
_setname="mwan3_sticky_${_fam_short}_${rule}_${_m_id}"
|
||||
|
||||
# Fall through to policy for new flows (no sticky entry hit -> mark still 0).
|
||||
$NFT list set inet mwan3 "$_setname" &>/dev/null || \
|
||||
mwan3_nft_push "add set inet mwan3 $_setname { type ${_addr_type}; flags timeout; timeout ${timeout}s; }"
|
||||
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 meta mark & $MMX_MASK == 0 jump mwan3_policy_$use_policy"
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 ${_saddr_kw} @${_setname} jump mwan3_or_meta_$(mwan3_or_chain_suffix "$_m_mark")"
|
||||
done
|
||||
done
|
||||
|
||||
# After the policy assigns a mark, populate the matching per-member
|
||||
# sticky set so subsequent packets from this saddr stay on the same WAN.
|
||||
# Fall through to policy for new flows (no sticky entry hit ->
|
||||
# mark still 0).
|
||||
|
||||
for _entry in $_policy_member_marks; do
|
||||
_m_id="${_entry%%:*}"
|
||||
_m_mark="${_entry##*:}"
|
||||
_setname="mwan3_sticky_${_fam_short}_${rule}_${_m_id}"
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 meta mark & $MMX_MASK == 0 jump mwan3_policy_$use_policy"
|
||||
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 meta mark & $MMX_MASK == $_m_mark update @${_setname} { ${_saddr_kw} timeout ${timeout}s }"
|
||||
done
|
||||
# After the policy assigns a mark, populate the matching
|
||||
# per-member sticky set so subsequent packets from this saddr
|
||||
# stay on the same WAN.
|
||||
|
||||
for _sfam in $_sticky_families; do
|
||||
_sticky_fam_vars "$_sfam"
|
||||
mwan3_get_policy_members_for_family "$use_policy" "$_sfam"
|
||||
|
||||
for _entry in $_policy_member_marks; do
|
||||
_m_id="${_entry%%:*}"
|
||||
_m_mark="${_entry##*:}"
|
||||
_setname="mwan3_sticky_${_fam_short}_${rule}_${_m_id}"
|
||||
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 meta mark & $MMX_MASK == $_m_mark update @${_setname} { ${_saddr_kw} timeout ${timeout}s }"
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
policy_action="jump mwan3_rule_$1"
|
||||
fi
|
||||
@@ -1638,14 +1854,21 @@ mwan3_set_user_iface_rules()
|
||||
|
||||
mwan3_set_user_rules()
|
||||
{
|
||||
local ipv
|
||||
# Record of the external sets this render has pre-created into the
|
||||
# batch, as space-delimited "name=addrtype" tokens, read and appended
|
||||
# to by mwan3_set_user_nft_rule through dynamic scoping. Kernel
|
||||
# queries inside an open batch see pre-batch state, so the renderer
|
||||
# carries its own shadow of what the batch already holds. Reset per
|
||||
# render, so repeated renders emit identical batches.
|
||||
|
||||
local MWAN3_RENDER_SETS=""
|
||||
|
||||
mwan3_nft_batch_start
|
||||
|
||||
mwan3_nft_push "flush chain inet mwan3 mwan3_rules"
|
||||
|
||||
# Pre-create and flush per-rule chains for all enabled UCI rules, before
|
||||
# the per-family loop. Sourcing names from UCI (not the kernel) prevents
|
||||
# the render pass. Sourcing names from UCI (not the kernel) prevents
|
||||
# chains for deleted rules from being recreated after reload. "add chain"
|
||||
# is idempotent: it recreates a chain deleted by mwan3_nft_reload_start or
|
||||
# is a no-op if the chain already exists (hotplug path).
|
||||
@@ -1659,10 +1882,22 @@ mwan3_set_user_rules()
|
||||
}
|
||||
config_foreach _init_rule_chain rule
|
||||
|
||||
for ipv in ipv4 ipv6; do
|
||||
[ "$ipv" = "ipv6" ] && [ $NO_IPV6 -ne 0 ] && continue
|
||||
config_foreach mwan3_set_user_nft_rule rule "$ipv"
|
||||
done
|
||||
# Render the rules in one pass over the config, in config order,
|
||||
# emitting each rule's IPv4 and IPv6 variants at that rule's position.
|
||||
# The chain is evaluated first-match, so a rule's position in the chain
|
||||
# must equal its position in the config; rendering per family instead
|
||||
# would place every IPv6-specific rule after every IPv4-pass rule,
|
||||
# letting a family-agnostic rule shadow IPv6 rules the config places
|
||||
# before it.
|
||||
|
||||
_render_rule() {
|
||||
local _rr_ipv
|
||||
for _rr_ipv in ipv4 ipv6; do
|
||||
[ "$_rr_ipv" = "ipv6" ] && [ $NO_IPV6 -ne 0 ] && continue
|
||||
mwan3_set_user_nft_rule "$1" "$_rr_ipv"
|
||||
done
|
||||
}
|
||||
config_foreach _render_rule rule
|
||||
|
||||
mwan3_nft_batch_commit
|
||||
}
|
||||
@@ -2018,12 +2253,13 @@ mwan3_flush_conntrack()
|
||||
config_list_foreach "$interface" flush_conntrack handle_flush "$action"
|
||||
fi
|
||||
|
||||
# On ifdown, selectively flush conntrack entries for this interface's mark.
|
||||
# This forces flows that were using the failed WAN to immediately re-establish
|
||||
# via the new policy rather than waiting for a TCP retransmit timeout.
|
||||
# More targeted than the UCI flush_conntrack mechanism which flushes everything.
|
||||
# On a teardown transition (a hard ifdown or a soft tracker disconnect),
|
||||
# selectively flush conntrack entries carrying this interface's mark. This
|
||||
# forces flows that were using the failed WAN to immediately re-establish via
|
||||
# the new policy rather than waiting for a TCP retransmit timeout. More
|
||||
# targeted than the UCI flush_conntrack mechanism which flushes everything.
|
||||
|
||||
if [ "$action" = "ifdown" ] && [ -e "$CONNTRACK_FILE" ]; then
|
||||
if { [ "$action" = "ifdown" ] || [ "$action" = "disconnected" ]; } && [ -e "$CONNTRACK_FILE" ]; then
|
||||
local iface_id iface_mark
|
||||
mwan3_get_iface_id iface_id "$interface"
|
||||
if [ -n "$iface_id" ]; then
|
||||
|
||||
@@ -87,14 +87,43 @@ let mwan3_cfg = null;
|
||||
|
||||
let pub6_seen = {};
|
||||
let pub4_seen = {};
|
||||
let lit_alias = {};
|
||||
|
||||
// Register one public address. The placeholder map is keyed and numbered on
|
||||
// normalised forms only; when the literal spelling differs from the
|
||||
// normalised form it is recorded as an alias so free-form text carrying
|
||||
// that spelling is still replaced.
|
||||
|
||||
function register(addr_str) {
|
||||
if (!addr_str) return;
|
||||
let lit = split(addr_str, '/')[0];
|
||||
let norm = norm_addr(addr_str);
|
||||
if (!norm) return;
|
||||
let sa = sock.sockaddr(split(addr_str, '/')[0]);
|
||||
let sa = sock.sockaddr(lit);
|
||||
if (sa.family == 10 && is_public_v6(norm)) pub6_seen[norm] = 1;
|
||||
else if (sa.family == 2 && is_public_v4(norm)) pub4_seen[norm] = 1;
|
||||
else return;
|
||||
if (lit != norm) lit_alias[lit] = norm;
|
||||
}
|
||||
|
||||
// Sweep a block of free-form text for address literals. Splitting on any
|
||||
// run of characters that cannot occur inside an address or prefix keeps
|
||||
// every address-bearing token intact; a token that is not a valid public
|
||||
// address fails validation inside register() and drops out. A colon-less
|
||||
// token must be a full dotted quad: the address parser also accepts
|
||||
// shorthand forms like "1.0", which free-form text produces as ordinary
|
||||
// decimal fragments, and registering one would alias that fragment to a
|
||||
// placeholder everywhere in the report.
|
||||
|
||||
function scan_text(text) {
|
||||
if (!text) return;
|
||||
for (let tok in split(text, /[^0-9a-fA-F:.\/]+/)) {
|
||||
if (!length(tok)) continue;
|
||||
if (index(tok, ':') == -1 &&
|
||||
!match(tok, /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(\/[0-9]+)?$/))
|
||||
continue;
|
||||
register(tok);
|
||||
}
|
||||
}
|
||||
|
||||
for (let n in nb_all) { register(n.dst); }
|
||||
@@ -110,57 +139,8 @@ if (mwan3_st?.interfaces)
|
||||
|
||||
// Register any addresses appearing in the mwan3 UCI config (e.g. track_ip
|
||||
// values not yet reflected in routes/status).
|
||||
if (mwan3_cfg)
|
||||
for (let line in split(mwan3_cfg, '\n'))
|
||||
for (let tok in split(replace(line, "'", ' '), ' '))
|
||||
register(trim(tok));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 3: build deterministic placeholder map
|
||||
// Sort addresses so the same address always gets the same placeholder across
|
||||
// multiple runs (e.g. v3.4 run vs v3.5.1 run).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let pub6_list = sort(keys(pub6_seen));
|
||||
let pub4_list = sort(keys(pub4_seen));
|
||||
let addr_map = {};
|
||||
for (let i = 0; i < length(pub6_list); i++)
|
||||
addr_map[pub6_list[i]] = sprintf('PUB6_%d', i + 1);
|
||||
for (let i = 0; i < length(pub4_list); i++)
|
||||
addr_map[pub4_list[i]] = sprintf('PUB4_%d', i + 1);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Anonymisation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Anonymise a single address string (preserves any /prefix-length suffix).
|
||||
function anon(addr_str) {
|
||||
if (!addr_str) return addr_str;
|
||||
let parts = split(addr_str, '/');
|
||||
let norm = norm_addr(parts[0]);
|
||||
let ph = norm ? (addr_map[norm] ?? null) : null;
|
||||
if (ph) return ph + (length(parts) > 1 ? '/' + parts[1] : '');
|
||||
return addr_str;
|
||||
}
|
||||
|
||||
// Anonymise a block of free-form text (nft output, log lines, etc.).
|
||||
// Sorted longest-first so longer addresses are replaced before any shorter
|
||||
// prefix they might share (prevents partial substitutions).
|
||||
let _sorted_addrs = sort(keys(addr_map), (a, b) => length(b) - length(a));
|
||||
function anon_text(text) {
|
||||
if (!text) return '(no output)';
|
||||
for (let addr in _sorted_addrs) {
|
||||
let ph = addr_map[addr];
|
||||
let pos = 0;
|
||||
while (true) {
|
||||
let found = index(text, addr, pos);
|
||||
if (found == -1) break;
|
||||
text = substr(text, 0, found) + ph + substr(text, found + length(addr));
|
||||
pos = found + length(ph);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
scan_text(mwan3_cfg);
|
||||
|
||||
// Strip elements from user-defined nft sets, replacing with { ... }.
|
||||
// mwan3 infrastructure sets (names starting with mwan3_) are left intact:
|
||||
@@ -197,13 +177,102 @@ function strip_set_elements(text) {
|
||||
return join('\n', out);
|
||||
}
|
||||
|
||||
// Run a shell command and return its anonymised output.
|
||||
function pcmd(cmd) {
|
||||
// Run a shell command and return its raw output.
|
||||
function rcmd(cmd) {
|
||||
let f = fs.popen(cmd);
|
||||
if (!f) return '(failed to run: ' + cmd + ')\n';
|
||||
let out = f.read('all');
|
||||
f.close();
|
||||
return anon_text(out || '(no output)\n');
|
||||
return out || '(no output)\n';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detect which table mwan3 is using (v3.4 = inet fw4, v3.5+ = inet mwan3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let mwan3_tbl = 'inet mwan3';
|
||||
{
|
||||
let f = fs.popen('nft list chain inet mwan3 mwan3_prerouting 2>/dev/null | head -1');
|
||||
let l = f?.read('line');
|
||||
f?.close();
|
||||
if (!l || !length(trim(l))) mwan3_tbl = 'inet fw4';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Capture the free-form text sections the report prints and sweep them for
|
||||
// address literals. Some public addresses exist only in this text, e.g. NAT
|
||||
// map elements derived from no route or interface address, or logged
|
||||
// addresses no longer present in live state, and are registered here so
|
||||
// they receive placeholders. Each section is captured exactly once and the
|
||||
// captured text itself is printed later, so no literal can appear in the
|
||||
// printed copy without having been swept. The mwan3 table is stripped of
|
||||
// user-set elements before the sweep so redacted addresses are not
|
||||
// assigned placeholders.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let nft_mwan3_txt = strip_set_elements(rcmd(sprintf('nft list table %s 2>/dev/null', mwan3_tbl)));
|
||||
let fw4_mangle_pre_txt = rcmd('nft list chain inet fw4 mangle_prerouting 2>/dev/null');
|
||||
let fw4_mangle_out_txt = rcmd('nft list chain inet fw4 mangle_output 2>/dev/null');
|
||||
let fw4_output_txt = rcmd('nft list chain inet fw4 output 2>/dev/null');
|
||||
let log_txt = rcmd('logread 2>/dev/null | grep -E "mwan3track\\[|mwan3-hotplug\\[|mwan3-init\\[|mwan3\\[|mwan3rtmon\\[|mwan3-manage-rules\\[|mwan3-create-iface-route\\[" | tail -200');
|
||||
|
||||
scan_text(nft_mwan3_txt);
|
||||
scan_text(fw4_mangle_pre_txt);
|
||||
scan_text(fw4_mangle_out_txt);
|
||||
scan_text(fw4_output_txt);
|
||||
scan_text(log_txt);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 3: build deterministic placeholder map
|
||||
// Sort addresses so the same address always gets the same placeholder across
|
||||
// multiple runs (e.g. v3.4 run vs v3.5.1 run).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let pub6_list = sort(keys(pub6_seen));
|
||||
let pub4_list = sort(keys(pub4_seen));
|
||||
let addr_map = {};
|
||||
for (let i = 0; i < length(pub6_list); i++)
|
||||
addr_map[pub6_list[i]] = sprintf('PUB6_%d', i + 1);
|
||||
for (let i = 0; i < length(pub4_list); i++)
|
||||
addr_map[pub4_list[i]] = sprintf('PUB4_%d', i + 1);
|
||||
|
||||
// A literal spelling that normalises to a registered address receives the
|
||||
// registered form's placeholder, so both spellings are replaced in text.
|
||||
|
||||
for (let lit, norm in lit_alias)
|
||||
if (addr_map[norm]) addr_map[lit] = addr_map[norm];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Anonymisation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Anonymise a single address string (preserves any /prefix-length suffix).
|
||||
function anon(addr_str) {
|
||||
if (!addr_str) return addr_str;
|
||||
let parts = split(addr_str, '/');
|
||||
let norm = norm_addr(parts[0]);
|
||||
let ph = norm ? (addr_map[norm] ?? null) : null;
|
||||
if (ph) return ph + (length(parts) > 1 ? '/' + parts[1] : '');
|
||||
return addr_str;
|
||||
}
|
||||
|
||||
// Anonymise a block of free-form text (nft output, log lines, etc.).
|
||||
// Sorted longest-first so longer addresses are replaced before any shorter
|
||||
// prefix they might share (prevents partial substitutions).
|
||||
let _sorted_addrs = sort(keys(addr_map), (a, b) => length(b) - length(a));
|
||||
function anon_text(text) {
|
||||
if (!text) return '(no output)';
|
||||
for (let addr in _sorted_addrs) {
|
||||
let ph = addr_map[addr];
|
||||
let pos = 0;
|
||||
while (true) {
|
||||
let found = index(text, addr, pos);
|
||||
if (found == -1) break;
|
||||
text = substr(text, 0, found) + ph + substr(text, found + length(addr));
|
||||
pos = found + length(ph);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -224,18 +293,6 @@ function tbl_label(n) {
|
||||
return name ? sprintf('%s (%d)', name, n) : sprintf('%d', n);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detect which table mwan3 is using (v3.4 = inet fw4, v3.5+ = inet mwan3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let mwan3_tbl = 'inet mwan3';
|
||||
{
|
||||
let f = fs.popen('nft list chain inet mwan3 mwan3_prerouting 2>/dev/null | head -1');
|
||||
let l = f?.read('line');
|
||||
f?.close();
|
||||
if (!l || !length(trim(l))) mwan3_tbl = 'inet fw4';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mwan3 version
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -407,20 +464,20 @@ else
|
||||
|
||||
// --- full mwan3 nft table ---
|
||||
printf('=== mwan3 nft table (%s) ===\n', mwan3_tbl);
|
||||
printf('%s\n', strip_set_elements(pcmd(sprintf('nft list table %s 2>/dev/null', mwan3_tbl))));
|
||||
printf('%s\n', anon_text(nft_mwan3_txt));
|
||||
|
||||
// --- fw4 mangle chains (relevant to mwan3 mark interactions) ---
|
||||
printf('=== fw4 mangle_prerouting chain ===\n');
|
||||
printf('%s\n', pcmd('nft list chain inet fw4 mangle_prerouting 2>/dev/null'));
|
||||
printf('%s\n', anon_text(fw4_mangle_pre_txt));
|
||||
|
||||
printf('=== fw4 mangle_output chain ===\n');
|
||||
printf('%s\n', pcmd('nft list chain inet fw4 mangle_output 2>/dev/null'));
|
||||
printf('%s\n', anon_text(fw4_mangle_out_txt));
|
||||
|
||||
printf('=== fw4 output chain ===\n');
|
||||
printf('%s\n', pcmd('nft list chain inet fw4 output 2>/dev/null'));
|
||||
printf('%s\n', anon_text(fw4_output_txt));
|
||||
|
||||
// --- recent mwan3 log ---
|
||||
printf('=== Recent mwan3 Log (last 200 lines) ===\n');
|
||||
printf('%s\n', pcmd('logread 2>/dev/null | grep -E "mwan3track\\[|mwan3-hotplug\\[|mwan3-init\\[|mwan3\\[" | tail -200'));
|
||||
printf('%s\n', anon_text(log_txt));
|
||||
|
||||
printf('=== End of Report ===\n');
|
||||
@@ -5,7 +5,8 @@ import * as rtnl from "rtnl";
|
||||
import * as uloop from "uloop";
|
||||
import * as uci from "uci";
|
||||
import * as ubus from "ubus";
|
||||
import { open, readfile, popen, stat } from "fs";
|
||||
import { open, readfile, popen, stat, mkdir, rename, unlink } from "fs";
|
||||
import { log_open, log_verbose, log_msg as mlog, ucibool, is_default_equivalent } from 'mwan3.common';
|
||||
|
||||
const RTM_NEWROUTE = rtnl.const.RTM_NEWROUTE;
|
||||
const RTM_DELROUTE = rtnl.const.RTM_DELROUTE;
|
||||
@@ -32,6 +33,7 @@ let iface_list = []; // [ { name, family, tid, enabled, has_tracking } ]
|
||||
let active_chains = {}; // { chain_name: true } cached from nft
|
||||
let connected_timer = null; // debounce timer for connected set rebuilds
|
||||
let last_connected_fingerprint = ""; // sorted element list from last populate call
|
||||
let route_state_timer = null; // debounce timer for ROUTE_STATUS refresh
|
||||
let ubus_conn;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -40,7 +42,7 @@ let ubus_conn;
|
||||
|
||||
function log_msg(level, msg) {
|
||||
|
||||
warn(`mwan3rtmon[${family_name}] ${level}: ${msg}\n`);
|
||||
mlog(level, `[${family_name}] ${msg}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -52,6 +54,10 @@ function load_config() {
|
||||
let cur = uci.cursor();
|
||||
cur.load("mwan3");
|
||||
|
||||
// Verbose logging gate
|
||||
|
||||
log_verbose(ucibool(cur.get("mwan3", "globals", "verbose_logging") ?? "0"));
|
||||
|
||||
// MMX mask
|
||||
|
||||
mmx_mask = cur.get("mwan3", "globals", "mmx_mask") ?? "0x3F00";
|
||||
@@ -219,9 +225,17 @@ function nft_exec(cmd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run a list of nft commands as a single nft -f transaction. The batch
|
||||
// file is per-family: one mwan3rtmon instance runs per address family
|
||||
// and each instance is single-threaded, so the family suffix alone
|
||||
// keeps the two instances' batch files apart without locking. The file
|
||||
// is removed on every exit path after creation. nft runs through a
|
||||
// read-mode popen with stderr folded into stdout so the failure log
|
||||
// carries nft's own error text.
|
||||
|
||||
function nft_batch(cmds) {
|
||||
|
||||
let tmpfile = "/var/run/mwan3/mwan3rtmon_nft_batch";
|
||||
let tmpfile = "/var/run/mwan3/mwan3rtmon_nft_batch." + family_name;
|
||||
let f = open(tmpfile, "w", 0600);
|
||||
if (!f) {
|
||||
log_msg("warn", "cannot open nft batch file");
|
||||
@@ -232,10 +246,13 @@ function nft_batch(cmds) {
|
||||
f.close();
|
||||
|
||||
let p = popen("nft -f " + tmpfile + " 2>&1");
|
||||
if (!p)
|
||||
if (!p) {
|
||||
unlink(tmpfile);
|
||||
return false;
|
||||
}
|
||||
let out = p.read("all");
|
||||
let rc = p.close();
|
||||
unlink(tmpfile);
|
||||
if (rc != 0) {
|
||||
log_msg("warn", "nft batch failed: " + (out ?? ""));
|
||||
return false;
|
||||
@@ -323,6 +340,126 @@ function is_linklocal_route(route) {
|
||||
match(route.dst, /^169\.254\./) != null));
|
||||
}
|
||||
|
||||
// Does this interface's own routing table hold a usable default route?
|
||||
//
|
||||
// This is the datapath truth: the interface's fwmark ip rule looks up
|
||||
// exactly this table, so an interface whose table N holds no default route
|
||||
// black-holes the traffic mwan3 marks to it, even when the main table still
|
||||
// has a default via the same device. This mirrors the predicate the rpcd
|
||||
// backend uses for has_default and the one mwan3-manage-rules.uc check-route
|
||||
// uses. 'routes' is a single full route dump for this family (all tables).
|
||||
|
||||
function table_has_default(routes, tid, dev) {
|
||||
|
||||
for (let r in routes)
|
||||
if (r.table == tid && is_default_route(r) && r.oif == dev)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// The interfaces this rtmon instance is responsible for: enabled, tracked,
|
||||
// of this family, and currently bound to an l3 device. iface_list is built
|
||||
// by load_config() and already carries name, family, tid, enabled, and
|
||||
// has_tracking. The device comes from the same ubus network.interface dump
|
||||
// build_dev_table_map() uses. An interface with no l3_device yet is skipped;
|
||||
// it gets evaluated later when its device appears and routes change.
|
||||
|
||||
function tracked_interfaces() {
|
||||
|
||||
let ifaces = [];
|
||||
if (!ubus_conn)
|
||||
return ifaces;
|
||||
let dump = ubus_conn.call("network.interface", "dump");
|
||||
if (!dump || !dump.interface)
|
||||
return ifaces;
|
||||
|
||||
let dev_by_name = {};
|
||||
for (let intf in dump.interface)
|
||||
if (intf.l3_device)
|
||||
dev_by_name[intf.interface] = intf.l3_device;
|
||||
|
||||
for (let iface in iface_list) {
|
||||
if (iface.family != family_name || !iface.enabled || !iface.has_tracking)
|
||||
continue;
|
||||
let dev = dev_by_name[iface.name];
|
||||
if (dev == null)
|
||||
continue;
|
||||
push(ifaces, { name: iface.name, tid: iface.tid, dev: dev });
|
||||
}
|
||||
return ifaces;
|
||||
}
|
||||
|
||||
// Write the interface's ROUTE_STATUS file, only when the value changes, and
|
||||
// log the transition. The write is atomic (temp file + rename within the
|
||||
// same tmpfs directory) so the tracker never reads a torn value. The
|
||||
// tracker creates these directories, but rtmon may evaluate an interface
|
||||
// before its tracker has, so mkdir best-effort first; it fails harmlessly
|
||||
// if the directory already exists.
|
||||
|
||||
function write_route_state(ifname, state) {
|
||||
|
||||
let path = MWAN3TRACK_STATUS_DIR + "/" + ifname + "/ROUTE_STATUS";
|
||||
let cur = trim(readfile(path) ?? "");
|
||||
if (cur == state)
|
||||
return;
|
||||
|
||||
mkdir(MWAN3TRACK_STATUS_DIR, 0700);
|
||||
mkdir(MWAN3TRACK_STATUS_DIR + "/" + ifname, 0755);
|
||||
|
||||
let tmp = path + ".tmp";
|
||||
let f = open(tmp, "w", 0644);
|
||||
if (!f) {
|
||||
log_msg("warn", "cannot open ROUTE_STATUS for " + ifname);
|
||||
return;
|
||||
}
|
||||
f.write(state + "\n");
|
||||
f.close();
|
||||
|
||||
if (!rename(tmp, path)) {
|
||||
unlink(tmp);
|
||||
log_msg("warn", "cannot commit ROUTE_STATUS for " + ifname);
|
||||
return;
|
||||
}
|
||||
|
||||
// A missing default route is abnormal, not routine failover, so log the
|
||||
// loss at warn and the return to normal at notice.
|
||||
|
||||
log_msg(state == "down" ? "warn" : "notice", ifname + " default route " + state);
|
||||
}
|
||||
|
||||
// Recompute and persist every responsible interface's route state from one
|
||||
// route dump. The dump spans all tables for this family; table_has_default
|
||||
// filters to each interface's own table. Called at startup, on SIGHUP, and
|
||||
// debounced from the route-event handler.
|
||||
|
||||
function refresh_route_states() {
|
||||
|
||||
let ifaces = tracked_interfaces();
|
||||
if (length(ifaces) == 0)
|
||||
return;
|
||||
|
||||
let routes = rtnl.request(RTM_GETROUTE, NLM_F_DUMP, { family: family_num }) ?? [];
|
||||
|
||||
for (let iface in ifaces)
|
||||
write_route_state(iface.name,
|
||||
table_has_default(routes, iface.tid, iface.dev) ? "up" : "down");
|
||||
}
|
||||
|
||||
// Arm the debounced refresh. Bursts of route events (PD renewal, reconnects,
|
||||
// the boot-time route flood) collapse into a single sweep, exactly as the
|
||||
// connected-set rebuild is debounced.
|
||||
|
||||
function arm_route_state_refresh() {
|
||||
|
||||
if (!route_state_timer)
|
||||
route_state_timer = uloop.timer(DEBOUNCE_MS, () => {
|
||||
refresh_route_states();
|
||||
route_state_timer = null;
|
||||
});
|
||||
else
|
||||
route_state_timer.set(DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Get all routes from main table (and extra lookup tables) via netlink
|
||||
|
||||
function get_all_routes() {
|
||||
@@ -370,8 +507,18 @@ function populate_connected_set() {
|
||||
|
||||
let seen = {};
|
||||
let elements = [];
|
||||
|
||||
// Main-table routes only. get_all_routes() also returns the extra lookup
|
||||
// tables for the per-interface mirroring, but their routes belong to
|
||||
// mwan3_custom_v4/v6, which grants the identical bypass and is maintained
|
||||
// on those tables' own route events. Sweeping them in here as well would
|
||||
// strand them: extra-table events never arm this rebuild, so a route
|
||||
// deleted from its table would stay in the connected set indefinitely.
|
||||
|
||||
for (let route in routes) {
|
||||
if (is_cidr_route(route) && !is_default_route(route) && !is_linklocal_route(route) && !seen[route.dst]) {
|
||||
if (route.table != RT_TABLE_MAIN)
|
||||
continue;
|
||||
if (is_cidr_route(route) && !is_default_equivalent(route) && !is_linklocal_route(route) && !seen[route.dst]) {
|
||||
push(elements, route.dst);
|
||||
seen[route.dst] = true;
|
||||
}
|
||||
@@ -456,7 +603,7 @@ function repopulate_custom_sets() {
|
||||
if (!extra_table_set[r.table])
|
||||
continue;
|
||||
let dst = r.dst;
|
||||
if (dst == null || is_default_route(r) || is_linklocal_route(r))
|
||||
if (dst == null || is_default_equivalent(r) || is_linklocal_route(r))
|
||||
continue;
|
||||
if (!seen[dst]) {
|
||||
push(cmds, "add element inet mwan3 " + setname + " { " + dst + " }");
|
||||
@@ -478,7 +625,7 @@ function repopulate_custom_sets() {
|
||||
function handle_custom_set_event(route, is_new) {
|
||||
|
||||
let dst = route.dst;
|
||||
if (dst == null || is_default_route(route) || is_linklocal_route(route))
|
||||
if (dst == null || is_default_equivalent(route) || is_linklocal_route(route))
|
||||
return;
|
||||
|
||||
let setname = (family_num == AF_INET) ? "mwan3_custom_v4" : "mwan3_custom_v6";
|
||||
@@ -536,6 +683,10 @@ function handle_route_event(event) {
|
||||
connected_timer.set(DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Re-evaluate every interface's default-route state on any routing change.
|
||||
|
||||
arm_route_state_refresh();
|
||||
|
||||
// -- Update per-interface routing tables --
|
||||
|
||||
build_dev_table_map();
|
||||
@@ -594,6 +745,7 @@ function main() {
|
||||
// Parse args
|
||||
|
||||
family_name = ARGV[0] ?? "ipv4";
|
||||
log_open("mwan3rtmon");
|
||||
|
||||
if (family_name == "ipv6") {
|
||||
if (stat("/proc/sys/net/ipv6") == null) {
|
||||
@@ -610,6 +762,7 @@ function main() {
|
||||
}
|
||||
|
||||
load_config();
|
||||
log_msg("notice", "started");
|
||||
|
||||
// Connect to ubus
|
||||
|
||||
@@ -653,6 +806,9 @@ function main() {
|
||||
log_msg("debug", "populating initial per-interface routes");
|
||||
populate_iface_routes();
|
||||
|
||||
log_msg("debug", "initial route-state sweep");
|
||||
refresh_route_states();
|
||||
|
||||
// Signal handlers for clean shutdown
|
||||
|
||||
uloop.signal("SIGTERM", () => {
|
||||
@@ -669,8 +825,23 @@ function main() {
|
||||
log_msg("debug", "received SIGHUP, reloading config");
|
||||
load_config();
|
||||
repopulate_custom_sets();
|
||||
refresh_route_states();
|
||||
});
|
||||
|
||||
// Slow periodic re-sweep as defense in depth: bounds recovery if a
|
||||
// route-state change ever arrives without an accompanying main-table
|
||||
// route event (a coalesced or missed netlink multicast). It is cheap
|
||||
// and silent when nothing moves, since write_route_state only writes
|
||||
// on an actual change.
|
||||
|
||||
function schedule_periodic_route_sweep() {
|
||||
uloop.timer(20000, () => {
|
||||
refresh_route_states();
|
||||
schedule_periodic_route_sweep();
|
||||
});
|
||||
}
|
||||
schedule_periodic_route_sweep();
|
||||
|
||||
log_msg("debug", "entering event loop");
|
||||
uloop.run();
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ main() {
|
||||
trap config_reload HUP
|
||||
|
||||
load_tracking_config
|
||||
local sleep_time result ping_status loss latency
|
||||
local sleep_time result ping_status loss latency route_state route_state_prev probe_ips
|
||||
mwan3_list_track_ips()
|
||||
{
|
||||
track_ips="$track_ips $1"
|
||||
@@ -352,6 +352,7 @@ main() {
|
||||
local lost=0
|
||||
local turn=0
|
||||
local failed_hosts=""
|
||||
route_state_prev=""
|
||||
|
||||
firstconnect
|
||||
while true; do
|
||||
@@ -391,7 +392,26 @@ main() {
|
||||
continue
|
||||
fi
|
||||
sleep_time=$interval
|
||||
for track_ip in $track_ips; do
|
||||
|
||||
# An interface can be reachable to its tracking target yet hold no
|
||||
# usable default route in its own table, in which case mwan3 marks
|
||||
# traffic to it and the per-interface backstop drops it. mwan3rtmon
|
||||
# writes the interface's default-route state to ROUTE_STATUS; treat
|
||||
# "down" as a failed round and skip probing so the existing score
|
||||
# machinery carries the interface offline, and back online when the
|
||||
# route returns. An absent file means no route monitor has evaluated
|
||||
# this interface, so probe exactly as before.
|
||||
|
||||
route_state=$(cat "$MWAN3TRACK_STATUS_DIR/$INTERFACE/ROUTE_STATUS" 2>/dev/null)
|
||||
probe_ips=$track_ips
|
||||
if [ "$route_state" = "down" ]; then
|
||||
probe_ips=""
|
||||
[ "$route_state_prev" != "down" ] && LOG warn "Interface $INTERFACE ($DEVICE) has no usable default route in its table; treating tracking round as failed"
|
||||
elif [ "$route_state_prev" = "down" ]; then
|
||||
LOG notice "Interface $INTERFACE ($DEVICE) default route restored"
|
||||
fi
|
||||
route_state_prev=$route_state
|
||||
for track_ip in $probe_ips; do
|
||||
if [ $host_up_count -lt $reliability ]; then
|
||||
case "$track_method" in
|
||||
ping)
|
||||
|
||||
@@ -75,6 +75,16 @@ function parse_elem_val(v) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allowlist for nft set elements written back to the kernel: IPv4 and IPv6
|
||||
// addresses, CIDR prefixes and hyphen-written ranges only, which is exactly
|
||||
// what the apply path emits. These values are interpolated into an nft add
|
||||
// element command line built with popen, so anything carrying a character
|
||||
// outside this class is dropped before it can break out of the element list.
|
||||
|
||||
function valid_nft_element(v) {
|
||||
return match(v, /^[0-9A-Fa-f:.\/-]+$/) != null;
|
||||
}
|
||||
|
||||
function count_nftset_elements(setname) {
|
||||
if (!match(setname, /^[a-zA-Z0-9_-]+$/))
|
||||
return 0;
|
||||
@@ -216,6 +226,14 @@ function get_table_routes(table_id) {
|
||||
}
|
||||
|
||||
function nslookup_resolve(host, type) {
|
||||
|
||||
// Reject any host outside the hostname character class before it is
|
||||
// interpolated into the /bin/sh -c command line below. This is the single
|
||||
// chokepoint for both resolve_host and the nftset_resolve domain path,
|
||||
// whose UCI-supplied domain values are otherwise unvalidated.
|
||||
|
||||
if (!match(host, /^[a-zA-Z0-9._-]+$/))
|
||||
return [];
|
||||
const p = popen(sprintf('/bin/busybox nslookup -type=%s %s 127.0.0.1 2>/dev/null', type, host), 'r');
|
||||
if (!p) return [];
|
||||
const raw = p.read('all');
|
||||
@@ -233,7 +251,6 @@ function nslookup_resolve(host, type) {
|
||||
|
||||
function routing_health() {
|
||||
const uci = cursor();
|
||||
|
||||
const mmx_mask = int(uci.get('mwan3', 'globals', 'mmx_mask')) || 0x3F00;
|
||||
let bitcnt = 0;
|
||||
for (let m = mmx_mask; m; m >>= 1) bitcnt += m & 1;
|
||||
@@ -250,34 +267,47 @@ function routing_health() {
|
||||
UNREACH_BASE = 3000;
|
||||
}
|
||||
|
||||
const SRC_BASE = UNREACH_BASE + iface_max + 1;
|
||||
|
||||
// Build ordered interface list with 1-based index (same ordering mwan3 uses)
|
||||
|
||||
const iface_order = [];
|
||||
const iface_index = {};
|
||||
const iface_family = {};
|
||||
uci.foreach('mwan3', 'interface', intf => {
|
||||
push(iface_order, intf['.name']);
|
||||
iface_index[intf['.name']] = length(iface_order);
|
||||
iface_family[intf['.name']] = intf['family'] || 'ipv4';
|
||||
});
|
||||
|
||||
// Fetch all ip rules and index by priority for O(1) lookup
|
||||
|
||||
const all_rules = get_ip_rules();
|
||||
const rule_by_priority = {};
|
||||
const v4_rule_by_priority = {};
|
||||
for (let rule in all_rules) {
|
||||
if (rule?.priority != null)
|
||||
if (rule?.priority != null) {
|
||||
rule_by_priority[rule.priority] = rule;
|
||||
if (rule.family == 2)
|
||||
v4_rule_by_priority[rule.priority] = rule;
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-build the set of priorities that correspond to current UCI interfaces
|
||||
|
||||
const valid_priorities = {};
|
||||
for (let ifname in iface_order) {
|
||||
const id = iface_index[ifname];
|
||||
valid_priorities[IIF_BASE + id] = true;
|
||||
valid_priorities[FWMARK_BASE + id] = true;
|
||||
valid_priorities[UNREACH_BASE + id] = true;
|
||||
valid_priorities[UNREACH_BASE + iface_max + 1 + id] = true;
|
||||
}
|
||||
valid_priorities[FWMARK_BASE + mmdefault - 2] = true;
|
||||
valid_priorities[FWMARK_BASE + mmdefault - 1] = true;
|
||||
|
||||
// Per-interface health report
|
||||
|
||||
const result = {};
|
||||
for (let ifname in iface_order) {
|
||||
const id = iface_index[ifname];
|
||||
@@ -319,17 +349,38 @@ function routing_health() {
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
// The source band is IPv4-only, so test presence against the IPv4
|
||||
// rule index alone. A device holds one source rule per global IPv4
|
||||
// address, all sharing this priority, so a populated slot means the
|
||||
// band exists for this interface, not that every address is covered.
|
||||
|
||||
if (iface_family[ifname] == 'ipv4') {
|
||||
result[ifname].src_rule = {
|
||||
present: v4_rule_by_priority[SRC_BASE + id] != null,
|
||||
priority: SRC_BASE + id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Stale rules: priorities in mwan3 ranges but not matching any current UCI interface
|
||||
|
||||
const stale = [];
|
||||
for (let rule in all_rules) {
|
||||
const p = rule?.priority;
|
||||
if (p == null) continue;
|
||||
|
||||
// The iif and fwmark windows are mmdefault wide (the fwmark band
|
||||
// carries the global blackhole and unreachable rules above
|
||||
// iface_max). The unreachable band holds only ids 1..iface_max;
|
||||
// the slot above it is unused and the source band follows at
|
||||
// iface_max + 1 + id.
|
||||
|
||||
const in_iif_range = (p > IIF_BASE && p <= IIF_BASE + mmdefault);
|
||||
const in_fwmark_range = (p > FWMARK_BASE && p <= FWMARK_BASE + mmdefault);
|
||||
const in_unreach_range = (p > UNREACH_BASE && p <= UNREACH_BASE + mmdefault);
|
||||
if ((in_iif_range || in_fwmark_range || in_unreach_range) && !valid_priorities[p])
|
||||
const in_unreach_range = (p > UNREACH_BASE && p <= UNREACH_BASE + iface_max);
|
||||
const in_src_range = (p > UNREACH_BASE + iface_max + 1 && p <= UNREACH_BASE + 2 * iface_max + 1);
|
||||
if ((in_iif_range || in_fwmark_range || in_unreach_range || in_src_range) && !valid_priorities[p])
|
||||
push(stale, { priority: p });
|
||||
}
|
||||
|
||||
@@ -339,7 +390,7 @@ function routing_health() {
|
||||
interfaces: result,
|
||||
stale_rules: stale,
|
||||
mwan3_active: mwan3_active,
|
||||
rule_bases: { iif: IIF_BASE, fwmark: FWMARK_BASE, unreach: UNREACH_BASE },
|
||||
rule_bases: { iif: IIF_BASE, fwmark: FWMARK_BASE, unreach: UNREACH_BASE, src: SRC_BASE },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -391,12 +442,14 @@ function get_policies(version) {
|
||||
const family_filter = (version == '6') ? 'ipv6' : 'ipv4';
|
||||
|
||||
// Build interface family map: iface_name -> 'ipv4'/'ipv6'
|
||||
|
||||
const iface_family = {};
|
||||
uci.foreach('mwan3', 'interface', intf => {
|
||||
iface_family[intf['.name']] = intf['family'] || 'ipv4';
|
||||
});
|
||||
|
||||
// Build member info map: member_name -> { interface, metric, weight }
|
||||
|
||||
const member_info = {};
|
||||
uci.foreach('mwan3', 'member', m => {
|
||||
member_info[m['.name']] = {
|
||||
@@ -415,6 +468,7 @@ function get_policies(version) {
|
||||
if (type(use_members) != 'array') use_members = [use_members];
|
||||
|
||||
// Collect members matching the requested address family
|
||||
|
||||
const by_metric = {};
|
||||
for (let mname in use_members) {
|
||||
const m = member_info[mname];
|
||||
@@ -431,6 +485,7 @@ function get_policies(version) {
|
||||
if (!length(keys(by_metric))) return;
|
||||
|
||||
// Find the lowest metric with at least one online member
|
||||
|
||||
const sorted_metrics = sort(keys(by_metric), (a, b) => int(a) - int(b));
|
||||
let active_metric = null;
|
||||
for (let metric in sorted_metrics) {
|
||||
@@ -444,6 +499,7 @@ function get_policies(version) {
|
||||
}
|
||||
|
||||
// Sum weights of online members at the active metric for percent calculation
|
||||
|
||||
let total_weight = 0;
|
||||
if (active_metric != null) {
|
||||
for (let m in by_metric[active_metric]) {
|
||||
@@ -498,10 +554,12 @@ function interfaces_status(request) {
|
||||
'packetloss': get_int(ifname, sprintf('LOSS_%s', ip)),
|
||||
});
|
||||
}
|
||||
|
||||
// Derive check_quality from LATENCY file content rather than UCI.
|
||||
// UCI may be stale if check_quality was changed without restarting mwan3track.
|
||||
// mwan3track only writes LATENCY_* files when check_quality=1, so file
|
||||
// presence with content is the authoritative indicator of actual state.
|
||||
|
||||
let check_quality = false;
|
||||
for (let tip in track_ips) {
|
||||
const lat = get_str_raw(ifname, sprintf('LATENCY_%s', tip.ip));
|
||||
@@ -694,7 +752,7 @@ const methods = {
|
||||
if (type(entries) != 'array') entries = [entries];
|
||||
for (let e in entries) {
|
||||
const ev = ltrim(rtrim(e, ' \t\r\n'), ' \t\r\n');
|
||||
if (length(ev) > 0) push(elements, ev);
|
||||
if (length(ev) > 0 && valid_nft_element(ev)) push(elements, ev);
|
||||
}
|
||||
if (cfg.loadfile) {
|
||||
const content = readfile(cfg.loadfile);
|
||||
@@ -703,7 +761,7 @@ const methods = {
|
||||
const ci = index(line, '#');
|
||||
if (ci >= 0) line = substr(line, 0, ci);
|
||||
line = ltrim(rtrim(line, ' \t\r\n'), ' \t\r\n');
|
||||
if (length(line) > 0) push(elements, line);
|
||||
if (length(line) > 0 && valid_nft_element(line)) push(elements, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
'use strict';
|
||||
|
||||
// common.uc: the shared mwan3 ucode library, the ucode analogue of common.sh.
|
||||
// Each shared ucode routine is defined here so it lives, and is tested, in one
|
||||
// place.
|
||||
|
||||
import { openlog, syslog, LOG_PID, LOG_DAEMON } from 'log';
|
||||
|
||||
// ---- Logging ---------------------------------------------------------------
|
||||
//
|
||||
// All mwan3 ucode logs through these functions so the sink, tag format and
|
||||
// verbose gate live in one place. Output goes to syslog, the same destination
|
||||
// logger(1) reaches, so it appears in logread regardless of how procd handles
|
||||
// the process stderr.
|
||||
|
||||
let verbose = false;
|
||||
|
||||
// Map the level names mwan3 uses (the shell's warn/error vocabulary and the
|
||||
// canonical syslog names) onto the exact tokens parse_priority() accepts. It
|
||||
// rejects "warn" and "error", so an unmapped name would be dropped; an unknown
|
||||
// level falls back to notice so a mislabelled message still logs.
|
||||
|
||||
const PRIO = {
|
||||
emerg: "emerg", alert: "alert", crit: "crit",
|
||||
err: "err", error: "err",
|
||||
warn: "warning", warning: "warning",
|
||||
notice: "notice", info: "info", debug: "debug",
|
||||
};
|
||||
|
||||
// Open the syslog connection with the given identity. LOG_PID appends the pid
|
||||
// so the tag reads ident[pid], matching the shell's "${SCRIPTNAME}[$$]"
|
||||
// convention. Facility daemon suits these long-running and service-invoked
|
||||
// components.
|
||||
|
||||
function log_open(ident) {
|
||||
openlog(ident, LOG_PID, LOG_DAEMON);
|
||||
}
|
||||
|
||||
// Set the verbose gate. The caller reads mwan3.globals.verbose_logging and
|
||||
// pushes the parsed bool here; debug is suppressed unless verbose, exactly as
|
||||
// common.sh:LOG() gates the debug facility behind MWAN3_VERBOSE_LOGGING.
|
||||
|
||||
function log_verbose(enable) {
|
||||
verbose = enable;
|
||||
}
|
||||
|
||||
// Emit one message. debug is dropped unless verbose; every other level always
|
||||
// logs. Message content is passed as a %s argument so it is never treated as a
|
||||
// format string.
|
||||
|
||||
function log_msg(level, msg) {
|
||||
if (level == "debug" && !verbose)
|
||||
return;
|
||||
syslog(PRIO[level] ?? "notice", "%s", msg);
|
||||
}
|
||||
|
||||
// ---- Config helpers --------------------------------------------------------
|
||||
|
||||
// Parse a UCI bool the way the shell's config_get_bool does: 1/on/true/yes/
|
||||
// enabled are true, everything else is false. Kept here so every ucode
|
||||
// consumer interprets a UCI bool identically to the shell. It reads no config
|
||||
// itself, so it does not couple the module to uci.
|
||||
|
||||
function ucibool(val) {
|
||||
switch (val) {
|
||||
case 'yes':
|
||||
case 'on':
|
||||
case 'true':
|
||||
case 'enabled':
|
||||
return true;
|
||||
default:
|
||||
return !!int(val);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Route classification --------------------------------------------------
|
||||
|
||||
// The width classifier for the bypass sweeps: a route is default-equivalent
|
||||
// when its destination is absent (a literal default) or its prefix is wide
|
||||
// enough that it can only exist as a component of an in-substance default
|
||||
// route - /2 or wider for IPv4, /3 or wider for IPv6, which covers the
|
||||
// split-half pairs VPN clients install and the 2000::/3 global-unicast
|
||||
// aggregate. Each route is judged alone, so a half-installed pair is still
|
||||
// excluded. The family comes from iptoarr, the inet_pton wrapper, so no
|
||||
// address text is hand-parsed; a destination it cannot parse is not
|
||||
// default-equivalent. The strict literal-default predicate the mirroring and
|
||||
// route-state paths use is deliberately not widened and lives on beside this.
|
||||
|
||||
function is_default_equivalent(route) {
|
||||
let dst = route.dst;
|
||||
if (dst == null)
|
||||
return true;
|
||||
let slash = index(dst, "/");
|
||||
if (slash < 0)
|
||||
return false;
|
||||
let a = iptoarr(substr(dst, 0, slash));
|
||||
if (a == null)
|
||||
return false;
|
||||
return +substr(dst, slash + 1) <= (length(a) == 16 ? 3 : 2);
|
||||
}
|
||||
|
||||
// ---- Exports ---------------------------------------------------------------
|
||||
|
||||
// The module's public interface. Declared as an export list rather than inline
|
||||
// on each function, which this ucode build does not accept.
|
||||
|
||||
export { log_open, log_verbose, log_msg, ucibool, is_default_equivalent };
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user