diff options
46 files changed, 652 insertions, 856 deletions
diff --git a/interface-definitions/include/interface-parameters-flowlabel.xml.i b/interface-definitions/include/interface-parameters-flowlabel.xml.i index ae65c27c9..0723c4b47 100644 --- a/interface-definitions/include/interface-parameters-flowlabel.xml.i +++ b/interface-definitions/include/interface-parameters-flowlabel.xml.i @@ -11,6 +11,5 @@ </constraint> <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage> </properties> - <defaultValue>inherit</defaultValue> </leafNode> <!-- included end --> diff --git a/interface-definitions/include/radius-server-ipv4-ipv6.xml.i b/interface-definitions/include/radius-server-ipv4-ipv6.xml.i index ab3c6d72a..c57d39b6b 100644 --- a/interface-definitions/include/radius-server-ipv4-ipv6.xml.i +++ b/interface-definitions/include/radius-server-ipv4-ipv6.xml.i @@ -26,7 +26,27 @@ #include <include/radius-server-port.xml.i> </children> </tagNode> - #include <include/source-address-ipv4-ipv6.xml.i> + <leafNode name="source-address"> + <properties> + <help>Source IP address used to initiate connection</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + </completionHelp> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 source address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> </children> </node> <!-- included end --> diff --git a/interface-definitions/include/source-address-ipv4-ipv6.xml.i b/interface-definitions/include/source-address-ipv4-ipv6.xml.i index 4da4698c2..004e04f7b 100644 --- a/interface-definitions/include/source-address-ipv4-ipv6.xml.i +++ b/interface-definitions/include/source-address-ipv4-ipv6.xml.i @@ -17,7 +17,6 @@ <validator name="ipv4-address"/> <validator name="ipv6-address"/> </constraint> - <multi/> </properties> </leafNode> <!-- included end --> diff --git a/interface-definitions/include/tunnel-local-remote-ip.xml.i b/interface-definitions/include/tunnel-local-remote-ip.xml.i deleted file mode 100644 index 85c20f482..000000000 --- a/interface-definitions/include/tunnel-local-remote-ip.xml.i +++ /dev/null @@ -1,37 +0,0 @@ -<!-- included start from tunnel-local-remote-ip.xml.i --> -<leafNode name="local-ip"> - <properties> - <help>Local IP address for this tunnel</help> - <valueHelp> - <format>ipv4</format> - <description>Local IPv4 address for this tunnel</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>Local IPv6 address for this tunnel</description> - </valueHelp> - <completionHelp> - <script>${vyos_completion_dir}/list_local_ips.sh --both</script> - </completionHelp> - <constraint> - <validator name="ip-address"/> - </constraint> - </properties> -</leafNode> -<leafNode name="remote-ip"> - <properties> - <help>Remote IP address for this tunnel</help> - <valueHelp> - <format>ipv4</format> - <description>Remote IPv4 address for this tunnel</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>Remote IPv6 address for this tunnel</description> - </valueHelp> - <constraint> - <!-- does it need fixing/changing to be more restrictive ? --> - <validator name="ip-address"/> - </constraint> - </properties> -</leafNode> diff --git a/interface-definitions/include/tunnel-remote.xml.i b/interface-definitions/include/tunnel-remote.xml.i new file mode 100644 index 000000000..d5b50d3f6 --- /dev/null +++ b/interface-definitions/include/tunnel-remote.xml.i @@ -0,0 +1,18 @@ +<!-- included start from tunnel-remote.xml.i --> +<leafNode name="remote"> + <properties> + <help>Tunnel remote address</help> + <valueHelp> + <format>ipv4</format> + <description>Tunnel remote IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Tunnel remote IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> +</leafNode> +<!-- included end --> diff --git a/interface-definitions/interfaces-erspan.xml.in b/interface-definitions/interfaces-erspan.xml.in index e36a64d3a..2394d3534 100644 --- a/interface-definitions/interfaces-erspan.xml.in +++ b/interface-definitions/interfaces-erspan.xml.in @@ -20,7 +20,8 @@ #include <include/interface-disable.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-mtu-64-8024.xml.i> - #include <include/tunnel-local-remote-ip.xml.i> + #include <include/source-address-ipv4-ipv6.xml.i> + #include <include/tunnel-remote.xml.i> <leafNode name="encapsulation"> <properties> <help>Encapsulation of this tunnel interface</help> diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in index 1064b2c18..5894f580c 100644 --- a/interface-definitions/interfaces-geneve.xml.in +++ b/interface-definitions/interfaces-geneve.xml.in @@ -23,18 +23,7 @@ #include <include/interface-ipv6-options.xml.i> #include <include/interface-mac.xml.i> #include <include/interface-mtu-1450-16000.xml.i> - <leafNode name="remote"> - <properties> - <help>Remote address of GENEVE tunnel</help> - <valueHelp> - <format>ipv4</format> - <description>Remote address of GENEVE tunnel</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> + #include <include/tunnel-remote.xml.i> #include <include/vni.xml.i> </children> </tagNode> diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in index 94d78c6dd..3f2e5bb69 100644 --- a/interface-definitions/interfaces-macsec.xml.in +++ b/interface-definitions/interfaces-macsec.xml.in @@ -28,14 +28,18 @@ <properties> <help>Cipher suite used</help> <completionHelp> - <list>gcm-aes-128</list> + <list>gcm-aes-128 gcm-aes-256</list> </completionHelp> <valueHelp> <format>gcm-aes-128</format> <description>Galois/Counter Mode of AES cipher with 128-bit key (default)</description> </valueHelp> + <valueHelp> + <format>gcm-aes-256</format> + <description>Galois/Counter Mode of AES cipher with 256-bit key</description> + </valueHelp> <constraint> - <regex>(gcm-aes-128)</regex> + <regex>^(gcm-aes-128|gcm-aes-256)$</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index 7a97980a2..c2d03c5ea 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -27,7 +27,8 @@ </leafNode> #include <include/interface-ipv4-options.xml.i> #include <include/interface-ipv6-options.xml.i> - #include <include/tunnel-local-remote-ip.xml.i> + #include <include/source-address-ipv4-ipv6.xml.i> + #include <include/tunnel-remote.xml.i> <leafNode name="source-interface"> <properties> <help>Physical Interface used for underlaying traffic</help> @@ -79,15 +80,15 @@ <properties> <help>Encapsulation of this tunnel interface</help> <completionHelp> - <list>gre gre-bridge ip6gre ip6ip6 ipip ipip6 sit</list> + <list>gre gretap 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> + <format>gretap</format> + <description>Generic Routing Encapsulation (virtual L2 tunnel)</description> </valueHelp> <valueHelp> <format>ip6gre</format> @@ -110,9 +111,9 @@ <description>Simple Internet Transition encapsulation</description> </valueHelp> <constraint> - <regex>^(gre|gre-bridge|ip6gre|ip6ip6|ipip|ipip6|sit)$</regex> + <regex>^(gre|gretap|ip6gre|ip6ip6|ipip|ipip6|sit)$</regex> </constraint> - <constraintErrorMessage>Invalid encapsulation, must be one of: gre, gre-bridge, ipip, sit, ipip6, ip6ip6, ip6gre</constraintErrorMessage> + <constraintErrorMessage>Invalid encapsulation, must be one of: gre, gretap, ipip, sit, ipip6, ip6ip6, ip6gre</constraintErrorMessage> </properties> </leafNode> <leafNode name="multicast"> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index 234770971..efe6218e1 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -44,22 +44,43 @@ <leafNode name="mtu"> <defaultValue>1450</defaultValue> </leafNode> - <leafNode name="remote"> + #include <include/tunnel-remote.xml.i> + <node name="parameters"> <properties> - <help>Remote address of VXLAN tunnel</help> - <valueHelp> - <format>ipv4</format> - <description>Remote IPv4 address of VXLAN tunnel</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>Remote IPv6 address of VXLAN tunnel</description> - </valueHelp> - <constraint> - <validator name="ip-address"/> - </constraint> + <help>VXLAN tunnel parameters</help> </properties> - </leafNode> + <children> + <node name="ip"> + <properties> + <help>IPv4 specific tunnel parameters</help> + </properties> + <children> + <leafNode name="df"> + <properties> + <help>Specifies the usage of the do not fragment (DF) bit</help> + <valueless/> + </properties> + </leafNode> + #include <include/interface-parameters-tos.xml.i> + #include <include/interface-parameters-ttl.xml.i> + </children> + </node> + <node name="ipv6"> + <properties> + <help>IPv6 specific tunnel parameters</help> + </properties> + <children> + #include <include/interface-parameters-flowlabel.xml.i> + </children> + </node> + <leafNode name="nolearning"> + <properties> + <help>Do not add unknown addresses into forwarding database</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> <leafNode name="port"> <properties> <help>Destination port of VXLAN tunnel (default: 8472)</help> diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 5a4d14c68..db3e7cc57 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -95,41 +95,42 @@ def verify_tunnel(config): """ from vyos.template import is_ipv4 from vyos.template import is_ipv6 - + if 'encapsulation' not in config: raise ConfigError('Must configure the tunnel encapsulation for '\ '{ifname}!'.format(**config)) - - if 'local_ip' not in config and 'dhcp_interface' not in config: - raise ConfigError('local-ip is mandatory for tunnel') - if 'remote_ip' not in config and config['encapsulation'] != 'gre': - raise ConfigError('remote-ip is mandatory for tunnel') + if 'source_address' not in config and 'dhcp_interface' not in config: + raise ConfigError('source-address is mandatory for tunnel') + + if 'remote' not in config and config['encapsulation'] != 'gre': + raise ConfigError('remote ip address is mandatory for tunnel') - if {'local_ip', 'dhcp_interface'} <= set(config): - raise ConfigError('Can not use both local-ip and dhcp-interface') + if {'source_address', 'dhcp_interface'} <= set(config): + raise ConfigError('Can not use both source-address and dhcp-interface') if config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre', 'ip6erspan']: error_ipv6 = 'Encapsulation mode requires IPv6' - if 'local_ip' in config and not is_ipv6(config['local_ip']): - raise ConfigError(f'{error_ipv6} local-ip') + if 'source_address' in config and not is_ipv6(config['source_address']): + raise ConfigError(f'{error_ipv6} source-address') - if 'remote_ip' in config and not is_ipv6(config['remote_ip']): - raise ConfigError(f'{error_ipv6} remote-ip') + if 'remote' in config and not is_ipv6(config['remote']): + raise ConfigError(f'{error_ipv6} remote') else: error_ipv4 = 'Encapsulation mode requires IPv4' - if 'local_ip' in config and not is_ipv4(config['local_ip']): - raise ConfigError(f'{error_ipv4} local-ip') + if 'source_address' in config and not is_ipv4(config['source_address']): + raise ConfigError(f'{error_ipv4} source-address') - if 'remote_ip' in config and not is_ipv4(config['remote_ip']): - raise ConfigError(f'{error_ipv4} remote-ip') + if 'remote' in config and not is_ipv4(config['remote']): + raise ConfigError(f'{error_ipv4} remote address') - if config['encapsulation'] in ['sit', 'gre-bridge']: + if config['encapsulation'] in ['sit', 'gretap']: if 'source_interface' in config: - raise ConfigError('Option source-interface can not be used with ' \ - 'encapsulation "sit" or "gre-bridge"') + encapsulation = config['encapsulation'] + raise ConfigError(f'Option source-interface can not be used with ' \ + f'encapsulation "{encapsulation}"!') elif config['encapsulation'] == 'gre': - if 'local_ip' in config and is_ipv6(config['local_ip']): + if 'source_address' in config and is_ipv6(config['source_address']): raise ConfigError('Can not use local IPv6 address is for mGRE tunnels') def verify_eapol(config): diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index f7b55c9dd..9d797d7f1 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -30,15 +30,7 @@ from vyos.ifconfig.vxlan import VXLANIf from vyos.ifconfig.wireguard import WireGuardIf from vyos.ifconfig.vtun import VTunIf from vyos.ifconfig.vti import VTIIf -from vyos.ifconfig.pppoe import PPPoEIf -from vyos.ifconfig.tunnel import GREIf -from vyos.ifconfig.tunnel import GRETapIf -from vyos.ifconfig.tunnel import IP6GREIf -from vyos.ifconfig.tunnel import IPIPIf -from vyos.ifconfig.tunnel import IPIP6If -from vyos.ifconfig.tunnel import IP6IP6If -from vyos.ifconfig.tunnel import SitIf -from vyos.ifconfig.tunnel import Sit6RDIf +from vyos.ifconfig.tunnel import TunnelIf from vyos.ifconfig.erspan import ERSpanIf from vyos.ifconfig.erspan import ER6SpanIf from vyos.ifconfig.wireless import WiFiIf diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 709222b09..28b5da3ee 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -31,9 +31,7 @@ class BondIf(Interface): monitoring may be performed. """ - default = { - 'type': 'bond', - } + iftype = 'bond' definition = { **Interface.definition, ** { diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 1bd617a05..69f652547 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -34,10 +34,7 @@ class BridgeIf(Interface): The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. """ - - default = { - 'type': 'bridge', - } + iftype = 'bridge' definition = { **Interface.definition, **{ @@ -283,21 +280,21 @@ class BridgeIf(Interface): if int(vlan_filter): add_vlan = [] cur_vlan_ids = get_vlan_ids(ifname) - + tmp = dict_search('vif', config) if tmp: for vif, vif_config in tmp.items(): add_vlan.append(vif) - + # Remove redundant VLANs from the system for vlan in list_diff(cur_vlan_ids, add_vlan): cmd = f'bridge vlan del dev {ifname} vid {vlan} self' self._cmd(cmd) - + for vlan in add_vlan: cmd = f'bridge vlan add dev {ifname} vid {vlan} self' self._cmd(cmd) - + # VLAN of bridge parent interface is always 1 # VLAN 1 is the default VLAN for all unlabeled packets cmd = f'bridge vlan add dev {ifname} vid 1 pvid untagged self' @@ -337,7 +334,7 @@ class BridgeIf(Interface): native_vlan_id = None allowed_vlan_ids= [] cur_vlan_ids = get_vlan_ids(interface) - + if 'native_vlan' in interface_config: vlan_id = interface_config['native_vlan'] add_vlan.append(vlan_id) @@ -353,12 +350,12 @@ class BridgeIf(Interface): else: add_vlan.append(vlan) allowed_vlan_ids.append(vlan) - + # Remove redundant VLANs from the system for vlan in list_diff(cur_vlan_ids, add_vlan): cmd = f'bridge vlan del dev {interface} vid {vlan} master' self._cmd(cmd) - + for vlan in allowed_vlan_ids: cmd = f'bridge vlan add dev {interface} vid {vlan} master' self._cmd(cmd) diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py index 43136f361..d41dfef47 100644 --- a/python/vyos/ifconfig/control.py +++ b/python/vyos/ifconfig/control.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py index 19ef9d304..0019fc52b 100644 --- a/python/vyos/ifconfig/dummy.py +++ b/python/vyos/ifconfig/dummy.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -23,9 +23,7 @@ class DummyIf(Interface): packets through without actually transmitting them. """ - default = { - 'type': 'dummy', - } + iftype = 'dummy' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/erspan.py b/python/vyos/ifconfig/erspan.py index 50230e14a..9e24cf6cd 100755 --- a/python/vyos/ifconfig/erspan.py +++ b/python/vyos/ifconfig/erspan.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -31,12 +31,7 @@ class _ERSpan(Interface): """ _ERSpan: private base class for ERSPAN tunnels """ - default = { - **Interface.default, - **{ - 'type': 'erspan', - } - } + iftype = 'erspan' definition = { **Interface.definition, **{ @@ -44,18 +39,23 @@ class _ERSpan(Interface): 'prefixes': ['ersp',], }, } - - options = ['local_ip','remote_ip','encapsulation','parameters'] - + def __init__(self,ifname,**config): self.config = deepcopy(config) if config else {} super().__init__(ifname, **self.config) - + def change_options(self): pass - + 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 @@ -63,10 +63,9 @@ class _ERSpan(Interface): # as certain parameters can only be changed when the interface is # in admin-down state. This ensures the link does not flap during # reconfiguration. - super().update(config) state = 'down' if 'disable' in config else 'up' self.set_admin_state(state) - + def _create(self): pass @@ -74,15 +73,15 @@ class ERSpanIf(_ERSpan): """ ERSpanIf: private base class for ERSPAN Over GRE and IPv4 tunnels """ - + def _create(self): ifname = self.config['ifname'] - local_ip = self.config['local_ip'] - remote_ip = self.config['remote_ip'] + source_address = self.config['source_address'] + remote = self.config['remote'] key = self.config['parameters']['ip']['key'] version = self.config['parameters']['version'] - command = f'ip link add dev {ifname} type erspan local {local_ip} remote {remote_ip} seq key {key} erspan_ver {version}' - + command = f'ip link add dev {ifname} type erspan local {source_address} remote {remote} seq key {key} erspan_ver {version}' + if int(version) == 1: idx=dict_search('parameters.erspan.idx',self.config) if idx: @@ -94,24 +93,24 @@ class ERSpanIf(_ERSpan): hwid=dict_search('parameters.erspan.hwid',self.config) if hwid: command += f' erspan_hwid {hwid}' - + ttl = dict_search('parameters.ip.ttl',self.config) if ttl: command += f' ttl {ttl}' tos = dict_search('parameters.ip.tos',self.config) if tos: command += f' tos {tos}' - + self._cmd(command) - + def change_options(self): ifname = self.config['ifname'] - local_ip = self.config['local_ip'] - remote_ip = self.config['remote_ip'] + source_address = self.config['source_address'] + remote = self.config['remote'] key = self.config['parameters']['ip']['key'] version = self.config['parameters']['version'] - command = f'ip link set dev {ifname} type erspan local {local_ip} remote {remote_ip} seq key {key} erspan_ver {version}' - + command = f'ip link set dev {ifname} type erspan local {source_address} remote {remote} seq key {key} erspan_ver {version}' + if int(version) == 1: idx=dict_search('parameters.erspan.idx',self.config) if idx: @@ -123,29 +122,29 @@ class ERSpanIf(_ERSpan): hwid=dict_search('parameters.erspan.hwid',self.config) if hwid: command += f' erspan_hwid {hwid}' - + ttl = dict_search('parameters.ip.ttl',self.config) if ttl: command += f' ttl {ttl}' tos = dict_search('parameters.ip.tos',self.config) if tos: command += f' tos {tos}' - + self._cmd(command) class ER6SpanIf(_ERSpan): """ ER6SpanIf: private base class for ERSPAN Over GRE and IPv6 tunnels """ - + def _create(self): ifname = self.config['ifname'] - local_ip = self.config['local_ip'] - remote_ip = self.config['remote_ip'] + source_address = self.config['source_address'] + remote = self.config['remote'] key = self.config['parameters']['ip']['key'] version = self.config['parameters']['version'] - command = f'ip link add dev {ifname} type ip6erspan local {local_ip} remote {remote_ip} seq key {key} erspan_ver {version}' - + command = f'ip link add dev {ifname} type ip6erspan local {source_address} remote {remote} seq key {key} erspan_ver {version}' + if int(version) == 1: idx=dict_search('parameters.erspan.idx',self.config) if idx: @@ -157,24 +156,24 @@ class ER6SpanIf(_ERSpan): hwid=dict_search('parameters.erspan.hwid',self.config) if hwid: command += f' erspan_hwid {hwid}' - + ttl = dict_search('parameters.ip.ttl',self.config) if ttl: command += f' ttl {ttl}' tos = dict_search('parameters.ip.tos',self.config) if tos: command += f' tos {tos}' - + self._cmd(command) - + def change_options(self): ifname = self.config['ifname'] - local_ip = self.config['local_ip'] - remote_ip = self.config['remote_ip'] + source_address = self.config['source_address'] + remote = self.config['remote'] key = self.config['parameters']['ip']['key'] version = self.config['parameters']['version'] - command = f'ip link set dev {ifname} type ip6erspan local {local_ip} remote {remote_ip} seq key {key} erspan_ver {version}' - + command = f'ip link set dev {ifname} type ip6erspan local {source_address} remote {remote} seq key {key} erspan_ver {version}' + if int(version) == 1: idx=dict_search('parameters.erspan.idx',self.config) if idx: @@ -186,5 +185,5 @@ class ER6SpanIf(_ERSpan): hwid=dict_search('parameters.erspan.hwid',self.config) if hwid: command += f' erspan_hwid {hwid}' - + self._cmd(command) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 547b54b84..aca0aeead 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -26,10 +26,7 @@ class EthernetIf(Interface): """ Abstraction of a Linux Ethernet Interface """ - - default = { - 'type': 'ethernet', - } + iftype = 'ethernet' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py index 5c4597be8..1b3ee0dc9 100644 --- a/python/vyos/ifconfig/geneve.py +++ b/python/vyos/ifconfig/geneve.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -26,14 +26,7 @@ class GeneveIf(Interface): https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve https://lwn.net/Articles/644938/ """ - - default = { - 'type': 'geneve', - 'vni': 0, - 'remote': '', - } - options = Interface.options + \ - ['vni', 'remote'] + iftype = 'geneve' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py index a6e566d87..db7d2b6b4 100644 --- a/python/vyos/ifconfig/input.py +++ b/python/vyos/ifconfig/input.py @@ -17,9 +17,6 @@ from vyos.ifconfig.interface import Interface @Interface.register class InputIf(Interface): - default = { - 'type': '', - } definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 4bdabd432..9c0ee2ab1 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -60,7 +60,6 @@ class Interface(Control): options = ['debug', 'create'] required = [] default = { - 'type': '', 'debug': True, 'create': True, } @@ -232,26 +231,21 @@ class Interface(Control): >>> from vyos.ifconfig import Interface >>> i = Interface('eth0') """ + self.config = deepcopy(kargs) + self.config['ifname'] = self.ifname = ifname - self.config = deepcopy(self.default) - for k in self.options: - if k in kargs: - self.config[k] = kargs[k] - - # make sure the ifname is the first argument and not from the dict - self.config['ifname'] = ifname self._admin_state_down_cnt = 0 # we must have updated config before initialising the Interface super().__init__(**kargs) - self.ifname = ifname if not self.exists(ifname): - # Any instance of Interface, such as Interface('eth0') - # can be used safely to access the generic function in this class - # as 'type' is unset, the class can not be created - if not self.config['type']: + # Any instance of Interface, such as Interface('eth0') can be used + # safely to access the generic function in this class as 'type' is + # unset, the class can not be created + if not self.iftype: raise Exception(f'interface "{ifname}" not found') + self.config['type'] = self.iftype # Should an Instance of a child class (EthernetIf, DummyIf, ..) # be required, then create should be set to False to not accidentally create it. @@ -1300,17 +1294,7 @@ class Interface(Control): class VLANIf(Interface): """ Specific class which abstracts 802.1q and 802.1ad (Q-in-Q) VLAN interfaces """ - default = { - 'type': 'vlan', - 'source_interface': '', - 'vlan_id': '', - 'protocol': '', - 'ingress_qos': '', - 'egress_qos': '', - } - - options = Interface.options + \ - ['source_interface', 'vlan_id', 'protocol', 'ingress_qos', 'egress_qos'] + iftype = 'vlan' def remove(self): """ @@ -1339,11 +1323,11 @@ class VLANIf(Interface): return cmd = 'ip link add link {source_interface} name {ifname} type vlan id {vlan_id}' - if self.config['protocol']: + if 'protocol' in self.config: cmd += ' protocol {protocol}' - if self.config['ingress_qos']: + if 'ingress_qos' in self.config: cmd += ' ingress-qos-map {ingress_qos}' - if self.config['egress_qos']: + if 'egress_qos' in self.config: cmd += ' egress-qos-map {egress_qos}' self._cmd(cmd.format(**self.config)) diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py index 8ed3d5afb..34f8cd4d3 100644 --- a/python/vyos/ifconfig/l2tpv3.py +++ b/python/vyos/ifconfig/l2tpv3.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -24,19 +24,7 @@ class L2TPv3If(Interface): either hot standby or load balancing services. Additionally, link integrity monitoring may be performed. """ - - default = { - 'type': 'l2tp', - 'peer_tunnel_id': '', - 'local_port': 0, - 'remote_port': 0, - 'encapsulation': 'udp', - 'local_address': '', - 'remote_address': '', - 'session_id': '', - 'tunnel_id': '', - 'peer_session_id': '' - } + iftype = 'l2tp' definition = { **Interface.definition, **{ @@ -45,20 +33,16 @@ class L2TPv3If(Interface): 'bridgeable': True, } } - options = Interface.options + \ - ['tunnel_id', 'peer_tunnel_id', 'local_port', 'remote_port', - 'encapsulation', 'local_address', 'remote_address', 'session_id', - 'peer_session_id'] def _create(self): # create tunnel interface cmd = 'ip l2tp add tunnel tunnel_id {tunnel_id}' cmd += ' peer_tunnel_id {peer_tunnel_id}' - cmd += ' udp_sport {local_port}' - cmd += ' udp_dport {remote_port}' + cmd += ' udp_sport {source_port}' + cmd += ' udp_dport {destination_port}' cmd += ' encap {encapsulation}' - cmd += ' local {local_address}' - cmd += ' remote {remote_address}' + cmd += ' local {local_ip}' + cmd += ' remote {remote_ip}' self._cmd(cmd.format(**self.config)) # setup session @@ -82,20 +66,20 @@ class L2TPv3If(Interface): >>> i.remove() """ - if self.exists(self.config['ifname']): + if self.exists(self.ifname): # interface is always A/D down. It needs to be enabled explicitly self.set_admin_state('down') - if self.config['tunnel_id'] and self.config['session_id']: + if {'tunnel_id', 'session_id'} <= set(self.config): cmd = 'ip l2tp del session tunnel_id {tunnel_id}' cmd += ' session_id {session_id}' self._cmd(cmd.format(**self.config)) - if self.config['tunnel_id']: + if 'tunnel_id' in self.config: cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}' self._cmd(cmd.format(**self.config)) - - + + 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 diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py index 0e632d826..d323feed8 100644 --- a/python/vyos/ifconfig/loopback.py +++ b/python/vyos/ifconfig/loopback.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -22,9 +22,8 @@ class LoopbackIf(Interface): uses to communicate with itself. """ _persistent_addresses = ['127.0.0.1/8', '::1/128'] - default = { - 'type': 'loopback', - } + iftype = 'loopback' + definition = { **Interface.definition, **{ @@ -33,9 +32,6 @@ class LoopbackIf(Interface): 'bridgeable': True, } } - - name = 'loopback' - def remove(self): """ Loopback interface can not be deleted from operating system. We can diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py index 456686ea6..c15273080 100644 --- a/python/vyos/ifconfig/macsec.py +++ b/python/vyos/ifconfig/macsec.py @@ -1,4 +1,4 @@ -# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2021 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 @@ -27,12 +27,7 @@ class MACsecIf(Interface): other security solutions such as IPsec (layer 3) or TLS (layer 4), as all those solutions are used for their own specific use cases. """ - - default = { - 'type': 'macsec', - 'security_cipher': '', - 'source_interface': '' - } + iftype = 'macsec' definition = { **Interface.definition, **{ @@ -40,8 +35,6 @@ class MACsecIf(Interface): 'prefixes': ['macsec', ], }, } - options = Interface.options + \ - ['security_cipher', 'source_interface'] def _create(self): """ @@ -49,9 +42,9 @@ class MACsecIf(Interface): down by default. """ # create tunnel interface - cmd = 'ip link add link {source_interface} {ifname} type {type}' - cmd += ' cipher {security_cipher}' - self._cmd(cmd.format(**self.config)) + cmd = 'ip link add link {source_interface} {ifname} type {type}'.format(**self.config) + cmd += f' cipher {self.config["security"]["cipher"]}' + self._cmd(cmd) # interface is always A/D down. It needs to be enabled explicitly self.set_admin_state('down') diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index 2447fec77..73e2eea9e 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -20,13 +20,7 @@ class MACVLANIf(Interface): """ Abstraction of a Linux MACvlan interface """ - - default = { - 'type': 'macvlan', - 'address': '', - 'source_interface': '', - 'mode': '', - } + iftype = 'macvlan' definition = { **Interface.definition, **{ @@ -34,17 +28,10 @@ class MACVLANIf(Interface): 'prefixes': ['peth', ], }, } - options = Interface.options + \ - ['source_interface', 'mode'] def _create(self): # please do not change the order when assembling the command - cmd = 'ip link add {ifname}' - if self.config['source_interface']: - cmd += ' link {source_interface}' - cmd += ' type macvlan' - if self.config['mode']: - cmd += ' mode {mode}' + cmd = 'ip link add {ifname} link {source_interface} type {type} mode {mode}' self._cmd(cmd.format(**self.config)) def set_mode(self, mode): diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py deleted file mode 100644 index 787245696..000000000 --- a/python/vyos/ifconfig/pppoe.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 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 -# 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 <http://www.gnu.org/licenses/>. - - -from vyos.ifconfig.interface import Interface - - -@Interface.register -class PPPoEIf(Interface): - default = { - 'type': 'pppoe', - } - definition = { - **Interface.definition, - **{ - 'section': 'pppoe', - 'prefixes': ['pppoe', ], - }, - } - - # stub this interface is created in the configure script - - def _create(self): - # we can not create this interface as it is managed outside - pass - - def _delete(self): - # we can not create this interface as it is managed outside - pass diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py index 4320bf8bc..bb940b0cf 100644 --- a/python/vyos/ifconfig/tunnel.py +++ b/python/vyos/ifconfig/tunnel.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -16,13 +16,12 @@ # https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/ # 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.util import dict_search from vyos.validate import assert_list def enable_to_on(value): @@ -32,11 +31,10 @@ def enable_to_on(value): return 'off' raise ValueError(f'expect enable or disable but got "{value}"') - @Interface.register -class _Tunnel(Interface): +class TunnelIf(Interface): """ - _Tunnel: private base class for tunnels + Tunnel: private base class for tunnels https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/tunnel.c https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ip6tunnel.c """ @@ -48,16 +46,30 @@ class _Tunnel(Interface): }, } - default = { - 'local' : '', - 'remote': '', - 'dev' : '', - 'ttl' : '', - 'tos' : '', - 'key' : '', - 'raw' : '', + # This table represents a mapping from VyOS internal config dict to + # arguments used by iproute2. For more information please refer to: + # - https://man7.org/linux/man-pages/man8/ip-link.8.html + # - https://man7.org/linux/man-pages/man8/ip-tunnel.8.html + mapping = { + 'source_address' : 'local', + 'source_interface' : 'dev', + 'remote' : 'remote', + 'parameters.ip.key' : 'key', + 'parameters.ip.tos' : 'tos', + 'parameters.ip.ttl' : 'ttl', + } + mapping_ipv4 = { + 'parameters.ip.key' : 'key', + 'parameters.ip.no_pmtu_discovery' : 'nopmtudisc', + 'parameters.ip.tos' : 'tos', + 'parameters.ip.ttl' : 'ttl', + } + mapping_ipv6 = { + 'parameters.ipv6.encaplimit' : 'encaplimit', + 'parameters.ipv6.flowlabel' : 'flowlabel', + 'parameters.ipv6.hoplimit' : 'hoplimit', + 'parameters.ipv6.tclass' : 'tclass', } - options = Interface.options + list(default.keys()) # TODO: This is surely used for more than tunnels # TODO: could be refactored elsewhere @@ -76,28 +88,67 @@ class _Tunnel(Interface): }, } } - _create_cmd = 'ip tunnel add {ifname} mode {type}' + + def __init__(self, ifname, **kargs): + self.iftype = kargs['encapsulation'] + super().__init__(ifname, **kargs) + + # The gretap interface has the possibility to act as L2 bridge + if self.iftype == 'gretap': + # no multicast, ttl or tos for gretap + self.definition = { + **TunnelIf.definition, + **{ + 'bridgeable': True, + }, + } + def _create(self): - # add " option-name option-name-value ..." for all options set - options = ' '.join(['{} {}'.format(k, self.config[k]) - for k,v in self.config.items() if v and k not in - ['ifname', 'type', 'raw']]) - if 'raw' in self.config: - options += ' ' + ' '.join(self.config['raw']) - self._cmd('{} {}'.format(self._create_cmd.format(**self.config), options)) - self.set_admin_state('down') + if self.config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: + mapping = { **self.mapping, **self.mapping_ipv6 } + else: + mapping = { **self.mapping, **self.mapping_ipv4 } + + cmd = 'ip tunnel add {ifname} mode {encapsulation}' + if self.iftype == 'gretap': + cmd = 'ip link add name {ifname} type {encapsulation}' + for vyos_key, iproute2_key in mapping.items(): + # dict_search will return an empty dict "{}" for valueless nodes like + # "parameters.nolearning" - thus we need to test the nodes existence + # by using isinstance() + tmp = dict_search(vyos_key, self.config) + if isinstance(tmp, dict): + cmd += f' {iproute2_key}' + elif tmp != None: + cmd += f' {iproute2_key} {tmp}' + + self._cmd(cmd.format(**self.config)) - def change_options(self): - change = 'ip tunnel change {ifname} mode {type}' + self.set_admin_state('down') - # add " option-name option-name-value ..." for all options set - options = ' '.join(['{} {}'.format(k, self.config[k]) - for k,v in self.config.items() if v and k not in - ['ifname', 'type', 'raw']]) - if 'raw' in self.config: - options += ' ' + ' '.join(self.config['raw']) - self._cmd('{} {}'.format(change.format(**self.config), options)) + def _change_options(self): + # gretap interfaces do not support changing any parameter + if self.iftype == 'gretap': + return + + if self.config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: + mapping = { **self.mapping, **self.mapping_ipv6 } + else: + mapping = { **self.mapping, **self.mapping_ipv4 } + + cmd = 'ip tunnel change {ifname} mode {encapsulation}' + for vyos_key, iproute2_key in mapping.items(): + # dict_search will return an empty dict "{}" for valueless nodes like + # "parameters.nolearning" - thus we need to test the nodes existence + # by using isinstance() + tmp = dict_search(vyos_key, self.config) + if isinstance(tmp, dict): + cmd += f' {iproute2_key}' + elif tmp != None: + cmd += f' {iproute2_key} {tmp}' + + self._cmd(cmd.format(**self.config)) def get_mac(self): """ @@ -128,6 +179,8 @@ class _Tunnel(Interface): 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. """ + # Adjust iproute2 tunnel parameters if necessary + self._change_options() # call base class first super().update(config) @@ -141,159 +194,3 @@ class _Tunnel(Interface): # reconfiguration. state = 'down' if 'disable' in config else 'up' self.set_admin_state(state) - -class GREIf(_Tunnel): - """ - GRE: Generic Routing Encapsulation - - For more information please refer to: - RFC1701, RFC1702, RFC2784 - https://tools.ietf.org/html/rfc2784 - https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre.c - """ - - default = { - **_Tunnel.default, - **{ - 'type': 'gre', - 'raw' : ['pmtudisc'], # parameters that we can pass raw to ip command - }, - } - -# GreTap also called GRE Bridge -class GRETapIf(_Tunnel): - """ - GRETapIF: GreIF using TAP instead of TUN - - https://en.wikipedia.org/wiki/TUN/TAP - """ - # no multicast, ttl or tos for gretap - definition = { - **_Tunnel.definition, - **{ - 'bridgeable': True, - }, - } - default = { - 'type': 'gretap', - 'local': '', - 'remote': '', - 'dev': '', - 'raw' : ['pmtudisc'], # parameters that we can pass raw to ip command - } - - _create_cmd = 'ip link add name {ifname} type {type}' - - def change_options(self): - pass - -class IP6GREIf(_Tunnel): - """ - IP6Gre: IPv6 Support for Generic Routing Encapsulation (GRE) - - For more information please refer to: - https://tools.ietf.org/html/rfc7676 - https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre6.c - """ - default = { - **_Tunnel.default, - **{ - 'type': 'ip6gre', - 'encaplimit': '', - 'hoplimit': '', - 'tclass': '', - 'flowlabel': '', - }, - } - -class IPIPIf(_Tunnel): - """ - IPIP: IP Encapsulation within IP - - For more information please refer to: - https://tools.ietf.org/html/rfc2003 - """ - # IPIP does not allow to pass multicast, unlike GRE - # but the interface itself can be set with multicast - default = { - **_Tunnel.default, - **{ - 'type': 'ipip', - 'raw' : ['pmtudisc'], # parameters that we can pass raw to ip command - }, - } - -class IPIP6If(_Tunnel): - """ - IPIP6: IPv4 over IPv6 tunnel - - For more information please refer to: - https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_ip6tnl.c - """ - default = { - **_Tunnel.default, - **{ - 'type': 'ipip6', - 'encaplimit': '', - 'hoplimit': '', - 'tclass': '', - 'flowlabel': '', - }, - } - -class IP6IP6If(IPIP6If): - """ - IP6IP6: IPv6 over IPv6 tunnel - - For more information please refer to: - https://tools.ietf.org/html/rfc2473 - """ - default = { - **_Tunnel.default, - **{ - 'type': 'ip6ip6', - }, - } - - -class SitIf(_Tunnel): - """ - Sit: Simple Internet Transition - - For more information please refer to: - https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_iptnl.c - """ - default = { - **_Tunnel.default, - **{ - 'type': 'sit', - }, - } - -class Sit6RDIf(SitIf): - """ - Sit6RDIf: Simple Internet Transition with 6RD - - https://en.wikipedia.org/wiki/IPv6_rapid_deployment - """ - # TODO: check if key can really be used with 6RD - default = { - **_Tunnel.default, - **{ - 'type': '6rd', - '6rd_prefix' : '', - '6rd_relay_prefix' : '', - }, - } - - def _create(self): - # do not call _Tunnel.create, building fully here - - create = 'ip tunnel add {ifname} mode {type} remote {remote}' - self._cmd(create.format(**self.config)) - self.set_interface('state','down') - - set6rd = 'ip tunnel 6rd dev {ifname} 6rd-prefix {6rd-prefix}' - if '6rd-relay-prefix' in self.config: - set6rd += ' 6rd-relay-prefix {6rd-relay-prefix}' - self._cmd(set6rd.format(**self.config)) diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index d0745898c..e2090c889 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -1,4 +1,4 @@ -# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021 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,7 @@ from vyos.ifconfig.interface import Interface @Interface.register class VTIIf(Interface): - default = { - 'type': 'vti', - } + iftype = 'vti' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py index 99a592b3e..8f027cf9d 100644 --- a/python/vyos/ifconfig/vtun.py +++ b/python/vyos/ifconfig/vtun.py @@ -1,4 +1,4 @@ -# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2021 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,10 +17,7 @@ from vyos.ifconfig.interface import Interface @Interface.register class VTunIf(Interface): - default = { - 'type': 'vtun', - 'device_type': 'tun', - } + iftype = 'vtun' definition = { **Interface.definition, **{ @@ -29,7 +26,6 @@ class VTunIf(Interface): 'bridgeable': True, }, } - options = Interface.options + ['device_type'] def _create(self): """ Depending on OpenVPN operation mode the interface is created diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index ad1f605ed..291332a77 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -15,6 +15,7 @@ from vyos import ConfigError from vyos.ifconfig.interface import Interface +from vyos.util import dict_search @Interface.register class VXLANIf(Interface): @@ -38,16 +39,7 @@ class VXLANIf(Interface): https://www.kernel.org/doc/Documentation/networking/vxlan.txt """ - default = { - 'type': 'vxlan', - 'group': '', - 'port': 8472, # The Linux implementation of VXLAN pre-dates - # the IANA's selection of a standard destination port - 'remote': '', - 'source_address': '', - 'source_interface': '', - 'vni': 0 - } + iftype = 'vxlan' definition = { **Interface.definition, **{ @@ -56,44 +48,35 @@ class VXLANIf(Interface): 'bridgeable': True, } } - options = Interface.options + \ - ['group', 'remote', 'source_interface', 'port', 'vni', 'source_address'] - - mapping = { - 'ifname': 'add', - 'vni': 'id', - 'port': 'dstport', - 'source_address': 'local', - 'source_interface': 'dev', - } def _create(self): - cmdline = ['ifname', 'type', 'vni', 'port'] - - if self.config['source_address']: - cmdline.append('source_address') - - if self.config['remote']: - cmdline.append('remote') - - if self.config['group'] or self.config['source_interface']: - if self.config['group'] and self.config['source_interface']: - cmdline.append('group') - cmdline.append('source_interface') - else: - ifname = self.config['ifname'] - raise ConfigError( - f'VXLAN "{ifname}" is missing mandatory underlay multicast' - 'group or source interface for a multicast network.') - - cmd = 'ip link' - for key in cmdline: - value = self.config.get(key, '') - if not value: - continue - cmd += ' {} {}'.format(self.mapping.get(key, key), value) + # This table represents a mapping from VyOS internal config dict to + # arguments used by iproute2. For more information please refer to: + # - https://man7.org/linux/man-pages/man8/ip-link.8.html + mapping = { + 'source_address' : 'local', + 'source_interface' : 'dev', + 'remote' : 'remote', + 'group' : 'group', + 'parameters.ip.tos' : 'tos', + 'parameters.ip.ttl' : 'ttl', + 'parameters.ipv6.flowlabel' : 'flowlabel', + 'parameters.nolearning' : 'nolearning', + } - self._cmd(cmd) + cmd = 'ip link add {ifname} type {type} id {vni} dstport {port}' + for vyos_key, iproute2_key in mapping.items(): + # dict_search will return an empty dict "{}" for valueless nodes like + # "parameters.nolearning" - thus we need to test the nodes existence + # by using isinstance() + tmp = dict_search(vyos_key, self.config) + if isinstance(tmp, dict): + cmd += f' {iproute2_key}' + elif tmp != None: + cmd += f' {iproute2_key} {tmp}' + + self._cmd(cmd.format(**self.config)) + self.set_admin_state('down') def update(self, config): """ General helper function which works on a dictionary retrived by diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index 9ee798ee8..f377e2b1d 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2021 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 @@ -148,18 +148,7 @@ class WireGuardOperational(Operational): @Interface.register class WireGuardIf(Interface): OperationalClass = WireGuardOperational - - default = { - 'type': 'wireguard', - 'port': 0, - 'private_key': None, - 'pubkey': None, - 'psk': '', - 'allowed_ips': [], - 'fwmark': 0x00, - 'endpoint': None, - 'keepalive': 0 - } + iftype = 'wireguard' definition = { **Interface.definition, **{ @@ -168,9 +157,6 @@ class WireGuardIf(Interface): 'bridgeable': False, } } - options = Interface.options + \ - ['port', 'private_key', 'pubkey', 'psk', - 'allowed_ips', 'fwmark', 'endpoint', 'keepalive'] def get_mac(self): """ diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index 37703d242..6c0eeec44 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -1,4 +1,4 @@ -# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2021 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 @@ -20,11 +20,7 @@ class WiFiIf(Interface): """ Handle WIFI/WLAN interfaces. """ - - default = { - 'type': 'wifi', - 'phy': 'phy0' - } + iftype = 'wifi' definition = { **Interface.definition, **{ @@ -33,14 +29,10 @@ class WiFiIf(Interface): 'bridgeable': True, } } - options = Interface.options + \ - ['phy', 'op_mode'] - def _create(self): # all interfaces will be added in monitor mode - cmd = 'iw phy {phy} interface add {ifname} type monitor' \ - .format(**self.config) - self._cmd(cmd) + cmd = 'iw phy {physical_device} interface add {ifname} type monitor' + self._cmd(cmd.format(**self.config)) # wireless interface is administratively down by default self.set_admin_state('down') @@ -81,14 +73,3 @@ class WiFiIf(Interface): # reconfiguration. state = 'down' if 'disable' in config else 'up' self.set_admin_state(state) - - -@Interface.register -class WiFiModemIf(WiFiIf): - definition = { - **WiFiIf.definition, - **{ - 'section': 'wirelessmodem', - 'prefixes': ['wlm', ], - } - } diff --git a/smoketest/configs/tunnel-broker b/smoketest/configs/tunnel-broker new file mode 100644 index 000000000..54e63abda --- /dev/null +++ b/smoketest/configs/tunnel-broker @@ -0,0 +1,103 @@ +interfaces { + dummy dum0 { + address 192.0.2.0/32 + } + dummy dum1 { + address 192.0.2.1/32 + } + dummy dum2 { + address 192.0.2.2/32 + } + dummy dum3 { + address 192.0.2.3/32 + } + dummy dum4 { + address 192.0.2.4/32 + } + ethernet eth0 { + duplex auto + smp-affinity auto + speed auto + address 172.18.202.10/24 + } + tunnel tun100 { + address 172.16.0.1/30 + encapsulation gre-bridge + local-ip 192.0.2.0 + remote-ip 192.0.2.100 + } + tunnel tun200 { + address 172.16.0.5/30 + encapsulation gre + local-ip 192.0.2.1 + remote-ip 192.0.2.101 + } + tunnel tun300 { + address 172.16.0.9/30 + encapsulation ipip + local-ip 192.0.2.2 + remote-ip 192.0.2.102 + } + tunnel tun400 { + address 172.16.0.13/30 + encapsulation gre-bridge + local-ip 192.0.2.3 + remote-ip 192.0.2.103 + } + tunnel tun500 { + address 172.16.0.17/30 + encapsulation gre + local-ip 192.0.2.4 + remote-ip 192.0.2.104 + } +} +protocols { + static { + route 0.0.0.0/0 { + next-hop 172.18.202.1 { + distance 10 + } + } + } +} +system { + config-management { + commit-revisions 100 + } + console { + device ttyS0 { + speed 115200 + } + } + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 + plaintext-password "" + } + } + } + ntp { + server 0.pool.ntp.org { + } + server 1.pool.ntp.org { + } + server 2.pool.ntp.org { + } + } + syslog { + global { + facility all { + level info + } + facility protocols { + level debug + } + } + } +} + +/* Warning: Do not remove the following line. */ +/* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pptp@1:qos@1:quagga@6:snmp@1:ssh@1:system@10:vrrp@2:wanloadbalance@3:webgui@1:webproxy@1:webproxy@2:zone-policy@1" === */ +/* Release version: 1.2.6-S1 */ diff --git a/smoketest/scripts/cli/test_interfaces_erspan.py b/smoketest/scripts/cli/test_interfaces_erspan.py index c180f0a34..d0814f2fb 100755 --- a/smoketest/scripts/cli/test_interfaces_erspan.py +++ b/smoketest/scripts/cli/test_interfaces_erspan.py @@ -27,51 +27,6 @@ mtu = 1500 def erspan_conf(interface): tmp = cmd(f'ip -d -j link show {interface}') - ''' - [ - { - "ifindex": 17, - "link": null, - "ifname": "ersp0", - "flags": [ - "BROADCAST", - "MULTICAST" - ], - "mtu": 1450, - "qdisc": "noop", - "operstate": "DOWN", - "linkmode": "DEFAULT", - "group": "default", - "txqlen": 1000, - "link_type": "ether", - "address": "22:27:14:7b:0d:79", - "broadcast": "ff:ff:ff:ff:ff:ff", - "promiscuity": 0, - "min_mtu": 68, - "max_mtu": 0, - "linkinfo": { - "info_kind": "erspan", - "info_data": { - "remote": "10.2.2.2", - "local": "10.1.1.1", - "ttl": 0, - "pmtudisc": true, - "ikey": "0.0.0.123", - "okey": "0.0.0.123", - "iseq": true, - "oseq": true, - "erspan_index": 0, - "erspan_ver": 1 - } - }, - "inet6_addr_gen_mode": "eui64", - "num_tx_queues": 1, - "num_rx_queues": 1, - "gso_max_size": 65536, - "gso_max_segs": 65535 - } - ] - ''' return json.loads(tmp)[0] class ERSPanTunnelInterfaceTest(BasicInterfaceTest.BaseTest): @@ -96,8 +51,8 @@ class ERSPanTunnelInterfaceTest(BasicInterfaceTest.BaseTest): key = 123 self.session.set(self._base_path + [interface, 'encapsulation', encapsulation]) - self.session.set(self._base_path + [interface, 'local-ip', self.local_v4]) - self.session.set(self._base_path + [interface, 'remote-ip', self.remote_v4]) + self.session.set(self._base_path + [interface, 'source-address', self.local_v4]) + self.session.set(self._base_path + [interface, 'remote', self.remote_v4]) self.session.set(self._base_path + [interface, 'parameters', 'ip' , 'key', str(key)]) self.session.commit() @@ -107,8 +62,8 @@ class ERSPanTunnelInterfaceTest(BasicInterfaceTest.BaseTest): self.assertEqual(encapsulation, conf['linkinfo']['info_kind']) self.assertEqual(mtu, conf['mtu']) - self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local']) - self.assertEqual(self.remote_v4, conf['linkinfo']['info_data']['remote']) + self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local']) + self.assertEqual(self.remote_v4, conf['linkinfo']['info_data']['remote']) def test_erspan_ipv6(self): @@ -117,8 +72,8 @@ class ERSPanTunnelInterfaceTest(BasicInterfaceTest.BaseTest): key = 123 self.session.set(self._base_path + [interface, 'encapsulation', encapsulation]) - self.session.set(self._base_path + [interface, 'local-ip', self.local_v6]) - self.session.set(self._base_path + [interface, 'remote-ip', self.remote_v6]) + self.session.set(self._base_path + [interface, 'source-address', self.local_v6]) + self.session.set(self._base_path + [interface, 'remote', self.remote_v6]) self.session.set(self._base_path + [interface, 'parameters', 'ip' , 'key', str(key)]) self.session.commit() @@ -128,8 +83,8 @@ class ERSPanTunnelInterfaceTest(BasicInterfaceTest.BaseTest): self.assertEqual(encapsulation, conf['linkinfo']['info_kind']) self.assertEqual(mtu, conf['mtu']) - self.assertEqual(self.local_v6, conf['linkinfo']['info_data']['local']) - self.assertEqual(self.remote_v6, conf['linkinfo']['info_data']['remote']) + self.assertEqual(self.local_v6, conf['linkinfo']['info_data']['local']) + self.assertEqual(self.remote_v6, conf['linkinfo']['info_data']['remote']) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index 3a3e7bff3..d6bef993a 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import re import unittest @@ -22,6 +23,7 @@ from netifaces import interfaces from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section +from vyos.util import cmd from vyos.util import read_file from vyos.util import process_named_running @@ -30,6 +32,16 @@ def get_config_value(interface, key): tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp) return tmp[0] +def get_cipher(interface): + """ Returns the used encapsulation protocol for given interface. + If interface does not exist, None is returned. + """ + if not os.path.exists(f'/sys/class/net/{interface}'): + return None + from json import loads + tmp = loads(cmd(f'ip -d -j link show {interface}'))[0] + return tmp['linkinfo']['info_data']['cipher_suite'].lower() + class MACsecInterfaceTest(BasicInterfaceTest.BaseTest): @classmethod def setUpClass(cls): @@ -107,8 +119,30 @@ class MACsecInterfaceTest(BasicInterfaceTest.BaseTest): # Check for running process self.assertTrue(process_named_running('wpa_supplicant')) - def test_macsec_mandatory_options(self): + def test_macsec_gcm_aes_128(self): interface = 'macsec1' + cipher = 'gcm-aes-128' + self.session.set(self._base_path + [interface]) + + # check validate() - source interface is mandatory + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(self._base_path + [interface, 'source-interface', 'eth0']) + + # check validate() - cipher is mandatory + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(self._base_path + [interface, 'security', 'cipher', cipher]) + + # final commit and verify + self.session.commit() + self.assertIn(interface, interfaces()) + self.assertIn(interface, interfaces()) + self.assertEqual(cipher, get_cipher(interface)) + + def test_macsec_gcm_aes_256(self): + interface = 'macsec4' + cipher = 'gcm-aes-256' self.session.set(self._base_path + [interface]) # check validate() - source interface is mandatory @@ -119,11 +153,12 @@ class MACsecInterfaceTest(BasicInterfaceTest.BaseTest): # check validate() - cipher is mandatory with self.assertRaises(ConfigSessionError): self.session.commit() - self.session.set(self._base_path + [interface, 'security', 'cipher', 'gcm-aes-128']) + self.session.set(self._base_path + [interface, 'security', 'cipher', cipher]) # final commit and verify self.session.commit() self.assertIn(interface, interfaces()) + self.assertEqual(cipher, get_cipher(interface)) def test_macsec_source_interface(self): # Ensure source-interface can bot be part of any other bond or bridge diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py index a9250e3e5..cc8fbd527 100755 --- a/smoketest/scripts/cli/test_interfaces_tunnel.py +++ b/smoketest/scripts/cli/test_interfaces_tunnel.py @@ -20,6 +20,7 @@ import json from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd +from vyos.template import inc_ip from base_interfaces_test import BasicInterfaceTest @@ -70,8 +71,8 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): cls.local_v4 = '192.0.2.1' cls.local_v6 = '2001:db8::1' cls._options = { - 'tun10': ['encapsulation ipip', 'remote-ip 192.0.2.10', 'local-ip ' + cls.local_v4], - 'tun20': ['encapsulation gre', 'remote-ip 192.0.2.20', 'local-ip ' + cls.local_v4], + 'tun10': ['encapsulation ipip', 'remote 192.0.2.10', 'source-address ' + cls.local_v4], + 'tun20': ['encapsulation gre', 'remote 192.0.2.20', 'source-address ' + cls.local_v4], } cls._interfaces = list(cls._options) @@ -90,25 +91,25 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): interface = f'tun1000' local_if_addr = f'10.10.200.1/24' - for encapsulation in ['ipip', 'sit', 'gre', 'gre-bridge']: + for encapsulation in ['ipip', 'sit', 'gre', 'gretap']: self.session.set(self._base_path + [interface, 'address', local_if_addr]) self.session.set(self._base_path + [interface, 'encapsulation', encapsulation]) - self.session.set(self._base_path + [interface, 'local-ip', self.local_v6]) - self.session.set(self._base_path + [interface, 'remote-ip', remote_ip6]) + self.session.set(self._base_path + [interface, 'source-address', self.local_v6]) + self.session.set(self._base_path + [interface, 'remote', remote_ip6]) - # Encapsulation mode requires IPv4 local-ip + # Encapsulation mode requires IPv4 source-address with self.assertRaises(ConfigSessionError): self.session.commit() - self.session.set(self._base_path + [interface, 'local-ip', self.local_v4]) + self.session.set(self._base_path + [interface, 'source-address', self.local_v4]) - # Encapsulation mode requires IPv4 local-ip + # Encapsulation mode requires IPv4 remote with self.assertRaises(ConfigSessionError): self.session.commit() - self.session.set(self._base_path + [interface, 'remote-ip', remote_ip4]) + self.session.set(self._base_path + [interface, 'remote', remote_ip4]) self.session.set(self._base_path + [interface, 'source-interface', source_if]) - # Source interface can not be used with sit and gre-bridge - if encapsulation in ['sit', 'gre-bridge']: + # Source interface can not be used with sit and gretap + if encapsulation in ['sit', 'gretap']: with self.assertRaises(ConfigSessionError): self.session.commit() self.session.delete(self._base_path + [interface, 'source-interface']) @@ -117,17 +118,14 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): self.session.commit() conf = tunnel_conf(interface) - self.assertEqual(interface, conf['ifname']) - self.assertEqual(mtu, conf['mtu']) - - if encapsulation not in ['sit', 'gre-bridge']: + if encapsulation not in ['sit', 'gretap']: self.assertEqual(source_if, conf['link']) - self.assertEqual(encapsulation, conf['link_type']) - elif encapsulation in ['gre-bridge']: - self.assertEqual('ether', conf['link_type']) + self.assertEqual(interface, conf['ifname']) + self.assertEqual(mtu, conf['mtu']) + self.assertEqual(encapsulation, conf['linkinfo']['info_kind']) self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local']) - self.assertEqual(remote_ip4, conf['linkinfo']['info_data']['remote']) + self.assertEqual(remote_ip4, conf['linkinfo']['info_data']['remote']) self.assertTrue(conf['linkinfo']['info_data']['pmtudisc']) # cleanup this instance @@ -143,18 +141,18 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): for encapsulation in ['ipip6', 'ip6ip6', 'ip6gre']: self.session.set(self._base_path + [interface, 'address', local_if_addr]) self.session.set(self._base_path + [interface, 'encapsulation', encapsulation]) - self.session.set(self._base_path + [interface, 'local-ip', self.local_v4]) - self.session.set(self._base_path + [interface, 'remote-ip', remote_ip4]) + self.session.set(self._base_path + [interface, 'source-address', self.local_v4]) + self.session.set(self._base_path + [interface, 'remote', remote_ip4]) - # Encapsulation mode requires IPv6 local-ip + # Encapsulation mode requires IPv6 source-address with self.assertRaises(ConfigSessionError): self.session.commit() - self.session.set(self._base_path + [interface, 'local-ip', self.local_v6]) + self.session.set(self._base_path + [interface, 'source-address', self.local_v6]) - # Encapsulation mode requires IPv6 local-ip + # Encapsulation mode requires IPv6 remote with self.assertRaises(ConfigSessionError): self.session.commit() - self.session.set(self._base_path + [interface, 'remote-ip', remote_ip6]) + self.session.set(self._base_path + [interface, 'remote', remote_ip6]) # Configure Tunnel Source interface self.session.set(self._base_path + [interface, 'source-interface', source_if]) @@ -167,14 +165,15 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): self.assertEqual(mtu, conf['mtu']) self.assertEqual(source_if, conf['link']) - # remap encapsulation protocol(s) - if encapsulation in ['ipip6', 'ip6ip6']: - encapsulation = 'tunnel6' - elif encapsulation in ['ip6gre']: - encapsulation = 'gre6' + # Not applicable for ip6gre + if 'proto' in conf['linkinfo']['info_data']: + self.assertEqual(encapsulation, conf['linkinfo']['info_data']['proto']) - self.assertEqual(encapsulation, conf['link_type']) + # remap encapsulation protocol(s) only for ipip6, ip6ip6 + if encapsulation in ['ipip6', 'ip6ip6']: + encapsulation = 'ip6tnl' + self.assertEqual(encapsulation, conf['linkinfo']['info_kind']) self.assertEqual(self.local_v6, conf['linkinfo']['info_data']['local']) self.assertEqual(remote_ip6, conf['linkinfo']['info_data']['remote']) @@ -183,18 +182,18 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): self.session.commit() def test_tunnel_verify_local_dhcp(self): - # We can not use local-ip and dhcp-interface at the same time + # We can not use source-address and dhcp-interface at the same time interface = f'tun1020' local_if_addr = f'10.0.0.1/24' self.session.set(self._base_path + [interface, 'address', local_if_addr]) self.session.set(self._base_path + [interface, 'encapsulation', 'gre']) - self.session.set(self._base_path + [interface, 'local-ip', self.local_v4]) - self.session.set(self._base_path + [interface, 'remote-ip', remote_ip4]) + self.session.set(self._base_path + [interface, 'source-address', self.local_v4]) + self.session.set(self._base_path + [interface, 'remote', remote_ip4]) self.session.set(self._base_path + [interface, 'dhcp-interface', 'eth0']) - # local-ip and dhcp-interface can not be used at the same time + # source-address and dhcp-interface can not be used at the same time with self.assertRaises(ConfigSessionError): self.session.commit() self.session.delete(self._base_path + [interface, 'dhcp-interface']) @@ -209,8 +208,8 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): tos = '20' self.session.set(self._base_path + [interface, 'encapsulation', encapsulation]) - self.session.set(self._base_path + [interface, 'local-ip', self.local_v4]) - self.session.set(self._base_path + [interface, 'remote-ip', remote_ip4]) + self.session.set(self._base_path + [interface, 'source-address', self.local_v4]) + self.session.set(self._base_path + [interface, 'remote', remote_ip4]) self.session.set(self._base_path + [interface, 'parameters', 'ip', 'no-pmtu-discovery']) self.session.set(self._base_path + [interface, 'parameters', 'ip', 'key', gre_key]) @@ -222,11 +221,41 @@ class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): conf = tunnel_conf(interface) self.assertEqual(mtu, conf['mtu']) self.assertEqual(interface, conf['ifname']) - self.assertEqual(encapsulation, conf['link_type']) + self.assertEqual(encapsulation, conf['linkinfo']['info_kind']) self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local']) self.assertEqual(remote_ip4, conf['linkinfo']['info_data']['remote']) self.assertEqual(0, conf['linkinfo']['info_data']['ttl']) self.assertFalse( conf['linkinfo']['info_data']['pmtudisc']) + def test_gretap_parameters_change(self): + interface = f'tun1040' + gre_key = '10' + encapsulation = 'gretap' + tos = '20' + + self.session.set(self._base_path + [interface, 'encapsulation', encapsulation]) + self.session.set(self._base_path + [interface, 'source-address', self.local_v4]) + self.session.set(self._base_path + [interface, 'remote', remote_ip4]) + + # Check if commit is ok + self.session.commit() + + conf = tunnel_conf(interface) + self.assertEqual(mtu, conf['mtu']) + self.assertEqual(interface, conf['ifname']) + self.assertEqual(encapsulation, conf['linkinfo']['info_kind']) + self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local']) + self.assertEqual(remote_ip4, conf['linkinfo']['info_data']['remote']) + self.assertEqual(0, conf['linkinfo']['info_data']['ttl']) + + # Change remote ip address (inc host by 2 + new_remote = inc_ip(remote_ip4, 2) + self.session.set(self._base_path + [interface, 'remote', new_remote]) + # Check if commit is ok + self.session.commit() + + conf = tunnel_conf(interface) + self.assertEqual(new_remote, conf['linkinfo']['info_data']['remote']) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/src/conf_mode/interfaces-erspan.py b/src/conf_mode/interfaces-erspan.py index 2d65b834c..97ae3cf55 100755 --- a/src/conf_mode/interfaces-erspan.py +++ b/src/conf_mode/interfaces-erspan.py @@ -48,7 +48,7 @@ def get_config(config=None): conf = Config() base = ['interfaces', 'erspan'] erspan = get_interface_dict(conf, base) - + tmp = leaf_node_changed(conf, ['encapsulation']) if tmp: erspan.update({'encapsulation_changed': {}}) @@ -58,30 +58,30 @@ def get_config(config=None): def verify(erspan): if 'deleted' in erspan: return None - + if 'encapsulation' not in erspan: raise ConfigError('Unable to detect the following ERSPAN tunnel encapsulation'\ '{ifname}!'.format(**erspan)) verify_mtu_ipv6(erspan) verify_tunnel(erspan) - + key = dict_search('parameters.ip.key',erspan) if key == None: raise ConfigError('parameters.ip.key is mandatory for ERSPAN tunnel') - + def generate(erspan): return None def apply(erspan): - if 'deleted' in erspan or 'encapsulation_changed' in erspan: - if erspan['ifname'] in interfaces(): - tmp = Interface(erspan['ifname']) - tmp.remove() - if 'deleted' in erspan: - return None - + if 'deleted' in erspan or 'encapsulation_changed' in erspan: + if erspan['ifname'] in interfaces(): + tmp = Interface(erspan['ifname']) + tmp.remove() + if 'deleted' in erspan: + return None + dispatch = { 'erspan': ERSpanIf, 'ip6erspan': ER6SpanIf @@ -90,14 +90,8 @@ def apply(erspan): # We need to re-map the tunnel encapsulation proto to a valid interface class encap = erspan['encapsulation'] klass = dispatch[encap] - - conf = deepcopy(erspan) - - conf.update(klass.get_config()) - - del conf['ifname'] - - erspan_tunnel = klass(erspan['ifname'],**conf) + + erspan_tunnel = klass(**erspan) erspan_tunnel.change_options() erspan_tunnel.update(erspan) diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index 979a5612e..2a63b60aa 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -72,18 +72,8 @@ def apply(geneve): g.remove() if 'deleted' not in geneve: - # 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 = GeneveIf.get_config() - - # Assign GENEVE instance configuration parameters to config dict - conf['vni'] = geneve['vni'] - conf['remote'] = geneve['remote'] - # Finally create the new interface - g = GeneveIf(geneve['ifname'], **conf) + g = GeneveIf(**geneve) g.update(geneve) return None diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 7b3afa058..5bae66074 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -83,34 +83,16 @@ def generate(l2tpv3): return None def apply(l2tpv3): - # 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 = L2TPv3If.get_config() - # Check if L2TPv3 interface already exists if l2tpv3['ifname'] in interfaces(): # L2TPv3 is picky when changing tunnels/sessions, thus we can simply # always delete it first. - conf['session_id'] = l2tpv3['session_id'] - conf['tunnel_id'] = l2tpv3['tunnel_id'] - l = L2TPv3If(l2tpv3['ifname'], **conf) + l = L2TPv3If(**l2tpv3) l.remove() if 'deleted' not in l2tpv3: - conf['peer_tunnel_id'] = l2tpv3['peer_tunnel_id'] - conf['local_port'] = l2tpv3['source_port'] - conf['remote_port'] = l2tpv3['destination_port'] - conf['encapsulation'] = l2tpv3['encapsulation'] - conf['local_address'] = l2tpv3['local_ip'] - conf['remote_address'] = l2tpv3['remote_ip'] - conf['session_id'] = l2tpv3['session_id'] - conf['tunnel_id'] = l2tpv3['tunnel_id'] - conf['peer_session_id'] = l2tpv3['peer_session_id'] - # Finally create the new interface - l = L2TPv3If(l2tpv3['ifname'], **conf) + l = L2TPv3If(**l2tpv3) l.update(l2tpv3) return None diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index bfebed7e4..eab69f36e 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -115,17 +115,9 @@ def apply(macsec): os.unlink(wpa_suppl_conf.format(**macsec)) else: - # 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 = MACsecIf.get_config() - conf['source_interface'] = macsec['source_interface'] - conf['security_cipher'] = macsec['security']['cipher'] - # It is safe to "re-create" the interface always, there is a sanity # check that the interface will only be create if its non existent - i = MACsecIf(macsec['ifname'], **conf) + i = MACsecIf(**macsec) i.update(macsec) call('systemctl restart wpa_supplicant-macsec@{source_interface}' diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index ee6f05fcd..4afb85526 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -502,10 +502,7 @@ def apply(openvpn): # existed - nevertheless, spawn new OpenVPN process call(f'systemctl start openvpn@{interface}.service') - conf = VTunIf.get_config() - conf['device_type'] = openvpn['device_type'] - - o = VTunIf(interface, **conf) + o = VTunIf(**openvpn) o.update(openvpn) return None diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index ddbef56ac..34a054837 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -75,19 +75,9 @@ def apply(peth): if 'mode_old' in peth: MACVLANIf(peth['ifname']).remove() - # 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 = MACVLANIf.get_config() - - # Assign MACVLAN instance configuration parameters to config dict - conf['source_interface'] = peth['source_interface'] - conf['mode'] = peth['mode'] - # It is safe to "re-create" the interface always, there is a sanity check # that the interface will only be create if its non existent - p = MACVLANIf(peth['ifname'], **conf) + p = MACVLANIf(**peth) p.update(peth) return None diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 87da214a8..2d2f29f94 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -31,21 +31,25 @@ from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vrf from vyos.configverify import verify_tunnel 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.ifconfig import TunnelIf from vyos.template import is_ipv4 from vyos.template import is_ipv6 +from vyos.util import cmd from vyos.util import dict_search from vyos import ConfigError from vyos import airbag airbag.enable() +def get_tunnel_encapsulation(interface): + """ Returns the used encapsulation protocol for given interface. + If interface does not exist, None is returned. + """ + if not os.path.exists(f'/sys/class/net/{interface}'): + return None + from json import loads + tmp = loads(cmd(f'ip -d -j link show {interface}'))[0] + return tmp['linkinfo']['info_kind'] + def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least @@ -79,8 +83,8 @@ def verify(tunnel): return None if 'encapsulation' not in tunnel: - raise ConfigError('Must configure the tunnel encapsulation for '\ - '{ifname}!'.format(**tunnel)) + error = 'Must configure encapsulation for "{ifname}"!' + raise ConfigError(error.format(**tunnel)) verify_mtu_ipv6(tunnel) verify_address(tunnel) @@ -103,67 +107,20 @@ 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']) + interface = tunnel['ifname'] + # If a gretap tunnel is already existing we can not "simply" change local or + # remote addresses. This returns "Operation not supported" by the Kernel. + # There is no other solution to destroy and recreate the tunnel. + encap = get_tunnel_encapsulation(interface) + + if 'deleted' in tunnel or 'encapsulation_changed' in tunnel or encap == 'gretap': + if interface in interfaces(): + tmp = Interface(interface) tmp.remove() if 'deleted' in tunnel: return None - dispatch = { - 'gre': GREIf, - 'gre-bridge': GRETapIf, - 'ipip': IPIPIf, - 'ipip6': IPIP6If, - 'ip6ip6': IP6IP6If, - 'ip6gre': IP6GREIf, - 'sit': SitIf, - } - - # 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', - } - - # Add additional IPv6 options if tunnel is IPv6 aware - if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: - mappingv6 = { - # this : get_config() - 'parameters.ipv6.encaplimit' : 'encaplimit', - 'parameters.ipv6.flowlabel' : 'flowlabel', - 'parameters.ipv6.hoplimit' : 'hoplimit', - 'parameters.ipv6.tclass' : 'flowlabel' - } - mapping.update(mappingv6) - - 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) - - if dict_search('parameters.ip.no_pmtu_discovery', tunnel) != None: - if 'pmtudisc' in conf['raw']: - conf['raw'].remove('pmtudisc') - conf['raw'].append('nopmtudisc') - - tun = klass(tunnel['ifname'], **conf) - tun.change_options() + tun = TunnelIf(**tunnel) tun.update(tunnel) return None diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 9a6d72772..8e6247a30 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -90,19 +90,8 @@ def apply(vxlan): v.remove() if 'deleted' not in vxlan: - # 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 = VXLANIf.get_config() - - # Assign VXLAN instance configuration parameters to config dict - for tmp in ['vni', 'group', 'source_address', 'source_interface', 'remote', 'port']: - if tmp in vxlan: - conf[tmp] = vxlan[tmp] - # Finally create the new interface - v = VXLANIf(vxlan['ifname'], **conf) + v = VXLANIf(**vxlan) v.update(vxlan) return None diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index b25fcd4e0..7b3de6e8a 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -255,17 +255,8 @@ def apply(wifi): if 'deleted' in wifi: WiFiIf(interface).remove() else: - # 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 = WiFiIf.get_config() - - # Assign WiFi instance configuration parameters to config dict - conf['phy'] = wifi['physical_device'] - # Finally create the new interface - w = WiFiIf(interface, **conf) + w = WiFiIf(**wifi) w.update(wifi) # Enable/Disable interface - interface is always placed in diff --git a/src/migration-scripts/interfaces/19-to-20 b/src/migration-scripts/interfaces/19-to-20 new file mode 100755 index 000000000..ed2780b92 --- /dev/null +++ b/src/migration-scripts/interfaces/19-to-20 @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + base = ['interfaces', 'tunnel'] + + if not config.exists(base): + # Nothing to do + exit(0) + + # + # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap + # Migrate "interface tunnel <tunX> local-ip" to source-address + # Migrate "interface tunnel <tunX> remote-ip" to remote + for interface in config.list_nodes(base): + encap_path = base + [interface, 'encapsulation'] + if config.exists(encap_path): + tmp = config.return_value(encap_path) + if tmp == 'gre-bridge': + config.set(encap_path, value='gretap') + + local_ip_path = base + [interface, 'local-ip'] + if config.exists(local_ip_path): + config.rename(local_ip_path, 'source-address') + + remote_ip_path = base + [interface, 'remote-ip'] + if config.exists(remote_ip_path): + config.rename(remote_ip_path, 'remote') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) |