From c505be9baa04b73abc14bc8a0175d705358bfcf2 Mon Sep 17 00:00:00 2001 From: kiddin9 <48883331+kiddin9@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:16:47 +0800 Subject: [PATCH] Create upnp.patch --- .github/diy/patches/upnp.patch | 5102 ++++++++++++++++++++++++++++++++ 1 file changed, 5102 insertions(+) create mode 100644 .github/diy/patches/upnp.patch diff --git a/.github/diy/patches/upnp.patch b/.github/diy/patches/upnp.patch new file mode 100644 index 00000000..d0e0caa9 --- /dev/null +++ b/.github/diy/patches/upnp.patch @@ -0,0 +1,5102 @@ +--- 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({ + '%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')]) + ]; + }); + +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 40218589d691..07ca8b0a9c62 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,53 +82,63 @@ 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, + rule.intport, + rule.extport, + rule.proto, +- rule.descr, ++ '', // expires ++ '%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')]) + ]; + }); + + 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 +146,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,86 +164,104 @@ 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; + + 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..56ae951ceb03 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,73 @@ 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 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(uci.get('upnpd', 'config', 'upnp_lease_file') || '/var/run/miniupnpd.leases', '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 +83,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 +98,6 @@ const methods = { + } + } + } +- + req.reply({ rules }); + }); + } +@@ -117,19 +107,15 @@ 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'); +- ++ const leasefile = uci.get('upnpd', 'config', 'upnp_lease_file') || '/var/run/miniupnpd.leases'; + if (access(leasefile)) { + system(['sed', '-i', '-e', `${idx}d`, leasefile]); + system(['/etc/init.d/miniupnpd', 'restart']); + } +- + return { result: 'OK' }; + } +- + return { result: 'Bad request' }; + } + } + +From e7100b61eaf00ae0e11e973ade457edc433c8daa Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 2/7] luci-app-upnp: add `UPnP IGD Adjustments` tab + +And rearrange as many options + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + .../luci-static/resources/view/upnp/upnp.js | 71 ++++++++++--------- + 1 file changed, 36 insertions(+), 35 deletions(-) + +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 07ca8b0a9c62..0f8a4e84ea18 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 +@@ -145,6 +145,7 @@ return view.extend({ + s.addremove = false; + s.tab('setup', _('Service Setup')); + s.tab('advanced', _('Advanced Settings')); ++ s.tab('igd', _('UPnP IGD Adjustments')); + + o = s.taboption('setup', form.Flag, 'enabled', _('Enable service'), + _('Enable the autonomous port mapping service')); +@@ -156,7 +157,7 @@ return view.extend({ + o = s.taboption('setup', form.Flag, 'enable_natpmp', _('Enable PCP/NAT-PMP protocols')); + o.default = '1'; + +- s.taboption('setup', form.Flag, 'ext_allow_private_ipv4', _('Allow private IPv4'), ++ s.taboption('advanced', form.Flag, 'ext_allow_private_ipv4', _('Allow private IPv4'), + _('Enable forwarding for private/reserved IPv4 address')); + + o = s.taboption('setup', form.Flag, 'igdv1', _('UPnP IGDv1 compatibility mode'), +@@ -166,16 +167,6 @@ return view.extend({ + 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'), + _('To detect the public IPv4 address for unrestricted full-cone/one-to-one NATs')); +@@ -199,50 +190,60 @@ return view.extend({ + + s.taboption('advanced', form.Flag, 'ipv6_disable', _('Disable IPv6 mapping')); + +- o = s.taboption('advanced', form.Value, 'notify_interval', _('Notify interval'), +- _('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 = s.taboption('advanced', form.Flag, 'system_uptime', _('Report system instead of service uptime')); ++ o.default = '1'; ++ o.depends('keep-translation', 'to-disable-as-rare-use'); + o.retain = true; + +- o = s.taboption('advanced', form.Value, 'port', _('SOAP/HTTP port')); +- o.datatype = 'port'; +- o.placeholder = '5000'; ++ 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.depends('keep-translation', 'to-disable-as-rare-use'); ++ o.retain = true; ++ ++ o = s.taboption('igd', form.Value, 'download', _('Download speed'), ++ _('Report maximum download speed in kByte/s')); + o.depends('enable_upnp', '1'); + o.retain = true; + +- 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 = s.taboption('igd', form.Value, 'upload', _('Upload speed'), ++ _('Report maximum upload speed in kByte/s')); + o.depends('enable_upnp', '1'); + o.retain = true; + +- o = s.taboption('advanced', form.Value, 'uuid', _('Device UUID')); +- // o.depends('enable_upnp', '1'); +- o.depends('keep-translation', 'to-disable-as-rare-use'); ++ o = s.taboption('igd', form.Value, 'model_number', _('Announced model number')); ++ o.depends('enable_upnp', '1'); + o.retain = true; + +- o = s.taboption('advanced', form.Value, 'model_number', _('Announced model number')); ++ o = s.taboption('igd', form.Value, 'serial_number', _('Announced serial number')); + o.depends('enable_upnp', '1'); + o.retain = true; + +- o = s.taboption('advanced', form.Value, 'serial_number', _('Announced serial number')); ++ o = s.taboption('igd', 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.Flag, 'system_uptime', _('Report system instead of service uptime')); +- o.default = '1'; ++ o = s.taboption('igd', form.Value, 'uuid', _('Device UUID')); ++ // 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('igd', 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, 'upnp_lease_file', _('Service lease file')); +- o.depends('keep-translation', 'to-disable-as-rare-use'); ++ o = s.taboption('igd', form.Value, 'notify_interval', _('Notify interval'), ++ _('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; + + s = m.section(form.GridSection, 'perm_rule', _('Service Access Control List'), + +From dc6910d395c2eed5b27c5f974a4db63a9253d0e3 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 3/7] luci-app-upnp: revision and adapt to new UCI options + +The following settings UCI options been added or changed, and the +previous options are migrated on updating: + +- Only display `Active Port Maps` if the service is enabled and `Access + Control List` if it is used +- Enable protocols (`enable_protocols`): Combined UI option added +- Allow CGNAT/STUN (`allow_cgnat`): Accept new values for IPv4 CGNAT use + and update help with newer wording of RFC 5780 +- STUN server (`stun_host`): Allow port inclusion +- STUN port: Removed, as now accepted in STUN server +- Override external IPv4 (`external_ip`): UI option added for CGNAT use +- Allow third-party mapping (`allow_third_party_mapping`): Inverted from + secure mode and optionally extended to PCP +- Log level (`log_output`): Allow info log level +- UPnP IGD compatibility (`upnp_igd_compat`): Reworded/extensible +- Download/upload speed (`download_kbps`/`upload_kbps`): In kbit/s and + datatype set, now, interface link speed by default +- Router/friendly name (`friendly_name`): UI option added to set name + displayed in Windows Explorer, model/serial number removed +- Enable Networks / Access Control (`internal_network`): Section added + to select the enabled networks and their access control. By: + - Internal network (`interface`): UI option added to select the + local/internal (LAN) network interface to enable the service for + - Access defaults (`access_defaults`): UI option added to select + access defaults for ports that all devices on the network can map + - Accept extra ports (`accept_ports`): UI option added to accept these + ports or port ranges on the network as well + - Reject ports (`reject_ports`): UI option added to reject ports on + the network; overrides other settings + - Check ACL (`check_acl`): UI option added to check the ACL entries + first; these extend/override the defaults +- Slightly improve introduction text + +More details on changed options can be found in the dependent package PR + +Fixes: https://redirect.github.com/openwrt/luci/issues/8313 +Fixes: https://redirect.github.com/openwrt/luci/issues/8677 +Depends on: https://redirect.github.com/openwrt/packages/pull/28765 + +Signed-off-by: Self Hosting Group +--- + luci-app-upnp/Makefile | 2 +- + .../luci-static/resources/view/upnp/upnp.js | 202 +++++++++++++----- + .../root/usr/share/rpcd/ucode/luci.upnp | 4 +- + 3 files changed, 151 insertions(+), 57 deletions(-) + +diff --git a/luci-app-upnp/Makefile b/luci-app-upnp/Makefile +index 80e668764d96..601122426a89 100644 +--- a/luci-app-upnp/Makefile ++++ b/luci-app-upnp/Makefile +@@ -6,7 +6,7 @@ + + include $(TOPDIR)/rules.mk + +-LUCI_TITLE:=UPnP IGD & PCP/NAT-PMP configuration module | Universal Plug and Play ++LUCI_TITLE:=LuCI support for UPnP IGD & PCP/NAT-PMP service | originally: Universal Plug and Play + LUCI_DEPENDS:=+luci-base +miniupnpd +rpcd-mod-ucode + + PKG_LICENSE:=Apache-2.0 +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 0f8a4e84ea18..1dab80bef4b9 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 +@@ -6,6 +6,7 @@ + 'require ui'; + 'require rpc'; + 'require form'; ++'require tools.widgets as widgets'; + + const callInitAction = rpc.declare({ + object: 'luci', +@@ -87,8 +88,8 @@ return view.extend({ + 'PCP', + 'NAT-PMP'); + 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.') ++ _('The %s protocols/service enable permitted devices on local networks to autonomously set up IPv4/IPv6 port maps/forwards on this router.', ++ 'The %s (%s = UPnP IGD & PCP/NAT-PMP) protocols/service enable permitted devices on local networks to autonomously set up IPv4/IPv6 port maps/forwards on this router.') + .format(protocols) + ); + if (!uci.get('upnpd', 'config')) { +@@ -100,6 +101,7 @@ return view.extend({ + } + + s = m.section(form.GridSection, '_active_rules'); ++ s.disable = uci.get('upnpd', 'config', 'enabled') == '0'; + + s.render = L.bind(function(view, section_id) { + const table = E('table', { 'class': 'table cbi-section-table', 'id': 'upnp_status_table' }, [ +@@ -151,42 +153,50 @@ return view.extend({ + _('Enable the autonomous port mapping service')); + o.rmempty = false; + +- o = s.taboption('setup', form.Flag, 'enable_upnp', _('Enable UPnP IGD protocol')); +- o.default = '1'; +- +- o = s.taboption('setup', form.Flag, 'enable_natpmp', _('Enable PCP/NAT-PMP protocols')); +- o.default = '1'; +- +- s.taboption('advanced', form.Flag, 'ext_allow_private_ipv4', _('Allow private IPv4'), +- _('Enable forwarding for private/reserved IPv4 address')); +- +- o = s.taboption('setup', form.Flag, 'igdv1', _('UPnP IGDv1 compatibility mode'), +- _('Advertise as IGDv1 (IPv4 only) device instead of IGDv2')); +- o.default = '1'; +- o.rmempty = false; +- o.depends('enable_upnp', '1'); ++ o = s.taboption('setup', form.ListValue, 'enable_protocols', _('Enable protocols')); ++ o.value('all', _('All protocols')); ++ o.value('upnp-igd', _('UPnP IGD')); ++ o.value('pcp+nat-pmp', _('PCP and NAT-PMP')); ++ o.default = 'all'; ++ o.widget = 'radio'; ++ ++ o = s.taboption('setup', form.ListValue, 'upnp_igd_compat', _('UPnP IGD compatibility'), ++ _('Set compatibility mode (act as device) to workaround IGDv2-incompatible clients; %s are known to only work with %s (or)
Emulate/report a specific/different device to workaround/support/handle/bypass/assist/mitigate... (Alternative text welcome)').format('Sony PS, Activision CoD…', 'IGDv1')); ++ o.value('igdv1', _('IGDv1 (IPv4 only)')); ++ o.value('igdv2', _('IGDv2 (with workarounds)')); ++ o.depends('enable_protocols', 'upnp-igd'); ++ o.depends('enable_protocols', 'all'); + o.retain = true; + +- s.taboption('advanced', form.Flag, 'use_stun', _('Use %s', 'Use %s (%s = 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.datatype = 'host'; +- o.depends('use_stun', '1'); ++ o = s.taboption('advanced', form.RichListValue, 'allow_cgnat', _('Allow %s/%s', 'Allow %s/%s (%s = CGNAT, %s = STUN)') ++ .format('CGNAT', ++ 'STUN'), ++ _('Allow use of unrestricted endpoint-independent (1:1) CGNATs and detect the public IPv4')); ++ o.value('', _('Disabled'), _('Manually override external IPv4 with public')); ++ o.value('1', _('Enabled'), _('Filtering test currently requires an extra firewall rule')); ++ o.value('allow-filtered', _('Enabled') + ' (' + _('allow filtered') + ')', _('Workaround filtered IPv4 CGNAT test result')); ++ o.value('report-private-ipv4', _('Disabled') + ' (' + _('report private IPv4, avoid') + ')', _('No STUN public IPv4 detection; client issues')); ++ o.optional = true; ++ ++ o = s.taboption('advanced', form.Value, 'stun_host', _('STUN server')); ++ o.datatype = 'or(hostname,hostport,ip4addr("nomask"))'; ++ o.placeholder = 'stun.nextcloud.com'; ++ o.depends('allow_cgnat', '1'); ++ o.depends('allow_cgnat', 'allow-filtered'); + o.retain = true; + +- o = s.taboption('advanced', form.Value, 'stun_port', _('STUN port')); +- o.datatype = 'port'; +- o.placeholder = '3478'; +- o.depends('use_stun', '1'); +- o.retain = true; ++ o = s.taboption('advanced', form.Value, 'external_ip', _('Override external IPv4'), ++ _('Report custom public/external (WAN) IPv4 address')); ++ o.datatype = 'ip4addr("nomask")'; ++ o.depends('allow_cgnat', ''); ++ o.depends('allow_cgnat', 'report-private-ipv4'); + +- 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; ++ o = s.taboption('advanced', form.ListValue, 'allow_third_party_mapping', _('Allow third-party mapping'), ++ _('Allow adding port maps for non-requesting IP addresses; use with care')); ++ o.value('', _('Disabled') + ' (' + _('recommended') + ')'); ++ o.value('1', _('Enabled')); ++ o.value('upnp-igd', _('Enabled') + ' (' + _('UPnP IGD only') + ')'); ++ o.value('pcp', _('Enabled') + ' (' + _('PCP only') + ')'); + + s.taboption('advanced', form.Flag, 'ipv6_disable', _('Disable IPv6 mapping')); + +@@ -195,46 +205,69 @@ return view.extend({ + 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.ListValue, 'log_output', _('Log level')); ++ o.value('default', _('Default')); ++ o.value('info', _('Info')); ++ o.value('debug', _('Debug')); ++ o.default = 'default'; ++ o.widget = 'radio'; + +- o = s.taboption('advanced', form.Value, 'upnp_lease_file', _('Service lease file')); ++ o = s.taboption('advanced', form.Value, 'lease_file', _('Service lease file')); + o.depends('keep-translation', 'to-disable-as-rare-use'); + o.retain = true; + +- o = s.taboption('igd', form.Value, 'download', _('Download speed'), +- _('Report maximum download speed in kByte/s')); +- o.depends('enable_upnp', '1'); ++ o = s.taboption('igd', form.Value, 'download_kbps', _('Download speed'), ++ _('Report maximum connection speed in kbit/s')); ++ o.datatype = 'uinteger'; ++ o.placeholder = _('Default interface link speed'); ++ o.depends('enable_protocols', 'upnp-igd'); ++ o.depends('enable_protocols', 'all'); + o.retain = true; + +- o = s.taboption('igd', form.Value, 'upload', _('Upload speed'), +- _('Report maximum upload speed in kByte/s')); +- o.depends('enable_upnp', '1'); ++ o = s.taboption('igd', form.Value, 'upload_kbps', _('Upload speed'), ++ _('Report maximum connection speed in kbit/s')); ++ o.datatype = 'uinteger'; ++ o.placeholder = _('Default interface link speed'); ++ o.depends('enable_protocols', 'upnp-igd'); ++ o.depends('enable_protocols', 'all'); ++ o.retain = true; ++ ++ o = s.taboption('igd', form.Value, 'friendly_name', _('Router/friendly name')); ++ o.placeholder = 'OpenWrt UPnP IGD & PCP'; ++ o.depends('enable_protocols', 'upnp-igd'); ++ o.depends('enable_protocols', 'all'); + o.retain = true; + + o = s.taboption('igd', form.Value, 'model_number', _('Announced model number')); +- o.depends('enable_upnp', '1'); ++ // o.depends('enable_protocols', 'upnp-igd'); ++ // o.depends('enable_protocols', 'all'); ++ o.depends('keep-translation', 'to-disable-as-rare-use'); + o.retain = true; + + o = s.taboption('igd', form.Value, 'serial_number', _('Announced serial number')); +- o.depends('enable_upnp', '1'); ++ // o.depends('enable_protocols', 'upnp-igd'); ++ // o.depends('enable_protocols', 'all'); ++ o.depends('keep-translation', 'to-disable-as-rare-use'); + o.retain = true; + + o = s.taboption('igd', 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.depends('enable_protocols', 'upnp-igd'); ++ o.depends('enable_protocols', 'all'); + o.retain = true; + + o = s.taboption('igd', form.Value, 'uuid', _('Device UUID')); +- // o.depends('enable_upnp', '1'); ++ // o.depends('enable_protocols', 'upnp-igd'); ++ // o.depends('enable_protocols', 'all'); + o.depends('keep-translation', 'to-disable-as-rare-use'); + o.retain = true; + +- o = s.taboption('igd', form.Value, 'port', _('SOAP/HTTP port')); ++ o = s.taboption('igd', form.Value, 'http_port', _('SOAP/HTTP port')); + o.datatype = 'port'; + o.placeholder = '5000'; +- o.depends('enable_upnp', '1'); ++ o.depends('enable_protocols', 'upnp-igd'); ++ o.depends('enable_protocols', 'all'); + o.retain = true; + + o = s.taboption('igd', form.Value, 'notify_interval', _('Notify interval'), +@@ -243,7 +276,57 @@ return view.extend({ + .format('SSDP', 'Cache-Control: max-age=1800')); + o.datatype = 'min(900)'; + o.placeholder = '900'; +- o.depends('enable_upnp', '1'); ++ o.depends('enable_protocols', 'upnp-igd'); ++ o.depends('enable_protocols', 'all'); ++ o.retain = true; ++ ++ s = m.section(form.GridSection, 'internal_network', '
' + _('Enable Networks / Access Control') + '
', ++ _('Select local/internal (LAN) network interfaces to enable the service for.') + ' ' + ++ _('Set access control defaults for ports that all devices on a network can map.') + ' ' + ++ _('IPv6 is currently always accepted unless disabled. (Alternative text welcome)')); ++ s.anonymous = true; ++ s.addremove = true; ++ s.cloneable = true; ++ s.sortable = true; ++ s.nodescriptions = true; ++ s.modaltitle = _('UPnP IGD & PCP/NAT-PMP') + ' - ' + _('Edit Network Access Control Settings'); ++ ++ o = s.option(widgets.NetworkSelect, 'interface', _('Internal network'), ++ _('Select the local/internal (LAN) network interface to enable the service for')); ++ o.nocreate = true; ++ o.editable = true; ++ o.rmempty = false; ++ o.retain = true; ++ o.filter = function(section_id, value) { ++ return (value == 'wan' || value == 'wan6') ? '' : value; ++ }; ++ ++ o = s.option(form.ListValue, 'access_defaults', _('Access defaults'), ++ _('Set access control defaults for ports that all devices on the network can map')); ++ o.value('', _('None / accept extra ports only')); ++ o.value('accept-high-ports', _('Accept ports >= 1024')); ++ o.value('accept-web+high-ports', _('Accept HTTP/HTTPS + ports >= 1024')); ++ o.value('accept-web-ports', _('Accept HTTP/HTTPS ports')); ++ o.value('accept-all-ports', _('Accept all ports')); ++ o.editable = true; ++ o.retain = true; ++ ++ o = s.option(form.Value, 'accept_ports', _('Accept extra ports')); ++ o.datatype = 'list(portrange)'; ++ o.retain = true; ++ ++ o = s.option(form.Value, 'reject_ports', _('Reject ports'), ++ _('Reject unsafe/insecure/risky FTP/Telnet/DCE/NetBIOS/SMB/RDP ports on the network by default; overrides other settings; use %s for none').format('0')); ++ o.datatype = 'list(portrange)'; ++ o.placeholder = '21 23 135 137-139 445 3389'; ++ o.modalonly = true; ++ o.retain = true; ++ ++ o = s.option(form.Flag, 'check_acl', _('Check ACL'), ++ _('Extend or override access defaults by device-specific permissions using the access control list (ACL)') + '
' + ++ _('Sequence:') + ' 1. ' + _('Reject ports') + ', 2. ' + _('ACL entries (if checked)') + ', 3. ' + _('Access defaults') + ', 4. ' + _('Accept extra ports')); ++ o.default = '1'; ++ o.editable = true; + o.retain = true; + + s = m.section(form.GridSection, 'perm_rule', _('Service Access Control List'), +@@ -251,6 +334,15 @@ return view.extend({ + s.anonymous = true; + s.addremove = true; + s.sortable = true; ++ // To do: ACL part of extra tab with dependency on option as immediately, and network section part of service setup tab ++ let acl_used = false; ++ for (let ifnr = 0; uci.get('upnpd', `@internal_network[${ifnr}]`, 'interface'); ifnr++) { ++ if (uci.get('upnpd', `@internal_network[${ifnr}]`, 'check_acl') != '0') { ++ acl_used = true; ++ break; ++ } ++ } ++ s.disable = !acl_used; + + s.option(form.Value, 'comment', _('Comment')); + +@@ -271,11 +363,13 @@ return view.extend({ + o.value('deny', _('Deny')); + + return m.render().then(L.bind(function(m, nodes) { +- poll.add(L.bind(function() { +- return Promise.all([ +- callUpnpGetStatus() +- ]).then(L.bind(this.poll_status, this, nodes)); +- }, this), 5); ++ if (uci.get('upnpd', 'config', 'enabled') != '0') { ++ poll.add(L.bind(function() { ++ return Promise.all([ ++ callUpnpGetStatus() ++ ]).then(L.bind(this.poll_status, this, nodes)); ++ }, this), 5); ++ } + return nodes; + }, this, m)); + } +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 56ae951ceb03..a7bf8e6f5d86 100644 +--- a/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp ++++ b/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp +@@ -16,7 +16,7 @@ const methods = { + const ubus = connect(); + const rules = []; + const leases = []; +- const leasefile = open(uci.get('upnpd', 'config', 'upnp_lease_file') || '/var/run/miniupnpd.leases', 'r'); ++ const leasefile = open(uci.get('upnpd', 'config', 'lease_file') || '/var/run/miniupnpd.leases', 'r'); + if (leasefile) { + for (let line = leasefile.read('line'); length(line); line = leasefile.read('line')) { + const record = split(line, ':', 6); +@@ -109,7 +109,7 @@ const methods = { + const idx = +req.args?.token; + if (idx > 0) { + const uci = cursor(); +- const leasefile = uci.get('upnpd', 'config', 'upnp_lease_file') || '/var/run/miniupnpd.leases'; ++ const leasefile = uci.get('upnpd', 'config', 'lease_file') || '/var/run/miniupnpd.leases'; + if (access(leasefile)) { + system(['sed', '-i', '-e', `${idx}d`, leasefile]); + system(['/etc/init.d/miniupnpd', 'restart']); + +From e203add9f4372d85f97b4222abe69066a08cb571 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 4/7] luci-app-upnp: rename UCI section to settings v2.0 + +Rename UCI section `config` (v1.0) -> `settings` (v2.0), helps on +migration and to distinguish the updated config from the previous one + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + .../htdocs/luci-static/resources/view/upnp/upnp.js | 10 +++++----- + .../luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp | 4 ++-- + 2 files changed, 7 insertions(+), 7 deletions(-) + +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 1dab80bef4b9..e5f1245636af 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 +@@ -92,16 +92,16 @@ return view.extend({ + 'The %s (%s = UPnP IGD & PCP/NAT-PMP) protocols/service enable permitted devices on local networks to autonomously set up IPv4/IPv6 port maps/forwards on this router.') + .format(protocols) + ); +- if (!uci.get('upnpd', 'config')) { ++ if (!uci.get('upnpd', 'settings')) { + 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') + '
' + ++ _('No suitable config (LuCI app %s) found in %s. A related package update (daemon or LuCI app) may be missing.').format('v2.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.disable = uci.get('upnpd', 'config', 'enabled') == '0'; ++ s.disable = uci.get('upnpd', 'settings', 'enabled') == '0'; + + s.render = L.bind(function(view, section_id) { + const table = E('table', { 'class': 'table cbi-section-table', 'id': 'upnp_status_table' }, [ +@@ -143,7 +143,7 @@ return view.extend({ + ]); + }, o, this); + +- s = m.section(form.NamedSection, 'config', 'upnpd', _('Service Settings')); ++ s = m.section(form.NamedSection, 'settings', 'upnpd', _('Service Settings')); + s.addremove = false; + s.tab('setup', _('Service Setup')); + s.tab('advanced', _('Advanced Settings')); +@@ -363,7 +363,7 @@ return view.extend({ + o.value('deny', _('Deny')); + + return m.render().then(L.bind(function(m, nodes) { +- if (uci.get('upnpd', 'config', 'enabled') != '0') { ++ if (uci.get('upnpd', 'settings', 'enabled') != '0') { + poll.add(L.bind(function() { + return Promise.all([ + callUpnpGetStatus() +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 a7bf8e6f5d86..e38f5a26352d 100644 +--- a/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp ++++ b/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp +@@ -16,7 +16,7 @@ const methods = { + const ubus = connect(); + const rules = []; + const leases = []; +- const leasefile = open(uci.get('upnpd', 'config', 'lease_file') || '/var/run/miniupnpd.leases', 'r'); ++ const leasefile = open(uci.get('upnpd', 'settings', 'lease_file') || '/var/run/miniupnpd.leases', 'r'); + if (leasefile) { + for (let line = leasefile.read('line'); length(line); line = leasefile.read('line')) { + const record = split(line, ':', 6); +@@ -109,7 +109,7 @@ const methods = { + const idx = +req.args?.token; + if (idx > 0) { + const uci = cursor(); +- const leasefile = uci.get('upnpd', 'config', 'lease_file') || '/var/run/miniupnpd.leases'; ++ const leasefile = uci.get('upnpd', 'settings', 'lease_file') || '/var/run/miniupnpd.leases'; + if (access(leasefile)) { + system(['sed', '-i', '-e', `${idx}d`, leasefile]); + system(['/etc/init.d/miniupnpd', 'restart']); + +From 88c8ccce976eb54ed4d3e6cea00b18660eb3eec7 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 5/7] luci-app-upnp: update ACL options, migrate section + +- Disableable and cloneable ACL entries, always translated `Action` +- Improve UI with direct editability, clearer help wording, and rename + to `Access Control List` +- The ACL is now rejected last if not accepted by access defaults. Add + (disabled) ACL template entries on migration +- Migrate ACL entries to the new section name `acl_entry` +- The following ACL UCI options been added or changed, and the previous + options are migrated on updating: + +acl_entry UCI options | Change | Previous name +----------------------------|----------------------------|-------------- +action | New/updated values (1) | +int_port | Remove colon separator (2) | int_ports +ext_port | Remove colon separator (2) | ext_ports +descr_filter | New option (3) | + +1. Allow disabled, and update action option to use the nftables terms + (allow/deny -> accept/reject). To avoid adding inverted actions when + changing via LuCI, ensure any missing are set, as LuCI and UCI had + not matching action defaults. Missing actions are now ignored/logged +2. Ensure that the hyphen (-) is only used as a port range separator by + migration, as the colon (:) is not valid in LuCI +3. Add missing UCI option to set a regular expression to check for a + UPnP IGD IPv4 port map description, and fix the current collision + with the comment field which was not noticed due to a daemon bug + https://redirect.github.com/openwrt/packages/pull/24495 + https://redirect.github.com/miniupnp/miniupnp/pull/853 + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + .../luci-static/resources/view/upnp/upnp.js | 46 +++++++++++++++---- + 1 file changed, 36 insertions(+), 10 deletions(-) + +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 e5f1245636af..87c7c2a25fe0 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 +@@ -329,11 +329,14 @@ return view.extend({ + o.editable = true; + 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 = m.section(form.GridSection, 'acl_entry', '
' + _('Access Control List') + '
', ++ _('The access control list (ACL) specifies which IP addresses and ports can be mapped.') + ' ' + ++ _('ACL entries are checked in order, then rejected if not matched and not accepted by access defaults. (should be part of extra tab)')); + s.anonymous = true; + s.addremove = true; ++ s.cloneable = true; + s.sortable = true; ++ s.modaltitle = _('UPnP IGD & PCP/NAT-PMP') + ' - ' + _('Edit ACL Entry'); + // To do: ACL part of extra tab with dependency on option as immediately, and network section part of service setup tab + let acl_used = false; + for (let ifnr = 0; uci.get('upnpd', `@internal_network[${ifnr}]`, 'interface'); ifnr++) { +@@ -344,23 +347,46 @@ return view.extend({ + } + s.disable = !acl_used; + +- s.option(form.Value, 'comment', _('Comment')); ++ o = s.option(form.Value, 'comment', _('Comment')); ++ o.default = _('unspecified'); + + o = s.option(form.Value, 'int_addr', _('IP address')); + o.datatype = 'ip4addr'; +- o.placeholder = '0.0.0.0/0'; ++ o.rmempty = false; ++ o.editable = true; ++ o.retain = true; ++ o.modalonly = false; ++ ++ o = s.option(form.Value, 'int_addr', _('IP address'), ++ _('Enter device\'s IPv4 address, or an address with netmask')); ++ o.datatype = 'ip4addr'; ++ o.rmempty = false; ++ o.retain = true; ++ o.modalonly = true; + +- o = s.option(form.Value, 'int_ports', _('Port')); ++ o = s.option(form.Value, 'int_port', _('Port')); + o.datatype = 'portrange'; +- o.placeholder = '1-65535'; ++ o.placeholder = '1-65535 (' + _('any port') + ')'; ++ o.editable = true; ++ o.retain = true; + +- o = s.option(form.Value, 'ext_ports', _('External port')); ++ o = s.option(form.Value, 'ext_port', _('External port')); + o.datatype = 'portrange'; +- o.placeholder = '1-65535'; ++ o.placeholder = '1-65535 (' + _('any port') + ')'; ++ o.editable = true; ++ o.retain = true; ++ ++ o = s.option(form.Value, 'descr_filter', _('Description filter'), ++ _('Regular expression to check description of UPnP IGD IPv4 port maps')); ++ o.placeholder = '.* (' + _('any description') + ')'; ++ o.modalonly = true; + + o = s.option(form.ListValue, 'action', _('Action')); +- o.value('allow', _('Allow')); +- o.value('deny', _('Deny')); ++ o.value('accept', _('Accept')); ++ o.value('reject', _('Reject')); ++ o.value('disabled', _('Disabled')); ++ o.editable = true; ++ o.retain = true; + + return m.render().then(L.bind(function(m, nodes) { + if (uci.get('upnpd', 'settings', 'enabled') != '0') { + +From 095283e5b52745251d12f8254f3257f4c4cfe7f8 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 6/7] luci-app-upnp: list active IPv6 port maps + +Supports nftables and ip6tables; IPv6 port maps not yet deletable, as +not parsing extra IPv6 lease file + +(Note: Daemon's IPv6 lease file differs from the IPv4 format in order +and separator. With IPv4, the remote IP field is missing and not +retained during daemon restarts. IMHO, it would be better to have a +complete/combined IPv4/IPv6 lease file upstream, with unique mapping IDs +for reliable deletion in the 1st field, with a standardised order and +more fields, and then use it without rewriting the parsing.) + +(to merge with prior) + +Fixes: https://redirect.github.com/openwrt/luci/issues/2985 +Fixes: https://redirect.github.com/openwrt/luci/issues/5197 +Fixes: https://redirect.github.com/openwrt/luci/pull/8567 +Signed-off-by: Self Hosting Group +--- + .../resources/view/status/include/80_upnp.js | 8 +++- + .../luci-static/resources/view/upnp/upnp.js | 16 ++++++-- + .../root/usr/share/rpcd/ucode/luci.upnp | 40 ++++++++++++++++++- + 3 files changed, 57 insertions(+), 7 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 c82a108232de..7d06b2a56d6d 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 +@@ -70,15 +70,19 @@ return baseclass.extend({ + rule.proto, + expires_str, + '%h'.format(rule.descr), +- E('button', { ++ rule.num != '0' ? E('button', { + 'class': 'btn cbi-button-remove', + 'click': L.bind(handleDelRule, this, rule.num), + 'title': _('Delete') ++ }, [_('Delete')]) : E('button', { ++ 'class': 'btn cbi-button-remove', ++ 'title': _('Not yet deletable; UPnP IGDv2 IPv6 port maps expire sooner, have no description'), ++ 'style': 'cursor: not-allowed' + }, [_('Delete')]) + ]; + }); + +- cbi_update_table(table, rows, E('em', _('There are no active port maps.'))); ++ cbi_update_table(table, rows, E('em', _('There are no active IPv4/IPv6 port maps'))); + + return table; + } +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 87c7c2a25fe0..2f790e6c4a12 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 +@@ -68,15 +68,19 @@ return view.extend({ + rule.proto, + expires_str, + '%h'.format(rule.descr), +- E('button', { ++ rule.num != '0' ? E('button', { + 'class': 'btn cbi-button-remove', + 'click': L.bind(handleDelRule, this, rule.num), + 'title': _('Delete') ++ }, [_('Delete')]) : E('button', { ++ 'class': 'btn cbi-button-remove', ++ 'title': _('Not yet deletable; UPnP IGDv2 IPv6 port maps expire sooner, have no description'), ++ 'style': 'cursor: not-allowed' + }, [_('Delete')]) + ]; + }); + +- cbi_update_table(nodes.querySelector('#upnp_status_table'), rows, E('em', _('There are no active port maps.'))); ++ cbi_update_table(nodes.querySelector('#upnp_status_table'), rows, E('em', _('There are no active IPv4/IPv6 port maps'))); + }, + + render: function(data) { +@@ -128,15 +132,19 @@ return view.extend({ + rule.proto, + '', // expires + '%h'.format(rule.descr), +- E('button', { ++ rule.num != '0' ? E('button', { + 'class': 'btn cbi-button-remove', + 'click': L.bind(handleDelRule, this, rule.num), + 'title': _('Delete') ++ }, [_('Delete')]) : E('button', { ++ 'class': 'btn cbi-button-remove', ++ 'title': _('Not yet deletable; UPnP IGDv2 IPv6 port maps expire sooner, have no description'), ++ 'style': 'cursor: not-allowed' + }, [_('Delete')]) + ]; + }); + +- cbi_update_table(table, rows, E('em', _('There are no active port maps.'))); ++ cbi_update_table(table, rows, E('em', _('There are no active IPv4/IPv6 port maps'))); + + return E('div', { 'class': 'cbi-section cbi-tblsection' }, [ + E('h3', _('Active Port Maps')), table +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 e38f5a26352d..b9bf72809f66 100644 +--- a/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp ++++ b/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp +@@ -59,6 +59,23 @@ const methods = { + } + } + ipt.close(); ++ ++ const iptipv6 = popen('ip6tables --line-numbers -xnvL MINIUPNPD 2>/dev/null'); ++ if (iptipv6) { ++ for (let line = iptipv6.read('line'); length(line); line = iptipv6.read('line')) { ++ let m = match(line, /^([0-9]+) .+ ([0-9a-f:]+) ([ut].p) dpt:([0-9]+)/); ++ if (m) { ++ push(rules, { ++ num: '0', ++ intaddr: arrtoip(iptoarr(m[2])), ++ intport: +m[4], ++ extport: +m[4], ++ proto: uc(m[3]) ++ }); ++ } ++ } ++ iptipv6.close(); ++ } + } + + const nft = popen('nft --handle list chain inet fw4 upnp_prerouting 2>/dev/null'); +@@ -77,6 +94,27 @@ const methods = { + } + } + nft.close(); ++ ++ const nftipv6 = popen('nft --handle list chain inet fw4 upnp_forward 2>/dev/null'); ++ if (nftipv6) { ++ for (let line = nftipv6.read('line'), num = 1; length(line); line = nftipv6.read('line')) { ++ let m = match(line, /^\t\tiif ".+" th dport ([0-9]+) @nh,192,128 0x([0-9a-f]+) @nh,48,8 (0x6|0x11) accept/); ++ if (m) { ++ push(rules, { ++ num: '0', ++ intaddr: arrtoip(iptoarr(join(':', [substr(m[2], 0, 4), ++ substr(m[2], 4, 4), substr(m[2], 8, 4), substr(m[2], 12, 4), substr(m[2], 16, 4), ++ substr(m[2], 20, 4), substr(m[2], 24, 4), substr(m[2], 28, 4) ++ ]))), ++ intport: +m[1], ++ extport: +m[1], ++ proto: (m[3] == '0x6') ? 'TCP' : 'UDP' ++ }); ++ num++; ++ } ++ } ++ nftipv6.close(); ++ } + } + + return ubus.defer('luci-rpc', 'getHostHints', {}, function(rc, host_hints) { +@@ -92,7 +130,7 @@ const methods = { + } + } + for (let mac, hint in host_hints) { +- if (rule.intaddr in hint.ipaddrs) { ++ if (rule.intaddr in hint.ipaddrs || rule.intaddr in hint.ip6addrs) { + rule.host_hint = hint.name; + break; + } + +From 5550b4a056860acd9ba6509b80f52ebeda47dbce Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 7/7] luci-app-upnp: service-wide access control settings + +This implements variant B + +upnpd.settings UCI options | Change | Previous name +-----------------------------|---------------------------|-------------- +access_defaults | New option (1) | +accept_ports | New option (1) | +reject_ports | New option (2) | +check_acl | New option (1) | + +Notes: +1. New options added for default ports that all devices can map, + client-specific permissions using the access control list (ACL) can + extend/override the defaults. Access defaults: accept-high-ports + accept-web+high-ports/accept-web-ports/accept-all-ports +2. Reject ports; overrides other settings. By default reject unsafe: + FTP (21), Telnet (23), DCE/NetBIOS/SMB (135/137-139/445), RDP (3389) + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + .../luci-static/resources/view/upnp/upnp.js | 106 ++++++++---------- + 1 file changed, 45 insertions(+), 61 deletions(-) + +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 2f790e6c4a12..68de0e56a807 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 +@@ -154,6 +154,7 @@ return view.extend({ + s = m.section(form.NamedSection, 'settings', 'upnpd', _('Service Settings')); + s.addremove = false; + s.tab('setup', _('Service Setup')); ++ s.tab('access_control', _('Access Control')); + s.tab('advanced', _('Advanced Settings')); + s.tab('igd', _('UPnP IGD Adjustments')); + +@@ -168,6 +169,18 @@ return view.extend({ + o.default = 'all'; + o.widget = 'radio'; + ++ o = s.taboption('setup', widgets.NetworkSelect, 'internal_iface', _('Enable networks'), ++ _('Select local/internal (LAN) network interfaces to enable the service for')); ++ o.nocreate = true; ++ o.multiple = true; ++ o.rmempty = false; ++ o.filter = function(section_id, value) { ++ return (value == 'wan' || value == 'wan6') ? '' : value; ++ }; ++ o.write = function(section_id, formvalue) { ++ uci.set('upnpd', section_id, 'internal_iface', Array.isArray(formvalue) ? formvalue.join(' ') : formvalue); ++ }; ++ + o = s.taboption('setup', form.ListValue, 'upnp_igd_compat', _('UPnP IGD compatibility'), + _('Set compatibility mode (act as device) to workaround IGDv2-incompatible clients; %s are known to only work with %s (or)
Emulate/report a specific/different device to workaround/support/handle/bypass/assist/mitigate... (Alternative text welcome)').format('Sony PS, Activision CoD…', 'IGDv1')); + o.value('igdv1', _('IGDv1 (IPv4 only)')); +@@ -176,6 +189,35 @@ return view.extend({ + o.depends('enable_protocols', 'all'); + o.retain = true; + ++ o = s.taboption('access_control', form.ListValue, 'access_defaults', _('Access defaults'), ++ _('Set access control defaults for ports that all devices can map')); ++ o.value('', _('None / accept extra ports only')); ++ o.value('accept-high-ports', _('Accept ports >= 1024')); ++ o.value('accept-web+high-ports', _('Accept HTTP/HTTPS + ports >= 1024')); ++ o.value('accept-web-ports', _('Accept HTTP/HTTPS ports')); ++ o.value('accept-all-ports', _('Accept all ports')); ++ ++ o = s.taboption('access_control', form.Value, 'accept_ports', _('Accept extra ports')); ++ o.datatype = 'list(portrange)'; ++ ++ o = s.taboption('access_control', form.Value, 'reject_ports', _('Reject ports'), ++ _('Reject unsafe/insecure/risky FTP/Telnet/DCE/NetBIOS/SMB/RDP ports by default; overrides other settings; use %s for none').format('0')); ++ o.datatype = 'list(portrange)'; ++ o.placeholder = '21 23 135 137-139 445 3389'; ++ o.modalonly = true; ++ ++ o = s.taboption('access_control', form.Flag, 'check_acl', _('Check ACL'), ++ _('Extend or override access defaults by device-specific permissions using the access control list (ACL)') + '
' + ++ _('Sequence:') + ' 1. ' + _('Reject ports') + ', 2. ' + _('ACL entries (if checked)') + ', 3. ' + _('Access defaults') + ', 4. ' + _('Accept extra ports')); ++ o.default = '1'; ++ o.onchange = function(ev, section_id, value) { ++ let acl = document.getElementById('cbi-upnpd-acl_entry'); ++ value == 0 ? acl.style.display = 'none' : acl.style.display = 'block'; ++ }; ++ ++ s.taboption('access_control', form.Flag, 'ipv6_disable', _('Disable IPv6 mapping'), ++ _('IPv6 is currently always accepted unless disabled')); ++ + o = s.taboption('advanced', form.RichListValue, 'allow_cgnat', _('Allow %s/%s', 'Allow %s/%s (%s = CGNAT, %s = STUN)') + .format('CGNAT', + 'STUN'), +@@ -206,8 +248,6 @@ return view.extend({ + o.value('upnp-igd', _('Enabled') + ' (' + _('UPnP IGD only') + ')'); + o.value('pcp', _('Enabled') + ' (' + _('PCP only') + ')'); + +- s.taboption('advanced', form.Flag, 'ipv6_disable', _('Disable IPv6 mapping')); +- + o = s.taboption('advanced', form.Flag, 'system_uptime', _('Report system instead of service uptime')); + o.default = '1'; + o.depends('keep-translation', 'to-disable-as-rare-use'); +@@ -288,72 +328,16 @@ return view.extend({ + o.depends('enable_protocols', 'all'); + o.retain = true; + +- s = m.section(form.GridSection, 'internal_network', '
' + _('Enable Networks / Access Control') + '
', +- _('Select local/internal (LAN) network interfaces to enable the service for.') + ' ' + +- _('Set access control defaults for ports that all devices on a network can map.') + ' ' + +- _('IPv6 is currently always accepted unless disabled. (Alternative text welcome)')); +- s.anonymous = true; +- s.addremove = true; +- s.cloneable = true; +- s.sortable = true; +- s.nodescriptions = true; +- s.modaltitle = _('UPnP IGD & PCP/NAT-PMP') + ' - ' + _('Edit Network Access Control Settings'); +- +- o = s.option(widgets.NetworkSelect, 'interface', _('Internal network'), +- _('Select the local/internal (LAN) network interface to enable the service for')); +- o.nocreate = true; +- o.editable = true; +- o.rmempty = false; +- o.retain = true; +- o.filter = function(section_id, value) { +- return (value == 'wan' || value == 'wan6') ? '' : value; +- }; +- +- o = s.option(form.ListValue, 'access_defaults', _('Access defaults'), +- _('Set access control defaults for ports that all devices on the network can map')); +- o.value('', _('None / accept extra ports only')); +- o.value('accept-high-ports', _('Accept ports >= 1024')); +- o.value('accept-web+high-ports', _('Accept HTTP/HTTPS + ports >= 1024')); +- o.value('accept-web-ports', _('Accept HTTP/HTTPS ports')); +- o.value('accept-all-ports', _('Accept all ports')); +- o.editable = true; +- o.retain = true; +- +- o = s.option(form.Value, 'accept_ports', _('Accept extra ports')); +- o.datatype = 'list(portrange)'; +- o.retain = true; +- +- o = s.option(form.Value, 'reject_ports', _('Reject ports'), +- _('Reject unsafe/insecure/risky FTP/Telnet/DCE/NetBIOS/SMB/RDP ports on the network by default; overrides other settings; use %s for none').format('0')); +- o.datatype = 'list(portrange)'; +- o.placeholder = '21 23 135 137-139 445 3389'; +- o.modalonly = true; +- o.retain = true; +- +- o = s.option(form.Flag, 'check_acl', _('Check ACL'), +- _('Extend or override access defaults by device-specific permissions using the access control list (ACL)') + '
' + +- _('Sequence:') + ' 1. ' + _('Reject ports') + ', 2. ' + _('ACL entries (if checked)') + ', 3. ' + _('Access defaults') + ', 4. ' + _('Accept extra ports')); +- o.default = '1'; +- o.editable = true; +- o.retain = true; +- + s = m.section(form.GridSection, 'acl_entry', '
' + _('Access Control List') + '
', + _('The access control list (ACL) specifies which IP addresses and ports can be mapped.') + ' ' + +- _('ACL entries are checked in order, then rejected if not matched and not accepted by access defaults. (should be part of extra tab)')); ++ _('ACL entries are checked in order, then rejected if not matched and not accepted by access defaults. (To do: should be part of access control tab)')); + s.anonymous = true; + s.addremove = true; + s.cloneable = true; + s.sortable = true; + s.modaltitle = _('UPnP IGD & PCP/NAT-PMP') + ' - ' + _('Edit ACL Entry'); +- // To do: ACL part of extra tab with dependency on option as immediately, and network section part of service setup tab +- let acl_used = false; +- for (let ifnr = 0; uci.get('upnpd', `@internal_network[${ifnr}]`, 'interface'); ifnr++) { +- if (uci.get('upnpd', `@internal_network[${ifnr}]`, 'check_acl') != '0') { +- acl_used = true; +- break; +- } +- } +- s.disable = !acl_used; ++ // To do: ACL part of access control tab and hide (or dependency on option) instead of disable as immediately, and to not break onchange function ++ s.disable = uci.get('upnpd', 'settings', 'check_acl') == '0'; + + o = s.option(form.Value, 'comment', _('Comment')); + o.default = _('unspecified'); + +--- a/miniupnpd/Makefile ++++ b/miniupnpd/Makefile +@@ -8,12 +8,12 @@ + include $(TOPDIR)/rules.mk + + PKG_NAME:=miniupnpd +-PKG_VERSION:=2.3.9 +-PKG_RELEASE:=3 ++PKG_VERSION:=2.3.10 ++PKG_RELEASE:=1 + + PKG_SOURCE_URL:=https://github.com/miniupnp/miniupnp/releases/download/miniupnpd_$(subst .,_,$(PKG_VERSION)) + PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +-PKG_HASH:=66cb3c3d697ab2bb3a61d3c48628166d6ba328d7c2dbeb95898fdf2a3202af7b ++PKG_HASH:=f9c34ed3632fb60cd248dd5897bd98479a103a75688b056ca2f069e68ab32987 + + PKG_MAINTAINER:=Alexandru Ardelean + PKG_LICENSE:=BSD-3-Clause + +From 1bca0ee5caa52f493a4e6cef12d2665ba246ecbe Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 02/10] miniupnpd: patch for UPnP IGDv2 Microsoft/Apple + compatibility + +- Add workaround to list port maps with the Windows IGDv2-incompatible + client by returning an infinite (0) lease duration. To fix listing and + editing via GUI (Explorer/Network), if daemon was compiled with IGDv2 +- Extend detection to older versions of Windows and add Xbox +- Detect Apple IGDv2-incompatible clients and apply existing workaround, + that only caused problems if PCP/NAT-PMP (prioritised) was disabled + +(to merge with prior) + +Link: https://github.com/Self-Hosting-Group/miniupnp/tree/upnp-igdv2-compat +Link: https://github.com/Self-Hosting-Group/wiki/wiki/Port-Mapping-Protocols-Overview#compatibility-issues +Signed-off-by: Self Hosting Group +--- + .../patches/10-upnp-igdv2-compat.patch | 80 +++++++++++++++++++ + 1 file changed, 80 insertions(+) + create mode 100644 miniupnpd/patches/10-upnp-igdv2-compat.patch + +diff --git a/miniupnpd/patches/10-upnp-igdv2-compat.patch b/miniupnpd/patches/10-upnp-igdv2-compat.patch +new file mode 100644 +index 00000000000000..2307aeb83ceb74 +--- /dev/null ++++ b/miniupnpd/patches/10-upnp-igdv2-compat.patch +@@ -0,0 +1,80 @@ ++From 38e01919d585b0fd663d2b3249963b336061cbd3 Mon Sep 17 00:00:00 2001 ++From: Self-Hosting-Group ++ <155233284+Self-Hosting-Group@users.noreply.github.com> ++Date: Wed, 25 Mar 2026 00:00:00 +0000 ++Subject: [PATCH] miniupnpd: UPnP IGDv2 Microsoft/Apple compatibility ++ ++* Add workaround to list port maps with the Windows IGDv2-incompatible ++ client by returning an infinite (0) lease duration. To fix listing and ++ editing via GUI (Explorer/Network), if daemon was compiled with IGDv2 ++* Extend detection to older versions of Windows and add Xbox ++* Detect Apple IGDv2-incompatible clients and apply existing workaround, ++ that only caused problems if PCP/NAT-PMP (prioritised) was disabled ++ ++Link: https://github.com/Self-Hosting-Group/miniupnp/tree/upnp-igdv2-compat ++Link: https://github.com/Self-Hosting-Group/wiki/wiki/Port-Mapping-Protocols-Overview#compatibility-issues ++--- ++ upnphttp.c | 19 ++++++++++++++++--- ++ upnpsoap.c | 16 ++++++++++++++++ ++ 2 files changed, 32 insertions(+), 3 deletions(-) ++ ++--- a/upnphttp.c +++++ b/upnphttp.c ++@@ -304,9 +304,22 @@ ParseHttpHeaders(struct upnphttp * h) ++ } ++ else if(strncasecmp(line, "user-agent:", 11) == 0) ++ { ++- /* - User-Agent: Microsoft-Windows/10.0 UPnP/1.0 ++- * - User-Agent: FDSSDP */ ++- if(strcasestr(line + 11, "microsoft") != NULL || strstr(line + 11, "FDSSDP") != NULL) { +++ /* Detect Microsoft UPnP IGDv2-incompatible clients that only support IGDv1 routers, +++ * and Windows requires extra UDA 1.x (Win XP 1.0), via User-Agent SOAP/HTTP header: +++ * - Microsoft-Windows/10.0 UPnP/1.0 (Win >=10) +++ * - Microsoft-Windows/6.1 UPnP/1.0 (Win 7) +++ * - FDSSDP (Win >=Vista for GET) +++ * - Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1) (Win XP/Vista for GET) +++ * - Mozilla/4.0 (compatible; UPnP/1.0; Windows 9x) (Win XP/Vista for POST) +++ * - Xbox/2.0.17559.0 UPnP/1.0 Xbox/2.0.17559.0 +++ * Detect Apple UPnP IGDv2-incompatible clients that only support IGDv1 routers: +++ * - Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1) (for GET) +++ * - Mozilla/4.0 (compatible; UPnP/1.0; Windows 9x) (for POST) */ +++ if (((strstr(line + 11, "Microsoft-Windows/") != NULL || +++ strstr(line + 11, "Xbox/") != NULL) && +++ strstr(line + 11, " UPnP/1.0") != NULL) || +++ strstr(line + 11, "FDSSDP") != NULL || +++ strstr(line + 11, "Mozilla/4.0 (compatible; UPnP/1.0; Windows") != NULL) { ++ h->respflags |= FLAG_MS_CLIENT; ++ } ++ } ++--- a/upnpsoap.c +++++ b/upnpsoap.c ++@@ -854,6 +854,14 @@ GetSpecificPortMappingEntry(struct upnph ++ #ifdef ENABLE_PCP ++ hide_pcp_nonce(desc); ++ #endif +++#ifdef IGD_V2 +++ /* Workaround to list port maps with the Windows IGDv2-incompatible client +++ * by returning an infinite (0) lease duration. To fix listing and editing +++ * via GUI (Explorer/Network), if daemon was compiled with IGDv2 */ +++ if (h->respflags & FLAG_MS_CLIENT) { +++ leaseduration = 0; +++ } +++#endif ++ bodylen = snprintf(body, sizeof(body), resp, ++ action, ns/*SERVICE_TYPE_WANIPC*/, ++ (unsigned int)iport, int_ip, desc, leaseduration, ++@@ -1124,6 +1132,14 @@ GetGenericPortMappingEntry(struct upnpht ++ #ifdef ENABLE_PCP ++ hide_pcp_nonce(desc); ++ #endif +++#ifdef IGD_V2 +++ /* Workaround to list port maps with the Windows IGDv2-incompatible client +++ * by returning an infinite (0) lease duration. To fix listing and editing +++ * via GUI (Explorer/Network), if daemon was compiled with IGDv2 */ +++ if (h->respflags & FLAG_MS_CLIENT) { +++ leaseduration = 0; +++ } +++#endif ++ bodylen = snprintf(body, sizeof(body), resp, ++ action, ns, /*SERVICE_TYPE_WANIPC,*/ rhost, ++ (unsigned int)eport, protocol, (unsigned int)iport, iaddr, desc, + +From 4606783f9fa877e5fd6da150ee96067a71f60966 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 03/10] miniupnpd: patch to improve logging +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +- Clearer logging of enabled protocols/ports, IPv6 mapping, and UPnP IGD + compatibility mode… in start banner +- Log warnings with an enabled allow third-party mapping option +- Less verbose logging by changing the log level of many normally + occurring, internal messages, or those not relevant to users, to debug + or info +- Comment out log filling messages when internal interfaces lose link, + and for e.g. `rule with label '%s' is not a IGD pinhole`, as normally + occurring + +(to merge with prior) + +Fixes: https://redirect.github.com/openwrt/packages/issues/17601 +Fixes: https://redirect.github.com/openwrt/packages/issues/26483 +Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging +Signed-off-by: Self Hosting Group +--- + .../patches/20-improve-logging.patch | 1186 +++++++++++++++++ + 1 file changed, 1186 insertions(+) + create mode 100644 miniupnpd/patches/20-improve-logging.patch + +diff --git a/miniupnpd/patches/20-improve-logging.patch b/miniupnpd/patches/20-improve-logging.patch +new file mode 100644 +index 00000000000000..ee0673535c5cef +--- /dev/null ++++ b/miniupnpd/patches/20-improve-logging.patch +@@ -0,0 +1,1186 @@ ++From 38e01919d585b0fd663d2b3249963b336061cbd3 Mon Sep 17 00:00:00 2001 ++From: Self-Hosting-Group ++ <155233284+Self-Hosting-Group@users.noreply.github.com> ++Date: Tue, 12 May 2026 00:00:00 +0000 ++Subject: [PATCH] miniupnpd: Improve logging ++ ++* Clearer logging of enabled protocols/ports, IPv6 mapping, and UPnP IGD ++ compatibility mode… in start banner ++* Log warnings with an enabled allow third-party mapping option ++* Less verbose logging by changing the log level of many normally ++ occurring or internal messages, or those not relevant to users, to ++ debug or info ++* Comment out log-filling messages when internal interfaces lose link, ++ and for e.g. `rule with label '%s' is not a IGD pinhole`, as normally ++ occurring ++ ++Fixes: https://redirect.github.com/openwrt/packages/issues/17601 ++Fixes: https://redirect.github.com/openwrt/packages/issues/26483 ++Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging ++--- ++ asyncsendto.c | 4 +-- ++ getifaddr.c | 4 +-- ++ minissdp.c | 24 ++++++------- ++ miniupnpd.c | 20 +++++------ ++ natpmp.c | 16 ++++----- ++ netfilter/iptcrdr.c | 10 +++--- ++ netfilter/iptpinhole.c | 4 +-- ++ netfilter_nft/nftnlrdr.c | 2 +- ++ netfilter_nft/nftnlrdr_misc.c | 8 ++--- ++ netfilter_nft/nftpinhole.c | 10 +++--- ++ pcpserver.c | 18 +++++----- ++ pf/pfpinhole.c | 8 ++--- ++ upnphttp.c | 6 ++-- ++ upnppinhole.c | 48 ++++++++++++------------- ++ upnpredirect.c | 20 +++++------ ++ upnpsoap.c | 28 +++++++-------- ++ 16 files changed, 115 insertions(+), 115 deletions(-) ++ ++--- a/asyncsendto.c +++++ b/asyncsendto.c ++@@ -254,9 +254,9 @@ int try_sendto(fd_set * writefds) ++ /* uncatched error */ ++ if(sockaddr_to_string(elt->dest_addr, addr_str, sizeof(addr_str)) <= 0) ++ addr_str[0] = '\0'; ++- syslog(LOG_ERR, "%s(sock=%d, len=%u, dest=%s): sendto: %m", +++ /*syslog(LOG_DEBUG, "%s(sock=%d, len=%u, dest=%s): sendto: %m", ++ "try_sendto", elt->sockfd, (unsigned)elt->len, ++- addr_str); +++ addr_str);*/ ++ ret--; ++ } ++ } else if((int)n != (int)elt->len) { ++--- a/getifaddr.c +++++ b/getifaddr.c ++@@ -74,7 +74,7 @@ getifaddr(const char * ifname, char * bu ++ } else { ++ r = GETIFADDR_IOCTL_ERROR; ++ } ++- syslog(LOG_ERR, "ioctl(s, SIOCGIFADDR, ...): %m"); +++ syslog(LOG_DEBUG, "ioctl(s, SIOCGIFADDR, ...): %m"); ++ close(s); ++ return r; ++ } ++@@ -149,7 +149,7 @@ getifaddr(const char * ifname, char * bu ++ if(addr) *addr = ((struct sockaddr_in *)candidate->ifa_addr)->sin_addr; ++ if(mask) *mask = ((struct sockaddr_in *)candidate->ifa_netmask)->sin_addr; ++ } else { ++- syslog(LOG_WARNING, "no AF_INET address found for %s", ifname); +++ syslog(LOG_DEBUG, "no AF_INET address found for %s", ifname); ++ freeifaddrs(ifap); ++ return GETIFADDR_NO_ADDRESS; ++ } ++--- a/minissdp.c +++++ b/minissdp.c ++@@ -741,8 +741,8 @@ SendSSDPNotify(int s, const struct socka ++ } ++ n = sendto_or_schedule(s, bufr, l, 0, dest, dest_len); ++ if(n < 0) { ++- syslog(LOG_ERR, "sendto(udp_notify=%d, %s): %m", s, ++- host ? host : "NULL"); +++ /*syslog(LOG_DEBUG, "sendto(udp_notify=%d, %s): %m", s, +++ host ? host : "NULL");*/ ++ } else if(n != l) { ++ syslog(LOG_NOTICE, "sendto() sent %d out of %d bytes", n, l); ++ } ++@@ -752,8 +752,8 @@ SendSSDPNotify(int s, const struct socka ++ * discovery messages SHOULD NOT be sent more than three times. */ ++ n = sendto_schedule(s, bufr, l, 0, dest, dest_len, 250); ++ if(n < 0) { ++- syslog(LOG_ERR, "sendto(udp_notify=%d, %s): %m", s, ++- host ? host : "NULL"); +++ /*syslog(LOG_DEBUG, "sendto(udp_notify=%d, %s): %m", s, +++ host ? host : "NULL");*/ ++ } ++ } ++ ++@@ -1058,7 +1058,7 @@ ProcessSSDPData(int s, const char *bufr, ++ } ++ if(lan_addr == NULL) ++ { ++- syslog(LOG_WARNING, "SSDP packet sender %s (if_index=%d) not from a LAN, ignoring", +++ syslog(LOG_DEBUG, "SSDP packet sender %s (if_index=%d) not from a LAN, ignoring", ++ sender_str, source_if); ++ return; ++ } ++@@ -1161,7 +1161,7 @@ ProcessSSDPData(int s, const char *bufr, ++ sender_str );*/ ++ if(st && (st_len > 0)) ++ { ++- syslog(LOG_INFO, "SSDP M-SEARCH from %s ST: %.*s", +++ syslog(LOG_DEBUG, "SSDP M-SEARCH from %s ST: %.*s", ++ sender_str, st_len, st); ++ /* find in which sub network the client is */ ++ #ifdef ENABLE_IPV6 ++@@ -1274,7 +1274,7 @@ ProcessSSDPData(int s, const char *bufr, ++ else ++ snprintf(ver_str, sizeof(ver_str), "%d", known_service_types[i].version); ++ #endif ++- syslog(LOG_INFO, "Single search found"); +++ syslog(LOG_DEBUG, "Single search found"); ++ #ifdef DELAY_MSEARCH_RESPONSE ++ delay = random() / (1 + RAND_MAX / (1000 * mx_value)); ++ #ifdef DEBUG ++@@ -1303,7 +1303,7 @@ ProcessSSDPData(int s, const char *bufr, ++ #ifdef DELAY_MSEARCH_RESPONSE ++ unsigned int delay_increment = (mx_value * 1000) / 15; ++ #endif ++- syslog(LOG_INFO, "ssdp:all found"); +++ syslog(LOG_DEBUG, "ssdp:all found"); ++ for(i=0; known_service_types[i].s; i++) ++ { ++ #ifdef DELAY_MSEARCH_RESPONSE ++@@ -1361,7 +1361,7 @@ ProcessSSDPData(int s, const char *bufr, ++ #endif ++ if(0 == memcmp(st, uuidvalue_igd, l)) ++ { ++- syslog(LOG_INFO, "ssdp:uuid (IGD) found"); +++ syslog(LOG_DEBUG, "ssdp:uuid (IGD) found"); ++ SendSSDPResponse(s, sender, st, st_len, "", ++ announced_host, http_port, ++ #ifdef ENABLE_HTTPS ++@@ -1371,7 +1371,7 @@ ProcessSSDPData(int s, const char *bufr, ++ } ++ else if(0 == memcmp(st, uuidvalue_wan, l)) ++ { ++- syslog(LOG_INFO, "ssdp:uuid (WAN) found"); +++ syslog(LOG_DEBUG, "ssdp:uuid (WAN) found"); ++ SendSSDPResponse(s, sender, st, st_len, "", ++ announced_host, http_port, ++ #ifdef ENABLE_HTTPS ++@@ -1381,7 +1381,7 @@ ProcessSSDPData(int s, const char *bufr, ++ } ++ else if(0 == memcmp(st, uuidvalue_wcd, l)) ++ { ++- syslog(LOG_INFO, "ssdp:uuid (WCD) found"); +++ syslog(LOG_DEBUG, "ssdp:uuid (WCD) found"); ++ SendSSDPResponse(s, sender, st, st_len, "", ++ announced_host, http_port, ++ #ifdef ENABLE_HTTPS ++@@ -1442,7 +1442,7 @@ SendSSDPbyebye(int s, const struct socka ++ n = sendto_or_schedule(s, bufr, l, 0, dest, destlen); ++ if(n < 0) ++ { ++- syslog(LOG_ERR, "sendto(udp_shutdown=%d) to %s: %m", s, dest_str); +++ syslog(LOG_DEBUG, "sendto(udp_shutdown=%d) to %s: %m", s, dest_str); ++ return -1; ++ } ++ else if(n != l) ++--- a/miniupnpd.c +++++ b/miniupnpd.c ++@@ -489,7 +489,7 @@ ProcessIncomingHTTP(int shttpl, const ch ++ if(get_lan_for_peer((struct sockaddr *)&clientname) == NULL) ++ { ++ /* The peer is not a LAN ! */ ++- syslog(LOG_WARNING, +++ syslog(LOG_DEBUG, ++ "%s peer %s is not from a LAN, closing the connection", ++ protocol, addr_str); ++ close(shttp); ++@@ -859,7 +859,7 @@ set_startup_time(void) ++ } ++ else ++ { ++- syslog(LOG_INFO, "system uptime is %lu seconds", uptime); +++ syslog(LOG_DEBUG, "system uptime is %lu seconds", uptime); ++ } ++ fclose(f); ++ startup_time -= uptime; ++@@ -2109,6 +2109,22 @@ init(int argc, char * * argv, struct run ++ pidfilename = NULL; ++ #endif ++ +++syslog(LOG_NOTICE, "MiniUPnP daemon " MINIUPNPD_VERSION " starting, enable protocols %s%s%s, ext_ifname=%s BOOTID=%u", +++ GETFLAG(ENABLEUPNPMASK) ? "UPnP IGD" : "", +++#ifdef ENABLE_NATPMP +++ GETFLAG(ENABLEUPNPMASK) && GETFLAG(ENABLENATPMPMASK) ? " & " : "", +++#ifdef ENABLE_PCP +++ GETFLAG(ENABLENATPMPMASK) ? "PCP/NAT-PMP" : "", +++#else +++ GETFLAG(ENABLENATPMPMASK) ? "NAT-PMP" : "", +++#endif +++#else +++ "", "", +++#endif +++ ext_if_name, upnp_bootid); +++syslog(LOG_INFO, "More information at https://miniupnp.tuxfamily.org/ or http://miniupnp.free.fr/"); +++syslog(LOG_NOTICE, "Extra logging with log level info (-v) or debug (-v -v)"); +++ ++ #ifdef USE_SYSTEMD ++ if (systemd_flag) { ++ int r = sd_notify(0, ++@@ -2121,7 +2137,7 @@ init(int argc, char * * argv, struct run ++ ++ #ifdef ENABLE_LEASEFILE ++ /*remove(lease_file);*/ ++- syslog(LOG_INFO, "Reloading rules from lease file"); +++ syslog(LOG_INFO, "Reloading port maps from lease file"); ++ reload_from_lease_file(); ++ #ifdef ENABLE_UPNPPINHOLE ++ reload_from_lease_file6(); ++@@ -2409,21 +2425,9 @@ main(int argc, char * * argv) ++ return 0; ++ } ++ ++- syslog(LOG_INFO, "version " MINIUPNPD_VERSION " starting%s%sext if %s BOOTID=%u", ++-#ifdef ENABLE_NATPMP ++-#ifdef ENABLE_PCP ++- GETFLAG(ENABLENATPMPMASK) ? " NAT-PMP/PCP " : " ", ++-#else ++- GETFLAG(ENABLENATPMPMASK) ? " NAT-PMP " : " ", ++-#endif ++-#else ++- " ", ++-#endif ++- GETFLAG(ENABLEUPNPMASK) ? "UPnP-IGD " : "", ++- ext_if_name, upnp_bootid); ++ #ifdef ENABLE_IPV6 ++ if (strcmp(ext_if_name6, ext_if_name) != 0) { ++- syslog(LOG_INFO, "specific IPv6 ext if %s", ext_if_name6); +++ syslog(LOG_INFO, "Separate ext_ifname6=%s set", ext_if_name6); ++ } ++ #endif ++ ++@@ -2467,7 +2471,7 @@ main(int argc, char * * argv) ++ return 1; ++ } ++ v.port = listen_port; ++- syslog(LOG_NOTICE, "HTTP listening on port %d", v.port); +++ syslog(LOG_NOTICE, "Listening for UPnP IGD (SOAP/HTTP) traffic on port %d/TCP, SSDP 1900/UDP", v.port); ++ #if defined(V6SOCKETS_ARE_V6ONLY) && defined(ENABLE_IPV6) ++ if(!GETFLAG(IPV6DISABLEDMASK)) ++ { ++@@ -2493,7 +2497,7 @@ main(int argc, char * * argv) ++ return 1; ++ } ++ v.https_port = listen_port; ++- syslog(LOG_NOTICE, "HTTPS listening on port %d", v.https_port); +++ syslog(LOG_NOTICE, "Listening for UPnP IGD (SOAP/HTTPS) traffic on port %d/TCP", v.https_port); ++ #if defined(V6SOCKETS_ARE_V6ONLY) && defined(ENABLE_IPV6) ++ shttpsl_v4 = OpenAndConfHTTPSocket(&listen_port, 0); ++ if(shttpsl_v4 < 0) ++@@ -2507,11 +2511,11 @@ main(int argc, char * * argv) ++ if(!GETFLAG(IPV6DISABLEDMASK)) { ++ if(find_ipv6_addr(lan_addrs.lh_first ? lan_addrs.lh_first->ifname : NULL, ++ ipv6_addr_for_http_with_brackets, sizeof(ipv6_addr_for_http_with_brackets)) > 0) { ++- syslog(LOG_NOTICE, "HTTP IPv6 address given to control points : %s", +++ syslog(LOG_NOTICE, "IPv6 address given to UPnP IGD clients: %s", ++ ipv6_addr_for_http_with_brackets); ++ } else { ++ memcpy(ipv6_addr_for_http_with_brackets, "[::1]", 6); ++- syslog(LOG_WARNING, "no HTTP IPv6 address, disabling IPv6"); +++ syslog(LOG_DEBUG, "no HTTP IPv6 address, disabling IPv6"); ++ SETFLAG(IPV6DISABLEDMASK); ++ } ++ } ++@@ -2563,7 +2567,7 @@ main(int argc, char * * argv) ++ if(SendSSDPGoodbye(snotify, addr_count * 2) < 0) ++ #endif ++ { ++- syslog(LOG_WARNING, "Failed to broadcast good-bye notifications"); +++ syslog(LOG_DEBUG, "Failed to broadcast good-bye notifications"); ++ } ++ } ++ #endif /* UPNP_STRICT */ ++@@ -2588,16 +2592,16 @@ main(int argc, char * * argv) ++ if(OpenAndConfNATPMPSockets(snatpmp) < 0) ++ #ifdef ENABLE_PCP ++ { ++- syslog(LOG_ERR, "Failed to open sockets for NAT-PMP/PCP."); +++ syslog(LOG_ERR, "Failed to open port 5351/UDP for PCP/NAT-PMP"); ++ } else { ++- syslog(LOG_NOTICE, "Listening for NAT-PMP/PCP traffic on port %u", +++ syslog(LOG_NOTICE, "Listening for PCP/NAT-PMP traffic on port %u/UDP", ++ NATPMP_PORT); ++ } ++ #else ++ { ++- syslog(LOG_ERR, "Failed to open sockets for NAT PMP."); +++ syslog(LOG_ERR, "Failed to open port 5351/UDP for NAT-PMP"); ++ } else { ++- syslog(LOG_NOTICE, "Listening for NAT-PMP traffic on port %u", +++ syslog(LOG_NOTICE, "Listening for NAT-PMP traffic on port %u/UDP", ++ NATPMP_PORT); ++ } ++ #endif ++@@ -2706,6 +2710,30 @@ main(int argc, char * * argv) ++ } ++ #endif /* HAS_LIBCAP_NG */ ++ +++if (GETFLAG(ENABLEUPNPMASK) && !GETFLAG(SECUREMODEMASK)) +++ syslog(LOG_WARNING, "WARNING: Allow adding port maps for non-requesting IP addresses via UPnP IGD, as secure_mode=no set"); +++#ifdef ENABLE_PCP +++if (GETFLAG(ENABLENATPMPMASK) && GETFLAG(PCP_ALLOWTHIRDPARTYMASK)) +++ syslog(LOG_WARNING, "WARNING: Allow adding port maps for non-requesting IP addresses via PCP, as pcp_allow_thirdparty=yes set"); +++#endif +++#ifdef ENABLE_IPV6 +++if (GETFLAG(IPV6DISABLEDMASK)) +++ syslog(LOG_NOTICE, "IPv6 mapping disabled"); +++#else +++syslog(LOG_NOTICE, "IPv6 mapping disabled"); +++#endif +++if (GETFLAG(ENABLEUPNPMASK)) { +++#ifdef IGD_V2 +++ if (GETFLAG(FORCEIGDDESCV1MASK)) { +++ syslog(LOG_NOTICE, "UPnP IGD compatibility mode set to IGDv1 (IPv4 only)"); +++ } else { +++ syslog(LOG_NOTICE, "UPnP IGD compatibility mode set to IGDv2 (with workarounds)"); +++ } +++#else +++ syslog(LOG_NOTICE, "UPnP IGD compatibility mode set to IGDv1 (IPv4 only)"); +++#endif +++} +++ ++ #ifdef USE_SYSTEMD ++ if (v.systemd_notify) { ++ upnp_update_status(); ++@@ -3016,7 +3044,7 @@ main(int argc, char * * argv) ++ } ++ i = try_sendto(&writeset); ++ if(i < 0) { ++- syslog(LOG_ERR, "try_sendto failed to send %d packets", -i); +++ /*syslog(LOG_DEBUG, "try_sendto failed to send %d packets", -i);*/ ++ } ++ #ifdef USE_MINIUPNPDCTL ++ for(ectl = ctllisthead.lh_first; ectl;) ++@@ -3116,7 +3144,7 @@ main(int argc, char * * argv) ++ if(lan_addr == NULL) { ++ char sender_str[64]; ++ sockaddr_to_string((struct sockaddr *)&senderaddr, sender_str, sizeof(sender_str)); ++- syslog(LOG_WARNING, "NAT-PMP packet sender %s not from a LAN, ignoring", +++ syslog(LOG_DEBUG, "NAT-PMP packet sender %s not from a LAN, ignoring", ++ sender_str); ++ continue; ++ } ++@@ -3139,7 +3167,7 @@ main(int argc, char * * argv) ++ if(lan_addr == NULL) { ++ char sender_str[64]; ++ sockaddr_to_string((struct sockaddr *)&senderaddr, sender_str, sizeof(sender_str)); ++- syslog(LOG_WARNING, "NAT-PMP packet sender %s not from a LAN, ignoring", +++ syslog(LOG_DEBUG, "NAT-PMP packet sender %s not from a LAN, ignoring", ++ sender_str); ++ continue; ++ } ++@@ -3173,7 +3201,7 @@ main(int argc, char * * argv) ++ /* process SSDP packets */ ++ if(sudp >= 0 && FD_ISSET(sudp, &readset)) ++ { ++- /*syslog(LOG_INFO, "Received UDP Packet");*/ +++ /*syslog(LOG_DEBUG, "Received UDP Packet");*/ ++ #ifdef ENABLE_HTTPS ++ ProcessSSDPRequest(sudp, (unsigned short)v.port, (unsigned short)v.https_port); ++ #else ++@@ -3183,7 +3211,7 @@ main(int argc, char * * argv) ++ #ifdef ENABLE_IPV6 ++ if(sudpv6 >= 0 && FD_ISSET(sudpv6, &readset)) ++ { ++- syslog(LOG_INFO, "Received UDP Packet (IPv6)"); +++ /*syslog(LOG_DEBUG, "Received UDP Packet (IPv6)");*/ ++ #ifdef ENABLE_HTTPS ++ ProcessSSDPRequest(sudpv6, (unsigned short)v.port, (unsigned short)v.https_port); ++ #else ++@@ -3285,7 +3313,7 @@ main(int argc, char * * argv) ++ ++ shutdown: ++ ++- syslog(LOG_NOTICE, "shutting down MiniUPnPd"); +++ syslog(LOG_NOTICE, "Shutting down MiniUPnPd"); ++ #ifdef USE_SYSTEMD ++ if (v.systemd_notify) { ++ sd_notify(0, ++@@ -3304,7 +3332,7 @@ shutdown: ++ if(SendSSDPGoodbye(snotify, addr_count * 2) < 0) ++ #endif ++ { ++- syslog(LOG_ERR, "Failed to broadcast good-bye notifications"); +++ syslog(LOG_DEBUG, "Failed to broadcast good-bye notifications"); ++ } ++ } ++ /* try to send pending packets */ ++--- a/natpmp.c +++++ b/natpmp.c ++@@ -106,7 +106,7 @@ static void FillPublicAddressResponse(un ++ resp[3] = 3; /* Network Failure (e.g. NAT box itself ++ * has not obtained a DHCP lease) */ ++ } else if(getifaddr(ext_if_name, tmp, INET_ADDRSTRLEN, &addr, NULL) < 0) { ++- syslog(LOG_ERR, "Failed to get IP for interface %s", ext_if_name); +++ syslog(LOG_DEBUG, "Failed to get IP for interface %s", ext_if_name); ++ resp[3] = 3; /* Network Failure (e.g. NAT box itself ++ * has not obtained a DHCP lease) */ ++ } else if (!GETFLAG(ALLOWPRIVATEIPV4MASK) && addr_is_reserved(&addr)) { ++@@ -231,7 +231,7 @@ void ProcessIncomingNATPMPPacket(int s, ++ syslog(LOG_ERR, "inet_ntop(natpmp): %m"); ++ } ++ ++- syslog(LOG_INFO, "NAT-PMP request received from %s:%hu %dbytes", +++ syslog(LOG_DEBUG, "NAT-PMP request received from %s:%hu %d bytes", ++ senderaddrstr, ntohs(senderaddr->sin_port), n); ++ ++ if(n<2 || ((((req[1]-1)&~1)==0) && n<12)) { ++@@ -260,7 +260,7 @@ void ProcessIncomingNATPMPPacket(int s, ++ resp[3] = 1; /* unsupported version */ ++ } else switch(req[1]) { ++ case 0: /* Public address request */ ++- syslog(LOG_INFO, "NAT-PMP public address request"); +++ syslog(LOG_DEBUG, "NAT-PMP public address request"); ++ FillPublicAddressResponse(resp, senderaddr->sin_addr.s_addr); ++ resplen = 12; ++ break; ++@@ -319,7 +319,7 @@ void ProcessIncomingNATPMPPacket(int s, ++ resp[3] = 2; /* Not Authorized/Refused */ ++ break; ++ } else { ++- syslog(LOG_INFO, "NAT-PMP %s port %hu mapping removed", +++ syslog(LOG_DEBUG, "NAT-PMP %s port %hu mapping removed", ++ proto2==IPPROTO_TCP?"TCP":"UDP", eport2); ++ index--; ++ } ++@@ -340,7 +340,7 @@ void ProcessIncomingNATPMPPacket(int s, ++ eport_first = eport; ++ } else if(eport == eport_first) { /* no eport available */ ++ if(any_eport_allowed == 0) { /* all eports rejected by permissions */ ++- syslog(LOG_ERR, "No allowed eport for NAT-PMP %hu %s->%s:%hu", +++ syslog(LOG_INFO, "No allowed eport for NAT-PMP %hu %s->%s:%hu", ++ eport, proto_itoa(proto), senderaddrstr, iport); ++ resp[3] = 2; /* Not Authorized/Refused */ ++ } else { /* at least one eport allowed (but none available) */ ++@@ -443,7 +443,7 @@ void SendNATPMPPublicAddressChangeNotifi ++ FillPublicAddressResponse(notif, 0); ++ if(notif[3]) ++ { ++- syslog(LOG_WARNING, "%s: cannot get public IP address, stopping", +++ syslog(LOG_DEBUG, "%s: cannot get public IP address, stopping", ++ "SendNATPMPPublicAddressChangeNotification"); ++ return; ++ } ++@@ -471,7 +471,7 @@ void SendNATPMPPublicAddressChangeNotifi ++ (struct sockaddr *)&sockname, sizeof(struct sockaddr_in)); ++ if(n < 0) ++ { ++- syslog(LOG_ERR, "%s: sendto(s_udp=%d, port=%d): %m", +++ syslog(LOG_DEBUG, "%s: sendto(s_udp=%d, port=%d): %m", ++ "SendNATPMPPublicAddressChangeNotification", sockets[j], NATPMP_PORT); ++ return; ++ } ++@@ -481,7 +481,7 @@ void SendNATPMPPublicAddressChangeNotifi ++ (struct sockaddr *)&sockname, sizeof(struct sockaddr_in)); ++ if(n < 0) ++ { ++- syslog(LOG_ERR, "%s: sendto(s_udp=%d, port=%d): %m", +++ syslog(LOG_DEBUG, "%s: sendto(s_udp=%d, port=%d): %m", ++ "SendNATPMPPublicAddressChangeNotification", sockets[j], NATPMP_NOTIF_PORT); ++ return; ++ } ++--- a/netfilter/iptcrdr.c +++++ b/netfilter/iptcrdr.c ++@@ -711,7 +711,7 @@ delete_filter_rule(const char * ifname, ++ continue; ++ } ++ index = i; ++- /*syslog(LOG_INFO, "Trying to delete filter rule at index %u", index);*/ +++ /*syslog(LOG_DEBUG, "Trying to delete filter rule at index %u", index);*/ ++ r = delete_rule_and_commit(index, h, miniupnpd_forward_chain, "delete_filter_rule"); ++ h = NULL; ++ break; ++@@ -804,7 +804,7 @@ delete_redirect_and_filter_rules(unsigne ++ #endif ++ if(r == 0) ++ { ++- syslog(LOG_INFO, "Trying to delete nat rule at index %u", index); +++ syslog(LOG_DEBUG, "Trying to delete nat rule at index %u", index); ++ /* Now delete both rules */ ++ /* first delete the nat rule */ ++ h = iptc_init("nat"); ++@@ -848,7 +848,7 @@ delete_redirect_and_filter_rules(unsigne ++ if(iaddr != e->ip.dst.s_addr) ++ continue; ++ index = i; ++- syslog(LOG_INFO, "Trying to delete filter rule at index %u", index); +++ syslog(LOG_DEBUG, "Trying to delete filter rule at index %u", index); ++ r = delete_rule_and_commit(index, h, miniupnpd_forward_chain, "delete_filter_rule"); ++ h = NULL; ++ break; ++@@ -906,7 +906,7 @@ delete_redirect_and_filter_rules(unsigne ++ } ++ ++ index = i; ++- syslog(LOG_INFO, "Trying to delete peer rule at index %u", index); +++ syslog(LOG_DEBUG, "Trying to delete peer rule at index %u", index); ++ r2 = delete_rule_and_commit(index, h, miniupnpd_nat_postrouting_chain, "delete_peer_rule"); ++ h = NULL; ++ break; ++@@ -958,7 +958,7 @@ delete_redirect_and_filter_rules(unsigne ++ if(iaddr != e->ip.src.s_addr) ++ continue; ++ index = i; ++- syslog(LOG_INFO, "Trying to delete dscp rule at index %u", index); +++ syslog(LOG_DEBUG, "Trying to delete dscp rule at index %u", index); ++ r2 = delete_rule_and_commit(index, h, miniupnpd_nat_chain, "delete_dscp_rule"); ++ h = NULL; ++ break; ++--- a/netfilter/iptpinhole.c +++++ b/netfilter/iptpinhole.c ++@@ -295,14 +295,14 @@ find_pinhole(const char * ifname, ++ ++ if(rem_host && (rem_host[0] != '\0')) { ++ if (inet_pton(AF_INET6, rem_host, &saddr) < 1) { ++- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", rem_host); +++ syslog(LOG_INFO, "Failed to parse INET6 address \"%s\"", rem_host); ++ memset(&saddr, 0, sizeof(struct in6_addr)); ++ } ++ } else { ++ memset(&saddr, 0, sizeof(struct in6_addr)); ++ } ++ if (inet_pton(AF_INET6, int_client, &daddr) < 1) { ++- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", int_client); +++ syslog(LOG_INFO, "Failed to parse INET6 address \"%s\"", int_client); ++ memset(&daddr, 0, sizeof(struct in6_addr)); ++ } ++ for(p = pinhole_list.lh_first; p != NULL; p = p->entries.le_next) { ++--- a/netfilter_nft/nftnlrdr.c +++++ b/netfilter_nft/nftnlrdr.c ++@@ -353,7 +353,7 @@ delete_redirect_and_filter_rules(unsigne ++ } ++ } ++ } else { ++- syslog(LOG_WARNING, "%s: redirect rule with eport=%hu proto %d NOT FOUND", +++ syslog(LOG_INFO, "%s: redirect rule with eport=%hu proto %d NOT FOUND", ++ "delete_redirect_and_filter_rules", eport, proto); ++ } ++ ++--- a/netfilter_nft/nftnlrdr_misc.c +++++ b/netfilter_nft/nftnlrdr_misc.c ++@@ -104,7 +104,7 @@ nft_mnl_connect(void) ++ return -1; ++ } ++ mnl_portid = mnl_socket_get_portid(mnl_sock); ++- syslog(LOG_INFO, "mnl_socket bound, port_id=%u", mnl_portid); +++ syslog(LOG_DEBUG, "mnl_socket bound, port_id=%u", mnl_portid); ++ return 0; ++ } ++ ++@@ -745,7 +745,7 @@ refresh_nft_cache(struct rule_list *head ++ errno = 0; ++ ret = mnl_cb_run(buf, n, mnl_seq, mnl_portid, table_cb, &data); ++ if (ret <= -1 /*== MNL_CB_ERROR*/) { ++- syslog(LOG_ERR, "%s: mnl_cb_run returned %d: %m", +++ syslog(LOG_DEBUG, "%s: mnl_cb_run returned %d: %m", ++ "refresh_nft_cache", ret); ++ return -1; ++ } ++@@ -1279,7 +1279,7 @@ nft_send_rule(struct nftnl_rule * rule, ++ ++ result = send_batch(batch); ++ if (result < 0) { ++- syslog(LOG_ERR, "%s(%p, %d, %d) send_batch failed %d", +++ syslog(LOG_DEBUG, "%s(%p, %d, %d) send_batch failed %d", ++ "nft_send_rule", rule, (int)cmd, (int)chain_type, result); ++ } ++ } ++@@ -1462,7 +1462,7 @@ send_batch(struct mnl_nlmsg_batch *batch ++ errno = 0; ++ ret = mnl_cb_run(buf, n, 0, mnl_portid, NULL, NULL); ++ if (ret <= -1 /*== MNL_CB_ERROR*/) { ++- syslog(LOG_ERR, "%s: mnl_cb_run returned %d: %m", +++ syslog(LOG_DEBUG, "%s: mnl_cb_run returned %d: %m", ++ "send_batch", ret); ++ return -4; ++ } ++--- a/netfilter_nft/nftpinhole.c +++++ b/netfilter_nft/nftpinhole.c ++@@ -129,7 +129,7 @@ find_pinhole(const char * ifname, ++ ++ if (rem_host && rem_host[0] != '\0' && rem_host[0] != '*') { ++ if (inet_pton(AF_INET6, rem_host, &saddr) < 1) { ++- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", rem_host); +++ syslog(LOG_INFO, "Failed to parse INET6 address \"%s\"", rem_host); ++ memset(&saddr, 0, sizeof(struct in6_addr)); ++ } ++ } else { ++@@ -137,7 +137,7 @@ find_pinhole(const char * ifname, ++ } ++ ++ if (inet_pton(AF_INET6, int_client, &daddr) < 1) { ++- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", int_client); +++ syslog(LOG_INFO, "Failed to parse INET6 address \"%s\"", int_client); ++ memset(&daddr, 0, sizeof(struct in6_addr)); ++ } ++ ++@@ -159,7 +159,7 @@ find_pinhole(const char * ifname, ++ (0 == memcmp(&daddr, &p->daddr6, sizeof(struct in6_addr)))) { ++ ++ if (sscanf(p->desc, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) { ++- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc); +++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);*/ ++ continue; ++ } ++ ++@@ -396,7 +396,7 @@ get_pinhole_info(unsigned short uid, ++ if (timestamp) { ++ int uid_temp; ++ if (sscanf(p->desc, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid_temp, &ts) != 2) { ++- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc); +++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);*/ ++ continue; ++ } ++ ++@@ -460,7 +460,7 @@ clean_pinhole_list(unsigned int * next_t ++ continue; ++ ++ if (sscanf(p->desc, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) { ++- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc); +++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);*/ ++ continue; ++ } ++ ++--- a/pcpserver.c +++++ b/pcpserver.c ++@@ -1086,7 +1086,7 @@ static void CreatePCPMap(pcp_info_t *pcp ++ else ++ r = CreatePCPMap_NAT(pcp_msg_info); ++ pcp_msg_info->result_code = r; ++- syslog(r == PCP_SUCCESS ? LOG_INFO : LOG_ERR, +++ syslog(LOG_INFO, ++ "PCP MAP: %s mapping %s %hu->%s:%hu '%s'", ++ r == PCP_SUCCESS ? "added" : "failed to add", ++ proto_itoa(pcp_msg_info->protocol), ++@@ -1128,7 +1128,7 @@ static void DeletePCPMap(pcp_info_t *pcp ++ if(0 != strcmp(desc, pcp_msg_info->desc)) { ++ /* nonce does not match */ ++ pcp_msg_info->result_code = PCP_ERR_NOT_AUTHORIZED; ++- syslog(LOG_ERR, "Unauthorized to remove PCP mapping internal port %hu, protocol %s", +++ syslog(LOG_INFO, "Unauthorized to remove PCP mapping internal port %hu, protocol %s", ++ iport, proto_itoa(pcp_msg_info->protocol)); ++ return; ++ } else { ++@@ -1146,14 +1146,14 @@ static void DeletePCPMap(pcp_info_t *pcp ++ desc, sizeof(desc), ++ NULL /* lifetime */); ++ if (uid < 0) { ++- syslog(LOG_ERR, "Failed to find mapping to %s:%hu, protocol %s", +++ syslog(LOG_INFO, "Failed to find mapping to %s:%hu, protocol %s", ++ pcp_msg_info->mapped_str, iport, proto_itoa(pcp_msg_info->protocol)); ++ return; ++ } else { ++ if(0 != strcmp(desc, pcp_msg_info->desc)) { ++ /* nonce does not match */ ++ pcp_msg_info->result_code = PCP_ERR_NOT_AUTHORIZED; ++- syslog(LOG_ERR, "Unauthorized to remove PCP mapping internal port %hu, protocol %s", +++ syslog(LOG_INFO, "Unauthorized to remove PCP mapping internal port %hu, protocol %s", ++ iport, proto_itoa(pcp_msg_info->protocol)); ++ return; ++ } else { ++@@ -1168,7 +1168,7 @@ static void DeletePCPMap(pcp_info_t *pcp ++ syslog(LOG_INFO, "PCP: %s port %hu mapping removed", ++ proto==IPPROTO_TCP?"TCP":"UDP", (pcp_msg_info->is_fw ? iport : eport2)); ++ } else { ++- syslog(LOG_ERR, "Failed to remove PCP mapping to %s:%hu %s", +++ syslog(LOG_INFO, "Failed to remove PCP mapping to %s:%hu %s", ++ pcp_msg_info->mapped_str, iport, proto_itoa(proto)); ++ pcp_msg_info->result_code = PCP_ERR_NO_RESOURCES; ++ } ++@@ -1391,7 +1391,7 @@ static int processPCPRequest(void * req, ++ CreatePCPMap(pcp_msg_info); ++ } ++ } else { ++- syslog(LOG_ERR, "PCP: Invalid PCP v2 MAP message."); +++ syslog(LOG_DEBUG, "PCP: Invalid PCP v2 MAP message."); ++ return pcp_msg_info->result_code; ++ } ++ ++@@ -1601,7 +1601,7 @@ int ProcessIncomingPCPPacket(int s, unsi ++ if (!GETFLAG(PCP_ALLOWTHIRDPARTYMASK)) { ++ lan_addr = get_lan_for_peer(senderaddr); ++ if(lan_addr == NULL) { ++- syslog(LOG_WARNING, "PCP packet sender %s not from a LAN, ignoring", +++ syslog(LOG_DEBUG, "PCP packet sender %s not from a LAN, ignoring", ++ addr_str); ++ return 0; ++ } ++@@ -1722,7 +1722,7 @@ void PCPSendUnsolicitedAnnounce(int * so ++ } ++ len = sendto_or_schedule(sockets[i], buff, PCP_MIN_LEN, 0, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)); ++ if( len < 0 ) { ++- syslog(LOG_ERR, "PCPSendUnsolicitedAnnounce(sockets[%d]) sendto(): %m", i); +++ syslog(LOG_DEBUG, "PCPSendUnsolicitedAnnounce(sockets[%d]) sendto(): %m", i); ++ } ++ } ++ #ifdef ENABLE_IPV6 ++@@ -1733,7 +1733,7 @@ void PCPSendUnsolicitedAnnounce(int * so ++ addr6.sin6_port = htons(5350); ++ len = sendto_or_schedule(socket6, buff, PCP_MIN_LEN, 0, (struct sockaddr *)&addr6, sizeof(struct sockaddr_in6)); ++ if( len < 0 ) { ++- syslog(LOG_ERR, "PCPSendUnsolicitedAnnounce() IPv6 sendto(): %m"); +++ syslog(LOG_DEBUG, "PCPSendUnsolicitedAnnounce() IPv6 sendto(): %m"); ++ } ++ } ++ #endif /* ENABLE_IPV6 */ ++--- a/pf/pfpinhole.c +++++ b/pf/pfpinhole.c ++@@ -246,12 +246,12 @@ int find_pinhole(const char * ifname, ++ (0 == memcmp(&daddr, &RULE.dst.addr.v.a.addr.v6, sizeof(struct in6_addr)))) { ++ #ifdef USE_LIBPFCTL ++ if(sscanf(RULE.label[0], PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) { ++- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]); +++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);*/ ++ continue; ++ } ++ #else /* USE_LIBPFCTL */ ++ if(sscanf(RULE.label, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) { ++- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label); +++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);*/ ++ continue; ++ } ++ #endif /* USE_LIBPFCTL */ ++@@ -565,7 +565,7 @@ int clean_pinhole_list(unsigned int * ne ++ return -1; ++ } ++ if(sscanf(RULE.label[0], PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) { ++- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]); +++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);*/ ++ continue; ++ } ++ #else /* USE_LIBPFCTL */ ++@@ -576,7 +576,7 @@ int clean_pinhole_list(unsigned int * ne ++ return -1; ++ } ++ if(sscanf(RULE.label, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) { ++- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label); +++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);*/ ++ continue; ++ } ++ #endif /* USE_LIBPFCTL */ ++--- a/upnphttp.c +++++ b/upnphttp.c ++@@ -514,7 +514,7 @@ ProcessHTTPPOST_upnphttp(struct upnphttp ++ if(h->req_soapActionOff > 0) ++ { ++ /* we can process the request */ ++- syslog(LOG_INFO, "SOAPAction: %.*s", +++ syslog(LOG_DEBUG, "SOAPAction: %.*s", ++ h->req_soapActionLen, h->req_buf + h->req_soapActionOff); ++ ExecuteSoapAction(h, ++ h->req_buf + h->req_soapActionOff, ++@@ -819,7 +819,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt ++ for(i = 0; i<15 && *p != '\r'; i++) ++ HttpVer[i] = *(p++); ++ HttpVer[i] = '\0'; ++- syslog(LOG_INFO, "HTTP REQUEST from %s : %s %s (%s)", +++ syslog(LOG_DEBUG, "HTTP REQUEST from %s : %s %s (%s)", ++ h->clientaddr_str, HttpCommand, HttpUrl, HttpVer); ++ ParseHttpHeaders(h); ++ if(h->req_HostOff > 0 && h->req_HostLen > 0) { ++@@ -899,7 +899,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt ++ return; ++ } ++ #endif ++- syslog(LOG_NOTICE, "%s not found, responding ERROR 404", HttpUrl); +++ syslog(LOG_DEBUG, "%s not found, responding ERROR 404", HttpUrl); ++ Send404(h); ++ } ++ #ifdef ENABLE_EVENTS ++--- a/upnppinhole.c +++++ b/upnppinhole.c ++@@ -110,7 +110,7 @@ lease_file6_add(const char * rem_client, ++ ++ fd = fopen( lease_file6, "a"); ++ if (fd==NULL) { ++- syslog(LOG_ERR, "could not open lease file: %s", lease_file); +++ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file); ++ return -1; ++ } ++ ++@@ -189,13 +189,13 @@ lease_file6_update(int uid, unsigned int ++ proto = line; ++ p = strchr(line, ';'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ p2 = strchr(p, ';'); ++ if(!p2) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p2++) = '\0'; ++@@ -203,13 +203,13 @@ lease_file6_update(int uid, unsigned int ++ int_client = p; ++ p = strchr(p2, ';'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ p2 = strchr(p, ';'); ++ if(!p2) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p2++) = '\0'; ++@@ -217,20 +217,20 @@ lease_file6_update(int uid, unsigned int ++ rem_client = p; ++ p = strchr(p2, ';'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ p2 = strchr(p, ';'); ++ if(!p2) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p2++) = '\0'; ++ desc = strchr(p2, ';'); ++ uid_rule = atoi(p); ++ if(!desc) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(desc++) = '\0'; ++@@ -385,49 +385,49 @@ int lease_file6_expire(void) ++ // Internal Host ++ p = strchr(line, ';'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ // Internal Port ++ p2 = strchr(p, ';'); ++ if(!p2) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p2++) = '\0'; ++ // External Host ++ p = strchr(p2, ';'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ // External Port ++ p2 = strchr(p, ';'); ++ if(!p2) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p2++) = '\0'; ++ // uid ++ p = strchr(p2, ';'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ // Timestamp ++ p2 = strchr(p, ';'); ++ if(!p2) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p2++) = '\0'; ++ // descr ++ desc = strchr(p2, ';'); ++ if(!desc) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(desc++) = '\0'; ++@@ -476,7 +476,7 @@ int reload_from_lease_file6(void) ++ if(!lease_file6) return -1; ++ fd = fopen( lease_file6, "r"); ++ if (fd==NULL) { ++- syslog(LOG_ERR, "could not open lease file: %s", lease_file6); +++ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file6); ++ return -1; ++ } ++ if(unlink(lease_file6) < 0) { ++@@ -491,13 +491,13 @@ int reload_from_lease_file6(void) ++ proto = line; ++ p = strchr(line, ';'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ p2 = strchr(p, ';'); ++ if(!p2) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p2++) = '\0'; ++@@ -505,13 +505,13 @@ int reload_from_lease_file6(void) ++ int_client = p; ++ p = strchr(p2, ';'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ p2 = strchr(p, ';'); ++ if(!p2) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p2++) = '\0'; ++@@ -519,20 +519,20 @@ int reload_from_lease_file6(void) ++ rem_client = p; ++ p = strchr(p2, ';'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ p2 = strchr(p, ';'); ++ if(!p2) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p2++) = '\0'; ++ desc = strchr(p2, ';'); ++ uid = atoi(p); ++ if(!desc) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(desc++) = '\0'; ++@@ -549,7 +549,7 @@ int reload_from_lease_file6(void) ++ ++ if(timestamp > 0) { ++ if(timestamp <= (unsigned int)current_unix_time) { ++- syslog(LOG_NOTICE, "already expired lease in lease file"); +++ syslog(LOG_INFO, "already expired lease in lease file"); ++ continue; ++ } else { ++ leaseduration = timestamp - current_unix_time; ++--- a/upnpredirect.c +++++ b/upnpredirect.c ++@@ -66,7 +66,7 @@ lease_file_add(unsigned short eport, ++ ++ fd = fopen( lease_file, "a"); ++ if (fd==NULL) { ++- syslog(LOG_ERR, "could not open lease file: %s", lease_file); +++ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file); ++ return -1; ++ } ++ ++@@ -168,7 +168,7 @@ int reload_from_lease_file(void) ++ if(!lease_file) return -1; ++ fd = fopen( lease_file, "r"); ++ if (fd==NULL) { ++- syslog(LOG_ERR, "could not open lease file: %s", lease_file); +++ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file); ++ return -1; ++ } ++ if(unlink(lease_file) < 0) { ++@@ -184,33 +184,33 @@ int reload_from_lease_file(void) ++ proto = line; ++ p = strchr(line, ':'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ iaddr = strchr(p, ':'); ++ if(!iaddr) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(iaddr++) = '\0'; ++ eport = (unsigned short)atoi(p); ++ p = strchr(iaddr, ':'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ iport = (unsigned short)atoi(p); ++ p = strchr(p, ':'); ++ if(!p) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(p++) = '\0'; ++ desc = strchr(p, ':'); ++ if(!desc) { ++- syslog(LOG_ERR, "unrecognized data in lease file"); +++ syslog(LOG_DEBUG, "unrecognized data in lease file"); ++ continue; ++ } ++ *(desc++) = '\0'; ++@@ -231,7 +231,7 @@ int reload_from_lease_file(void) ++ timestamp += current_time; /* convert to our time */ ++ #else ++ if(timestamp <= (unsigned int)current_unix_time) { ++- syslog(LOG_NOTICE, "already expired lease in lease file (%hu=>%s:%hu %s)", +++ syslog(LOG_INFO, "already expired lease in lease file (%hu=>%s:%hu %s)", ++ eport, iaddr, iport, proto); ++ continue; ++ } else { ++@@ -391,7 +391,7 @@ upnp_redirect(const char * rhost, unsign ++ #endif /* CHECK_PORTINUSE */ ++ } else { ++ timestamp = (leaseduration > 0) ? upnp_time() + leaseduration : 0; ++- syslog(LOG_INFO, "redirecting port %hu to %s:%hu protocol %s for: %s", +++ syslog(LOG_DEBUG, "redirecting port %hu to %s:%hu protocol %s for: %s", ++ eport, iaddr, iport, protocol, desc); ++ return upnp_redirect_internal(rhost, eport, iaddr, iport, proto, ++ desc, timestamp); ++@@ -641,7 +641,7 @@ get_upnp_rules_state_list(int max_rules_ ++ { ++ if(tmp->to_remove) ++ { ++- syslog(LOG_NOTICE, "remove port mapping %hu %s because it has expired", +++ syslog(LOG_INFO, "remove port mapping %hu %s because it has expired", ++ tmp->eport, proto_itoa(tmp->proto)); ++ _upnp_delete_redir(tmp->eport, tmp->proto); ++ *p = tmp->next; ++--- a/upnpsoap.c +++++ b/upnpsoap.c ++@@ -491,7 +491,7 @@ AddPortMapping(struct upnphttp * h, cons ++ } ++ else ++ { ++- syslog(LOG_ERR, "Failed to convert hostname '%s' to ip address", int_ip); +++ syslog(LOG_INFO, "Failed to convert hostname '%s' to ip address", int_ip); ++ ClearNameValueList(&data); ++ SoapError(h, 402, "Invalid Args"); ++ return; ++@@ -690,7 +690,7 @@ AddAnyPortMapping(struct upnphttp * h, c ++ } ++ else ++ { ++- syslog(LOG_ERR, "Failed to convert hostname '%s' to ip address", int_ip); +++ syslog(LOG_INFO, "Failed to convert hostname '%s' to ip address", int_ip); ++ ClearNameValueList(&data); ++ SoapError(h, 402, "Invalid Args"); ++ return; ++@@ -847,7 +847,7 @@ GetSpecificPortMappingEntry(struct upnph ++ } ++ else ++ { ++- syslog(LOG_INFO, "%s: rhost='%s' %s %s found => %s:%u desc='%s' duration=%u", +++ syslog(LOG_DEBUG, "%s: rhost='%s' %s %s found => %s:%u desc='%s' duration=%u", ++ action, ++ r_host ? r_host : "NULL", ext_port, protocol, int_ip, ++ (unsigned int)iport, desc, leaseduration); ++@@ -931,7 +931,7 @@ DeletePortMapping(struct upnphttp * h, c ++ return; ++ } ++ ++- syslog(LOG_INFO, "%s: external port: %hu, protocol: %s", +++ syslog(LOG_DEBUG, "%s: external port: %hu, protocol: %s", ++ action, eport, protocol); ++ ++ /* if in secure mode, check the IP ++@@ -1047,7 +1047,7 @@ DeletePortMappingRange(struct upnphttp * ++ for(i = 0; i < number; i++) ++ { ++ r = upnp_delete_redirection(port_list[i], protocol); ++- syslog(LOG_INFO, "%s: deleting external port: %hu, protocol: %s: %s", +++ syslog(LOG_DEBUG, "%s: deleting external port: %hu, protocol: %s: %s", ++ action, port_list[i], protocol, r < 0 ? "failed" : "ok"); ++ } ++ free(port_list); ++@@ -1112,7 +1112,7 @@ GetGenericPortMappingEntry(struct upnpht ++ return; ++ } ++ ++- syslog(LOG_INFO, "%s: index=%d", action, (int)index); +++ syslog(LOG_DEBUG, "%s: index=%d", action, (int)index); ++ ++ rhost[0] = '\0'; ++ r = upnp_get_redirection_infos_by_index((int)index, &eport, protocol, &iport, ++@@ -1635,7 +1635,7 @@ PinholeVerification(struct upnphttp * h, ++ int r; ++ struct addrinfo hints, *ai, *p; ++ ++- syslog(LOG_INFO, "%s: InternalClient %s is not an IPv6, assume hostname and convert", +++ syslog(LOG_DEBUG, "%s: InternalClient %s is not an IPv6, assume hostname and convert", ++ "PinholeVerification", int_ip); ++ ++ memset(&hints, 0, sizeof(hints)); ++@@ -1655,8 +1655,8 @@ PinholeVerification(struct upnphttp * h, ++ { ++ result_ip = ((struct sockaddr_in6 *)p->ai_addr)->sin6_addr; ++ if (inet_ntop(AF_INET6, &result_ip, int_ip, sizeof(struct in6_addr)) == NULL) ++- syslog(LOG_WARNING, "%s: inet_ntop(): %m", "PinholeVerification"); ++- syslog(LOG_INFO, "%s: InternalClient resolved as %s", +++ syslog(LOG_DEBUG, "%s: inet_ntop(): %m", "PinholeVerification"); +++ syslog(LOG_DEBUG, "%s: InternalClient resolved as %s", ++ "PinholeVerification", int_ip); ++ found = 1; ++ } ++@@ -1672,7 +1672,7 @@ PinholeVerification(struct upnphttp * h, ++ freeaddrinfo(ai); ++ if (!found) ++ { ++- syslog(LOG_NOTICE, "%s: No IPv6 address for hostname '%s'", +++ syslog(LOG_INFO, "%s: No IPv6 address for hostname '%s'", ++ "PinholeVerification", int_ip); ++ SoapError(h, 402, "Invalid Args"); ++ return -1; ++@@ -1680,7 +1680,7 @@ PinholeVerification(struct upnphttp * h, ++ } ++ else ++ { ++- syslog(LOG_WARNING, "%s: Failed to convert hostname '%s' to IP address : %s", +++ syslog(LOG_INFO, "%s: Failed to convert hostname '%s' to IP address : %s", ++ "PinholeVerification", int_ip, gai_strerror(r)); ++ SoapError(h, 402, "Invalid Args"); ++ return -1; ++@@ -1813,7 +1813,7 @@ AddPinhole(struct upnphttp * h, const ch ++ inet_ntop(AF_INET6, ++ &(((struct sockaddr_in6 *)p->ai_addr)->sin6_addr), ++ rem_ip, sizeof(rem_ip)); ++- syslog(LOG_INFO, "resolved '%s' to '%s'", rem_host, rem_ip); +++ syslog(LOG_DEBUG, "resolved '%s' to '%s'", rem_host, rem_ip); ++ rem_host = rem_ip; ++ break; ++ } ++@@ -1822,7 +1822,7 @@ AddPinhole(struct upnphttp * h, const ch ++ } ++ else ++ { ++- syslog(LOG_WARNING, "AddPinhole : getaddrinfo(%s) : %s", +++ syslog(LOG_INFO, "AddPinhole : getaddrinfo(%s) : %s", ++ rem_host, gai_strerror(err)); ++ #if 0 ++ SoapError(h, 402, "Invalid Args"); ++@@ -2481,7 +2481,7 @@ SoapError(struct upnphttp * h, int errCo ++ char body[2048]; ++ int bodylen; ++ ++- syslog(LOG_INFO, "Returning UPnPError %d: %s", errCode, errDesc); +++ syslog(LOG_DEBUG, "Returning UPnPError %d: %s", errCode, errDesc); ++ bodylen = snprintf(body, sizeof(body), resp, errCode, errDesc); ++ BuildResp2_upnphttp(h, 500, "Internal Server Error", body, bodylen); ++ SendRespAndClose_upnphttp(h); + +From 159e795432dd4da1ef464156570d5447c401ea5c Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 04/10] miniupnpd: package revision and new UCI options + +The following settings UCI options been added or changed, and the +previous options are migrated on updating: + +upnpd.config UCI options | Change | Previous name +-----------------------------|---------------------------|-------------- +enabled | Match default (1) | +enable_protocols=upnp-igd | Combined option | enable_upnp=1 +enable_protocols=pcp+nat-pmp | Combined option | enable_natpmp=1 +allow_cgnat | Accept extra values (2) | use_stun +stun_host | Accept port inclusion (3) | +stun_port | Removed, included in host | +allow_third_party_mapping=0 | Inverted/extended to PCP | secure_mode=1 +log_output | Allow info log level | +lease_file | Set by default + IPv6 (4) | upnp_lease_file +upnp_igd_compat=igdv1 | Renamed/match default (1) | igdv1=1 +download_kbps | In kbit/s and renamed (5) | download +upload_kbps | In kbit/s and renamed (5) | upload +friendly_name | New option, router name | +http_port | Renamed, rem. if default | port +notify_interval | Removed if <900s, minimum | +internal_iface | Migrated, new section | + +internal_network UCI options | Change | Previous name +-----------------------------|---------------------------|-------------- +interface | New option | +access_defaults | New option (6) | +accept_ports | New option (6) | +reject_ports | New option (7) | +check_acl | New option (6) | + +Notes: +1. Init UCI default now matches LuCI and initial config file defaults + for: enabled=0 and upnp_igd_compat=igdv1 +2. Accept extra values for IPv4 CGNAT use, migrate option from X-Wrt and + only use STUN when necessary with a private/CGNAT external IPv4 +3. Remove known unsupported STUN servers and set compatible by default +4. Configure undocumented daemon option `lease_file6=${lease_file}-ipv6` + so that active IPv6 port maps are not lost when service restarts, + e.g. by deleting an active port map. Remove option if UCI default set +5. Gets converted, config file now defaults to interface link speed + instead of 8/4 Mbit/s, which is removed on migration +6. New options added for default ports that all devices on a network can + map, client-specific permissions using the access control list (ACL) + can extend/override the defaults. Access defaults: accept-high-ports + accept-web+high-ports/accept-web-ports/accept-all-ports +7. Reject ports; overrides other settings. By default reject unsafe: + FTP (21), Telnet (23), DCE/NetBIOS/SMB (135/137-139/445), RDP (3389) + +Code refactoring: +- Add a function for logging and output to stderr, and extend logging +- Revise daemon init/config-gen slightly by declare all UCI options + (incl. booleans) according to the same principle and remove + `upnpd_write_bool` +- Document and reformat default `/etc/config/upnpd` UCI config file +- Don't configure/flush unnecessary `upnp_postrouting` nftables chain + +Fixes: https://redirect.github.com/openwrt/packages/issues/17413 +Fixes: https://redirect.github.com/openwrt/packages/issues/29314 +Depends on: https://redirect.github.com/openwrt/luci/pull/8415 + +Signed-off-by: Self Hosting Group +--- + miniupnpd/Makefile | 2 + + miniupnpd/files/miniupnpd.init | 213 ++++++++++++------ + .../chain-post/srcnat/20-miniupnpd.nft | 1 - + .../nftables.d/table-post/20-miniupnpd.nft | 1 - + .../files/upnpd-migration.uci-defaults | 159 +++++++++++++ + miniupnpd/files/upnpd.config | 56 +++-- + 6 files changed, 335 insertions(+), 97 deletions(-) + delete mode 100644 miniupnpd/files/nftables.d/chain-post/srcnat/20-miniupnpd.nft + create mode 100644 miniupnpd/files/upnpd-migration.uci-defaults + +diff --git a/miniupnpd/Makefile b/miniupnpd/Makefile +index 042a49faf32803..33c27862b309a4 100644 +--- a/miniupnpd/Makefile ++++ b/miniupnpd/Makefile +@@ -98,8 +98,10 @@ define Package/miniupnpd/install/Default + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_DIR) $(1)/etc/hotplug.d/iface ++ $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/miniupnpd $(1)/usr/sbin/miniupnpd + $(INSTALL_BIN) ./files/miniupnpd.init $(1)/etc/init.d/miniupnpd ++ $(INSTALL_BIN) ./files/upnpd-migration.uci-defaults $(1)/etc/uci-defaults/99-miniupnpd-upnpd-migration + $(INSTALL_CONF) ./files/upnpd.config $(1)/etc/config/upnpd + $(INSTALL_DATA) ./files/miniupnpd.hotplug $(1)/etc/hotplug.d/iface/50-miniupnpd + endef +diff --git a/miniupnpd/files/miniupnpd.init b/miniupnpd/files/miniupnpd.init +index 861bec0d3124f9..5dd786a50462d5 100644 +--- a/miniupnpd/files/miniupnpd.init ++++ b/miniupnpd/files/miniupnpd.init +@@ -42,53 +42,44 @@ conf_rule_add() { + echo "$action $ext_start${ext_end:+-}$ext_end $int_addr $int_start${int_end:+-}$int_end #$comment" + } + +-upnpd_write_bool() { +- local opt="$1" +- local def="${2:-0}" +- local alt="${3:-$opt}" +- local val +- +- config_get_bool val config "$opt" "$def" +- if [ "$val" -eq 0 ]; then +- echo "$alt=no" +- else +- echo "$alt=yes" +- fi +-} +- + upnpd() { + config_load "upnpd" +- local external_iface external_iface6 external_zone external_ip internal_iface +- local upload download log_output port config_file serial_number model_number +- local use_stun stun_host stun_port uuid notify_interval presentation_url +- local upnp_lease_file upnp_lease_file6 ipv6_disable ext_allow_private_ipv4 ++ local external_iface external_iface6 external_zone external_ip ++ local upload_kbps download_kbps log_output http_port config_file serial_number model_number ++ local allow_cgnat stun_host uuid notify_interval presentation_url ++ local lease_file ipv6_disable ++ local enable_protocols allow_third_party_mapping system_uptime upnp_igd_compat ++ local friendly_name + + local enabled +- config_get_bool enabled config enabled 1 +- [ "$enabled" -eq 0 ] && return 1 +- ++ config_get enabled config enabled 0 ++ if [ "$enabled" != "1" ]; then ++ log "Service disabled, enabled UCI option not set" ++ return 1 ++ fi + config_get external_iface config external_iface + config_get external_iface6 config external_iface6 + config_get external_zone config external_zone + config_get external_ip config external_ip +- config_get internal_iface config internal_iface +- config_get port config port 5000 +- config_get upload config upload +- config_get download config download +- config_get_bool log_output config log_output 0 ++ config_get http_port config http_port 5000 ++ config_get upload_kbps config upload_kbps ++ config_get download_kbps config download_kbps ++ config_get log_output config log_output + config_get config_file config config_file + config_get serial_number config serial_number + config_get model_number config model_number + config_get uuid config uuid +- config_get use_stun config use_stun 0 +- config_get stun_host config stun_host +- config_get stun_port config stun_port ++ config_get allow_cgnat config allow_cgnat 0 ++ config_get stun_host config stun_host stun.nextcloud.com + config_get notify_interval config notify_interval + config_get presentation_url config presentation_url +- config_get upnp_lease_file config upnp_lease_file +- config_get upnp_lease_file6 config upnp_lease_file6 ++ config_get lease_file config lease_file /var/run/miniupnpd.leases + config_get ipv6_disable config ipv6_disable 0 +- config_get ext_allow_private_ipv4 config ext_allow_private_ipv4 0 ++ config_get enable_protocols config enable_protocols all ++ config_get allow_third_party_mapping config allow_third_party_mapping 0 ++ config_get system_uptime config system_uptime 1 ++ config_get upnp_igd_compat config upnp_igd_compat igdv1 ++ config_get friendly_name config friendly_name "OpenWrt UPnP IGD & PCP" + + local conf ifname ifname6 + +@@ -121,55 +112,66 @@ upnpd() { + local tmpconf="/var/etc/miniupnpd.conf" + conf="$tmpconf" + mkdir -p /var/etc +- ++ if [ "$ifname" = "" ]; then ++ log "No external network interface found, not starting" daemon.err ++ return 1 ++ fi ++ if ! uci -q get upnpd.@internal_network[0].interface >/dev/null; then ++ log "No internal networks configured, not starting" daemon.err ++ return 1 ++ fi + { + echo "ext_ifname=$ifname" + echo "ext_ifname6=$ifname6" + [ -n "$external_ip" ] && echo "ext_ip=$external_ip" + +- local iface +- for iface in ${internal_iface:-lan}; do +- local device +- network_get_device device "$iface" && echo "listening_ip=$device" +- done +- +- config_load "upnpd" +- upnpd_write_bool enable_natpmp 1 +- upnpd_write_bool enable_upnp 1 +- upnpd_write_bool secure_mode 1 +- upnpd_write_bool system_uptime 1 +- upnpd_write_bool igdv1 0 force_igd_desc_v1 +- upnpd_write_bool use_stun 0 ext_perform_stun +- upnpd_write_bool ipv6_disable $ipv6_disable +- upnpd_write_bool ext_allow_private_ipv4 $ext_allow_private_ipv4 +- +- [ "$use_stun" -eq 0 ] || { +- [ -n "$stun_host" ] && echo "ext_stun_host=$stun_host" +- [ -n "$stun_port" ] && echo "ext_stun_port=$stun_port" +- } ++ [ "$enable_protocols" = "all" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=yes" ++ [ "$enable_protocols" = "upnp-igd" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=no" ++ [ "$enable_protocols" = "pcp+nat-pmp" ] && echo "enable_upnp=no" && echo "enable_pcp_pmp=yes" ++ [ "$allow_third_party_mapping" = "0" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=no" ++ [ "$allow_third_party_mapping" = "1" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=yes" ++ [ "$allow_third_party_mapping" = "upnp-igd" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=no" ++ [ "$allow_third_party_mapping" = "pcp" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=yes" ++ [ "$system_uptime" = "0" ] && echo "system_uptime=no" || echo "system_uptime=yes" ++ [ "$upnp_igd_compat" = "igdv1" ] && echo "force_igd_desc_v1=yes" || echo "force_igd_desc_v1=no" ++ # Only perform an STUN CGNAT test if necessary, with a private/CGNAT-reserved external IPv4 ++ local extipv4 extipv4private ++ extipv4="$(ip -4 addr show dev "$ifname" | grep inet | head -1 | sed -E "s/.*inet ([0-9.]+).*/\1/")" ++ case "$extipv4" in ++ 10.* | 100.6[4-9].* | 100.[7-9][0-9].* | 100.1[0-1][0-9].* | 100.12[0-7].* | 172.1[6-9].* | \ ++ 172.2[0-9].* | 172.3[0-1].* | 192.0.0.[1-6] | 192.168.* | 198.1[89].*) extipv4private=1 ;; ++ esac ++ [ "$extipv4private" = "1" ] && log "Private/CGNAT-reserved external IPv4 detected ($extipv4)" ++ if [ "$extipv4private" = "1" ] && [ "$allow_cgnat" != "0" ]; then ++ [ "$allow_cgnat" = "1" ] && echo "ext_perform_stun=yes" ++ [ "$allow_cgnat" = "allow-filtered" ] && echo "ext_perform_stun=allow-filtered" ++ # Avoid next option, as no STUN public IPv4 detection, required by clients (PCP/NAT-PMP among others) ++ [ "$allow_cgnat" = "report-private-ipv4" ] && echo "ext_allow_private_ipv4=yes" ++ echo "ext_stun_host=${stun_host%%:*}" ++ [ "${stun_host%%:*}" != "${stun_host##*:}" ] && echo "ext_stun_port=${stun_host##*:}" ++ fi ++ [ "$ipv6_disable" = "0" ] && echo "ipv6_disable=no" || echo "ipv6_disable=yes" + +- [ -n "$upload" ] && [ -n "$download" ] && { +- echo "bitrate_down=$((download * 1024 * 8))" +- echo "bitrate_up=$((upload * 1024 * 8))" +- } ++ [ -n "$download_kbps" ] && echo "bitrate_down=$((download_kbps * 1000))" ++ [ -n "$upload_kbps" ] && echo "bitrate_up=$((upload_kbps * 1000))" + +- [ -n "$upnp_lease_file" ] && touch "$upnp_lease_file" && echo "lease_file=$upnp_lease_file" +- [ -n "$upnp_lease_file6" ] && touch "$upnp_lease_file6" && echo "lease_file6=$upnp_lease_file6" ++ touch "$lease_file" && echo "lease_file=$lease_file" ++ [ "$ipv6_disable" = "0" ] && touch "${lease_file}-ipv6" && echo "lease_file6=${lease_file}-ipv6" ++ [ -n "$friendly_name" ] && echo "friendly_name=$friendly_name" + [ -n "$presentation_url" ] && echo "presentation_url=$presentation_url" + [ -n "$notify_interval" ] && echo "notify_interval=$notify_interval" +- [ -n "$serial_number" ] && echo "serial=$serial_number" +- [ -n "$model_number" ] && echo "model_number=$model_number" +- [ -n "$port" ] && echo "port=$port" ++ echo "serial=$serial_number" ++ echo "model_number=$model_number" ++ echo "http_port=$http_port" + + [ -z "$uuid" ] && { ++ log "Generate UPnP IGD UUID" + uuid="$(cat /proc/sys/kernel/random/uuid)" + uci set upnpd.config.uuid="$uuid" + uci commit upnpd + } + +- [ "$uuid" = "nocli" ] || echo "uuid=$uuid" +- +- config_foreach conf_rule_add perm_rule ++ [ "$uuid" != "nocli" ] && echo "uuid=$uuid" || log "uuid=nocli deprecated, set to 00000000-0000-0000-0000-000000000000 instead" + + if [ "$FW" = "fw4" ]; then + #When using nftables configure miniupnpd to use its own table and chains +@@ -177,9 +179,14 @@ upnpd() { + echo "upnp_nat_table_name=fw4" + echo "upnp_forward_chain=upnp_forward" + echo "upnp_nat_chain=upnp_prerouting" +- echo "upnp_nat_postrouting_chain=upnp_postrouting" ++ #echo "upnp_nat_postrouting_chain=upnp_postrouting" + fi + ++ echo "# Enable internal networks / access control" ++ config_foreach upnpd_add_int_network_and_defaults internal_network pre-acl ++ config_foreach conf_rule_add perm_rule ++ config_foreach upnpd_add_int_network_and_defaults internal_network post-acl ++ + } > "$tmpconf" + fi + +@@ -190,15 +197,14 @@ upnpd() { + else + iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload + fi +- else +- logger -t "upnp daemon" "external interface not found, not starting" + fi + + procd_open_instance + procd_set_param file "$conf" "/etc/config/firewall" + procd_set_param command "$PROG" + procd_append_param command -f "$conf" +- [ "$log_output" = "1" ] && procd_append_param command -d ++ [ "$log_output" = "info" ] && procd_append_param command -v ++ [ "$log_output" = "debug" ] && procd_append_param command -v -v + procd_close_instance + } + +@@ -211,7 +217,7 @@ stop_service() { + else + nft flush chain inet fw4 upnp_forward 2>/dev/null + nft flush chain inet fw4 upnp_prerouting 2>/dev/null +- nft flush chain inet fw4 upnp_postrouting 2>/dev/null ++ #nft flush chain inet fw4 upnp_postrouting 2>/dev/null + fi + } + +@@ -223,3 +229,70 @@ start_service() { + service_triggers() { + procd_add_reload_trigger "upnpd" + } ++ ++log() { ++ logger -s -p "${2:-daemon.notice}" -t "miniupnpd-init" "$1" || echo "miniupnpd-init: $1" >&2 ++} ++ ++upnpd_add_int_network_and_defaults() { ++ local cfg="$1" ++ local interface access_defaults accept_ports reject_ports check_acl ++ config_get interface "$cfg" interface ++ config_get access_defaults "$cfg" access_defaults none ++ config_get accept_ports "$cfg" accept_ports ++ config_get reject_ports "$cfg" reject_ports "21 23 135 137-139 445 3389" ++ config_get check_acl "$cfg" check_acl 1 ++ local device subnet rejectport accessdefaultsports acceptport ++ network_get_device device "$interface" ++ network_get_subnet subnet "$interface" ++ [ "$subnet" = "" ] && log "Cannot get IPv4 subnet for network $interface, network ignored" daemon.warn && return 0 ++ if [ "$2" = "pre-acl" ]; then ++ echo "# Enable internal network $interface ($device) with access defaults $access_defaults and check ACL $check_acl" ++ echo "listening_ip=$device" ++ for rejectport in $reject_ports; do ++ if [ "$rejectport" -ge "1" ] 2>/dev/null && [ "$rejectport" -le "65535" ] 2>/dev/null || ++ { ++ [ "${rejectport%%-*}" -ge "1" ] 2>/dev/null && ++ [ "${rejectport%%-*}" -le "65535" ] 2>/dev/null && ++ [ "${rejectport##*-}" -ge "1" ] 2>/dev/null && ++ [ "${rejectport##*-}" -le "65535" ] 2>/dev/null && ++ [ "${rejectport##*-}" -ge "${rejectport%%-*}" ] 2>/dev/null ++ }; then ++ echo "deny $rejectport $subnet $rejectport # Reject port $rejectport on $interface" ++ else ++ log "Invalid port or port range ($rejectport) in reject_ports ignored" daemon.warn ++ fi ++ done ++ fi ++ if { [ "$2" = "post-acl" ] && [ "$check_acl" = "1" ]; } || ++ { [ "$2" = "pre-acl" ] && [ "$check_acl" = "0" ]; }; then ++ if [ "$access_defaults" = "accept-high-ports" ]; then ++ accessdefaultsports="1024-65535" ++ elif [ "$access_defaults" = "accept-web+high-ports" ]; then ++ accessdefaultsports="80 443 1024-65535" ++ elif [ "$access_defaults" = "accept-web-ports" ]; then ++ accessdefaultsports="80 443" ++ elif [ "$access_defaults" = "accept-all-ports" ]; then ++ accessdefaultsports="1-65535" ++ elif [ "$access_defaults" != "none" ]; then ++ log "Invalid access_defaults ($access_defaults) ignored" daemon.warn ++ fi ++ for acceptport in $accessdefaultsports $accept_ports; do ++ if [ "$acceptport" -ge "1" ] 2>/dev/null && [ "$acceptport" -le "65535" ] 2>/dev/null || ++ { ++ [ "${acceptport%%-*}" -ge "1" ] 2>/dev/null && ++ [ "${acceptport%%-*}" -le "65535" ] 2>/dev/null && ++ [ "${acceptport##*-}" -ge "1" ] 2>/dev/null && ++ [ "${acceptport##*-}" -le "65535" ] 2>/dev/null && ++ [ "${acceptport##*-}" -ge "${acceptport%%-*}" ] 2>/dev/null ++ }; then ++ echo "allow $acceptport $subnet $acceptport # Accept port $acceptport on $interface" ++ else ++ log "Invalid port or port range ($acceptport) in accept_ports ignored" daemon.warn ++ fi ++ done ++ fi ++ if [ "$2" = "pre-acl" ] && [ "$check_acl" = "0" ]; then ++ echo "deny 1-65535 $subnet 1-65535 # Reject ACL by default on $interface" ++ fi ++} +diff --git a/miniupnpd/files/nftables.d/chain-post/srcnat/20-miniupnpd.nft b/miniupnpd/files/nftables.d/chain-post/srcnat/20-miniupnpd.nft +deleted file mode 100644 +index 2d0051e202f7b1..00000000000000 +--- a/miniupnpd/files/nftables.d/chain-post/srcnat/20-miniupnpd.nft ++++ /dev/null +@@ -1 +0,0 @@ +-jump upnp_postrouting comment "Hook into miniupnpd postrouting chain"; +diff --git a/miniupnpd/files/nftables.d/table-post/20-miniupnpd.nft b/miniupnpd/files/nftables.d/table-post/20-miniupnpd.nft +index 7d914e5c84fdf3..3d787fba88d0da 100644 +--- a/miniupnpd/files/nftables.d/table-post/20-miniupnpd.nft ++++ b/miniupnpd/files/nftables.d/table-post/20-miniupnpd.nft +@@ -1,3 +1,2 @@ + chain upnp_forward {} + chain upnp_prerouting {} +-chain upnp_postrouting {} +diff --git a/miniupnpd/files/upnpd-migration.uci-defaults b/miniupnpd/files/upnpd-migration.uci-defaults +new file mode 100644 +index 00000000000000..19445c9198994b +--- /dev/null ++++ b/miniupnpd/files/upnpd-migration.uci-defaults +@@ -0,0 +1,159 @@ ++#!/bin/sh ++ ++log() { ++ logger -s -p "${2:-daemon.notice}" -t "upnpd" "$1" || echo "upnpd: $1" >&2 ++} ++ ++log "Check UCI options in /etc/config/upnpd to be migrated to v2.0" ++ ++# Set missing enabled option to fix previously different defaults in LuCI/config (0) and init UCI (1) ++if ! uci -q get upnpd.config.enabled >/dev/null; then ++ uci -q set upnpd.config.enabled="1" ++fi ++ ++# Migrate boolean options to only use 0/1 for LuCI support ++for option in enabled ipv6_disable system_uptime; do ++ if uci -q get upnpd.config.$option >/dev/null; then ++ uci get upnpd.config.$option | grep -q -E -x "0|off|false|no|disabled" && uci set upnpd.config.$option="0" ++ uci get upnpd.config.$option | grep -q -E -x "1|on|true|yes|enabled" && uci set upnpd.config.$option="1" ++ fi ++done ++ ++# Migrate enable_upnp/enable_natpmp -> enable_protocols: Combined option ++if uci -q get upnpd.config.enable_upnp >/dev/null || uci -q get upnpd.config.enable_natpmp >/dev/null; then ++ log "enable_upnp/enable_natpmp -> enable_protocols: Combined option" ++ if ! uci -q get upnpd.config.enable_upnp | grep -q -E -x "0|off|false|no|disabled"; then ++ uci -q get upnpd.config.enable_natpmp | grep -q -E -x "0|off|false|no|disabled" && ++ uci set upnpd.config.enable_protocols="upnp-igd" || ++ uci set upnpd.config.enable_protocols="all" ++ elif ! uci -q get upnpd.config.enable_natpmp | grep -q -E -x "0|off|false|no|disabled"; then ++ uci set upnpd.config.enable_protocols="pcp+nat-pmp" ++ else ++ uci set upnpd.config.enable_protocols="all" ++ uci set upnpd.config.enabled="0" ++ fi ++ uci -q delete upnpd.config.enable_upnp ++ uci -q delete upnpd.config.enable_natpmp ++fi ++ ++# Rename use_stun -> allow_cgnat ++if uci -q get upnpd.config.use_stun >/dev/null; then ++ log "use_stun -> allow_cgnat" ++ uci rename upnpd.config.use_stun="allow_cgnat" ++fi ++ ++# Migrate force_forwarding=1 (in X-Wrt since 2021) to new similar option allow_cgnat=allow-filtered for cross-upgrades ++if uci -q get upnpd.config.force_forwarding >/dev/null; then ++ log "force_forwarding=1 -> allow_cgnat=allow-filtered: New option" ++ uci get upnpd.config.force_forwarding | grep -q -E -x "1|on|true|yes|enabled" && ++ uci set upnpd.config.allow_cgnat="allow-filtered" ++ uci delete upnpd.config.force_forwarding ++fi ++ ++# Remove known unsupported (not CGNAT filtering test capable) STUN servers and include stun_port in stun_host ++if uci -q get upnpd.config.stun_host | grep -q -E "stun[0-9]?.l.google.com|stun.cloudflare.com"; then ++ log "stun_host: Unsupported STUN server ($(uci -q get upnpd.config.stun_host)) set, remove to set default" ++ uci delete upnpd.config.stun_host ++ # Workaround for false negative filtered STUN CGNAT test results for unsupported servers before daemon 2.3.10 ++ uci set upnpd.config.allow_cgnat="allow-filtered" ++ uci -q delete upnpd.config.stun_port ++elif uci -q get upnpd.config.stun_port >/dev/null; then ++ uci -q get upnpd.config.stun_host >/dev/null && [ "$(uci -q get upnpd.config.stun_port)" != "3478" ] && ++ log "stun_port: Include stun_port in stun_host, and remove option" && ++ uci set upnpd.config.stun_host="$(uci -q get upnpd.config.stun_host | cut -d ":" -f 1):$(uci -q get upnpd.config.stun_port)" ++ uci delete upnpd.config.stun_port ++fi ++ ++# Migrate secure_mode=1/0 -> allow_third_party_mapping=0/upnp-igd: Invert and extend to PCP ++if uci -q get upnpd.config.secure_mode >/dev/null; then ++ log "secure_mode=1/0 -> allow_third_party_mapping=0/upnp-igd: Invert and extend to PCP" ++ uci get upnpd.config.secure_mode | grep -q -E -x "0|off|false|no|disabled" && ++ uci set upnpd.config.allow_third_party_mapping="upnp-igd" || ++ uci set upnpd.config.allow_third_party_mapping="0" ++ uci delete upnpd.config.secure_mode ++fi ++ ++# Migrate log_output=0/1 -> log_output=default/debug: Now info also allowed ++if uci -q get upnpd.config.log_output >/dev/null; then ++ log "log_output=0/1 -> log_output=default/debug: Now info also allowed" ++ uci get upnpd.config.log_output | grep -q -E -x "1|on|true|yes|enabled" && ++ uci set upnpd.config.log_output="debug" ++ uci get upnpd.config.log_output | grep -q -E -x "0|off|false|no|disabled" && ++ uci set upnpd.config.log_output="default" ++fi ++ ++# Rename upnp_lease_file -> lease_file: To original daemon name, and remove if UCI default set ++if uci -q get upnpd.config.upnp_lease_file >/dev/null; then ++ if [ "$(uci -q get upnpd.config.upnp_lease_file)" = "/var/run/miniupnpd.leases" ]; then ++ log "upnp_lease_file -> lease_file: Remove option as UCI default set" ++ uci delete upnpd.config.upnp_lease_file ++ else ++ log "upnp_lease_file -> lease_file" ++ uci rename upnpd.config.upnp_lease_file="lease_file" ++ fi ++fi ++if uci -q get upnpd.config.upnp_lease_file6 >/dev/null; then ++ uci delete upnpd.config.upnp_lease_file6 ++fi ++ ++# Migrate igdv1=1/0 -> upnp_igd_compat=igdv1/igdv2: Extensible/clearer ++if uci -q get upnpd.config.igdv1 >/dev/null; then ++ log "igdv1=1/0 -> upnp_igd_compat=igdv1/igdv2" ++ uci get upnpd.config.igdv1 | grep -q -E -x "1|on|true|yes|enabled" && ++ uci set upnpd.config.upnp_igd_compat="igdv1" || ++ uci set upnpd.config.upnp_igd_compat="igdv2" ++ uci delete upnpd.config.igdv1 ++fi ++ ++# Migrate download/upload -> download_kbps/upload_kbps: Convert to kbit/s ++if uci -q get upnpd.config.download >/dev/null; then ++ download="$(uci -q get upnpd.config.download)" ++ if [ "$download" != "1024" ] && [ "$download" -ge "1" ] 2>/dev/null; then ++ log "download -> download_kbps: Convert to kbit/s" ++ download_kbps="$((download * 8 * 1000 / 1024))" ++ uci set upnpd.config.download_kbps="$download_kbps" ++ fi ++ uci delete upnpd.config.download ++fi ++if uci -q get upnpd.config.upload >/dev/null; then ++ upload="$(uci -q get upnpd.config.upload)" ++ if [ "$upload" != "512" ] && [ "$upload" -ge "1" ] 2>/dev/null; then ++ log "upload -> upload_kbps: Convert to kbit/s" ++ upload_kbps="$((upload * 8 * 1000 / 1024))" ++ uci set upnpd.config.upload_kbps="$upload_kbps" ++ fi ++ uci delete upnpd.config.upload ++fi ++ ++# Rename port -> http_port: Remove if UCI default set ++if uci -q get upnpd.config.port >/dev/null; then ++ if [ "$(uci -q get upnpd.config.port)" = "5000" ]; then ++ log "port -> http_port: Remove option as UCI default set" ++ uci delete upnpd.config.port ++ else ++ log "port -> http_port" ++ uci rename upnpd.config.port="http_port" ++ fi ++fi ++ ++# Migrate notify_interval <= 900 s: Remove to set minimum of 900 (default) ++if [ "$(uci -q get upnpd.config.notify_interval)" -le "900" ] 2>/dev/null; then ++ log "notify_interval <= 900 s: Remove to set minimum of 900 (default)" ++ uci delete upnpd.config.notify_interval ++fi ++ ++# Migrate internal_iface option to new internal_network section ++if ! uci -q get upnpd.@internal_network[0] >/dev/null; then ++ ifnr=0 ++ for interface in $(uci -q get upnpd.config.internal_iface || echo lan); do ++ log "Create new internal_network section for $interface" ++ uci add upnpd internal_network >/dev/null ++ uci set upnpd.@internal_network[$ifnr].interface="$interface" ++ ifnr=$((ifnr + 1)) ++ done ++ uci -q delete upnpd.config.internal_iface ++fi ++ ++uci commit upnpd >/dev/null ++ ++exit 0 +diff --git a/miniupnpd/files/upnpd.config b/miniupnpd/files/upnpd.config +index c4546e018f5de7..6344a60071bd72 100644 +--- a/miniupnpd/files/upnpd.config ++++ b/miniupnpd/files/upnpd.config +@@ -1,29 +1,35 @@ +-config upnpd config +- option enabled 0 +- option enable_natpmp 1 +- option enable_upnp 1 +- option secure_mode 1 +- option log_output 0 +- option download 1024 +- option upload 512 +-#by default, looked up dynamically from ubus +-# option external_iface wan +- option internal_iface lan +- option port 5000 +- option upnp_lease_file /var/run/miniupnpd.leases +- option upnp_lease_file6 /var/run/miniupnpd.leases6 +- option igdv1 1 ++# UPnP IGD & PCP/NAT-PMP Service Settings (v2.0) ++ ++config upnpd 'config' ++ option enabled '0' ++ # Can be set to all/upnp-igd/pcp+nat-pmp ++ option enable_protocols 'all' ++ #option ipv6_disable '0' ++ #option allow_third_party_mapping '0' ++ # Extra logging by setting to info or debug (previously 1) ++ option log_output 'default' ++ option upnp_igd_compat 'igdv1' ++ ++# Enable Networks / Access Control ++ ++config internal_network ++ option interface 'lan' ++ option access_defaults 'accept-high-ports' # default none ++ option check_acl '0' # default 1 ++ ++# Access Control List ++# Empty ACL rejected alone, IPv6 accepted unless disabled + + config perm_rule +- option action allow +- option ext_ports 1024-65535 +- option int_addr 0.0.0.0/0 # Does not override secure_mode +- option int_ports 1024-65535 +- option comment "Allow high ports" ++ option comment 'Allow high ports' ++ option int_addr '0.0.0.0/0' ++ option int_ports '1024-65535' ++ option ext_ports '1024-65535' ++ option action 'allow' + + config perm_rule +- option action deny +- option ext_ports 0-65535 +- option int_addr 0.0.0.0/0 +- option int_ports 0-65535 +- option comment "Default deny" ++ option comment 'Default deny' ++ option int_addr '0.0.0.0/0' ++ option int_ports '1-65535' ++ option ext_ports '1-65535' ++ option action 'deny' + +From 39860c186473551ec4a0156cbf8cc1f73b139b12 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 05/10] miniupnpd: group/rearrange config-gen, refactoring + +- Group and rearrange UCI option declaration and config-gen by + function/LuCI UI, and comment +- Encode required XML entities of text UPnP IGD config options until the + daemon does so using the created function `xml_encode` +- Only generate UPnP IGD config if the protocol is enabled + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + miniupnpd/files/miniupnpd.hotplug | 2 +- + miniupnpd/files/miniupnpd.init | 125 +++++++++++++------------- + 2 files changed, 66 insertions(+), 61 deletions(-) + +diff --git a/miniupnpd/files/miniupnpd.hotplug b/miniupnpd/files/miniupnpd.hotplug +index 607a32bdc6885b..5d34c441a829ea 100644 +--- a/miniupnpd/files/miniupnpd.hotplug ++++ b/miniupnpd/files/miniupnpd.hotplug +@@ -40,6 +40,6 @@ fi + + [ "$DEVICE" != "$ifname" ] && [ "$DEVICE" != "$ifname6" ] && exit 0 + +-grep -qs "^ext_ifname=$ifname" "$tmpconf" && grep -qs "^ext_ifname6=$ifname6" "$tmpconf" && exit 0 ++grep -qs "^ext_ifname=$ifname" "$tmpconf" && grep -qs "^ext_ifname6=${ifname6:-$ifname}" "$tmpconf" && exit 0 + + /etc/init.d/miniupnpd restart +diff --git a/miniupnpd/files/miniupnpd.init b/miniupnpd/files/miniupnpd.init +index 5dd786a50462d5..969b22e60cacc4 100644 +--- a/miniupnpd/files/miniupnpd.init ++++ b/miniupnpd/files/miniupnpd.init +@@ -29,7 +29,6 @@ conf_rule_add() { + local cfg="$1" + local action int_addr + local ext_start ext_end int_start int_end comment +- + config_get action "$cfg" action "deny" # allow or deny + upnpd_get_port_range "ext" "$cfg" ext_ports "0-65535" # external ports: x, x-y, x:y + config_get int_addr "$cfg" int_addr "0.0.0.0/0" # ip or network and subnet mask (internal) +@@ -44,45 +43,45 @@ conf_rule_add() { + + upnpd() { + config_load "upnpd" +- local external_iface external_iface6 external_zone external_ip +- local upload_kbps download_kbps log_output http_port config_file serial_number model_number +- local allow_cgnat stun_host uuid notify_interval presentation_url +- local lease_file ipv6_disable +- local enable_protocols allow_third_party_mapping system_uptime upnp_igd_compat +- local friendly_name +- + local enabled + config_get enabled config enabled 0 + if [ "$enabled" != "1" ]; then + log "Service disabled, enabled UCI option not set" + return 1 + fi +- config_get external_iface config external_iface +- config_get external_iface6 config external_iface6 +- config_get external_zone config external_zone +- config_get external_ip config external_ip +- config_get http_port config http_port 5000 +- config_get upload_kbps config upload_kbps +- config_get download_kbps config download_kbps +- config_get log_output config log_output +- config_get config_file config config_file +- config_get serial_number config serial_number +- config_get model_number config model_number +- config_get uuid config uuid ++ # Daemon ++ local enable_protocols allow_cgnat stun_host allow_third_party_mapping ipv6_disable system_uptime log_output lease_file config_file ++ config_get enable_protocols config enable_protocols all + config_get allow_cgnat config allow_cgnat 0 + config_get stun_host config stun_host stun.nextcloud.com +- config_get notify_interval config notify_interval +- config_get presentation_url config presentation_url +- config_get lease_file config lease_file /var/run/miniupnpd.leases +- config_get ipv6_disable config ipv6_disable 0 +- config_get enable_protocols config enable_protocols all + config_get allow_third_party_mapping config allow_third_party_mapping 0 ++ config_get ipv6_disable config ipv6_disable 0 + config_get system_uptime config system_uptime 1 ++ config_get log_output config log_output ++ config_get lease_file config lease_file /var/run/miniupnpd.leases ++ config_get config_file config config_file ++ ++ # UPnP IGD ++ local upnp_igd_compat download_kbps upload_kbps friendly_name model_number serial_number presentation_url uuid http_port notify_interval + config_get upnp_igd_compat config upnp_igd_compat igdv1 ++ config_get download_kbps config download_kbps ++ config_get upload_kbps config upload_kbps + config_get friendly_name config friendly_name "OpenWrt UPnP IGD & PCP" ++ config_get model_number config model_number ++ config_get serial_number config serial_number ++ config_get presentation_url config presentation_url ++ config_get uuid config uuid ++ config_get http_port config http_port 5000 ++ config_get notify_interval config notify_interval + +- local conf ifname ifname6 ++ # External network interface ++ local external_iface external_iface6 external_zone external_ip ++ config_get external_iface config external_iface ++ config_get external_iface6 config external_iface6 ++ config_get external_zone config external_zone ++ config_get external_ip config external_ip + ++ local conf ifname ifname6 + . /lib/functions/network.sh + + if [ -n "$external_iface" ] ; then +@@ -120,20 +119,6 @@ upnpd() { + log "No internal networks configured, not starting" daemon.err + return 1 + fi +- { +- echo "ext_ifname=$ifname" +- echo "ext_ifname6=$ifname6" +- [ -n "$external_ip" ] && echo "ext_ip=$external_ip" +- +- [ "$enable_protocols" = "all" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=yes" +- [ "$enable_protocols" = "upnp-igd" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=no" +- [ "$enable_protocols" = "pcp+nat-pmp" ] && echo "enable_upnp=no" && echo "enable_pcp_pmp=yes" +- [ "$allow_third_party_mapping" = "0" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=no" +- [ "$allow_third_party_mapping" = "1" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=yes" +- [ "$allow_third_party_mapping" = "upnp-igd" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=no" +- [ "$allow_third_party_mapping" = "pcp" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=yes" +- [ "$system_uptime" = "0" ] && echo "system_uptime=no" || echo "system_uptime=yes" +- [ "$upnp_igd_compat" = "igdv1" ] && echo "force_igd_desc_v1=yes" || echo "force_igd_desc_v1=no" + # Only perform an STUN CGNAT test if necessary, with a private/CGNAT-reserved external IPv4 + local extipv4 extipv4private + extipv4="$(ip -4 addr show dev "$ifname" | grep inet | head -1 | sed -E "s/.*inet ([0-9.]+).*/\1/")" +@@ -142,6 +127,11 @@ upnpd() { + 172.2[0-9].* | 172.3[0-1].* | 192.0.0.[1-6] | 192.168.* | 198.1[89].*) extipv4private=1 ;; + esac + [ "$extipv4private" = "1" ] && log "Private/CGNAT-reserved external IPv4 detected ($extipv4)" ++ { ++ echo "# Daemon" ++ [ "$enable_protocols" = "all" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=yes" ++ [ "$enable_protocols" = "upnp-igd" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=no" ++ [ "$enable_protocols" = "pcp+nat-pmp" ] && echo "enable_upnp=no" && echo "enable_pcp_pmp=yes" + if [ "$extipv4private" = "1" ] && [ "$allow_cgnat" != "0" ]; then + [ "$allow_cgnat" = "1" ] && echo "ext_perform_stun=yes" + [ "$allow_cgnat" = "allow-filtered" ] && echo "ext_perform_stun=allow-filtered" +@@ -150,31 +140,37 @@ upnpd() { + echo "ext_stun_host=${stun_host%%:*}" + [ "${stun_host%%:*}" != "${stun_host##*:}" ] && echo "ext_stun_port=${stun_host##*:}" + fi ++ [ "$allow_third_party_mapping" = "0" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=no" ++ [ "$allow_third_party_mapping" = "1" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=yes" ++ [ "$allow_third_party_mapping" = "upnp-igd" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=no" ++ [ "$allow_third_party_mapping" = "pcp" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=yes" + [ "$ipv6_disable" = "0" ] && echo "ipv6_disable=no" || echo "ipv6_disable=yes" +- +- [ -n "$download_kbps" ] && echo "bitrate_down=$((download_kbps * 1000))" +- [ -n "$upload_kbps" ] && echo "bitrate_up=$((upload_kbps * 1000))" +- ++ [ "$system_uptime" = "0" ] && echo "system_uptime=no" || echo "system_uptime=yes" + touch "$lease_file" && echo "lease_file=$lease_file" + [ "$ipv6_disable" = "0" ] && touch "${lease_file}-ipv6" && echo "lease_file6=${lease_file}-ipv6" +- [ -n "$friendly_name" ] && echo "friendly_name=$friendly_name" +- [ -n "$presentation_url" ] && echo "presentation_url=$presentation_url" +- [ -n "$notify_interval" ] && echo "notify_interval=$notify_interval" +- echo "serial=$serial_number" +- echo "model_number=$model_number" +- echo "http_port=$http_port" +- +- [ -z "$uuid" ] && { +- log "Generate UPnP IGD UUID" +- uuid="$(cat /proc/sys/kernel/random/uuid)" +- uci set upnpd.config.uuid="$uuid" +- uci commit upnpd +- } + +- [ "$uuid" != "nocli" ] && echo "uuid=$uuid" || log "uuid=nocli deprecated, set to 00000000-0000-0000-0000-000000000000 instead" ++ if [ "$enable_protocols" = "upnp-igd" ] || [ "$enable_protocols" = "all" ]; then ++ echo "# UPnP IGD" ++ [ "$upnp_igd_compat" = "igdv1" ] && echo "force_igd_desc_v1=yes" || echo "force_igd_desc_v1=no" ++ [ -n "$download_kbps" ] && echo "bitrate_down=$((download_kbps * 1000))" ++ [ -n "$upload_kbps" ] && echo "bitrate_up=$((upload_kbps * 1000))" ++ [ -n "$friendly_name" ] && echo "friendly_name=$(xml_encode "$friendly_name")" ++ [ -n "$model_number" ] && echo "model_number=$(xml_encode "$model_number")" || echo "model_number=" ++ [ -n "$serial_number" ] && echo "serial=$(xml_encode "$serial_number")" || echo "serial=" ++ [ -n "$presentation_url" ] && echo "presentation_url=$presentation_url" ++ [ -z "$uuid" ] && { ++ log "Generate UPnP IGD UUID" ++ uuid="$(cat /proc/sys/kernel/random/uuid)" ++ uci set upnpd.config.uuid="$uuid" ++ uci commit upnpd ++ } ++ [ "$uuid" != "nocli" ] && echo "uuid=$uuid" || log "uuid=nocli deprecated, set to 00000000-0000-0000-0000-000000000000 instead" ++ echo "http_port=$http_port" ++ [ -n "$notify_interval" ] && echo "notify_interval=$notify_interval" ++ fi + + if [ "$FW" = "fw4" ]; then +- #When using nftables configure miniupnpd to use its own table and chains ++ echo "# Firewall backend" + echo "upnp_table_name=fw4" + echo "upnp_nat_table_name=fw4" + echo "upnp_forward_chain=upnp_forward" +@@ -182,6 +178,11 @@ upnpd() { + #echo "upnp_nat_postrouting_chain=upnp_postrouting" + fi + ++ echo "# External network interface" ++ echo "ext_ifname=$ifname" ++ echo "ext_ifname6=${ifname6:-$ifname}" ++ [ -n "$external_ip" ] && echo "ext_ip=$external_ip" ++ + echo "# Enable internal networks / access control" + config_foreach upnpd_add_int_network_and_defaults internal_network pre-acl + config_foreach conf_rule_add perm_rule +@@ -191,7 +192,6 @@ upnpd() { + fi + + if [ -n "$ifname" ]; then +- # start firewall + if [ "$FW" = "fw4" ]; then + nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload + else +@@ -234,6 +234,11 @@ log() { + logger -s -p "${2:-daemon.notice}" -t "miniupnpd-init" "$1" || echo "miniupnpd-init: $1" >&2 + } + ++xml_encode() { ++ # Encode required XML entities of text UPnP IGD config options until the daemon does so ++ echo "$1" | sed "s/&/\&/g; s//\>/g" ++} ++ + upnpd_add_int_network_and_defaults() { + local cfg="$1" + local interface access_defaults accept_ports reject_ports check_acl + +From dde9664570cac7236cb243b663ca0ce2c61f5c35 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 06/10] miniupnpd: rename UCI section to settings v2.0 + +Rename UCI section `config` (v1.0) -> `settings` (v2.0), helps on +migration and to distinguish the updated config from the previous one + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + miniupnpd/files/firewall3.include | 4 +- + miniupnpd/files/miniupnpd.hotplug | 6 +-- + miniupnpd/files/miniupnpd.init | 50 +++++++++---------- + .../files/upnpd-migration.uci-defaults | 13 +++++ + miniupnpd/files/upnpd.config | 2 +- + 5 files changed, 44 insertions(+), 31 deletions(-) + +diff --git a/miniupnpd/files/firewall3.include b/miniupnpd/files/firewall3.include +index 4fd483974306c1..53ca8dc716cc2f 100644 +--- a/miniupnpd/files/firewall3.include ++++ b/miniupnpd/files/firewall3.include +@@ -48,11 +48,11 @@ add_extzone_rules() { + + # By default, user configuration is king. + +-for ext_iface in $(uci -q get upnpd.config.external_iface); do ++for ext_iface in $(uci -q get upnpd.settings.external_iface); do + add_extzone_rules $(fw3 -q network "$ext_iface") + done + +-add_extzone_rules $(uci -q get upnpd.config.external_zone) ++add_extzone_rules $(uci -q get upnpd.settings.external_zone) + + [ "$ADDED" -ne 0 ] && exit 0 + +diff --git a/miniupnpd/files/miniupnpd.hotplug b/miniupnpd/files/miniupnpd.hotplug +index 5d34c441a829ea..20ee344c9a0e72 100644 +--- a/miniupnpd/files/miniupnpd.hotplug ++++ b/miniupnpd/files/miniupnpd.hotplug +@@ -10,9 +10,9 @@ + [ "$ACTION" != "ifup" ] && /etc/init.d/miniupnpd running && exit 0 + + tmpconf="/var/etc/miniupnpd.conf" +-external_iface=$(uci -q get upnpd.config.external_iface) +-external_iface6=$(uci -q get upnpd.config.external_iface6) +-external_zone=$(uci -q get upnpd.config.external_zone) ++external_iface=$(uci -q get upnpd.settings.external_iface) ++external_iface6=$(uci -q get upnpd.settings.external_iface6) ++external_zone=$(uci -q get upnpd.settings.external_zone) + [ -x "$(command -v nft)" ] && FW="fw4" || FW="fw3" + + . /lib/functions/network.sh +diff --git a/miniupnpd/files/miniupnpd.init b/miniupnpd/files/miniupnpd.init +index 969b22e60cacc4..80aa789c039803 100644 +--- a/miniupnpd/files/miniupnpd.init ++++ b/miniupnpd/files/miniupnpd.init +@@ -44,42 +44,42 @@ conf_rule_add() { + upnpd() { + config_load "upnpd" + local enabled +- config_get enabled config enabled 0 ++ config_get enabled settings enabled 0 + if [ "$enabled" != "1" ]; then + log "Service disabled, enabled UCI option not set" + return 1 + fi + # Daemon + local enable_protocols allow_cgnat stun_host allow_third_party_mapping ipv6_disable system_uptime log_output lease_file config_file +- config_get enable_protocols config enable_protocols all +- config_get allow_cgnat config allow_cgnat 0 +- config_get stun_host config stun_host stun.nextcloud.com +- config_get allow_third_party_mapping config allow_third_party_mapping 0 +- config_get ipv6_disable config ipv6_disable 0 +- config_get system_uptime config system_uptime 1 +- config_get log_output config log_output +- config_get lease_file config lease_file /var/run/miniupnpd.leases +- config_get config_file config config_file ++ config_get enable_protocols settings enable_protocols all ++ config_get allow_cgnat settings allow_cgnat 0 ++ config_get stun_host settings stun_host stun.nextcloud.com ++ config_get allow_third_party_mapping settings allow_third_party_mapping 0 ++ config_get ipv6_disable settings ipv6_disable 0 ++ config_get system_uptime settings system_uptime 1 ++ config_get log_output settings log_output ++ config_get lease_file settings lease_file /var/run/miniupnpd.leases ++ config_get config_file settings config_file + + # UPnP IGD + local upnp_igd_compat download_kbps upload_kbps friendly_name model_number serial_number presentation_url uuid http_port notify_interval +- config_get upnp_igd_compat config upnp_igd_compat igdv1 +- config_get download_kbps config download_kbps +- config_get upload_kbps config upload_kbps +- config_get friendly_name config friendly_name "OpenWrt UPnP IGD & PCP" +- config_get model_number config model_number +- config_get serial_number config serial_number +- config_get presentation_url config presentation_url +- config_get uuid config uuid +- config_get http_port config http_port 5000 +- config_get notify_interval config notify_interval ++ config_get upnp_igd_compat settings upnp_igd_compat igdv1 ++ config_get download_kbps settings download_kbps ++ config_get upload_kbps settings upload_kbps ++ config_get friendly_name settings friendly_name "OpenWrt UPnP IGD & PCP" ++ config_get model_number settings model_number ++ config_get serial_number settings serial_number ++ config_get presentation_url settings presentation_url ++ config_get uuid settings uuid ++ config_get http_port settings http_port 5000 ++ config_get notify_interval settings notify_interval + + # External network interface + local external_iface external_iface6 external_zone external_ip +- config_get external_iface config external_iface +- config_get external_iface6 config external_iface6 +- config_get external_zone config external_zone +- config_get external_ip config external_ip ++ config_get external_iface settings external_iface ++ config_get external_iface6 settings external_iface6 ++ config_get external_zone settings external_zone ++ config_get external_ip settings external_ip + + local conf ifname ifname6 + . /lib/functions/network.sh +@@ -161,7 +161,7 @@ upnpd() { + [ -z "$uuid" ] && { + log "Generate UPnP IGD UUID" + uuid="$(cat /proc/sys/kernel/random/uuid)" +- uci set upnpd.config.uuid="$uuid" ++ uci set upnpd.settings.uuid="$uuid" + uci commit upnpd + } + [ "$uuid" != "nocli" ] && echo "uuid=$uuid" || log "uuid=nocli deprecated, set to 00000000-0000-0000-0000-000000000000 instead" +diff --git a/miniupnpd/files/upnpd-migration.uci-defaults b/miniupnpd/files/upnpd-migration.uci-defaults +index 19445c9198994b..efde6ca1232212 100644 +--- a/miniupnpd/files/upnpd-migration.uci-defaults ++++ b/miniupnpd/files/upnpd-migration.uci-defaults +@@ -4,7 +4,12 @@ log() { + logger -s -p "${2:-daemon.notice}" -t "upnpd" "$1" || echo "upnpd: $1" >&2 + } + ++# Skip migration with existing settings (v2.0) or with no config (v1.0) UCI section ++# Enables the creation of a merged v1.0/v2.0 config file ++{ uci -q get upnpd.settings >/dev/null || ! uci -q get upnpd.config >/dev/null; } && exit 0 ++ + log "Check UCI options in /etc/config/upnpd to be migrated to v2.0" ++cp /etc/config/upnpd /tmp + + # Set missing enabled option to fix previously different defaults in LuCI/config (0) and init UCI (1) + if ! uci -q get upnpd.config.enabled >/dev/null; then +@@ -154,6 +159,14 @@ if ! uci -q get upnpd.@internal_network[0] >/dev/null; then + uci -q delete upnpd.config.internal_iface + fi + ++# Rename UCI section config -> settings (v2.0) ++if uci -q get upnpd.config >/dev/null; then ++ log "Rename UCI section config -> settings (v2.0)" && uci rename upnpd.config="settings" || ++ log "Error renaming the UCI section" daemon.err ++fi ++ + uci commit upnpd >/dev/null + ++log "Previous v1.0 config file copied to /tmp/upnpd (kept until reboot)" ++ + exit 0 +diff --git a/miniupnpd/files/upnpd.config b/miniupnpd/files/upnpd.config +index 6344a60071bd72..2245b0a6f015f8 100644 +--- a/miniupnpd/files/upnpd.config ++++ b/miniupnpd/files/upnpd.config +@@ -1,6 +1,6 @@ + # UPnP IGD & PCP/NAT-PMP Service Settings (v2.0) + +-config upnpd 'config' ++config upnpd 'settings' + option enabled '0' + # Can be set to all/upnp-igd/pcp+nat-pmp + option enable_protocols 'all' + +From 19bbb0dd7e9c878e39fe830e173dc54c150dd2a1 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 07/10] miniupnpd: update ACL options, migrate section + +- The ACL is now rejected last if not accepted by access defaults. Add + (disabled) ACL template entries on migration +- Migrate ACL entries to the new section name `acl_entry` +- The following ACL UCI options been added or changed, and the previous + options are migrated on updating: + +acl_entry UCI options | Change | Previous name +----------------------------|----------------------------|-------------- +action | New/updated values (1) | +int_port | Remove colon separator (2) | int_ports +ext_port | Remove colon separator (2) | ext_ports +descr_filter | New option (3) | + +1. Allow disabled, and update action option to use the nftables terms + (allow/deny -> accept/reject). To avoid adding inverted actions when + changing via LuCI, ensure any missing are set, as LuCI and UCI had + not matching action defaults. Missing actions are now ignored/logged +2. Ensure that the hyphen (-) is only used as a port range separator by + migration, as the colon (:) is not valid in LuCI +3. Add missing UCI option to set a regular expression to check for a + UPnP IGD IPv4 port map description, and fix the current collision + with the comment field which was not noticed due to a daemon bug + https://redirect.github.com/openwrt/packages/pull/24495 + https://redirect.github.com/miniupnp/miniupnp/pull/853 + +- Refactoring by adding a more universal usable `is_port_or_range` + function instead of `upnpd_get_port_range` and check if it has a valid + range, and removes a shellcheck warning +- Rename `conf_rule_add` function to `upnpd_add_acl_entry` + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + miniupnpd/files/miniupnpd.init | 86 ++++++++----------- + .../files/upnpd-migration.uci-defaults | 86 +++++++++++++++++++ + miniupnpd/files/upnpd.config | 22 ++--- + 3 files changed, 131 insertions(+), 63 deletions(-) + +diff --git a/miniupnpd/files/miniupnpd.init b/miniupnpd/files/miniupnpd.init +index 80aa789c039803..b4e09eb84067db 100644 +--- a/miniupnpd/files/miniupnpd.init ++++ b/miniupnpd/files/miniupnpd.init +@@ -7,38 +7,39 @@ USE_PROCD=1 + PROG=/usr/sbin/miniupnpd + [ -x "$(command -v nft)" ] && FW="fw4" || FW="fw3" + +-upnpd_get_port_range() { +- local var="$1"; shift +- local val +- +- config_get val "$@" +- +- case "$val" in +- [0-9]*[:-][0-9]*) +- export -n -- "${var}_start=${val%%[:-]*}" +- export -n -- "${var}_end=${val##*[:-]}" +- ;; +- [0-9]*) +- export -n -- "${var}_start=$val" +- export -n -- "${var}_end=" +- ;; +- esac ++is_port_or_range() { ++ [ "$1" = "0" ] && [ "$2" != "allowport0" ] && return 1 ++ [ "$1" -ge "1" ] 2>/dev/null && [ "$1" -le "65535" ] 2>/dev/null && return 0 ++ [ "$2" = "allowport0" ] && local minport=0 || local minport=1 ++ [ "${1%%-*}" -ge "$minport" ] 2>/dev/null && [ "${1%%-*}" -le "65535" ] 2>/dev/null && ++ [ "${1##*-}" -ge "$minport" ] 2>/dev/null && [ "${1##*-}" -le "65535" ] 2>/dev/null && ++ [ "${1##*-}" -ge "${1%%-*}" ] 2>/dev/null && return 0 || return 1 + } + +-conf_rule_add() { ++upnpd_add_acl_entry() { + local cfg="$1" +- local action int_addr +- local ext_start ext_end int_start int_end comment +- config_get action "$cfg" action "deny" # allow or deny +- upnpd_get_port_range "ext" "$cfg" ext_ports "0-65535" # external ports: x, x-y, x:y +- config_get int_addr "$cfg" int_addr "0.0.0.0/0" # ip or network and subnet mask (internal) +- upnpd_get_port_range "int" "$cfg" int_ports "0-65535" # internal ports: x, x-y, x:y or range +- config_get comment "$cfg" comment "ACL" # comment +- +- # Make a single IP IP/32 so that miniupnpd.conf can use it. +- [ "${int_addr%/*}" = "$int_addr" ] && int_addr="$int_addr/32" +- +- echo "$action $ext_start${ext_end:+-}$ext_end $int_addr $int_start${int_end:+-}$int_end #$comment" ++ local comment int_addr int_port ext_port descr_filter action ++ config_get comment "$cfg" comment "unspecified" # comment ++ config_get int_addr "$cfg" int_addr "0.0.0.0/0" # IPv4 or network and subnet mask (internal) ++ config_get int_port "$cfg" int_port "1-65535" # internal port/range: x or x-y ++ config_get ext_port "$cfg" ext_port "1-65535" # external port/range: x or x-y ++ config_get descr_filter "$cfg" descr_filter # description regex filter (must be built in) ++ config_get action "$cfg" action # accept/reject/disabled ++ ! is_port_or_range "$int_port" allowport0 && ++ log "ACL entry: Invalid port or port range ($int_port) in int_port ignored" daemon.warn && int_port=1-65535 ++ ! is_port_or_range "$ext_port" allowport0 && ++ log "ACL entry: Invalid port or port range ($ext_port) in ext_port ignored" daemon.warn && ext_port=1-65535 ++ [ "$descr_filter" != "" ] && descr_filter=" \"$descr_filter\"" ++ if [ "$action" = "accept" ]; then ++ action=allow ++ elif [ "$action" = "reject" ]; then ++ action=deny ++ elif [ "$action" != "disabled" ]; then ++ log "ACL entry: Entry with invalid action ($action) ignored" daemon.warn ++ action=disabled ++ fi ++ [ "$action" = "disabled" ] && return 0 ++ echo "$action $ext_port $int_addr $int_port${descr_filter} # $comment" + } + + upnpd() { +@@ -185,8 +186,9 @@ upnpd() { + + echo "# Enable internal networks / access control" + config_foreach upnpd_add_int_network_and_defaults internal_network pre-acl +- config_foreach conf_rule_add perm_rule ++ config_foreach upnpd_add_acl_entry acl_entry + config_foreach upnpd_add_int_network_and_defaults internal_network post-acl ++ echo "deny 1-65535 0.0.0.0/0 1-65535 # Reject ACL by default" + + } > "$tmpconf" + fi +@@ -255,18 +257,8 @@ upnpd_add_int_network_and_defaults() { + echo "# Enable internal network $interface ($device) with access defaults $access_defaults and check ACL $check_acl" + echo "listening_ip=$device" + for rejectport in $reject_ports; do +- if [ "$rejectport" -ge "1" ] 2>/dev/null && [ "$rejectport" -le "65535" ] 2>/dev/null || +- { +- [ "${rejectport%%-*}" -ge "1" ] 2>/dev/null && +- [ "${rejectport%%-*}" -le "65535" ] 2>/dev/null && +- [ "${rejectport##*-}" -ge "1" ] 2>/dev/null && +- [ "${rejectport##*-}" -le "65535" ] 2>/dev/null && +- [ "${rejectport##*-}" -ge "${rejectport%%-*}" ] 2>/dev/null +- }; then +- echo "deny $rejectport $subnet $rejectport # Reject port $rejectport on $interface" +- else ++ is_port_or_range "$rejectport" allowport0 && echo "deny $rejectport $subnet $rejectport # Reject port $rejectport on $interface" || + log "Invalid port or port range ($rejectport) in reject_ports ignored" daemon.warn +- fi + done + fi + if { [ "$2" = "post-acl" ] && [ "$check_acl" = "1" ]; } || +@@ -283,18 +275,8 @@ upnpd_add_int_network_and_defaults() { + log "Invalid access_defaults ($access_defaults) ignored" daemon.warn + fi + for acceptport in $accessdefaultsports $accept_ports; do +- if [ "$acceptport" -ge "1" ] 2>/dev/null && [ "$acceptport" -le "65535" ] 2>/dev/null || +- { +- [ "${acceptport%%-*}" -ge "1" ] 2>/dev/null && +- [ "${acceptport%%-*}" -le "65535" ] 2>/dev/null && +- [ "${acceptport##*-}" -ge "1" ] 2>/dev/null && +- [ "${acceptport##*-}" -le "65535" ] 2>/dev/null && +- [ "${acceptport##*-}" -ge "${acceptport%%-*}" ] 2>/dev/null +- }; then +- echo "allow $acceptport $subnet $acceptport # Accept port $acceptport on $interface" +- else ++ is_port_or_range "$acceptport" && echo "allow $acceptport $subnet $acceptport # Accept port $acceptport on $interface" || + log "Invalid port or port range ($acceptport) in accept_ports ignored" daemon.warn +- fi + done + fi + if [ "$2" = "pre-acl" ] && [ "$check_acl" = "0" ]; then +diff --git a/miniupnpd/files/upnpd-migration.uci-defaults b/miniupnpd/files/upnpd-migration.uci-defaults +index efde6ca1232212..ac0c8d3245c175 100644 +--- a/miniupnpd/files/upnpd-migration.uci-defaults ++++ b/miniupnpd/files/upnpd-migration.uci-defaults +@@ -147,6 +147,90 @@ if [ "$(uci -q get upnpd.config.notify_interval)" -le "900" ] 2>/dev/null; then + uci delete upnpd.config.notify_interval + fi + ++# Migrate ACL to new section, note that an empty ACL is now rejected alone ++# a) Empty/unmodified ACL: Set access defaults, add/update template entries ++# b) Modified ACL: ++# - Add missing entry action to avoid adding inverted actions when changing via LuCI ++# - Update entry action allow/deny -> accept/reject ++# - Update entry port options to only use the LuCI (and daemon) supported hyphen (-) as port range separator ++# - Not using access defaults, add template entries ++if uci -q get upnpd.@acl_entry[0] >/dev/null; then ++ log "Error migrating ACL, as the new UCI section already exists" daemon.err ++elif ! uci -q get upnpd.@perm_rule[0] >/dev/null; then ++ log "Empty ACL: Set access defaults, add templates, empty ACL rejected alone" ++ access_defaults=accept-all-ports ++ check_acl=0 ++ addtemplateentries=1 ++elif ! uci -q get upnpd.@perm_rule[2] >/dev/null && ++ [ "$(uci -q get upnpd.@perm_rule[0].int_addr)" = "0.0.0.0/0" ] && ++ [ "$(uci -q get upnpd.@perm_rule[0].int_ports)" = "1024-65535" ] && ++ [ "$(uci -q get upnpd.@perm_rule[0].ext_ports)" = "1024-65535" ] && ++ [ "$(uci -q get upnpd.@perm_rule[0].action)" = "allow" ] && ++ [ "$(uci -q get upnpd.@perm_rule[1].int_addr)" = "0.0.0.0/0" ] && ++ [ "$(uci -q get upnpd.@perm_rule[1].int_ports)" = "0-65535" ] && ++ [ "$(uci -q get upnpd.@perm_rule[1].ext_ports)" = "0-65535" ] && ++ [ "$(uci -q get upnpd.@perm_rule[1].action)" = "deny" ]; then ++ log "Unmodified ACL: Set access defaults, empty ACL rejected alone" ++ access_defaults=accept-high-ports ++ check_acl=0 ++ addtemplateentries=1 ++ uci delete upnpd.@perm_rule[-1] ++ uci delete upnpd.@perm_rule[-1] ++else ++ log "Modified ACL: Migrate entries/section, empty ACL rejected alone" ++ addtemplateentries=1 ++ entrynr=0 ++ while uci -q get upnpd.@perm_rule[$entrynr] >/dev/null; do ++ comment="$(uci -q get upnpd.@perm_rule[$entrynr].comment)" ++ int_addr="$(uci -q get upnpd.@perm_rule[$entrynr].int_addr)" ++ int_port="$(uci -q get upnpd.@perm_rule[$entrynr].int_ports)" ++ ext_port="$(uci -q get upnpd.@perm_rule[$entrynr].ext_ports)" ++ action="$(uci -q get upnpd.@perm_rule[$entrynr].action)" ++ echo "$int_port" | grep -q ":" && ++ log "ACL entry: Update int_port to use hyphen (-) as port range separator" && ++ int_port="$(echo "$int_port" | tr ":" "-")" ++ echo "$ext_port" | grep -q ":" && ++ log "ACL entry: Update ext_port to use hyphen (-) as port range separator" && ++ ext_port="$(echo "$ext_port" | tr ":" "-")" ++ [ "$action" = "" ] && log "ACL entry: Add missing action option" && action=reject ++ [ "$action" = "allow" ] && action=accept ++ [ "$action" = "deny" ] && action=reject ++ uci batch >/dev/null <<-EOF ++ add upnpd acl_entry ++ set upnpd.@acl_entry[-1].comment="${comment:-unspecified}" ++ set upnpd.@acl_entry[-1].int_addr="${int_addr:-0.0.0.0/0}" ++ set upnpd.@acl_entry[-1].int_port="$int_port" ++ set upnpd.@acl_entry[-1].ext_port="$ext_port" ++ set upnpd.@acl_entry[-1].action="$action" ++ EOF ++ entrynr=$((entrynr + 1)) ++ done ++ if [ "${int_addr:-0.0.0.0/0}" = "0.0.0.0/0" ] && [ "${int_port:-0-65535}" = "0-65535" ] && ++ [ "${ext_port:-0-65535}" = "0-65535" ] && [ "$action" = "reject" ]; then ++ log "ACL entry: Remove no longer useful reject by default entry" ++ uci delete upnpd.@acl_entry[-1] ++ fi ++ while uci -q delete upnpd.@perm_rule[-1]; do :; done ++fi ++if [ "$addtemplateentries" = "1" ]; then ++ uci batch >/dev/null <<-EOF ++ add upnpd acl_entry ++ add upnpd acl_entry ++ set upnpd.@acl_entry[-2].comment="High ports" ++ set upnpd.@acl_entry[-2].int_addr="0.0.0.0/0" ++ set upnpd.@acl_entry[-2].int_port="1024-65535" ++ set upnpd.@acl_entry[-2].ext_port="1024-65535" ++ set upnpd.@acl_entry[-2].action="disabled" ++ set upnpd.@acl_entry[-1].comment="Low/system ports" ++ set upnpd.@acl_entry[-1].int_addr="0.0.0.0/0" ++ set upnpd.@acl_entry[-1].int_port="1-1023" ++ set upnpd.@acl_entry[-1].ext_port="1-1023" ++ set upnpd.@acl_entry[-1].action="disabled" ++ EOF ++ uci -q get upnpd.@acl_entry[-3] >/dev/null && ++ uci reorder upnpd.@acl_entry[-2]=0 && uci reorder upnpd.@acl_entry[-1]=1 ++fi ++ + # Migrate internal_iface option to new internal_network section + if ! uci -q get upnpd.@internal_network[0] >/dev/null; then + ifnr=0 +@@ -154,6 +238,8 @@ if ! uci -q get upnpd.@internal_network[0] >/dev/null; then + log "Create new internal_network section for $interface" + uci add upnpd internal_network >/dev/null + uci set upnpd.@internal_network[$ifnr].interface="$interface" ++ [ "$access_defaults" != "" ] && uci set upnpd.@internal_network[$ifnr].access_defaults="$access_defaults" ++ [ "$check_acl" = "0" ] && uci set upnpd.@internal_network[$ifnr].check_acl="0" + ifnr=$((ifnr + 1)) + done + uci -q delete upnpd.config.internal_iface +diff --git a/miniupnpd/files/upnpd.config b/miniupnpd/files/upnpd.config +index 2245b0a6f015f8..74fc64b58e9bf6 100644 +--- a/miniupnpd/files/upnpd.config ++++ b/miniupnpd/files/upnpd.config +@@ -18,18 +18,18 @@ config internal_network + option check_acl '0' # default 1 + + # Access Control List +-# Empty ACL rejected alone, IPv6 accepted unless disabled ++# Empty ACL rejected alone, IPv6 accepted unless disabled, action: accept/reject + +-config perm_rule +- option comment 'Allow high ports' ++config acl_entry ++ option comment 'High ports' + option int_addr '0.0.0.0/0' +- option int_ports '1024-65535' +- option ext_ports '1024-65535' +- option action 'allow' ++ option int_port '1024-65535' ++ option ext_port '1024-65535' ++ option action 'disabled' + +-config perm_rule +- option comment 'Default deny' ++config acl_entry ++ option comment 'Low/system ports' + option int_addr '0.0.0.0/0' +- option int_ports '1-65535' +- option ext_ports '1-65535' +- option action 'deny' ++ option int_port '1-1023' ++ option ext_port '1-1023' ++ option action 'disabled' + +From b58735acf4c5419987bde2d8947e67d14bcd7b9e Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 08/10] miniupnpd: separate service start and config-gen + +- Remove `config_foreach upnpd "upnpd"` and replace it with regular + function call, as init was not designed for a multi-instance setup, as + the same `tmpconf` will be used/overwritten, and non-anonymous section +- Move code to make the custom vs. config file generation decision + earlier, and only perform external interface detection with the second + one, and rename function `upnpd` to `upnpd_generate_config` +- Replace unnecessary `if` cases with `elif` in init/hotplug +- Exit with 1 on errors to get an inactive service status +- Use `procd_add_reload_trigger "firewall"` instead of listening + `/etc/config/firewall` + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + miniupnpd/files/miniupnpd.hotplug | 33 +++--- + miniupnpd/files/miniupnpd.init | 146 +++++++++++++------------- + 2 files changed, 86 insertions(+), 93 deletions(-) + +diff --git a/miniupnpd/files/miniupnpd.hotplug b/miniupnpd/files/miniupnpd.hotplug +index 20ee344c9a0e72..e86097e86010a7 100644 +--- a/miniupnpd/files/miniupnpd.hotplug ++++ b/miniupnpd/files/miniupnpd.hotplug +@@ -1,11 +1,9 @@ ++#!/bin/sh + /etc/init.d/miniupnpd enabled || exit 0 + +-# If miniupnpd is not running: +-# - check on _any_ event (event updates may contribute to network_find_wan*) +- +-# If miniupnpd _is_ running: +-# - check only on ifup (otherwise lease updates etc would cause +-# miniupnpd state loss) ++# If daemon is: ++# - not running: check on any event (event updates may contribute to network_find_wan*) ++# - running: check only on ifup (otherwise lease updates etc. would cause daemon state loss) + + [ "$ACTION" != "ifup" ] && /etc/init.d/miniupnpd running && exit 0 + +@@ -16,26 +14,19 @@ external_zone=$(uci -q get upnpd.settings.external_zone) + [ -x "$(command -v nft)" ] && FW="fw4" || FW="fw3" + + . /lib/functions/network.sh +- +-if [ -n "$external_iface" ] ; then ++if [ -n "$external_iface" ]; then + network_get_device ifname "$external_iface" ++elif [ -n "$external_zone" ]; then ++ ifname=$($FW -q zone "$external_zone" 2>/dev/null | head -1) + else +- if [ -n "$external_zone" ] ; then +- ifname=$($FW -q zone "$external_zone" 2>/dev/null | head -1) +- else +- network_find_wan external_iface && \ +- network_get_device ifname "$external_iface" +- fi ++ network_find_wan external_iface && network_get_device ifname "$external_iface" + fi +-if [ -n "$external_iface6" ] ; then ++if [ -n "$external_iface6" ]; then + network_get_device ifname6 "$external_iface6" ++elif [ -n "$external_zone" ]; then ++ ifname6=$($FW -q zone "$external_zone" 2>/dev/null | head -1) + else +- if [ -n "$external_zone" ] ; then +- ifname6=$($FW -q zone "$external_zone" 2>/dev/null | head -1) +- else +- network_find_wan6 external_iface6 && \ +- network_get_device ifname6 "$external_iface6" +- fi ++ network_find_wan6 external_iface6 && network_get_device ifname6 "$external_iface6" + fi + + [ "$DEVICE" != "$ifname" ] && [ "$DEVICE" != "$ifname6" ] && exit 0 +diff --git a/miniupnpd/files/miniupnpd.init b/miniupnpd/files/miniupnpd.init +index b4e09eb84067db..a66587153d2706 100644 +--- a/miniupnpd/files/miniupnpd.init ++++ b/miniupnpd/files/miniupnpd.init +@@ -42,25 +42,16 @@ upnpd_add_acl_entry() { + echo "$action $ext_port $int_addr $int_port${descr_filter} # $comment" + } + +-upnpd() { +- config_load "upnpd" +- local enabled +- config_get enabled settings enabled 0 +- if [ "$enabled" != "1" ]; then +- log "Service disabled, enabled UCI option not set" +- return 1 +- fi ++upnpd_generate_config() { + # Daemon +- local enable_protocols allow_cgnat stun_host allow_third_party_mapping ipv6_disable system_uptime log_output lease_file config_file ++ local enable_protocols allow_cgnat stun_host allow_third_party_mapping ipv6_disable system_uptime lease_file + config_get enable_protocols settings enable_protocols all + config_get allow_cgnat settings allow_cgnat 0 + config_get stun_host settings stun_host stun.nextcloud.com + config_get allow_third_party_mapping settings allow_third_party_mapping 0 + config_get ipv6_disable settings ipv6_disable 0 + config_get system_uptime settings system_uptime 1 +- config_get log_output settings log_output + config_get lease_file settings lease_file /var/run/miniupnpd.leases +- config_get config_file settings config_file + + # UPnP IGD + local upnp_igd_compat download_kbps upload_kbps friendly_name model_number serial_number presentation_url uuid http_port notify_interval +@@ -82,53 +73,51 @@ upnpd() { + config_get external_zone settings external_zone + config_get external_ip settings external_ip + +- local conf ifname ifname6 ++ local ifname ifname6 + . /lib/functions/network.sh +- +- if [ -n "$external_iface" ] ; then ++ if [ -n "$external_iface" ]; then + network_get_device ifname "$external_iface" ++ elif [ -n "$external_zone" ]; then ++ ifname=$($FW -q zone "$external_zone" 2>/dev/null | head -1) + else +- if [ -n "$external_zone" ] ; then +- ifname=$($FW -q zone "$external_zone" 2>/dev/null | head -1) +- else +- network_find_wan external_iface && \ +- network_get_device ifname "$external_iface" +- fi ++ network_find_wan external_iface && network_get_device ifname "$external_iface" + fi +- if [ -n "$external_iface6" ] ; then ++ if [ -n "$external_iface6" ]; then + network_get_device ifname6 "$external_iface6" ++ elif [ -n "$external_zone" ]; then ++ ifname6=$($FW -q zone "$external_zone" 2>/dev/null | head -1) + else +- if [ -n "$external_zone" ] ; then +- ifname6=$($FW -q zone "$external_zone" 2>/dev/null | head -1) +- else +- network_find_wan6 external_iface6 && \ +- network_get_device ifname6 "$external_iface6" +- fi ++ network_find_wan6 external_iface6 && network_get_device ifname6 "$external_iface6" ++ fi ++ if [ "$ifname" = "" ]; then ++ log "No external network interface found, not starting" daemon.err ++ return 1 ++ fi ++ # Workaround for daemon bug with UPnP IGDv2 if IPv6 is not ready at start ++ if [ "$ipv6_disable" = "0" ] && [ "$(uci -q get firewall.@defaults[0].disable_ipv6)" != "1" ] && ++ [ "$(uci -q get network.wan6.disabled)" != "1" ]; then ++ local pass=0 ++ while ! ip -6 addr show dev "${ifname6:-$ifname}" | grep -q "inet6 [23]"; do ++ log "IPv6 not ready yet; delay start" ++ sleep 5 ++ pass=$((pass + 1)) ++ [ "$pass" = "4" ] && log "IPv6 GUA not yet available, UPnP IGD mapping not possible" && break ++ done ++ fi ++ if ! uci -q get upnpd.@internal_network[0].interface >/dev/null; then ++ log "No internal networks configured, not starting" daemon.err ++ return 1 + fi ++ # Only perform an STUN CGNAT test if necessary, with a private/CGNAT-reserved external IPv4 ++ local extipv4 extipv4private ++ extipv4="$(ip -4 addr show dev "$ifname" | grep inet | head -1 | sed -E "s/.*inet ([0-9.]+).*/\1/")" ++ case "$extipv4" in ++ 10.* | 100.6[4-9].* | 100.[7-9][0-9].* | 100.1[0-1][0-9].* | 100.12[0-7].* | 172.1[6-9].* | \ ++ 172.2[0-9].* | 172.3[0-1].* | 192.0.0.[1-6] | 192.168.* | 198.1[89].*) extipv4private=1 ;; ++ esac ++ [ "$extipv4private" = "1" ] && log "Private/CGNAT-reserved external IPv4 detected ($extipv4)" + +- if [ -n "$config_file" ]; then +- conf="$config_file" +- else +- local tmpconf="/var/etc/miniupnpd.conf" +- conf="$tmpconf" +- mkdir -p /var/etc +- if [ "$ifname" = "" ]; then +- log "No external network interface found, not starting" daemon.err +- return 1 +- fi +- if ! uci -q get upnpd.@internal_network[0].interface >/dev/null; then +- log "No internal networks configured, not starting" daemon.err +- return 1 +- fi +- # Only perform an STUN CGNAT test if necessary, with a private/CGNAT-reserved external IPv4 +- local extipv4 extipv4private +- extipv4="$(ip -4 addr show dev "$ifname" | grep inet | head -1 | sed -E "s/.*inet ([0-9.]+).*/\1/")" +- case "$extipv4" in +- 10.* | 100.6[4-9].* | 100.[7-9][0-9].* | 100.1[0-1][0-9].* | 100.12[0-7].* | 172.1[6-9].* | \ +- 172.2[0-9].* | 172.3[0-1].* | 192.0.0.[1-6] | 192.168.* | 198.1[89].*) extipv4private=1 ;; +- esac +- [ "$extipv4private" = "1" ] && log "Private/CGNAT-reserved external IPv4 detected ($extipv4)" +- { ++ { + echo "# Daemon" + [ "$enable_protocols" = "all" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=yes" + [ "$enable_protocols" = "upnp-igd" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=no" +@@ -190,32 +179,15 @@ upnpd() { + config_foreach upnpd_add_int_network_and_defaults internal_network post-acl + echo "deny 1-65535 0.0.0.0/0 1-65535 # Reject ACL by default" + +- } > "$tmpconf" +- fi +- +- if [ -n "$ifname" ]; then +- if [ "$FW" = "fw4" ]; then +- nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload +- else +- iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload +- fi +- fi +- +- procd_open_instance +- procd_set_param file "$conf" "/etc/config/firewall" +- procd_set_param command "$PROG" +- procd_append_param command -f "$conf" +- [ "$log_output" = "info" ] && procd_append_param command -v +- [ "$log_output" = "debug" ] && procd_append_param command -v -v +- procd_close_instance ++ } >"$1" + } + + stop_service() { + if [ "$FW" = "fw3" ]; then +- iptables -t nat -F MINIUPNPD 2>/dev/null +- iptables -t nat -F MINIUPNPD-POSTROUTING 2>/dev/null + iptables -t filter -F MINIUPNPD 2>/dev/null + [ -x /usr/sbin/ip6tables ] && ip6tables -t filter -F MINIUPNPD 2>/dev/null ++ iptables -t nat -F MINIUPNPD 2>/dev/null ++ iptables -t nat -F MINIUPNPD-POSTROUTING 2>/dev/null + else + nft flush chain inet fw4 upnp_forward 2>/dev/null + nft flush chain inet fw4 upnp_prerouting 2>/dev/null +@@ -225,11 +197,41 @@ stop_service() { + + start_service() { + config_load "upnpd" +- config_foreach upnpd "upnpd" ++ local enabled config_file log_output conf ++ config_get enabled settings enabled 0 ++ config_get config_file settings config_file ++ config_get log_output settings log_output ++ if [ "$enabled" != "1" ]; then ++ log "Service disabled, enabled UCI option not set" ++ return 1 ++ fi ++ ++ if [ -n "$config_file" ]; then ++ conf="$config_file" ++ else ++ local tmpconf="/var/etc/miniupnpd.conf" ++ conf="$tmpconf" ++ mkdir -p /var/etc ++ upnpd_generate_config "$tmpconf" || return 1 ++ fi ++ ++ if [ "$FW" = "fw4" ]; then ++ nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload ++ else ++ iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload ++ fi ++ ++ procd_open_instance ++ procd_set_param file "$conf" ++ procd_set_param command "$PROG" ++ procd_append_param command -f "$conf" ++ [ "$log_output" = "info" ] && procd_append_param command -v ++ [ "$log_output" = "debug" ] && procd_append_param command -v -v ++ procd_close_instance + } + + service_triggers() { +- procd_add_reload_trigger "upnpd" ++ procd_add_reload_trigger "upnpd" "firewall" + } + + log() { + +From 562d07c55e1e8d46e6ec45f4397c9a02ba5429c5 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 09/10] miniupnpd: rearrange init, format `firewall3.include` + +- Arrange `start_service` and main init functions first +- Format `firewall3.include` using shfmt + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + miniupnpd/files/firewall3.include | 44 +++---- + miniupnpd/files/miniupnpd.init | 164 +++++++++++++------------- + 2 files changed, 104 insertions(+), 104 deletions(-) + +diff --git a/miniupnpd/files/firewall3.include b/miniupnpd/files/firewall3.include +index 53ca8dc716cc2f..8fc2144db31932 100644 +--- a/miniupnpd/files/firewall3.include ++++ b/miniupnpd/files/firewall3.include +@@ -20,36 +20,36 @@ iptables_prepend_rule() { + local chain="$3" + local target="$4" + +- $iptables "$IPTARGS" -t "$table" -I "$chain" $($iptables "$IPTARGS" -t "$table" --line-numbers -nL "$chain" | \ ++ $iptables "$IPTARGS" -t "$table" -I "$chain" $($iptables "$IPTARGS" -t "$table" --line-numbers -nL "$chain" | + sed -ne '$s/[^0-9].*//p') -j "$target" + } + + ADDED=0 + + add_extzone_rules() { +- local ext_zone="$1" +- +- [ -z "$ext_zone" ] && return +- +- # IPv4 - due to NAT, need to add both to nat and filter table +- # need to insert as penultimate rule for input & forward & postrouting since final rule might be a fw3 REJECT +- iptables_prepend_rule "$IPTABLES" filter "zone_${ext_zone}_input" MINIUPNPD +- iptables_prepend_rule "$IPTABLES" filter "zone_${ext_zone}_forward" MINIUPNPD +- $IPTABLES -t nat -A "zone_${ext_zone}_prerouting" -j MINIUPNPD +- iptables_prepend_rule "$IPTABLES" nat "zone_${ext_zone}_postrouting" MINIUPNPD-POSTROUTING +- +- # IPv6 if available - filter only +- [ -x $IP6TABLES ] && { +- iptables_prepend_rule "$IP6TABLES" filter "zone_${ext_zone}_input" MINIUPNPD +- iptables_prepend_rule "$IP6TABLES" filter "zone_${ext_zone}_forward" MINIUPNPD +- } +- ADDED=$(($ADDED + 1)) ++ local ext_zone="$1" ++ ++ [ -z "$ext_zone" ] && return ++ ++ # IPv4 - due to NAT, need to add both to nat and filter table ++ # need to insert as penultimate rule for input & forward & postrouting since final rule might be a fw3 REJECT ++ iptables_prepend_rule "$IPTABLES" filter "zone_${ext_zone}_input" MINIUPNPD ++ iptables_prepend_rule "$IPTABLES" filter "zone_${ext_zone}_forward" MINIUPNPD ++ $IPTABLES -t nat -A "zone_${ext_zone}_prerouting" -j MINIUPNPD ++ iptables_prepend_rule "$IPTABLES" nat "zone_${ext_zone}_postrouting" MINIUPNPD-POSTROUTING ++ ++ # IPv6 if available - filter only ++ [ -x $IP6TABLES ] && { ++ iptables_prepend_rule "$IP6TABLES" filter "zone_${ext_zone}_input" MINIUPNPD ++ iptables_prepend_rule "$IP6TABLES" filter "zone_${ext_zone}_forward" MINIUPNPD ++ } ++ ADDED=$(($ADDED + 1)) + } + + # By default, user configuration is king. + + for ext_iface in $(uci -q get upnpd.settings.external_iface); do +- add_extzone_rules $(fw3 -q network "$ext_iface") ++ add_extzone_rules $(fw3 -q network "$ext_iface") + done + + add_extzone_rules $(uci -q get upnpd.settings.external_zone) +@@ -66,7 +66,7 @@ network_find_wan wan_iface + network_find_wan6 wan6_iface + + for ext_iface in $wan_iface $wan6_iface; do +- # fw3 -q network fails on sub-interfaces => map to device first +- network_get_device ext_device $ext_iface +- add_extzone_rules $(fw3 -q device "$ext_device") ++ # fw3 -q network fails on sub-interfaces => map to device first ++ network_get_device ext_device $ext_iface ++ add_extzone_rules $(fw3 -q device "$ext_device") + done +diff --git a/miniupnpd/files/miniupnpd.init b/miniupnpd/files/miniupnpd.init +index a66587153d2706..a6611c0e732e0d 100644 +--- a/miniupnpd/files/miniupnpd.init ++++ b/miniupnpd/files/miniupnpd.init +@@ -7,39 +7,56 @@ USE_PROCD=1 + PROG=/usr/sbin/miniupnpd + [ -x "$(command -v nft)" ] && FW="fw4" || FW="fw3" + +-is_port_or_range() { +- [ "$1" = "0" ] && [ "$2" != "allowport0" ] && return 1 +- [ "$1" -ge "1" ] 2>/dev/null && [ "$1" -le "65535" ] 2>/dev/null && return 0 +- [ "$2" = "allowport0" ] && local minport=0 || local minport=1 +- [ "${1%%-*}" -ge "$minport" ] 2>/dev/null && [ "${1%%-*}" -le "65535" ] 2>/dev/null && +- [ "${1##*-}" -ge "$minport" ] 2>/dev/null && [ "${1##*-}" -le "65535" ] 2>/dev/null && +- [ "${1##*-}" -ge "${1%%-*}" ] 2>/dev/null && return 0 || return 1 ++start_service() { ++ config_load "upnpd" ++ local enabled config_file log_output conf ++ config_get enabled settings enabled 0 ++ config_get config_file settings config_file ++ config_get log_output settings log_output ++ if [ "$enabled" != "1" ]; then ++ log "Service disabled, enabled UCI option not set" ++ return 1 ++ fi ++ ++ if [ -n "$config_file" ]; then ++ conf="$config_file" ++ else ++ local tmpconf="/var/etc/miniupnpd.conf" ++ conf="$tmpconf" ++ mkdir -p /var/etc ++ upnpd_generate_config "$tmpconf" || return 1 ++ fi ++ ++ if [ "$FW" = "fw4" ]; then ++ nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload ++ else ++ iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload ++ fi ++ ++ procd_open_instance ++ procd_set_param file "$conf" ++ procd_set_param command "$PROG" ++ procd_append_param command -f "$conf" ++ [ "$log_output" = "info" ] && procd_append_param command -v ++ [ "$log_output" = "debug" ] && procd_append_param command -v -v ++ procd_close_instance + } + +-upnpd_add_acl_entry() { +- local cfg="$1" +- local comment int_addr int_port ext_port descr_filter action +- config_get comment "$cfg" comment "unspecified" # comment +- config_get int_addr "$cfg" int_addr "0.0.0.0/0" # IPv4 or network and subnet mask (internal) +- config_get int_port "$cfg" int_port "1-65535" # internal port/range: x or x-y +- config_get ext_port "$cfg" ext_port "1-65535" # external port/range: x or x-y +- config_get descr_filter "$cfg" descr_filter # description regex filter (must be built in) +- config_get action "$cfg" action # accept/reject/disabled +- ! is_port_or_range "$int_port" allowport0 && +- log "ACL entry: Invalid port or port range ($int_port) in int_port ignored" daemon.warn && int_port=1-65535 +- ! is_port_or_range "$ext_port" allowport0 && +- log "ACL entry: Invalid port or port range ($ext_port) in ext_port ignored" daemon.warn && ext_port=1-65535 +- [ "$descr_filter" != "" ] && descr_filter=" \"$descr_filter\"" +- if [ "$action" = "accept" ]; then +- action=allow +- elif [ "$action" = "reject" ]; then +- action=deny +- elif [ "$action" != "disabled" ]; then +- log "ACL entry: Entry with invalid action ($action) ignored" daemon.warn +- action=disabled ++stop_service() { ++ if [ "$FW" = "fw3" ]; then ++ iptables -t filter -F MINIUPNPD 2>/dev/null ++ [ -x /usr/sbin/ip6tables ] && ip6tables -t filter -F MINIUPNPD 2>/dev/null ++ iptables -t nat -F MINIUPNPD 2>/dev/null ++ iptables -t nat -F MINIUPNPD-POSTROUTING 2>/dev/null ++ else ++ nft flush chain inet fw4 upnp_forward 2>/dev/null ++ nft flush chain inet fw4 upnp_prerouting 2>/dev/null ++ #nft flush chain inet fw4 upnp_postrouting 2>/dev/null + fi +- [ "$action" = "disabled" ] && return 0 +- echo "$action $ext_port $int_addr $int_port${descr_filter} # $comment" ++} ++ ++service_triggers() { ++ procd_add_reload_trigger "upnpd" "firewall" + } + + upnpd_generate_config() { +@@ -182,58 +199,6 @@ upnpd_generate_config() { + } >"$1" + } + +-stop_service() { +- if [ "$FW" = "fw3" ]; then +- iptables -t filter -F MINIUPNPD 2>/dev/null +- [ -x /usr/sbin/ip6tables ] && ip6tables -t filter -F MINIUPNPD 2>/dev/null +- iptables -t nat -F MINIUPNPD 2>/dev/null +- iptables -t nat -F MINIUPNPD-POSTROUTING 2>/dev/null +- else +- nft flush chain inet fw4 upnp_forward 2>/dev/null +- nft flush chain inet fw4 upnp_prerouting 2>/dev/null +- #nft flush chain inet fw4 upnp_postrouting 2>/dev/null +- fi +-} +- +-start_service() { +- config_load "upnpd" +- local enabled config_file log_output conf +- config_get enabled settings enabled 0 +- config_get config_file settings config_file +- config_get log_output settings log_output +- if [ "$enabled" != "1" ]; then +- log "Service disabled, enabled UCI option not set" +- return 1 +- fi +- +- if [ -n "$config_file" ]; then +- conf="$config_file" +- else +- local tmpconf="/var/etc/miniupnpd.conf" +- conf="$tmpconf" +- mkdir -p /var/etc +- upnpd_generate_config "$tmpconf" || return 1 +- fi +- +- if [ "$FW" = "fw4" ]; then +- nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload +- else +- iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload +- fi +- +- procd_open_instance +- procd_set_param file "$conf" +- procd_set_param command "$PROG" +- procd_append_param command -f "$conf" +- [ "$log_output" = "info" ] && procd_append_param command -v +- [ "$log_output" = "debug" ] && procd_append_param command -v -v +- procd_close_instance +-} +- +-service_triggers() { +- procd_add_reload_trigger "upnpd" "firewall" +-} +- + log() { + logger -s -p "${2:-daemon.notice}" -t "miniupnpd-init" "$1" || echo "miniupnpd-init: $1" >&2 + } +@@ -243,6 +208,15 @@ xml_encode() { + echo "$1" | sed "s/&/\&/g; s//\>/g" + } + ++is_port_or_range() { ++ [ "$1" = "0" ] && [ "$2" != "allowport0" ] && return 1 ++ [ "$1" -ge "1" ] 2>/dev/null && [ "$1" -le "65535" ] 2>/dev/null && return 0 ++ [ "$2" = "allowport0" ] && local minport=0 || local minport=1 ++ [ "${1%%-*}" -ge "$minport" ] 2>/dev/null && [ "${1%%-*}" -le "65535" ] 2>/dev/null && ++ [ "${1##*-}" -ge "$minport" ] 2>/dev/null && [ "${1##*-}" -le "65535" ] 2>/dev/null && ++ [ "${1##*-}" -ge "${1%%-*}" ] 2>/dev/null && return 0 || return 1 ++} ++ + upnpd_add_int_network_and_defaults() { + local cfg="$1" + local interface access_defaults accept_ports reject_ports check_acl +@@ -285,3 +259,29 @@ upnpd_add_int_network_and_defaults() { + echo "deny 1-65535 $subnet 1-65535 # Reject ACL by default on $interface" + fi + } ++ ++upnpd_add_acl_entry() { ++ local cfg="$1" ++ local comment int_addr int_port ext_port descr_filter action ++ config_get comment "$cfg" comment "unspecified" # comment ++ config_get int_addr "$cfg" int_addr "0.0.0.0/0" # IPv4 or network and subnet mask (internal) ++ config_get int_port "$cfg" int_port "1-65535" # internal port/range: x or x-y ++ config_get ext_port "$cfg" ext_port "1-65535" # external port/range: x or x-y ++ config_get descr_filter "$cfg" descr_filter # description regex filter (must be built in) ++ config_get action "$cfg" action # accept/reject/disabled ++ ! is_port_or_range "$int_port" allowport0 && ++ log "ACL entry: Invalid port or port range ($int_port) in int_port ignored" daemon.warn && int_port=1-65535 ++ ! is_port_or_range "$ext_port" allowport0 && ++ log "ACL entry: Invalid port or port range ($ext_port) in ext_port ignored" daemon.warn && ext_port=1-65535 ++ [ "$descr_filter" != "" ] && descr_filter=" \"$descr_filter\"" ++ if [ "$action" = "accept" ]; then ++ action=allow ++ elif [ "$action" = "reject" ]; then ++ action=deny ++ elif [ "$action" != "disabled" ]; then ++ log "ACL entry: Entry with invalid action ($action) ignored" daemon.warn ++ action=disabled ++ fi ++ [ "$action" = "disabled" ] && return 0 ++ echo "$action $ext_port $int_addr $int_port${descr_filter} # $comment" ++} + +From 397f547a4cda0a6ed83ecbb9dfd6a3f0252294a7 Mon Sep 17 00:00:00 2001 +From: Self Hosting Group +Date: Tue, 14 Jul 2026 00:00:00 +0000 +Subject: [PATCH 10/10] miniupnpd: service-wide access control settings + +This implements variant B + +upnpd.settings UCI options | Change | Previous name +-----------------------------|---------------------------|-------------- +access_defaults | New option (1) | +accept_ports | New option (1) | +reject_ports | New option (2) | +check_acl | New option (1) | + +Notes: +1. New options added for default ports that all devices can map, + client-specific permissions using the access control list (ACL) can + extend/override the defaults. Access defaults: accept-high-ports + accept-web+high-ports/accept-web-ports/accept-all-ports +2. Reject ports; overrides other settings. By default reject unsafe: + FTP (21), Telnet (23), DCE/NetBIOS/SMB (135/137-139/445), RDP (3389) + +(to merge with prior) + +Signed-off-by: Self Hosting Group +--- + miniupnpd/files/miniupnpd.init | 98 ++++++++----------- + .../files/upnpd-migration.uci-defaults | 16 +-- + miniupnpd/files/upnpd.config | 11 +-- + 3 files changed, 49 insertions(+), 76 deletions(-) + +diff --git a/miniupnpd/files/miniupnpd.init b/miniupnpd/files/miniupnpd.init +index a6611c0e732e0d..d7256603f0c34c 100644 +--- a/miniupnpd/files/miniupnpd.init ++++ b/miniupnpd/files/miniupnpd.init +@@ -61,15 +61,22 @@ service_triggers() { + + upnpd_generate_config() { + # Daemon +- local enable_protocols allow_cgnat stun_host allow_third_party_mapping ipv6_disable system_uptime lease_file ++ local enable_protocols allow_cgnat stun_host allow_third_party_mapping system_uptime lease_file + config_get enable_protocols settings enable_protocols all + config_get allow_cgnat settings allow_cgnat 0 + config_get stun_host settings stun_host stun.nextcloud.com + config_get allow_third_party_mapping settings allow_third_party_mapping 0 +- config_get ipv6_disable settings ipv6_disable 0 + config_get system_uptime settings system_uptime 1 + config_get lease_file settings lease_file /var/run/miniupnpd.leases + ++ # Access control ++ local access_defaults accept_ports reject_ports check_acl ipv6_disable ++ config_get access_defaults settings access_defaults none ++ config_get accept_ports settings accept_ports ++ config_get reject_ports settings reject_ports "21 23 135 137-139 445 3389" ++ config_get check_acl settings check_acl 1 ++ config_get ipv6_disable settings ipv6_disable 0 ++ + # UPnP IGD + local upnp_igd_compat download_kbps upload_kbps friendly_name model_number serial_number presentation_url uuid http_port notify_interval + config_get upnp_igd_compat settings upnp_igd_compat igdv1 +@@ -83,12 +90,13 @@ upnpd_generate_config() { + config_get http_port settings http_port 5000 + config_get notify_interval settings notify_interval + +- # External network interface +- local external_iface external_iface6 external_zone external_ip ++ # Network interfaces ++ local external_iface external_iface6 external_zone external_ip internal_iface + config_get external_iface settings external_iface + config_get external_iface6 settings external_iface6 + config_get external_zone settings external_zone + config_get external_ip settings external_ip ++ config_get internal_iface settings internal_iface lan + + local ifname ifname6 + . /lib/functions/network.sh +@@ -121,10 +129,6 @@ upnpd_generate_config() { + [ "$pass" = "4" ] && log "IPv6 GUA not yet available, UPnP IGD mapping not possible" && break + done + fi +- if ! uci -q get upnpd.@internal_network[0].interface >/dev/null; then +- log "No internal networks configured, not starting" daemon.err +- return 1 +- fi + # Only perform an STUN CGNAT test if necessary, with a private/CGNAT-reserved external IPv4 + local extipv4 extipv4private + extipv4="$(ip -4 addr show dev "$ifname" | grep inet | head -1 | sed -E "s/.*inet ([0-9.]+).*/\1/")" +@@ -151,7 +155,6 @@ upnpd_generate_config() { + [ "$allow_third_party_mapping" = "1" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=yes" + [ "$allow_third_party_mapping" = "upnp-igd" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=no" + [ "$allow_third_party_mapping" = "pcp" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=yes" +- [ "$ipv6_disable" = "0" ] && echo "ipv6_disable=no" || echo "ipv6_disable=yes" + [ "$system_uptime" = "0" ] && echo "system_uptime=no" || echo "system_uptime=yes" + touch "$lease_file" && echo "lease_file=$lease_file" + [ "$ipv6_disable" = "0" ] && touch "${lease_file}-ipv6" && echo "lease_file6=${lease_file}-ipv6" +@@ -191,9 +194,37 @@ upnpd_generate_config() { + [ -n "$external_ip" ] && echo "ext_ip=$external_ip" + + echo "# Enable internal networks / access control" +- config_foreach upnpd_add_int_network_and_defaults internal_network pre-acl +- config_foreach upnpd_add_acl_entry acl_entry +- config_foreach upnpd_add_int_network_and_defaults internal_network post-acl ++ [ "$ipv6_disable" = "0" ] && echo "ipv6_disable=no" || echo "ipv6_disable=yes" ++ local rejectport ++ for rejectport in $reject_ports; do ++ is_port_or_range "$rejectport" allowport0 && echo "deny $rejectport 0.0.0.0/0 $rejectport # Reject port $rejectport" || ++ log "Invalid port or port range ($rejectport) in reject_ports ignored" daemon.warn ++ done ++ [ "$check_acl" = "1" ] && echo "# Access control list" && config_foreach upnpd_add_acl_entry acl_entry ++ local iface ++ for iface in $internal_iface; do ++ local device subnet accessdefaultsports acceptport ++ network_get_device device "$iface" ++ network_get_subnet subnet "$iface" ++ [ "$subnet" = "" ] && log "Cannot get IPv4 subnet for network $iface, network ignored" daemon.warn && continue ++ echo "# Enable internal network $iface ($device) with access defaults $access_defaults and check ACL $check_acl" ++ echo "listening_ip=$device" ++ if [ "$access_defaults" = "accept-high-ports" ]; then ++ accessdefaultsports="1024-65535" ++ elif [ "$access_defaults" = "accept-web+high-ports" ]; then ++ accessdefaultsports="80 443 1024-65535" ++ elif [ "$access_defaults" = "accept-web-ports" ]; then ++ accessdefaultsports="80 443" ++ elif [ "$access_defaults" = "accept-all-ports" ]; then ++ accessdefaultsports="1-65535" ++ elif [ "$access_defaults" != "none" ]; then ++ log "Invalid access_defaults ($access_defaults) ignored" daemon.warn ++ fi ++ for acceptport in $accessdefaultsports $accept_ports; do ++ is_port_or_range "$acceptport" && echo "allow $acceptport $subnet $acceptport # Accept port $acceptport on $iface" || ++ log "Invalid port or port range ($acceptport) in accept_ports ignored" daemon.warn ++ done ++ done + echo "deny 1-65535 0.0.0.0/0 1-65535 # Reject ACL by default" + + } >"$1" +@@ -217,49 +248,6 @@ is_port_or_range() { + [ "${1##*-}" -ge "${1%%-*}" ] 2>/dev/null && return 0 || return 1 + } + +-upnpd_add_int_network_and_defaults() { +- local cfg="$1" +- local interface access_defaults accept_ports reject_ports check_acl +- config_get interface "$cfg" interface +- config_get access_defaults "$cfg" access_defaults none +- config_get accept_ports "$cfg" accept_ports +- config_get reject_ports "$cfg" reject_ports "21 23 135 137-139 445 3389" +- config_get check_acl "$cfg" check_acl 1 +- local device subnet rejectport accessdefaultsports acceptport +- network_get_device device "$interface" +- network_get_subnet subnet "$interface" +- [ "$subnet" = "" ] && log "Cannot get IPv4 subnet for network $interface, network ignored" daemon.warn && return 0 +- if [ "$2" = "pre-acl" ]; then +- echo "# Enable internal network $interface ($device) with access defaults $access_defaults and check ACL $check_acl" +- echo "listening_ip=$device" +- for rejectport in $reject_ports; do +- is_port_or_range "$rejectport" allowport0 && echo "deny $rejectport $subnet $rejectport # Reject port $rejectport on $interface" || +- log "Invalid port or port range ($rejectport) in reject_ports ignored" daemon.warn +- done +- fi +- if { [ "$2" = "post-acl" ] && [ "$check_acl" = "1" ]; } || +- { [ "$2" = "pre-acl" ] && [ "$check_acl" = "0" ]; }; then +- if [ "$access_defaults" = "accept-high-ports" ]; then +- accessdefaultsports="1024-65535" +- elif [ "$access_defaults" = "accept-web+high-ports" ]; then +- accessdefaultsports="80 443 1024-65535" +- elif [ "$access_defaults" = "accept-web-ports" ]; then +- accessdefaultsports="80 443" +- elif [ "$access_defaults" = "accept-all-ports" ]; then +- accessdefaultsports="1-65535" +- elif [ "$access_defaults" != "none" ]; then +- log "Invalid access_defaults ($access_defaults) ignored" daemon.warn +- fi +- for acceptport in $accessdefaultsports $accept_ports; do +- is_port_or_range "$acceptport" && echo "allow $acceptport $subnet $acceptport # Accept port $acceptport on $interface" || +- log "Invalid port or port range ($acceptport) in accept_ports ignored" daemon.warn +- done +- fi +- if [ "$2" = "pre-acl" ] && [ "$check_acl" = "0" ]; then +- echo "deny 1-65535 $subnet 1-65535 # Reject ACL by default on $interface" +- fi +-} +- + upnpd_add_acl_entry() { + local cfg="$1" + local comment int_addr int_port ext_port descr_filter action +diff --git a/miniupnpd/files/upnpd-migration.uci-defaults b/miniupnpd/files/upnpd-migration.uci-defaults +index ac0c8d3245c175..60f80d54fffa28 100644 +--- a/miniupnpd/files/upnpd-migration.uci-defaults ++++ b/miniupnpd/files/upnpd-migration.uci-defaults +@@ -212,6 +212,8 @@ else + fi + while uci -q delete upnpd.@perm_rule[-1]; do :; done + fi ++[ "$access_defaults" != "" ] && uci set upnpd.config.access_defaults="$access_defaults" ++[ "$check_acl" = "0" ] && uci set upnpd.config.check_acl="0" + if [ "$addtemplateentries" = "1" ]; then + uci batch >/dev/null <<-EOF + add upnpd acl_entry +@@ -231,20 +233,6 @@ if [ "$addtemplateentries" = "1" ]; then + uci reorder upnpd.@acl_entry[-2]=0 && uci reorder upnpd.@acl_entry[-1]=1 + fi + +-# Migrate internal_iface option to new internal_network section +-if ! uci -q get upnpd.@internal_network[0] >/dev/null; then +- ifnr=0 +- for interface in $(uci -q get upnpd.config.internal_iface || echo lan); do +- log "Create new internal_network section for $interface" +- uci add upnpd internal_network >/dev/null +- uci set upnpd.@internal_network[$ifnr].interface="$interface" +- [ "$access_defaults" != "" ] && uci set upnpd.@internal_network[$ifnr].access_defaults="$access_defaults" +- [ "$check_acl" = "0" ] && uci set upnpd.@internal_network[$ifnr].check_acl="0" +- ifnr=$((ifnr + 1)) +- done +- uci -q delete upnpd.config.internal_iface +-fi +- + # Rename UCI section config -> settings (v2.0) + if uci -q get upnpd.config >/dev/null; then + log "Rename UCI section config -> settings (v2.0)" && uci rename upnpd.config="settings" || +diff --git a/miniupnpd/files/upnpd.config b/miniupnpd/files/upnpd.config +index 74fc64b58e9bf6..1ae67365486063 100644 +--- a/miniupnpd/files/upnpd.config ++++ b/miniupnpd/files/upnpd.config +@@ -4,19 +4,16 @@ config upnpd 'settings' + option enabled '0' + # Can be set to all/upnp-igd/pcp+nat-pmp + option enable_protocols 'all' ++ # Enable networks, multiple can be specified, separated by a space ++ option internal_iface 'lan' ++ option access_defaults 'accept-high-ports' # default none ++ option check_acl '0' # default 1 + #option ipv6_disable '0' + #option allow_third_party_mapping '0' + # Extra logging by setting to info or debug (previously 1) + option log_output 'default' + option upnp_igd_compat 'igdv1' + +-# Enable Networks / Access Control +- +-config internal_network +- option interface 'lan' +- option access_defaults 'accept-high-ports' # default none +- option check_acl '0' # default 1 +- + # Access Control List + # Empty ACL rejected alone, IPv6 accepted unless disabled, action: accept/reject +