# Copyright 2019-2021 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . # 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 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): if value == 'enable': return 'on' if value == 'disable': return 'off' raise ValueError(f'expect enable or disable but got "{value}"') @Interface.register class _Tunnel(Interface): """ _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 """ definition = { **Interface.definition, **{ 'section': 'tunnel', 'prefixes': ['tun',], }, } # 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 = { 'local_ip' : 'local', 'remote_ip' : 'remote', 'source_interface' : 'dev', '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', } # TODO: This is surely used for more than tunnels # TODO: could be refactored elsewhere _command_set = { **Interface._command_set, **{ 'multicast': { 'validate': lambda v: assert_list(v, ['enable', 'disable']), 'convert': enable_to_on, 'shellcmd': 'ip link set dev {ifname} multicast {value}', }, 'allmulticast': { 'validate': lambda v: assert_list(v, ['enable', 'disable']), 'convert': enable_to_on, 'shellcmd': 'ip link set dev {ifname} allmulticast {value}', }, } } def _create(self): 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 {type}' if self.config['encapsulation'] == 'gre-bridge': cmd = 'ip link add name {ifname} type {type}' 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 change_options(self): if self.config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: mapping = { **self.mapping, **self.mapping_ipv6 } else: mapping = { **self.mapping, **self.mapping_ipv4 } self.config['type'] = self.iftype cmd = 'ip tunnel change {ifname} mode {type}' 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): """ 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): """ 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 """ iftype = 'gre' # GreTap also called GRE Bridge class GRETapIf(_Tunnel): """ GRETapIF: GreIF using TAP instead of TUN https://en.wikipedia.org/wiki/TUN/TAP """ iftype = 'gretap' # no multicast, ttl or tos for gretap definition = { **_Tunnel.definition, **{ 'bridgeable': True, }, } 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 """ iftype = 'ip6gre' class IPIPIf(_Tunnel): """ IPIP: IP Encapsulation within IP For more information please refer to: https://tools.ietf.org/html/rfc2003 """ iftype = 'ipip' 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 """ iftype = 'ipip6' class IP6IP6If(IPIP6If): """ IP6IP6: IPv6 over IPv6 tunnel For more information please refer to: https://tools.ietf.org/html/rfc2473 """ iftype = '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 """ iftype = '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 iftype = '6rd' 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))