#!/usr/bin/env python3 # # Copyright (C) 2019-2020 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 copy import deepcopy from netifaces import interfaces from vyos.config import Config from vyos.ifconfig import VXLANIf, Interface from vyos.validate import is_member from vyos import ConfigError from vyos import airbag airbag.enable() default_config_data = { 'address': [], 'deleted': False, 'description': '', 'disable': False, 'group': '', 'intf': '', 'ip_arp_cache_tmo': 30, 'ip_disable_arp_filter': 1, 'ip_enable_arp_accept': 0, 'ip_enable_arp_announce': 0, 'ip_enable_arp_ignore': 0, 'ip_proxy_arp': 0, 'ipv6_accept_ra': 1, 'ipv6_autoconf': 0, 'ipv6_eui64_prefix': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, 'is_bridge_member': False, 'source_address': '', 'source_interface': '', 'mtu': 1450, 'remote': '', 'remote_port': 8472, # The Linux implementation of VXLAN pre-dates # the IANA's selection of a standard destination port 'vni': '' } def get_config(): vxlan = deepcopy(default_config_data) conf = Config() # determine tagNode instance if 'VYOS_TAGNODE_VALUE' not in os.environ: raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # check if interface is member if a bridge vxlan['is_bridge_member'] = is_member(conf, vxlan['intf'], 'bridge') # Check if interface has been removed if not conf.exists('interfaces vxlan ' + vxlan['intf']): vxlan['deleted'] = True return vxlan # set new configuration level conf.set_level('interfaces vxlan ' + vxlan['intf']) # retrieve configured interface addresses if conf.exists('address'): vxlan['address'] = conf.return_values('address') # retrieve interface description if conf.exists('description'): vxlan['description'] = conf.return_value('description') # Disable this interface if conf.exists('disable'): vxlan['disable'] = True # VXLAN multicast grou if conf.exists('group'): vxlan['group'] = conf.return_value('group') # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) # ARP filter configuration if conf.exists('ip disable-arp-filter'): vxlan['ip_disable_arp_filter'] = 0 # ARP enable accept if conf.exists('ip enable-arp-accept'): vxlan['ip_enable_arp_accept'] = 1 # ARP enable announce if conf.exists('ip enable-arp-announce'): vxlan['ip_enable_arp_announce'] = 1 # ARP enable ignore if conf.exists('ip enable-arp-ignore'): vxlan['ip_enable_arp_ignore'] = 1 # Enable proxy-arp on this interface if conf.exists('ip enable-proxy-arp'): vxlan['ip_proxy_arp'] = 1 # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) if conf.exists('ipv6 address autoconf'): vxlan['ipv6_autoconf'] = 1 # Get prefixes for IPv6 addressing based on MAC address (EUI-64) if conf.exists('ipv6 address eui64'): vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') # Remove the default link-local address if set. if not ( conf.exists('ipv6 address no-default-link-local') or vxlan['is_bridge_member'] ): # add the link-local by default to make IPv6 work vxlan['ipv6_eui64_prefix'].append('fe80::/64') # Disable IPv6 forwarding on this interface if conf.exists('ipv6 disable-forwarding'): vxlan['ipv6_forwarding'] = 0 # IPv6 Duplicate Address Detection (DAD) tries if conf.exists('ipv6 dup-addr-detect-transmits'): vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, # accept_ra must be 2 if vxlan['ipv6_autoconf'] or 'dhcpv6' in vxlan['address']: vxlan['ipv6_accept_ra'] = 2 # VXLAN source address if conf.exists('source-address'): vxlan['source_address'] = conf.return_value('source-address') # VXLAN underlay interface if conf.exists('source-interface'): vxlan['source_interface'] = conf.return_value('source-interface') # Maximum Transmission Unit (MTU) if conf.exists('mtu'): vxlan['mtu'] = int(conf.return_value('mtu')) # Remote address of VXLAN tunnel if conf.exists('remote'): vxlan['remote'] = conf.return_value('remote') # Remote port of VXLAN tunnel if conf.exists('port'): vxlan['remote_port'] = int(conf.return_value('port')) # Virtual Network Identifier if conf.exists('vni'): vxlan['vni'] = conf.return_value('vni') return vxlan def verify(vxlan): if vxlan['deleted']: if vxlan['is_bridge_member']: raise ConfigError(( f'Cannot delete interface "{vxlan["intf"]}" as it is a ' f'member of bridge "{vxlan["is_bridge_member"]}"!')) return None if vxlan['mtu'] < 1500: print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') if vxlan['group']: if not vxlan['source_interface']: raise ConfigError('Multicast VXLAN requires an underlaying interface ') if not vxlan['source_interface'] in interfaces(): raise ConfigError('VXLAN source interface does not exist') if not (vxlan['group'] or vxlan['remote'] or vxlan['source_address']): raise ConfigError('Group, remote or source-address must be configured') if not vxlan['vni']: raise ConfigError('Must configure VNI for VXLAN') if vxlan['source_interface']: # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU # if our configured MTU is at least 50 bytes less underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu()) if underlay_mtu < (vxlan['mtu'] + 50): raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ 'MTU is to small ({})'.format(underlay_mtu)) if ( vxlan['is_bridge_member'] and ( vxlan['address'] or vxlan['ipv6_eui64_prefix'] or vxlan['ipv6_autoconf'] ) ): raise ConfigError(( f'Cannot assign address to interface "{vxlan["intf"]}" ' f'as it is a member of bridge "{vxlan["is_bridge_member"]}"!')) return None def generate(vxlan): return None def apply(vxlan): # Check if the VXLAN interface already exists if vxlan['intf'] in interfaces(): v = VXLANIf(vxlan['intf']) # VXLAN is super picky and the tunnel always needs to be recreated, # thus we can simply always delete it first. v.remove() if not vxlan['deleted']: # VXLAN interface needs to be created on-block # instead of passing a ton of arguments, I just use a dict # that is managed by vyos.ifconfig conf = deepcopy(VXLANIf.get_config()) # Assign VXLAN instance configuration parameters to config dict conf['vni'] = vxlan['vni'] conf['group'] = vxlan['group'] conf['src_address'] = vxlan['source_address'] conf['src_interface'] = vxlan['source_interface'] conf['remote'] = vxlan['remote'] conf['port'] = vxlan['remote_port'] # Finally create the new interface v = VXLANIf(vxlan['intf'], **conf) # update interface description used e.g. by SNMP v.set_alias(vxlan['description']) # Maximum Transfer Unit (MTU) v.set_mtu(vxlan['mtu']) # configure ARP cache timeout in milliseconds v.set_arp_cache_tmo(vxlan['ip_arp_cache_tmo']) # configure ARP filter configuration v.set_arp_filter(vxlan['ip_disable_arp_filter']) # configure ARP accept v.set_arp_accept(vxlan['ip_enable_arp_accept']) # configure ARP announce v.set_arp_announce(vxlan['ip_enable_arp_announce']) # configure ARP ignore v.set_arp_ignore(vxlan['ip_enable_arp_ignore']) # Enable proxy-arp on this interface v.set_proxy_arp(vxlan['ip_proxy_arp']) # IPv6 accept RA v.set_ipv6_accept_ra(vxlan['ipv6_accept_ra']) # IPv6 address autoconfiguration v.set_ipv6_autoconf(vxlan['ipv6_autoconf']) # IPv6 forwarding v.set_ipv6_forwarding(vxlan['ipv6_forwarding']) # IPv6 Duplicate Address Detection (DAD) tries v.set_ipv6_dad_messages(vxlan['ipv6_dup_addr_detect']) # Configure interface address(es) - no need to implicitly delete the # old addresses as they have already been removed by deleting the # interface above for addr in vxlan['address']: v.add_addr(addr) # IPv6 EUI-based addresses for addr in vxlan['ipv6_eui64_prefix']: v.add_ipv6_eui64_address(addr) # As the VXLAN interface is always disabled first when changing # parameters we will only re-enable the interface if it is not # administratively disabled if not vxlan['disable']: v.set_admin_state('up') # re-add ourselves to any bridge we might have fallen out of if vxlan['is_bridge_member']: v.add_to_bridge(vxlan['is_bridge_member']) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)