diff options
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | data/templates/wifi/hostapd.conf.tmpl | 8 | ||||
-rw-r--r-- | interface-definitions/interfaces-tunnel.xml.in | 41 | ||||
-rw-r--r-- | interface-definitions/interfaces-wireguard.xml.in | 1 | ||||
-rw-r--r-- | interface-definitions/interfaces-wireless.xml.in | 6 | ||||
-rw-r--r-- | python/vyos/ifconfig/bridge.py | 12 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 18 | ||||
-rw-r--r-- | python/vyos/ifconfig/tunnel.py | 194 | ||||
-rw-r--r-- | python/vyos/ifconfig/wireguard.py | 29 | ||||
-rw-r--r-- | python/vyos/ifconfig/wireless.py | 29 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_tunnel.py | 6 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-bridge.py | 16 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-tunnel.py | 781 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireguard.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireless.py | 1 |
15 files changed, 282 insertions, 864 deletions
@@ -76,6 +76,7 @@ interface_definitions: $(BUILD_DIR) $(obj) rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ip/node.def rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/wirelessmodem/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/wireguard/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/protocols/node.def rm -rf $(TMPL_DIR)/protocols/nbgp rm -rf $(TMPL_DIR)/protocols/isis diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index 16d9f7c98..e66e3472b 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -451,14 +451,6 @@ macaddr_acl=0 max_num_sta={{ max_stations }} {% endif %} -{% if wds is defined %} -# WDS (4-address frame) mode with per-station virtual interfaces -# (only supported with driver=nl80211) -# This mode allows associated stations to use 4-address frames to allow layer 2 -# bridging to be used. -wds_sta=1 -{% endif %} - {% if isolate_stations is defined %} # Client isolation can be used to prevent low-level bridging of frames between # associated stations in the BSS. By default, this bridging is allowed. diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index c3f178d59..b322374b3 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -39,7 +39,6 @@ <script>${vyos_completion_dir}/list_local.py</script> </completionHelp> <constraint> - <!-- does it need fixing/changing to be more restrictive ? --> <validator name="ip-address"/> </constraint> </properties> @@ -104,7 +103,7 @@ <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> <constraint> - <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex> + <regex>^(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+$</regex> </constraint> </properties> </leafNode> @@ -112,36 +111,40 @@ <properties> <help>Encapsulation of this tunnel interface</help> <completionHelp> - <list>gre gre-bridge ipip sit ipip6 ip6ip6 ip6gre</list> + <list>gre gre-bridge ip6gre ip6ip6 ipip ipip6 sit</list> </completionHelp> <valueHelp> + <format>gre</format> + <description>Generic Routing Encapsulation</description> + </valueHelp> + <valueHelp> <format>gre-bridge</format> <description>Generic Routing Encapsulation bridge interface</description> </valueHelp> <valueHelp> - <format>ipip</format> - <description>IP in IP encapsulation</description> + <format>ip6gre</format> + <description>GRE over IPv6 network</description> </valueHelp> <valueHelp> - <format>sit</format> - <description>Simple Internet Transition encapsulation</description> + <format>ip6ip6</format> + <description>IP6 in IP6 encapsulation</description> </valueHelp> <valueHelp> - <format>ipip6</format> - <description>IP in IP6 encapsulation</description> + <format>ipip</format> + <description>IP in IP encapsulation</description> </valueHelp> <valueHelp> - <format>ip6ip6</format> - <description>IP6 in IP6 encapsulation</description> + <format>ipip6</format> + <description>IP in IP6 encapsulation</description> </valueHelp> <valueHelp> - <format>ip6gre</format> - <description>GRE over IPv6 network</description> + <format>sit</format> + <description>Simple Internet Transition encapsulation</description> </valueHelp> <constraint> - <regex>(gre|gre-bridge|ipip|sit|ipip6|ip6ip6|ip6gre)</regex> + <regex>^(gre|gre-bridge|ip6gre|ip6ip6|ipip|ipip6|sit)$</regex> </constraint> - <constraintErrorMessage>Must be one of 'gre' 'gre-bridge' 'ipip' 'sit' 'ipip6' 'ip6ip6' 'ip6gre'</constraintErrorMessage> + <constraintErrorMessage>Invalid encapsulation, must be one of: gre, gre-bridge, ipip, sit, ipip6, ip6ip6, ip6gre</constraintErrorMessage> </properties> </leafNode> <leafNode name="multicast"> @@ -159,7 +162,7 @@ <description>Disable Multicast (default)</description> </valueHelp> <constraint> - <regex>(enable|disable)</regex> + <regex>^(enable|disable)$</regex> </constraint> <constraintErrorMessage>Must be 'disable' or 'enable'</constraintErrorMessage> </properties> @@ -186,6 +189,7 @@ </constraint> <constraintErrorMessage>TTL must be between 0 and 255</constraintErrorMessage> </properties> + <defaultValue>255</defaultValue> </leafNode> <leafNode name="tos"> <properties> @@ -199,6 +203,7 @@ </constraint> <constraintErrorMessage>TOS must be between 0 and 99</constraintErrorMessage> </properties> + <defaultValue>inherit</defaultValue> </leafNode> <leafNode name="key"> <properties> @@ -232,6 +237,7 @@ </constraint> <constraintErrorMessage>key must be between 0-255</constraintErrorMessage> </properties> + <defaultValue>4</defaultValue> </leafNode> <leafNode name="flowlabel"> <properties> @@ -245,6 +251,7 @@ </constraint> <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage> </properties> + <defaultValue>inherit</defaultValue> </leafNode> <leafNode name="hoplimit"> <properties> @@ -258,6 +265,7 @@ </constraint> <constraintErrorMessage>hoplimit must be between 0-255</constraintErrorMessage> </properties> + <defaultValue>64</defaultValue> </leafNode> <leafNode name="tclass"> <properties> @@ -271,6 +279,7 @@ </constraint> <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage> </properties> + <defaultValue>inherit</defaultValue> </leafNode> </children> </node> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index aa63e4ac7..84f7803a0 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -22,6 +22,7 @@ #include <include/interface-vrf.xml.i> #include <include/port-number.xml.i> #include <include/interface-mtu-68-16000.xml.i> + #include <include/interface-ipv6-options.xml.i> <leafNode name="fwmark"> <properties> <help>A 32-bit fwmark value set on all outgoing packets</help> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index fdea1e3ab..6b238e313 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -771,12 +771,6 @@ </leafNode> #include <include/vif.xml.i> #include <include/vif-s.xml.i> - <leafNode name="wds"> - <properties> - <help>Enable WDS (Wireless Distribution System)</help> - <valueless/> - </properties> - </leafNode> </children> </tagNode> </children> diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 7c77e050a..e6cda4adb 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -309,15 +309,12 @@ class BridgeIf(Interface): vlan_filter = 1 cmd = f'bridge vlan del dev {interface} vid 1' self._cmd(cmd) - vlan_del.add(1) vlan_id = interface_config['native_vlan'] + if vlan_id != 1: + vlan_del.add(1) cmd = f'bridge vlan add dev {interface} vid {vlan_id} pvid untagged master' self._cmd(cmd) vlan_add.add(vlan_id) - else: - cmd = f'bridge vlan del dev {interface} vid 1' - self._cmd(cmd) - vlan_del.add(1) if 'allowed_vlan' in interface_config: vlan_filter = 1 @@ -325,6 +322,11 @@ class BridgeIf(Interface): cmd = f'bridge vlan add dev {interface} vid {vlan} master' self._cmd(cmd) vlan_add.add(vlan) + + if vlan_filter: + if 'native_vlan' not in interface_config: + cmd = f'bridge vlan del dev {interface} vid 1' + self._cmd(cmd) for vlan in vlan_del: diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 893623284..39b80ce08 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -942,6 +942,15 @@ class Interface(Control): # method to apply()? self._config = config + # Change interface MAC address - re-set to real hardware address (hw-id) + # if custom mac is removed. Skip if bond member. + if 'is_bond_member' not in config: + mac = config.get('hw_id') + if 'mac' in config: + mac = config.get('mac') + if mac: + self.set_mac(mac) + # Update interface description self.set_alias(config.get('description', '')) @@ -1058,15 +1067,6 @@ class Interface(Control): for addr in tmp: self.del_ipv6_eui64_address(addr) - # Change interface MAC address - re-set to real hardware address (hw-id) - # if custom mac is removed. Skip if bond member. - if 'is_bond_member' not in config: - mac = config.get('hw_id') - if 'mac' in config: - mac = config.get('mac') - if mac: - self.set_mac(mac) - # Manage IPv6 link-local addresses tmp = dict_search('ipv6.address.no_default_link_local', config) # we must check explicitly for None type as if the key is set we will diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py index 4122d1a2f..4d1441a29 100644 --- a/python/vyos/ifconfig/tunnel.py +++ b/python/vyos/ifconfig/tunnel.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -17,9 +17,11 @@ # https://community.hetzner.com/tutorials/linux-setup-gre-tunnel from copy import deepcopy +from netaddr import EUI +from netaddr import mac_unix_expanded +from random import getrandbits from vyos.ifconfig.interface import Interface -from vyos.ifconfig.afi import IP4, IP6 from vyos.validate import assert_list def enable_to_on(value): @@ -61,68 +63,73 @@ class _Tunnel(Interface): }, }} - # use for "options" and "updates" - # If an key is only in the options list, it can only be set at creation time - # the create comand will only be make using the key in options - - # If an option is in the updates list, it can be updated - # upon, the creation, all key not yet applied will be updated - - # multicast/allmulticast can not be part of the create command - - # options matrix: - # with ip = 4, we have multicast - # wiht ip = 6, nothing - # with tunnel = 4, we have tos, ttl, key - # with tunnel = 6, we have encaplimit, hoplimit, tclass, flowlabel - - # TODO: For multicast, it is allowed on IP6IP6 and Sit6RD - # TODO: to match vyatta but it should be checked for correctness - - updates = [] - - create = '' - change = '' - delete = '' - - ip = [] # AFI of the families which can be used in the tunnel - tunnel = 0 # invalid - need to be set by subclasses - def __init__(self, ifname, **config): self.config = deepcopy(config) if config else {} super().__init__(ifname, **config) def _create(self): + create = 'ip tunnel add {ifname} mode {type}' + # add " option-name option-name-value ..." for all options set options = " ".join(["{} {}".format(k, self.config[k]) for k in self.options if k in self.config and self.config[k]]) - self._cmd('{} {}'.format(self.create.format(**self.config), options)) + self._cmd('{} {}'.format(create.format(**self.config), options)) self.set_admin_state('down') - def _delete(self): - self.set_admin_state('down') - cmd = self.delete.format(**self.config) - return self._cmd(cmd) - - def set_interface(self, option, value): - try: - return Interface.set_interface(self, option, value) - except Exception: - pass - - if value == '': - # remove the value so that it is not used - self.config.pop(option, '') + def change_options(self): + change = 'ip tunnel cha {ifname} mode {type}' - if self.change: - self._cmd('{} {} {}'.format( - self.change.format(**self.config), option, value)) - return True + # add " option-name option-name-value ..." for all options set + options = " ".join(["{} {}".format(k, self.config[k]) + for k in self.options if k in self.config and self.config[k]]) + self._cmd('{} {}'.format(change.format(**self.config), options)) @classmethod def get_config(cls): return dict(zip(cls.options, ['']*len(cls.options))) + def get_mac(self): + """ + Get current interface MAC (Media Access Contrl) address used. + + NOTE: Tunnel interfaces have no "MAC" address by default. The content + of the 'address' file in /sys/class/net/device contains the + local-ip thus we generate a random MAC address instead + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_mac() + '00:50:ab:cd:ef:00' + """ + # we choose 40 random bytes for the MAC address, this gives + # us e.g. EUI('00-EA-EE-D6-A3-C8') or EUI('00-41-B9-0D-F2-2A') + tmp = EUI(getrandbits(48)).value + # set locally administered bit in MAC address + tmp |= 0xf20000000000 + # convert integer to "real" MAC address representation + mac = EUI(hex(tmp).split('x')[-1]) + # change dialect to use : as delimiter instead of - + mac.dialect = mac_unix_expanded + return str(mac) + + 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 + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # 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 + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) class GREIf(_Tunnel): """ @@ -141,20 +148,8 @@ class GREIf(_Tunnel): }, } - ip = [IP4, IP6] - tunnel = IP4 - default = {'type': 'gre'} - required = ['local', ] # mGRE is a GRE without remote endpoint - options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] - updates = ['local', 'remote', 'dev', 'ttl', 'tos', - 'mtu', 'multicast', 'allmulticast'] - - create = 'ip tunnel add {ifname} mode {type}' - change = 'ip tunnel cha {ifname}' - delete = 'ip tunnel del {ifname}' - # GreTap also called GRE Bridge class GRETapIf(_Tunnel): @@ -173,19 +168,8 @@ class GRETapIf(_Tunnel): }, } - ip = [IP4, ] - tunnel = IP4 - default = {'type': 'gretap'} - required = ['local', ] - options = ['local', 'remote', 'ttl',] - updates = ['mtu', ] - - create = 'ip link add {ifname} type {type}' - change = '' - delete = 'ip link del {ifname}' - class IP6GREIf(_Tunnel): """ @@ -196,30 +180,9 @@ class IP6GREIf(_Tunnel): https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre6.c """ - ip = [IP4, IP6] - tunnel = IP6 - default = {'type': 'ip6gre'} - required = ['local', 'remote'] - options = ['local', 'remote', 'dev', 'encaplimit', 'hoplimit', 'tclass', 'flowlabel'] - updates = ['local', 'remote', 'dev', 'encaplimit', - 'hoplimit', 'tclass', 'flowlabel', - 'mtu', 'multicast', 'allmulticast'] - - create = 'ip tunnel add {ifname} mode {type}' - change = 'ip tunnel cha {ifname} mode {type}' - delete = 'ip tunnel del {ifname}' - - # using "ip tunnel change" without using "mode" causes errors - # sudo ip tunnel add tun100 mode ip6gre local ::1 remote 1::1 - # sudo ip tunnel cha tun100 hoplimit 100 - # *** stack smashing detected ** *: < unknown > terminated - # sudo ip tunnel cha tun100 local: : 2 - # Error: an IP address is expected rather than "::2" - # works if mode is explicit - class IPIPIf(_Tunnel): """ @@ -232,20 +195,8 @@ class IPIPIf(_Tunnel): # IPIP does not allow to pass multicast, unlike GRE # but the interface itself can be set with multicast - ip = [IP4,] - tunnel = IP4 - default = {'type': 'ipip'} - required = ['local', 'remote'] - options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] - updates = ['local', 'remote', 'dev', 'ttl', 'tos', - 'mtu', 'multicast', 'allmulticast'] - - create = 'ip tunnel add {ifname} mode {type}' - change = 'ip tunnel cha {ifname}' - delete = 'ip tunnel del {ifname}' - class IPIP6If(_Tunnel): """ @@ -255,22 +206,9 @@ class IPIP6If(_Tunnel): https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_ip6tnl.c """ - ip = [IP4,] - tunnel = IP6 - default = {'type': 'ipip6'} - required = ['local', 'remote'] - options = ['local', 'remote', 'dev', 'encaplimit', 'hoplimit', 'tclass', 'flowlabel'] - updates = ['local', 'remote', 'dev', 'encaplimit', - 'hoplimit', 'tclass', 'flowlabel', - 'mtu', 'multicast', 'allmulticast'] - - create = 'ip -6 tunnel add {ifname} mode {type}' - change = 'ip -6 tunnel cha {ifname}' - delete = 'ip -6 tunnel del {ifname}' - class IP6IP6If(IPIP6If): """ @@ -279,9 +217,6 @@ class IP6IP6If(IPIP6If): For more information please refer to: https://tools.ietf.org/html/rfc2473 """ - - ip = [IP6,] - default = {'type': 'ip6ip6'} @@ -293,20 +228,8 @@ class SitIf(_Tunnel): https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_iptnl.c """ - ip = [IP6, IP4] - tunnel = IP4 - default = {'type': 'sit'} - required = ['local', 'remote'] - options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] - updates = ['local', 'remote', 'dev', 'ttl', 'tos', - 'mtu', 'multicast', 'allmulticast'] - - create = 'ip tunnel add {ifname} mode {type}' - change = 'ip tunnel cha {ifname}' - delete = 'ip tunnel del {ifname}' - class Sit6RDIf(SitIf): """ @@ -314,15 +237,8 @@ class Sit6RDIf(SitIf): https://en.wikipedia.org/wiki/IPv6_rapid_deployment """ - - ip = [IP6,] - - required = ['remote', '6rd-prefix'] - # TODO: check if key can really be used with 6RD options = ['remote', 'ttl', 'tos', 'key', '6rd-prefix', '6rd-relay-prefix'] - updates = ['remote', 'ttl', 'tos', - 'mtu', 'multicast', 'allmulticast'] def _create(self): # do not call _Tunnel.create, building fully here diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index da3bd4e89..ac6dc2109 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -17,6 +17,9 @@ import os import time from datetime import timedelta +from netaddr import EUI +from netaddr import mac_unix_expanded +from random import getrandbits from hurry.filesize import size from hurry.filesize import alternative @@ -169,6 +172,30 @@ class WireGuardIf(Interface): ['port', 'private_key', 'pubkey', 'psk', 'allowed_ips', 'fwmark', 'endpoint', 'keepalive'] + def get_mac(self): + """ + Get current interface MAC (Media Access Contrl) address used. + + NOTE: Tunnel interfaces have no "MAC" address by default. The content + of the 'address' file in /sys/class/net/device contains the + local-ip thus we generate a random MAC address instead + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_mac() + '00:50:ab:cd:ef:00' + """ + # we choose 40 random bytes for the MAC address, this gives + # us e.g. EUI('00-EA-EE-D6-A3-C8') or EUI('00-41-B9-0D-F2-2A') + tmp = EUI(getrandbits(48)).value + # set locally administered bit in MAC address + tmp |= 0xf20000000000 + # convert integer to "real" MAC address representation + mac = EUI(hex(tmp).split('x')[-1]) + # change dialect to use : as delimiter instead of - + mac.dialect = mac_unix_expanded + return str(mac) + 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 @@ -221,7 +248,7 @@ class WireGuardIf(Interface): # Endpoint configuration is optional if {'address', 'port'} <= set(peer): - if is_ipv6(config['address']): + if is_ipv6(peer['address']): cmd += ' endpoint [{address}]:{port}' else: cmd += ' endpoint {address}:{port}' diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index deca68bf0..37703d242 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -23,10 +23,8 @@ class WiFiIf(Interface): default = { 'type': 'wifi', - 'phy': '', - 'wds': 'off', + 'phy': 'phy0' } - definition = { **Interface.definition, **{ @@ -35,19 +33,12 @@ class WiFiIf(Interface): 'bridgeable': True, } } - options = Interface.options + \ ['phy', 'op_mode'] - _command_set = {**Interface._command_set, **{ - '4addr': { - 'shellcmd': 'iw dev {ifname} set 4addr {value}', - }, - }} - def _create(self): # all interfaces will be added in monitor mode - cmd = 'iw phy {phy} interface add {ifname} type monitor 4addr {wds}' \ + cmd = 'iw phy {phy} interface add {ifname} type monitor' \ .format(**self.config) self._cmd(cmd) @@ -59,20 +50,28 @@ class WiFiIf(Interface): .format(**self.config) self._cmd(cmd) - def set_4aadr_mode(self, state): - return self.set_interface('4addr', state) - 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 interface setup code and provide a single point of entry when workin on any interface. """ - self.set_4aadr_mode('on' if 'wds' in config else 'off') + # We can not call add_to_bridge() until wpa_supplicant is running, thus + # we will remove the key from the config dict and react to this specal + # case in thie derived class. + # re-add ourselves to any bridge we might have fallen out of + bridge_member = '' + if 'is_bridge_member' in config: + bridge_member = config['is_bridge_member'] + del config['is_bridge_member'] # call base class first super().update(config) + # re-add ourselves to any bridge we might have fallen out of + if bridge_member: + self.add_to_bridge(bridge_member) + # 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 diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py index 4817321cf..aaff92dea 100755 --- a/smoketest/scripts/cli/test_interfaces_tunnel.py +++ b/smoketest/scripts/cli/test_interfaces_tunnel.py @@ -168,12 +168,12 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): self.session.set(self._base_path + [interface, 'local-ip', self.local_v6]) self.session.set(self._base_path + [interface, 'remote-ip', remote_ip6]) - # Encapsulation mode requires IPv6 local-ip + # Encapsulation mode requires IPv4 local-ip with self.assertRaises(ConfigSessionError): self.session.commit() self.session.set(self._base_path + [interface, 'local-ip', self.local_v4]) - # Encapsulation mode requires IPv6 local-ip + # Encapsulation mode requires IPv4 local-ip with self.assertRaises(ConfigSessionError): self.session.commit() self.session.set(self._base_path + [interface, 'remote-ip', remote_ip4]) @@ -360,7 +360,7 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): # No assertion is raised for GRE remote-ip when missing self.session.set(self._base_path + [interface, 'remote-ip', remote_ip4]) - # Source interface can not be used with si + # Source interface can not be used with sit self.session.set(self._base_path + [interface, 'source-interface', source_if]) with self.assertRaises(ConfigSessionError): self.session.commit() diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 076bdb63e..7af3e3d7c 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -123,12 +123,12 @@ def get_config(config=None): # VLAN-aware bridge members must not have VLAN interface configuration if 'native_vlan' in interface_config: - if 'disable' not in interface_config['native_vlan']: - vlan_aware = True + vlan_aware = True if 'allowed_vlan' in interface_config: vlan_aware = True + if vlan_aware: tmp = has_vlan_subinterface_configured(conf,interface) if tmp: @@ -142,6 +142,8 @@ def verify(bridge): verify_dhcpv6(bridge) verify_vrf(bridge) + + vlan_aware = False if dict_search('member.interface', bridge): for interface, interface_config in bridge['member']['interface'].items(): @@ -168,6 +170,16 @@ def verify(bridge): if 'has_vlan' in interface_config: raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') + # VLAN-aware bridge members must not have VLAN interface configuration + if 'native_vlan' in interface_config: + vlan_aware = True + + if 'allowed_vlan' in interface_config: + vlan_aware = True + + if vlan_aware and 'wlan' in interface: + raise ConfigError(error_msg + 'VLAN aware cannot be set!') + if 'allowed_vlan' in interface_config: for vlan in interface_config['allowed_vlan']: if re.search('[0-9]{1,4}-[0-9]{1,4}', vlan): diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index f1217b62d..1a7e9a96d 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) 2019 VyOS maintainers and contributors +# Copyright (C) 2018-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 @@ -15,354 +15,124 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import netifaces from sys import exit -from copy import deepcopy from netifaces import interfaces from vyos.config import Config -from vyos.configdict import is_member -from vyos.configdict import list_diff -from vyos.dicts import FixedDict -from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf -from vyos.ifconfig.afi import IP4, IP6 +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_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_mtu_ipv6 +from vyos.ifconfig import Interface +from vyos.ifconfig import GREIf +from vyos.ifconfig import GRETapIf +from vyos.ifconfig import IPIPIf +from vyos.ifconfig import IP6GREIf +from vyos.ifconfig import IPIP6If +from vyos.ifconfig import IP6IP6If +from vyos.ifconfig import SitIf +from vyos.ifconfig import Sit6RDIf from vyos.template import is_ipv4 from vyos.template import is_ipv6 +from vyos.util import dict_search from vyos import ConfigError - - from vyos import airbag airbag.enable() - -class ConfigurationState(object): +def get_config(config=None): """ - The current API require a dict to be generated by get_config() - which is then consumed by verify(), generate() and apply() - - ConfiguartionState is an helper class wrapping Config and providing - an common API to this dictionary structure - - Its to_api() function return a dictionary containing three fields, - each a dict, called options, changes, actions. - - options: - - contains the configuration options for the dict and its value - {'options': {'commment': 'test'}} will be set if - 'set interface dummy dum1 description test' was used and - the key 'commment' is used to index the description info. - - changes: - - per key, let us know how the data was modified using one of the action - a special key called 'section' is used to indicate what happened to the - section. for example: - - 'set interface dummy dum1 description test' when no interface was setup - will result in the following changes - {'changes': {'section': 'create', 'comment': 'create'}} - - on an existing interface, depending if there was a description - 'set interface dummy dum1 description test' will result in one of - {'changes': {'comment': 'create'}} (not present before) - {'changes': {'comment': 'static'}} (unchanged) - {'changes': {'comment': 'modify'}} (changed from half) - - and 'delete interface dummy dummy1 description' will result in: - {'changes': {'comment': 'delete'}} - - actions: - - for each action list the configuration key which were changes - in our example if we added the 'description' and added an IP we would have - {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}} - - the actions are: - 'create': it did not exist previously and was created - 'modify': it did exist previously but its content changed - 'static': it did exist and did not change - 'delete': it was present but was removed from the configuration - 'absent': it was not and is not present - which for each field represent how it was modified since the last commit + 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', 'tunnel'] + tunnel = get_interface_dict(conf, base) - def __init__(self, configuration, section, default): - """ - initialise the class for a given configuration path: - - >>> conf = ConfigurationState(conf, 'interfaces ethernet eth1') - all further references to get_value(s) and get_effective(s) - will be for this part of the configuration (eth1) - """ - self._conf = configuration - - self.default = deepcopy(default) - self.options = FixedDict(**default) - self.actions = { - 'create': [], # the key did not exist and was added - 'static': [], # the key exists and its value was not modfied - 'modify': [], # the key exists and its value was modified - 'absent': [], # the key is not present - 'delete': [], # the key was present and was deleted - } - self.changes = {} - if not self._conf.exists(section): - self.changes['section'] = 'delete' - elif self._conf.exists_effective(section): - self.changes['section'] = 'modify' - else: - self.changes['section'] = 'create' - - self.set_level(section) - - def set_level(self, lpath): - self.section = lpath - self._conf.set_level(lpath) - - def _act(self, section): - """ - Returns for a given configuration field determine what happened to it - - 'create': it did not exist previously and was created - 'modify': it did exist previously but its content changed - 'static': it did exist and did not change - 'delete': it was present but was removed from the configuration - 'absent': it was not and is not present - """ - if self._conf.exists(section): - if self._conf.exists_effective(section): - if self._conf.return_value(section) != self._conf.return_effective_value(section): - return 'modify' - return 'static' - return 'create' - else: - if self._conf.exists_effective(section): - return 'delete' - return 'absent' - - def _action(self, name, key): - action = self._act(key) - self.changes[name] = action - self.actions[action].append(name) - return action - - def _get(self, name, key, default, getter): - value = getter(key) - if not value: - if default: - self.options[name] = default - return - self.options[name] = self.default[name] - return - self.options[name] = value - - def get_value(self, name, key, default=None): - """ - >>> conf.get_value('comment', 'description') - will place the string of 'interface dummy description test' - into the dictionnary entry 'comment' using Config.return_value - (the data in the configuration to apply) - """ - if self._action(name, key) in ('delete', 'absent'): - return - return self._get(name, key, default, self._conf.return_value) - - def get_values(self, name, key, default=None): - """ - >>> conf.get_values('addresses', 'address') - will place a list of the new IP present in 'interface dummy dum1 address' - into the dictionnary entry "-add" (here 'addresses-add') using - Config.return_values and will add the the one which were removed in into - the entry "-del" (here addresses-del') - """ - add_name = f'{name}-add' - - if self._action(add_name, key) in ('delete', 'absent'): - return - - self._get(add_name, key, default, self._conf.return_values) - - # get the effective values to determine which data is no longer valid - self.options['addresses-del'] = list_diff( - self._conf.return_effective_values('address'), - self.options['addresses-add'] - ) - - def get_effective(self, name, key, default=None): - """ - >>> conf.get_value('comment', 'description') - will place the string of 'interface dummy description test' - into the dictionnary entry 'comment' using Config.return_effective_value - (the data in the configuration to apply) - """ - self._action(name, key) - return self._get(name, key, default, self._conf.return_effective_value) - - def get_effectives(self, name, key, default=None): - """ - >>> conf.get_effectives('addresses-add', 'address') - will place a list made of the IP present in 'interface ethernet eth1 address' - into the dictionnary entry 'addresses-add' using Config.return_effectives_value - (the data in the un-modified configuration) - """ - self._action(name, key) - return self._get(name, key, default, self._conf.return_effectives_value) + # Wireguard is "special" the default MTU is 1420 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + tunnel['mtu'] = '1476' - def load(self, mapping): - """ - load will take a dictionary defining how we wish the configuration - to be parsed and apply this definition to set the data. + tmp = leaf_node_changed(conf, ['encapsulation']) + if tmp: tunnel.update({'encapsulation_changed': {}}) - >>> mapping = { - 'addresses-add' : ('address', True, None), - 'comment' : ('description', False, 'auto'), - } - >>> conf.load(mapping) + # We must check if our interface is configured to be a DMVPN member + nhrp_base = ['protocols', 'nhrp', 'tunnel'] + conf.set_level(nhrp_base) + nhrp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if nhrp: tunnel.update({'nhrp' : list(nhrp.keys())}) - mapping is a dictionary where each key represents the name we wish - to have (such as 'addresses-add'), with a list a content representing - how the data should be parsed: - - the configuration section name - such as 'address' under 'interface ethernet eth1' - - boolean indicating if this data can have multiple values - for 'address', True, as multiple IPs can be set - for 'description', False, as it is a single string - - default represent the default value if absent from the configuration - 'None' indicate that no default should be set if the configuration - does not have the configuration section + return tunnel - """ - for local_name, (config_name, multiple, default) in mapping.items(): - if multiple: - self.get_values(local_name, config_name, default) - else: - self.get_value(local_name, config_name, default) +def verify(tunnel): + if 'deleted' in tunnel: + verify_bridge_delete(tunnel) - def remove_default(self,*options): - """ - remove all the values which were not changed from the default - """ - for option in options: - if not self._conf.exists(option): - del self.options[option] - continue + if 'nhrp' in tunnel and tunnel['ifname'] in tunnel['nhrp']: + raise ConfigError('Tunnel used for NHRP, it can not be deleted!') - if self._conf.return_value(option) == self.default[option]: - del self.options[option] - continue + return None - if self._conf.return_values(option) == self.default[option]: - del self.options[option] - continue + if 'encapsulation' not in tunnel: + raise ConfigError('Must configure the tunnel encapsulation for '\ + '{ifname}!'.format(**tunnel)) - def as_dict(self, lpath): - l = self._conf.get_level() - self._conf.set_level([]) - d = self._conf.get_config_dict(lpath) - # XXX: that not what I would have expected from get_config_dict - if lpath: - d = d[lpath[-1]] - # XXX: it should have provided me the content and not the key - self._conf.set_level(l) - return d + verify_mtu_ipv6(tunnel) + verify_address(tunnel) + verify_vrf(tunnel) - def to_api(self): - """ - provide a dictionary with the generated data for the configuration - options: the configuration value for the key - changes: per key how they changed from the previous configuration - actions: per changes all the options which were changed - """ - # as we have to use a dict() for the API for verify and apply the options - return { - 'options': self.options, - 'changes': self.changes, - 'actions': self.actions, - } + if 'local_ip' not in tunnel and 'dhcp_interface' not in tunnel: + raise ConfigError('local-ip is mandatory for tunnel') + if 'remote_ip' not in tunnel and tunnel['encapsulation'] != 'gre': + raise ConfigError('remote-ip is mandatory for tunnel') -default_config_data = { - # interface definition - 'vrf': '', - 'addresses-add': [], - 'addresses-del': [], - 'state': 'up', - 'dhcp-interface': '', - 'link_detect': 1, - 'ip': False, - 'ipv6': False, - 'nhrp': [], - 'arp_filter': 1, - 'arp_accept': 0, - 'arp_announce': 0, - 'arp_ignore': 0, - 'ipv6_accept_ra': 1, - 'ipv6_autoconf': 0, - 'ipv6_forwarding': 1, - 'ipv6_dad_transmits': 1, - # internal - 'interfaces': [], - 'tunnel': {}, - 'bridge': '', - # the following names are exactly matching the name - # for the ip command and must not be changed - 'ifname': '', - 'type': '', - 'alias': '', - 'mtu': '1476', - 'local': '', - 'remote': '', - 'dev': '', - 'multicast': 'disable', - 'allmulticast': 'disable', - 'ttl': '255', - 'tos': 'inherit', - 'key': '', - 'encaplimit': '4', - 'flowlabel': 'inherit', - 'hoplimit': '64', - 'tclass': 'inherit', - '6rd-prefix': '', - '6rd-relay-prefix': '', -} + if {'local_ip', 'dhcp_interface'} <= set(tunnel): + raise ConfigError('Can not use both local-ip and dhcp-interface') + if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: + error_ipv6 = 'Encapsulation mode requires IPv6' + if 'local_ip' in tunnel and not is_ipv6(tunnel['local_ip']): + raise ConfigError(f'{error_ipv6} local-ip') -# dict name -> config name, multiple values, default -mapping = { - 'type': ('encapsulation', False, None), - 'alias': ('description', False, None), - 'mtu': ('mtu', False, None), - 'local': ('local-ip', False, None), - 'remote': ('remote-ip', False, None), - 'multicast': ('multicast', False, None), - 'dev': ('source-interface', False, None), - 'ttl': ('parameters ip ttl', False, None), - 'tos': ('parameters ip tos', False, None), - 'key': ('parameters ip key', False, None), - 'encaplimit': ('parameters ipv6 encaplimit', False, None), - 'flowlabel': ('parameters ipv6 flowlabel', False, None), - 'hoplimit': ('parameters ipv6 hoplimit', False, None), - 'tclass': ('parameters ipv6 tclass', False, None), - '6rd-prefix': ('6rd-prefix', False, None), - '6rd-relay-prefix': ('6rd-relay-prefix', False, None), - 'dhcp-interface': ('dhcp-interface', False, None), - 'state': ('disable', False, 'down'), - 'link_detect': ('disable-link-detect', False, 2), - 'vrf': ('vrf', False, None), - 'addresses': ('address', True, None), - 'arp_filter': ('ip disable-arp-filter', False, 0), - 'arp_accept': ('ip enable-arp-accept', False, 1), - 'arp_announce': ('ip enable-arp-announce', False, 1), - 'arp_ignore': ('ip enable-arp-ignore', False, 1), - 'ipv6_autoconf': ('ipv6 address autoconf', False, 1), - 'ipv6_forwarding': ('ipv6 disable-forwarding', False, 0), - 'ipv6_dad_transmits:': ('ipv6 dup-addr-detect-transmits', False, None) -} + if 'remote_ip' in tunnel and not is_ipv6(tunnel['remote_ip']): + raise ConfigError(f'{error_ipv6} remote-ip') + else: + error_ipv4 = 'Encapsulation mode requires IPv4' + if 'local_ip' in tunnel and not is_ipv4(tunnel['local_ip']): + raise ConfigError(f'{error_ipv4} local-ip') + + if 'remote_ip' in tunnel and not is_ipv4(tunnel['remote_ip']): + raise ConfigError(f'{error_ipv4} remote-ip') + + if tunnel['encapsulation'] in ['sit', 'gre-bridge']: + if 'source_interface' in tunnel: + raise ConfigError('Option source-interface can not be used with ' \ + 'encapsulation "sit" or "gre-bridge"') + elif tunnel['encapsulation'] == 'gre': + if 'local_ip' in tunnel and is_ipv6(tunnel['local_ip']): + raise ConfigError('Can not use local IPv6 address is for mGRE tunnels') + +def generate(tunnel): + return None +def apply(tunnel): + if 'deleted' in tunnel or 'encapsulation_changed' in tunnel: + if tunnel['ifname'] in interfaces(): + tmp = Interface(tunnel['ifname']) + tmp.remove() + if 'deleted' in tunnel: + return None -def get_class (options): dispatch = { 'gre': GREIf, 'gre-bridge': GRETapIf, @@ -373,353 +143,52 @@ def get_class (options): 'sit': SitIf, } - kls = dispatch[options['type']] - if options['type'] == 'gre' and not options['remote'] \ - and not options['key'] and not options['multicast']: - # will use GreTapIf on GreIf deletion but it does not matter - return GRETapIf - elif options['type'] == 'sit' and options['6rd-prefix']: - # will use SitIf on Sit6RDIf deletion but it does not matter - return Sit6RDIf - return kls - -def get_interface_ip (ifname): - if not ifname: - return '' - try: - addrs = Interface(ifname).get_addr() - if addrs: - return addrs[0].split('/')[0] - except Exception: - return '' - -def get_afi (ip): - return IP6 if is_ipv6(ip) else IP4 - -def ip_proto (afi): - return 6 if afi == IP6 else 4 - - -def get_config(config=None): - ifname = os.environ.get('VYOS_TAGNODE_VALUE','') - if not ifname: - raise ConfigError('Interface not specified') - - if config: - config = config - else: - config = Config() - - conf = ConfigurationState(config, ['interfaces', 'tunnel ', ifname], default_config_data) - options = conf.options - changes = conf.changes - options['ifname'] = ifname - - if changes['section'] == 'delete': - conf.get_effective('type', mapping['type'][0]) - config.set_level(['protocols', 'nhrp', 'tunnel']) - options['nhrp'] = config.list_nodes('') - return conf.to_api() - - # load all the configuration option according to the mapping - conf.load(mapping) - - # remove default value if not set and not required - afi_local = get_afi(options['local']) - if afi_local == IP6: - conf.remove_default('ttl', 'tos', 'key') - if afi_local == IP4: - conf.remove_default('encaplimit', 'flowlabel', 'hoplimit', 'tclass') - - # if the local-ip is not set, pick one from the interface ! - # hopefully there is only one, otherwise it will not be very deterministic - # at time of writing the code currently returns ipv4 before ipv6 in the list - - # XXX: There is no way to trigger an update of the interface source IP if - # XXX: the underlying interface IP address does change, I believe this - # XXX: limit/issue is present in vyatta too - - if not options['local'] and options['dhcp-interface']: - # XXX: This behaviour changes from vyatta which would return 127.0.0.1 if - # XXX: the interface was not DHCP. As there is no easy way to find if an - # XXX: interface is using DHCP, and using this feature to get 127.0.0.1 - # XXX: makes little sense, I feel the change in behaviour is acceptable - picked = get_interface_ip(options['dhcp-interface']) - if picked == '': - picked = '127.0.0.1' - print('Could not get an IP address from {dhcp-interface} using 127.0.0.1 instead') - options['local'] = picked - options['dhcp-interface'] = '' - - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if options['ipv6_autoconf'] or 'dhcpv6' in options['addresses-add']: - options['ipv6_accept_ra'] = 2 - - # allmulticast fate is linked to multicast - options['allmulticast'] = options['multicast'] - - # check that per encapsulation all local-remote pairs are unique - ct = conf.as_dict(['interfaces', 'tunnel']) - options['tunnel'] = {} - - # check for bridges - tmp = is_member(config, ifname, 'bridge') - if tmp: options['bridge'] = next(iter(tmp)) - options['interfaces'] = interfaces() - - for name in ct: - tunnel = ct[name] - encap = tunnel.get('encapsulation', '') - local = tunnel.get('local-ip', '') - if not local: - local = get_interface_ip(tunnel.get('dhcp-interface', '')) - remote = tunnel.get('remote-ip', '<unset>') - pair = f'{local}-{remote}' - options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1 - - return conf.to_api() - - -def verify(conf): - options = conf['options'] - changes = conf['changes'] - actions = conf['actions'] - - ifname = options['ifname'] - iftype = options['type'] - - if changes['section'] == 'delete': - if ifname in options['nhrp']: - raise ConfigError(( - f'Cannot delete interface tunnel {iftype} {ifname}, ' - 'it is used by NHRP')) - - if options['bridge']: - raise ConfigError(( - f'Cannot delete interface "{options["ifname"]}" as it is a ' - f'member of bridge "{options["bridge"]}"!')) - - # done, bail out early - return None - - # tunnel encapsulation checks - - if not iftype: - raise ConfigError(f'Must provide an "encapsulation" for tunnel {iftype} {ifname}') - - if changes['type'] in ('modify', 'delete'): - # TODO: we could now deal with encapsulation modification by deleting / recreating - raise ConfigError(f'Encapsulation can only be set at tunnel creation for tunnel {iftype} {ifname}') - - if iftype != 'sit' and options['6rd-prefix']: - # XXX: should be able to remove this and let the definition catch it - print(f'6RD can only be configured for sit interfaces not tunnel {iftype} {ifname}') - - # what are the tunnel options we can set / modified / deleted - - kls = get_class(options) - valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state'] - valid += ['arp_filter', 'arp_accept', 'arp_announce', 'arp_ignore'] - valid += ['ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'] - - if changes['section'] == 'create': - valid.extend(['type',]) - valid.extend([o for o in kls.options if o not in kls.updates]) - - for create in actions['create']: - if create not in valid: - raise ConfigError(f'Can not set "{create}" for tunnel {iftype} {ifname} at tunnel creation') - - for modify in actions['modify']: - if modify not in valid: - raise ConfigError(f'Can not modify "{modify}" for tunnel {iftype} {ifname}. it must be set at tunnel creation') - - for delete in actions['delete']: - if delete in kls.required: - raise ConfigError(f'Can not remove "{delete}", it is an mandatory option for tunnel {iftype} {ifname}') - - # tunnel information - - tun_local = options['local'] - afi_local = get_afi(tun_local) - tun_remote = options['remote'] or tun_local - afi_remote = get_afi(tun_remote) - tun_ismgre = iftype == 'gre' and not options['remote'] - tun_is6rd = iftype == 'sit' and options['6rd-prefix'] - tun_dev = options['dev'] - - # incompatible options - - if not tun_local and not options['dhcp-interface'] and not tun_is6rd: - raise ConfigError(f'Must configure either local-ip or dhcp-interface for tunnel {iftype} {ifname}') - - if tun_local and options['dhcp-interface']: - raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}') - - if tun_dev and iftype in ('gre-bridge', 'sit'): - raise ConfigError(f'source interface can not be used with {iftype} {ifname}') - - # tunnel endpoint - - if afi_local != afi_remote: - raise ConfigError(f'IPv4/IPv6 mismatch between local-ip and remote-ip for tunnel {iftype} {ifname}') - - if afi_local != kls.tunnel: - version = 4 if tun_local == IP4 else 6 - raise ConfigError(f'Invalid IPv{version} local-ip for tunnel {iftype} {ifname}') - - ipv4_count = len([ip for ip in options['addresses-add'] if is_ipv4(ip)]) - ipv6_count = len([ip for ip in options['addresses-add'] if is_ipv6(ip)]) - - if tun_ismgre and afi_local == IP6: - raise ConfigError(f'Using an IPv6 address is forbidden for mGRE tunnels such as tunnel {iftype} {ifname}') - - # check address family use - # checks are not enforced (but ip command failing) for backward compatibility - - if ipv4_count and not IP4 in kls.ip: - print(f'Should not use IPv4 addresses on tunnel {iftype} {ifname}') - - if ipv6_count and not IP6 in kls.ip: - print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}') - - # vrf check - if options['vrf']: - if options['vrf'] not in options['interfaces']: - raise ConfigError(f'VRF "{options["vrf"]}" does not exist') - - if options['bridge']: - raise ConfigError(( - f'Interface "{options["ifname"]}" cannot be member of VRF ' - f'"{options["vrf"]}" and bridge {options["bridge"]} ' - f'at the same time!')) - - # bridge and address check - if ( options['bridge'] - and ( options['addresses-add'] - or options['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{options["name"]}" ' - f'as it is a member of bridge "{options["bridge"]}"!')) - - # source-interface check - - if tun_dev and tun_dev not in options['interfaces']: - raise ConfigError(f'device "{tun_dev}" does not exist') - - # tunnel encapsulation check - - convert = { - (6, 4, 'gre'): 'ip6gre', - (6, 6, 'gre'): 'ip6gre', - (4, 6, 'ipip'): 'ipip6', - (6, 6, 'ipip'): 'ip6ip6', + # We need to re-map the tunnel encapsulation proto to a valid interface class + encap = tunnel['encapsulation'] + klass = dispatch[encap] + + # This is a special type of interface which needs additional parameters + # when created using iproute2. Instead of passing a ton of arguments, + # use a dictionary provided by the interface class which holds all the + # options necessary. + conf = klass.get_config() + + # Copy/re-assign our dictionary values to values understood by the + # derived _Tunnel classes + mapping = { + # this : get_config() + 'local_ip' : 'local', + 'remote_ip' : 'remote', + 'source_interface' : 'dev', + 'parameters.ip.ttl' : 'ttl', + 'parameters.ip.tos' : 'tos', + 'parameters.ip.key' : 'key', + 'parameters.ipv6.encaplimit' : 'encaplimit' } - iprotos = [] - if ipv4_count: - iprotos.append(4) - if ipv6_count: - iprotos.append(6) - - for iproto in iprotos: - replace = convert.get((kls.tunnel, iproto, iftype), '') - if replace: - raise ConfigError( - f'Using IPv6 address in local-ip or remote-ip is not possible with "encapsulation {iftype}". ' + - f'Use "encapsulation {replace}" for tunnel {iftype} {ifname} instead.' - ) - - # tunnel options - - incompatible = [] - if afi_local == IP6: - incompatible.extend(['ttl', 'tos', 'key',]) - if afi_local == IP4: - incompatible.extend(['encaplimit', 'flowlabel', 'hoplimit', 'tclass']) - - for option in incompatible: - if option in options: - # TODO: raise converted to print as not enforced by vyatta - # raise ConfigError(f'{option} is not valid for tunnel {iftype} {ifname}') - print(f'Using "{option}" is invalid for tunnel {iftype} {ifname}') - - # duplicate tunnel pairs - - pair = '{}-{}'.format(options['local'], options['remote']) - if options['tunnel'].get(iftype, {}).get(pair, 0) > 1: - raise ConfigError(f'More than one tunnel configured for with the same encapulation and IPs for tunnel {iftype} {ifname}') + # Add additional IPv6 options if tunnel is IPv6 aware + if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: + mappingv6 = { + # this : get_config() + 'parameters.ipv6.encaplimit' : 'encaplimit' + } + mapping.update(mappingv6) - return None + for our_key, their_key in mapping.items(): + if dict_search(our_key, tunnel) and their_key in conf: + conf[their_key] = dict_search(our_key, tunnel) + tun = klass(tunnel['ifname'], **conf) + tun.change_options() + tun.update(tunnel) -def generate(gre): return None -def apply(conf): - options = conf['options'] - changes = conf['changes'] - actions = conf['actions'] - kls = get_class(options) - - # extract ifname as otherwise it is duplicated on the interface creation - ifname = options.pop('ifname') - - # only the valid keys for creation of a Interface - config = dict((k, options[k]) for k in kls.options if options[k]) - - # setup or create the tunnel interface if it does not exist - tunnel = kls(ifname, **config) - - if changes['section'] == 'delete': - tunnel.remove() - # The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup - # which identified tunnels type which were not used anymore to remove them - # (ie: gre0, gretap0, etc.) The perl code did however nothing - # This feature is also not implemented yet - return - - # A GRE interface without remote will be mGRE - # if the interface does not suppor the option, it skips the change - for option in tunnel.updates: - if changes['section'] in 'create' and option in tunnel.options: - # it was setup at creation - continue - if not options[option]: - # remote can be set to '' and it would generate an invalide command - continue - tunnel.set_interface(option, options[option]) - - # set other interface properties - for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast', - 'arp_accept', 'arp_filter', 'arp_announce', 'arp_ignore', - 'ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'): - if not options[option]: - # should never happen but better safe - continue - tunnel.set_interface(option, options[option]) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not options['bridge']: - tunnel.set_vrf(options['vrf']) - - # Configure interface address(es) - for addr in options['addresses-del']: - tunnel.del_addr(addr) - for addr in options['addresses-add']: - tunnel.add_addr(addr) - - # now bring it up (or not) - tunnel.set_admin_state(options['state']) - - if __name__ == '__main__': try: c = get_config() - verify(c) generate(c) + verify(c) apply(c) except ConfigError as e: print(e) diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 9bda35d0a..7cfc76aa0 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -80,9 +80,6 @@ def verify(wireguard): raise ConfigError('Wireguard private-key not found! Execute: ' \ '"run generate wireguard [default-keypair|named-keypairs]"') - if 'address' not in wireguard: - raise ConfigError('IP address required!') - if 'peer' not in wireguard: raise ConfigError('At least one Wireguard peer is required!') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 5d723bbfd..d302c7df7 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -261,7 +261,6 @@ def apply(wifi): # Assign WiFi instance configuration parameters to config dict conf['phy'] = wifi['physical_device'] - conf['wds'] = 'on' if 'wds' in wifi else 'off' # Finally create the new interface w = WiFiIf(interface, **conf) |