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; 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; 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
+