diff options
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-x | src/conf_mode/conntrack_sync.py | 6 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-ethernet.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 13 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-tunnel.py | 45 | ||||
-rwxr-xr-x | src/conf_mode/policy-route.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 108 |
6 files changed, 98 insertions, 80 deletions
diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py index 8f9837c2b..34d1f7398 100755 --- a/src/conf_mode/conntrack_sync.py +++ b/src/conf_mode/conntrack_sync.py @@ -93,9 +93,9 @@ def verify(conntrack): raise ConfigError('Can not configure expect-sync "all" with other protocols!') if 'listen_address' in conntrack: - address = conntrack['listen_address'] - if not is_addr_assigned(address): - raise ConfigError(f'Specified listen-address {address} not assigned to any interface!') + for address in conntrack['listen_address']: + if not is_addr_assigned(address): + raise ConfigError(f'Specified listen-address {address} not assigned to any interface!') vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack) if vrrp_group == None: diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index e7250fb49..ab8d58f81 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -165,7 +165,7 @@ def generate(ethernet): if 'ca_certificate' in ethernet['eapol']: ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem') ca_cert_name = ethernet['eapol']['ca_certificate'] - pki_ca_cert = ethernet['pki']['ca'][cert_name] + pki_ca_cert = ethernet['pki']['ca'][ca_cert_name] write_file(ca_cert_file_path, wrap_certificate(pki_ca_cert['certificate'])) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 0f6114b4a..329399274 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2022 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 @@ -226,11 +226,12 @@ def verify(openvpn): if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn: raise ConfigError('Must specify "local-address" or add interface to bridge') - if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1: - raise ConfigError('Only one IPv4 local-address can be specified') + if 'local_address' in openvpn: + if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1: + raise ConfigError('Only one IPv4 local-address can be specified') - if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1: - raise ConfigError('Only one IPv6 local-address can be specified') + if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1: + raise ConfigError('Only one IPv6 local-address can be specified') if openvpn['device_type'] == 'tun': if 'remote_address' not in openvpn: @@ -269,7 +270,7 @@ def verify(openvpn): if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn): raise ConfigError('"remote-address" and "remote-host" can not be the same') - if openvpn['device_type'] == 'tap': + if openvpn['device_type'] == 'tap' and 'local_address' in openvpn: # we can only have one local_address, this is ensured above v4addr = None for laddr in openvpn['local_address']: diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 30f57ec0c..4c1204b4e 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 yOS 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 @@ -21,9 +21,7 @@ from netifaces import interfaces from ipaddress import IPv4Address from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configdict import get_interface_dict -from vyos.configdict import node_changed from vyos.configdict import leaf_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -34,8 +32,6 @@ from vyos.configverify import verify_tunnel from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.ifconfig import TunnelIf -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 from vyos.util import get_interface_config from vyos.util import dict_search from vyos import ConfigError @@ -103,19 +99,22 @@ def verify(tunnel): raise ConfigError('Tunnel parameters ip key must be set!') if tunnel['encapsulation'] in ['gre', 'gretap']: - if dict_search('parameters.ip.key', tunnel) != None: - # Check pairs tunnel source-address/encapsulation/key with exists tunnels. - # Prevent the same key for 2 tunnels with same source-address/encap. T2920 - for tunnel_if in Section.interfaces('tunnel'): - # It makes no sense to run the test for re-used GRE keys on our - # own interface we are currently working on - if tunnel['ifname'] == tunnel_if: - continue - tunnel_cfg = get_interface_config(tunnel_if) - # no match on encapsulation - bail out - if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: - continue - new_source_address = dict_search('source_address', tunnel) + # Check pairs tunnel source-address/encapsulation/key with exists tunnels. + # Prevent the same key for 2 tunnels with same source-address/encap. T2920 + for tunnel_if in Section.interfaces('tunnel'): + # It makes no sense to run the test against our own interface we + # are currently configuring + if tunnel['ifname'] == tunnel_if: + continue + + tunnel_cfg = get_interface_config(tunnel_if) + # no match on encapsulation - bail out + if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: + continue + + new_source_address = dict_search('source_address', tunnel) + new_source_interface = dict_search('source_interface', tunnel) + if dict_search('parameters.ip.key', tunnel) != None: # Convert tunnel key to ip key, format "ip -j link show" # 1 => 0.0.0.1, 999 => 0.0.3.231 orig_new_key = dict_search('parameters.ip.key', tunnel) @@ -125,6 +124,16 @@ def verify(tunnel): dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key: raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \ f'is already used for tunnel "{tunnel_if}"!') + else: + # If no IP GRE key is used we can not have more then one GRE tunnel + # bound to any one interface/IP address. This will result in a OS + # PermissionError: add tunnel "gre0" failed: File exists + if (dict_search('address', tunnel_cfg) == new_source_address or + (dict_search('address', tunnel_cfg) == '0.0.0.0' and + dict_search('link', tunnel_cfg) == new_source_interface)): + raise ConfigError(f'Missing required "ip key" parameter when \ + running more then one GRE based tunnel on the \ + same source-interface/source-address') # Keys are not allowed with ipip and sit tunnels if tunnel['encapsulation'] in ['ipip', 'sit']: diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 82f668acf..3d1d7d8c5 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -123,6 +123,10 @@ def verify_rule(policy, name, rule_conf, ipv6): for group in valid_groups: if group in side_conf['group']: group_name = side_conf['group'][group] + + if group_name.startswith('!'): + group_name = group_name[1:] + fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group error_group = fw_group.replace("_", "-") group_obj = dict_search_args(policy['firewall_group'], fw_group, group_name) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 38c0c4463..cfe0f4d8e 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2022 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 @@ -29,6 +29,7 @@ from vyos.util import dict_search from vyos.util import get_interface_config from vyos.util import popen from vyos.util import run +from vyos.util import sysctl from vyos import ConfigError from vyos import frr from vyos import airbag @@ -37,10 +38,16 @@ airbag.enable() config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf' nft_vrf_config = '/tmp/nftables-vrf-zones' -def list_rules(): - command = 'ip -j -4 rule show' - answer = loads(cmd(command)) - return [_ for _ in answer if _] +def has_rule(af : str, priority : int, table : str): + """ Check if a given ip rule exists """ + if af not in ['-4', '-6']: + raise ValueError() + command = f'ip -j {af} rule show' + for tmp in loads(cmd(command)): + if {'priority', 'table'} <= set(tmp): + if tmp['priority'] == priority and tmp['table'] == table: + return True + return False def vrf_interfaces(c, match): matched = [] @@ -69,7 +76,6 @@ def vrf_routing(c, match): c.set_level(old_level) return matched - def get_config(config=None): if config: conf = config @@ -148,13 +154,11 @@ def apply(vrf): bind_all = '0' if 'bind-to-all' in vrf: bind_all = '1' - call(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') - call(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') + sysctl('net.ipv4.tcp_l3mdev_accept', bind_all) + sysctl('net.ipv4.udp_l3mdev_accept', bind_all) for tmp in (dict_search('vrf_remove', vrf) or []): if os.path.isdir(f'/sys/class/net/{tmp}'): - call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272') - call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272') call(f'ip link delete dev {tmp}') # Remove nftables conntrack zone map item nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}' @@ -165,31 +169,59 @@ def apply(vrf): # check if table already exists _, err = popen('nft list table inet vrf_zones') # If not, create a table - if err: - if os.path.exists(nft_vrf_config): - cmd(f'nft -f {nft_vrf_config}') - os.unlink(nft_vrf_config) + if err and os.path.exists(nft_vrf_config): + cmd(f'nft -f {nft_vrf_config}') + os.unlink(nft_vrf_config) + + # 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 further down once VRFs + # are enabled. + # + # Thanks to https://stbuehler.de/blog/article/2020/02/29/using_vrf__virtual_routing_and_forwarding__on_linux.html + + for afi in ['-4', '-6']: + # move lookup local to pref 32765 (from 0) + if not has_rule(afi, 32765, 'local'): + call(f'ip {afi} rule add pref 32765 table local') + if has_rule(afi, 0, 'local'): + call(f'ip {afi} rule del pref 0') + # make sure that in VRFs after failed lookup in the VRF specific table + # nothing else is reached + if not has_rule(afi, 1000, 'l3mdev'): + # this should be added by the kernel when a VRF is created + # add it here for completeness + call(f'ip {afi} rule add pref 1000 l3mdev protocol kernel') + + # add another rule with an unreachable target which only triggers in VRF context + # if a route could not be reached + if not has_rule(afi, 2000, 'l3mdev'): + call(f'ip {afi} rule add pref 2000 l3mdev unreachable') for name, config in vrf['name'].items(): table = config['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 call(f'ip link add {name} type vrf table {table}') - # The kernel Documentation/networking/vrf.txt also recommends - # adding unreachable routes to the VRF routing tables so that routes - # afterwards are taken. - call(f'ip -4 route add vrf {name} unreachable default metric 4278198272') - call(f'ip -6 route add vrf {name} unreachable default metric 4278198272') - # We also should add proper loopback IP addresses to the newly - # created VRFs for services bound to the loopback address (SNMP, NTP) - call(f'ip -4 addr add 127.0.0.1/8 dev {name}') - call(f'ip -6 addr add ::1/128 dev {name}') # set VRF description for e.g. SNMP monitoring vrf_if = Interface(name) + # We also should add proper loopback IP addresses to the newly + # created VRFs for services bound to the loopback address (SNMP, NTP) + vrf_if.add_addr('127.0.0.1/8') + vrf_if.add_addr('::1/128') + # add VRF description if available vrf_if.set_alias(config.get('description', '')) + # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as @@ -203,37 +235,9 @@ def apply(vrf): nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}' cmd(f'nft {nft_add_element}') - # 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. - - # get current preference on local table - local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0] - - # change preference when VRFs are enabled and local lookup table is default - if not local_pref and 'name' in vrf: - for af in ['-4', '-6']: - call(f'ip {af} rule add pref 32765 table local') - call(f'ip {af} rule del pref 0') # return to default lookup preference when no VRF is configured if 'name' not in vrf: - for af in ['-4', '-6']: - call(f'ip {af} rule add pref 0 table local') - call(f'ip {af} rule del pref 32765') - - # clean out l3mdev-table rule if present - if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]: - call(f'ip {af} rule del pref 1000') # Remove VRF zones table from nftables tmp = run('nft list table inet vrf_zones') if tmp == 0: |