From b3820ff67acbda28ea1ba41fd134af12f3dc60a7 Mon Sep 17 00:00:00 2001 From: Smth Dagg Date: Sat, 8 Aug 2026 23:31:37 +0800 Subject: [PATCH 1/8] luci-app-wificalling-gateway: add package Per-device transparent Wi-Fi Calling gateway for OpenWrt / ImmortalWrt. Forwards selected LAN clients through a selected sing-box node (AnyTLS, Hysteria2, TUIC, VLESS Reality, VMess WebSocket) with one sing-box process and nftables TPROXY, while other clients keep the normal gateway or PassWall policy. Observes ePDG/IPsec UDP 500/4500 evidence and records handshake success/failure and sustained encrypted communication in an encrypted IMS activity log (toggleable from the Settings page). Dependencies: luci-base, sing-box, firewall4, kmod-nft-tproxy, kmod-nft-socket, ip-full, tcping. Tested on ImmortalWrt 24.10.6 (Redmi AX6S, aarch64_cortex-a53); i18n path, luci.mk LUCI_LC_ALIAS and dependencies are identical on ImmortalWrt 25.12. Credential-bearing files are written with mode 0600; raw import links are never logged. Signed-off-by: Smth Dagg --- .../luci-app-wificalling-gateway/LICENSE | 21 ++ .../luci-app-wificalling-gateway/Makefile | 22 ++ .../luci-app-wificalling-gateway/README.md | 23 ++ .../view/wificalling-gateway/events.js | 61 +++ .../view/wificalling-gateway/overview.js | 146 ++++++++ .../view/wificalling-gateway/status.js | 40 ++ .../wificalling-gateway/node-import.js | 82 +++++ .../po/templates/wificalling-gateway.pot | 345 +++++++++++++++++ .../po/zh_Hans/wificalling-gateway.po | 346 ++++++++++++++++++ .../root/etc/config/wificalling-gateway | 10 + .../root/etc/init.d/wificalling-gateway | 71 ++++ .../libexec/wificalling-gateway/compiler.sh | 82 +++++ .../libexec/wificalling-gateway/firewall.sh | 25 ++ .../wificalling-gateway/monitor-loop.sh | 11 + .../libexec/wificalling-gateway/monitor.sh | 112 ++++++ .../wificalling-gateway/node-health.sh | 43 +++ .../wificalling-gateway/passwall-bypass.sh | 34 ++ .../menu.d/luci-app-wificalling-gateway.json | 23 ++ .../acl.d/luci-app-wificalling-gateway.json | 25 ++ 19 files changed, 1522 insertions(+) create mode 100644 applications/luci-app-wificalling-gateway/LICENSE create mode 100644 applications/luci-app-wificalling-gateway/Makefile create mode 100644 applications/luci-app-wificalling-gateway/README.md create mode 100644 applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/events.js create mode 100644 applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js create mode 100644 applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/status.js create mode 100644 applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js create mode 100644 applications/luci-app-wificalling-gateway/po/templates/wificalling-gateway.pot create mode 100644 applications/luci-app-wificalling-gateway/po/zh_Hans/wificalling-gateway.po create mode 100644 applications/luci-app-wificalling-gateway/root/etc/config/wificalling-gateway create mode 100755 applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway create mode 100755 applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh create mode 100755 applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/firewall.sh create mode 100755 applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/monitor-loop.sh create mode 100755 applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/monitor.sh create mode 100755 applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/node-health.sh create mode 100755 applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/passwall-bypass.sh create mode 100644 applications/luci-app-wificalling-gateway/root/usr/share/luci/menu.d/luci-app-wificalling-gateway.json create mode 100644 applications/luci-app-wificalling-gateway/root/usr/share/rpcd/acl.d/luci-app-wificalling-gateway.json diff --git a/applications/luci-app-wificalling-gateway/LICENSE b/applications/luci-app-wificalling-gateway/LICENSE new file mode 100644 index 0000000000..9642b55503 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Wi-Fi Calling Gateway contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/applications/luci-app-wificalling-gateway/Makefile b/applications/luci-app-wificalling-gateway/Makefile new file mode 100644 index 0000000000..f882fb741e --- /dev/null +++ b/applications/luci-app-wificalling-gateway/Makefile @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (C) 2026 Smth Dagg + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for per-device Wi-Fi Calling gateway +LUCI_URL:=https://github.com/smthdagg/luci-app-wificalling-gateway +LUCI_DEPENDS:=+luci-base +sing-box +firewall4 +kmod-nft-tproxy +kmod-nft-socket +ip-full +tcping +LUCI_PKGARCH:=all + +PKG_LICENSE:=MIT +PKG_LICENSE_FILES:=LICENSE +PKG_MAINTAINER:=Smth Dagg + +define Package/luci-app-wificalling-gateway/conffiles +/etc/config/wificalling-gateway +endef + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/applications/luci-app-wificalling-gateway/README.md b/applications/luci-app-wificalling-gateway/README.md new file mode 100644 index 0000000000..7af65df471 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/README.md @@ -0,0 +1,23 @@ +# luci-app-wificalling-gateway + +Per-device transparent Wi-Fi Calling gateway for OpenWrt / ImmortalWrt. + +It forwards selected LAN clients through a selected sing-box node (AnyTLS, Hysteria2, +TUIC, VLESS Reality, VMess WebSocket) via a single sing-box process with nftables TPROXY, +while other clients keep the normal gateway or PassWall policy. It observes ePDG/IPsec +UDP 500/4500 evidence and records handshake outcomes and sustained encrypted +communication in an encrypted IMS activity log (toggleable). + +Maintainer docs, install guide and build instructions: +https://github.com/smthdagg/luci-app-wificalling-gateway + +## Install + +```sh +opkg update +opkg install luci-app-wificalling-gateway +/etc/init.d/rpcd restart +``` + +Then open **Services -> Wi-Fi Calling Gateway**. Save a node first, then create a +device policy. Devices must have static DHCP leases. diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/events.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/events.js new file mode 100644 index 0000000000..76ff2257b1 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/events.js @@ -0,0 +1,61 @@ +'use strict'; +'require view'; +'require fs'; +'require poll'; +'require dom'; +'require ui'; +'require uci'; + +return view.extend({ + load: function() { + return Promise.all([ + L.resolveDefault(fs.read('/var/run/wificalling-gateway/events.log'), ''), + uci.load('wificalling-gateway') + ]); + }, + render: function(data) { + var raw = data[0]; + var logEnabled = uci.get('wificalling-gateway', 'main', 'log_enabled'); + function when(epoch) { return epoch ? new Date(epoch * 1000).toLocaleString() : '-'; } + function lines(value) { return value.trim() ? value.trim().split('\n').reverse() : []; } + function wfcLabel(v) { + switch (v) { + case 'registered': return _('Registered'); + case 'connecting': return _('Connecting'); + case 'not_detected': return _('Not detected'); + default: return v || '-'; + } + } + function activityLabel(v) { + switch (v) { + case 'handshake_success': return _('Handshake success'); + case 'handshake_failed': return _('Handshake failed'); + case 'sustained_traffic': return _('Sustained traffic'); + default: return v || '-'; + } + } + function rows(value) { + return lines(value).map(function(line) { + var f = line.split('|'); + return E('tr', { class: 'tr' }, [when(Number(f[0])), f[1], f[2], wfcLabel(f[7]), activityLabel(f[3]), (f[4] || '0') + ' ↑ / ' + (f[5] || '0') + ' ↓', _('Encrypted activity; call/SMS unknown')].map(function(x) { return E('td', { class: 'td' }, String(x)); })); + }); + } + var body = E('tbody', {}, rows(raw)); + var count = E('span', {}, String(lines(raw).length)); + function update(value) { dom.content(body, rows(value)); dom.content(count, String(lines(value).length)); } + var clear = E('button', { class: 'btn cbi-button-negative', click: function() { + ui.showModal(_('Clear activity log?'), [E('p', {}, _('This permanently removes only the Wi-Fi Calling activity history. Settings and system logs are not affected.')), + E('div', { class: 'right' }, [E('button', { class: 'btn', click: ui.hideModal }, _('Cancel')), + E('button', { class: 'btn cbi-button-negative', click: function() { fs.write('/var/run/wificalling-gateway/events.log', '').then(function() { update(''); ui.hideModal(); ui.addNotification(null, E('p', {}, _('Activity log cleared.')), 'info'); }).catch(function(err) { ui.addNotification(null, E('p', {}, _('Unable to clear log:') + ' ' + err.message), 'error'); }); } }, _('Clear log'))])]); + } }, _('Clear log')); + poll.add(function() { return L.resolveDefault(fs.read('/var/run/wificalling-gateway/events.log'), '').then(update); }, 5); + var children = [ + E('h2', {}, _('Encrypted IMS activity log')), + E('p', {}, _('Records handshake success or failure and sustained encrypted communication such as ringing or calls. Brief traffic bursts are not logged. Phone numbers, message content, and whether an event is a call or SMS are not visible.')) + ]; + if (logEnabled === '0') + children.push(E('div', { class: 'alert-message warning' }, _('Activity log recording is disabled. Enable it in Settings.'))); + children.push(E('div', { class: 'cbi-section' }, [E('p', {}, [_('Records:') + ' ', count, ' ', clear]), E('table', { class: 'table' }, [E('tr', { class: 'tr table-titles' }, [_('Time'), _('Device'), _('IP'), _('Wi-Fi Calling'), _('Activity'), _('Packet delta'), _('Meaning')].map(function(x) { return E('th', { class: 'th' }, x); })), body])])); + return E([], children); + } +}); diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js new file mode 100644 index 0000000000..5728d27008 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js @@ -0,0 +1,146 @@ +'use strict'; +'require view'; +'require form'; +'require fs'; +'require poll'; +'require uci'; +'require dom'; +'require ui'; +'require wificalling-gateway.node-import as nodeImport'; + +return view.extend({ + load: function() { + return Promise.all([ + L.resolveDefault(fs.read('/var/run/wificalling-gateway/node-status.json'), '{}'), + uci.load('wificalling-gateway') + ]); + }, + render: function(data) { + var nodeParsed; + try { nodeParsed = JSON.parse(data[0]); } catch (e) { nodeParsed = { nodes: [] }; } + function nodeById(id, source) { + var nodes = (source || nodeParsed).nodes || []; + for (var i = 0; i < nodes.length; i++) if (nodes[i].id === id) return nodes[i]; + return null; + } + function quality(n) { + if (!n) return '-'; + if (n.state === 'unreachable') return _('Offline'); + if (n.ping_ms == null) return _('Unknown'); + if (n.ping_ms <= 100) return _('Excellent'); + if (n.ping_ms <= 200) return _('Good'); + if (n.ping_ms <= 300) return _('Fair'); + return _('Poor'); + } + function nodeState(n) { + if (!n) return '-'; + if (n.state === 'reachable' || n.state === 'tcp_reachable') return _('Alive'); + if (n.state === 'unreachable') return _('Offline'); + return _('Unknown'); + } + function latency(n) { return n && n.ping_ms != null ? n.ping_ms + ' ms (' + n.measurement + ')' : '-'; } + + var m = new form.Map('wificalling-gateway', _('Wi-Fi Calling Gateway settings'), + _('Configure proxy nodes and assign fixed LAN devices. Monitoring and logs are available from the submenu.')); + var importPanel = E('div', { class: 'cbi-section' }, [ + E('h3', {}, _('Import proxy node')), + E('p', {}, _('Paste one AnyTLS, Hysteria2/Hy2, TUIC, VLESS, or VMess link. It is parsed locally in this browser and is not sent to an external service.')), + E('button', { class: 'btn cbi-button-positive', click: function() { + var input = E('textarea', { class: 'cbi-input-textarea', rows: 6, style: 'width:100%', placeholder: 'anytls://…' }); + ui.showModal(_('Import node link'), [input, E('div', { class: 'right' }, [ + E('button', { class: 'btn', click: ui.hideModal }, _('Cancel')), + E('button', { class: 'btn cbi-button-positive', click: function() { + var parsed; + try { parsed = nodeImport.parse(input.value); } + catch (err) { ui.addNotification(null, E('p', {}, _('Unable to parse node link:') + ' ' + err.message), 'error'); return; } + var sid = uci.add('wificalling-gateway', 'node'); + Object.keys(parsed).forEach(function(key) { if (parsed[key] !== '') uci.set('wificalling-gateway', sid, key, parsed[key]); }); + uci.save().then(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, _('Node imported successfully. Reloading settings…')), 'info'); + window.setTimeout(function() { window.location.reload(); }, 500); + }).catch(function(err) { ui.addNotification(null, E('p', {}, _('Unable to save imported node:') + ' ' + err.message), 'error'); }); + } }, _('Import')) + ])]); + } }, _('Import node link')) + ]); + var s = m.section(form.NamedSection, 'main', 'global', _('General')); + s.option(form.Flag, 'enabled', _('Enable')); + var logLevel = s.option(form.ListValue, 'log_level', _('Log level')); + logLevel.value('warn', _('Warning')); logLevel.value('info', _('Information')); logLevel.value('debug', _('Debug')); + var logEnabled = s.option(form.Flag, 'log_enabled', _('Activity log')); + logEnabled.default = '1'; + logEnabled.description = _('Record handshake outcomes and sustained encrypted communication. Turn off to stop writing the activity log.'); + var eventInterval = s.option(form.Value, 'event_interval', _('Sustained activity log interval (seconds)')); + eventInterval.datatype = 'range(30,3600)'; eventInterval.default = '60'; + eventInterval.depends('log_enabled', '1'); + eventInterval.description = _('Continuous traffic is aggregated and written at most once per interval.'); + var maxEvents = s.option(form.Value, 'max_events_per_device', _('Maximum records per device')); + maxEvents.datatype = 'range(1,500)'; maxEvents.default = '20'; + maxEvents.depends('log_enabled', '1'); + maxEvents.description = _('Each device keeps its own newest records, so one device cannot fill the entire log.'); + + s = m.section(form.GridSection, 'node', _('Proxy nodes')); + s.addremove = true; s.nodescriptions = true; s.anonymous = true; s.addbtntitle = _('Add proxy node'); + s.sectiontitle = function(id) { return uci.get('wificalling-gateway', id, 'label') || id; }; + s.option(form.Flag, 'enabled', _('Enable')).default = '1'; + var nodeLabel = s.option(form.Value, 'label', _('Node display name')); + nodeLabel.rmempty = false; nodeLabel.placeholder = _('Example: UK AnyTLS'); + nodeLabel.description = _('This name is shown in the device node selector.'); + var p = s.option(form.ListValue, 'protocol', _('Protocol')); + ['anytls','hysteria2','tuic','vless','vmess'].forEach(function(x) { p.value(x); }); + s.option(form.Value, 'server', _('Server')).datatype = 'host'; + s.option(form.Value, 'port', _('Port')).datatype = 'port'; + var nodeStatus = s.option(form.DummyValue, '_node_status', _('Node status')); + nodeStatus.textvalue = function(id) { return E('span', { id: 'wfc-node-state-' + id }, nodeState(nodeById(id))); }; + var nodePing = s.option(form.DummyValue, '_node_ping', _('Ping / latency')); + nodePing.textvalue = function(id) { return E('span', { id: 'wfc-node-ping-' + id }, latency(nodeById(id))); }; + var nodeQuality = s.option(form.DummyValue, '_node_quality', _('Quality')); + nodeQuality.textvalue = function(id) { return E('span', { id: 'wfc-node-quality-' + id }, quality(nodeById(id))); }; + var secret = s.option(form.Value, 'password', _('Password')); + secret.password = true; secret.textvalue = function(id) { return this.cfgvalue(id) ? _('Set') : _('Not set'); }; + s.option(form.Value, 'uuid', _('UUID')); + s.option(form.Value, 'sni', _('TLS server name')); + s.option(form.Flag, 'insecure', _('Allow insecure certificate')); + s.option(form.Value, 'alpn', _('ALPN')); + s.option(form.Value, 'pin_sha256', _('TLS public-key SHA-256 (base64)')); + s.option(form.Value, 'flow', _('VLESS flow')); + s.option(form.Value, 'public_key', _('Reality public key')); + s.option(form.Value, 'short_id', _('Reality short ID')); + s.option(form.Value, 'fingerprint', _('Reality fingerprint')); + var udpMode = s.option(form.ListValue, 'udp_mode', _('TUIC UDP mode')); + udpMode.value('native', _('Native')); udpMode.value('quic', _('QUIC')); + var transport = s.option(form.ListValue, 'transport', _('Transport')); + transport.value('', _('None')); transport.value('ws', _('WebSocket')); + s.option(form.Value, 'path', _('WebSocket path')); + s.option(form.Value, 'host', _('WebSocket Host')); + + s = m.section(form.GridSection, 'device', _('Device policies')); + s.addremove = true; s.nodescriptions = true; s.anonymous = true; s.addbtntitle = _('Add LAN device'); + s.sectiontitle = function(id) { return uci.get('wificalling-gateway', id, 'label') || id; }; + s.option(form.Flag, 'enabled', _('Enable')).default = '1'; + var deviceLabel = s.option(form.Value, 'label', _('Device display name')); + deviceLabel.rmempty = false; deviceLabel.placeholder = _('Example: iPhone 12'); + var routeMode = s.option(form.ListValue, 'route_mode', _('Routing mode')); + routeMode.value('independent', _('Independent tunnel')); routeMode.value('follow_gateway', _('Follow gateway')); + routeMode.default = 'independent'; + var selectedNode = s.option(form.ListValue, 'node', _('Node')); + selectedNode.rmempty = false; selectedNode.depends('route_mode', 'independent'); + selectedNode.description = _('Save the node first, then reload this page to select it for a device.'); + uci.sections('wificalling-gateway', 'node').forEach(function(node) { selectedNode.value(node['.name'], node.label || node['.name']); }); + var ips = s.option(form.DynamicList, 'source_ip', _('LAN IPv4 addresses')); + ips.datatype = 'ip4addr'; ips.rmempty = false; ips.placeholder = '192.168.31.189'; + + poll.add(function() { + return L.resolveDefault(fs.read('/var/run/wificalling-gateway/node-status.json'), '{}').then(function(raw) { + var current; try { current = JSON.parse(raw); } catch (e) { current = { nodes: [] }; } + (current.nodes || []).forEach(function(n) { + [['state', nodeState(n)], ['ping', latency(n)], ['quality', quality(n)]].forEach(function(v) { + var el = document.getElementById('wfc-node-' + v[0] + '-' + n.id); if (el) dom.content(el, v[1]); + }); + }); + }); + }, 5); + return m.render().then(function(formNode) { return E([], [importPanel, formNode]); }); + } +}); diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/status.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/status.js new file mode 100644 index 0000000000..2f5185fdb0 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/status.js @@ -0,0 +1,40 @@ +'use strict'; +'require view'; +'require fs'; +'require poll'; +'require dom'; + +return view.extend({ + load: function() { return L.resolveDefault(fs.read('/var/run/wificalling-gateway/status.json'), '{}'); }, + render: function(raw) { + function parse(value) { try { return JSON.parse(value); } catch (e) { return { devices: [] }; } } + function when(epoch) { return epoch ? new Date(epoch * 1000).toLocaleString() : '-'; } + function wfcLabel(v) { + switch (v) { + case 'registered': return _('Registered'); + case 'connecting': return _('Connecting'); + case 'not_detected': return _('Not detected'); + case 'likely_registered': return _('Likely registered'); + case 'active_traffic': return _('Active traffic'); + case 'nat_t_seen': return _('NAT-T seen'); + case 'negotiating': return _('Negotiating'); + case 'no_session': return _('No session'); + default: return v || '-'; + } + } + function rows(source) { + return (source.devices || []).map(function(d) { + var values = [d.label, d.ip, wfcLabel(d.wificalling || d.state), d.node || '-', d.epdg_ip || '-', + (d.ike_seen ? '500' : '-') + ' / ' + (d.nat_t_seen ? '4500' : '-'), + d.assured ? _('Yes') : _('No'), d.sent_packets + ' ↑ / ' + d.reply_packets + ' ↓', when(d.last_activity)]; + return E('tr', { class: 'tr' }, values.map(function(x) { return E('td', { class: 'td' }, String(x)); })); + }); + } + var body = E('tbody', {}, rows(parse(raw))); + poll.add(function() { return L.resolveDefault(fs.read('/var/run/wificalling-gateway/status.json'), '{}').then(function(v) { dom.content(body, rows(parse(v))); }); }, 5); + return E([], [E('h2', {}, _('Wi-Fi Calling status')), E('p', {}, _('Registered means an ASSURED bidirectional UDP 4500 tunnel was observed. This is network evidence, not carrier activation confirmation.')), + E('div', { class: 'table cbi-section-table' }, [E('table', { class: 'table' }, [ + E('tr', { class: 'tr table-titles' }, [_('Device'), _('IP'), _('Wi-Fi Calling status'), _('Node'), _('ePDG IP'), _('UDP 500/4500'), _('ASSURED'), _('Packets'), _('Last activity')].map(function(x) { return E('th', { class: 'th' }, x); })), body + ])])]); + } +}); diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js new file mode 100644 index 0000000000..841095cf10 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js @@ -0,0 +1,82 @@ +'use strict'; +'require baseclass'; + +function decodeLabel(value) { + try { return decodeURIComponent(value || ''); } catch (e) { return value || ''; } +} + +function decodeBase64(value) { + var normalized = value.replace(/-/g, '+').replace(/_/g, '/').replace(/\s+/g, ''); + while (normalized.length % 4) normalized += '='; + var binary = atob(normalized), bytes = new Uint8Array(binary.length); + for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return new TextDecoder('utf-8').decode(bytes); +} + +function truthy(value) { + return /^(1|true|yes)$/i.test(value || '') ? '1' : '0'; +} + +function common(protocol, url) { + if (!url.hostname || !url.port) throw new Error(_('Server and port are required')); + return { + enabled: '1', protocol: protocol, server: url.hostname, port: url.port, + label: decodeLabel(url.hash.replace(/^#/, '')) || protocol.toUpperCase() + ' ' + url.hostname + }; +} + +function parseUrl(uri, protocol) { + var url = new URL(uri), p = url.searchParams, out = common(protocol, url); + if (protocol === 'anytls' || protocol === 'hysteria2') { + out.password = decodeURIComponent(url.username || ''); + out.sni = p.get('peer') || p.get('sni') || ''; + out.insecure = truthy(p.get('insecure') || p.get('allowInsecure')); + out.alpn = p.get('alpn') || ''; + out.pin_sha256 = p.get('pinSHA256') || ''; + out.fingerprint = p.get('fingerprint') || p.get('fp') || ''; + out.udp = truthy(p.get('udp')); + } else if (protocol === 'tuic') { + out.uuid = decodeURIComponent(url.username || ''); + out.password = decodeURIComponent(url.password || ''); + out.sni = p.get('sni') || ''; + out.insecure = truthy(p.get('insecure') || p.get('allowInsecure') || p.get('allow_insecure')); + out.alpn = p.get('alpn') || ''; + out.congestion = p.get('congestion_control') || p.get('congestion') || 'bbr'; + out.udp_mode = p.get('udp_relay_mode') || 'native'; + } else if (protocol === 'vless') { + out.uuid = decodeURIComponent(url.username || ''); + out.flow = p.get('flow') || ''; + out.security = p.get('security') || ''; + out.sni = p.get('sni') || ''; + out.public_key = p.get('pbk') || p.get('publicKey') || ''; + out.short_id = p.get('sid') || p.get('shortId') || ''; + out.fingerprint = p.get('fp') || p.get('fingerprint') || 'chrome'; + if (p.get('type') === 'ws') { + out.transport = 'ws'; out.path = p.get('path') || '/'; out.host = p.get('host') || ''; + } + } + return out; +} + +function parseVmess(uri) { + var raw = JSON.parse(decodeBase64(uri.slice('vmess://'.length).trim())); + if (!raw.add || !raw.port || !raw.id) throw new Error(_('VMess server, port and UUID are required')); + var out = { + enabled: '1', protocol: 'vmess', label: raw.ps || 'VMess ' + raw.add, + server: raw.add, port: String(raw.port), uuid: raw.id, alter_id: String(raw.aid || 0), + sni: raw.sni || '', host: raw.host || '', path: raw.path || '' + }; + if (raw.net === 'ws') out.transport = 'ws'; + return out; +} + +function parse(uri) { + var value = (uri || '').trim(), scheme = value.split(':', 1)[0].toLowerCase(); + if (scheme === 'vmess') return parseVmess(value); + if (scheme === 'hy2') scheme = 'hysteria2'; + if (['anytls', 'hysteria2', 'tuic', 'vless'].indexOf(scheme) < 0) + throw new Error(_('Unsupported node link format')); + return parseUrl(value, scheme); +} + +return baseclass.extend({ parse: parse }); diff --git a/applications/luci-app-wificalling-gateway/po/templates/wificalling-gateway.pot b/applications/luci-app-wificalling-gateway/po/templates/wificalling-gateway.pot new file mode 100644 index 0000000000..047ce5288c --- /dev/null +++ b/applications/luci-app-wificalling-gateway/po/templates/wificalling-gateway.pot @@ -0,0 +1,345 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" + +msgid "ALPN" +msgstr "" + +msgid "ASSURED" +msgstr "" + +msgid "Active traffic" +msgstr "" + +msgid "Activity" +msgstr "" + +msgid "Activity Log" +msgstr "" + +msgid "Activity log" +msgstr "" + +msgid "Activity log cleared." +msgstr "" + +msgid "Activity log recording is disabled. Enable it in Settings." +msgstr "" + +msgid "Add LAN device" +msgstr "" + +msgid "Add proxy node" +msgstr "" + +msgid "Alive" +msgstr "" + +msgid "Allow insecure certificate" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Clear activity log?" +msgstr "" + +msgid "Clear log" +msgstr "" + +msgid "Configure proxy nodes and assign fixed LAN devices. Monitoring and logs are available from the submenu." +msgstr "" + +msgid "Connecting" +msgstr "" + +msgid "Continuous traffic is aggregated and written at most once per interval." +msgstr "" + +msgid "Debug" +msgstr "" + +msgid "Device" +msgstr "" + +msgid "Device display name" +msgstr "" + +msgid "Device policies" +msgstr "" + +msgid "Each device keeps its own newest records, so one device cannot fill the entire log." +msgstr "" + +msgid "Enable" +msgstr "" + +msgid "Encrypted IMS activity log" +msgstr "" + +msgid "Encrypted activity; call/SMS unknown" +msgstr "" + +msgid "Example: UK AnyTLS" +msgstr "" + +msgid "Example: iPhone 12" +msgstr "" + +msgid "Excellent" +msgstr "" + +msgid "Fair" +msgstr "" + +msgid "Follow gateway" +msgstr "" + +msgid "General" +msgstr "" + +msgid "Good" +msgstr "" + +msgid "Handshake failed" +msgstr "" + +msgid "Handshake success" +msgstr "" + +msgid "IP" +msgstr "" + +msgid "Import" +msgstr "" + +msgid "Import node link" +msgstr "" + +msgid "Import proxy node" +msgstr "" + +msgid "Independent tunnel" +msgstr "" + +msgid "Information" +msgstr "" + +msgid "LAN IPv4 addresses" +msgstr "" + +msgid "Last activity" +msgstr "" + +msgid "Likely registered" +msgstr "" + +msgid "Log level" +msgstr "" + +msgid "Maximum records per device" +msgstr "" + +msgid "Meaning" +msgstr "" + +msgid "NAT-T seen" +msgstr "" + +msgid "Native" +msgstr "" + +msgid "Negotiating" +msgstr "" + +msgid "No" +msgstr "" + +msgid "No session" +msgstr "" + +msgid "Node" +msgstr "" + +msgid "Node display name" +msgstr "" + +msgid "Node imported successfully. Reloading settings…" +msgstr "" + +msgid "Node status" +msgstr "" + +msgid "None" +msgstr "" + +msgid "Not detected" +msgstr "" + +msgid "Not set" +msgstr "" + +msgid "Offline" +msgstr "" + +msgid "Packet delta" +msgstr "" + +msgid "Packets" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Paste one AnyTLS, Hysteria2/Hy2, TUIC, VLESS, or VMess link. It is parsed locally in this browser and is not sent to an external service." +msgstr "" + +msgid "Ping / latency" +msgstr "" + +msgid "Poor" +msgstr "" + +msgid "Port" +msgstr "" + +msgid "Protocol" +msgstr "" + +msgid "Proxy nodes" +msgstr "" + +msgid "QUIC" +msgstr "" + +msgid "Quality" +msgstr "" + +msgid "Reality fingerprint" +msgstr "" + +msgid "Reality public key" +msgstr "" + +msgid "Reality short ID" +msgstr "" + +msgid "Record handshake outcomes and sustained encrypted communication. Turn off to stop writing the activity log." +msgstr "" + +msgid "Records handshake success or failure and sustained encrypted communication such as ringing or calls. Brief traffic bursts are not logged. Phone numbers, message content, and whether an event is a call or SMS are not visible." +msgstr "" + +msgid "Records:" +msgstr "" + +msgid "Registered" +msgstr "" + +msgid "Registered means an ASSURED bidirectional UDP 4500 tunnel was observed. This is network evidence, not carrier activation confirmation." +msgstr "" + +msgid "Routing mode" +msgstr "" + +msgid "Save the node first, then reload this page to select it for a device." +msgstr "" + +msgid "Server" +msgstr "" + +msgid "Server and port are required" +msgstr "" + +msgid "Set" +msgstr "" + +msgid "Settings" +msgstr "" + +msgid "Sustained activity log interval (seconds)" +msgstr "" + +msgid "Sustained traffic" +msgstr "" + +msgid "TLS public-key SHA-256 (base64)" +msgstr "" + +msgid "TLS server name" +msgstr "" + +msgid "TUIC UDP mode" +msgstr "" + +msgid "This name is shown in the device node selector." +msgstr "" + +msgid "This permanently removes only the Wi-Fi Calling activity history. Settings and system logs are not affected." +msgstr "" + +msgid "Time" +msgstr "" + +msgid "Transport" +msgstr "" + +msgid "UDP 500/4500" +msgstr "" + +msgid "UUID" +msgstr "" + +msgid "Unable to clear log:" +msgstr "" + +msgid "Unable to parse node link:" +msgstr "" + +msgid "Unable to save imported node:" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Unsupported node link format" +msgstr "" + +msgid "VLESS flow" +msgstr "" + +msgid "VMess server, port and UUID are required" +msgstr "" + +msgid "Warning" +msgstr "" + +msgid "WebSocket" +msgstr "" + +msgid "WebSocket Host" +msgstr "" + +msgid "WebSocket path" +msgstr "" + +msgid "Wi-Fi Calling" +msgstr "" + +msgid "Wi-Fi Calling Gateway settings" +msgstr "" + +msgid "Wi-Fi Calling Status" +msgstr "" + +msgid "Wi-Fi Calling status" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "ePDG IP" +msgstr "" diff --git a/applications/luci-app-wificalling-gateway/po/zh_Hans/wificalling-gateway.po b/applications/luci-app-wificalling-gateway/po/zh_Hans/wificalling-gateway.po new file mode 100644 index 0000000000..377fb5ba33 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/po/zh_Hans/wificalling-gateway.po @@ -0,0 +1,346 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" + +msgid "ALPN" +msgstr "ALPN" + +msgid "ASSURED" +msgstr "ASSURED" + +msgid "Active traffic" +msgstr "活动流量" + +msgid "Activity" +msgstr "活动" + +msgid "Activity Log" +msgstr "活动日志" + +msgid "Activity log" +msgstr "活动日志" + +msgid "Activity log cleared." +msgstr "活动日志已清空。" + +msgid "Activity log recording is disabled. Enable it in Settings." +msgstr "活动日志记录已关闭,可在设置中开启。" + +msgid "Add LAN device" +msgstr "添加局域网设备" + +msgid "Add proxy node" +msgstr "添加代理节点" + +msgid "Alive" +msgstr "在线" + +msgid "Allow insecure certificate" +msgstr "允许不安全证书" + +msgid "Cancel" +msgstr "取消" + +msgid "Clear activity log?" +msgstr "清空活动日志?" + +msgid "Clear log" +msgstr "清空日志" + +msgid "Configure proxy nodes and assign fixed LAN devices. Monitoring and logs are available from the submenu." +msgstr "配置代理节点并绑定固定的局域网设备。监控和日志可在子菜单中查看。" + +msgid "Connecting" +msgstr "连接中" + +msgid "Continuous traffic is aggregated and written at most once per interval." +msgstr "持续流量会被聚合,每个间隔最多写入一次。" + +msgid "Debug" +msgstr "调试" + +msgid "Device" +msgstr "设备" + +msgid "Device display name" +msgstr "设备显示名称" + +msgid "Device policies" +msgstr "设备策略" + +msgid "Each device keeps its own newest records, so one device cannot fill the entire log." +msgstr "每台设备独立保留各自的最新记录,单台设备不会占满整个日志。" + +msgid "Enable" +msgstr "启用" + +msgid "Encrypted IMS activity log" +msgstr "加密 IMS 活动日志" + +msgid "Encrypted activity; call/SMS unknown" +msgstr "加密活动;无法区分通话/短信" + +msgid "Example: UK AnyTLS" +msgstr "示例:UK AnyTLS" + +msgid "Example: iPhone 12" +msgstr "示例:iPhone 12" + +msgid "Excellent" +msgstr "优秀" + +msgid "Fair" +msgstr "一般" + +msgid "Follow gateway" +msgstr "跟随网关" + +msgid "General" +msgstr "常规" + +msgid "Good" +msgstr "良好" + +msgid "Handshake failed" +msgstr "握手失败" + +msgid "Handshake success" +msgstr "握手成功" + +msgid "IP" +msgstr "IP" + +msgid "Import" +msgstr "导入" + +msgid "Import node link" +msgstr "导入节点链接" + +msgid "Import proxy node" +msgstr "导入代理节点" + +msgid "Independent tunnel" +msgstr "独立通道" + +msgid "Information" +msgstr "信息" + +msgid "LAN IPv4 addresses" +msgstr "局域网 IPv4 地址" + +msgid "Last activity" +msgstr "最后活动" + +msgid "Likely registered" +msgstr "可能已注册" + +msgid "Log level" +msgstr "日志级别" + +msgid "Maximum records per device" +msgstr "每台设备最大记录数" + +msgid "Meaning" +msgstr "含义" + +msgid "NAT-T seen" +msgstr "已发现 NAT-T" + +msgid "Native" +msgstr "原生" + +msgid "Negotiating" +msgstr "协商中" + +msgid "No" +msgstr "否" + +msgid "No session" +msgstr "无会话" + +msgid "Node" +msgstr "节点" + +msgid "Node display name" +msgstr "节点显示名称" + +msgid "Node imported successfully. Reloading settings…" +msgstr "节点导入成功。正在重新加载设置…" + +msgid "Node status" +msgstr "节点状态" + +msgid "None" +msgstr "无" + +msgid "Not detected" +msgstr "未检测到" + +msgid "Not set" +msgstr "未设置" + +msgid "Offline" +msgstr "离线" + +msgid "Packet delta" +msgstr "数据包增量" + +msgid "Packets" +msgstr "数据包" + +msgid "Password" +msgstr "密码" + +msgid "Paste one AnyTLS, Hysteria2/Hy2, TUIC, VLESS, or VMess link. It is parsed locally in this browser and is not sent to an external service." +msgstr "粘贴一个 AnyTLS、Hysteria2/Hy2、TUIC、VLESS 或 VMess 链接。链接仅在本浏览器中本地解析,不会发送到外部服务。" + +msgid "Ping / latency" +msgstr "Ping / 延迟" + +msgid "Poor" +msgstr "较差" + +msgid "Port" +msgstr "端口" + +msgid "Protocol" +msgstr "协议" + +msgid "Proxy nodes" +msgstr "代理节点" + +msgid "QUIC" +msgstr "QUIC" + +msgid "Quality" +msgstr "质量" + +msgid "Reality fingerprint" +msgstr "Reality 指纹" + +msgid "Reality public key" +msgstr "Reality 公钥" + +msgid "Reality short ID" +msgstr "Reality 短 ID" + +msgid "Record handshake outcomes and sustained encrypted communication. Turn off to stop writing the activity log." +msgstr "记录握手结果与持续加密通讯。关闭后将停止写入活动日志。" + +msgid "Records handshake success or failure and sustained encrypted communication such as ringing or calls. Brief traffic bursts are not logged. Phone numbers, message content, and whether an event is a call or SMS are not visible." +msgstr "记录握手成功或失败,以及响铃、通话等持续加密通讯。短暂流量脉冲不记录。电话号码、消息内容,以及是通话还是短信均不可见。" + +msgid "Records:" +msgstr "记录数:" + +msgid "Registered" +msgstr "已注册" + +msgid "Registered means an ASSURED bidirectional UDP 4500 tunnel was observed. This is network evidence, not carrier activation confirmation." +msgstr "已注册表示观察到 ASSURED 的双向 UDP 4500 隧道。这是网络层面的证据,不代表运营商激活已完成。" + +msgid "Routing mode" +msgstr "路由模式" + +msgid "Save the node first, then reload this page to select it for a device." +msgstr "请先保存节点,再刷新本页以便为设备选择该节点。" + +msgid "Server" +msgstr "服务器" + +msgid "Server and port are required" +msgstr "服务器和端口为必填项" + +msgid "Set" +msgstr "已设置" + +msgid "Settings" +msgstr "设置" + +msgid "Sustained activity log interval (seconds)" +msgstr "持续活动日志间隔(秒)" + +msgid "Sustained traffic" +msgstr "持续通讯" + +msgid "TLS public-key SHA-256 (base64)" +msgstr "TLS 公钥 SHA-256 (base64)" + +msgid "TLS server name" +msgstr "TLS 服务器名称" + +msgid "TUIC UDP mode" +msgstr "TUIC UDP 模式" + +msgid "This name is shown in the device node selector." +msgstr "此名称显示在设备节点选择器中。" + +msgid "This permanently removes only the Wi-Fi Calling activity history. Settings and system logs are not affected." +msgstr "此操作仅永久删除 Wi-Fi Calling 活动历史。设置和系统日志不受影响。" + +msgid "Time" +msgstr "时间" + +msgid "Transport" +msgstr "传输" + +msgid "UDP 500/4500" +msgstr "UDP 500/4500" + +msgid "UUID" +msgstr "UUID" + +msgid "Unable to clear log:" +msgstr "无法清空日志:" + +msgid "Unable to parse node link:" +msgstr "无法解析节点链接:" + +msgid "Unable to save imported node:" +msgstr "无法保存导入的节点:" + +msgid "Unknown" +msgstr "未知" + +msgid "Unsupported node link format" +msgstr "不支持的节点链接格式" + +msgid "VLESS flow" +msgstr "VLESS 流控" + +msgid "VMess server, port and UUID are required" +msgstr "VMess 服务器、端口和 UUID 为必填项" + +msgid "Warning" +msgstr "警告" + +msgid "WebSocket" +msgstr "WebSocket" + +msgid "WebSocket Host" +msgstr "WebSocket 主机" + +msgid "WebSocket path" +msgstr "WebSocket 路径" + +msgid "Wi-Fi Calling" +msgstr "Wi-Fi Calling" + +msgid "Wi-Fi Calling Gateway settings" +msgstr "Wi-Fi Calling Gateway 设置" + +msgid "Wi-Fi Calling Status" +msgstr "Wi-Fi Calling 状态" + +msgid "Wi-Fi Calling status" +msgstr "Wi-Fi Calling 状态" + +msgid "Yes" +msgstr "是" + +msgid "ePDG IP" +msgstr "ePDG IP" diff --git a/applications/luci-app-wificalling-gateway/root/etc/config/wificalling-gateway b/applications/luci-app-wificalling-gateway/root/etc/config/wificalling-gateway new file mode 100644 index 0000000000..7711eea8bf --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/etc/config/wificalling-gateway @@ -0,0 +1,10 @@ +config global 'main' + option enabled '0' + option log_level 'warn' + option monitor_interval '5' + option event_interval '60' + option max_events_per_device '20' + option log_enabled '1' + +# Add nodes and device policies in LuCI. Secrets are intentionally not included. +# Reserve each client address with static DHCP before enabling a policy. diff --git a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway new file mode 100755 index 0000000000..c81051bea5 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway @@ -0,0 +1,71 @@ +#!/bin/sh /etc/rc.common +USE_PROCD=1 +START=99 +STOP=10 + +APP=wificalling-gateway +RUNDIR=/var/run/$APP + +append_node() { + local s="$1" enabled label protocol server port password sni insecure alpn uuid congestion udp_mode public_key short_id fingerprint security transport path host flow alter_id credential auxiliary pin_sha256 + config_get_bool enabled "$s" enabled 1 + [ "$enabled" -eq 1 ] || return 0 + config_get label "$s" label "$s"; config_get protocol "$s" protocol + config_get server "$s" server; config_get port "$s" port; config_get password "$s" password + config_get sni "$s" sni; config_get insecure "$s" insecure 0; config_get alpn "$s" alpn + config_get uuid "$s" uuid; config_get congestion "$s" congestion bbr; config_get udp_mode "$s" udp_mode native + config_get public_key "$s" public_key; config_get short_id "$s" short_id; config_get fingerprint "$s" fingerprint chrome + config_get security "$s" security; config_get transport "$s" transport; config_get path "$s" path; config_get host "$s" host + config_get pin_sha256 "$s" pin_sha256 + config_get flow "$s" flow; config_get alter_id "$s" alter_id 0 + case "$label$protocol$server$password$sni$uuid$public_key$short_id$host" in *'|'*) logger -t "$APP" "invalid delimiter in node $s"; return 1;; esac + credential=$password; auxiliary=$uuid + case "$protocol" in + vless|vmess) credential=$uuid; auxiliary=${flow:-$alter_id} ;; + esac + printf 'node|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$s" "$protocol" "$server" "$port" "$credential" "$sni" "$insecure" "$alpn" "$auxiliary" "$congestion" "$udp_mode" "$public_key" "$short_id" "$fingerprint" "$security" "$transport" "$path" "$host" "$pin_sha256" >> "$RUNDIR/normalized.conf" + printf '%s|%s|%s|%s|%s\n' "$s" "$label" "$protocol" "$server" "$port" >> "$RUNDIR/nodes" +} + +append_ip() { DEVICE_IPS="${DEVICE_IPS}${DEVICE_IPS:+,}$1"; } +append_device() { + local s="$1" enabled label node route_mode + config_get_bool enabled "$s" enabled 1; [ "$enabled" -eq 1 ] || return 0 + config_get label "$s" label "$s"; config_get node "$s" node; config_get route_mode "$s" route_mode independent + DEVICE_IPS=""; config_list_foreach "$s" source_ip append_ip + [ "$route_mode" = independent ] || return 0 + printf 'device|%s|%s|%s\n' "$label" "$node" "$DEVICE_IPS" >> "$RUNDIR/normalized.conf" + IFS=,; for ip in $DEVICE_IPS; do printf '%s|%s|node-%s\n' "$label" "$ip" "$node" >> "$RUNDIR/clients"; done; unset IFS +} + +start_service() { + chmod 600 "/etc/config/$APP" 2>/dev/null || true + config_load "$APP"; config_get_bool enabled main enabled 0; [ "$enabled" -eq 1 ] || return 0 + mkdir -p "$RUNDIR"; chmod 700 "$RUNDIR"; : > "$RUNDIR/normalized.conf"; : > "$RUNDIR/clients"; : > "$RUNDIR/nodes" + config_get log_level main log_level warn; printf 'global|log_level|%s\n' "$log_level" >> "$RUNDIR/normalized.conf" + config_get event_interval main event_interval 60 + config_get max_events_per_device main max_events_per_device 20 + case "$event_interval" in ''|*[!0-9]*) event_interval=60;; esac + case "$max_events_per_device" in ''|*[!0-9]*) max_events_per_device=20;; esac + [ "$event_interval" -ge 30 ] && [ "$event_interval" -le 3600 ] || event_interval=60 + [ "$max_events_per_device" -ge 1 ] && [ "$max_events_per_device" -le 500 ] || max_events_per_device=20 + config_get_bool log_enabled main log_enabled 1 + config_foreach append_node node; config_foreach append_device device + /usr/libexec/$APP/compiler.sh "$RUNDIR/normalized.conf" "$RUNDIR/sing-box.json" || return 1 + /usr/bin/sing-box check -c "$RUNDIR/sing-box.json" || { logger -t "$APP" "sing-box rejected generated configuration"; return 1; } + /usr/libexec/$APP/firewall.sh start "$RUNDIR/clients" + procd_open_instance sing-box + procd_set_param command /usr/bin/sing-box run -c "$RUNDIR/sing-box.json" + procd_set_param respawn 3600 5 5 + procd_set_param limits nofile="65535 65535" + procd_close_instance + procd_open_instance monitor + procd_set_param command /usr/libexec/$APP/monitor-loop.sh "$RUNDIR/clients" "$RUNDIR/status.json" "$RUNDIR/nodes" "$RUNDIR/node-status.json" "$RUNDIR/events.log" "$RUNDIR/monitor.state" "$event_interval" "$max_events_per_device" "$log_enabled" + procd_set_param respawn + procd_close_instance +} + +stop_service() { /usr/libexec/$APP/firewall.sh stop "$RUNDIR/clients"; } +reload_service() { restart; } + +service_triggers() { procd_add_reload_trigger "$APP"; } diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh new file mode 100755 index 0000000000..b5a56b278f --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh @@ -0,0 +1,82 @@ +#!/bin/sh +set -eu + +input=${1:?normalized configuration required} +output=${2:?output path required} +tmp="${output}.tmp.$$" +trap 'rm -f "$tmp"' EXIT HUP INT TERM + +awk -F '|' ' +function esc(s, x) { x=s; gsub(/\\/, "\\\\", x); gsub(/\"/, "\\\"", x); gsub(/\r/, "\\r", x); gsub(/\n/, "\\n", x); return x } +function q(s) { return "\"" esc(s) "\"" } +function fail(s) { print "wificalling-gateway: " s > "/dev/stderr"; exit 2 } +function private4(ip, a) { + if (ip !~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) return 0 + split(ip,a,"."); if (a[1]>255||a[2]>255||a[3]>255||a[4]>255) return 0 + return a[1]==10 || (a[1]==172 && a[2]>=16 && a[2]<=31) || (a[1]==192 && a[2]==168) +} +function tls(sni, insecure, alpn, pin, extra) { + extra="\"enabled\":true,\"server_name\":" q(sni) ",\"insecure\":" (insecure=="1"?"true":"false") + if (alpn!="") extra=extra ",\"alpn\":[" q(alpn) "]" + if (pin!="") extra=extra ",\"certificate_public_key_sha256\":[" q(pin) "]" + return "{" extra "}" +} +$1=="global" { if ($2=="log_level") level=$3; next } +$1=="node" { + id=$2; proto=$3 + if (id=="" || seen_node[id]++) fail("duplicate or empty node id: " id) + if (proto!="anytls" && proto!="hysteria2" && proto!="tuic" && proto!="vless" && proto!="vmess") fail("unsupported protocol: " proto) + if ($4=="" || $5 !~ /^[0-9]+$/ || $5<1 || $5>65535) fail("invalid server or port for node: " id) + node[++nn]=$0; node_id[nn]=id; node_proto[id]=proto; next +} +$1=="device" { + if (!node_proto[$3]) fail("device references unknown node: " $3) + n=split($4, ips, ","); if (n<1 || $4=="") fail("device has no client IP: " $2) + normalized="" + for(i=1;i<=n;i++) { + ip=ips[i]; gsub(/^[ \t]+|[ \t]+$/, "", ip) + if (!private4(ip)) fail("client IP must be private IPv4: " ip) + if (owner[ip] && owner[ip]!=$2) fail("duplicate client IP assignment: " ip) + owner[ip]=$2; normalized=normalized (normalized?",":"") ip + } + dev[++nd]=$2; devnode[nd]=$3; devips[nd]=normalized; next +} +END { + if (nn<1) fail("at least one enabled node is required") + if (level=="") level="warn" + print "{" + print " \"log\":{\"level\":" q(level) ",\"timestamp\":true}," + print " \"inbounds\":[{\"type\":\"tproxy\",\"tag\":\"wfc-tcp\",\"listen\":\"0.0.0.0\",\"listen_port\":11441,\"network\":\"tcp\"},{\"type\":\"tproxy\",\"tag\":\"wfc-udp\",\"listen\":\"0.0.0.0\",\"listen_port\":11442,\"network\":\"udp\"}]," + print " \"outbounds\":[" + for(k=1;k<=nn;k++) { + split(node[k],f,"|"); id=f[2]; p=f[3] + s="{\"type\":" q(p) ",\"tag\":" q("node-" id) ",\"server\":" q(f[4]) ",\"server_port\":" f[5] + if (p=="anytls") s=s ",\"password\":" q(f[6]) ",\"tls\":" tls(f[7],f[8],f[9],f[20]) + if (p=="hysteria2") s=s ",\"password\":" q(f[6]) ",\"tls\":" tls(f[7],f[8],f[9],f[20]) + if (p=="tuic") s=s ",\"uuid\":" q(f[10]) ",\"password\":" q(f[6]) ",\"congestion_control\":" q(f[11]?f[11]:"bbr") ",\"udp_relay_mode\":" q(f[12]?f[12]:"native") ",\"tls\":" tls(f[7],f[8],f[9],f[20]) + if (p=="vless") { + s=s ",\"uuid\":" q(f[6]) + if (f[10]!="") s=s ",\"flow\":" q(f[10]) + if (f[16]=="reality") s=s ",\"tls\":{\"enabled\":true,\"server_name\":" q(f[7]) ",\"reality\":{\"enabled\":true,\"public_key\":" q(f[13]) ",\"short_id\":" q(f[14]) "},\"utls\":{\"enabled\":true,\"fingerprint\":" q(f[15]?f[15]:"chrome") "}}" + } + if (p=="vmess") { + s=s ",\"uuid\":" q(f[6]) ",\"security\":\"auto\",\"alter_id\":" (f[10]?f[10]:0) + if (f[17]=="ws") s=s ",\"transport\":{\"type\":\"ws\",\"path\":" q(f[18]) ",\"headers\":{\"Host\":" q(f[19]) "}}" + } + s=s "}"; print " " s "," + } + print " {\"type\":\"direct\",\"tag\":\"direct\"}" + print " ]," + print " \"route\":{\"auto_detect_interface\":true,\"final\":\"direct\",\"rules\":[" + print " {\"ip_is_private\":true,\"action\":\"route\",\"outbound\":\"direct\"}" (nd?",":"") + for(k=1;k<=nd;k++) { + n=split(devips[k],ips,","); list="" + for(i=1;i<=n;i++) list=list (list?",":"") q(ips[i] "/32") + print " {\"source_ip_cidr\":[" list "],\"action\":\"route\",\"outbound\":" q("node-" devnode[k]) "}" (k "$tmp" || exit $? +chmod 600 "$tmp" +mv "$tmp" "$output" +trap - EXIT HUP INT TERM diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/firewall.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/firewall.sh new file mode 100755 index 0000000000..8972ea5f5c --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/firewall.sh @@ -0,0 +1,25 @@ +#!/bin/sh +set -eu +action=${1:-start}; clients=${2:-/var/run/wificalling-gateway/clients} +table='inet wificalling_gateway' +bypass_helper="${0%/*}/passwall-bypass.sh" +[ "$action" = stop ] && { "$bypass_helper" clear "$clients"; nft delete table $table 2>/dev/null || true; ip rule del fwmark 0x66 table 166 2>/dev/null || true; ip route flush table 166 2>/dev/null || true; exit 0; } + +ips=$(awk -F '|' 'NF>=2 { printf "%s%s", (n++?", ":""), $2 }' "$clients") +[ -n "$ips" ] || exit 1 +nft delete table $table 2>/dev/null || true +nft -f - </dev/null || true +ip route replace local 0.0.0.0/0 dev lo table 166 +"$bypass_helper" ensure "$clients" diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/monitor-loop.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/monitor-loop.sh new file mode 100755 index 0000000000..d1448c5ac6 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/monitor-loop.sh @@ -0,0 +1,11 @@ +#!/bin/sh +clients=$1; output=$2; nodes=$3; node_output=$4; events=$5; state=$6; event_interval=${7:-60}; max_events=${8:-20}; log_enabled=${9:-1}; tick=0 +while :; do + /usr/libexec/wificalling-gateway/passwall-bypass.sh ensure "$clients" + /usr/libexec/wificalling-gateway/monitor.sh "$clients" /proc/net/nf_conntrack "$output" "$state" "$events" "$event_interval" "$max_events" "$log_enabled" + if [ "$tick" -eq 0 ]; then + /usr/libexec/wificalling-gateway/node-health.sh "$nodes" "$node_output" + fi + tick=$(( (tick + 1) % 6 )) + sleep 5 +done diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/monitor.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/monitor.sh new file mode 100755 index 0000000000..8d52f4b3e9 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/monitor.sh @@ -0,0 +1,112 @@ +#!/bin/sh +set -eu + +clients=${1:?client map required} +conntrack=${2:-/proc/net/nf_conntrack} +output=${3:-/var/run/wificalling-gateway/status.json} +output_dir=${output%/*} +[ "$output_dir" != "$output" ] || output_dir=. +state=${4:-$output_dir/monitor.state} +events=${5:-$output_dir/events.log} +event_interval=${6:-60} +max_events=${7:-20} +log_enabled=${8:-1} +tmp="${output}.tmp.$$" +state_tmp="${state}.tmp.$$" +event_tmp="${events}.tmp.$$" +trim_tmp="${events}.trim.$$" +trap 'rm -f "$tmp" "$state_tmp" "$event_tmp" "$trim_tmp"' EXIT HUP INT TERM + +now=${WFC_NOW:-$(date +%s)} +touch "$state" "$events" +: > "$state_tmp" +: > "$event_tmp" + +awk -F '|' -v now="$now" -v clients_file="$clients" -v conntrack_file="$conntrack" \ + -v state_file="$state" -v state_out="$state_tmp" -v event_out="$event_tmp" -v event_interval="$event_interval" -v log_enabled="$log_enabled" ' +function q(s, x) { x=s; gsub(/\\/,"\\\\",x); gsub(/\"/,"\\\"",x); return "\"" x "\"" } +FILENAME==clients_file { + if ($1!="" && $2!="") { n++; label[n]=$1; ip[n]=$2; node[n]=$3; index_by_ip[$2]=n } + next +} +FILENAME==state_file { + i=index_by_ip[$2] + if (i) { + old_wfc[i]=$3; old_sent[i]=$4+0; old_reply[i]=$5+0; old_last[i]=$6+0 + old_event[i]=$7+0; old_streak[i]=$8+0; old_acc_sent[i]=$9+0; old_acc_reply[i]=$10+0 + old_traffic_since[i]=($11!="" ? $11+0 : 0) + } + next +} +FILENAME==conntrack_file { + line=$0 + for (i=1;i<=n;i++) { + if (line !~ ("src=" ip[i] " ")) continue + if (match(line,/dst=[0-9.]+/)) dst=substr(line,RSTART+4,RLENGTH-4) + is500=(line ~ /dport=500 /); is4500=(line ~ /dport=4500 /) + if (!is500 && !is4500) continue + if (is500) ike[i]=1 + if (is4500) natt[i]=1 + if (is4500 && line ~ /\[ASSURED\]/) assured[i]=1 + epdg[i]=dst + count=0; rest=line + while (match(rest,/packets=[0-9]+/)) { + val=substr(rest,RSTART+8,RLENGTH-8)+0; count++ + if (count==1) sent[i]=val; else if(count==2) reply[i]=val + rest=substr(rest,RSTART+RLENGTH) + } + } + next +} +END { + print "{\"generated_at\":" now ",\"disclaimer\":\"Encrypted IPsec evidence only; calls and SMS cannot be distinguished.\",\"devices\":[" + for(i=1;i<=n;i++) { + wfc=(assured[i]?"registered":natt[i]||ike[i]?"connecting":"not_detected") + legacy=(assured[i] && sent[i]+reply[i]>=100?"active_traffic":assured[i]?"likely_registered":natt[i]?"nat_t_seen":ike[i]?"negotiating":"no_session") + ds=(sent[i]>=old_sent[i]?sent[i]-old_sent[i]:sent[i]) + dr=(reply[i]>=old_reply[i]?reply[i]-old_reply[i]:reply[i]) + activity=(ds+dr>0?"encrypted_ims_traffic":"none") + last=(ds+dr>0?now:old_last[i]) + if (ds+dr>0) { + streak=(old_streak[i]+1) + traffic_since=(old_streak[i]==0 ? now : old_traffic_since[i]) + } else { + streak=0; traffic_since=0 + } + acc_sent=old_acc_sent[i]+ds; acc_reply=old_acc_reply[i]+dr + handshake_success=(old_wfc[i]!="registered" && wfc=="registered") + handshake_failed=(wfc=="not_detected" && (old_wfc[i]=="registered" || old_wfc[i]=="connecting")) + sustained=(!handshake_success && wfc=="registered" && streak>=1 && traffic_since>0 && now-traffic_since>=3 && now-old_event[i]>=event_interval) + printf "%s{", (i>1?",":"") + printf "\"label\":%s,\"ip\":%s,\"node\":%s,\"state\":%s,\"wificalling\":%s,", q(label[i]),q(ip[i]),q(node[i]),q(legacy),q(wfc) + printf "\"epdg_ip\":%s,\"ike_seen\":%s,\"nat_t_seen\":%s,\"assured\":%s,", q(epdg[i]),(ike[i]?"true":"false"),(natt[i]?"true":"false"),(assured[i]?"true":"false") + printf "\"sent_packets\":%d,\"reply_packets\":%d,\"delta_sent\":%d,\"delta_reply\":%d,\"last_activity\":%d,\"activity_evidence\":%s}", sent[i]+0,reply[i]+0,ds,dr,last,q(activity) + if (log_enabled) { + if (handshake_success) { + print now "|" label[i] "|" ip[i] "|handshake_success|" ds "|" dr "|call_or_sms_unknown|" wfc > event_out + old_event[i]=now; acc_sent=0; acc_reply=0 + } else if (handshake_failed) { + print now "|" label[i] "|" ip[i] "|handshake_failed|" ds "|" dr "|call_or_sms_unknown|" wfc > event_out + old_event[i]=now; acc_sent=0; acc_reply=0 + } else if (sustained) { + print now "|" label[i] "|" ip[i] "|sustained_traffic|" acc_sent "|" acc_reply "|call_or_sms_unknown|" wfc > event_out + old_event[i]=now; acc_sent=0; acc_reply=0 + } + } + print label[i] "|" ip[i] "|" wfc "|" sent[i]+0 "|" reply[i]+0 "|" last "|" old_event[i]+0 "|" streak "|" acc_sent "|" acc_reply "|" traffic_since+0 > state_out + } + print "]}" +} +' "$clients" "$state" "$conntrack" > "$tmp" + +cat "$event_tmp" >> "$events" +awk -F '|' -v limit="$max_events" ' +FNR==NR { count[$2 FS $3]++; next } +{ key=$2 FS $3; seen[key]++; if (seen[key] > count[key]-limit) print } +' "$events" "$events" > "$trim_tmp" +mv "$trim_tmp" "$events" +chmod 644 "$tmp" "$events" +chmod 600 "$state_tmp" +mv "$state_tmp" "$state" +mv "$tmp" "$output" +trap - EXIT HUP INT TERM diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/node-health.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/node-health.sh new file mode 100755 index 0000000000..6600113516 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/node-health.sh @@ -0,0 +1,43 @@ +#!/bin/sh +set -eu + +nodes=${1:?node list required} +output=${2:-/var/run/wificalling-gateway/node-status.json} +tmp="${output}.tmp.$$" +trap 'rm -f "$tmp"' EXIT HUP INT TERM + +json_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +{ + printf '{"generated_at":%s,"note":"ICMP ping only; this is not a proxy protocol handshake.","nodes":[' "$(date +%s)" + first=1 + while IFS='|' read -r id label protocol server port; do + [ -n "$id" ] || continue + ping_output=$(ping -c 1 -W 1 "$server" 2>/dev/null || true) + latency=$(printf '%s\n' "$ping_output" | sed -n 's/.*time[=<]\{0,1\}\([0-9][0-9.]*\)[[:space:]]*ms.*/\1/p' | head -n 1) + state=no_icmp_reply; ping_json=null; measurement=icmp + if [ -n "$latency" ]; then + state=reachable; ping_json=$latency + else + case "$protocol" in + anytls|vless|vmess) + measurement=tcp + tcp_output=$(tcping -c 1 -t 1 -p "$port" "$server" 2>/dev/null || true) + latency=$(printf '%s\n' "$tcp_output" | sed -n 's/.*time=\([0-9][0-9.]*\)[[:space:]]*ms.*/\1/p' | head -n 1) + if [ -n "$latency" ]; then state=tcp_reachable; ping_json=$latency; else state=unreachable; fi + ;; + esac + fi + [ "$first" -eq 1 ] || printf ',' + first=0 + printf '{"id":"%s","label":"%s","protocol":"%s","server":"%s","port":%s,"state":"%s","measurement":"%s","ping_ms":%s}' \ + "$(json_escape "$id")" "$(json_escape "$label")" "$(json_escape "$protocol")" \ + "$(json_escape "$server")" "$port" "$state" "$measurement" "$ping_json" + done < "$nodes" + printf ']}\n' +} > "$tmp" +chmod 644 "$tmp" +mv "$tmp" "$output" +trap - EXIT HUP INT TERM diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/passwall-bypass.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/passwall-bypass.sh new file mode 100755 index 0000000000..124cccc4a9 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/passwall-bypass.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -eu + +action=${1:-ensure} +clients=${2:-/var/run/wificalling-gateway/clients} +comment=WFC_GATEWAY_BYPASS + +clear_chain() { + chain=$1 + nft -a list chain inet passwall "$chain" 2>/dev/null | + awk -v marker="$comment" '$0 ~ marker { print $NF }' | + while read -r handle; do + case "$handle" in ''|*[!0-9]*) continue;; esac + nft delete rule inet passwall "$chain" handle "$handle" 2>/dev/null || true + done +} + +nft list table inet passwall >/dev/null 2>&1 || exit 0 + +if [ "$action" = clear ]; then + clear_chain PSW_MANGLE + clear_chain PSW_NAT + exit 0 +fi + +[ -f "$clients" ] || exit 0 +ips=$(awk -F '|' 'NF>=2 { printf "%s%s", (n++?", ":""), $2 }' "$clients") +[ -n "$ips" ] || { "$0" clear "$clients"; exit 0; } + +for chain in PSW_MANGLE PSW_NAT; do + if ! nft list chain inet passwall "$chain" 2>/dev/null | grep -q "$comment"; then + nft insert rule inet passwall "$chain" ip saddr { $ips } counter return comment "$comment" + fi +done diff --git a/applications/luci-app-wificalling-gateway/root/usr/share/luci/menu.d/luci-app-wificalling-gateway.json b/applications/luci-app-wificalling-gateway/root/usr/share/luci/menu.d/luci-app-wificalling-gateway.json new file mode 100644 index 0000000000..15e4b10d4a --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/usr/share/luci/menu.d/luci-app-wificalling-gateway.json @@ -0,0 +1,23 @@ +{ + "admin/services/wificalling-gateway": { + "title": "Wi-Fi Calling Gateway", + "order": 70, + "action": { "type": "firstchild" }, + "depends": { "acl": [ "luci-app-wificalling-gateway" ] } + }, + "admin/services/wificalling-gateway/settings": { + "title": "Settings", + "order": 10, + "action": { "type": "view", "path": "wificalling-gateway/overview" } + }, + "admin/services/wificalling-gateway/status": { + "title": "Wi-Fi Calling Status", + "order": 20, + "action": { "type": "view", "path": "wificalling-gateway/status" } + }, + "admin/services/wificalling-gateway/events": { + "title": "Activity Log", + "order": 30, + "action": { "type": "view", "path": "wificalling-gateway/events" } + } +} diff --git a/applications/luci-app-wificalling-gateway/root/usr/share/rpcd/acl.d/luci-app-wificalling-gateway.json b/applications/luci-app-wificalling-gateway/root/usr/share/rpcd/acl.d/luci-app-wificalling-gateway.json new file mode 100644 index 0000000000..78f8007441 --- /dev/null +++ b/applications/luci-app-wificalling-gateway/root/usr/share/rpcd/acl.d/luci-app-wificalling-gateway.json @@ -0,0 +1,25 @@ +{ + "luci-app-wificalling-gateway": { + "description": "Manage Wi-Fi Calling Gateway", + "read": { + "uci": [ "wificalling-gateway" ], + "ubus": { "file": [ "read" ] }, + "file": { + "/var/run/wificalling-gateway/status.json": [ "read" ], + "/var/run/wificalling-gateway/node-status.json": [ "read" ], + "/var/run/wificalling-gateway/events.log": [ "read" ], + "/tmp/run/wificalling-gateway/status.json": [ "read" ], + "/tmp/run/wificalling-gateway/node-status.json": [ "read" ], + "/tmp/run/wificalling-gateway/events.log": [ "read" ] + } + }, + "write": { + "uci": [ "wificalling-gateway" ], + "ubus": { "file": [ "write" ] }, + "file": { + "/var/run/wificalling-gateway/events.log": [ "write" ], + "/tmp/run/wificalling-gateway/events.log": [ "write" ] + } + } + } +} From d05d0c91029c160536246f0051db0aac9ffdd55c Mon Sep 17 00:00:00 2001 From: Smth Dagg Date: Sun, 9 Aug 2026 10:01:43 +0800 Subject: [PATCH 2/8] luci-app-wificalling-gateway: fix review issues - Drop tcping dependency (not in feeds); make TCP probe optional. - VLESS/VMess: emit TLS block for non-Reality TLS; fix alter_id JSON. - Add security form field for VLESS; mask UUID in UI. - Check firewall exit status; add device delimiter guard. - firewall.sh: exit 0 on empty clients; remove unused monitor_interval. Signed-off-by: Smth Dagg --- .../luci-app-wificalling-gateway/Makefile | 2 +- .../luci-app-wificalling-gateway/README.md | 23 ++++--------------- .../view/wificalling-gateway/overview.js | 6 ++++- .../root/etc/config/wificalling-gateway | 1 - .../root/etc/init.d/wificalling-gateway | 6 +++-- .../libexec/wificalling-gateway/compiler.sh | 4 +++- .../libexec/wificalling-gateway/firewall.sh | 2 +- .../wificalling-gateway/node-health.sh | 10 ++++---- 8 files changed, 25 insertions(+), 29 deletions(-) diff --git a/applications/luci-app-wificalling-gateway/Makefile b/applications/luci-app-wificalling-gateway/Makefile index f882fb741e..d6aa14098d 100644 --- a/applications/luci-app-wificalling-gateway/Makefile +++ b/applications/luci-app-wificalling-gateway/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI support for per-device Wi-Fi Calling gateway LUCI_URL:=https://github.com/smthdagg/luci-app-wificalling-gateway -LUCI_DEPENDS:=+luci-base +sing-box +firewall4 +kmod-nft-tproxy +kmod-nft-socket +ip-full +tcping +LUCI_DEPENDS:=+luci-base +sing-box +firewall4 +kmod-nft-tproxy +kmod-nft-socket +ip-full LUCI_PKGARCH:=all PKG_LICENSE:=MIT diff --git a/applications/luci-app-wificalling-gateway/README.md b/applications/luci-app-wificalling-gateway/README.md index 7af65df471..ad83ce2d26 100644 --- a/applications/luci-app-wificalling-gateway/README.md +++ b/applications/luci-app-wificalling-gateway/README.md @@ -2,22 +2,9 @@ Per-device transparent Wi-Fi Calling gateway for OpenWrt / ImmortalWrt. -It forwards selected LAN clients through a selected sing-box node (AnyTLS, Hysteria2, -TUIC, VLESS Reality, VMess WebSocket) via a single sing-box process with nftables TPROXY, -while other clients keep the normal gateway or PassWall policy. It observes ePDG/IPsec -UDP 500/4500 evidence and records handshake outcomes and sustained encrypted -communication in an encrypted IMS activity log (toggleable). +Routes selected LAN clients through a sing-box node (AnyTLS, Hysteria2, +TUIC, VLESS Reality, VMess WebSocket) with nftables TPROXY, observes +ePDG/IPsec UDP 500/4500 evidence, and records handshake outcomes in an +encrypted IMS activity log. -Maintainer docs, install guide and build instructions: -https://github.com/smthdagg/luci-app-wificalling-gateway - -## Install - -```sh -opkg update -opkg install luci-app-wificalling-gateway -/etc/init.d/rpcd restart -``` - -Then open **Services -> Wi-Fi Calling Gateway**. Save a node first, then create a -device policy. Devices must have static DHCP leases. +See https://github.com/smthdagg/luci-app-wificalling-gateway for full docs. diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js index 5728d27008..d5cf98e9f5 100644 --- a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js @@ -99,8 +99,12 @@ return view.extend({ nodeQuality.textvalue = function(id) { return E('span', { id: 'wfc-node-quality-' + id }, quality(nodeById(id))); }; var secret = s.option(form.Value, 'password', _('Password')); secret.password = true; secret.textvalue = function(id) { return this.cfgvalue(id) ? _('Set') : _('Not set'); }; - s.option(form.Value, 'uuid', _('UUID')); + var uuidField = s.option(form.Value, 'uuid', _('UUID')); + uuidField.password = true; uuidField.textvalue = function(id) { return this.cfgvalue(id) ? _('Set') : _('Not set'); }; s.option(form.Value, 'sni', _('TLS server name')); + var securityOpt = s.option(form.ListValue, 'security', _('Security')); + securityOpt.value(''); securityOpt.value('tls'); securityOpt.value('reality'); + securityOpt.depends('protocol', 'vless'); s.option(form.Flag, 'insecure', _('Allow insecure certificate')); s.option(form.Value, 'alpn', _('ALPN')); s.option(form.Value, 'pin_sha256', _('TLS public-key SHA-256 (base64)')); diff --git a/applications/luci-app-wificalling-gateway/root/etc/config/wificalling-gateway b/applications/luci-app-wificalling-gateway/root/etc/config/wificalling-gateway index 7711eea8bf..7397e39f42 100644 --- a/applications/luci-app-wificalling-gateway/root/etc/config/wificalling-gateway +++ b/applications/luci-app-wificalling-gateway/root/etc/config/wificalling-gateway @@ -1,7 +1,6 @@ config global 'main' option enabled '0' option log_level 'warn' - option monitor_interval '5' option event_interval '60' option max_events_per_device '20' option log_enabled '1' diff --git a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway index c81051bea5..06a6a8e630 100755 --- a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway +++ b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway @@ -21,7 +21,8 @@ append_node() { case "$label$protocol$server$password$sni$uuid$public_key$short_id$host" in *'|'*) logger -t "$APP" "invalid delimiter in node $s"; return 1;; esac credential=$password; auxiliary=$uuid case "$protocol" in - vless|vmess) credential=$uuid; auxiliary=${flow:-$alter_id} ;; + vless) credential=$uuid; auxiliary=${flow:-$alter_id} ;; + vmess) credential=$uuid; auxiliary=$alter_id ;; esac printf 'node|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$s" "$protocol" "$server" "$port" "$credential" "$sni" "$insecure" "$alpn" "$auxiliary" "$congestion" "$udp_mode" "$public_key" "$short_id" "$fingerprint" "$security" "$transport" "$path" "$host" "$pin_sha256" >> "$RUNDIR/normalized.conf" printf '%s|%s|%s|%s|%s\n' "$s" "$label" "$protocol" "$server" "$port" >> "$RUNDIR/nodes" @@ -32,6 +33,7 @@ append_device() { local s="$1" enabled label node route_mode config_get_bool enabled "$s" enabled 1; [ "$enabled" -eq 1 ] || return 0 config_get label "$s" label "$s"; config_get node "$s" node; config_get route_mode "$s" route_mode independent + case "$label$node" in *'|'*) logger -t "$APP" "invalid delimiter in device $s"; return 1;; esac DEVICE_IPS=""; config_list_foreach "$s" source_ip append_ip [ "$route_mode" = independent ] || return 0 printf 'device|%s|%s|%s\n' "$label" "$node" "$DEVICE_IPS" >> "$RUNDIR/normalized.conf" @@ -53,7 +55,7 @@ start_service() { config_foreach append_node node; config_foreach append_device device /usr/libexec/$APP/compiler.sh "$RUNDIR/normalized.conf" "$RUNDIR/sing-box.json" || return 1 /usr/bin/sing-box check -c "$RUNDIR/sing-box.json" || { logger -t "$APP" "sing-box rejected generated configuration"; return 1; } - /usr/libexec/$APP/firewall.sh start "$RUNDIR/clients" + /usr/libexec/$APP/firewall.sh start "$RUNDIR/clients" || { logger -t "$APP" "firewall setup failed"; return 1; } procd_open_instance sing-box procd_set_param command /usr/bin/sing-box run -c "$RUNDIR/sing-box.json" procd_set_param respawn 3600 5 5 diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh index b5a56b278f..e220b28d9d 100755 --- a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh @@ -58,10 +58,12 @@ END { s=s ",\"uuid\":" q(f[6]) if (f[10]!="") s=s ",\"flow\":" q(f[10]) if (f[16]=="reality") s=s ",\"tls\":{\"enabled\":true,\"server_name\":" q(f[7]) ",\"reality\":{\"enabled\":true,\"public_key\":" q(f[13]) ",\"short_id\":" q(f[14]) "},\"utls\":{\"enabled\":true,\"fingerprint\":" q(f[15]?f[15]:"chrome") "}}" + else if (f[16]=="tls"||f[7]!="") s=s ",\"tls\":" tls(f[7],f[8],f[9],f[20]) } if (p=="vmess") { - s=s ",\"uuid\":" q(f[6]) ",\"security\":\"auto\",\"alter_id\":" (f[10]?f[10]:0) + s=s ",\"uuid\":" q(f[6]) ",\"security\":\"auto\",\"alter_id\":" (f[10]~/^[0-9]+$/?f[10]:0) if (f[17]=="ws") s=s ",\"transport\":{\"type\":\"ws\",\"path\":" q(f[18]) ",\"headers\":{\"Host\":" q(f[19]) "}}" + if (f[7]!="") s=s ",\"tls\":" tls(f[7],f[8],f[9],f[20]) } s=s "}"; print " " s "," } diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/firewall.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/firewall.sh index 8972ea5f5c..cf41040df1 100755 --- a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/firewall.sh +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/firewall.sh @@ -6,7 +6,7 @@ bypass_helper="${0%/*}/passwall-bypass.sh" [ "$action" = stop ] && { "$bypass_helper" clear "$clients"; nft delete table $table 2>/dev/null || true; ip rule del fwmark 0x66 table 166 2>/dev/null || true; ip route flush table 166 2>/dev/null || true; exit 0; } ips=$(awk -F '|' 'NF>=2 { printf "%s%s", (n++?", ":""), $2 }' "$clients") -[ -n "$ips" ] || exit 1 +[ -n "$ips" ] || exit 0 nft delete table $table 2>/dev/null || true nft -f - </dev/null || true) - latency=$(printf '%s\n' "$tcp_output" | sed -n 's/.*time=\([0-9][0-9.]*\)[[:space:]]*ms.*/\1/p' | head -n 1) - if [ -n "$latency" ]; then state=tcp_reachable; ping_json=$latency; else state=unreachable; fi + if command -v tcping >/dev/null 2>&1; then + measurement=tcp + tcp_output=$(tcping -c 1 -t 1 -p "$port" "$server" 2>/dev/null || true) + latency=$(printf '%s\n' "$tcp_output" | sed -n 's/.*time=\([0-9][0-9.]*\)[[:space:]]*ms.*/\1/p' | head -n 1) + if [ -n "$latency" ]; then state=tcp_reachable; ping_json=$latency; else state=unreachable; fi + fi ;; esac fi From 0001de767cd12ba3695d0b49595f2aaddf8cea76 Mon Sep 17 00:00:00 2001 From: Smth Dagg Date: Sun, 9 Aug 2026 15:03:22 +0800 Subject: [PATCH 3/8] luci-app-wificalling-gateway: fix second review round - VLESS: auxiliary=$flow (not ${flow:-$alter_id}) so empty flow does not emit "flow":"0" which sing-box rejects. - VMess: TLS gated on security field (f[16]=="tls") in addition to SNI; parseVmess reads raw.tls from the share link and sets security. - firewall: on startup failure, call firewall.sh stop to clean up the TPROXY table before aborting (prevents blackhole with no listener). Signed-off-by: Smth Dagg --- .../luci-static/resources/wificalling-gateway/node-import.js | 3 ++- .../root/etc/init.d/wificalling-gateway | 4 ++-- .../root/usr/libexec/wificalling-gateway/compiler.sh | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js index 841095cf10..d32259c639 100644 --- a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js @@ -64,7 +64,8 @@ function parseVmess(uri) { var out = { enabled: '1', protocol: 'vmess', label: raw.ps || 'VMess ' + raw.add, server: raw.add, port: String(raw.port), uuid: raw.id, alter_id: String(raw.aid || 0), - sni: raw.sni || '', host: raw.host || '', path: raw.path || '' + sni: raw.sni || '', host: raw.host || '', path: raw.path || '', + security: raw.tls === 'tls' ? 'tls' : '' }; if (raw.net === 'ws') out.transport = 'ws'; return out; diff --git a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway index 06a6a8e630..59533ffbc9 100755 --- a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway +++ b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway @@ -21,7 +21,7 @@ append_node() { case "$label$protocol$server$password$sni$uuid$public_key$short_id$host" in *'|'*) logger -t "$APP" "invalid delimiter in node $s"; return 1;; esac credential=$password; auxiliary=$uuid case "$protocol" in - vless) credential=$uuid; auxiliary=${flow:-$alter_id} ;; + vless) credential=$uuid; auxiliary=$flow ;; vmess) credential=$uuid; auxiliary=$alter_id ;; esac printf 'node|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$s" "$protocol" "$server" "$port" "$credential" "$sni" "$insecure" "$alpn" "$auxiliary" "$congestion" "$udp_mode" "$public_key" "$short_id" "$fingerprint" "$security" "$transport" "$path" "$host" "$pin_sha256" >> "$RUNDIR/normalized.conf" @@ -55,7 +55,7 @@ start_service() { config_foreach append_node node; config_foreach append_device device /usr/libexec/$APP/compiler.sh "$RUNDIR/normalized.conf" "$RUNDIR/sing-box.json" || return 1 /usr/bin/sing-box check -c "$RUNDIR/sing-box.json" || { logger -t "$APP" "sing-box rejected generated configuration"; return 1; } - /usr/libexec/$APP/firewall.sh start "$RUNDIR/clients" || { logger -t "$APP" "firewall setup failed"; return 1; } + /usr/libexec/$APP/firewall.sh start "$RUNDIR/clients" || { logger -t "$APP" "firewall setup failed"; /usr/libexec/$APP/firewall.sh stop "$RUNDIR/clients"; return 1; } procd_open_instance sing-box procd_set_param command /usr/bin/sing-box run -c "$RUNDIR/sing-box.json" procd_set_param respawn 3600 5 5 diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh index e220b28d9d..7edcba47ff 100755 --- a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh @@ -63,7 +63,7 @@ END { if (p=="vmess") { s=s ",\"uuid\":" q(f[6]) ",\"security\":\"auto\",\"alter_id\":" (f[10]~/^[0-9]+$/?f[10]:0) if (f[17]=="ws") s=s ",\"transport\":{\"type\":\"ws\",\"path\":" q(f[18]) ",\"headers\":{\"Host\":" q(f[19]) "}}" - if (f[7]!="") s=s ",\"tls\":" tls(f[7],f[8],f[9],f[20]) + if (f[16]=="tls"||f[7]!="") s=s ",\"tls\":" tls(f[7],f[8],f[9],f[20]) } s=s "}"; print " " s "," } From d2c76649e79efd93b2e414f97860aba689b7db86 Mon Sep 17 00:00:00 2001 From: Smth Dagg Date: Sun, 9 Aug 2026 18:40:45 +0800 Subject: [PATCH 4/8] luci-app-wificalling-gateway: label empty security option as "None" nit fix: value('') rendered as a blank dropdown row; now value('', _('None')). Signed-off-by: Smth Dagg --- .../luci-static/resources/view/wificalling-gateway/overview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js index d5cf98e9f5..24e991fd72 100644 --- a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js @@ -103,7 +103,7 @@ return view.extend({ uuidField.password = true; uuidField.textvalue = function(id) { return this.cfgvalue(id) ? _('Set') : _('Not set'); }; s.option(form.Value, 'sni', _('TLS server name')); var securityOpt = s.option(form.ListValue, 'security', _('Security')); - securityOpt.value(''); securityOpt.value('tls'); securityOpt.value('reality'); + securityOpt.value('', _('None')); securityOpt.value('tls'); securityOpt.value('reality'); securityOpt.depends('protocol', 'vless'); s.option(form.Flag, 'insecure', _('Allow insecure certificate')); s.option(form.Value, 'alpn', _('ALPN')); From 95913b271ecc5688e078f3c7008929d63c9e660c Mon Sep 17 00:00:00 2001 From: Smth Dagg Date: Mon, 10 Aug 2026 10:14:04 +0800 Subject: [PATCH 5/8] luci-app-wificalling-gateway: fix third review round - security: depends on vless AND vmess (preserves imported tls across edits) - tls(): omit server_name when empty (sing-box uses server address) - delimiter guard: add $flow to checked fields Signed-off-by: Smth Dagg --- .../resources/view/wificalling-gateway/overview.js | 1 + .../root/etc/init.d/wificalling-gateway | 2 +- .../root/usr/libexec/wificalling-gateway/compiler.sh | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js index 24e991fd72..9981f9ee8b 100644 --- a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js @@ -105,6 +105,7 @@ return view.extend({ var securityOpt = s.option(form.ListValue, 'security', _('Security')); securityOpt.value('', _('None')); securityOpt.value('tls'); securityOpt.value('reality'); securityOpt.depends('protocol', 'vless'); + securityOpt.depends('protocol', 'vmess'); s.option(form.Flag, 'insecure', _('Allow insecure certificate')); s.option(form.Value, 'alpn', _('ALPN')); s.option(form.Value, 'pin_sha256', _('TLS public-key SHA-256 (base64)')); diff --git a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway index 59533ffbc9..676917c481 100755 --- a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway +++ b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway @@ -18,7 +18,7 @@ append_node() { config_get security "$s" security; config_get transport "$s" transport; config_get path "$s" path; config_get host "$s" host config_get pin_sha256 "$s" pin_sha256 config_get flow "$s" flow; config_get alter_id "$s" alter_id 0 - case "$label$protocol$server$password$sni$uuid$public_key$short_id$host" in *'|'*) logger -t "$APP" "invalid delimiter in node $s"; return 1;; esac + case "$label$protocol$server$password$sni$uuid$public_key$short_id$host$flow" in *'|'*) logger -t "$APP" "invalid delimiter in node $s"; return 1;; esac credential=$password; auxiliary=$uuid case "$protocol" in vless) credential=$uuid; auxiliary=$flow ;; diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh index 7edcba47ff..f6376295cc 100755 --- a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh @@ -16,7 +16,9 @@ function private4(ip, a) { return a[1]==10 || (a[1]==172 && a[2]>=16 && a[2]<=31) || (a[1]==192 && a[2]==168) } function tls(sni, insecure, alpn, pin, extra) { - extra="\"enabled\":true,\"server_name\":" q(sni) ",\"insecure\":" (insecure=="1"?"true":"false") + extra="\"enabled\":true" + if (sni!="") extra=extra ",\"server_name\":" q(sni) + extra=extra ",\"insecure\":" (insecure=="1"?"true":"false") if (alpn!="") extra=extra ",\"alpn\":[" q(alpn) "]" if (pin!="") extra=extra ",\"certificate_public_key_sha256\":[" q(pin) "]" return "{" extra "}" From 88326361d2eacd91ab4b7786409d4debcd5d9e3b Mon Sep 17 00:00:00 2001 From: Smth Dagg Date: Mon, 10 Aug 2026 11:40:01 +0800 Subject: [PATCH 6/8] luci-app-wificalling-gateway: clear status.json and monitor.state on start Fix stale status page after device add/modify: clear both files at startup so the status page shows only current configuration, not leftover data from the previous run. Signed-off-by: Smth Dagg --- .../root/etc/init.d/wificalling-gateway | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway index 676917c481..0611354694 100755 --- a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway +++ b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway @@ -43,7 +43,7 @@ append_device() { start_service() { chmod 600 "/etc/config/$APP" 2>/dev/null || true config_load "$APP"; config_get_bool enabled main enabled 0; [ "$enabled" -eq 1 ] || return 0 - mkdir -p "$RUNDIR"; chmod 700 "$RUNDIR"; : > "$RUNDIR/normalized.conf"; : > "$RUNDIR/clients"; : > "$RUNDIR/nodes" + mkdir -p "$RUNDIR"; chmod 700 "$RUNDIR"; : > "$RUNDIR/normalized.conf"; : > "$RUNDIR/clients"; : > "$RUNDIR/nodes"; : > "$RUNDIR/status.json"; : > "$RUNDIR/monitor.state" config_get log_level main log_level warn; printf 'global|log_level|%s\n' "$log_level" >> "$RUNDIR/normalized.conf" config_get event_interval main event_interval 60 config_get max_events_per_device main max_events_per_device 20 From dbabc367ef05cd727317b661584ba8012bb0e9ca Mon Sep 17 00:00:00 2001 From: Smth Dagg Date: Mon, 10 Aug 2026 15:00:35 +0800 Subject: [PATCH 7/8] luci-app-wificalling-gateway: fix fourth review round - init.d: stop truncating monitor.state on start (it is the monitor's per-device baseline; clearing it fabricated handshake_success events on the first tick after every restart). Only status.json is cleared. - init.d: extend the delimiter guard to every field interpolated into the | -delimited record (alpn, congestion, udp_mode, fingerprint, security, transport, path, pin_sha256). - init.d: also clear status.json in stop_service for the stopped case. - compiler.sh: when sni (f[7]) is empty, fall back to the WS Host (f[19]) as server_name for VLESS and VMess; the TLS decision itself still uses the original sni so plain ws stays cleartext. - overview.js: reject Security = reality on VMess (no compiler arm). Signed-off-by: Smth Dagg --- .../resources/view/wificalling-gateway/overview.js | 7 +++++++ .../root/etc/init.d/wificalling-gateway | 14 +++++++++++--- .../usr/libexec/wificalling-gateway/compiler.sh | 11 +++++++++-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js index 9981f9ee8b..fded6f2d80 100644 --- a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js @@ -106,6 +106,13 @@ return view.extend({ securityOpt.value('', _('None')); securityOpt.value('tls'); securityOpt.value('reality'); securityOpt.depends('protocol', 'vless'); securityOpt.depends('protocol', 'vmess'); + // The compiler has no reality arm for VMess; selecting it would emit a + // cleartext outbound that sing-box check accepts. Reject it up front. + securityOpt.validate = function(section_id, value) { + if (value == 'reality' && this.map.getSectionValue(section_id, 'protocol') == 'vmess') + return false; + return true; + }; s.option(form.Flag, 'insecure', _('Allow insecure certificate')); s.option(form.Value, 'alpn', _('ALPN')); s.option(form.Value, 'pin_sha256', _('TLS public-key SHA-256 (base64)')); diff --git a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway index 0611354694..34df840254 100755 --- a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway +++ b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway @@ -18,7 +18,7 @@ append_node() { config_get security "$s" security; config_get transport "$s" transport; config_get path "$s" path; config_get host "$s" host config_get pin_sha256 "$s" pin_sha256 config_get flow "$s" flow; config_get alter_id "$s" alter_id 0 - case "$label$protocol$server$password$sni$uuid$public_key$short_id$host$flow" in *'|'*) logger -t "$APP" "invalid delimiter in node $s"; return 1;; esac + case "$label$protocol$server$password$sni$uuid$public_key$short_id$host$flow$alpn$path$fingerprint$pin_sha256$security$transport$congestion$udp_mode" in *'|'*) logger -t "$APP" "invalid delimiter in node $s"; return 1;; esac credential=$password; auxiliary=$uuid case "$protocol" in vless) credential=$uuid; auxiliary=$flow ;; @@ -43,7 +43,10 @@ append_device() { start_service() { chmod 600 "/etc/config/$APP" 2>/dev/null || true config_load "$APP"; config_get_bool enabled main enabled 0; [ "$enabled" -eq 1 ] || return 0 - mkdir -p "$RUNDIR"; chmod 700 "$RUNDIR"; : > "$RUNDIR/normalized.conf"; : > "$RUNDIR/clients"; : > "$RUNDIR/nodes"; : > "$RUNDIR/status.json"; : > "$RUNDIR/monitor.state" + # Clear display state only. monitor.state is the monitor's per-device + # baseline (old_wfc/old_sent/old_reply); truncating it would fabricate + # handshake_success events on the first tick after every restart. + mkdir -p "$RUNDIR"; chmod 700 "$RUNDIR"; : > "$RUNDIR/normalized.conf"; : > "$RUNDIR/clients"; : > "$RUNDIR/nodes"; : > "$RUNDIR/status.json" config_get log_level main log_level warn; printf 'global|log_level|%s\n' "$log_level" >> "$RUNDIR/normalized.conf" config_get event_interval main event_interval 60 config_get max_events_per_device main max_events_per_device 20 @@ -67,7 +70,12 @@ start_service() { procd_close_instance } -stop_service() { /usr/libexec/$APP/firewall.sh stop "$RUNDIR/clients"; } +stop_service() { + /usr/libexec/$APP/firewall.sh stop "$RUNDIR/clients" + # Also drop the stale snapshot when the gateway is stopped/disabled so + # the status page does not keep rendering the last state indefinitely. + : > "$RUNDIR/status.json" 2>/dev/null || true +} reload_service() { restart; } service_triggers() { procd_add_reload_trigger "$APP"; } diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh index f6376295cc..4fc838bc8e 100755 --- a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh @@ -60,12 +60,19 @@ END { s=s ",\"uuid\":" q(f[6]) if (f[10]!="") s=s ",\"flow\":" q(f[10]) if (f[16]=="reality") s=s ",\"tls\":{\"enabled\":true,\"server_name\":" q(f[7]) ",\"reality\":{\"enabled\":true,\"public_key\":" q(f[13]) ",\"short_id\":" q(f[14]) "},\"utls\":{\"enabled\":true,\"fingerprint\":" q(f[15]?f[15]:"chrome") "}}" - else if (f[16]=="tls"||f[7]!="") s=s ",\"tls\":" tls(f[7],f[8],f[9],f[20]) + # TLS is decided by security/sni alone; only the server_name falls + # back to the WS Host (f[19]) when sni (f[7]) is empty, so a plain + # ws node with a Host header but no TLS never gains a tls block. + else if (f[16]=="tls"||f[7]!="") s=s ",\"tls\":" tls((f[7]!=""?f[7]:f[19]),f[8],f[9],f[20]) } if (p=="vmess") { s=s ",\"uuid\":" q(f[6]) ",\"security\":\"auto\",\"alter_id\":" (f[10]~/^[0-9]+$/?f[10]:0) if (f[17]=="ws") s=s ",\"transport\":{\"type\":\"ws\",\"path\":" q(f[18]) ",\"headers\":{\"Host\":" q(f[19]) "}}" - if (f[16]=="tls"||f[7]!="") s=s ",\"tls\":" tls(f[7],f[8],f[9],f[20]) + # Imported VMess links carry the TLS name in the WS Host (f[19]) when + # sni (f[7]) is empty and the server is a bare IP; fall back to it so + # certificate verification has a name to check. Plain ws nodes with + # a Host header but no TLS stay cleartext. + if (f[16]=="tls"||f[7]!="") s=s ",\"tls\":" tls((f[7]!=""?f[7]:f[19]),f[8],f[9],f[20]) } s=s "}"; print " " s "," } From 5ee9f146a5a0acbd6f3d4f92483c734c39afc8be Mon Sep 17 00:00:00 2001 From: Smth Dagg Date: Tue, 11 Aug 2026 10:43:08 +0800 Subject: [PATCH 8/8] luci-app-wificalling-gateway: add Trojan and WireGuard node protocols - compiler: trojan outbound (password + TLS); wireguard endpoint form for sing-box >= 1.11 and legacy outbound for 1.10.x, selected from the installed sing-box version at service start (the legacy wireguard outbound was removed in 1.13.0) - LuCI node form: wireguard private key / local address / reserved / MTU - import: trojan:// and wg:// share links (Clash Meta / sing-box style) - node-health: tcping fallback for trojan (TCP-based) - i18n: new strings with Simplified Chinese translations Signed-off-by: Smth Dagg --- .../luci-app-wificalling-gateway/README.md | 6 +-- .../view/wificalling-gateway/overview.js | 9 ++++- .../wificalling-gateway/node-import.js | 12 +++++- .../po/templates/wificalling-gateway.pot | 14 ++++++- .../po/zh_Hans/wificalling-gateway.po | 16 +++++++- .../root/etc/init.d/wificalling-gateway | 21 ++++++++-- .../libexec/wificalling-gateway/compiler.sh | 40 +++++++++++++++++-- .../wificalling-gateway/node-health.sh | 2 +- 8 files changed, 102 insertions(+), 18 deletions(-) diff --git a/applications/luci-app-wificalling-gateway/README.md b/applications/luci-app-wificalling-gateway/README.md index ad83ce2d26..422d39a6e4 100644 --- a/applications/luci-app-wificalling-gateway/README.md +++ b/applications/luci-app-wificalling-gateway/README.md @@ -3,8 +3,8 @@ Per-device transparent Wi-Fi Calling gateway for OpenWrt / ImmortalWrt. Routes selected LAN clients through a sing-box node (AnyTLS, Hysteria2, -TUIC, VLESS Reality, VMess WebSocket) with nftables TPROXY, observes -ePDG/IPsec UDP 500/4500 evidence, and records handshake outcomes in an -encrypted IMS activity log. +TUIC, VLESS Reality, VMess WebSocket, Trojan, WireGuard) with nftables +TPROXY, observes ePDG/IPsec UDP 500/4500 evidence, and records handshake +outcomes in an encrypted IMS activity log. See https://github.com/smthdagg/luci-app-wificalling-gateway for full docs. diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js index fded6f2d80..9e4a57812a 100644 --- a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/view/wificalling-gateway/overview.js @@ -44,7 +44,7 @@ return view.extend({ _('Configure proxy nodes and assign fixed LAN devices. Monitoring and logs are available from the submenu.')); var importPanel = E('div', { class: 'cbi-section' }, [ E('h3', {}, _('Import proxy node')), - E('p', {}, _('Paste one AnyTLS, Hysteria2/Hy2, TUIC, VLESS, or VMess link. It is parsed locally in this browser and is not sent to an external service.')), + E('p', {}, _('Paste one AnyTLS, Hysteria2/Hy2, TUIC, VLESS, VMess, Trojan, or WireGuard (wg://) link. It is parsed locally in this browser and is not sent to an external service.')), E('button', { class: 'btn cbi-button-positive', click: function() { var input = E('textarea', { class: 'cbi-input-textarea', rows: 6, style: 'width:100%', placeholder: 'anytls://…' }); ui.showModal(_('Import node link'), [input, E('div', { class: 'right' }, [ @@ -88,7 +88,7 @@ return view.extend({ nodeLabel.rmempty = false; nodeLabel.placeholder = _('Example: UK AnyTLS'); nodeLabel.description = _('This name is shown in the device node selector.'); var p = s.option(form.ListValue, 'protocol', _('Protocol')); - ['anytls','hysteria2','tuic','vless','vmess'].forEach(function(x) { p.value(x); }); + ['anytls','hysteria2','tuic','vless','vmess','trojan','wireguard'].forEach(function(x) { p.value(x); }); s.option(form.Value, 'server', _('Server')).datatype = 'host'; s.option(form.Value, 'port', _('Port')).datatype = 'port'; var nodeStatus = s.option(form.DummyValue, '_node_status', _('Node status')); @@ -126,6 +126,11 @@ return view.extend({ transport.value('', _('None')); transport.value('ws', _('WebSocket')); s.option(form.Value, 'path', _('WebSocket path')); s.option(form.Value, 'host', _('WebSocket Host')); + var wgKey = s.option(form.Value, 'private_key', _('WireGuard private key')); + wgKey.password = true; wgKey.textvalue = function(id) { return this.cfgvalue(id) ? _('Set') : _('Not set'); }; + s.option(form.Value, 'local_address', _('WireGuard local address')); + s.option(form.Value, 'reserved', _('WireGuard reserved (comma-separated)')); + s.option(form.Value, 'mtu', _('WireGuard MTU')); s = m.section(form.GridSection, 'device', _('Device policies')); s.addremove = true; s.nodescriptions = true; s.anonymous = true; s.addbtntitle = _('Add LAN device'); diff --git a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js index d32259c639..c0f7c98cac 100644 --- a/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js +++ b/applications/luci-app-wificalling-gateway/htdocs/luci-static/resources/wificalling-gateway/node-import.js @@ -27,7 +27,7 @@ function common(protocol, url) { function parseUrl(uri, protocol) { var url = new URL(uri), p = url.searchParams, out = common(protocol, url); - if (protocol === 'anytls' || protocol === 'hysteria2') { + if (protocol === 'anytls' || protocol === 'hysteria2' || protocol === 'trojan') { out.password = decodeURIComponent(url.username || ''); out.sni = p.get('peer') || p.get('sni') || ''; out.insecure = truthy(p.get('insecure') || p.get('allowInsecure')); @@ -54,6 +54,13 @@ function parseUrl(uri, protocol) { if (p.get('type') === 'ws') { out.transport = 'ws'; out.path = p.get('path') || '/'; out.host = p.get('host') || ''; } + } else if (protocol === 'wireguard') { + // wg://@:?private_key=…&local_address=…&reserved=…&mtu=… + out.public_key = decodeURIComponent(url.username || ''); + out.private_key = p.get('private_key') || ''; + out.local_address = (p.get('local_address') || p.get('ip') || '').split(',')[0] || ''; + out.reserved = p.get('reserved') || ''; + out.mtu = p.get('mtu') || ''; } return out; } @@ -75,7 +82,8 @@ function parse(uri) { var value = (uri || '').trim(), scheme = value.split(':', 1)[0].toLowerCase(); if (scheme === 'vmess') return parseVmess(value); if (scheme === 'hy2') scheme = 'hysteria2'; - if (['anytls', 'hysteria2', 'tuic', 'vless'].indexOf(scheme) < 0) + if (scheme === 'wg') scheme = 'wireguard'; + if (['anytls', 'hysteria2', 'tuic', 'vless', 'trojan', 'wireguard'].indexOf(scheme) < 0) throw new Error(_('Unsupported node link format')); return parseUrl(value, scheme); } diff --git a/applications/luci-app-wificalling-gateway/po/templates/wificalling-gateway.pot b/applications/luci-app-wificalling-gateway/po/templates/wificalling-gateway.pot index 047ce5288c..30e192d825 100644 --- a/applications/luci-app-wificalling-gateway/po/templates/wificalling-gateway.pot +++ b/applications/luci-app-wificalling-gateway/po/templates/wificalling-gateway.pot @@ -194,7 +194,7 @@ msgstr "" msgid "Password" msgstr "" -msgid "Paste one AnyTLS, Hysteria2/Hy2, TUIC, VLESS, or VMess link. It is parsed locally in this browser and is not sent to an external service." +msgid "Paste one AnyTLS, Hysteria2/Hy2, TUIC, VLESS, VMess, Trojan, or WireGuard (wg://) link. It is parsed locally in this browser and is not sent to an external service." msgstr "" msgid "Ping / latency" @@ -329,6 +329,18 @@ msgstr "" msgid "Wi-Fi Calling" msgstr "" +msgid "WireGuard MTU" +msgstr "" + +msgid "WireGuard local address" +msgstr "" + +msgid "WireGuard private key" +msgstr "" + +msgid "WireGuard reserved (comma-separated)" +msgstr "" + msgid "Wi-Fi Calling Gateway settings" msgstr "" diff --git a/applications/luci-app-wificalling-gateway/po/zh_Hans/wificalling-gateway.po b/applications/luci-app-wificalling-gateway/po/zh_Hans/wificalling-gateway.po index 377fb5ba33..de3a4ea43b 100644 --- a/applications/luci-app-wificalling-gateway/po/zh_Hans/wificalling-gateway.po +++ b/applications/luci-app-wificalling-gateway/po/zh_Hans/wificalling-gateway.po @@ -195,8 +195,8 @@ msgstr "数据包" msgid "Password" msgstr "密码" -msgid "Paste one AnyTLS, Hysteria2/Hy2, TUIC, VLESS, or VMess link. It is parsed locally in this browser and is not sent to an external service." -msgstr "粘贴一个 AnyTLS、Hysteria2/Hy2、TUIC、VLESS 或 VMess 链接。链接仅在本浏览器中本地解析,不会发送到外部服务。" +msgid "Paste one AnyTLS, Hysteria2/Hy2, TUIC, VLESS, VMess, Trojan, or WireGuard (wg://) link. It is parsed locally in this browser and is not sent to an external service." +msgstr "粘贴一个 AnyTLS、Hysteria2/Hy2、TUIC、VLESS、VMess、Trojan 或 WireGuard (wg://) 链接。链接仅在本浏览器中本地解析,不会发送到外部服务。" msgid "Ping / latency" msgstr "Ping / 延迟" @@ -330,6 +330,18 @@ msgstr "WebSocket 路径" msgid "Wi-Fi Calling" msgstr "Wi-Fi Calling" +msgid "WireGuard MTU" +msgstr "WireGuard MTU" + +msgid "WireGuard local address" +msgstr "WireGuard 本地地址" + +msgid "WireGuard private key" +msgstr "WireGuard 私钥" + +msgid "WireGuard reserved (comma-separated)" +msgstr "WireGuard 保留位(逗号分隔)" + msgid "Wi-Fi Calling Gateway settings" msgstr "Wi-Fi Calling Gateway 设置" diff --git a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway index 34df840254..fbf14ab348 100755 --- a/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway +++ b/applications/luci-app-wificalling-gateway/root/etc/init.d/wificalling-gateway @@ -6,8 +6,19 @@ STOP=10 APP=wificalling-gateway RUNDIR=/var/run/$APP +# sing-box removed the wireguard OUTBOUND in 1.13.0 (deprecated in 1.11.0, +# gated behind ENABLE_DEPRECATED_WIREGUARD_OUTBOUND on 1.11/1.12); the +# wireguard ENDPOINT works from 1.11.0 on. Emit the endpoint form for +# 1.11+, and the legacy outbound only for ancient 1.10.x installs. +wireguard_style() { + ver=$(/usr/bin/sing-box version 2>/dev/null | sed -n 's/.*version[[:space:]]*\([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1.\2/p' | head -n 1) + major=${ver%%.*}; minor=${ver#*.}; minor=${minor%%.*} + case "$major" in ''|*[!0-9]*) printf 'endpoint'; return;; esac + if [ "$major" -eq 1 ] && [ "$minor" -lt 11 ]; then printf 'legacy'; else printf 'endpoint'; fi +} + append_node() { - local s="$1" enabled label protocol server port password sni insecure alpn uuid congestion udp_mode public_key short_id fingerprint security transport path host flow alter_id credential auxiliary pin_sha256 + local s="$1" enabled label protocol server port password sni insecure alpn uuid congestion udp_mode public_key short_id fingerprint security transport path host flow alter_id credential auxiliary pin_sha256 private_key local_address reserved mtu config_get_bool enabled "$s" enabled 1 [ "$enabled" -eq 1 ] || return 0 config_get label "$s" label "$s"; config_get protocol "$s" protocol @@ -18,13 +29,16 @@ append_node() { config_get security "$s" security; config_get transport "$s" transport; config_get path "$s" path; config_get host "$s" host config_get pin_sha256 "$s" pin_sha256 config_get flow "$s" flow; config_get alter_id "$s" alter_id 0 - case "$label$protocol$server$password$sni$uuid$public_key$short_id$host$flow$alpn$path$fingerprint$pin_sha256$security$transport$congestion$udp_mode" in *'|'*) logger -t "$APP" "invalid delimiter in node $s"; return 1;; esac + config_get private_key "$s" private_key; config_get local_address "$s" local_address; config_get reserved "$s" reserved; config_get mtu "$s" mtu + case "$label$protocol$server$password$sni$uuid$public_key$short_id$host$flow$alpn$path$fingerprint$pin_sha256$security$transport$congestion$udp_mode$private_key$local_address$reserved$mtu" in *'|'*) logger -t "$APP" "invalid delimiter in node $s"; return 1;; esac credential=$password; auxiliary=$uuid case "$protocol" in vless) credential=$uuid; auxiliary=$flow ;; vmess) credential=$uuid; auxiliary=$alter_id ;; + trojan) credential=$password; auxiliary= ;; + wireguard) credential=$private_key; auxiliary= ;; esac - printf 'node|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$s" "$protocol" "$server" "$port" "$credential" "$sni" "$insecure" "$alpn" "$auxiliary" "$congestion" "$udp_mode" "$public_key" "$short_id" "$fingerprint" "$security" "$transport" "$path" "$host" "$pin_sha256" >> "$RUNDIR/normalized.conf" + printf 'node|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$s" "$protocol" "$server" "$port" "$credential" "$sni" "$insecure" "$alpn" "$auxiliary" "$congestion" "$udp_mode" "$public_key" "$short_id" "$fingerprint" "$security" "$transport" "$path" "$host" "$pin_sha256" "$private_key" "$local_address" "$reserved" "$mtu" >> "$RUNDIR/normalized.conf" printf '%s|%s|%s|%s|%s\n' "$s" "$label" "$protocol" "$server" "$port" >> "$RUNDIR/nodes" } @@ -48,6 +62,7 @@ start_service() { # handshake_success events on the first tick after every restart. mkdir -p "$RUNDIR"; chmod 700 "$RUNDIR"; : > "$RUNDIR/normalized.conf"; : > "$RUNDIR/clients"; : > "$RUNDIR/nodes"; : > "$RUNDIR/status.json" config_get log_level main log_level warn; printf 'global|log_level|%s\n' "$log_level" >> "$RUNDIR/normalized.conf" + printf 'global|wireguard_style|%s\n' "$(wireguard_style)" >> "$RUNDIR/normalized.conf" config_get event_interval main event_interval 60 config_get max_events_per_device main max_events_per_device 20 case "$event_interval" in ''|*[!0-9]*) event_interval=60;; esac diff --git a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh index 4fc838bc8e..b8707388bd 100755 --- a/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh +++ b/applications/luci-app-wificalling-gateway/root/usr/libexec/wificalling-gateway/compiler.sh @@ -23,13 +23,15 @@ function tls(sni, insecure, alpn, pin, extra) { if (pin!="") extra=extra ",\"certificate_public_key_sha256\":[" q(pin) "]" return "{" extra "}" } -$1=="global" { if ($2=="log_level") level=$3; next } +$1=="global" { if ($2=="log_level") level=$3; if ($2=="wireguard_style") wg_style=$3; next } $1=="node" { id=$2; proto=$3 if (id=="" || seen_node[id]++) fail("duplicate or empty node id: " id) - if (proto!="anytls" && proto!="hysteria2" && proto!="tuic" && proto!="vless" && proto!="vmess") fail("unsupported protocol: " proto) + if (proto!="anytls" && proto!="hysteria2" && proto!="tuic" && proto!="vless" && proto!="vmess" && proto!="trojan" && proto!="wireguard") fail("unsupported protocol: " proto) if ($4=="" || $5 !~ /^[0-9]+$/ || $5<1 || $5>65535) fail("invalid server or port for node: " id) - node[++nn]=$0; node_id[nn]=id; node_proto[id]=proto; next + node[++nn]=$0; node_id[nn]=id; node_proto[id]=proto + if (proto=="wireguard") wg_nodes[++nw]=nn + next } $1=="device" { if (!node_proto[$3]) fail("device references unknown node: " $3) @@ -46,12 +48,32 @@ $1=="device" { END { if (nn<1) fail("at least one enabled node is required") if (level=="") level="warn" + if (wg_style=="") wg_style="legacy" print "{" + # The wireguard outbound was removed in sing-box 1.13.0 (deprecated in + # 1.11.0, gated behind ENABLE_DEPRECATED_WIREGUARD_OUTBOUND on 1.11/1.12); + # the wireguard endpoint works from 1.11.0 on. init.d picks the style from + # the installed sing-box version; "endpoint" emits an endpoints block and + # routes straight to the endpoint tag, "legacy" keeps the old outbound. + if (nw>0 && wg_style=="endpoint") { + print " \"endpoints\":[" + for(w=1;w<=nw;w++) { + split(node[wg_nodes[w]],f,"|"); id=f[2] + s="{\"type\":\"wireguard\",\"tag\":" q("wg-" id) ",\"address\":[" q(f[22]) "],\"private_key\":" q(f[21]) + s=s ",\"peers\":[{\"address\":" q(f[4]) ",\"port\":" f[5] ",\"public_key\":" q(f[13]) ",\"allowed_ips\":[\"0.0.0.0/0\"]" + if (f[23]!="") { nr=split(f[23],rv,","); rv_s=rv[1]; for(ri=2;ri<=nr;ri++) rv_s=rv_s "," rv[ri]; s=s ",\"reserved\":[" rv_s "]" } + s=s "}]" + if (f[24]!="") s=s ",\"mtu\":" f[24] + s=s "}"; print " " s (w/dev/null 2>&1; then measurement=tcp tcp_output=$(tcping -c 1 -t 1 -p "$port" "$server" 2>/dev/null || true)