summaryrefslogtreecommitdiff
path: root/src/conf_mode/interfaces_vxlan.py
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2023-12-30 23:25:20 +0100
committerChristian Breunig <christian@breunig.cc>2023-12-31 23:49:48 +0100
commit4ef110fd2c501b718344c72d495ad7e16d2bd465 (patch)
treee98bf08f93c029ec4431a3b6ca078e7562e0cc58 /src/conf_mode/interfaces_vxlan.py
parent2286b8600da6c631b17e1d5b9b341843e50f9abf (diff)
downloadvyos-1x-4ef110fd2c501b718344c72d495ad7e16d2bd465.tar.gz
vyos-1x-4ef110fd2c501b718344c72d495ad7e16d2bd465.zip
T5474: establish common file name pattern for XML conf mode commands
We will use _ as CLI level divider. The XML definition filename and also the Python helper should match the CLI node. Example: set interfaces ethernet -> interfaces_ethernet.xml.in set interfaces bond -> interfaces_bond.xml.in set service dhcp-server -> service_dhcp-server-xml.in
Diffstat (limited to 'src/conf_mode/interfaces_vxlan.py')
-rwxr-xr-xsrc/conf_mode/interfaces_vxlan.py236
1 files changed, 236 insertions, 0 deletions
diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py
new file mode 100755
index 000000000..4251e611b
--- /dev/null
+++ b/src/conf_mode/interfaces_vxlan.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from netifaces import interfaces
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+from vyos.configdict import is_node_changed
+from vyos.configdict import node_changed
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import Interface
+from vyos.ifconfig import VXLANIf
+from vyos.template import is_ipv6
+from vyos.utils.dict import dict_search
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least
+ the interface name will be added or a deleted flag
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['interfaces', 'vxlan']
+ ifname, vxlan = get_interface_dict(conf, base)
+
+ # VXLAN interfaces are picky and require recreation if certain parameters
+ # change. But a VXLAN interface should - of course - not be re-created if
+ # it's description or IP address is adjusted. Feels somehow logic doesn't it?
+ for cli_option in ['parameters', 'gpe', 'group', 'port', 'remote',
+ 'source-address', 'source-interface', 'vni']:
+ if is_node_changed(conf, base + [ifname, cli_option]):
+ 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': {}})
+ 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
+ conf.set_level(base)
+ vxlan['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # This if-clause is just to be sure - it will always evaluate to true
+ ifname = vxlan['ifname']
+ if ifname in vxlan['other_tunnels']:
+ del vxlan['other_tunnels'][ifname]
+ if len(vxlan['other_tunnels']) == 0:
+ del vxlan['other_tunnels']
+
+ return vxlan
+
+def verify(vxlan):
+ if 'deleted' in vxlan:
+ verify_bridge_delete(vxlan)
+ return None
+
+ if int(vxlan['mtu']) < 1500:
+ Warning('RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
+
+ if 'group' in vxlan:
+ if 'source_interface' not in vxlan:
+ raise ConfigError('Multicast VXLAN requires an underlaying interface')
+ verify_source_interface(vxlan)
+
+ if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan):
+ raise ConfigError('Group, remote, source-address or source-interface must be configured')
+
+ 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) != None:
+ if 'vni' in vxlan:
+ raise ConfigError('Can not specify both "external" and "VNI"!')
+
+ if 'other_tunnels' in vxlan:
+ # 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" '\
+ f'CLI option is used.')
+
+ if 'source_interface' in vxlan:
+ # VXLAN adds at least an overhead of 50 byte - we need to check the
+ # underlaying device if our VXLAN package is not going to be fragmented!
+ vxlan_overhead = 50
+ if 'source_address' in vxlan and is_ipv6(vxlan['source_address']):
+ # IPv6 adds an extra 20 bytes overhead because the IPv6 header is 20
+ # bytes larger than the IPv4 header - assuming no extra options are
+ # in use.
+ vxlan_overhead += 20
+
+ # If source_address is not used - check IPv6 'remote' list
+ elif 'remote' in vxlan:
+ if any(is_ipv6(a) for a in vxlan['remote']):
+ vxlan_overhead += 20
+
+ lower_mtu = Interface(vxlan['source_interface']).get_mtu()
+ if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead):
+ raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
+ f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
+
+ # Check for mixed IPv4 and IPv6 addresses
+ protocol = None
+ if 'source_address' in vxlan:
+ if is_ipv6(vxlan['source_address']):
+ protocol = 'ipv6'
+ else:
+ protocol = 'ipv4'
+
+ if 'remote' in vxlan:
+ error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay'
+ for remote in vxlan['remote']:
+ if is_ipv6(remote):
+ if protocol == 'ipv4':
+ raise ConfigError(error_msg)
+ protocol = 'ipv6'
+ else:
+ if protocol == 'ipv6':
+ raise ConfigError(error_msg)
+ protocol = 'ipv4'
+
+ if 'vlan_to_vni' in vxlan:
+ if 'is_bridge_member' not in vxlan:
+ raise ConfigError('VLAN to VNI mapping requires that VXLAN interface '\
+ 'is member of a bridge interface!')
+
+ vnis_used = []
+ for vif, vif_config in vxlan['vlan_to_vni'].items():
+ if 'vni' not in vif_config:
+ raise ConfigError(f'Must define VNI for VLAN "{vif}"!')
+ vni = vif_config['vni']
+ if vni in vnis_used:
+ raise ConfigError(f'VNI "{vni}" is already assigned to a different VLAN!')
+ vnis_used.append(vni)
+
+ 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!')
+
+ verify_mtu_ipv6(vxlan)
+ verify_address(vxlan)
+ verify_bond_bridge_member(vxlan)
+ verify_mirror_redirect(vxlan)
+
+ # We use a defaultValue for port, thus it's always safe to use
+ if vxlan['port'] == '8472':
+ Warning('Starting from VyOS 1.4, the default port for VXLAN '\
+ 'has been changed to 4789. This matches the IANA assigned '\
+ 'standard port number!')
+
+ return None
+
+def generate(vxlan):
+ return None
+
+def apply(vxlan):
+ # Check if the VXLAN interface already exists
+ if 'rebuild_required' in vxlan or 'delete' in vxlan:
+ if vxlan['ifname'] in interfaces():
+ v = VXLANIf(vxlan['ifname'])
+ # VXLAN is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ v.remove()
+
+ if 'deleted' not in vxlan:
+ # Finally create the new interface
+ v = VXLANIf(**vxlan)
+ v.update(vxlan)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)