From 5886f7478de8e89f55a10d3fbfdcb3bb64cbcc8c Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Tue, 1 Jun 2021 22:23:09 +0200 Subject: [PATCH] luci-mod-network: interfaces: restructure DHCPv6 and IPv6 RA options - Condense overly large IPv6 RA/DHCPv6 description texts and get rid of most embedded markup - Switch ra/ndp/dhcpv6 mode selections to rich dropdown lists and move extended choice descriptions next to the selection options - Drop ndproxy_static option which has been removed from odhcpd long ago - Add format validations to all text input fields - Add ability to configure master/relay modes for non-static interfaces (#2998) - Move extended RA configuration options into a new tab - Prevent enabling master mode on multiple interfaces - Prevent enabling ra/dhcpv6 server mode on non-static or master interfaces - Drop ra_management in favor to ra_flags option (#5083) - Add support for dns_service option - Read current effective IPv6 MTU and hop limit placeholder values from procfs Signed-off-by: Jo-Philipp Wich (cherry picked from commit 3fbd4338846e8229935b54256a3a541a3e15d8bd) --- .../resources/view/network/interfaces.js | 394 ++++++++++++------ .../share/rpcd/acl.d/luci-mod-network.json | 2 + 2 files changed, 259 insertions(+), 137 deletions(-) diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js index a8e289c480..bb9e63d1e5 100644 --- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js @@ -228,6 +228,39 @@ function get_netmask(s, use_cfgvalue) { return subnetmask; } +var cbiRichListValue = form.ListValue.extend({ + renderWidget: function(section_id, option_index, cfgvalue) { + var choices = this.transformChoices(); + var widget = new ui.Dropdown((cfgvalue != null) ? cfgvalue : this.default, choices, { + id: this.cbid(section_id), + sort: this.keylist, + optional: true, + select_placeholder: this.select_placeholder || this.placeholder, + custom_placeholder: this.custom_placeholder || this.placeholder, + validate: L.bind(this.validate, this, section_id), + disabled: (this.readonly != null) ? this.readonly : this.map.readonly + }); + + return widget.render(); + }, + + value: function(value, title, description) { + if (description) { + form.ListValue.prototype.value.call(this, value, E([], [ + E('span', { 'class': 'hide-open' }, [ title ]), + E('div', { 'class': 'hide-close', 'style': 'min-width:25vw' }, [ + E('strong', [ title ]), + E('br'), + E('span', { 'style': 'white-space:normal' }, description) + ]) + ])); + } + else { + form.ListValue.prototype.value.call(this, value, title); + } + } +}); + return view.extend({ poll_status: function(map, networks) { var resolveZone = null; @@ -578,7 +611,6 @@ return view.extend({ if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd')) { o = s.taboption('dhcp', form.SectionValue, '_dhcp', form.TypedSection, 'dhcp'); - o.depends('proto', 'static'); ss = o.subsection; ss.uciconfig = 'dhcp'; @@ -588,6 +620,7 @@ return view.extend({ ss.tab('general', _('General Setup')); ss.tab('advanced', _('Advanced Settings')); ss.tab('ipv6', _('IPv6 Settings')); + ss.tab('ipv6-ra', _('IPv6 RA Settings')); ss.filter = function(section_id) { return (uci.get('dhcp', section_id, 'interface') == ifc.getName()); @@ -603,9 +636,15 @@ return view.extend({ this.map.save(function() { uci.add('dhcp', 'dhcp', section_id); uci.set('dhcp', section_id, 'interface', section_id); - uci.set('dhcp', section_id, 'start', 100); - uci.set('dhcp', section_id, 'limit', 150); - uci.set('dhcp', section_id, 'leasetime', '12h'); + + if (protoval == 'static') { + uci.set('dhcp', section_id, 'start', 100); + uci.set('dhcp', section_id, 'limit', 150); + uci.set('dhcp', section_id, 'leasetime', '12h'); + } + else { + uci.set('dhcp', section_id, 'ignore', 1); + } }); }, ifc.getName()) }, _('Setup DHCP Server')) @@ -614,169 +653,252 @@ return view.extend({ ss.taboption('general', form.Flag, 'ignore', _('Ignore interface'), _('Disable DHCP for this interface.')); - so = ss.taboption('general', form.Value, 'start', _('Start'), _('Lowest leased address as offset from the network address.')); - so.optional = true; - so.datatype = 'or(uinteger,ip4addr("nomask"))'; - so.default = '100'; + if (protoval == 'static') { + so = ss.taboption('general', form.Value, 'start', _('Start'), _('Lowest leased address as offset from the network address.')); + so.optional = true; + so.datatype = 'or(uinteger,ip4addr("nomask"))'; + so.default = '100'; - so = ss.taboption('general', form.Value, 'limit', _('Limit'), _('Maximum number of leased addresses.')); - so.optional = true; - so.datatype = 'uinteger'; - so.default = '150'; + so = ss.taboption('general', form.Value, 'limit', _('Limit'), _('Maximum number of leased addresses.')); + so.optional = true; + so.datatype = 'uinteger'; + so.default = '150'; - so = ss.taboption('general', form.Value, 'leasetime', _('Lease time'), _('Expiry time of leased addresses, minimum is 2 minutes (2m).')); - so.optional = true; - so.default = '12h'; + so = ss.taboption('general', form.Value, 'leasetime', _('Lease time'), _('Expiry time of leased addresses, minimum is 2 minutes (2m).')); + so.optional = true; + so.default = '12h'; - so = ss.taboption('advanced', form.Flag, 'dynamicdhcp', _('Dynamic DHCP'), _('Dynamically allocate DHCP addresses for clients. If disabled, only clients having static leases will be served.')); - so.default = so.enabled; + so = ss.taboption('advanced', form.Flag, 'dynamicdhcp', _('Dynamic DHCP'), _('Dynamically allocate DHCP addresses for clients. If disabled, only clients having static leases will be served.')); + so.default = so.enabled; - ss.taboption('advanced', form.Flag, 'force', _('Force'), _('Force DHCP on this network even if another server is detected.')); + ss.taboption('advanced', form.Flag, 'force', _('Force'), _('Force DHCP on this network even if another server is detected.')); - // XXX: is this actually useful? - //ss.taboption('advanced', form.Value, 'name', _('Name'), _('Define a name for this network.')); + // XXX: is this actually useful? + //ss.taboption('advanced', form.Value, 'name', _('Name'), _('Define a name for this network.')); - so = ss.taboption('advanced', form.Value, 'netmask', _('IPv4-Netmask'), _('Override the netmask sent to clients. Normally it is calculated from the subnet that is served.')); - so.optional = true; - so.datatype = 'ip4addr'; + so = ss.taboption('advanced', form.Value, 'netmask', _('IPv4-Netmask'), _('Override the netmask sent to clients. Normally it is calculated from the subnet that is served.')); + so.optional = true; + so.datatype = 'ip4addr'; - so.render = function(option_index, section_id, in_table) { - this.placeholder = get_netmask(s, true); - return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]); - }; + so.render = function(option_index, section_id, in_table) { + this.placeholder = get_netmask(s, true); + return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]); + }; + + so.validate = function(section_id, value) { + var uielem = this.getUIElement(section_id); + if (uielem) + uielem.setPlaceholder(get_netmask(s, false)); + return form.Value.prototype.validate.apply(this, [ section_id, value ]); + }; + + ss.taboption('advanced', form.DynamicList, 'dhcp_option', _('DHCP-Options'), _('Define additional DHCP options, for example "6,192.168.2.1,192.168.2.2" which advertises different DNS servers to clients.')); + } + + + var has_other_master = uci.sections('dhcp', 'dhcp').filter(function(s) { + return (s.interface != ifc.getName() && s.master == '1'); + })[0]; + + so = ss.taboption('ipv6', form.Flag , 'master', _('Designated master')); + so.readonly = has_other_master ? true : false; + so.description = has_other_master + ? _('Interface "%h" is already marked as designated master.').format(has_other_master.interface || has_other_master['.name']) + : _('Set this interface as master for RA and DHCPv6 relaying as well as NDP proxying.') + ; so.validate = function(section_id, value) { - var uielem = this.getUIElement(section_id); - if (uielem) - uielem.setPlaceholder(get_netmask(s, false)); - return form.Value.prototype.validate.apply(this, [ section_id, value ]); + var hybrid_downstream_desc = _('Operate in relay mode if a designated master interface is configured and active, otherwise fall back to server mode.'), + ndp_downstream_desc = _('Operate in relay mode if a designated master interface is configured and active, otherwise disable NDP proxying.'), + hybrid_master_desc = _('Operate in relay mode if an upstream IPv6 prefix is present, otherwise disable service.'), + checked = this.formvalue(section_id), + dhcpv6 = this.section.getOption('dhcpv6').getUIElement(section_id), + ndp = this.section.getOption('ndp').getUIElement(section_id), + ra = this.section.getOption('ra').getUIElement(section_id); + + if (checked == '1' || protoval != 'static') { + dhcpv6.node.querySelector('li[data-value="server"]').setAttribute('unselectable', ''); + + if (dhcpv6.getValue() == 'server') + dhcpv6.setValue('hybrid'); + + ra.node.querySelector('li[data-value="server"]').setAttribute('unselectable', ''); + + if (ra.getValue() == 'server') + ra.setValue('hybrid'); + } + + if (checked == '1') { + dhcpv6.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc; + ra.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc; + ndp.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc; + } + else { + if (protoval == 'static') { + dhcpv6.node.querySelector('li[data-value="server"]').removeAttribute('unselectable'); + ra.node.querySelector('li[data-value="server"]').removeAttribute('unselectable'); + } + + dhcpv6.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_downstream_desc; + ra.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_downstream_desc; + ndp.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = ndp_downstream_desc ; + } + + return true; }; - ss.taboption('advanced', form.DynamicList, 'dhcp_option', _('DHCP-Options'), _('Define additional DHCP options, \ - for example "6,192.168.2.1,192.168.2.2" which advertises different DNS servers to clients.')); - - for (var i = 0; i < ss.children.length; i++) - if (ss.children[i].option != 'ignore') - ss.children[i].depends('ignore', '0'); - - so = ss.taboption('ipv6', form.ListValue, 'ra', _('RA-Service'), _('')); - so.value('', _('disabled')); - so.value('server', _('server mode')); - so.value('relay', _('relay mode')); - so.value('hybrid', _('hybrid mode')); - - so = ss.taboption('ipv6', form.Value, 'ra_maxinterval', _('Max RA interval'), _('Maximum time allowed \ - between sending unsolicited RA. Default is 600 seconds (600).')); + + so = ss.taboption('ipv6', cbiRichListValue, 'ra', _('RA-Service'), + _('Configures the operation mode of the RA service on this interface.')); + so.value('', _('disabled'), + _('Do not send any RA messages on this interface.')); + so.value('server', _('server mode'), + _('Send RA messages advertising this device as IPv6 router.')); + so.value('relay', _('relay mode'), + _('Forward RA messages received on the designated master interface to downstream interfaces.')); + so.value('hybrid', _('hybrid mode'), ' '); + + + so = ss.taboption('ipv6-ra', cbiRichListValue, 'ra_default', _('Default router'), + _('Configures the default router advertisement in RA messages.')); + so.value('', _('automatic'), + _('Announce this device as default router if a local IPv6 default route is present.')); + so.value('1', _('on available prefix'), + _('Announce this device as default router if a public IPv6 prefix is available, regardless of local default route availability.')); + so.value('2', _('forced'), + _('Announce this device as default router regardless of whether a prefix or default route is present.')); + so.depends('ra', 'server'); + so.depends({ ra: 'hybrid', master: '0' }); + + so = ss.taboption('ipv6-ra', form.Flag, 'ra_slaac', _('Enable SLAAC'), + _('Set the autonomous address-configuration flag in the prefix information options of sent RA messages. When enabled, clients will perform stateless IPv6 address autoconfiguration.')); + so.default = so.enabled; + so.depends('ra', 'server'); + so.depends({ ra: 'hybrid', master: '0' }); + + so = ss.taboption('ipv6-ra', cbiRichListValue, 'ra_flags', _('RA Flags'), + _('Specifies the flags sent in RA messages, for example to instruct clients to request further information via stateful DHCPv6.')); + so.value('managed-config', _('managed config (M)'), + _('The Managed address configuration (M) flag indicates that IPv6 addresses are available via DHCPv6.')); + so.value('other-config', _('other config (O)'), + _('The Other configuration (O) flag indicates that other information, such as DNS servers, is available via DHCPv6.')); + so.value('home-agent', _('mobile home agent (H)'), + _('The Mobile IPv6 Home Agent (H) flag indicates that the device is also acting as Mobile IPv6 home agent on this link.')); + so.multiple = true; + so.select_placeholder = _('none'); + so.depends('ra', 'server'); + so.depends({ ra: 'hybrid', master: '0' }); + so.cfgvalue = function(section_id) { + var flags = L.toArray(uci.get('dhcp', section_id, 'ra_flags')); + return flags.length ? flags : [ 'other-config' ]; + }; + so.remove = function(section_id) { + uci.set('dhcp', section_id, 'ra_flags', [ 'none' ]); + }; + + so = ss.taboption('ipv6-ra', form.Value, 'ra_maxinterval', _('Max RA interval'), _('Maximum time allowed between sending unsolicited RA. Default is 600 seconds.')); so.optional = true; + so.datatype = 'uinteger'; so.placeholder = '600'; so.depends('ra', 'server'); - so.depends('ra', 'hybrid'); - so.depends('ra', 'relay'); + so.depends({ ra: 'hybrid', master: '0' }); - - so = ss.taboption('ipv6', form.Value, 'ra_mininterval', _('Min RA interval'), _('Minimum time allowed \ - between sending unsolicited RA. Default is 200 seconds (200).')); + so = ss.taboption('ipv6-ra', form.Value, 'ra_mininterval', _('Min RA interval'), _('Minimum time allowed between sending unsolicited RA. Default is 200 seconds.')); so.optional = true; + so.datatype = 'uinteger'; so.placeholder = '200'; so.depends('ra', 'server'); - so.depends('ra', 'hybrid'); - so.depends('ra', 'relay'); + so.depends({ ra: 'hybrid', master: '0' }); - so = ss.taboption('ipv6', form.Value, 'ra_lifetime', _('RA Lifetime'), _('Router Lifetime published \ - in RA messages. Default is 1800 seconds (1800). \ - Max 9000 seconds.')); + so = ss.taboption('ipv6-ra', form.Value, 'ra_lifetime', _('RA Lifetime'), _('Router Lifetime published in RA messages. Maximum is 9000 seconds.')); so.optional = true; + so.datatype = 'range(0, 9000)'; + so.placeholder = '1800'; so.depends('ra', 'server'); - so.depends('ra', 'hybrid'); - so.depends('ra', 'relay'); + so.depends({ ra: 'hybrid', master: '0' }); - so = ss.taboption('ipv6', form.Value, 'ra_mtu', _('RA MTU'), _('The MTU \ - to be published in RA messages. Default is 0 (0).\ - Min 1280.')); + so = ss.taboption('ipv6-ra', form.Value, 'ra_mtu', _('RA MTU'), _('The MTU to be published in RA messages. Minimum is 1280 bytes.')); so.optional = true; + so.datatype = 'range(1280, 65535)'; so.depends('ra', 'server'); - so.depends('ra', 'hybrid'); - so.depends('ra', 'relay'); + so.depends({ ra: 'hybrid', master: '0' }); + so.load = function(section_id) { + var dev = ifc.getL3Device(); + + if (dev) { + var path = "/proc/sys/net/ipv6/conf/%s/mtu".format(dev.getName()); + + return L.resolveDefault(fs.read(path), dev.getMTU()).then(L.bind(function(data) { + this.placeholder = data; + }, this)); + } + }; - so = ss.taboption('ipv6', form.Value, 'ra_hoplimit', _('RA Hop Limit'), _('The maximum hops \ - to be published in RA messages.
Default is 0 (0), meaning unspecified.\ - Max 255.')); + so = ss.taboption('ipv6-ra', form.Value, 'ra_hoplimit', _('RA Hop Limit'), _('The maximum hops to be published in RA messages. Maximum is 255 hops.')); so.optional = true; + so.datatype = 'range(0, 255)'; so.depends('ra', 'server'); - so.depends('ra', 'hybrid'); - so.depends('ra', 'relay'); - - so = ss.taboption('ipv6', form.ListValue, 'ra_management', _('DHCPv6-Mode'), _('Default is stateless + stateful
\ - ')); - so.value('0', _('stateless')); - so.value('1', _('stateless + stateful')); - so.value('2', _('stateful-only')); + so.depends({ ra: 'hybrid', master: '0' }); + so.load = function(section_id) { + var dev = ifc.getL3Device(); + + if (dev) { + var path = "/proc/sys/net/ipv6/conf/%s/hop_limit".format(dev.getName()); + + return L.resolveDefault(fs.read(path), 64).then(L.bind(function(data) { + this.placeholder = data; + }, this)); + } + }; + + + so = ss.taboption('ipv6', cbiRichListValue, 'dhcpv6', _('DHCPv6-Service'), + _('Configures the operation mode of the DHCPv6 service on this interface.')); + so.value('', _('disabled'), + _('Do not offer DHCPv6 service on this interface.')); + so.value('server', _('server mode'), + _('Provide a DHCPv6 server on this interface and reply to DHCPv6 solicitations and requests.')); + so.value('relay', _('relay mode'), + _('Forward DHCPv6 messages between the designated master interface and downstream interfaces.')); + so.value('hybrid', _('hybrid mode'), ' '); + + + so = ss.taboption('ipv6', form.DynamicList, 'dns', _('Announced IPv6 DNS servers'), + _('Specifies a fixed list of IPv6 DNS server addresses to announce via DHCPv6. If left unspecified, the device will announce itself as IPv6 DNS server unless the Local IPv6 DNS server option is disabled.')); + so.datatype = 'ip6addr("nomask")'; /* restrict to IPv6 only for now since dnsmasq (DHCPv4) does not honour this option */ so.depends('dhcpv6', 'server'); - so.depends('dhcpv6', 'hybrid'); - so.default = '1'; - - so = ss.taboption('ipv6', form.ListValue, 'dhcpv6', _('DHCPv6-Service'), _('')); - so.value('', _('disabled')); - so.value('server', _('server mode')); - so.value('relay', _('relay mode')); - so.value('hybrid', _('hybrid mode')); - - so = ss.taboption('ipv6', form.ListValue, 'ndp', _('NDP-Proxy'), _('Reverts to \ - disabled internally if there are no interfaces with boolean ndproxy_slave set to 1. Think of \ - NDP Proxy as Proxy ARP for IPv6: unify hosts on different physical \ - hardware segments into the same IP subnet. Consists of NS and \ - NA messages. NDP-Proxy \ - listens for NS on an interface marked with boolean \ - master as 1 (i.e. upstream), then queries the slave/internal interfaces for that target IP before finally \ - sending an NA message. \ - NDP is effectively ARP for IPv6. \ - NS and NA \ - detect reachability and duplicate addresses on a link, themselves also a prerequisite for SLAAC autoconfig.
\ - ')); - so.value('', _('disabled')); - so.value('relay', _('relay mode')); - so.value('hybrid', _('hybrid mode')); - - so = ss.taboption('ipv6', form.Flag, 'ndproxy_routing', _('Learn routes from NDP'), _('Default is on.')); - so.default = '1'; - so.optional = true; + so.depends({ dhcpv6: 'hybrid', master: '0' }); - so = ss.taboption('ipv6', form.Flag, 'ndproxy_slave', _('NDP-Proxy slave'), _('Set interface as NDP-Proxy external slave. Default is off.')); + so = ss.taboption('ipv6', form.Flag, 'dns_service', _('Local IPv6 DNS server'), + _('Announce this device as IPv6 DNS server.')); + so.default = so.enabled; + so.depends({ dhcpv6: 'server', dns: /^$/ }); + so.depends({ dhcpv6: 'hybrid', dns: /^$/, master: '0' }); - so = ss.taboption('ipv6', form.DynamicList, 'ndproxy_static', _('Static NDP-Proxy prefixes')); + so = ss.taboption('ipv6', form.DynamicList, 'domain', _('Announced DNS domains'), + _('Specifies a fixed list of DNS search domains to announce via DHCPv6. If left unspecified, the local device DNS search domain will be announced.')); + so.datatype = 'hostname'; + so.depends('dhcpv6', 'server'); + so.depends({ dhcpv6: 'hybrid', master: '0' }); - so = ss.taboption('ipv6', form.Flag , 'master', _('Master'), _('Set this interface as master for the dhcpv6 relay.')); - so.depends('dhcpv6', 'relay'); - so.depends('dhcpv6', 'hybrid'); - so = ss.taboption('ipv6', form.Flag, 'ra_default', _('Announce as default router'), _('Always, even if no public prefix is available.')); - so.depends('ra', 'server'); - so.depends('ra', 'hybrid'); + so = ss.taboption('ipv6', cbiRichListValue, 'ndp', _('NDP-Proxy'), + _('Configures the operation mode of the NDP proxy service on this interface.')); + so.value('', _('disabled'), + _('Do not proxy any NDP packets.')); + so.value('relay', _('relay mode'), + _('Forward NDP NS and NA messages between the designated master interface and downstream interfaces.')); + so.value('hybrid', _('hybrid mode'), ' '); - ss.taboption('ipv6', form.DynamicList, 'dns', _('Announced DNS servers')); - ss.taboption('ipv6', form.DynamicList, 'domain', _('Announced DNS domains')); + + so = ss.taboption('ipv6', form.Flag, 'ndproxy_routing', _('Learn routes'), _('Setup routes for proxied IPv6 neighbours.')); + so.default = so.enabled; + so.depends('ndp', 'relay'); + so.depends('ndp', 'hybrid'); + + so = ss.taboption('ipv6', form.Flag, 'ndproxy_slave', _('NDP-Proxy slave'), _('Set interface as NDP-Proxy external slave. Default is off.')); + so.depends({ ndp: 'relay', master: '0' }); + so.depends({ ndp: 'hybrid', master: '0' }); } ifc.renderFormOptions(s); @@ -1292,9 +1414,7 @@ return view.extend({ s.addremove = false; s.anonymous = true; - o = s.option(form.Value, 'ula_prefix', _('IPv6 ULA-Prefix'), _('Unique Local Address - in the range fc00::/7. \ - Typically only within the ‘local’ half fd00::/8. ULA for IPv6 is analogous to IPv4 private network addressing.\ - This prefix is randomly generated at first install.')); + o = s.option(form.Value, 'ula_prefix', _('IPv6 ULA-Prefix'), _('Unique Local Address - in the range fc00::/7. Typically only within the ‘local’ half fd00::/8. ULA for IPv6 is analogous to IPv4 private network addressing. This prefix is randomly generated at first install.')); o.datatype = 'cidr6'; o = s.option(form.Flag, 'packet_steering', _('Packet Steering'), _('Enable packet steering across all CPUs. May help or hinder network speed.')); diff --git a/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json b/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json index ade9915b91..6943d95637 100644 --- a/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json +++ b/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json @@ -5,6 +5,8 @@ "cgi-io": [ "exec" ], "file": { "/etc/iproute2/rt_tables": [ "read" ], + "/proc/sys/net/ipv6/conf/*/mtu": [ "read" ], + "/proc/sys/net/ipv6/conf/*/hop_limit": [ "read" ], "/usr/libexec/luci-peeraddr": [ "exec" ], "/usr/lib/opkg/info/netifd.control": [ "read" ] }, -- 2.30.2