From 665d1c5bdb24aa0aef79405dc2f2962b930fb9b3 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Tue, 3 Mar 2020 20:01:56 +0100 Subject: vrf: T31: initial support for a VRF backend in XML/Python This is a work in progress to complete T31 whoever thought it was less than 1 hour of work was ..... optimistic. Only VRF vreation and show is supported right now. No interface can be bound to any one VRF. --- src/conf_mode/vrf.py | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100755 src/conf_mode/vrf.py (limited to 'src/conf_mode') diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py new file mode 100755 index 000000000..9896c7c85 --- /dev/null +++ b/src/conf_mode/vrf.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 . + +import os + +from sys import exit +from copy import deepcopy +from vyos.config import Config +from vyos import ConfigError +from vyos import vrf + + +# https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt + + +def sysctl(name, value): + os.system('sysctl -wq {}={}'.format(name, value)) + +def interfaces_with_vrf (match, effective): + matched = [] + config = Config() + section = config.get_config_dict('interfaces', effective) + for type in section: + interfaces = section[type] + for name in interfaces: + interface = interfaces[name] + if 'vrf' in interface: + v = interface.get('vrf', '') + if v == match: + matched.append(name) + return matched + +def get_config(): + command = { + 'bind':{}, + 'vrf':[], + 'int': {}, # per vrf name list of interfaces which will have it + } + + config = Config() + + old = {} + new = {} + + if config.exists_effective('vrf'): + old = deepcopy(config.get_config_dict('vrf', True)) + + if config.exists('vrf'): + new = deepcopy(config.get_config_dict('vrf', False)) + + integer = lambda _: '1' if _ else '0' + command['bind']['ipv4'] = integer('ipv4' not in new.get('disable-bind-to-all', {})) + command['bind']['ipv6'] = integer('ipv6' not in new.get('disable-bind-to-all', {})) + + old_names = old.get('name', []) + new_names = new.get('name', []) + all_names = list(set(old_names) | set(new_names)) + del_names = list(set(old_names).difference(new_names)) + mod_names = list(set(old_names).intersection(new_names)) + add_names = list(set(new_names).difference(old_names)) + + for name in all_names: + v = { + 'name': name, + 'action': 'miss', + 'table': -1, + 'check': -1, + } + + if name in new_names: + v['table'] = new.get('name', {}).get(name, {}).get('table', -1) + v['check'] = old.get('name', {}).get(name, {}).get('table', -1) + + if name in add_names: + v['action'] = 'add' + elif name in del_names: + v['action'] = 'delete' + elif name in mod_names: + if v['table'] != -1: + if v['check'] == -1: + v['action'] = 'add' + else: + v['action'] = 'modify' + + command['vrf'].append(v) + + for v in vrf.list_vrfs(): + name = v['ifname'] + command['int'][name] = interfaces_with_vrf(name,False) + + return command + + +def verify(command): + for v in command['vrf']: + action = v['action'] + name = v['name'] + if action == 'modify' and v['table'] != v['check']: + raise ConfigError(f'set vrf name {name}: modification of vrf table is not supported yet') + if action == 'delete' and name in command['int']: + interface = ', '.join(command['int'][name]) + if interface: + raise ConfigError(f'delete vrf name {name}: can not delete vrf as it is used on {interface}') + + return command + + +def generate(command): + return command + + +def apply(command): + # set the default VRF global behaviour + sysctl('net.ipv4.tcp_l3mdev_accept', command['bind']['ipv4']) + sysctl('net.ipv4.udp_l3mdev_accept', command['bind']['ipv4']) + + errors = [] + for v in command['vrf']: + name = v['name'] + action = v['action'] + table = v['table'] + + errors.append(f'could not {action} vrf {name}') + + if action == 'miss': + continue + + if action == 'delete': + if os.system(f'sudo ip link delete dev {name}'): + continue + errors.pop() + continue + + if action == 'modify': + # > uname -a + # Linux vyos 4.19.101-amd64-vyos #1 SMP Sun Feb 2 10:18:07 UTC 2020 x86_64 GNU/Linux + # > ip link add my-vrf type vrf table 100 + # > ip link set my-vrf type vrf table 200 + # RTNETLINK answers: Operation not supported + # so require to remove vrf and change all existing the interfaces + + if os.system(f'sudo ip link delete dev {name}'): + continue + action = 'add' + + if action == 'add': + commands = [ + f'sudo ip link add {name} type vrf table {table}', + f'sudo ip link set dev {name} up', + f'sudo ip -4 rule add oif {name} lookup {table}', + f'sudo ip -4 rule add iif {name} lookup {table}', + f'sudo ip -6 rule add oif {name} lookup {table}', + f'sudo ip -6 rule add iif {name} lookup {table}', + ] + + for command in commands: + if os.system(command): + errors[-1] += ' ('+command+')' + continue + errors.pop() + + if errors: + raise ConfigError(', '.join(errors)) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) -- cgit v1.2.3 From c26fb9bc15bd625b11ae1674bddf8daf168b0f76 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:12:18 +0100 Subject: vrf: T31: no need to use sudo calls in vrf.py All configuration mode scripts are already run with sudo. --- src/conf_mode/vrf.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 9896c7c85..4dddc7de9 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -152,18 +152,18 @@ def apply(command): # RTNETLINK answers: Operation not supported # so require to remove vrf and change all existing the interfaces - if os.system(f'sudo ip link delete dev {name}'): + if os.system(f'ip link delete dev {name}'): continue action = 'add' if action == 'add': commands = [ - f'sudo ip link add {name} type vrf table {table}', - f'sudo ip link set dev {name} up', - f'sudo ip -4 rule add oif {name} lookup {table}', - f'sudo ip -4 rule add iif {name} lookup {table}', - f'sudo ip -6 rule add oif {name} lookup {table}', - f'sudo ip -6 rule add iif {name} lookup {table}', + f'ip link add {name} type vrf table {table}', + f'ip link set dev {name} up', + f'ip -4 rule add oif {name} lookup {table}', + f'ip -4 rule add iif {name} lookup {table}', + f'ip -6 rule add oif {name} lookup {table}', + f'ip -6 rule add iif {name} lookup {table}', ] for command in commands: -- cgit v1.2.3 From e2a46280601aee4846776ebad08ed367aa115460 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:32:58 +0100 Subject: vrf: T31: reduce script complexity Keep it simple and stupid :) --- src/conf_mode/vrf.py | 217 ++++++++++++++++++++++----------------------------- 1 file changed, 95 insertions(+), 122 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 4dddc7de9..75b19da23 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -18,21 +18,33 @@ import os from sys import exit from copy import deepcopy +from subprocess import check_call, CalledProcessError from vyos.config import Config +from vyos.configdict import list_diff from vyos import ConfigError from vyos import vrf +default_config_data = { + 'vrf_add': [], + 'vrf_existing': [], + 'vrf_remove': [] +} -# https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt - +def _cmd(command): + """ + Run any arbitrary command on the system + """ + try: + check_call(command.split()) + except CalledProcessError as e: + pass + raise ConfigError(f'Error operationg on VRF: {e}') -def sysctl(name, value): - os.system('sysctl -wq {}={}'.format(name, value)) -def interfaces_with_vrf (match, effective): +def interfaces_with_vrf(match): matched = [] config = Config() - section = config.get_config_dict('interfaces', effective) + section = config.get_config_dict('interfaces') for type in section: interfaces = section[type] for name in interfaces: @@ -43,137 +55,98 @@ def interfaces_with_vrf (match, effective): matched.append(name) return matched -def get_config(): - command = { - 'bind':{}, - 'vrf':[], - 'int': {}, # per vrf name list of interfaces which will have it - } - - config = Config() - - old = {} - new = {} - if config.exists_effective('vrf'): - old = deepcopy(config.get_config_dict('vrf', True)) - - if config.exists('vrf'): - new = deepcopy(config.get_config_dict('vrf', False)) - - integer = lambda _: '1' if _ else '0' - command['bind']['ipv4'] = integer('ipv4' not in new.get('disable-bind-to-all', {})) - command['bind']['ipv6'] = integer('ipv6' not in new.get('disable-bind-to-all', {})) - - old_names = old.get('name', []) - new_names = new.get('name', []) - all_names = list(set(old_names) | set(new_names)) - del_names = list(set(old_names).difference(new_names)) - mod_names = list(set(old_names).intersection(new_names)) - add_names = list(set(new_names).difference(old_names)) - - for name in all_names: - v = { - 'name': name, - 'action': 'miss', - 'table': -1, - 'check': -1, +def get_config(): + conf = Config() + vrf_config = deepcopy(default_config_data) + + cfg_base = ['vrf'] + if not conf.exists(cfg_base): + # get all currently effetive VRFs and mark them for deletion + vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) + return vrf_config + + # Determine vrf interfaces (currently effective) - to determine which + # vrf interface is no longer present and needs to be removed + eff_vrf = conf.list_effective_nodes(cfg_base + ['name']) + act_vrf = conf.list_nodes(cfg_base + ['name']) + vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) + + # read in individual VRF definition and build up + # configuration + for name in conf.list_nodes(cfg_base + ['name']): + vrf_inst = { + 'description' : '\0', + 'members': [], + 'name' : name, + 'table' : '', + 'table_mod': False } + conf.set_level(cfg_base + ['name', name]) + + if conf.exists(['table']): + # VRF table can't be changed on demand, thus we need to read in the + # current and the effective routing table number + act_table = conf.return_value(['table']) + eff_table = conf.return_effective_value(['table']) + vrf_inst['table'] = act_table + if eff_table and eff_table != act_table: + vrf_inst['table_mod'] = True - if name in new_names: - v['table'] = new.get('name', {}).get(name, {}).get('table', -1) - v['check'] = old.get('name', {}).get(name, {}).get('table', -1) + if conf.exists(['description']): + vrf_inst['description'] = conf.return_value(['description']) - if name in add_names: - v['action'] = 'add' - elif name in del_names: - v['action'] = 'delete' - elif name in mod_names: - if v['table'] != -1: - if v['check'] == -1: - v['action'] = 'add' - else: - v['action'] = 'modify' + # find member interfaces of this particulat VRF + vrf_inst['members'] = interfaces_with_vrf(name) - command['vrf'].append(v) + # append individual VRF configuration to global configuration list + vrf_config['vrf_add'].append(vrf_inst) - for v in vrf.list_vrfs(): - name = v['ifname'] - command['int'][name] = interfaces_with_vrf(name,False) + return vrf_config - return command +def verify(vrf_config): + # ensure VRF is not assigned to any interface + for vrf in vrf_config['vrf_add']: + if vrf['table_mod']: + raise ConfigError('VRF routing table id modification is not possible') -def verify(command): - for v in command['vrf']: - action = v['action'] - name = v['name'] - if action == 'modify' and v['table'] != v['check']: - raise ConfigError(f'set vrf name {name}: modification of vrf table is not supported yet') - if action == 'delete' and name in command['int']: - interface = ', '.join(command['int'][name]) - if interface: - raise ConfigError(f'delete vrf name {name}: can not delete vrf as it is used on {interface}') + # add test to see if routing table already exists or not? - return command + return None -def generate(command): - return command +def generate(vrf_config): + return None +def apply(vrf_config): + # https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt -def apply(command): # set the default VRF global behaviour - sysctl('net.ipv4.tcp_l3mdev_accept', command['bind']['ipv4']) - sysctl('net.ipv4.udp_l3mdev_accept', command['bind']['ipv4']) - - errors = [] - for v in command['vrf']: - name = v['name'] - action = v['action'] - table = v['table'] - - errors.append(f'could not {action} vrf {name}') - - if action == 'miss': - continue - - if action == 'delete': - if os.system(f'sudo ip link delete dev {name}'): - continue - errors.pop() - continue - - if action == 'modify': - # > uname -a - # Linux vyos 4.19.101-amd64-vyos #1 SMP Sun Feb 2 10:18:07 UTC 2020 x86_64 GNU/Linux - # > ip link add my-vrf type vrf table 100 - # > ip link set my-vrf type vrf table 200 - # RTNETLINK answers: Operation not supported - # so require to remove vrf and change all existing the interfaces - - if os.system(f'ip link delete dev {name}'): - continue - action = 'add' - - if action == 'add': - commands = [ - f'ip link add {name} type vrf table {table}', - f'ip link set dev {name} up', - f'ip -4 rule add oif {name} lookup {table}', - f'ip -4 rule add iif {name} lookup {table}', - f'ip -6 rule add oif {name} lookup {table}', - f'ip -6 rule add iif {name} lookup {table}', - ] - - for command in commands: - if os.system(command): - errors[-1] += ' ('+command+')' - continue - errors.pop() - - if errors: - raise ConfigError(', '.join(errors)) + #sysctl('net.ipv4.tcp_l3mdev_accept', command['bind']['ipv4']) + #sysctl('net.ipv4.udp_l3mdev_accept', command['bind']['ipv4']) + + for vrf_name in vrf_config['vrf_remove']: + if os.path.isdir(f'/sys/class/net/{vrf_name}'): + _cmd(f'ip link delete dev {vrf_name}') + + for vrf in vrf_config['vrf_add']: + name = vrf['name'] + table = vrf['table'] + + if not os.path.isdir(f'/sys/class/net/{name}'): + _cmd(f'ip link add {name} type vrf table {table}') + _cmd(f'ip link set dev {name} up') + _cmd(f'ip -4 rule add oif {name} lookup {table}') + _cmd(f'ip -4 rule add iif {name} lookup {table}') + _cmd(f'ip -6 rule add oif {name} lookup {table}') + _cmd(f'ip -6 rule add iif {name} lookup {table}') + + # set VRF description for e.g. SNMP monitoring + with open(f'/sys/class/net/{name}/ifalias', 'w') as f: + f.write(vrf['description']) + + return None if __name__ == '__main__': try: -- cgit v1.2.3 From bb705d1a90cad14f07b5e61c44365868283feee9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:38:01 +0100 Subject: vrf: T31: remove superfluous vyos.vrf library functions vyos.vrf.list_vrfs() was only used in one function thus building a library is no longer needed. If it is needed in the future it should be placed into a library again. --- python/vyos/vrf.py | 23 ----------------------- src/conf_mode/vrf.py | 1 - src/op_mode/show_vrf.py | 27 ++++++++++++++++++++++++--- 3 files changed, 24 insertions(+), 27 deletions(-) delete mode 100644 python/vyos/vrf.py (limited to 'src/conf_mode') diff --git a/python/vyos/vrf.py b/python/vyos/vrf.py deleted file mode 100644 index 99e4cb7d1..000000000 --- a/python/vyos/vrf.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2020 VyOS maintainers and contributors -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see . - -import json -import subprocess - - -def list_vrfs(): - command = 'ip -j -br link show type vrf' - answer = json.loads(subprocess.check_output(command.split()).decode()) - return [_ for _ in answer if _] diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 75b19da23..c2bbc72b3 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -22,7 +22,6 @@ from subprocess import check_call, CalledProcessError from vyos.config import Config from vyos.configdict import list_diff from vyos import ConfigError -from vyos import vrf default_config_data = { 'vrf_add': [], diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py index 210b3c9a4..ec894d572 100755 --- a/src/op_mode/show_vrf.py +++ b/src/op_mode/show_vrf.py @@ -1,7 +1,28 @@ #!/usr/bin/env python3 +# +# Copyright (C) 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 . import argparse -import vyos.vrf + +from subprocess import check_output +from json import loads + +def list_vrfs(): + command = 'ip -j -br link show type vrf' + answer = loads(check_output(command.split()).decode()) + return [_ for _ in answer if _] parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() @@ -15,7 +36,7 @@ args = parser.parse_args() if args.extensive: print('{:16} {:7} {:17} {}'.format('interface', 'state', 'mac', 'flags')) print('{:16} {:7} {:17} {}'.format('---------', '-----', '---', '-----')) - for vrf in vyos.vrf.list_vrfs(): + for vrf in list_vrfs(): name = vrf['ifname'] if args.interface and name != args.interface: continue @@ -24,4 +45,4 @@ if args.extensive: info = ','.join([_.lower() for _ in vrf['flags']]) print(f'{name:16} {state:7} {mac:17} {info}') else: - print(" ".join([vrf['ifname'] for vrf in vyos.vrf.list_vrfs()])) + print(" ".join([vrf['ifname'] for vrf in list_vrfs()])) -- cgit v1.2.3 From 5bf9dfd17096af6e7cf06e8e20eb16e8e55b9177 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:51:37 +0100 Subject: vrf: T31: support add/remove of interfaces from vrf --- interface-definitions/include/interface-vrf.xml.i | 8 +-- python/vyos/ifconfig.py | 28 +++++++- src/conf_mode/vrf.py | 81 +++++++++++++++-------- 3 files changed, 83 insertions(+), 34 deletions(-) (limited to 'src/conf_mode') diff --git a/interface-definitions/include/interface-vrf.xml.i b/interface-definitions/include/interface-vrf.xml.i index 7e880e6ee..355e7f0f3 100644 --- a/interface-definitions/include/interface-vrf.xml.i +++ b/interface-definitions/include/interface-vrf.xml.i @@ -1,12 +1,12 @@ VRF instance name + + text + VRF instance name + vrf name - - - - VRF name not allowed or to long diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index beeafa420..430b3c4e4 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -202,6 +202,12 @@ class Interface(Control): 'validate': assert_mac, 'shellcmd': 'ip link set dev {ifname} address {value}', }, + 'add_vrf': { + 'shellcmd': 'ip link set dev {ifname} master {value}', + }, + 'del_vrf': { + 'shellcmd': 'ip link set dev {ifname} nomaster {value}', + }, } _sysfs_get = { @@ -345,7 +351,7 @@ class Interface(Control): self.del_addr(addr) # --------------------------------------------------------------------- - # A code refactoring is required as this type check is present as + # A code refactoring is required as this type check is present as # Interface implement behaviour for one of it's sub-class. # It is required as the current pattern for vlan is: @@ -405,6 +411,26 @@ class Interface(Control): """ self.set_interface('mac', mac) + def add_vrf(self, vrf): + """ + Add interface to given VRF instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').add_vrf('foo') + """ + self.set_interface('add_vrf', vrf) + + def del_vrf(self, vrf): + """ + Remove interface from given VRF instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').del_vrf('foo') + """ + self.set_interface('del_vrf', vrf) + def set_arp_cache_tmo(self, tmo): """ Set ARP cache timeout value in seconds. Internal Kernel representation diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index c2bbc72b3..ad2b72a5b 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -24,6 +24,7 @@ from vyos.configdict import list_diff from vyos import ConfigError default_config_data = { + 'deleted': False, 'vrf_add': [], 'vrf_existing': [], 'vrf_remove': [] @@ -63,49 +64,71 @@ def get_config(): if not conf.exists(cfg_base): # get all currently effetive VRFs and mark them for deletion vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) - return vrf_config - - # Determine vrf interfaces (currently effective) - to determine which - # vrf interface is no longer present and needs to be removed - eff_vrf = conf.list_effective_nodes(cfg_base + ['name']) - act_vrf = conf.list_nodes(cfg_base + ['name']) - vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) - - # read in individual VRF definition and build up - # configuration - for name in conf.list_nodes(cfg_base + ['name']): + else: + # Determine vrf interfaces (currently effective) - to determine which + # vrf interface is no longer present and needs to be removed + eff_vrf = conf.list_effective_nodes(cfg_base + ['name']) + act_vrf = conf.list_nodes(cfg_base + ['name']) + vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) + + # read in individual VRF definition and build up + # configuration + for name in conf.list_nodes(cfg_base + ['name']): + vrf_inst = { + 'description' : '\0', + 'members': [], + 'name' : name, + 'table' : '', + 'table_mod': False + } + conf.set_level(cfg_base + ['name', name]) + + if conf.exists(['table']): + # VRF table can't be changed on demand, thus we need to read in the + # current and the effective routing table number + act_table = conf.return_value(['table']) + eff_table = conf.return_effective_value(['table']) + vrf_inst['table'] = act_table + if eff_table and eff_table != act_table: + vrf_inst['table_mod'] = True + + if conf.exists(['description']): + vrf_inst['description'] = conf.return_value(['description']) + + # find member interfaces of this particulat VRF + vrf_inst['members'] = interfaces_with_vrf(name) + + # append individual VRF configuration to global configuration list + vrf_config['vrf_add'].append(vrf_inst) + + # check VRFs which need to be removed as they are not allowed to have + # interfaces attached + tmp = [] + for name in vrf_config['vrf_remove']: vrf_inst = { - 'description' : '\0', 'members': [], 'name' : name, - 'table' : '', - 'table_mod': False } - conf.set_level(cfg_base + ['name', name]) - - if conf.exists(['table']): - # VRF table can't be changed on demand, thus we need to read in the - # current and the effective routing table number - act_table = conf.return_value(['table']) - eff_table = conf.return_effective_value(['table']) - vrf_inst['table'] = act_table - if eff_table and eff_table != act_table: - vrf_inst['table_mod'] = True - - if conf.exists(['description']): - vrf_inst['description'] = conf.return_value(['description']) # find member interfaces of this particulat VRF vrf_inst['members'] = interfaces_with_vrf(name) - # append individual VRF configuration to global configuration list - vrf_config['vrf_add'].append(vrf_inst) + # append individual VRF configuration to temporary configuration list + tmp.append(vrf_inst) + # replace values in vrf_remove with list of dictionaries + # as we need it in verify() - we can't delete a VRF with members attached + vrf_config['vrf_remove'] = tmp return vrf_config def verify(vrf_config): # ensure VRF is not assigned to any interface + for vrf in vrf_config['vrf_remove']: + if len(vrf['members']) > 0: + raise ConfigError('VRF "{}" can not be deleted as it has active members'.format(vrf['name'])) + + # routing table id can't be changed - OS restriction for vrf in vrf_config['vrf_add']: if vrf['table_mod']: raise ConfigError('VRF routing table id modification is not possible') -- cgit v1.2.3 From 93f7ae7f1ed1e218ef64d2582d11ac0ed769a438 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:53:58 +0100 Subject: vrf: T31: rename 'vrf disable-bind-to-all ipv4' to 'vrf bind-to-all' By default the scope of the port bindings for unbound sockets is limited to the default VRF. That is, it will not be matched by packets arriving on interfaces enslaved to an l3mdev and processes may bind to the same port if they bind to an l3mdev. TCP & UDP services running in the default VRF context (ie., not bound to any VRF device) can work across all VRF domains by enabling the 'vrf bind-to-all' option. --- interface-definitions/vrf.xml.in | 15 ++++----------- src/conf_mode/vrf.py | 15 +++++++++------ 2 files changed, 13 insertions(+), 17 deletions(-) (limited to 'src/conf_mode') diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index a6c67e9dd..f1895598e 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -7,19 +7,12 @@ 210 - + - Disable services running on the default VRF from other VRF (ssh, bgp, ...) + Enable binding services to all VRFs + - - - - - Enable binding across all VRF domains for IPv4 - - - - + VRF instance name diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index ad2b72a5b..e31285dde 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -24,6 +24,7 @@ from vyos.configdict import list_diff from vyos import ConfigError default_config_data = { + 'bind_to_all': 0, 'deleted': False, 'vrf_add': [], 'vrf_existing': [], @@ -40,7 +41,6 @@ def _cmd(command): pass raise ConfigError(f'Error operationg on VRF: {e}') - def interfaces_with_vrf(match): matched = [] config = Config() @@ -55,7 +55,6 @@ def interfaces_with_vrf(match): matched.append(name) return matched - def get_config(): conf = Config() vrf_config = deepcopy(default_config_data) @@ -65,6 +64,11 @@ def get_config(): # get all currently effetive VRFs and mark them for deletion vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) else: + + # Should services be allowed to bind to all VRFs? + if conf.exists(['bind-to-all']): + vrf_config['bind_to_all'] = 1 + # Determine vrf interfaces (currently effective) - to determine which # vrf interface is no longer present and needs to be removed eff_vrf = conf.list_effective_nodes(cfg_base + ['name']) @@ -121,7 +125,6 @@ def get_config(): vrf_config['vrf_remove'] = tmp return vrf_config - def verify(vrf_config): # ensure VRF is not assigned to any interface for vrf in vrf_config['vrf_remove']: @@ -137,7 +140,6 @@ def verify(vrf_config): return None - def generate(vrf_config): return None @@ -145,8 +147,9 @@ def apply(vrf_config): # https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt # set the default VRF global behaviour - #sysctl('net.ipv4.tcp_l3mdev_accept', command['bind']['ipv4']) - #sysctl('net.ipv4.udp_l3mdev_accept', command['bind']['ipv4']) + bind_all = vrf_config['bind_to_all'] + _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') + _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') for vrf_name in vrf_config['vrf_remove']: if os.path.isdir(f'/sys/class/net/{vrf_name}'): -- cgit v1.2.3 From 39dfcfb5077a4024c454f9cd127fc1f65529f591 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 20:55:04 +0100 Subject: vrf: T31: create iproute2 table to name mapping reference --- src/conf_mode/vrf.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'src/conf_mode') diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index e31285dde..91d8f8432 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import os +import jinja2 from sys import exit from copy import deepcopy @@ -23,6 +24,21 @@ from vyos.config import Config from vyos.configdict import list_diff from vyos import ConfigError +config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by vrf.py ### +# +# Routing table ID to name mapping reference + +# id vrf name comment +{% for vrf in vrf_add -%} +{{ "%-10s" | format(vrf.table) }} {{ "%-16s" | format(vrf.name) }} # {{ vrf.description }} +{% endfor -%} + +""" + default_config_data = { 'bind_to_all': 0, 'deleted': False, @@ -141,6 +157,11 @@ def verify(vrf_config): return None def generate(vrf_config): + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(vrf_config) + with open(config_file, 'w') as f: + f.write(config_text) + return None def apply(vrf_config): -- cgit v1.2.3 From d8808f46955fdaed1d2e86ec10b078db66e4639e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 21:28:03 +0100 Subject: vrf: T31: prior to the v4.8 kernel iif and oif rules are needed .. we run on 4.19 thus this is no longer needed. --- src/conf_mode/vrf.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 91d8f8432..8036703f1 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -181,12 +181,11 @@ def apply(vrf_config): table = vrf['table'] if not os.path.isdir(f'/sys/class/net/{name}'): + # For each VRF apart from your default context create a VRF + # interface with a separate routing table _cmd(f'ip link add {name} type vrf table {table}') + # Start VRf _cmd(f'ip link set dev {name} up') - _cmd(f'ip -4 rule add oif {name} lookup {table}') - _cmd(f'ip -4 rule add iif {name} lookup {table}') - _cmd(f'ip -6 rule add oif {name} lookup {table}') - _cmd(f'ip -6 rule add iif {name} lookup {table}') # set VRF description for e.g. SNMP monitoring with open(f'/sys/class/net/{name}/ifalias', 'w') as f: -- cgit v1.2.3 From 5b69a581831ba431d6b56077ad6340925a73a371 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 21:29:31 +0100 Subject: vrf: T31: adding unreachable routes to the routing tables --- src/conf_mode/vrf.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/conf_mode') diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 8036703f1..242fc7ccb 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -186,6 +186,11 @@ def apply(vrf_config): _cmd(f'ip link add {name} type vrf table {table}') # Start VRf _cmd(f'ip link set dev {name} up') + # The kernel Documentation/networking/vrf.txt also recommends + # adding unreachable routes to the VRF routing tables so that routes + # afterwards are taken. + _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272') + _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272') # set VRF description for e.g. SNMP monitoring with open(f'/sys/class/net/{name}/ifalias', 'w') as f: -- cgit v1.2.3 From 3d231292c8beaa00d40f922c01ca4191b2b89da1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 21:30:13 +0100 Subject: vrf: T31: reorder routing table lookups Linux routing uses rules to find tables - routing targets are then looked up in those tables. If the lookup got a matching route, the process ends. TL;DR; first table with a matching entry wins! You can see your routing table lookup rules using "ip rule", sadly the local lookup is hit before any VRF lookup. Pinging an addresses from the VRF will usually find a hit in the local table, and never reach the VRF routing table - this is usually not what you want. Thus we will re-arrange the tables and move the local lookup furhter down once VRFs are enabled. --- src/conf_mode/vrf.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'src/conf_mode') diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 242fc7ccb..a39366126 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -196,6 +196,34 @@ def apply(vrf_config): with open(f'/sys/class/net/{name}/ifalias', 'w') as f: f.write(vrf['description']) + # Linux routing uses rules to find tables - routing targets are then + # looked up in those tables. If the lookup got a matching route, the + # process ends. + # + # TL;DR; first table with a matching entry wins! + # + # You can see your routing table lookup rules using "ip rule", sadly the + # local lookup is hit before any VRF lookup. Pinging an addresses from the + # VRF will usually find a hit in the local table, and never reach the VRF + # routing table - this is usually not what you want. Thus we will + # re-arrange the tables and move the local lookup furhter down once VRFs + # are enabled. + + # set "normal" non VRF table lookups + add_pref = '0' + del_pref = '32765' + + # Lookup table is adjusted if we are in VRF mode + if vrf_config['vrf_add']: + add_pref = '32765' + del_pref = '0' + + # Configure table lookups + _cmd(f'ip -4 rule add pref {add_pref} table local') + _cmd(f'ip -4 rule del pref {del_pref}') + _cmd(f'ip -6 rule add pref {add_pref} table local') + _cmd(f'ip -6 rule del pref {del_pref}') + return None if __name__ == '__main__': -- cgit v1.2.3 From 153d1535d954f59cc48ed26f6cc552703b8cbd73 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 21:53:48 +0100 Subject: vrf: T31: enable vrf support for dummy interface --- interface-definitions/interfaces-dummy.xml.in | 1 + src/conf_mode/interfaces-dummy.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in index 39809a610..5229e602a 100644 --- a/interface-definitions/interfaces-dummy.xml.in +++ b/interface-definitions/interfaces-dummy.xml.in @@ -19,6 +19,7 @@ #include #include #include + #include diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index e79e6222d..10cae5d7d 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -18,6 +18,7 @@ import os from copy import deepcopy from sys import exit +from netifaces import interfaces from vyos.ifconfig import DummyIf from vyos.configdict import list_diff @@ -30,7 +31,8 @@ default_config_data = { 'deleted': False, 'description': '', 'disable': False, - 'intf': '' + 'intf': '', + 'vrf': '' } def get_config(): @@ -69,9 +71,17 @@ def get_config(): act_addr = conf.return_values('address') dummy['address_remove'] = list_diff(eff_addr, act_addr) + # retrieve VRF instance + if conf.exists('vrf'): + dummy['vrf'] = conf.return_value('vrf') + return dummy def verify(dummy): + vrf_name = dummy['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + return None def generate(dummy): @@ -95,6 +105,12 @@ def apply(dummy): for addr in dummy['address']: d.add_addr(addr) + # assign to VRF + if dummy['vrf']: + d.add_vrf(dummy['vrf']) + else: + d.del_vrf(dummy['vrf']) + # disable interface on demand if dummy['disable']: d.set_state('down') -- cgit v1.2.3