diff --git a/applications/luci-app-upnp/Makefile b/applications/luci-app-upnp/Makefile
index 80e668764d96..601122426a89 100644
--- a/applications/luci-app-upnp/Makefile
+++ b/applications/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/applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js b/applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js
index 67e255caea7a..45f03c68a457 100644
--- a/applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js
+++ b/applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js
@@ -7,14 +7,14 @@
const callUpnpGetStatus = rpc.declare({
object: 'luci.upnp',
method: 'get_status',
- expect: { }
+ expect: {}
});
const callUpnpDeleteRule = rpc.declare({
object: 'luci.upnp',
method: 'delete_rule',
- params: [ 'token' ],
- expect: { result : "OK" },
+ params: ['token'],
+ expect: { result: 'OK' },
});
function handleDelRule(num, ev) {
@@ -35,23 +35,23 @@ return baseclass.extend({
},
render: function(data) {
- var table = E('table', { 'class': 'table', 'id': 'upnp_status_table' }, [
+ const table = E('table', { 'class': 'table', 'id': 'upnp_status_table' }, [
E('tr', { 'class': 'tr table-titles' }, [
- E('th', { 'class': 'th' }, _('Client Name')),
- E('th', { 'class': 'th' }, _('Client Address')),
- E('th', { 'class': 'th' }, _('Client Port')),
- E('th', { 'class': 'th' }, _('External Port')),
+ E('th', { 'class': 'th' }, _('Hostname')),
+ E('th', { 'class': 'th' }, _('IP address')),
+ E('th', { 'class': 'th' }, _('Port')),
+ E('th', { 'class': 'th' }, _('External port')),
E('th', { 'class': 'th' }, _('Protocol')),
E('th', { 'class': 'th right' }, _('Expires')),
- E('th', { 'class': 'th' }, _('Description')),
+ E('th', { 'class': 'th' }, _('Added via / description')),
E('th', { 'class': 'th cbi-section-actions' }, '')
])
]);
- var rules = Array.isArray(data[0].rules) ? data[0].rules : [];
+ const rules = Array.isArray(data[0].rules) ? data[0].rules : [];
- var rows = rules.map(function(rule) {
- const padnum = (num, length) => num.toString().padStart(length, "0");
+ const rows = rules.map(function(rule) {
+ const padnum = (num, length) => num.toString().padStart(length, '0');
const expires_sec = rule?.expires || 0;
const hour = Math.floor(expires_sec / 3600);
const minute = Math.floor((expires_sec % 3600) / 60);
@@ -72,8 +72,9 @@ return baseclass.extend({
rule.descr,
E('button', {
'class': 'btn cbi-button-remove',
- 'click': L.bind(handleDelRule, this, rule.num)
- }, [ _('Delete') ])
+ 'click': L.bind(handleDelRule, this, rule.num),
+ 'title': _('Delete')
+ }, [_('Delete')])
];
});
diff --git a/applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js b/applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js
index 4657113b5813..1aa080886dd0 100644
--- a/applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js
+++ b/applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js
@@ -3,27 +3,29 @@
'require dom';
'require poll';
'require uci';
+'require ui';
'require rpc';
'require form';
+'require tools.widgets as widgets';
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 +46,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 +70,9 @@ return view.extend({
rule.descr,
E('button', {
'class': 'btn cbi-button-remove',
- 'click': L.bind(handleDelRule, this, rule.num)
- }, [ _('Delete') ])
+ 'click': L.bind(handleDelRule, this, rule.num),
+ 'title': _('Delete')
+ }, [_('Delete')])
];
});
@@ -80,35 +83,43 @@ 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')],
- _('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)
+ m = new form.Map('upnpd', _('UPnP IGD & PCP/NAT-PMP Service'),
+ _('The %s protocols/service enable permitted devices on local networks to autonomously set up 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 port maps (forwards) on this router.')
+ .format(protocols)
);
+ if (!uci.get('upnpd', 'settings')) {
+ ui.addNotification(null, E('div', '
' + _('No suitable configuration was found!') + '
' +
+ _('No suitable (LuCI app %s) config found in %s. 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 on the daemon package update.') + '
' +
+ '' + _('Go to package manager…') + ''), 'warning');
+ m.readonly = true;
+ }
s = m.section(form.GridSection, '_active_rules');
+ s.disable = uci.get('upnpd', 'settings', 'enabled') == '0';
s.render = L.bind(function(view, section_id) {
- var table = E('table', { 'class': 'table cbi-section-table', 'id': 'upnp_status_table' }, [
+ const table = E('table', { 'class': 'table cbi-section-table', 'id': 'upnp_status_table' }, [
E('tr', { 'class': 'tr table-titles' }, [
- E('th', { 'class': 'th' }, _('Client Name')),
- E('th', { 'class': 'th' }, _('Client Address')),
- E('th', { 'class': 'th' }, _('Client Port')),
- E('th', { 'class': 'th' }, _('External Port')),
+ E('th', { 'class': 'th' }, _('Hostname')),
+ E('th', { 'class': 'th' }, _('IP address')),
+ E('th', { 'class': 'th' }, _('Port')),
+ E('th', { 'class': 'th' }, _('External port')),
E('th', { 'class': 'th' }, _('Protocol')),
E('th', { 'class': 'th right' }, _('Expires')),
- E('th', { 'class': 'th' }, _('Description')),
+ E('th', { 'class': 'th' }, _('Added via / description')),
E('th', { 'class': 'th cbi-section-actions' }, '')
])
]);
- var rules = Array.isArray(data[0].rules) ? data[0].rules : [];
+ const rules = Array.isArray(data[0].rules) ? data[0].rules : [];
- var rows = rules.map(function(rule) {
+ const rows = rules.map(function(rule) {
return [
rule.host_hint || _('Unknown'),
rule.intaddr,
@@ -118,130 +129,273 @@ return view.extend({
rule.descr,
E('button', {
'class': 'btn cbi-button-remove',
- 'click': L.bind(handleDelRule, this, rule.num)
- }, [ _('Delete') ])
+ 'click': L.bind(handleDelRule, this, rule.num),
+ 'title': _('Delete')
+ }, [_('Delete')])
];
});
cbi_update_table(table, rows, E('em', _('There are no active port maps.')));
return E('div', { 'class': 'cbi-section cbi-tblsection' }, [
- E('h3', _('Active Service Port Maps')), table ]);
+ E('h3', _('Active Port Maps')), table
+ ]);
}, o, this);
- s = m.section(form.NamedSection, 'config', 'upnpd', _('Service Settings'));
+ s = m.section(form.NamedSection, 'settings', 'upnpd', _('Service Settings'));
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', _('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'));
- o.default = '1';
-
- o = s.taboption('setup', form.Flag, 'enable_natpmp', _('Enable PCP/NAT-PMP protocols'));
- o.default = '1';
-
- 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.Value, 'download', _('Download speed'),
- _('Report maximum download speed in kByte/s'));
- o.depends('enable_upnp', '1');
-
- o = s.taboption('setup', form.Value, 'upload', _('Upload speed'),
- _('Report maximum upload speed in kByte/s'));
- 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;
+
+ 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 to allow private IPs'));
+ o.value('1', _('Enabled'), _('Filtering test currently requires an extra firewall rule'));
+ o.value('allow-filtered', _('Enabled') + ' (' + _('allow filtered') + ')', _('Allow filtered IPv4 CGNAT test result'));
+ o.value('allow-private-ext-ipv4', _('Ignore CGNAT (allow private IPv4, avoid)'), _('No STUN public IPv4 detection; various 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, 'external_ip', _('Override external IPv4'),
+ _('Report custom public/external (WAN) IPv4 address'));
+ o.datatype = 'ip4addr("nomask")';
+ o.placeholder = '(203.1.2.3)';
+ o.depends('allow_cgnat', '');
+ o.depends('allow_cgnat', 'allow-private-ext-ipv4');
+
+ 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'));
- 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.depends('use_stun', '1');
- o.datatype = 'host';
-
- o = s.taboption('advanced', form.Value, 'stun_port', _('STUN port'));
- o.depends('use_stun', '1');
- o.datatype = 'port';
- o.placeholder = '3478';
-
- o = s.taboption('advanced', form.Flag, 'secure_mode', _('Enable secure mode'),
- _('Allow adding port maps for requesting IP addresses only'));
+ o = s.taboption('advanced', form.Flag, 'system_uptime', _('Report system instead of service uptime'));
o.default = '1';
- o.depends('enable_upnp', '1');
-
- 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.depends('to-disable-as-rarely-used', '1');
+ o.retain = true;
+
+ 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, 'lease_file', _('Service lease file'));
+ o.depends('to-disable-as-rarely-used', '1');
+ o.retain = true;
+
+ o = s.taboption('igd', form.Value, 'download_kbps', _('Download speed'),
+ _('Report maximum connection speed in kbit/s'));
o.datatype = 'uinteger';
- o.placeholder = '900';
- o.depends('enable_upnp', '1');
-
- o = s.taboption('advanced', form.Value, 'port', _('SOAP/HTTP port'));
- o.datatype = 'port';
- o.placeholder = '5000';
- o.depends('enable_upnp', '1');
+ o.placeholder = _('Default interface link speed');
+ o.depends('enable_protocols', 'upnp-igd');
+ o.depends('enable_protocols', 'all');
+ o.retain = true;
- o = s.taboption('advanced', form.Value, 'presentation_url', _('Presentation URL'),
- _('Report custom router web interface (presentation) URL'));
+ 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_protocols', 'upnp-igd');
+ // o.depends('enable_protocols', 'all');
+ o.depends('to-disable-as-rarely-used', '1');
+ o.retain = true;
+
+ o = s.taboption('igd', form.Value, 'serial_number', _('Announced serial number'));
+ // o.depends('enable_protocols', 'upnp-igd');
+ // o.depends('enable_protocols', 'all');
+ o.depends('to-disable-as-rarely-used', '1');
+ 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 = s.taboption('advanced', form.Value, 'uuid', _('Device UUID'));
- o.depends('enable_upnp', '1');
-
- o = s.taboption('advanced', form.Value, 'model_number', _('Announced model number'));
- o.depends('enable_upnp', '1');
-
- o = s.taboption('advanced', form.Value, 'serial_number', _('Announced serial number'));
- o.depends('enable_upnp', '1');
-
- o = s.taboption('advanced', form.Flag, 'system_uptime', _('Report system instead of service uptime'));
- o.default = '1';
- o.depends('enable_upnp', '1');
-
- s.taboption('advanced', form.Flag, 'log_output', _('Enable additional logging'),
- _('Puts extra debugging information into the system log'));
+ o.depends('enable_protocols', 'upnp-igd');
+ o.depends('enable_protocols', 'all');
+ o.retain = true;
- o = s.taboption('advanced', form.Value, 'upnp_lease_file', _('Service lease file'));
- o.placeholder = '/var/run/miniupnpd.leases';
+ o = s.taboption('igd', form.Value, 'uuid', _('Device UUID'));
+ // o.depends('enable_protocols', 'upnp-igd');
+ // o.depends('enable_protocols', 'all');
+ o.depends('to-disable-as-rarely-used', '1');
+ 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.'));
+ o = s.taboption('igd', form.Value, 'http_port', _('SOAP/HTTP port'));
+ o.datatype = 'port';
+ o.placeholder = '5000';
+ o.depends('enable_protocols', 'upnp-igd');
+ o.depends('enable_protocols', 'all');
+ o.retain = true;
+
+ 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_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.') + ' ' +
+ _('Use an access control preset for ports that all devices on a network can map.') + ' ' +
+ _('Alternatively, add client-specific permissions using the access control list (ACL), which can also extend/override a preset.') + ' ' +
+ _('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') + ' - ' + _('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.exclude = 'wan'; // wan6 should also be excluded
+ o.nocreate = true;
+ o.editable = true;
+ o.retain = true;
+ o.validate = function(section_id, value) {
+ // Commented out, as it causes issues with cloning
+ //let netcount = 0;
+ //for (let ifnr = 0; uci.get('upnpd', `@internal_network[${ifnr}]`, 'interface'); ifnr++) {
+ // if (uci.get('upnpd', `@internal_network[${ifnr}]`, 'interface') == value) netcount++;
+ //};
+ return (value == '' || value == 'wan' || value == 'wan6') ? '' : true;
+ };
+
+ o = s.option(form.ListValue, 'access_preset', _('Access preset'));
+ 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 only'));
+ 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.retain = true;
+ o.validate = function(section_id, value) {
+ return value.search(/^[0-9 -]*$/) != -1 ? true : _('Expecting: %s').format(_('valid port or port range (port1-port2)'));
+ };
+
+ 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; override other settings; use space for none'));
+ o.placeholder = '21 23 135 137-139 445 3389';
+ o.modalonly = true;
+ o.retain = true;
+ o.validate = function(section_id, value) {
+ return value.search(/^[0-9 -]*$/) != -1 ? true : _('Expecting: %s').format(_('valid port or port range (port1-port2)'));
+ };
+
+ o = s.option(form.Flag, 'ignore_acl', _('Ignore ACL'),
+ _('Do not check ACL entries before a preset; can extend/override a preset') + '
' +
+ _('Sequence: 1. Reject ports, 2. ACL entries (if not checked), 3. Preset ports, 4. Accept extra ports'));
+ 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 and rejected by default, with no preset. (should be part of extra tab)'));
s.anonymous = true;
s.addremove = true;
-
- s.option(form.Value, 'comment', _('Comment'));
-
- o = s.option(form.Value, 'int_addr', _('Client Address'));
+ s.cloneable = true;
+ s.sortable = true;
+ s.modaltitle = _('UPnP IGD & PCP') + ' - ' + _('Edit ACL Entry');
+ // Preferably: ACL part of extra tab with depends for section as immediately, and network section part of service setup tab. Nice to have: Add button (+input) calls function and opens modal pre-filled
+ let acl_used = false;
+ for (let ifnr = 0; uci.get('upnpd', `@internal_network[${ifnr}]`, 'interface'); ifnr++) {
+ if (!uci.get('upnpd', `@internal_network[${ifnr}]`, 'ignore_acl') == '1') {
+ acl_used = true;
+ break;
+ }
+ }
+ s.disable = !acl_used;
+
+ 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.default = '0.0.0.0/0';
+ o.editable = true;
+ o.retain = true;
- o = s.option(form.Value, 'int_ports', _('Client 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'),
+ _('A regular expression to check for a UPnP IGD IPv4 port map description'));
+ 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('ignore', _('Ignore'));
+ o.editable = true;
+ o.retain = true;
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', 'settings', '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/applications/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json b/applications/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json
index 4611f501692a..40cb4c17eac0 100644
--- a/applications/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json
+++ b/applications/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",
"read": {
"ubus": {
"luci.upnp": [ "get_status" ],
diff --git a/applications/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp b/applications/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp
index f72b57b7ba84..ecd516ec2707 100644
--- a/applications/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp
+++ b/applications/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,74 @@ import { access, open, popen } from 'fs';
import { connect } from 'ubus';
import { cursor } from 'uci';
-// Establish ubus connection persistently outside of the call handler scope to
-// prevent premature GC'ing. Can be moved into `get_status` callback once
-// https://github.com/jow-/ucode/commit/a58fe4709f661b5f28e26701ea8638efccf5aeb6
-// is merged.
-const ubus = connect();
+const uci = cursor();
+const leasefilepath = uci.get('upnpd', 'settings', 'lease_file') || '/var/run/miniupnpd.leases';
const methods = {
get_status: {
call: function(req) {
- const uci = cursor();
-
+ const ubus = connect();
const rules = [];
const leases = [];
-
- const leasefile = open(uci.get('upnpd', 'config', 'upnp_lease_file'), 'r');
-
+ const leasefile = open(leasefilepath, 'r');
if (leasefile) {
for (let line = leasefile.read('line'); length(line); line = leasefile.read('line')) {
const record = split(line, ':', 6);
-
if (length(record) == 6) {
+ let descr = trim(record[5]);
+ let m = match(descr, /^PCP [A-Z]+ ([0-9a-f]{24})$/);
+ if (m) descr = 'PCP (nonce ' + m[1] + ')';
+ else if (match(descr, /^NAT-PMP \d+ \w+$/)) descr = 'NAT-PMP';
+ else if (match(descr, /^IGD2 pinhole$/)) descr = 'UPnP IGDv2 IPv6';
+ else if (!match(descr, /^UPnP IGD/)) descr = 'UPnP IGD / ' + descr;
push(leases, {
proto: uc(record[0]),
extport: +record[1],
intaddr: arrtoip(iptoarr(record[2])),
intport: +record[3],
expires: record[4] - timelocal(localtime()),
- description: trim(record[5])
+ descr: descr
});
}
}
-
leasefile.close();
}
- const ipt = popen('iptables --line-numbers -t nat -xnvL MINIUPNPD 2>/dev/null');
-
+ //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
+ 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]+)\s+([a-z]+).+dpt:([0-9]+) to:(\S+):([0-9]+)/);
+ let m = match(line, /^([0-9]+).*\*\s+([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 +84,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 +99,6 @@ const methods = {
}
}
}
-
req.reply({ rules });
});
}
@@ -117,19 +108,13 @@ const methods = {
args: { token: 'token' },
call: function(req) {
const idx = +req.args?.token;
-
if (idx > 0) {
- const uci = cursor();
- const leasefile = uci.get('upnpd', 'config', 'upnp_lease_file');
-
- if (access(leasefile)) {
- system(['sed', '-i', '-e', `${idx}d`, leasefile]);
+ if (access(leasefilepath)) {
+ system(['sed', '-i', '-e', `${idx}d`, leasefilepath]);
system(['/etc/init.d/miniupnpd', 'restart']);
}
-
return { result: 'OK' };
}
-
return { result: 'Bad request' };
}
}