mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 06:13:56 +08:00
🍓 Sync 2026-09-15 02:13:06
This commit is contained in:
1 parent
0e5393594c
commit
1dd96ac2bb
146 files changed
+26541
-508
No files matched your search
@@ -0,0 +1,33 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-flowproxy
|
||||
PKG_VERSION:=1.0.3
|
||||
PKG_RELEASE:=1
|
||||
|
||||
LUCI_TITLE:=LuCI Support for FlowProxy Traffic Diversion
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
/etc/init.d/flowproxy stop
|
||||
/etc/init.d/flowproxy disable
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postrm
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
rm -rf /tmp/flowproxy
|
||||
rm -rf /usr/share/flowproxy
|
||||
rm -f /var/log/flowproxy.log*
|
||||
rm -f /var/run/flowproxy.*
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,560 @@
|
||||
'use strict';
|
||||
'require form';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require poll';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require network';
|
||||
|
||||
var callGetStatus = rpc.declare({
|
||||
object: 'luci.flowproxy',
|
||||
method: 'get_status'
|
||||
});
|
||||
|
||||
var callGetLogs = rpc.declare({
|
||||
object: 'luci.flowproxy',
|
||||
method: 'get_logs',
|
||||
params: ['lines']
|
||||
});
|
||||
|
||||
var callClearLogs = rpc.declare({
|
||||
object: 'luci.flowproxy',
|
||||
method: 'clear_logs'
|
||||
});
|
||||
|
||||
var callGenerateNftConfig = rpc.declare({
|
||||
object: 'luci.flowproxy',
|
||||
method: 'generate_nft_config'
|
||||
});
|
||||
|
||||
var callGetRuntimeConfig = rpc.declare({
|
||||
object: 'luci.flowproxy',
|
||||
method: 'get_runtime_config'
|
||||
});
|
||||
|
||||
var callRestartService = rpc.declare({
|
||||
object: 'luci.flowproxy',
|
||||
method: 'restart_service'
|
||||
});
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('flowproxy').catch(function() { return {}; }),
|
||||
network.getDevices(),
|
||||
callGetStatus().catch(function() { return {}; }),
|
||||
network.getNetworks().catch(function() { return []; })
|
||||
]);
|
||||
},
|
||||
|
||||
highlightNft: function(text) {
|
||||
if (!text || text.trim() === '') return '<span style="color: #999;">' + _('(no content / table not loaded)') + '</span>';
|
||||
var rules = [
|
||||
{ rex: /#(.*)/g, cls: 'comment' },
|
||||
{ rex: /\b(table|chain|set|elements|type)\b/g, cls: 'keyword' },
|
||||
{ rex: /\b(ip|ip6|tcp|udp|ether|meta|meta nfproto)\b/g, cls: 'proto' },
|
||||
{ rex: /\b(saddr|daddr|sport|dport|mark)\b/g, cls: 'match' },
|
||||
{ rex: /\b(return|accept|drop|reject|counter|set)\b/g, cls: 'action' },
|
||||
{ rex: /@[\w_]+/g, cls: 'variable' },
|
||||
{ rex: /\{|\}/g, cls: 'bracket' }
|
||||
];
|
||||
var html = text.replace(/[&<>"']/g, function(m) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m];
|
||||
});
|
||||
rules.forEach(function(r) {
|
||||
html = html.replace(r.rex, function(match) {
|
||||
return '<span class="nft-' + r.cls + '">' + match + '</span>';
|
||||
});
|
||||
});
|
||||
return html;
|
||||
},
|
||||
refreshStatus: function(map, node) {
|
||||
return callGetStatus().then(L.bind(function(status) {
|
||||
if (!status) return;
|
||||
var isRunning = (status.running == 1);
|
||||
var container = node || document;
|
||||
|
||||
var statusEl = container.querySelector('#service-status');
|
||||
if (statusEl) {
|
||||
var content = [];
|
||||
if (isRunning) {
|
||||
var chains = [];
|
||||
if (status.nft && status.nft.tcp) chains.push('TCP');
|
||||
if (status.nft && status.nft.udp) chains.push('UDP');
|
||||
var ip = status.proxy_server_ip_addr || '-';
|
||||
content.push(E('span', { 'style': 'color: green; font-weight: bold;' }, [ _('running') ]));
|
||||
content.push(' (' + ip + ':');
|
||||
content.push(E('span', { 'style': 'color: #FF9800; font-weight: bold;' }, [ chains.join('+') || '-' ]));
|
||||
content.push(') ');
|
||||
content.push(E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
'style': 'margin-left: 10px;',
|
||||
'click': ui.createHandlerFn(this, 'handleRestart')
|
||||
}, [ _('Restart') ]));
|
||||
} else {
|
||||
content.push(E('span', { 'style': 'color: red; font-weight: bold;' }, [ _('stopped') ]));
|
||||
}
|
||||
L.dom.content(statusEl, content);
|
||||
}
|
||||
|
||||
var dnsEl = container.querySelector('#dns-status');
|
||||
if (dnsEl) {
|
||||
var dcontent = [];
|
||||
if (status.dns_running == 1) {
|
||||
var ip = status.proxy_server_ip_addr || '-';
|
||||
var port = status.proxy_server_dns_port || '5353';
|
||||
dcontent.push(E('span', { 'style': 'color: green; font-weight: bold;' }, [ _('running') ]));
|
||||
dcontent.push(' (' + ip + ':');
|
||||
dcontent.push(E('span', { 'style': 'color: #FF9800; font-weight: bold;' }, [ port ]));
|
||||
dcontent.push(')');
|
||||
} else {
|
||||
dcontent.push(E('span', { 'style': 'color: red; font-weight: bold;' }, [ _('stopped') ]));
|
||||
}
|
||||
L.dom.content(dnsEl, dcontent);
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
|
||||
refreshLogs: function() {
|
||||
return callGetLogs(500).then(L.bind(function(data) {
|
||||
var logEl = document.getElementById('log-content');
|
||||
if (logEl) {
|
||||
var logs = (data && Array.isArray(data.logs)) ? data.logs : [];
|
||||
logEl.value = logs.length > 0 ? logs.join('\n') : _('no logs available');
|
||||
if (logs.length > 0) logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
|
||||
handleRestart: function() {
|
||||
ui.showModal(null, [
|
||||
E('p', { 'class': 'spinning' }, _('Restarting FlowProxy service...'))
|
||||
]);
|
||||
|
||||
return callRestartService().then(L.bind(function(res) {
|
||||
ui.hideModal();
|
||||
if (res && res.result) {
|
||||
ui.addNotification(null, E('p', _('FlowProxy service restarted successfully.')), 'info');
|
||||
window.setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Failed to restart FlowProxy service.')), 'error');
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('RPC error: %s').format((err && (err.message || String(err))) || 'Unknown error')), 'error');
|
||||
});
|
||||
},
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var devices = data[1];
|
||||
var status = data[2];
|
||||
var ifaces = data[3] || [];
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('flowproxy', _('FlowProxy'),
|
||||
_('Traffic diversion based on nftables rules. The service will automatically start/stop when you click "Save & Apply".'));
|
||||
|
||||
if (!document.getElementById('flowproxy-style')) {
|
||||
document.head.appendChild(E('style', { id: 'flowproxy-style' }, `
|
||||
#log-content { width: 100% !important; height: 60vh; min-height: 400px; font-family: monospace; font-size: 12px; background: #f5f5f5 !important; color: #333 !important; border: 1px solid #ccc !important; padding: 10px; resize: vertical; border-radius: 4px; box-sizing: border-box; }
|
||||
.nft-code-view { background: #f5f5f5 !important; color: #333333 !important; padding: 15px !important; font-family: monospace !important; font-size: 12px !important; overflow-x: auto !important; white-space: pre-wrap !important; width: 100% !important; border: 1px solid #cccccc !important; border-radius: 4px; box-sizing: border-box; max-height: 70vh; }
|
||||
.nft-comment { color: #777777; font-style: italic; } .nft-keyword { color: #a626a1; font-weight: bold; } .nft-proto { color: #4078f2; } .nft-match { color: #986801; } .nft-action { color: #e45649; font-weight: bold; } .nft-variable { color: #50a14f; font-weight: bold; }
|
||||
.cbi-section-table-titles th[data-sortable-row]::after, .cbi-section-table-titles th[data-sortable-row]::before { display: none !important; }
|
||||
.cbi-section-table-titles th[data-sortable-row] { pointer-events: none !important; cursor: default !important; }
|
||||
`));
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'global', 'flowproxy');
|
||||
s.tab('settings', _('Settings'));
|
||||
s.tab('rules', _('Rules'));
|
||||
s.tab('lists', _('Lists'));
|
||||
s.tab('preview', _('Preview'));
|
||||
s.tab('logs', _('Logs'));
|
||||
|
||||
// --- Settings ---
|
||||
o = s.taboption('settings', form.DummyValue, '_service_status', _('Current Status'));
|
||||
o.rawhtml = true;
|
||||
o.cfgvalue = function() { return '<div id="service-status" style="display:inline-block;"><em class="spinning">' + _('checking...') + '</em></div>'; };
|
||||
|
||||
o = s.taboption('settings', form.DummyValue, '_dns_status', _('DNS Redirection'));
|
||||
o.rawhtml = true;
|
||||
o.cfgvalue = function() { return '<div id="dns-status" style="display:inline-block;">-</div>'; };
|
||||
|
||||
s.taboption('settings', form.Flag, 'enabled', _('Enable FlowProxy')).rmempty = false;
|
||||
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.validate = function(section_id, value) {
|
||||
var ipInCidr = function(ipStr, cidrStr) {
|
||||
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);
|
||||
|
||||
var ipToNum = function(s) {
|
||||
var p = s.split('.');
|
||||
if (p.length !== 4) return 0;
|
||||
return (((parseInt(p[0], 10) << 24) |
|
||||
(parseInt(p[1], 10) << 16) |
|
||||
(parseInt(p[2], 10) << 8) |
|
||||
parseInt(p[3], 10)) >>> 0);
|
||||
};
|
||||
|
||||
var ipNum = ipToNum(ipStr);
|
||||
var cidrNum = ipToNum(cidrIp);
|
||||
|
||||
if (maskBits === 0) return true;
|
||||
|
||||
var mask = (0xffffffff << (32 - maskBits)) >>> 0;
|
||||
return (ipNum & mask) === (cidrNum & mask);
|
||||
};
|
||||
|
||||
var selected_interface = s.formvalue(section_id, 'interface');
|
||||
if (!value || !selected_interface) return true;
|
||||
|
||||
var target_iface = null;
|
||||
for (var i = 0; i < ifaces.length; i++) {
|
||||
var dev = ifaces[i].getL3Device() || ifaces[i].getDevice();
|
||||
if ((dev && dev.getName() === selected_interface) || ifaces[i].getName() === selected_interface) {
|
||||
target_iface = ifaces[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target_iface) return true;
|
||||
|
||||
var ips = target_iface.getIPAddrs();
|
||||
if (!ips || ips.length === 0) return true;
|
||||
|
||||
var inSubnet = false;
|
||||
for (var j = 0; j < ips.length; j++) {
|
||||
if (ipInCidr(value, ips[j])) {
|
||||
inSubnet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!inSubnet) {
|
||||
return _('The proxy server IP address must be within the subnet of the selected interface (%s).').format(ips.join(', '));
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.taboption('settings', form.Value, 'proxy_server_dns_port', _('Proxy server DNS port'));
|
||||
o.datatype = 'port'; o.default = '5353'; o.rmempty = false;
|
||||
|
||||
o = s.taboption('settings', form.ListValue, 'interface', _('Proxy server interface'));
|
||||
devices.forEach(function(d) { o.value(d.getName(), d.getName()); });
|
||||
o.default = 'br-lan';
|
||||
|
||||
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']; });
|
||||
|
||||
var setupRuleTable = function(type, title, switch_opt) {
|
||||
var st = s.taboption('rules', form.SectionValue, '_tab_' + type, form.TableSection, type, title);
|
||||
var ss = st.subsection;
|
||||
ss.addremove = true; ss.anonymous = true; ss.sortable = true;
|
||||
|
||||
var refreshTable = function() {
|
||||
return ss.render().then(function(newNode) {
|
||||
var el = document.getElementById('flowproxy-table-' + type);
|
||||
if (el && el.parentNode) {
|
||||
el.parentNode.replaceChild(newNode, el);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var createTemplateButtons = function() {
|
||||
var presets = {
|
||||
'priv': { name: 'private (dst)', type: 'dst_ip', val: '@private_dst_ip_v4' },
|
||||
'china': { name: 'china (dst)', type: 'dst_ip', val: '@chnroute_dst_ip_v4' },
|
||||
'src_ip': { name: 'ip (src)', type: 'src_ip', val: '@no_proxy_src_ip_v4' },
|
||||
'dst_ip': { name: 'ip (dst)', type: 'dst_ip', val: '@no_proxy_dst_ip_v4' },
|
||||
'mac': { name: 'mac (src)', type: 'src_mac', val: '@no_proxy_src_mac' },
|
||||
'ports': { name: 'ports (dst)', type: 'dst_port', val: (type === 'tcp_rule') ? '@no_proxy_dst_tcp_ports' : '@no_proxy_dst_udp_ports' }
|
||||
};
|
||||
var btnGroup = E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; align-items: center;' }, [ E('small', { 'style': 'margin-right: 5px; color: #888;' }, _('Quick add:')) ]);
|
||||
Object.keys(presets).forEach(function(k) {
|
||||
var p = presets[k];
|
||||
|
||||
var bgColors = {
|
||||
'dst_ip': '#2196F3',
|
||||
'src_ip': '#2196F3',
|
||||
'custom': '#2196F3',
|
||||
'src_mac': '#9C27B0',
|
||||
'dst_port': '#FF9800'
|
||||
};
|
||||
var btnStyle = 'padding: 0 6px; font-size: 0.75rem; height: 22px; line-height: 22px; opacity: 0.9; margin-bottom: 4px; border: none; color: #fff; border-radius: 3px; cursor: pointer;';
|
||||
var bgColor = bgColors[p.type] || '#607d8b';
|
||||
btnStyle += ' background-color: ' + bgColor + ';';
|
||||
|
||||
btnGroup.appendChild(E('button', {
|
||||
'style': btnStyle,
|
||||
'title': p.val,
|
||||
'click': ui.createHandlerFn(self, function() {
|
||||
var sid = uci.add('flowproxy', type);
|
||||
uci.set('flowproxy', sid, 'enabled', '1');
|
||||
uci.set('flowproxy', sid, 'match_type', p.type);
|
||||
uci.set('flowproxy', sid, 'match_value', p.val);
|
||||
uci.set('flowproxy', sid, 'action', 'return');
|
||||
uci.set('flowproxy', sid, 'counter', '0');
|
||||
return Promise.resolve().then(function() {
|
||||
return m.load().then(function() {
|
||||
return refreshTable();
|
||||
});
|
||||
});
|
||||
})
|
||||
}, [ E('em', { 'class': 'icon-plus' }), ' ', p.name ]));
|
||||
});
|
||||
return btnGroup;
|
||||
};
|
||||
|
||||
ss.render = function() {
|
||||
return form.TableSection.prototype.render.apply(ss).then(function(node) {
|
||||
node.id = 'flowproxy-table-' + type;
|
||||
var titleEl = node.querySelector('h3');
|
||||
if (titleEl) {
|
||||
titleEl.style.display = 'block';
|
||||
var headerRow = E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||
E('span', {}, title),
|
||||
E('div', { 'style': 'font-size: 0.8em; font-weight: normal; display: inline-flex; align-items: center; gap: 5px; color: #666;' }, [
|
||||
(function() {
|
||||
var val = uci.get('flowproxy', 'global', switch_opt);
|
||||
return E('input', { 'type': 'checkbox', 'style': 'width: 16px; height: 18px; cursor: pointer;', 'checked': (val !== '0') ? 'checked' : null,
|
||||
'change': function(ev) { uci.set('flowproxy', 'global', switch_opt, ev.target.checked ? '1' : '0'); ui.addNotification(null, E('p', _('Master switch updated.')), 'info'); }
|
||||
});
|
||||
})(),
|
||||
E('span', {}, _('enable protocol'))
|
||||
])
|
||||
]);
|
||||
L.dom.content(titleEl, [ headerRow, createTemplateButtons() ]);
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
ss.renderSectionAdd = function(extra_class) {
|
||||
var node = form.TableSection.prototype.renderSectionAdd.apply(this, [extra_class]);
|
||||
var label = (type === 'tcp_rule') ? 'TCP' : 'UDP';
|
||||
var addBtn = node.querySelector('.cbi-button-add'); if (addBtn) addBtn.innerText = _('Add rule');
|
||||
var resetBtn = E('button', { 'class': 'cbi-button cbi-button-reset', 'style': 'margin-left: 10px; border: 1px solid #cc0000; color: #cc0000;',
|
||||
'click': ui.createHandlerFn(self, function() {
|
||||
if (confirm(_('this will delete ALL current %s rules and generate default templates. are you sure?').format(label))) {
|
||||
uci.sections('flowproxy', type).forEach(function(r) { uci.remove('flowproxy', r['.name']); });
|
||||
var defs = [ { t: 'src_mac', v: '@no_proxy_src_mac' }, { t: 'dst_ip', v: '@private_dst_ip_v4' }, { t: 'dst_ip', v: '@chnroute_dst_ip_v4' } ];
|
||||
if (type === 'tcp_rule') defs.push({ t: 'dst_port', v: '@no_proxy_dst_tcp_ports' }); else if (type === 'udp_rule') defs.push({ t: 'dst_port', v: '@no_proxy_dst_udp_ports' });
|
||||
defs.forEach(function(r) { var sid = uci.add('flowproxy', type); uci.set('flowproxy', sid, 'enabled', '1'); uci.set('flowproxy', sid, 'match_type', r.t); uci.set('flowproxy', sid, 'match_value', r.v); uci.set('flowproxy', sid, 'action', 'return'); uci.set('flowproxy', sid, 'counter', '0'); });
|
||||
return Promise.resolve().then(function() {
|
||||
return m.load().then(function() {
|
||||
return refreshTable();
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
}, [ E('em', { 'class': 'icon-reload' }), ' ', _('reset %s templates').format(label) ]);
|
||||
node.appendChild(resetBtn); return node;
|
||||
};
|
||||
|
||||
ss.option(form.Flag, 'enabled', _('Enabled')).width = '8%';
|
||||
var match_type = ss.option(form.ListValue, 'match_type', _('Match Type'));
|
||||
match_type.value('dst_ip', 'dest ip'); match_type.value('src_ip', 'src ip'); match_type.value('src_mac', 'src mac');
|
||||
match_type.value('dst_port', 'dest port'); match_type.value('src_port', 'src port'); match_type.value('custom', 'custom (raw)');
|
||||
match_type.default = 'dst_ip'; match_type.rmempty = false; match_type.width = '15%';
|
||||
|
||||
var match_value = ss.option(form.Value, 'match_value', _('Match Value'));
|
||||
match_value.rmempty = false; match_value.width = '45%';
|
||||
nftsets.forEach(function(set) { match_value.value(set); });
|
||||
match_value.validate = function(sid, val) {
|
||||
if (!val || val === '') return _('Expecting: %s').format(_('Match Value'));
|
||||
|
||||
var type = match_type.formvalue(sid);
|
||||
if (!type) {
|
||||
var typeEl = document.querySelector('[name="cbid.flowproxy.' + sid + '.match_type"]');
|
||||
if (typeEl && typeEl.value) type = typeEl.value;
|
||||
}
|
||||
if (!type) type = uci.get('flowproxy', sid, 'match_type');
|
||||
|
||||
if (val.match(/^@/)) {
|
||||
var setName = val.substring(1);
|
||||
// 仅当 section 确实存在时才尝试获取 type,否则不进行类型校验
|
||||
var section = uci.get('flowproxy', setName);
|
||||
var setType = (section && section['.type'] === 'nftset') ? uci.get('flowproxy', setName, 'type') : null;
|
||||
if (!setType) return true;
|
||||
|
||||
var expectedType = '';
|
||||
if (type === 'dst_ip' || type === 'src_ip') expectedType = 'ipv4_addr';
|
||||
else if (type === 'src_mac') expectedType = 'ether_addr';
|
||||
else if (type === 'dst_port' || type === 'src_port') expectedType = 'inet_service';
|
||||
|
||||
if (expectedType && setType !== expectedType && type !== 'custom') {
|
||||
return _('Set type mismatch: expected %s, got %s').format(expectedType, setType);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'dst_ip':
|
||||
case 'src_ip':
|
||||
if (!val.match(/^([0-9]{1,3}\.){3}[0-9]{1,3}(\/[0-9]{1,2})?$/)) return _('Invalid IPv4 address or CIDR');
|
||||
break;
|
||||
case 'src_mac':
|
||||
if (!val.match(/^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/)) return _('Invalid MAC address');
|
||||
break;
|
||||
case 'dst_port':
|
||||
case 'src_port':
|
||||
if (!val.match(/^[0-9]+(-[0-9]+)?$/)) return _('Invalid port or range');
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// 联动:当 match_type 改变时,强制触发 match_value 的校验
|
||||
match_type.onchange = function(ev, sid, val) {
|
||||
var value_input = m.lookupOption('match_value', sid)[0];
|
||||
if (value_input) setTimeout(function() {
|
||||
var el = document.getElementById(value_input.cbid(sid));
|
||||
if (el) {
|
||||
var inputEl = el.tagName === 'INPUT' ? el : el.querySelector('input');
|
||||
if (inputEl) {
|
||||
inputEl.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
||||
inputEl.dispatchEvent(new CustomEvent('blur', { bubbles: true }));
|
||||
inputEl.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
ss.option(form.Flag, 'counter', _('Counter')).width = '8%';
|
||||
o = ss.option(form.ListValue, 'action', _('Action')); o.value('return', 'return'); o.value('accept', 'accept'); o.value('drop', 'drop'); o.default = 'return'; o.width = '12%';
|
||||
};
|
||||
setupRuleTable('tcp_rule', _('TCP Matching Rules'), 'tcp_enabled');
|
||||
setupRuleTable('udp_rule', _('UDP Matching Rules'), 'udp_enabled');
|
||||
|
||||
// --- Lists ---
|
||||
var predefined = [ { id: 'no_proxy_src_mac', name: _('no_proxy_src_mac'), type: 'macaddr' }, { id: 'no_proxy_src_ip_v4', name: _('no_proxy_src_ip_v4'), type: 'or(ip4addr, cidr4)' }, { id: 'no_proxy_dst_ip_v4', name: _('no_proxy_dst_ip_v4'), type: 'or(ip4addr, cidr4)' }, { id: 'no_proxy_dst_tcp_ports', name: _('no_proxy_dst_tcp_ports'), type: 'or(port, portrange)' }, { id: 'no_proxy_dst_udp_ports', name: _('no_proxy_dst_udp_ports'), type: 'or(port, portrange)' } ];
|
||||
predefined.forEach(function(p) {
|
||||
var sl = s.taboption('lists', form.SectionValue, '_list_' + p.id, form.NamedSection, p.id, 'nftset', p.name + ' (@' + p.id + ')');
|
||||
sl.subsection.option(form.Flag, 'enabled', _('Enabled')).default = '1';
|
||||
sl.subsection.option(form.DynamicList, 'elements', _('Elements')).datatype = p.type;
|
||||
});
|
||||
var spriv = s.taboption('lists', form.SectionValue, '_list_priv', form.NamedSection, 'private_dst_ip_v4', 'nftset', _('private_dst_ip_v4') + ' (@private_dst_ip_v4)');
|
||||
spriv.subsection.option(form.Flag, 'enabled', _('Enabled')).default = '1';
|
||||
spriv.subsection.option(form.Flag, 'auto_generate', _('auto_generate')).default = '1';
|
||||
o = spriv.subsection.option(form.DynamicList, 'elements', _('elements')); o.datatype = 'cidr4'; o.depends('auto_generate', '0');
|
||||
|
||||
var sc = s.taboption('lists', form.SectionValue, '_list_chnroute', form.NamedSection, 'chnroute_dst_ip_v4', 'nftset', _('chnroute_dst_ip_v4') + ' (@chnroute_dst_ip_v4)');
|
||||
sc.subsection.option(form.Flag, 'enabled', _('Enabled')).default = '1';
|
||||
o = sc.subsection.option(form.Value, 'file_path', _('File Path')); o.default = '/usr/share/flowproxy/chnroute.txt';
|
||||
o.render = function(sid) {
|
||||
return form.Value.prototype.render.apply(this, arguments).then(function(node) {
|
||||
var path = uci.get('flowproxy', sid, 'file_path') || '/usr/share/flowproxy/chnroute.txt';
|
||||
var resNode = E('span', { 'style': 'margin-left: 10px; font-weight: bold; color: #444;', 'id': 'line-count-status' }, [ '...' ]);
|
||||
var input = node.querySelector('input'); if (input) input.parentNode.appendChild(resNode);
|
||||
fs.exec('/usr/bin/wc', ['-l', path]).then(function(res) { L.dom.content(resNode, (res.code === 0) ? [ _('(%d lines)').format(res.stdout.trim().split(' ')[0]) ] : [ _('(n/a)') ]); });
|
||||
return node;
|
||||
});
|
||||
};
|
||||
sc.subsection.option(form.Value, 'download_url', _('Download URL'));
|
||||
o = sc.subsection.option(form.Button, '_download', _('Update Chnroute')); o.inputstyle = 'apply';
|
||||
o.onclick = function(ev, sid) {
|
||||
var urlOpt = m.lookupOption('download_url', sid)[0];
|
||||
var url = urlOpt ? urlOpt.formvalue(sid) : uci.get('flowproxy', sid, 'download_url');
|
||||
var pathOpt = m.lookupOption('file_path', sid)[0];
|
||||
var path = pathOpt ? pathOpt.formvalue(sid) : uci.get('flowproxy', sid, 'file_path');
|
||||
if (!path) path = '/usr/share/flowproxy/chnroute.txt';
|
||||
if (!url) { ui.addNotification(null, E('p', _('Please set download_url first')), 'error'); return; }
|
||||
ui.showModal(null, [ E('p', { 'class': 'spinning', 'id': 'download-msg' }, [ _('Downloading chnroute data...') ]) ]);
|
||||
return fs.exec('/usr/bin/wget', ['-q', '-O', path, url, '--timeout=10', '--no-check-certificate']).then(function(res) {
|
||||
var msgEl = document.getElementById('download-msg');
|
||||
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) 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')), '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'));
|
||||
sg.subsection.addremove = true; sg.subsection.anonymous = false; sg.subsection.nodescription = true;
|
||||
sg.subsection.filter = function(sid) { var pre = predefined.map(function(p){return p.id}); pre.push('private_dst_ip_v4','chnroute_dst_ip_v4'); return pre.indexOf(sid) === -1; };
|
||||
sg.subsection.option(form.Flag, 'enabled', _('Enabled')).default = '1';
|
||||
o = sg.subsection.option(form.ListValue, 'type', _('Element Type')); o.value('ipv4_addr', 'IPv4 Address/CIDR'); o.value('ether_addr', 'MAC Address'); o.value('inet_service', 'Port/Service'); o.default = 'ipv4_addr';
|
||||
sg.subsection.option(form.DynamicList, 'elements', _('Elements'));
|
||||
|
||||
// --- Preview & Logs ---
|
||||
var sp = s.taboption('preview', form.SectionValue, '_preview_section', form.NamedSection, 'global', 'flowproxy');
|
||||
sp.subsection.tab('generated', _('Generated Config')); sp.subsection.tab('runtime', _('Live Runtime State'));
|
||||
|
||||
o = sp.subsection.taboption('generated', form.DummyValue, '_preview_gen');
|
||||
o.render = function() {
|
||||
var node = E('div', { 'style': 'width:100%; box-sizing:border-box; margin-top: 10px;' }, [
|
||||
E('div', { 'style': 'margin-bottom: 10px' }, [
|
||||
E('button', { 'class': 'cbi-button cbi-button-apply', 'click': function() {
|
||||
var el = document.getElementById('gen-code');
|
||||
if (el) { navigator.clipboard.writeText(el.innerText); ui.addNotification(null, E('p', _('Copied')), 'info'); }
|
||||
} }, [ _('Copy Config') ])
|
||||
]),
|
||||
E('pre', { 'class': 'nft-code-view', 'id': 'gen-code' }, [ _('loading...') ])
|
||||
]);
|
||||
callGenerateNftConfig().then(function(res) {
|
||||
var el = node.querySelector('#gen-code');
|
||||
if (el) el.innerHTML = self.highlightNft(res.config || '');
|
||||
});
|
||||
return node;
|
||||
};
|
||||
|
||||
o = sp.subsection.taboption('runtime', form.DummyValue, '_preview_run');
|
||||
o.render = function() {
|
||||
var node = E('div', { 'style': 'width:100%; box-sizing:border-box; margin-top: 10px;' }, [
|
||||
E('div', { 'style': 'margin-bottom: 10px' }, [
|
||||
E('button', { 'class': 'cbi-button cbi-button-refresh', 'click': function() { location.reload(); } }, [ _('Refresh Status') ])
|
||||
]),
|
||||
E('pre', { 'class': 'nft-code-view', 'id': 'run-code' }, [ _('loading...') ])
|
||||
]);
|
||||
callGetRuntimeConfig().then(function(res) {
|
||||
var el = node.querySelector('#run-code');
|
||||
if (el) el.innerHTML = self.highlightNft(res.runtime || '');
|
||||
});
|
||||
return node;
|
||||
};
|
||||
|
||||
o = s.taboption('logs', form.ListValue, 'log_level', _('Log Level'));
|
||||
o.value('debug'); o.value('info'); o.value('warn'); o.value('error'); o.default = 'info';
|
||||
s.taboption('logs', form.Value, 'log_size', _('Log Size (KB)')).datatype = 'uinteger';
|
||||
o = s.taboption('logs', form.DummyValue, '_log_view');
|
||||
o.render = function() {
|
||||
var node = E('div', { 'style': 'width:100%; box-sizing:border-box; margin-top: 15px;' }, [
|
||||
E('div', { 'style': 'margin-bottom: 5px; text-align: left;' }, [
|
||||
E('button', { 'class': 'cbi-button cbi-button-refresh', 'click': L.bind(self.refreshLogs, self) }, [ _('Refresh Logs') ]),
|
||||
E('button', { 'class': 'cbi-button cbi-button-reset', 'style': 'margin-left: 10px;', 'click': L.bind(function() { if(confirm(_('Clear logs?'))) callClearLogs().then(L.bind(self.refreshLogs, self)); }, self) }, [ _('Clear Logs') ])
|
||||
]),
|
||||
E('textarea', { 'id': 'log-content', 'readonly': true }, [ _('loading logs...') ])
|
||||
]);
|
||||
callGetLogs(500).then(function(data) {
|
||||
var logEl = node.querySelector('#log-content');
|
||||
if (logEl) {
|
||||
var logs = (data && Array.isArray(data.logs)) ? data.logs : [];
|
||||
logEl.value = logs.length > 0 ? logs.join('\n') : _('no logs available');
|
||||
if (logs.length > 0) logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
});
|
||||
return node;
|
||||
};
|
||||
|
||||
return m.render().then(L.bind(function(node) {
|
||||
this.refreshStatus(m, node);
|
||||
poll.add(L.bind(function() { if (!document.body.contains(node)) return false; return this.refreshStatus(m, node); }, this), 5);
|
||||
return node;
|
||||
}, this));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Project-Id-Version: luci-app-flowproxy\n"
|
||||
""
|
||||
|
||||
msgid "(%d lines)"
|
||||
msgstr ""
|
||||
|
||||
msgid "(n/a)"
|
||||
msgstr ""
|
||||
|
||||
msgid "(no content / table not loaded)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add rule"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear Logs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear logs?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copied"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy Config"
|
||||
msgstr ""
|
||||
|
||||
msgid "Counter"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom nftables sets"
|
||||
msgstr ""
|
||||
|
||||
msgid "DNS Redirection"
|
||||
msgstr ""
|
||||
|
||||
msgid "Download URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Download failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Downloading chnroute data..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Element Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Elements"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable FlowProxy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expecting: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "File Path"
|
||||
msgstr ""
|
||||
|
||||
msgid "FlowProxy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force upstream DNS to proxy server"
|
||||
msgstr ""
|
||||
|
||||
msgid "Generated Config"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid IPv4 address or CIDR"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid MAC address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid port or range"
|
||||
msgstr ""
|
||||
|
||||
msgid "Lists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Live Runtime State"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log Level"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log Size (KB)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
msgid "LuCI Support for FlowProxy Traffic Diversion"
|
||||
msgstr ""
|
||||
|
||||
msgid "Master switch updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "Match Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Match Value"
|
||||
msgstr ""
|
||||
|
||||
msgid "Network interface"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please set download_url first"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "Proxy server DNS port"
|
||||
msgstr ""
|
||||
|
||||
msgid "Proxy server IP address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quick add:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh Logs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Routing Table ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Rules"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set type mismatch: expected %s, got %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "TCP Matching Rules"
|
||||
msgstr ""
|
||||
|
||||
msgid "Traffic Mark"
|
||||
msgstr ""
|
||||
|
||||
msgid "Traffic diversion based on nftables rules. The service will automatically start/stop when you click \"Save & Apply\"."
|
||||
msgstr ""
|
||||
|
||||
msgid "UDP Matching Rules"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update Chnroute"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updated successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "auto_generate"
|
||||
msgstr ""
|
||||
|
||||
msgid "checking..."
|
||||
msgstr ""
|
||||
|
||||
msgid "chnroute_dst_ip_v4"
|
||||
msgstr ""
|
||||
|
||||
msgid "elements"
|
||||
msgstr ""
|
||||
|
||||
msgid "enable protocol"
|
||||
msgstr ""
|
||||
|
||||
msgid "loading logs..."
|
||||
msgstr ""
|
||||
|
||||
msgid "loading..."
|
||||
msgstr ""
|
||||
|
||||
msgid "no logs available"
|
||||
msgstr ""
|
||||
|
||||
msgid "no_proxy_dst_ip_v4"
|
||||
msgstr ""
|
||||
|
||||
msgid "no_proxy_dst_tcp_ports"
|
||||
msgstr ""
|
||||
|
||||
msgid "no_proxy_dst_udp_ports"
|
||||
msgstr ""
|
||||
|
||||
msgid "no_proxy_src_ip_v4"
|
||||
msgstr ""
|
||||
|
||||
msgid "no_proxy_src_mac"
|
||||
msgstr ""
|
||||
|
||||
msgid "private_dst_ip_v4"
|
||||
msgstr ""
|
||||
|
||||
msgid "reset %s templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "running"
|
||||
msgstr ""
|
||||
|
||||
msgid "stopped"
|
||||
msgstr ""
|
||||
|
||||
msgid "this will delete ALL current %s rules and generate default templates. are you sure?"
|
||||
msgstr ""
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
zh_Hans
|
||||
@@ -0,0 +1,252 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Project-Id-Version: luci-app-flowproxy\n"
|
||||
"Language: zh_Hans\n"
|
||||
|
||||
msgid "(%d lines)"
|
||||
msgstr "(%d 行)"
|
||||
|
||||
msgid "(n/a)"
|
||||
msgstr "(未知)"
|
||||
|
||||
msgid "(no content / table not loaded)"
|
||||
msgstr "(无内容 / 列表未加载)"
|
||||
|
||||
msgid "Action"
|
||||
msgstr "动作"
|
||||
|
||||
msgid "Add rule"
|
||||
msgstr "添加规则"
|
||||
|
||||
msgid "Clear Logs"
|
||||
msgstr "清空日志"
|
||||
|
||||
msgid "Clear logs?"
|
||||
msgstr "确定要清空日志吗?"
|
||||
|
||||
msgid "Copied"
|
||||
msgstr "已复制"
|
||||
|
||||
msgid "Copy Config"
|
||||
msgstr "复制配置"
|
||||
|
||||
msgid "Counter"
|
||||
msgstr "计数器"
|
||||
|
||||
msgid "Current Status"
|
||||
msgstr "当前状态"
|
||||
|
||||
msgid "Custom nftables sets"
|
||||
msgstr "自定义 nftables 集合"
|
||||
|
||||
msgid "DNS Redirection"
|
||||
msgstr "DNS 转发"
|
||||
|
||||
msgid "Download URL"
|
||||
msgstr "下载地址"
|
||||
|
||||
msgid "Download failed"
|
||||
msgstr "下载失败"
|
||||
|
||||
msgid "Downloading chnroute data..."
|
||||
msgstr "正在下载数据集..."
|
||||
|
||||
msgid "Element Type"
|
||||
msgstr "元素类型"
|
||||
|
||||
msgid "Elements"
|
||||
msgstr "元素列表"
|
||||
|
||||
msgid "Enable FlowProxy"
|
||||
msgstr "启用代理分流"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "已启用"
|
||||
|
||||
msgid "Error: %s"
|
||||
msgstr "错误: %s"
|
||||
|
||||
msgid "Expecting: %s"
|
||||
msgstr "期望值: %s"
|
||||
|
||||
msgid "File Path"
|
||||
msgstr "文件路径"
|
||||
|
||||
msgid "FlowProxy"
|
||||
msgstr "代理分流"
|
||||
|
||||
msgid "Force upstream DNS to proxy server"
|
||||
msgstr "强制上游 DNS 为代理服务器"
|
||||
|
||||
msgid "Generated Config"
|
||||
msgstr "生成的配置"
|
||||
|
||||
msgid "Invalid IPv4 address or CIDR"
|
||||
msgstr "无效的 IPv4 地址或 CIDR"
|
||||
|
||||
msgid "Invalid MAC address"
|
||||
msgstr "无效的 MAC 地址"
|
||||
|
||||
msgid "Invalid port or range"
|
||||
msgstr "无效的端口或端口范围"
|
||||
|
||||
msgid "Lists"
|
||||
msgstr "名单"
|
||||
|
||||
msgid "Live Runtime State"
|
||||
msgstr "实时运行状态"
|
||||
|
||||
msgid "Log Level"
|
||||
msgstr "日志级别"
|
||||
|
||||
msgid "Log Size (KB)"
|
||||
msgstr "日志文件大小 (KB)"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "LuCI Support for FlowProxy Traffic Diversion"
|
||||
msgstr "LuCI 支持 FlowProxy 流量分流"
|
||||
|
||||
msgid "Master switch updated."
|
||||
msgstr "总开关状态已更新。"
|
||||
|
||||
msgid "Match Type"
|
||||
msgstr "匹配类型"
|
||||
|
||||
msgid "Match Value"
|
||||
msgstr "匹配值"
|
||||
|
||||
msgid "Network interface"
|
||||
msgstr "网络接口"
|
||||
|
||||
msgid "Please set download_url first"
|
||||
msgstr "请先设置下载 URL"
|
||||
|
||||
msgid "Preview"
|
||||
msgstr "预览"
|
||||
|
||||
msgid "Proxy server DNS port"
|
||||
msgstr "代理服务器 DNS 端口"
|
||||
|
||||
msgid "Proxy server IP address"
|
||||
msgstr "代理服务器 IP 地址"
|
||||
|
||||
msgid "Proxy server interface"
|
||||
msgstr "代理服务器所在接口"
|
||||
|
||||
msgid "Quick add:"
|
||||
msgstr "快速添加预设:"
|
||||
|
||||
msgid "Refresh Logs"
|
||||
msgstr "刷新日志"
|
||||
|
||||
msgid "Refresh Status"
|
||||
msgstr "刷新实时状态"
|
||||
|
||||
msgid "Routing Table ID"
|
||||
msgstr "路由表 ID"
|
||||
|
||||
msgid "Rule Priority"
|
||||
msgstr "策略路由优先级"
|
||||
|
||||
msgid "Rules"
|
||||
msgstr "规则"
|
||||
|
||||
msgid "Set type mismatch: expected %s, got %s"
|
||||
msgstr "集合类型不匹配: 期望 %s, 实际为 %s"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
msgid "TCP Matching Rules"
|
||||
msgstr "TCP 匹配规则列表"
|
||||
|
||||
msgid "Traffic Mark"
|
||||
msgstr "流量标记"
|
||||
|
||||
msgid "Traffic diversion based on nftables rules. The service will automatically start/stop when you click \"Save & Apply\"."
|
||||
msgstr "基于 nftables 规则的流量分流。当你点击 “保存并应用” 后,服务将自动启动或停止。"
|
||||
|
||||
msgid "UDP Matching Rules"
|
||||
msgstr "UDP 匹配规则列表"
|
||||
|
||||
msgid "Update Chnroute"
|
||||
msgstr "立即更新数据集"
|
||||
|
||||
msgid "Updated successfully."
|
||||
msgstr "数据集更新成功。"
|
||||
|
||||
msgid "auto_generate"
|
||||
msgstr "自动生成"
|
||||
|
||||
msgid "checking..."
|
||||
msgstr "检测中..."
|
||||
|
||||
msgid "chnroute_dst_ip_v4"
|
||||
msgstr "中国大陆 IP"
|
||||
|
||||
msgid "elements"
|
||||
msgstr "元素列表"
|
||||
|
||||
msgid "enable protocol"
|
||||
msgstr "启用协议"
|
||||
|
||||
msgid "loading logs..."
|
||||
msgstr "日志加载中..."
|
||||
|
||||
msgid "loading..."
|
||||
msgstr "加载中..."
|
||||
|
||||
msgid "no logs available"
|
||||
msgstr "暂无日志内容"
|
||||
|
||||
msgid "no_proxy_dst_ip_v4"
|
||||
msgstr "不代理的目标 IP"
|
||||
|
||||
msgid "no_proxy_dst_tcp_ports"
|
||||
msgstr "不代理的目标 TCP 端口"
|
||||
|
||||
msgid "no_proxy_dst_udp_ports"
|
||||
msgstr "不代理的目标 UDP 端口"
|
||||
|
||||
msgid "no_proxy_src_ip_v4"
|
||||
msgstr "不代理的源 IP"
|
||||
|
||||
msgid "no_proxy_src_mac"
|
||||
msgstr "不代理的源 MAC"
|
||||
|
||||
msgid "private_dst_ip_v4"
|
||||
msgstr "私有 IP"
|
||||
|
||||
msgid "reset %s templates"
|
||||
msgstr "重置 %s 模版"
|
||||
|
||||
msgid "running"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "stopped"
|
||||
msgstr "已停止"
|
||||
|
||||
msgid "this will delete ALL current %s rules and generate default templates. are you sure?"
|
||||
msgstr "这将删除当前所有 %s 规则并加载推荐的默认模版。确定吗?"
|
||||
|
||||
msgid "Restart"
|
||||
msgstr "重启"
|
||||
|
||||
msgid "Restarting FlowProxy service..."
|
||||
msgstr "正在重启 FlowProxy 服务..."
|
||||
|
||||
msgid "FlowProxy service restarted successfully."
|
||||
msgstr "FlowProxy 服务重启成功。"
|
||||
|
||||
msgid "Failed to restart FlowProxy service."
|
||||
msgstr "重启 FlowProxy 服务失败。"
|
||||
|
||||
msgid "RPC error: %s"
|
||||
msgstr "RPC 错误: %s"
|
||||
|
||||
msgid "The proxy server IP address must be within the subnet of the selected interface (%s)."
|
||||
msgstr "代理服务器 IP 地址必须在所选网络接口的子网网段之内 (%s)。"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
config flowproxy 'global'
|
||||
option enabled '0'
|
||||
option tcp_enabled '1'
|
||||
option udp_enabled '1'
|
||||
option proxy_server_ip_addr ''
|
||||
option interface 'br-lan'
|
||||
option traffic_mark '0x666'
|
||||
option routing_table '888'
|
||||
option rule_priority '1000'
|
||||
option dns_proxy_enabled '0'
|
||||
option proxy_server_dns_port '5353'
|
||||
option log_level 'info'
|
||||
option log_size '1024'
|
||||
option log_count '3'
|
||||
|
||||
# --- TCP 默认规则 ---
|
||||
config tcp_rule
|
||||
option enabled '1'
|
||||
option match_type 'src_mac'
|
||||
option match_value '@no_proxy_src_mac'
|
||||
option action 'return'
|
||||
option counter '0'
|
||||
|
||||
config tcp_rule
|
||||
option enabled '1'
|
||||
option match_type 'dst_ip'
|
||||
option match_value '@private_dst_ip_v4'
|
||||
option action 'return'
|
||||
option counter '0'
|
||||
|
||||
config tcp_rule
|
||||
option enabled '1'
|
||||
option match_type 'dst_ip'
|
||||
option match_value '@chnroute_dst_ip_v4'
|
||||
option action 'return'
|
||||
option counter '0'
|
||||
|
||||
config tcp_rule
|
||||
option enabled '1'
|
||||
option match_type 'dst_port'
|
||||
option match_value '@no_proxy_dst_tcp_ports'
|
||||
option action 'return'
|
||||
option counter '0'
|
||||
|
||||
# --- UDP 默认规则 ---
|
||||
config udp_rule
|
||||
option enabled '1'
|
||||
option match_type 'src_mac'
|
||||
option match_value '@no_proxy_src_mac'
|
||||
option action 'return'
|
||||
option counter '0'
|
||||
|
||||
config udp_rule
|
||||
option enabled '1'
|
||||
option match_type 'dst_ip'
|
||||
option match_value '@private_dst_ip_v4'
|
||||
option action 'return'
|
||||
option counter '0'
|
||||
|
||||
config udp_rule
|
||||
option enabled '1'
|
||||
option match_type 'dst_ip'
|
||||
option match_value '@chnroute_dst_ip_v4'
|
||||
option action 'return'
|
||||
option counter '0'
|
||||
|
||||
config udp_rule
|
||||
option enabled '1'
|
||||
option match_type 'dst_port'
|
||||
option match_value '@no_proxy_dst_udp_ports'
|
||||
option action 'return'
|
||||
option counter '0'
|
||||
|
||||
# nftables set 配置
|
||||
config nftset 'no_proxy_src_mac'
|
||||
option enabled '1'
|
||||
option type 'ether_addr'
|
||||
|
||||
config nftset 'no_proxy_src_ip_v4'
|
||||
option enabled '1'
|
||||
option type 'ipv4_addr'
|
||||
|
||||
config nftset 'no_proxy_dst_ip_v4'
|
||||
option enabled '1'
|
||||
option type 'ipv4_addr'
|
||||
|
||||
config nftset 'private_dst_ip_v4'
|
||||
option enabled '1'
|
||||
option type 'ipv4_addr'
|
||||
option auto_generate '1'
|
||||
|
||||
config nftset 'chnroute_dst_ip_v4'
|
||||
option enabled '1'
|
||||
option type 'ipv4_addr'
|
||||
option file_path '/usr/share/flowproxy/chnroute.txt'
|
||||
option download_url 'https://proxy.seckv.com/https://raw.githubusercontent.com/mayaxcn/china-ip-list/master/chnroute.txt'
|
||||
|
||||
config nftset 'no_proxy_dst_tcp_ports'
|
||||
option enabled '1'
|
||||
option type 'inet_service'
|
||||
|
||||
config nftset 'no_proxy_dst_udp_ports'
|
||||
option enabled '1'
|
||||
option type 'inet_service'
|
||||
Executable
+299
@@ -0,0 +1,299 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# FlowProxy - Traffic Diversion Service
|
||||
|
||||
START=99
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
NAME="flowproxy"
|
||||
UCI_CONFIG="flowproxy"
|
||||
LOG_FILE="/var/log/flowproxy.log"
|
||||
NFT_CONF="/tmp/flowproxy/nft.conf"
|
||||
GENERATE_SCRIPT="/usr/share/flowproxy/generate_nft.sh"
|
||||
STATE_FILE="/var/run/flowproxy.running"
|
||||
|
||||
get_level_weight() {
|
||||
case "$1" in
|
||||
debug) echo 0 ;;
|
||||
info) echo 1 ;;
|
||||
warn) echo 2 ;;
|
||||
error) echo 3 ;;
|
||||
*) echo 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
log_msg() {
|
||||
local level="$1"; shift
|
||||
local msg="$*"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 等级过滤
|
||||
local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info")
|
||||
local conf_weight=$(get_level_weight "$conf_level")
|
||||
local msg_weight=$(get_level_weight "$level")
|
||||
[ "$msg_weight" -lt "$conf_weight" ] && return
|
||||
|
||||
# 简单的日志截断逻辑,防止日志无限增长
|
||||
# 读取配置的大小限制,默认 1024KB
|
||||
local max_size=$(uci -q get "$UCI_CONFIG.global.log_size" || echo 1024)
|
||||
local current_size=$(du -k "$LOG_FILE" 2>/dev/null | cut -f1)
|
||||
if [ "${current_size:-0}" -gt "$max_size" ]; then
|
||||
mv "$LOG_FILE" "$LOG_FILE.old"
|
||||
touch "$LOG_FILE"
|
||||
fi
|
||||
|
||||
echo "[$timestamp] [$level] $msg" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
check_env() {
|
||||
# 检测核心二进制工具是否存在
|
||||
if ! command -v nft >/dev/null 2>&1; then
|
||||
log_msg error "nftables binary not found"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
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; }
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_nft_rules() {
|
||||
log_msg info "Generating nftables configuration..."
|
||||
local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info")
|
||||
|
||||
mkdir -p "$(dirname "$NFT_CONF")"
|
||||
if [ -x "$GENERATE_SCRIPT" ]; then
|
||||
# 传递日志等级并捕获 stderr 到临时文件
|
||||
local err_file="/tmp/flowproxy/gen_err.tmp"
|
||||
FLOWPROXY_LOG_LEVEL="$conf_level" "$GENERATE_SCRIPT" >/dev/null 2> "$err_file"
|
||||
local ret=$?
|
||||
|
||||
# 将脚本的输出记录到日志
|
||||
if [ -s "$err_file" ]; then
|
||||
while read -r line; do
|
||||
log_msg debug "Script: $line"
|
||||
done < "$err_file"
|
||||
rm -f "$err_file"
|
||||
fi
|
||||
|
||||
if [ $ret -eq 0 ]; then
|
||||
log_msg info "Applying nftables rules from $NFT_CONF"
|
||||
# 确保内核支持 inet 表
|
||||
nft delete table inet flowproxy 2>/dev/null
|
||||
if nft -f "$NFT_CONF"; then
|
||||
return 0
|
||||
else
|
||||
log_msg error "nftables rules application failed (syntax error)"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_msg error "Failed to generate nftables config"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_msg error "Generator script $GENERATE_SCRIPT not found or not executable"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
add_route() {
|
||||
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
|
||||
local interface=$(uci -q get "$UCI_CONFIG.global.interface" || echo "br-lan")
|
||||
local traffic_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666")
|
||||
local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888")
|
||||
local rule_priority=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000")
|
||||
|
||||
# 1. 先尝试用旧记录清理一次,防止由于配置变更导致的残留
|
||||
delete_route "quiet"
|
||||
|
||||
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" > "/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 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: $err"
|
||||
fi
|
||||
}
|
||||
|
||||
delete_route() {
|
||||
local traffic_mark routing_table rule_priority
|
||||
local quiet="$1"
|
||||
|
||||
# 1. 获取两组清理目标:旧值(记录文件) 和 新值(当前UCI)
|
||||
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)
|
||||
[ "$quiet" != "quiet" ] && rm -f "/var/run/flowproxy.mark"
|
||||
fi
|
||||
|
||||
local curr_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666")
|
||||
local curr_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888")
|
||||
local curr_prio=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000")
|
||||
|
||||
[ "$quiet" != "quiet" ] && log_msg info "Cleaning up FlowProxy routes (Atomic Mode)..."
|
||||
|
||||
# 2. 核心清理函数:只清理带 fwmark 且指向特定 table 的规则
|
||||
safe_del_rule() {
|
||||
local m="$1" t="$2" p="$3"
|
||||
[ -z "$m" ] || [ -z "$t" ] && return
|
||||
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
|
||||
log_msg debug "Removed stale ip rule: mark $m lookup $t priority $p"
|
||||
done
|
||||
# 模糊清理 (不带优先级,防止配置中途被手动改动)
|
||||
while ip rule del fwmark "$m" lookup "$t" 2>/dev/null; do
|
||||
log_msg debug "Removed stale ip rule (fuzzy): mark $m lookup $t"
|
||||
done
|
||||
# 清空路由表
|
||||
ip route flush table "$t" 2>/dev/null
|
||||
log_msg debug "Flushed routing table $t"
|
||||
}
|
||||
|
||||
# 执行清理:先清旧值,再清当前值 (防止残留)
|
||||
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"
|
||||
}
|
||||
|
||||
delete_nft() {
|
||||
nft delete table inet flowproxy 2>/dev/null
|
||||
log_msg info "Removed nftables table 'flowproxy'"
|
||||
}
|
||||
|
||||
setup_dns() {
|
||||
local enabled=$(uci -q get "$UCI_CONFIG.global.enabled" || echo 0)
|
||||
local dns_proxy_enabled=$(uci -q get "$UCI_CONFIG.global.dns_proxy_enabled" || echo 0)
|
||||
local proxy_server_dns_port=$(uci -q get "$UCI_CONFIG.global.proxy_server_dns_port" || echo 5353)
|
||||
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
|
||||
|
||||
# Only apply DNS proxy if BOTH main service and DNS proxy are enabled
|
||||
if [ "$enabled" -eq 0 ] || [ "$dns_proxy_enabled" -eq 0 ]; then
|
||||
cleanup_dns
|
||||
return 0
|
||||
fi
|
||||
|
||||
[ -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..."
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
cleanup_dns() {
|
||||
if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
|
||||
log_msg info "Restoring original DNS configuration..."
|
||||
|
||||
local noresolv=$(uci -q get "$UCI_CONFIG.dns_backup.noresolv")
|
||||
local dns_redirect=$(uci -q get "$UCI_CONFIG.dns_backup.dns_redirect")
|
||||
local servers=$(uci -q get "$UCI_CONFIG.dns_backup.servers")
|
||||
|
||||
log_msg debug "Restoring DNS values: noresolv=${noresolv:-0}, redirect=${dns_redirect:-1}, servers=${servers:-empty}"
|
||||
|
||||
# Restore noresolv (default to 0 if not backed up)
|
||||
[ -n "$noresolv" ] && uci set dhcp.@dnsmasq[0].noresolv="$noresolv" || uci set dhcp.@dnsmasq[0].noresolv="0"
|
||||
|
||||
# Restore dns_redirect (default to 1 as requested for OpenWrt typical modded builds)
|
||||
[ -n "$dns_redirect" ] && uci set dhcp.@dnsmasq[0].dns_redirect="$dns_redirect" || uci set dhcp.@dnsmasq[0].dns_redirect="1"
|
||||
|
||||
# Restore servers
|
||||
uci -q delete dhcp.@dnsmasq[0].server
|
||||
if [ -n "$servers" ]; then
|
||||
for s in $servers; do
|
||||
[ -n "$s" ] && uci add_list dhcp.@dnsmasq[0].server="$s"
|
||||
done
|
||||
fi
|
||||
|
||||
uci commit dhcp
|
||||
uci delete "$UCI_CONFIG.dns_backup"
|
||||
uci commit "$UCI_CONFIG"
|
||||
/etc/init.d/dnsmasq restart
|
||||
fi
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled=$(uci -q get "$UCI_CONFIG.global.enabled" || echo 0)
|
||||
|
||||
if [ "$enabled" -eq 0 ]; then
|
||||
log_msg info "FlowProxy is disabled, ensuring service is stopped"
|
||||
stop_service
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_msg info "Starting FlowProxy..."
|
||||
|
||||
check_env || return 1
|
||||
check_config || return 1
|
||||
apply_nft_rules || return 1
|
||||
add_route
|
||||
setup_dns
|
||||
|
||||
touch "$STATE_FILE"
|
||||
log_msg info "FlowProxy service started successfully"
|
||||
}
|
||||
stop_service() {
|
||||
log_msg info "Stopping FlowProxy..."
|
||||
# 强制先移除标记,防止状态误报
|
||||
rm -f "$STATE_FILE"
|
||||
delete_route
|
||||
delete_nft
|
||||
cleanup_dns
|
||||
log_msg info "FlowProxy service stopped"
|
||||
}
|
||||
|
||||
restart_service() {
|
||||
stop_service
|
||||
start_service
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
# 监听自身配置改动
|
||||
procd_add_reload_trigger "$UCI_CONFIG"
|
||||
# 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复
|
||||
procd_add_reload_trigger "firewall"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+215
@@ -0,0 +1,215 @@
|
||||
#!/bin/sh
|
||||
# Generate nftables configuration for flowproxy
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
[ -n "$UCI_CONFIG_DIR" ] && export UCI_CONFIG_DIR
|
||||
|
||||
CONFIG="flowproxy"
|
||||
NFT_TABLE="flowproxy"
|
||||
OUTPUT_FILE="/tmp/flowproxy/nft.conf"
|
||||
|
||||
mkdir -p "$(dirname "$OUTPUT_FILE")"
|
||||
|
||||
ENABLED_SETS=" "
|
||||
|
||||
log_debug() {
|
||||
[ "$FLOWPROXY_LOG_LEVEL" = "debug" ] && echo "DEBUG: $*" >&2
|
||||
}
|
||||
|
||||
gen_set_definition() {
|
||||
local section="$1"
|
||||
local type enabled auto_gen file_path elems is_interval
|
||||
|
||||
enabled=$(uci -q get "$CONFIG.$section.enabled")
|
||||
[ "$enabled" = "0" ] && { log_debug "Set $section is disabled, skipping"; return; }
|
||||
|
||||
log_debug "Generating set: $section"
|
||||
ENABLED_SETS="${ENABLED_SETS}@${section} "
|
||||
type=$(uci -q get "$CONFIG.$section.type")
|
||||
[ -z "$type" ] && type="ipv4_addr"
|
||||
|
||||
if [ "$section" = "private_dst_ip_v4" ]; then
|
||||
auto_gen=$(uci -q get "$CONFIG.$section.auto_generate")
|
||||
if [ "$auto_gen" = "1" ] || [ -z "$auto_gen" ]; then
|
||||
elems="10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16"
|
||||
fi
|
||||
fi
|
||||
|
||||
local uci_elems=""
|
||||
for e in $(uci -q get "$CONFIG.$section.elements"); do
|
||||
[ -n "$uci_elems" ] && uci_elems="${uci_elems}, "
|
||||
uci_elems="${uci_elems}${e}"
|
||||
done
|
||||
[ -n "$uci_elems" ] && { [ -n "$elems" ] && elems="${elems}, "; elems="${elems}${uci_elems}"; }
|
||||
|
||||
file_path=$(uci -q get "$CONFIG.$section.file_path")
|
||||
download_url=$(uci -q get "$CONFIG.$section.download_url")
|
||||
if [ -n "$download_url" ] && [ -n "$file_path" ]; then
|
||||
if [ ! -f "$file_path" ]; then
|
||||
echo "Downloading $section data from $download_url to $file_path..." >&2
|
||||
wget -q -O "$file_path" "$download_url" --timeout=10 --no-check-certificate
|
||||
[ $? -ne 0 ] && { echo "Failed to download $section data" >&2; rm -f "$file_path"; }
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$file_path" ] && [ -f "$file_path" ]; then
|
||||
local file_elems=$(get_file_elements "$file_path")
|
||||
if [ -n "$file_elems" ]; then
|
||||
[ -n "$elems" ] && elems="${elems}, "
|
||||
elems="${elems}${file_elems}"
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$type" in "ipv4_addr"|"inet_service"|"ether_addr") is_interval=1 ;; *) is_interval=0 ;; esac
|
||||
[ "$is_interval" = "0" ] && { case "$elems" in */*|*-*) is_interval=1 ;; esac; }
|
||||
|
||||
printf "\tset %s {\n" "$section"
|
||||
printf "\t\ttype %s\n" "$type"
|
||||
[ "$is_interval" = "1" ] && printf "\t\tflags interval\n"
|
||||
[ -n "$elems" ] && printf "\t\telements = { %s }\n" "$elems"
|
||||
printf "\t}\n\n"
|
||||
}
|
||||
|
||||
get_file_elements() {
|
||||
local file_path="$1"
|
||||
[ ! -f "$file_path" ] && return
|
||||
|
||||
if [ "$FLOWPROXY_PREVIEW" = "1" ]; then
|
||||
local total=$(wc -l < "$file_path")
|
||||
if [ "$total" -gt 100 ]; then
|
||||
awk 'BEGIN { count=0 }
|
||||
!/^($|#)/ {
|
||||
if (count < 100) {
|
||||
if (count > 0) {
|
||||
printf ", "
|
||||
if (count % 2 == 0) printf "\n\t\t\t "
|
||||
}
|
||||
printf "%s", $0
|
||||
count++
|
||||
}
|
||||
}
|
||||
END { printf ", ... (%d more items omitted for preview)", '"$total"'-100 }' "$file_path"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
awk 'BEGIN { count=0 }
|
||||
!/^($|#)/ {
|
||||
if (count > 0) {
|
||||
printf ", "
|
||||
if (count % 2 == 0) printf "\n\t\t\t "
|
||||
}
|
||||
printf "%s", $0
|
||||
count++
|
||||
}
|
||||
END { print "" }' "$file_path"
|
||||
}
|
||||
|
||||
process_rule() {
|
||||
local section="$1"; local proto="$2"
|
||||
local enabled match_type match_value action counter
|
||||
|
||||
enabled=$(uci -q get "$CONFIG.$section.enabled"); [ "$enabled" = "0" ] && { log_debug "Rule $section is disabled, skipping"; return; }
|
||||
match_type=$(uci -q get "$CONFIG.$section.match_type")
|
||||
[ -z "$match_type" ] && match_type="dst_ip"
|
||||
match_value=$(uci -q get "$CONFIG.$section.match_value")
|
||||
|
||||
log_debug "Processing $proto rule: $section ($match_type=$match_value)"
|
||||
action=$(uci -q get "$CONFIG.$section.action"); [ -z "$action" ] && action="return"
|
||||
counter=$(uci -q get "$CONFIG.$section.counter")
|
||||
[ -z "$match_value" ] && return
|
||||
|
||||
case "$match_value" in
|
||||
@*)
|
||||
local set_ref=$(echo "$match_value" | cut -d' ' -f1)
|
||||
if ! echo "$ENABLED_SETS" | grep -q " ${set_ref} "; then
|
||||
log_debug "Skipping rule $section: set ${set_ref} is not defined or disabled"
|
||||
return
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
match_value=$(echo "$match_value" | sed -E 's/ (return|accept|drop)$//g')
|
||||
local segment=""
|
||||
case "$match_type" in
|
||||
src_mac) segment="ether saddr $match_value ip protocol $proto" ;;
|
||||
src_ip) segment="ip saddr $match_value ip protocol $proto" ;;
|
||||
dst_ip) segment="ip daddr $match_value ip protocol $proto" ;;
|
||||
src_port) segment="ip protocol $proto $proto sport $match_value" ;;
|
||||
dst_port) segment="ip protocol $proto $proto dport $match_value" ;;
|
||||
*) segment="$match_value ip protocol $proto" ;;
|
||||
esac
|
||||
|
||||
local line="$segment"; [ "$counter" = "1" ] && line="$line counter"; line="$line $action"
|
||||
[ -n "$line" ] && echo " $line"
|
||||
}
|
||||
|
||||
SECTIONS_SET=$(uci -q show "$CONFIG" | grep "=nftset" | cut -d'.' -f2 | cut -d'=' -f1)
|
||||
SECTIONS_TCP=$(uci -q show "$CONFIG" | grep "=tcp_rule" | cut -d'.' -f2 | cut -d'=' -f1)
|
||||
SECTIONS_UDP=$(uci -q show "$CONFIG" | grep "=udp_rule" | cut -d'.' -f2 | cut -d'=' -f1)
|
||||
|
||||
if [ "$1" = "runtime" ]; then
|
||||
TRAFFIC_MARK=$(uci -q get "$CONFIG.global.traffic_mark" || echo "0x666")
|
||||
ROUTING_TABLE=$(uci -q get "$CONFIG.global.routing_table" || echo "888")
|
||||
echo "--- [ nftables live rules ] ---"
|
||||
nft list table inet "$CONFIG" 2>/dev/null | awk 'BEGIN { count=0; skip=0 }
|
||||
/elements = \{/ { print $0; skip=1; next }
|
||||
skip == 1 && /\}/ { printf "\t\t\t ... (items truncated for preview)\n"; print $0; skip=0; next }
|
||||
skip == 1 { count++; if (count < 20) print $0; next }
|
||||
{ print $0 }' || echo "(table not found)"
|
||||
echo ""
|
||||
echo "--- [ ip rule list ] ---"
|
||||
ip rule show 2>/dev/null
|
||||
echo ""
|
||||
echo "--- [ ip route table $ROUTING_TABLE ] ---"
|
||||
ip route show table "$ROUTING_TABLE" 2>/dev/null || echo "(table empty)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat > "$OUTPUT_FILE" << EOF
|
||||
#!/usr/sbin/nft -f
|
||||
table inet $NFT_TABLE
|
||||
flush table inet $NFT_TABLE
|
||||
|
||||
table inet $NFT_TABLE {
|
||||
EOF
|
||||
|
||||
for s in $SECTIONS_SET; do
|
||||
gen_set_definition "$s" >> "$OUTPUT_FILE"
|
||||
done
|
||||
|
||||
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")
|
||||
|
||||
if [ "$TCP_ENABLED" = "1" ]; then
|
||||
cat >> "$OUTPUT_FILE" << EOF
|
||||
chain LAN_MARKFLOW_TCP {
|
||||
type filter hook prerouting priority mangle; policy accept;
|
||||
fib daddr type { unspec, local, anycast, multicast } counter return
|
||||
EOF
|
||||
if [ -n "$PROXY_SERVER_IP_ADDR" ]; then
|
||||
echo " ip saddr $PROXY_SERVER_IP_ADDR ip protocol tcp counter return" >> "$OUTPUT_FILE"
|
||||
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"
|
||||
echo " }" >> "$OUTPUT_FILE"
|
||||
fi
|
||||
|
||||
if [ "$UDP_ENABLED" = "1" ]; then
|
||||
cat >> "$OUTPUT_FILE" << EOF
|
||||
chain LAN_MARKFLOW_UDP {
|
||||
type filter hook prerouting priority mangle; policy accept;
|
||||
fib daddr type { unspec, local, anycast, multicast } counter return
|
||||
EOF
|
||||
if [ -n "$PROXY_SERVER_IP_ADDR" ]; then
|
||||
echo " ip saddr $PROXY_SERVER_IP_ADDR ip protocol udp counter return" >> "$OUTPUT_FILE"
|
||||
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"
|
||||
echo " }" >> "$OUTPUT_FILE"
|
||||
fi
|
||||
echo "}" >> "$OUTPUT_FILE"
|
||||
cat "$OUTPUT_FILE"
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"admin/services/flowproxy": {
|
||||
"title": "FlowProxy",
|
||||
"description": "LuCI Support for FlowProxy Traffic Diversion",
|
||||
"index": 50,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "flowproxy/flowproxy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"luci-app-flowproxy": {
|
||||
"description": "Grant access to flowproxy configuration",
|
||||
"read": {
|
||||
"uci": [ "flowproxy" ],
|
||||
"file": {
|
||||
"/usr/share/flowproxy/*": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"luci.flowproxy": [ "*" ],
|
||||
"network.interface": [ "dump" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "flowproxy" ],
|
||||
"file": {
|
||||
"/usr/bin/wget": [ "exec" ],
|
||||
"/usr/bin/wc": [ "exec" ],
|
||||
"/usr/share/flowproxy/generate_nft.sh": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"luci.flowproxy": [ "*" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
'use strict';
|
||||
|
||||
import { access, readfile, writefile, stat } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { connect } from 'ubus';
|
||||
|
||||
const UCI_CONFIG = 'flowproxy';
|
||||
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';
|
||||
|
||||
// 辅助函数:安全 trim
|
||||
function safe_trim(str) {
|
||||
return (type(str) === 'string') ? trim(str) : '';
|
||||
}
|
||||
|
||||
const methods = {
|
||||
get_status: {
|
||||
call: function() {
|
||||
// 1. 快速探测
|
||||
let is_running = access(STATE_FILE, 'f');
|
||||
|
||||
// 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);
|
||||
|
||||
if (real_running && !is_running) {
|
||||
try { writefile(STATE_FILE, ''); } catch(e) {}
|
||||
} else if (!real_running && is_running) {
|
||||
try { system('rm -f ' + STATE_FILE); } catch(e) {}
|
||||
}
|
||||
|
||||
// 3. IP 获取 (带缓存)
|
||||
let lan_ip = readfile(IP_CACHE) || '';
|
||||
if (!lan_ip) {
|
||||
try {
|
||||
let ubus_conn = connect();
|
||||
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) {
|
||||
lan_ip = iface['ipv4-address'][0].address;
|
||||
break;
|
||||
}
|
||||
if (!fallback_ip && iface.interface !== 'loopback' && iface['ipv4-address']?.[0]?.address) {
|
||||
fallback_ip = iface['ipv4-address'][0].address;
|
||||
}
|
||||
}
|
||||
if (!lan_ip) lan_ip = fallback_ip;
|
||||
}
|
||||
|
||||
if (lan_ip) {
|
||||
system('mkdir -p /tmp/flowproxy');
|
||||
writefile(IP_CACHE, lan_ip);
|
||||
}
|
||||
ubus_conn.disconnect();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
let uci_cursor = cursor();
|
||||
|
||||
let proxy_server_ip_addr = uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_ip_addr') || '';
|
||||
let dns_running = (uci_cursor.get(UCI_CONFIG, 'dns_backup') ? 1 : 0);
|
||||
|
||||
return {
|
||||
running: real_running ? 1 : 0,
|
||||
dns_running: dns_running,
|
||||
enabled: int(uci_cursor.get(UCI_CONFIG, 'global', 'enabled') || '0'),
|
||||
proxy_server_ip_addr: proxy_server_ip_addr,
|
||||
proxy_server_dns_port: uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_dns_port') || '5353',
|
||||
lan_ip: safe_trim(lan_ip),
|
||||
interface: uci_cursor.get(UCI_CONFIG, 'global', 'interface') || 'br-lan',
|
||||
traffic_mark: uci_cursor.get(UCI_CONFIG, 'global', 'traffic_mark') || '0x666',
|
||||
nft: { tcp: tcp_active ? 1 : 0, udp: udp_active ? 1 : 0 }
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
get_logs: {
|
||||
args: { lines: 100 },
|
||||
call: function(request) {
|
||||
let lines = request.args?.lines ?? 100;
|
||||
let logs = [];
|
||||
try {
|
||||
if (access(LOG_FILE, 'r')) {
|
||||
let log_stat = stat(LOG_FILE);
|
||||
let content = '';
|
||||
if (log_stat && log_stat.size > 65536) {
|
||||
let fd = popen('tail -c 65536 ' + LOG_FILE);
|
||||
if (fd) {
|
||||
content = fd.read('all');
|
||||
fd.close();
|
||||
}
|
||||
} else {
|
||||
content = readfile(LOG_FILE);
|
||||
}
|
||||
if (content) {
|
||||
let log_lines = split(content, "\n");
|
||||
let total = length(log_lines);
|
||||
let start = (total > lines) ? (total - lines) : 0;
|
||||
for (let i = start; i < total; i++) {
|
||||
if (log_lines[i]) push(logs, log_lines[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return { logs };
|
||||
}
|
||||
},
|
||||
|
||||
generate_nft_config: {
|
||||
call: function() {
|
||||
system('mkdir -p /tmp/flowproxy');
|
||||
system('FLOWPROXY_PREVIEW=1 /usr/share/flowproxy/generate_nft.sh 2>&1');
|
||||
return { config: safe_trim(readfile('/tmp/flowproxy/nft.conf') || '') };
|
||||
}
|
||||
},
|
||||
|
||||
get_runtime_config: {
|
||||
call: function() {
|
||||
let res_file = '/tmp/flowproxy/runtime.tmp';
|
||||
let dns_file = '/tmp/flowproxy/dns_runtime.tmp';
|
||||
|
||||
system('mkdir -p /tmp/flowproxy');
|
||||
system('/usr/share/flowproxy/generate_nft.sh runtime > ' + res_file + ' 2>&1');
|
||||
let nft_runtime = readfile(res_file) || "(failed)";
|
||||
system('rm -f ' + res_file);
|
||||
|
||||
system('uci show dhcp.@dnsmasq[0] | grep -E "noresolv|dns_redirect|server" | sed -E "s/^dhcp\\.[^.]*\\.([^=]*)=(.*)/\\1: \\2/; s/\\x27//g; s/#/:/g" > ' + dns_file + ' 2>&1');
|
||||
let dns_out = readfile(dns_file);
|
||||
system('rm -f ' + dns_file);
|
||||
|
||||
let dns_runtime = "\n--- [ DNSmasq Runtime Configuration ] ---\n";
|
||||
dns_runtime += (dns_out || "(no active dnsmasq overrides)");
|
||||
|
||||
return { runtime: safe_trim(nft_runtime + dns_runtime) };
|
||||
}
|
||||
},
|
||||
|
||||
get_interfaces: {
|
||||
call: function() {
|
||||
let interfaces = [];
|
||||
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) {
|
||||
push(interfaces, { name: name });
|
||||
}
|
||||
}
|
||||
ubus_conn.disconnect();
|
||||
}
|
||||
} catch (e) {}
|
||||
return { interfaces };
|
||||
}
|
||||
},
|
||||
|
||||
start_service: { call: function() { return { result: (system('/etc/init.d/flowproxy start >/dev/null 2>&1') === 0) }; } },
|
||||
stop_service: { call: function() { return { result: (system('/etc/init.d/flowproxy stop >/dev/null 2>&1') === 0) }; } },
|
||||
restart_service: { call: function() { return { result: (system('/etc/init.d/flowproxy restart >/dev/null 2>&1') === 0) }; } },
|
||||
clear_logs: { call: function() { try { writefile(LOG_FILE, ''); return { result: true }; } catch(e) { return { result: false }; } } }
|
||||
};
|
||||
|
||||
return { 'luci.flowproxy': methods };
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
Reference in new issue
Block a user