diff --git a/.github/diy/patches/luci-app-upnp.patch b/.github/diy/patches/luci-app-upnp.patch deleted file mode 100644 index d0e9f67c5..000000000 --- a/.github/diy/patches/luci-app-upnp.patch +++ /dev/null @@ -1,559 +0,0 @@ -From 439f5437a57303ca4220910cc5a9f9f367c270fb Mon Sep 17 00:00:00 2001 -From: Self-Hosting-Group -Date: Fri, 29 May 2026 00:00:00 +0000 -Subject: [PATCH] luci-app-upnp: improve existing UI - -- Workaround daemon bug to fix listing of active port maps with iptables - >= 1.8.8, and relax nftables parsing for the new release -- Change `Active Service Port Maps` to `Active Port Maps`, use the same - wording for the table headings and ACL as on the overview status page -- Change description field header to `Added via / description`, always - include the protocol and clearer/less redundant protocol labels -- Disable IPv6 mapping (`ipv6_disable`): UI option added, UCI exists -- Don't clear dependent options if UPnP IGD/STUN temporarily disabled -- Set `notify_interval` minimum to 900 s (default), as recommended by - [UDA 1.1] (2x=1800 in the standard), because daemon/OpenWrt wrongly - suggested 30x less in the past, and to reduce multicast traffic and - power consumption in wireless networks, clearer help -- Report system instead of service uptime, UUID and service lease file: - UI options removed (disabled, to keep translations) as rarely used -- Slightly improve some text strings -- Show hint if no suitable configuration found (missing related update), - link to package manager, and show form as read-only to prevent changes -- Configure service lease file by default in rpcd ucode for full UI - functionality without option set, but retrieve it only on rpcd start -- Refactoring by moving ubus connection as jow-/ucode@a58fe47 was merged -- Adapt to JavaScript ES6 and remove some line breaks and format files -- A commit has been prepared that manually adjusts the translations, - without losing them - -[UDA 1.1]: https://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf#page=30 - -Signed-off-by: Self-Hosting-Group ---- - .../resources/view/status/include/80_upnp.js | 29 ++--- - .../luci-static/resources/view/upnp/upnp.js | 109 +++++++++++------- - .../usr/share/rpcd/acl.d/luci-app-upnp.json | 2 +- - .../root/usr/share/rpcd/ucode/luci.upnp | 78 +++++-------- - 4 files changed, 117 insertions(+), 101 deletions(-) - -diff --git a/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js b/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -index 67e255caea7a..45f03c68a457 100644 ---- a/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -+++ b/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js -@@ -7,14 +7,14 @@ - const callUpnpGetStatus = rpc.declare({ - object: 'luci.upnp', - method: 'get_status', -- expect: { } -+ expect: {} - }); - - const callUpnpDeleteRule = rpc.declare({ - object: 'luci.upnp', - method: 'delete_rule', -- params: [ 'token' ], -- expect: { result : "OK" }, -+ params: ['token'], -+ expect: { result: 'OK' }, - }); - - function handleDelRule(num, ev) { -@@ -35,23 +35,23 @@ return baseclass.extend({ - }, - - render: function(data) { -- var table = E('table', { 'class': 'table', 'id': 'upnp_status_table' }, [ -+ const table = E('table', { 'class': 'table', 'id': 'upnp_status_table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ -- E('th', { 'class': 'th' }, _('Client Name')), -- E('th', { 'class': 'th' }, _('Client Address')), -- E('th', { 'class': 'th' }, _('Client Port')), -- E('th', { 'class': 'th' }, _('External Port')), -+ E('th', { 'class': 'th' }, _('Hostname')), -+ E('th', { 'class': 'th' }, _('IP address')), -+ E('th', { 'class': 'th' }, _('Port')), -+ E('th', { 'class': 'th' }, _('External port')), - E('th', { 'class': 'th' }, _('Protocol')), - E('th', { 'class': 'th right' }, _('Expires')), -- E('th', { 'class': 'th' }, _('Description')), -+ E('th', { 'class': 'th' }, _('Added via / description')), - E('th', { 'class': 'th cbi-section-actions' }, '') - ]) - ]); - -- var rules = Array.isArray(data[0].rules) ? data[0].rules : []; -+ const rules = Array.isArray(data[0].rules) ? data[0].rules : []; - -- var rows = rules.map(function(rule) { -- const padnum = (num, length) => num.toString().padStart(length, "0"); -+ const rows = rules.map(function(rule) { -+ const padnum = (num, length) => num.toString().padStart(length, '0'); - const expires_sec = rule?.expires || 0; - const hour = Math.floor(expires_sec / 3600); - const minute = Math.floor((expires_sec % 3600) / 60); -@@ -72,8 +72,9 @@ return baseclass.extend({ - rule.descr, - E('button', { - 'class': 'btn cbi-button-remove', -- 'click': L.bind(handleDelRule, this, rule.num) -- }, [ _('Delete') ]) -+ 'click': L.bind(handleDelRule, this, rule.num), -+ 'title': _('Delete') -+ }, [_('Delete')]) - ]; - }); - -diff --git a/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js b/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -index 955e1c45f5f8..494172864e35 100644 ---- a/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -+++ b/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js -@@ -3,27 +3,28 @@ - 'require dom'; - 'require poll'; - 'require uci'; -+'require ui'; - 'require rpc'; - 'require form'; - - const callInitAction = rpc.declare({ - object: 'luci', - method: 'setInitAction', -- params: [ 'name', 'action' ], -+ params: ['name', 'action'], - expect: { result: false } - }); - - const callUpnpGetStatus = rpc.declare({ - object: 'luci.upnp', - method: 'get_status', -- expect: { } -+ expect: {} - }); - - const callUpnpDeleteRule = rpc.declare({ - object: 'luci.upnp', - method: 'delete_rule', -- params: [ 'token' ], -- expect: { result : "OK" }, -+ params: ['token'], -+ expect: { result: 'OK' }, - }); - - function handleDelRule(num, ev) { -@@ -44,10 +45,10 @@ return view.extend({ - - poll_status: function(nodes, data) { - -- var rules = Array.isArray(data[0].rules) ? data[0].rules : []; -+ const rules = Array.isArray(data[0].rules) ? data[0].rules : []; - -- var rows = rules.map(function(rule) { -- const padnum = (num, length) => num.toString().padStart(length, "0"); -+ const rows = rules.map(function(rule) { -+ const padnum = (num, length) => num.toString().padStart(length, '0'); - const expires_sec = rule?.expires || 0; - const hour = Math.floor(expires_sec / 3600); - const minute = Math.floor((expires_sec % 3600) / 60); -@@ -68,8 +69,9 @@ return view.extend({ - '%h'.format(rule.descr), - E('button', { - 'class': 'btn cbi-button-remove', -- 'click': L.bind(handleDelRule, this, rule.num) -- }, [ _('Delete') ]) -+ 'click': L.bind(handleDelRule, this, rule.num), -+ 'title': _('Delete') -+ }, [_('Delete')]) - ]; - }); - -@@ -80,35 +82,42 @@ return view.extend({ - - let m, s, o; - -- var protocols = '%s & %s/%s'.format( -+ const protocols = _('%s & %s/%s', '%s & %s/%s (%s = UPnP IGD, %s = PCP, %s = NAT-PMP)').format( - 'UPnP IGD', - 'PCP', - 'NAT-PMP'); -- m = new form.Map('upnpd', [_('UPnP IGD & PCP/NAT-PMP Service')], -+ m = new form.Map('upnpd', _('UPnP IGD & PCP/NAT-PMP Service'), - _('The %s protocols allow clients on the local network to configure port maps/forwards on the router autonomously.', - 'The %s (%s = UPnP IGD & PCP/NAT-PMP) protocols allow clients on the local network to configure port maps/forwards on the router autonomously.') -- .format(protocols) -+ .format(protocols) - ); -+ if (!uci.get('upnpd', 'config')) { -+ ui.addNotification(null, E('div', '

' + _('No suitable configuration was found!') + '

' + -+ _('No suitable config (LuCI app %s) found in %s. A related package update (daemon or LuCI app) may be missing.').format('v1.0', '/etc/config/upnpd') + '
' + -+ _('Use the software package manager, update lists, and install the related update. Config is migrated with the daemon package update.') + '

' + -+ '' + _('Go to package manager…') + ''), 'warning'); -+ m.readonly = true; -+ } - - s = m.section(form.GridSection, '_active_rules'); - - s.render = L.bind(function(view, section_id) { -- var table = E('table', { 'class': 'table cbi-section-table', 'id': 'upnp_status_table' }, [ -+ const table = E('table', { 'class': 'table cbi-section-table', 'id': 'upnp_status_table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ -- E('th', { 'class': 'th' }, _('Client Name')), -- E('th', { 'class': 'th' }, _('Client Address')), -- E('th', { 'class': 'th' }, _('Client Port')), -- E('th', { 'class': 'th' }, _('External Port')), -+ E('th', { 'class': 'th' }, _('Hostname')), -+ E('th', { 'class': 'th' }, _('IP address')), -+ E('th', { 'class': 'th' }, _('Port')), -+ E('th', { 'class': 'th' }, _('External port')), - E('th', { 'class': 'th' }, _('Protocol')), - E('th', { 'class': 'th right' }, _('Expires')), -- E('th', { 'class': 'th' }, _('Description')), -+ E('th', { 'class': 'th' }, _('Added via / description')), - E('th', { 'class': 'th cbi-section-actions' }, '') - ]) - ]); - -- var rules = Array.isArray(data[0].rules) ? data[0].rules : []; -+ const rules = Array.isArray(data[0].rules) ? data[0].rules : []; - -- var rows = rules.map(function(rule) { -+ const rows = rules.map(function(rule) { - return [ - rule.host_hint || _('Unknown'), - rule.intaddr, -@@ -118,15 +127,17 @@ return view.extend({ - rule.descr, - E('button', { - 'class': 'btn cbi-button-remove', -- 'click': L.bind(handleDelRule, this, rule.num) -- }, [ _('Delete') ]) -+ 'click': L.bind(handleDelRule, this, rule.num), -+ 'title': _('Delete') -+ }, [_('Delete')]) - ]; - }); - - cbi_update_table(table, rows, E('em', _('There are no active port maps.'))); - - return E('div', { 'class': 'cbi-section cbi-tblsection' }, [ -- E('h3', _('Active Service Port Maps')), table ]); -+ E('h3', _('Active Port Maps')), table -+ ]); - }, o, this); - - s = m.section(form.NamedSection, 'config', 'upnpd', _('Service Settings')); -@@ -134,8 +145,8 @@ return view.extend({ - s.tab('setup', _('Service Setup')); - s.tab('advanced', _('Advanced Settings')); - -- o = s.taboption('setup', form.Flag, 'enabled', _('Start service'), -- _('Start autonomous port mapping service')); -+ o = s.taboption('setup', form.Flag, 'enabled', _('Enable service'), -+ _('Enable the autonomous port mapping service')); - o.rmempty = false; - - o = s.taboption('setup', form.Flag, 'enable_upnp', _('Enable UPnP IGD protocol')); -@@ -152,89 +163,107 @@ return view.extend({ - o.default = '1'; - o.rmempty = false; - o.depends('enable_upnp', '1'); -+ o.retain = true; - - o = s.taboption('setup', form.Value, 'download', _('Download speed'), - _('Report maximum download speed in kByte/s')); - o.depends('enable_upnp', '1'); -+ o.retain = true; - - o = s.taboption('setup', form.Value, 'upload', _('Upload speed'), - _('Report maximum upload speed in kByte/s')); - o.depends('enable_upnp', '1'); -+ o.retain = true; - - s.taboption('advanced', form.Flag, 'use_stun', _('Use %s', 'Use %s (%s = STUN)') -- .format('STUN'), -+ .format('STUN'), - _('To detect the public IPv4 address for unrestricted full-cone/one-to-one NATs')); - - o = s.taboption('advanced', form.Value, 'stun_host', _('STUN host')); -- o.depends('use_stun', '1'); - o.datatype = 'host'; -+ o.depends('use_stun', '1'); -+ o.retain = true; - - o = s.taboption('advanced', form.Value, 'stun_port', _('STUN port')); -- o.depends('use_stun', '1'); - o.datatype = 'port'; - o.placeholder = '3478'; -+ o.depends('use_stun', '1'); -+ o.retain = true; - - s.taboption('advanced', form.Flag, 'force_forwarding', _('Force forwarding'), - _('Force forwarding ports even the network is behind NAT')); - - o = s.taboption('advanced', form.Flag, 'secure_mode', _('Enable secure mode'), - _('Allow adding port maps for requesting IP addresses only')); - o.default = '1'; - o.depends('enable_upnp', '1'); -+ o.retain = true; -+ -+ s.taboption('advanced', form.Flag, 'ipv6_disable', _('Disable IPv6 mapping')); - - o = s.taboption('advanced', form.Value, 'notify_interval', _('Notify interval'), -- _('A 900s interval will result in %s notifications with the minimum max-age of 1800s', 'A 900s interval will result in %s (%s = SSDP) notifications with the minimum max-age of 1800s') -- .format('SSDP')); -- o.datatype = 'uinteger'; -+ _('A 900 s interval sends %s announcements with the minimum %s header', -+ 'A 900 s interval sends %s (%s = SSDP) announcements with the minimum %s (%s = Cache-Control: max-age=1800) header') -+ .format('SSDP', 'Cache-Control: max-age=1800')); -+ o.datatype = 'min(900)'; - o.placeholder = '900'; - o.depends('enable_upnp', '1'); -+ o.retain = true; - - o = s.taboption('advanced', form.Value, 'port', _('SOAP/HTTP port')); - o.datatype = 'port'; - o.placeholder = '5000'; - o.depends('enable_upnp', '1'); -+ o.retain = true; - -- o = s.taboption('advanced', form.Value, 'presentation_url', _('Presentation URL'), -- _('Report custom router web interface (presentation) URL')); -+ o = s.taboption('advanced', form.Value, 'presentation_url', _('Router/presentation URL'), -+ _('Report custom router web interface URL')); - o.placeholder = 'http://192.168.1.1/'; - o.depends('enable_upnp', '1'); -+ o.retain = true; - - o = s.taboption('advanced', form.Value, 'uuid', _('Device UUID')); -- o.depends('enable_upnp', '1'); -+ // o.depends('enable_upnp', '1'); -+ o.depends('keep-translation', 'to-disable-as-rare-use'); -+ o.retain = true; - - o = s.taboption('advanced', form.Value, 'model_number', _('Announced model number')); - o.depends('enable_upnp', '1'); -+ o.retain = true; - - o = s.taboption('advanced', form.Value, 'serial_number', _('Announced serial number')); - o.depends('enable_upnp', '1'); -+ o.retain = true; - - o = s.taboption('advanced', form.Flag, 'system_uptime', _('Report system instead of service uptime')); - o.default = '1'; -- o.depends('enable_upnp', '1'); -+ o.depends('keep-translation', 'to-disable-as-rare-use'); -+ o.retain = true; - - s.taboption('advanced', form.Flag, 'log_output', _('Enable additional logging'), - _('Puts extra debugging information into the system log')); - - o = s.taboption('advanced', form.Value, 'upnp_lease_file', _('Service lease file')); -- o.placeholder = '/var/run/miniupnpd.leases'; -+ o.depends('keep-translation', 'to-disable-as-rare-use'); -+ o.retain = true; - - s = m.section(form.GridSection, 'perm_rule', _('Service Access Control List'), - _('ACL specify which client addresses and ports can be mapped, IPv6 always allowed.')); -- s.sortable = true; - s.anonymous = true; - s.addremove = true; -+ s.sortable = true; - - s.option(form.Value, 'comment', _('Comment')); - -- o = s.option(form.Value, 'int_addr', _('Client Address')); -+ o = s.option(form.Value, 'int_addr', _('IP address')); - o.datatype = 'ip4addr'; - o.placeholder = '0.0.0.0/0'; - -- o = s.option(form.Value, 'int_ports', _('Client Port')); -+ o = s.option(form.Value, 'int_ports', _('Port')); - o.datatype = 'portrange'; - o.placeholder = '1-65535'; - -- o = s.option(form.Value, 'ext_ports', _('External Port')); -+ o = s.option(form.Value, 'ext_ports', _('External port')); - o.datatype = 'portrange'; - o.placeholder = '1-65535'; - -diff --git a/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json b/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json -index 4611f501692a..6e1fa8fcd52c 100644 ---- a/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json -+++ b/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json -@@ -1,6 +1,6 @@ - { - "luci-app-upnp": { -- "description": "Grant access to UPnP IGD & PCP/NAT-PMP", -+ "description": "Grant access to UPnP IGD & PCP/NAT-PMP service configuration", - "read": { - "ubus": { - "luci.upnp": [ "get_status" ], -diff --git a/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp b/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp -index f72b57b7ba84..f777f165beab 100644 ---- a/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp -+++ b/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp -@@ -1,3 +1,5 @@ -+#!/usr/bin/env ucode -+ - // Copyright 2022 Jo-Philipp Wich - // Licensed to the public under the Apache License 2.0. - -@@ -7,82 +9,75 @@ import { access, open, popen } from 'fs'; - import { connect } from 'ubus'; - import { cursor } from 'uci'; - --// Establish ubus connection persistently outside of the call handler scope to --// prevent premature GC'ing. Can be moved into `get_status` callback once --// https://github.com/jow-/ucode/commit/a58fe4709f661b5f28e26701ea8638efccf5aeb6 --// is merged. --const ubus = connect(); -+const uci = cursor(); -+const leasefilepath = uci.get('upnpd', 'config', 'upnp_lease_file') || '/var/run/miniupnpd.leases'; - - const methods = { - get_status: { - call: function(req) { -- const uci = cursor(); -- -+ const ubus = connect(); - const rules = []; - const leases = []; -- -- const leasefile = open(uci.get('upnpd', 'config', 'upnp_lease_file'), 'r'); -- -+ const leasefile = open(leasefilepath, 'r'); - if (leasefile) { - for (let line = leasefile.read('line'); length(line); line = leasefile.read('line')) { - const record = split(line, ':', 6); -- - if (length(record) == 6) { -+ let descr = trim(record[5]); -+ let m = match(descr, /^PCP [A-Z]+ ([0-9a-f]{24})$/); -+ if (m) descr = 'PCP (nonce ' + m[1] + ')'; -+ else if (match(descr, /^NAT-PMP \d+ \w+$/)) descr = 'NAT-PMP'; -+ else if (match(descr, /^IGD2 pinhole$/)) descr = 'UPnP IGDv2 IPv6'; -+ else if (!match(descr, /^UPnP IGD/)) descr = 'UPnP IGD / ' + descr; - push(leases, { - proto: uc(record[0]), - extport: +record[1], - intaddr: arrtoip(iptoarr(record[2])), - intport: +record[3], - expires: record[4] - timelocal(localtime()), -- description: trim(record[5]) -+ descr: descr - }); - } - } -- - leasefile.close(); - } - -- const ipt = popen('iptables --line-numbers -t nat -xnvL MINIUPNPD 2>/dev/null'); -- -+ // Workaround daemon bug with iptables >= 1.8.8 by parsing MINIUPNPD-POSTROUTING -+ // See https://github.com/miniupnp/miniupnp/issues/837 -+ //const ipt = popen('iptables --line-numbers -t nat -xnvL MINIUPNPD 2>/dev/null'); -+ const ipt = popen('iptables --line-numbers -t nat -xnvL MINIUPNPD-POSTROUTING 2>/dev/null'); - if (ipt) { - for (let line = ipt.read('line'); length(line); line = ipt.read('line')) { -- let m = match(line, /^([0-9]+)\s+([a-z]+).+dpt:([0-9]+) to:(\S+):([0-9]+)/); -- -+ //let m = match(line, /^([0-9]+) .+ ([ut].p) dpt:([0-9]+) to:(\S+):([0-9]+)/); -+ let m = match(line, /^([0-9]+) .+ \* +([0-9.]+) .+ ([ut].p) spt:([0-9]+) .+ ports: ([0-9]+)/); - if (m) { - push(rules, { - num: m[1], -- proto: uc(m[2]), -- extport: +m[3], -- intaddr: arrtoip(iptoarr(m[4])), -- intport: +m[5], -- descr: '' -+ intaddr: arrtoip(iptoarr(m[2])), -+ intport: +m[4], -+ extport: +m[5], -+ proto: uc(m[3]) - }); - } - } -- - ipt.close(); - } - - const nft = popen('nft --handle list chain inet fw4 upnp_prerouting 2>/dev/null'); -- - if (nft) { - for (let line = nft.read('line'), num = 1; length(line); line = nft.read('line')) { -- let m = match(line, /^\t\tiif ".+" @nh,72,8 (0x6|0x11) th dport ([0-9]+) dnat ip to ([0-9.]+):([0-9]+)/); -- -+ let m = match(line, /^\t\tiif ".+" @nh,72,8 (0x6|0x11) th dport ([0-9]+).* dnat ip to ([0-9.]+):([0-9]+)/); - if (m) { - push(rules, { - num: `${num}`, -- proto: (m[1] == '0x6') ? 'TCP' : 'UDP', -- extport: +m[2], - intaddr: arrtoip(iptoarr(m[3])), - intport: +m[4], -- descr: '' -+ extport: +m[2], -+ proto: (m[1] == '0x6') ? 'TCP' : 'UDP' - }); -- - num++; - } - } -- - nft.close(); - } - -@@ -90,16 +85,14 @@ const methods = { - for (let rule in rules) { - for (let lease in leases) { - if (lease.proto == rule.proto && -- lease.intaddr == rule.intaddr && -- lease.intport == rule.intport && -- lease.extport == rule.extport) -- { -- rule.descr = lease.description; -+ lease.intaddr == rule.intaddr && -+ lease.intport == rule.intport && -+ lease.extport == rule.extport) { - rule.expires = lease.expires; -+ rule.descr = lease.descr; - break; - } - } -- - for (let mac, hint in host_hints) { - if (rule.intaddr in hint.ipaddrs) { - rule.host_hint = hint.name; -@@ -107,7 +100,6 @@ const methods = { - } - } - } -- - req.reply({ rules }); - }); - } -@@ -117,19 +109,13 @@ const methods = { - args: { token: 'token' }, - call: function(req) { - const idx = +req.args?.token; -- - if (idx > 0) { -- const uci = cursor(); -- const leasefile = uci.get('upnpd', 'config', 'upnp_lease_file'); -- -- if (access(leasefile)) { -- system(['sed', '-i', '-e', `${idx}d`, leasefile]); -+ if (access(leasefilepath)) { -+ system(['sed', '-i', '-e', `${idx}d`, leasefilepath]); - system(['/etc/init.d/miniupnpd', 'restart']); - } -- - return { result: 'OK' }; - } -- - return { result: 'Bad request' }; - } - } -