summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--interface-definitions/interfaces-tunnel.xml.in41
-rw-r--r--python/vyos/ifconfig/tunnel.py46
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_tunnel.py6
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py778
4 files changed, 195 insertions, 676 deletions
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/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py
index 4122d1a2f..7264b6296 100644
--- a/python/vyos/ifconfig/tunnel.py
+++ b/python/vyos/ifconfig/tunnel.py
@@ -17,6 +17,9 @@
# 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
@@ -119,10 +122,53 @@ class _Tunnel(Interface):
self.change.format(**self.config), option, value))
return True
+
@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):
"""
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-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index f1217b62d..78fc9667c 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,119 @@
# 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)
-
- 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.
+ # 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'
- >>> 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 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:
+ tmp = Interface(tunnel['ifname'])
+ tmp.remove()
+ return None
-def get_class (options):
dispatch = {
'gre': GREIf,
'gre-bridge': GRETapIf,
@@ -373,353 +138,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)
+ # TODO: support encapsulation change per tunnel
-def generate(gre):
+ tun = klass(tunnel['ifname'], **conf)
+ tun.update(tunnel)
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)