From 33bc923f3f629fb7c9dfc268e7bde667b34166ce Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Sun, 22 Mar 2020 11:35:49 +0000 Subject: tunnel: T2028: move interface tunnel to XML/Python This patch migrates the "interface tunnel" section to xml/python --- .../include/interface-mtu-64-8024.xml.i | 13 + interface-definitions/interfaces-tunnel.xml.in | 289 ++++++++++++ python/vyos/ifconfig/__init__.py | 8 + python/vyos/ifconfig/afi.py | 19 + python/vyos/ifconfig/tunnel.py | 316 +++++++++++++ src/completion/list_local.py | 24 + src/conf_mode/interfaces-tunnel.py | 490 +++++++++++++++++++++ 7 files changed, 1159 insertions(+) create mode 100644 interface-definitions/include/interface-mtu-64-8024.xml.i create mode 100644 interface-definitions/interfaces-tunnel.xml.in create mode 100644 python/vyos/ifconfig/afi.py create mode 100644 python/vyos/ifconfig/tunnel.py create mode 100755 src/completion/list_local.py create mode 100755 src/conf_mode/interfaces-tunnel.py diff --git a/interface-definitions/include/interface-mtu-64-8024.xml.i b/interface-definitions/include/interface-mtu-64-8024.xml.i new file mode 100644 index 000000000..e917c816f --- /dev/null +++ b/interface-definitions/include/interface-mtu-64-8024.xml.i @@ -0,0 +1,13 @@ + + + Maximum Transmission Unit (MTU) + + 64-8024 + Maximum Transmission Unit + + + + + MTU must be between 64 and 8024 + + diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in new file mode 100644 index 000000000..848d6a1c7 --- /dev/null +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -0,0 +1,289 @@ + + + + + + + Tunnel interface + 380 + + tun[0-9]+$ + + tunnel interface must be named tunN + + tunN + Tunnel interface name + + + + + #include + #include + #include + #include + #include + + + + Local IP address for this tunnel + + ipv4 + Local IPv4 address for this tunnel + + + ipv6 + Local IPv6 address for this tunnel [NOTICE: unavailable for mGRE tunnels] + + + + + + + + + + + + + + Remote IP address for this tunnel + + ipv4 + Remote IPv4 address for this tunnel + + + ipv6 + Remote IPv6 address for this tunnel + + + + + + + + + + + 6rd network prefix + + ipv6 + IPv6 address and prefix length + + + + + + + + + + 6rd relay prefix + + ipv4net + IPv4 prefix of interface for 6rd + + + + + + + + + + dhcp interface + + interface + DHCP interface that supplies the local IP address for this tunnel + + + + + + (en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+ + + + + + + + Ignore link state changes + + gre gre-bridge ipip sit ipip6 ip6ip6 ip6gre + + + gre-bridge + Generic Routing Encapsulation bridge interface + + + ipip + IP in IP encapsulation + + + sit + Simple Internet Transition encapsulation + + + ipip6 + IP in IP6 encapsulation + + + ip6ip6 + IP6 in IP6 encapsulation + + + ip6gre + GRE over IPv6 network + + + (gre|gre-bridge|ipip|sit|ipip6|ip6ip6|ip6gre) + + Must be one of 'gre' 'gre-bridge' 'ipip' 'sit' 'ipip6' 'ip6ip6' 'ip6gre' + + + + + + Multicast operation over tunnel + + enable disable + + + enable + Enable Multicast + + + disable + Disable Multicast (default) + + + (enable|disable) + + Must be 'disable' or 'enable' + + + + + + Tunnel parameters + + + + + + IPv4 specific tunnel parameters + + + + + Time to live field + + 0-255 + Time to live (default 255) + + + + + TTL must be between 0 and 255 + + + + + + Type of Service (TOS) + + 0-99 + Type of Service (TOS) + + + + + TOS must be between 0 and 99 + + + + + + Tunnel key + + 0-4294967295 + Tunnel key + + + + + key must be between 0-4294967295 + + + + + + + + IPv6 specific tunnel parameters + + + + + + Encaplimit field + + 0-255 + Encaplimit (default 4) + + + + + key must be between 0-255 + + + + + + Flowlabel + + 0x0-0x0FFFFF + Tunnel key, 'inherit' or hex value + + + (0x){0,1}(0?[0-9A-Fa-f]{1,5}) + + Must be 'inherit' or a number + + + + + + Hoplimit + + 0-255 + Hoplimit (default 64) + + + + + hoplimit must be between 0-255 + + + + + + Traffic class (Tclass) + + 0x0-0x0FFFFF + Traffic class, 'inherit' or hex value + + + (0x){0,1}(0?[0-9A-Fa-f]{1,2}) + + Must be 'inherit' or a number + + + + + + + + + + + diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index c77b2f9db..a7588e5bc 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -27,3 +27,11 @@ from vyos.ifconfig.stp import STPIf from vyos.ifconfig.vlan import VLANIf from vyos.ifconfig.vxlan import VXLANIf from vyos.ifconfig.wireguard import WireGuardIf +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 \ No newline at end of file diff --git a/python/vyos/ifconfig/afi.py b/python/vyos/ifconfig/afi.py new file mode 100644 index 000000000..fd263d220 --- /dev/null +++ b/python/vyos/ifconfig/afi.py @@ -0,0 +1,19 @@ +# Copyright 2019 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://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml + +IP4 = 1 +IP6 = 2 diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py new file mode 100644 index 000000000..c82727eee --- /dev/null +++ b/python/vyos/ifconfig/tunnel.py @@ -0,0 +1,316 @@ +# Copyright 2019 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 copy import deepcopy + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.afi import IP4, IP6 +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}"') + + + +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 + """ + + # 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}', + }, + }} + + # use for "options" and "updates" + # If an key is only in the options list, it can only be set at creation time + # the create comand will only be make using the key in options + + # If an option is in the updates list, it can be updated + # upon, the creation, all key not yet applied will be updated + + # multicast/allmulticast can not be part of the create command + + # options matrix: + # with ip = 4, we have multicast + # wiht ip = 6, nothing + # with tunnel = 4, we have tos, ttl, key + # with tunnel = 6, we have encaplimit, hoplimit, tclass, flowlabel + + # TODO: For multicast, it is allowed on IP6IP6 and Sit6RD + # TODO: to match vyatta but it should be checked for correctness + + updates = [] + + create = '' + change = '' + delete = '' + + ip = [] # AFI of the families which can be used in the tunnel + tunnel = 0 # invalid - need to be set by subclasses + + def __init__(self, ifname, **config): + self.config = deepcopy(config) if config else {} + super().__init__(ifname, **config) + + def _create(self): + # add " option-name option-name-value ..." for all options set + options = " ".join(["{} {}".format(k, self.config[k]) + for k in self.options if k in self.config and self.config[k]]) + self._cmd('{} {}'.format(self.create.format(**self.config), options)) + self.set_interface('state', 'down') + + def _delete(self): + self.set_interface('state', 'down') + cmd = self.delete.format(**self.config) + return self._cmd(cmd) + + def set_interface(self, option, value): + try: + return Interface.set_interface(self, option, value) + except Exception: + pass + + if value == '': + # remove the value so that it is not used + self.config.pop(option, '') + + if self.change: + self._cmd('{} {} {}'.format( + self.change.format(**self.config), option, value)) + return True + + @classmethod + def get_config(cls): + return dict(zip(cls.options, ['']*len(cls.options))) + + +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 + """ + + ip = [IP4, IP6] + tunnel = IP4 + + default = {'type': 'gre'} + required = ['local', ] # mGRE is a GRE without remote endpoint + + options = ['local', 'remote', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'ttl', 'tos', + 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +# GreTap also called GRE Bridge +class GRETapIf(_Tunnel): + """ + GRETapIF: GreIF using TAP instead of TUN + + https://en.wikipedia.org/wiki/TUN/TAP + """ + + # no multicast, ttl or tos for gretap + + ip = [IP4, ] + tunnel = IP4 + + default = {'type': 'gretap'} + required = ['local', ] + + options = ['local', 'remote', ] + updates = [] + + create = 'ip link add {ifname} type {type}' + change = '' + delete = 'ip link del {ifname}' + + +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 + """ + + ip = [IP4, IP6] + tunnel = IP6 + + default = {'type': 'ip6gre'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel'] + updates = ['local', 'remote', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel', + 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname} mode {type}' + delete = 'ip tunnel del {ifname}' + + # using "ip tunnel change" without using "mode" causes errors + # sudo ip tunnel add tun100 mode ip6gre local ::1 remote 1::1 + # sudo ip tunnel cha tun100 hoplimit 100 + # *** stack smashing detected ** *: < unknown > terminated + # sudo ip tunnel cha tun100 local: : 2 + # Error: an IP address is expected rather than "::2" + # works if mode is explicit + + +class IPIPIf(_Tunnel): + """ + 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 + + ip = [IP4,] + tunnel = IP4 + + default = {'type': 'ipip'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'ttl', 'tos', + 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +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 + """ + + ip = [IP4,] + tunnel = IP6 + + default = {'type': 'ipip6'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel'] + updates = ['local', 'remote', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel', + 'multicast', 'allmulticast'] + + create = 'ip -6 tunnel add {ifname} mode {type}' + change = 'ip -6 tunnel cha {ifname}' + delete = 'ip -6 tunnel del {ifname}' + + +class IP6IP6If(IPIP6If): + """ + IP6IP6: IPv6 over IPv6 tunnel + + For more information please refer to: + https://tools.ietf.org/html/rfc2473 + """ + + ip = [IP6,] + + 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 + """ + + ip = [IP6, IP4] + tunnel = IP4 + + default = {'type': 'sit'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'ttl', 'tos', + 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +class Sit6RDIf(SitIf): + """ + Sit6RDIf: Simple Internet Transition with 6RD + + https://en.wikipedia.org/wiki/IPv6_rapid_deployment + """ + + ip = [IP6,] + + required = ['remote', '6rd-prefix'] + + # TODO: check if key can really be used with 6RD + options = ['remote', 'ttl', 'tos', 'key', '6rd-prefix', '6rd-relay-prefix'] + updates = ['remote', 'ttl', 'tos', + 'multicast', 'allmulticast'] + + 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/src/completion/list_local.py b/src/completion/list_local.py new file mode 100755 index 000000000..ddb65c05c --- /dev/null +++ b/src/completion/list_local.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +import json +import argparse +import subprocess + +# [{"ifindex":1,"ifname":"lo","flags":["LOOPBACK","UP","LOWER_UP"],"mtu":65536,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"loopback","address":"00:00:00:00:00:00","broadcast":"00:00:00:00:00:00","addr_info":[{"family":"inet","local":"127.0.0.1","prefixlen":8,"scope":"host","label":"lo","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"::1","prefixlen":128,"scope":"host","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":2,"ifname":"eth0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:fa:12:53","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet","local":"10.0.2.15","prefixlen":24,"broadcast":"10.0.2.255","scope":"global","label":"eth0","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"fe80::a00:27ff:fefa:1253","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":3,"ifname":"eth1","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:0d:25:dc","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fe0d:25dc","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":4,"ifname":"eth2","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:68:d0:b1","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fe68:d0b1","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":5,"ifname":"eth3","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:f0:17:c5","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fef0:17c5","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]}] + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + + cmd = 'ip -j address show' + out = subprocess.check_output(cmd.split()).decode().strip() + data = json.loads(out) + + + interfaces = [] + for interface in data: + if not 'addr_info' in interface: + continue + interfaces.extend(interface['addr_info']) + + print(' '.join([interface['local'] for interface in interfaces if 'local' in interface])) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py new file mode 100755 index 000000000..67b58719b --- /dev/null +++ b/src/conf_mode/interfaces-tunnel.py @@ -0,0 +1,490 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 . + +import os +import netifaces + +from sys import exit +from copy import deepcopy + +from vyos.config import Config +from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf +from vyos.ifconfig.afi import IP4, IP6 +from vyos.configdict import list_diff +from vyos.validate import is_ipv4, is_ipv6 +from vyos import ConfigError + + +class FixedDict(dict): + def __init__ (self, **options): + self._allowed = options.keys() + super().__init__(**options) + + def __setitem__ (self, k, v): + if k not in self._allowed: + raise ConfigError(f'Option "{k}" has no defined default') + super().__setitem__(k, v) + + +class ConfigurationState (Config): + def __init__ (self, section, default): + super().__init__() + self.section = section + 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.exists(section): + self.changes['section'] = 'delete' + elif self.exists_effective(section): + self.changes['section'] = 'modify' + else: + self.changes['section'] = 'create' + + def _act(self, section): + if self.exists(section): + if self.exists_effective(section): + if self.return_value(section) != self.return_effective_value(section): + return 'modify' + return 'static' + return 'create' + else: + if self.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): + if self._action(name, key) in ('delete', 'absent'): + return + return self._get(name, key, default, self.return_value) + + def get_values(self, name, key, default=None): + if self._action(name, key) in ('delete', 'absent'): + return + return self._get(name, key, default, self.return_values) + + def get_effective(self, name, key, default=None): + self._action(name, key) + return self._get(name, key, default, self.return_effective_value) + + def get_effectives(self, name, key, default=None): + self._action(name, key) + return self._get(name, key, default, self.return_effectives_value) + + def load(self, mapping): + 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 remove_default (self,*options): + for option in options: + if self.exists(option) and self_return_value(option) != self.default[option]: + continue + del self.options[option] + + def to_dict (self): + # 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, + } + +default_config_data = { + # interface definition + 'addresses-add': [], + 'addresses-del': [], + 'state': 'up', + 'dhcp-interface': '', + 'link_detect': 1, + 'ip': False, + 'ipv6': False, + 'nhrp': [], + # internal + 'tunnel': {}, + # the following names are exactly matching the name + # for the ip command and must not be changed + 'ifname': '', + 'type': '', + 'alias': '', + 'mtu': '1476', + 'local': '', + 'remote': '', + 'multicast': 'disable', + 'allmulticast': 'disable', + 'ttl': '255', + 'tos': 'inherit', + 'key': '', + 'encaplimit': '4', + 'flowlabel': 'inherit', + 'hoplimit': '64', + 'tclass': 'inherit', + '6rd-prefix': '', + '6rd-relay-prefix': '', +} + +# 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), + '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), + 'addresses-add': ('address', True, None), +} + +def get_class (options): + dispatch = { + 'gre': GREIf, + 'gre-bridge': GRETapIf, + 'ipip': IPIPIf, + 'ipip6': IPIP6If, + 'ip6ip6': IP6IP6If, + 'ip6gre': IP6GREIf, + 'sit': SitIf, + } + + kls = dispatch[options['type']] + if options['type'] == 'gre' and not options['remote']: + # 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(): + ifname = os.environ.get('VYOS_TAGNODE_VALUE','') + if not ifname: + raise ConfigError('Interface not specified') + + conf = ConfigurationState('interfaces tunnel ' + ifname, default_config_data) + options = conf.options + changes = conf.changes + options['ifname'] = ifname + + # set new configuration level + conf.set_level(conf.section) + + if changes['section'] == 'delete': + conf.get_effective('type', mapping['type'][0]) + conf.set_level('protocols nhrp tunnel') + options['nhrp'] = conf.list_nodes('') + return conf.to_dict() + + # 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'] = '' + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + # could be done within ConfigurationState + eff_addr = conf.return_effective_values('address') + options['addresses-del'] = list_diff(eff_addr, options['addresses-add']) + + # allmulticast fate is linked to multicast + options['allmulticast'] = options['multicast'] + + # check that per encapsulation all local-remote pairs are unique + conf.set_level('interfaces tunnel') + ct = conf.get_config_dict()['tunnel'] + options['tunnel'] = {} + + 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', '') + pair = f'{local}-{remote}' + options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1 + + return conf.to_dict() + + +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'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp') + # 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'] + + 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 kls.updates: + 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'] + + # 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}') + + # 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}') + + # tunnel encapsulation check + + convert = { + (6, 4, 'gre'): 'ip6gre', + (6, 6, 'gre'): 'ip6gre', + (4, 6, 'ipip'): 'ipip6', + (6, 6, 'ipip'): 'ip6ip6', + } + + 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}') + + return None + + +def generate(gre): + return None + +def apply(conf): + options = conf['options'] + changes = conf['changes'] + actions = conf['actions'] + kls = get_class(options) + + # extract ifname as otherwise it is duplicated on the interface creation + ifname = options.pop('ifname') + + # only the valid keys for creation of a Interface + config = dict((k, options[k]) for k in kls.options if options[k]) + + # setup or create the tunnel interface if it does not exist + tunnel = kls(ifname, **config) + + if changes['section'] == 'delete': + tunnel.remove() + # The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup + # which identified tunnels type which were not used anymore to remove them + # (ie: gre0, gretap0, etc.) The perl code did however nothing + # This feature is also not implemented yet + return + + # A GRE interface without remote will be mGRE + # if the interface does not suppor the option, it skips the change + for option in tunnel.updates: + if changes['section'] in 'create' and option in tunnel.options: + # it was setup at creation + continue + tunnel.set_interface(option, options[option]) + + # set other interface properties + for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast'): + tunnel.set_interface(option, options[option]) + + # 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_state(options['state']) + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) -- cgit v1.2.3