Skip to content

Commit

Permalink
Merge pull request #2499 from c-po/t5753-vxlan-vnifilter
Browse files Browse the repository at this point in the history
vxlan: T5753: add support for VNI filtering
  • Loading branch information
c-po authored Nov 22, 2023
2 parents af08c30 + 35f6033 commit 00a28fe
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 12 deletions.
6 changes: 6 additions & 0 deletions interface-definitions/interfaces-vxlan.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@
<valueless/>
</properties>
</leafNode>
<leafNode name="vni-filter">
<properties>
<help>Enable VNI filter support</help>
<valueless/>
</properties>
</leafNode>
</children>
</node>
#include <include/port-number.xml.i>
Expand Down
6 changes: 6 additions & 0 deletions op-mode-definitions/show-bridge.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
</leafNode>
</children>
</node>
<leafNode name="vni">
<properties>
<help>Virtual Network Identifier</help>
</properties>
<command>${vyos_op_scripts_dir}/bridge.py show_vni</command>
</leafNode>
</children>
</node>
<leafNode name="bridge">
Expand Down
21 changes: 15 additions & 6 deletions python/vyos/ifconfig/vxlan.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from vyos.utils.dict import dict_search
from vyos.utils.network import get_interface_config
from vyos.utils.network import get_vxlan_vlan_tunnels
from vyos.utils.network import get_vxlan_vni_filter

@Interface.register
class VXLANIf(Interface):
Expand Down Expand Up @@ -79,6 +80,7 @@ def _create(self):
'parameters.ip.ttl' : 'ttl',
'parameters.ipv6.flowlabel' : 'flowlabel',
'parameters.nolearning' : 'nolearning',
'parameters.vni_filter' : 'vnifilter',
'remote' : 'remote',
'source_address' : 'local',
'source_interface' : 'dev',
Expand Down Expand Up @@ -138,10 +140,14 @@ def set_vlan_vni_mapping(self, state):
if not isinstance(state, bool):
raise ValueError('Value out of range')

cur_vlan_ids = []
if 'vlan_to_vni_removed' in self.config:
cur_vlan_ids = self.config['vlan_to_vni_removed']
for vlan in cur_vlan_ids:
cur_vni_filter = get_vxlan_vni_filter(self.ifname)
for vlan, vlan_config in self.config['vlan_to_vni_removed'].items():
# If VNI filtering is enabled, remove matching VNI filter
if dict_search('parameters.vni_filter', self.config) != None:
vni = vlan_config['vni']
if vni in cur_vni_filter:
self._cmd(f'bridge vni delete dev {self.ifname} vni {vni}')
self._cmd(f'bridge vlan del dev {self.ifname} vid {vlan}')

# Determine current OS Kernel vlan_tunnel setting - only adjust when needed
Expand All @@ -151,10 +157,9 @@ def set_vlan_vni_mapping(self, state):
if cur_state != new_state:
self.set_interface('vlan_tunnel', new_state)

# Determine current OS Kernel configured VLANs
os_configured_vlan_ids = get_vxlan_vlan_tunnels(self.ifname)

if 'vlan_to_vni' in self.config:
# Determine current OS Kernel configured VLANs
os_configured_vlan_ids = get_vxlan_vlan_tunnels(self.ifname)
add_vlan = list_diff(list(self.config['vlan_to_vni'].keys()), os_configured_vlan_ids)

for vlan, vlan_config in self.config['vlan_to_vni'].items():
Expand All @@ -168,6 +173,10 @@ def set_vlan_vni_mapping(self, state):
self._cmd(f'bridge vlan add dev {self.ifname} vid {vlan}')
self._cmd(f'bridge vlan add dev {self.ifname} vid {vlan} tunnel_info id {vni}')

# If VNI filtering is enabled, install matching VNI filter
if dict_search('parameters.vni_filter', self.config) != None:
self._cmd(f'bridge vni add dev {self.ifname} vni {vni}')

