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' }; } }