From ba530891d56ff89db799d5ccb87333dadfabf88e Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Fri, 27 Sep 2024 03:11:02 +0200 Subject: [PATCH] luci-proto-vxlan: introduce peers tab and additional vxlan settings Added ip-bridge dep for bridge command to manage FDB entries. Reference: https://events.static.linuxfound.org/sites/events/files/slides/2013-linuxcon.pdf Signed-off-by: Paul Donald --- protocols/luci-proto-vxlan/Makefile | 2 +- .../luci-static/resources/protocol/vxlan.js | 182 +++++++++++++++++- .../luci-static/resources/protocol/vxlan6.js | 182 +++++++++++++++++- 3 files changed, 357 insertions(+), 9 deletions(-) diff --git a/protocols/luci-proto-vxlan/Makefile b/protocols/luci-proto-vxlan/Makefile index 1b9cb45c8306..f78b8cdb5167 100644 --- a/protocols/luci-proto-vxlan/Makefile +++ b/protocols/luci-proto-vxlan/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=Support for Virtual eXtensible Local Area Network (VXLAN, RFC7348) -LUCI_DEPENDS:=+vxlan +LUCI_DEPENDS:=+vxlan +ip-bridge PKG_MAINTAINER:=Wojciech Jowsa PKG_LICENSE:=Apache-2.0 diff --git a/protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js b/protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js index 1d5052fdfca9..3997bc627785 100644 --- a/protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js +++ b/protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js @@ -2,6 +2,7 @@ 'require form'; 'require network'; 'require tools.widgets as widgets'; +'require uci'; network.registerPatternVirtual(/^vxlan-.+$/); @@ -37,11 +38,14 @@ return network.registerProtocol('vxlan', { renderFormOptions: function(s) { var o; - o = s.taboption('general', form.Value, 'peeraddr', _('Remote IPv4 address'), _('The IPv4 address or the fully-qualified domain name of the remote end.')); + o = s.taboption('general', form.Value, 'peeraddr', _('Remote IPv4 address'), _('The IPv4 address or the fully-qualified domain name of the remote end.') + '
' + + _('Alternatively, a multicast address to reach a group of peers.') + '
' + + _('Remote VTEP')); o.optional = false; o.datatype = 'or(hostname,ip4addr("nomask"))'; - o = s.taboption('general', form.Value, 'ipaddr', _('Local IPv4 address'), _('The local IPv4 address over which the tunnel is created (optional).')); + o = s.taboption('general', form.Value, 'ipaddr', _('Local IPv4 address'), _('The local IPv4 address over which the tunnel is created (optional).') + '
' + + _('Local VTEP')); o.optional = true; o.datatype = 'ip4addr("nomask")'; @@ -50,7 +54,78 @@ return network.registerProtocol('vxlan', { o.placeholder = 4789; o.datatype = 'port'; - o = s.taboption('general', form.Value, 'vid', _('VXLAN network identifier'), _('ID used to uniquely identify the VXLAN')); + o = s.taboption('general', form.Value, 'srcport', _('Source port range')); + o.optional = true; + o.placeholder = '5000-6000'; + o.datatype = 'portrange'; + o.load = function(section_id) { + const min = uci.get('network', section_id, 'srcportmin'); + const max = uci.get('network', section_id, 'srcportmax'); + if (!min || !max) + return null; + return `${min}-${max}`; + }; + o.write = function(section_id, value) { + const ports = value?.split('-') || null; + if (ports){ + uci.set('network', section_id, 'srcportmin', ports[0]); + uci.set('network', section_id, 'srcportmax', ports[1]); + } + }; + + o = s.taboption('advanced', form.Value, 'ageing', _('Ageing'), + _('FDB entry lifetime') + '
' + + _('Units: seconds')); + o.optional = true; + o.placeholder = 300; + o.datatype = 'uinteger'; + + o = s.taboption('advanced', form.Value, 'mtu', _('MTU'), + _('Ensure MTU does not exceed that of parent interface') + '
' + + _('The VXLAN header adds 50 bytes of IPv4 encapsulation overhead, 74 bytes for IPv6.')); + o.optional = true; + o.placeholder = 1280; + o.datatype = 'min(128)'; + + o = s.taboption('advanced', form.Value, 'maxaddress', _('Max FDB size'), + _('Maximum number of FDB entries')); + o.optional = true; + o.placeholder = 1000; + o.datatype = 'min(1)'; + + o = s.taboption('general', form.Flag, 'learning', _('Learning'), + _('Automatic mac learning using multicast; inserts unknown source link layer addresses and IP addresses into the VXLAN device %s' + .format('%s'.format(_('Forwarding DataBase'), _('FDB'))))); + o.optional = true; + o.default = '1'; + o.rmempty = false; + + o = s.taboption('advanced', form.Flag, 'rsc', _('Route short-circuit (RSC)'), + _('If destination MAC refers to router, replace it with destination MAC address')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'proxy', _('ARP proxy'), + _('Reply on Neighbour request when mapping found in VXLAN FDB')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'l2miss', _('l2miss: Layer 2 miss'), + _('On a l2miss, send ARP') + '
' + + _('Emits netlink LLADDR miss notifications') + '
' + + _('Expect netlink reply to add MAC address into VXLAN FDB')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'l3miss', _('l3miss: Layer 3 miss'), + _('On a l3miss, send ARP for IP -> mac resolution') + '
' + + _('Emits netlink IP ADDR miss notifications') + '
' + + _('Expect netlink reply to add destination IP address into Neighbour table')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'gbp', _('GBP'), + _('Group Based Policy (VXLAN-GBP) extension')); + o.optional = true; + + o = s.taboption('general', form.Value, 'vid', _('VXLAN network identifier'), + _('VNI') + ': ' + _('ID used to uniquely identify the VXLAN')); o.optional = true; o.datatype = 'range(1, 16777216)'; @@ -66,7 +141,27 @@ return network.registerProtocol('vxlan', { o = s.taboption('advanced', form.Value, 'tos', _('Override TOS'), _('Specify a TOS (Type of Service).')); o.optional = true; - o.datatype = 'range(0, 255)'; + // values 0xA0-0xE0 are valid, but don't work + o.value('2', '2: 0x02'); + o.value('4', '4: 0x04'); + for (var i = 32; i <= 152; i += 8) { + o.value(i, i + ': 0x' + i.toString(16).padStart(2, '0')); + } + o.value('inherit'); + o.validate = function(section_id, value) { + if (!value || /^inherit$/.test(value)) return true; + const v = parseInt(value) + if (v % 4 != 0) return false + if (v <= 152 && v > 0) + return true; + return false; + }; + o.write = function(section_id, value) { + return uci.set('network', section_id, 'tos', parseInt(value).toString(16).padStart(2, '0')); + }; + o.load = function(section_id) { + return parseInt(uci.get('network', section_id, 'tos'), 16).toString(); + }; o = s.taboption('advanced', form.Flag, 'rxcsum', _('Enable rx checksum')); o.optional = true; @@ -76,5 +171,84 @@ return network.registerProtocol('vxlan', { o.optional = true; o.default = o.enabled; + try { + s.tab('peers', _('Additional Peers'), _('Further information about VXLAN interfaces and peers %s.').format('here')); + } + catch(e) {} + + o = s.taboption('peers', form.SectionValue, '_peers', form.GridSection, 'vxlan_peers'); + o.depends('proto', 'vxlan'); + + var ss = o.subsection; + ss.anonymous = true; + ss.addremove = true; + ss.addbtntitle = _('Add peer'); + ss.nodescriptions = true; + ss.modaltitle = _('Edit peer'); + ss.filter = function(section_id) { + let peer = uci.get('network', section_id); + return (peer.vxlan === s.section ? true: false); + }; + + o = ss.option(form.Value, 'description', _('Description'), _('Optional. Description of peer.')); + o.placeholder = _('My Peer'); + o.datatype = 'string'; + o.optional = true; + + o = ss.option(form.Value, 'lladr', _('Layer 2 Address'), + _('L2 (MAC) address of peer. Uses source-address learning when %s is specified') + .format('00:00:00:00:00:00')); + o.editable = true; + o.datatype = 'macaddr'; + o.optional = true; + o.modalonly = true; + o.value('00:00:00:00:00:00'); + + o = ss.option(form.Value, 'dst', _('Peer IP'), _('IP address of the remote VXLAN tunnel endpoint where the MAC address (Layer 2 Address) resides or a multicast address for a group of peers.') + + '
' + _('For multicast, an outgoing interface (%s) needs to be specified').format('via')); + o.editable = true; + o.datatype = 'ipaddr'; + o.placeholder = '239.1.1.1' + o.rmempty = false; + o.write = function(section_id, value) { + // Note: a heavier alternative is to chain vxlan parameter creation to handleAdd + // but because ipaddr is also mandatory, use this write function + uci.set('network', section_id, 'vxlan', s.section); + return this.super('write', [ section_id, value ]); + }; + + o = ss.option(form.Value, 'port', _('Port'), _('UDP destination port number to use to connect to the remote VXLAN tunnel endpoint')); + o.editable = true; + o.datatype = 'port'; + o.placeholder = 4789; + o.optional = true; + + o = ss.option(widgets.NetworkSelect, 'via', _('Via'), _('Name of the outgoing interface to reach the remote VXLAN tunnel endpoint')); + o.editable = true; + o.exclude = s.section; + o.nocreate = true; + o.optional = true; + o.validate = function(section_id, value) { + const dst = this.section.getOption('dst').formvalue(section_id).toLowerCase(); + const ipv4MulticastRegex = /^(22[4-9]|23[0-9])(\.\d{1,3}){3}$/; + const ipv6MulticastRegex = /^ff[0-9a-fA-F]{0,2}:.*/; + let isMulticastIP = ipv4MulticastRegex.test(dst) || ipv6MulticastRegex.test(dst); + + if (!value && isMulticastIP) { + return _('Via shall be specified when %s is a multicast address'.format(_('Peer IP'))); + } + return true; + }; + + o = ss.option(form.Value, 'vni', _('VNI'), _('the VXLAN Network Identifier (or VXLAN Segment ID) to use to connect to the remote VXLAN tunnel endpoint')); + o.editable = true; + o.datatype = 'range(1, 16777216)'; + o.optional = true; + + o = ss.option(form.Value, 'src_vni', _('Source VNI'), _('the source VNI Network Identifier (or VXLAN Segment ID) this entry belongs to. Used only when the VXLAN device is in external or collect metadata mode ')); + o.editable = true; + o.datatype = 'range(1, 16777216)'; + o.optional = true; + } }); diff --git a/protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js b/protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js index 04e702897b2a..9f54d4c1b823 100644 --- a/protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js +++ b/protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js @@ -2,6 +2,7 @@ 'require form'; 'require network'; 'require tools.widgets as widgets'; +'require uci'; network.registerPatternVirtual(/^vxlan-.+$/); @@ -37,11 +38,14 @@ return network.registerProtocol('vxlan6', { renderFormOptions: function(s) { var o; - o = s.taboption('general', form.Value, 'peer6addr', _('Remote IPv6 address'), _('The IPv6 address or the fully-qualified domain name of the remote end.')); + o = s.taboption('general', form.Value, 'peer6addr', _('Remote IPv6 address'), _('The IPv6 address or the fully-qualified domain name of the remote end.') + '
' + + _('Alternatively, a multicast address to reach a group of peers.') + '
' + + _('Remote VTEP')); o.optional = false; o.datatype = 'or(hostname,ip6addr("nomask"))'; - o = s.taboption('general', form.Value, 'ip6addr', _('Local IPv6 address'), _('The local IPv6 address over which the tunnel is created (optional).')); + o = s.taboption('general', form.Value, 'ip6addr', _('Local IPv6 address'), _('The local IPv6 address over which the tunnel is created (optional).') + '
' + + _('Local VTEP')); o.optional = true; o.datatype = 'ip6addr("nomask")'; @@ -50,7 +54,78 @@ return network.registerProtocol('vxlan6', { o.placeholder = 4789; o.datatype = 'port'; - o = s.taboption('general', form.Value, 'vid', _('VXLAN network identifier'), _('ID used to uniquely identify the VXLAN')); + o = s.taboption('general', form.Value, 'srcport', _('Source port range')); + o.optional = true; + o.placeholder = '5000-6000'; + o.datatype = 'portrange'; + o.load = function(section_id) { + const min = uci.get('network', section_id, 'srcportmin'); + const max = uci.get('network', section_id, 'srcportmax'); + if (!min || !max) + return null; + return `${min}-${max}`; + }; + o.write = function(section_id, value) { + const ports = value?.split('-') || null; + if (ports){ + uci.set('network', section_id, 'srcportmin', ports[0]); + uci.set('network', section_id, 'srcportmax', ports[1]); + } + }; + + o = s.taboption('advanced', form.Value, 'ageing', _('Ageing'), + _('FDB entry lifetime') + '
' + + _('Units: seconds')); + o.optional = true; + o.placeholder = 300; + o.datatype = 'uinteger'; + + o = s.taboption('advanced', form.Value, 'mtu', _('MTU'), + _('Ensure MTU does not exceed that of parent interface') + '
' + + _('The VXLAN header adds 50 bytes of IPv4 encapsulation overhead, 74 bytes for IPv6.')); + o.optional = true; + o.placeholder = 1280; + o.datatype = 'min(128)'; + + o = s.taboption('advanced', form.Value, 'maxaddress', _('Max FDB size'), + _('Maximum number of FDB entries')); + o.optional = true; + o.placeholder = 1000; + o.datatype = 'min(1)'; + + o = s.taboption('general', form.Flag, 'learning', _('Learning'), + _('Automatic mac learning using multicast; inserts unknown source link layer addresses and IP addresses into the VXLAN device %s' + .format('%s'.format(_('Forwarding DataBase'), _('FDB'))))); + o.optional = true; + o.default = '1'; + o.rmempty = false; + + o = s.taboption('advanced', form.Flag, 'rsc', _('Route short-circuit (RSC)'), + _('If destination MAC refers to router, replace it with destination MAC address')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'proxy', _('ARP proxy'), + _('Reply on Neighbour request when mapping found in VXLAN FDB')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'l2miss', _('l2miss: Layer 2 miss'), + _('On a l2miss, send ARP') + '
' + + _('Emits netlink LLADDR miss notifications') + '
' + + _('Expect netlink reply to add MAC address into VXLAN FDB')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'l3miss', _('l3miss: Layer 3 miss'), + _('On a l3miss, send ARP for IP -> mac resolution') + '
' + + _('Emits netlink IP ADDR miss notifications') + '
' + + _('Expect netlink reply to add destination IP address into Neighbour table')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'gbp', _('GBP'), + _('Group Based Policy (VXLAN-GBP) extension')); + o.optional = true; + + o = s.taboption('general', form.Value, 'vid', _('VXLAN network identifier'), + _('VNI') + ': ' + _('ID used to uniquely identify the VXLAN')); o.optional = true; o.datatype = 'range(1, 16777216)'; @@ -66,7 +141,27 @@ return network.registerProtocol('vxlan6', { o = s.taboption('advanced', form.Value, 'tos', _('Override TOS'), _('Specify a TOS (Type of Service).')); o.optional = true; - o.datatype = 'range(0, 255)'; + // values 0xA0-0xE0 are valid, but don't work + o.value('2', '2: 0x02'); + o.value('4', '4: 0x04'); + for (var i = 32; i <= 152; i += 8) { + o.value(i, i + ': 0x' + i.toString(16).padStart(2, '0')); + } + o.value('inherit'); + o.validate = function(section_id, value) { + if (!value || /^inherit$/.test(value)) return true; + const v = parseInt(value) + if (v % 4 != 0) return false + if (v <= 152 && v > 0) + return true; + return false; + }; + o.write = function(section_id, value) { + return uci.set('network', section_id, 'tos', parseInt(value).toString(16).padStart(2, '0')); + }; + o.load = function(section_id) { + return parseInt(uci.get('network', section_id, 'tos'), 16).toString(); + }; o = s.taboption('advanced', form.Flag, 'rxcsum', _('Enable rx checksum')); o.optional = true; @@ -76,5 +171,84 @@ return network.registerProtocol('vxlan6', { o.optional = true; o.default = o.enabled; + try { + s.tab('peers', _('Additional Peers'), _('Further information about VXLAN interfaces and peers %s.').format('here')); + } + catch(e) {} + + o = s.taboption('peers', form.SectionValue, '_peers', form.GridSection, 'vxlan_peers'); + o.depends('proto', 'vxlan'); + + var ss = o.subsection; + ss.anonymous = true; + ss.addremove = true; + ss.addbtntitle = _('Add peer'); + ss.nodescriptions = true; + ss.modaltitle = _('Edit peer'); + ss.filter = function(section_id) { + let peer = uci.get('network', section_id); + return (peer.vxlan === s.section ? true: false); + }; + + o = ss.option(form.Value, 'description', _('Description'), _('Optional. Description of peer.')); + o.placeholder = _('My Peer'); + o.datatype = 'string'; + o.optional = true; + + o = ss.option(form.Value, 'lladr', _('Layer 2 Address'), + _('L2 (MAC) address of peer. Uses source-address learning when %s is specified') + .format('00:00:00:00:00:00')); + o.editable = true; + o.datatype = 'macaddr'; + o.optional = true; + o.modalonly = true; + o.value('00:00:00:00:00:00'); + + o = ss.option(form.Value, 'dst', _('Peer IP'), _('IP address of the remote VXLAN tunnel endpoint where the MAC address (Layer 2 Address) resides or a multicast address for a group of peers.') + + '
' + _('For multicast, an outgoing interface (%s) needs to be specified').format('via')); + o.editable = true; + o.datatype = 'ipaddr'; + o.placeholder = '239.1.1.1' + o.rmempty = false; + o.write = function(section_id, value) { + // Note: a heavier alternative is to chain vxlan parameter creation to handleAdd + // but because ipaddr is also mandatory, use this write function + uci.set('network', section_id, 'vxlan', s.section); + return this.super('write', [ section_id, value ]); + }; + + o = ss.option(form.Value, 'port', _('Port'), _('UDP destination port number to use to connect to the remote VXLAN tunnel endpoint')); + o.editable = true; + o.datatype = 'port'; + o.placeholder = 4789; + o.optional = true; + + o = ss.option(widgets.NetworkSelect, 'via', _('Via'), _('Name of the outgoing interface to reach the remote VXLAN tunnel endpoint')); + o.editable = true; + o.exclude = s.section; + o.nocreate = true; + o.optional = true; + o.validate = function(section_id, value) { + const dst = this.section.getOption('dst').formvalue(section_id).toLowerCase(); + const ipv4MulticastRegex = /^(22[4-9]|23[0-9])(\.\d{1,3}){3}$/; + const ipv6MulticastRegex = /^ff[0-9a-fA-F]{0,2}:.*/; + let isMulticastIP = ipv4MulticastRegex.test(dst) || ipv6MulticastRegex.test(dst); + + if (!value && isMulticastIP) { + return _('Via shall be specified when %s is a multicast address'.format(_('Peer IP'))); + } + return true; + }; + + o = ss.option(form.Value, 'vni', _('VNI'), _('the VXLAN Network Identifier (or VXLAN Segment ID) to use to connect to the remote VXLAN tunnel endpoint')); + o.editable = true; + o.datatype = 'range(1, 16777216)'; + o.optional = true; + + o = ss.option(form.Value, 'src_vni', _('Source VNI'), _('the source VNI Network Identifier (or VXLAN Segment ID) this entry belongs to. Used only when the VXLAN device is in external or collect metadata mode ')); + o.editable = true; + o.datatype = 'range(1, 16777216)'; + o.optional = true; + } });