def update(self, config):
""" General helper function which works on a dictionary retrived by
get_config_dict(). It's main intention is to consolidate the scattered
Expand Down
33 changes: 33 additions & 0 deletions python/vyos/utils/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,3 +483,36 @@ def get_vxlan_vlan_tunnels(interface: str) -> list:
os_configured_vlan_ids.append(str(vlanStart))

return os_configured_vlan_ids

def get_vxlan_vni_filter(interface: str) -> list:
""" Return a list of strings with VNIs configured in the Kernel"""
from json import loads
from vyos.utils.process import cmd

if not interface.startswith('vxlan'):
raise ValueError('Only applicable for VXLAN interfaces!')

# Determine current OS Kernel configured VNI filters in VXLAN interface
#
# $ bridge -j vni show dev vxlan1
# [{"ifname":"vxlan1","vnis":[{"vni":100},{"vni":200},{"vni":300,"vniEnd":399}]}]
#
# Example output: ['10010', '10020', '10021', '10022']
os_configured_vnis = []
tmp = loads(cmd(f'bridge --json vni show dev {interface}'))
if tmp:
for tunnel in tmp[0].get('vnis', {}):
vniStart = tunnel['vni']
if 'vniEnd' in tunnel:
vniEnd = tunnel['vniEnd']
# Build a real list for user VNIs
vni_list = list(range(vniStart, vniEnd +1))
# Convert list of integers to list or strings
os_configured_vnis.extend(map(str, vni_list))
# Proceed with next tunnel - this one is complete
continue

# Add single tunel id - not part of a range
os_configured_vnis.append(str(vniStart))

return os_configured_vnis
90 changes: 90 additions & 0 deletions smoketest/scripts/cli/test_interfaces_vxlan.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from vyos.utils.network import get_interface_config
from vyos.utils.network import interface_exists
from vyos.utils.network import get_vxlan_vlan_tunnels
from vyos.utils.network import get_vxlan_vni_filter
from vyos.template import is_ipv6
from base_interfaces_test import BasicInterfaceTest

Expand Down Expand Up @@ -151,6 +152,7 @@ def test_vxlan_vlan_vni_mapping(self):
}

self.cli_set(self._base_path + [interface, 'parameters', 'external'])
self.cli_set(self._base_path + [interface, 'parameters', 'vni-filter'])
self.cli_set(self._base_path + [interface, 'source-interface', source_interface])

for vlan, vni in vlan_to_vni.items():
Expand Down Expand Up @@ -222,5 +224,93 @@ def test_vxlan_neighbor_suppress(self):

self.cli_delete(['interfaces', 'bridge', bridge])

def test_vxlan_vni_filter(self):
interfaces = ['vxlan987', 'vxlan986', 'vxlan985']
source_address = '192.0.2.77'

for interface in interfaces:
self.cli_set(self._base_path + [interface, 'parameters', 'external'])
self.cli_set(self._base_path + [interface, 'source-address', source_address])

# This must fail as there can only be one "external" VXLAN device unless "vni-filter" is defined
with self.assertRaises(ConfigSessionError):
self.cli_commit()

# Enable "vni-filter" on the first VXLAN interface
self.cli_set(self._base_path + [interfaces[0], 'parameters', 'vni-filter'])

# This must fail as if it's enabled on one VXLAN interface, it must be enabled on all
# VXLAN interfaces
with self.assertRaises(ConfigSessionError):
self.cli_commit()
for interface in interfaces:
self.cli_set(self._base_path + [interface, 'parameters', 'vni-filter'])

# commit configuration
self.cli_commit()

for interface in interfaces:
self.assertTrue(interface_exists(interface))

tmp = get_interface_config(interface)
self.assertTrue(tmp['linkinfo']['info_data']['vnifilter'])

def test_vxlan_vni_filter_add_remove(self):
interface = 'vxlan987'
source_address = '192.0.2.66'
bridge = 'br0'

self.cli_set(self._base_path + [interface, 'parameters', 'external'])
self.cli_set(self._base_path + [interface, 'source-address', source_address])
self.cli_set(self._base_path + [interface, 'parameters', 'vni-filter'])

# commit configuration
self.cli_commit()

# Check if VXLAN interface got created
self.assertTrue(interface_exists(interface))

# VNI filter configured?
tmp = get_interface_config(interface)
self.assertTrue(tmp['linkinfo']['info_data']['vnifilter'])

# Now create some VLAN mappings and VNI filter
vlan_to_vni = {
'50': '10050',
'51': '10051',
'52': '10052',
'53': '10053',
'54': '10054',
'60': '10060',
'69': '10069',
}
for vlan, vni in vlan_to_vni.items():
self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
# we need a bridge ...
self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', interface])
# commit configuration
self.cli_commit()

# All VNIs configured?
tmp = get_vxlan_vni_filter(interface)
self.assertListEqual(list(vlan_to_vni.values()), tmp)

#
# Delete a VLAN mappings and check if all VNIs are properly set up
#
vlan_to_vni.popitem()
self.cli_delete(self._base_path + [interface, 'vlan-to-vni'])
for vlan, vni in vlan_to_vni.items():
self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])

# commit configuration
self.cli_commit()

# All VNIs configured?
tmp = get_vxlan_vni_filter(interface)
self.assertListEqual(list(vlan_to_vni.values()), tmp)

self.cli_delete(['interfaces', 'bridge', bridge])

if __name__ == '__main__':
unittest.main(verbosity=2)
35 changes: 29 additions & 6 deletions src/conf_mode/interfaces-vxlan.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,14 @@ def get_config(config=None):
vxlan.update({'rebuild_required': {}})
break

# When dealing with VNI filtering we need to know what VNI was actually removed,
# so build up a dict matching the vlan_to_vni structure but with removed values.
tmp = node_changed(conf, base + [ifname, 'vlan-to-vni'], recursive=True)
if tmp: vxlan.update({'vlan_to_vni_removed': tmp})
if tmp:
vxlan.update({'vlan_to_vni_removed': {}})
for vlan in tmp:
vni = leaf_node_changed(conf, base + [ifname, 'vlan-to-vni', vlan, 'vni'])
vxlan['vlan_to_vni_removed'].update({vlan : {'vni' : vni[0]}})

# We need to verify that no other VXLAN tunnel is configured when external
# mode is in use - Linux Kernel limitation
Expand Down Expand Up @@ -98,14 +104,31 @@ def verify(vxlan):
if 'vni' not in vxlan and dict_search('parameters.external', vxlan) == None:
raise ConfigError('Must either configure VXLAN "vni" or use "external" CLI option!')

if dict_search('parameters.external', vxlan):
if dict_search('parameters.external', vxlan) != None:
if 'vni' in vxlan:
raise ConfigError('Can not specify both "external" and "VNI"!')

if 'other_tunnels' in vxlan:
other_tunnels = ', '.join(vxlan['other_tunnels'])
raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\
f'CLI option is used. Additional tunnels: {other_tunnels}')
# When multiple VXLAN interfaces are defined and "external" is used,
# all VXLAN interfaces need to have vni-filter enabled!
# See Linux Kernel commit f9c4bb0b245cee35ef66f75bf409c9573d934cf9
other_vni_filter = False
for tunnel, tunnel_config in vxlan['other_tunnels'].items():
if dict_search('parameters.vni_filter', tunnel_config) != None:
other_vni_filter = True
break
# eqivalent of the C foo ? 'a' : 'b' statement
vni_filter = True and (dict_search('parameters.vni_filter', vxlan) != None) or False
# If either one is enabled, so must be the other. Both can be off and both can be on
if (vni_filter and not other_vni_filter) or (not vni_filter and other_vni_filter):
raise ConfigError(f'Using multiple VXLAN interfaces with "external" '\
'requires all VXLAN interfaces to have "vni-filter" configured!')

if not vni_filter and not other_vni_filter:
other_tunnels = ', '.join(vxlan['other_tunnels'])
raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\
f'CLI option is used and "vni-filter" is unset. '\
f'Additional tunnels: {other_tunnels}')

if 'gpe' in vxlan and 'external' not in vxlan:
raise ConfigError(f'VXLAN-GPE is only supported when "external" '\
Expand Down Expand Up @@ -165,7 +188,7 @@ def verify(vxlan):
raise ConfigError(f'VNI "{vni}" is already assigned to a different VLAN!')
vnis_used.append(vni)

if dict_search('parameters.neighbor_suppress', vxlan):
if dict_search('parameters.neighbor_suppress', vxlan) != None:
if 'is_bridge_member' not in vxlan:
raise ConfigError('Neighbor suppression requires that VXLAN interface '\
'is member of a bridge interface!')
Expand Down
29 changes: 29 additions & 0 deletions src/op_mode/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ def _get_raw_data_vlan(tunnel:bool=False):
data_dict = json.loads(json_data)
return data_dict

def _get_raw_data_vni() -> dict:
"""
:returns dict
"""
json_data = cmd(f'bridge --json vni show')
data_dict = json.loads(json_data)
return data_dict

def _get_raw_data_fdb(bridge):
"""Get MAC-address for the bridge brX
Expand Down Expand Up @@ -165,6 +172,22 @@ def _get_formatted_output_vlan_tunnel(data):
output = tabulate(data_entries, headers)
return output

def _get_formatted_output_vni(data):
data_entries = []
for entry in data:
interface = entry.get('ifname')
vlans = entry.get('vnis')
for vlan_entry in vlans:
vlan = vlan_entry.get('vni')
if vlan_entry.get('vniEnd'):
vlan_end = vlan_entry.get('vniEnd')
vlan = f'{vlan}-{vlan_end}'
data_entries.append([interface, vlan])

headers = ["Interface", "VNI"]
output = tabulate(data_entries, headers)
return output

def _get_formatted_output_fdb(data):
data_entries = []
for entry in data:
Expand Down Expand Up @@ -228,6 +251,12 @@ def show_vlan(raw: bool, tunnel: typing.Optional[bool]):
else:
return _get_formatted_output_vlan(bridge_vlan)

def show_vni(raw: bool):
bridge_vni = _get_raw_data_vni()
if raw:
return bridge_vni
else:
return _get_formatted_output_vni(bridge_vni)

def show_fdb(raw: bool, interface: str):
fdb_data = _get_raw_data_fdb(interface)
Expand Down

0 comments on commit 00a28fe

Please sign in to comment.