summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--interface-definitions/include/interface-mtu-64-8024.xml.i13
-rw-r--r--interface-definitions/interfaces-tunnel.xml.in289
-rw-r--r--python/vyos/ifconfig/__init__.py8
-rw-r--r--python/vyos/ifconfig/afi.py19
-rw-r--r--python/vyos/ifconfig/tunnel.py316
-rwxr-xr-xsrc/completion/list_local.py24
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py490
7 files changed, 1159 insertions, 0 deletions
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 @@
+<leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>64-8024</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 64-8024"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 64 and 8024</constraintErrorMessage>
+ </properties>
+</leafNode>
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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="tunnel" owner="${vyos_conf_scripts_dir}/interfaces-tunnel.py">
+ <properties>
+ <help>Tunnel interface</help>
+ <priority>380</priority>
+ <constraint>
+ <regex>tun[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>tunnel interface must be named tunN</constraintErrorMessage>
+ <valueHelp>
+ <format>tunN</format>
+ <description>Tunnel interface name</description>
+ </valueHelp>
+ </properties>
+
+ <children>
+ #include <include/interface-description.xml.i>
+ #include <include/address-ipv4-ipv6.xml.i>
+ #include <include/interface-disable.xml.i>
+ #include <include/interface-disable-link-detect.xml.i>
+ #include <include/interface-mtu-64-8024.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 [NOTICE: unavailable for mGRE tunnels]</description>
+ </valueHelp>
+ <completionHelp>
+ <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>
+ </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>
+
+ <leafNode name="6rd-prefix">
+ <properties>
+ <help>6rd network prefix</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+
+ <leafNode name="6rd-relay-prefix">
+ <properties>
+ <help>6rd relay prefix</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix of interface for 6rd</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+
+ <leafNode name="dhcp-interface">
+ <properties>
+ <help>dhcp interface</help>
+ <valueHelp>
+ <format>interface</format>
+ <description>DHCP interface that supplies the local IP address for this tunnel</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <constraint>
+ <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+
+ <leafNode name="encapsulation">
+ <properties>
+ <help>Ignore link state changes</help>
+ <completionHelp>
+ <list>gre gre-bridge ipip sit ipip6 ip6ip6 ip6gre</list>
+ </completionHelp>
+ <valueHelp>
+ <format>gre-bridge</format>
+ <description>Generic Routing Encapsulation bridge interface</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipip</format>
+ <description>IP in IP encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sit</format>
+ <description>Simple Internet Transition encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipip6</format>
+ <description>IP in IP6 encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ip6ip6</format>
+ <description>IP6 in IP6 encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ip6gre</format>
+ <description>GRE over IPv6 network</description>
+ </valueHelp>
+ <constraint>
+ <regex>(gre|gre-bridge|ipip|sit|ipip6|ip6ip6|ip6gre)</regex>
+ </constraint>
+ <constraintErrorMessage>Must be one of 'gre' 'gre-bridge' 'ipip' 'sit' 'ipip6' 'ip6ip6' 'ip6gre'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+
+ <leafNode name="multicast">
+ <properties>
+ <help>Multicast operation over tunnel</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable Multicast</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable Multicast (default)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Must be 'disable' or 'enable'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+
+ <node name="parameters">
+ <properties>
+ <help>Tunnel parameters</help>
+ </properties>
+
+ <children>
+ <node name="ip">
+ <properties>
+ <help>IPv4 specific tunnel parameters</help>
+ </properties>
+ <children>
+ <leafNode name="ttl">
+ <properties>
+ <help>Time to live field</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>Time to live (default 255)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>TTL must be between 0 and 255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+
+ <leafNode name="tos">
+ <properties>
+ <help>Type of Service (TOS)</help>
+ <valueHelp>
+ <format>0-99</format>
+ <description>Type of Service (TOS)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-99"/>
+ </constraint>
+ <constraintErrorMessage>TOS must be between 0 and 99</constraintErrorMessage>
+ </properties>
+ </leafNode>
+
+ <leafNode name="key">
+ <properties>
+ <help>Tunnel key</help>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>Tunnel key</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ <constraintErrorMessage>key must be between 0-4294967295</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+
+ <node name="ipv6">
+ <properties>
+ <help>IPv6 specific tunnel parameters</help>
+ </properties>
+
+ <children>
+ <leafNode name="encaplimit">
+ <properties>
+ <help>Encaplimit field</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>Encaplimit (default 4)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>key must be between 0-255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+
+ <leafNode name="flowlabel">
+ <properties>
+ <help>Flowlabel</help>
+ <valueHelp>
+ <format>0x0-0x0FFFFF</format>
+ <description>Tunnel key, 'inherit' or hex value</description>
+ </valueHelp>
+ <constraint>
+ <regex>(0x){0,1}(0?[0-9A-Fa-f]{1,5})</regex>
+ </constraint>
+ <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage>
+ </properties>
+ </leafNode>
+
+ <leafNode name="hoplimit">
+ <properties>
+ <help>Hoplimit</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>Hoplimit (default 64)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>hoplimit must be between 0-255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+
+ <leafNode name="tclass">
+ <properties>
+ <help>Traffic class (Tclass)</help>
+ <valueHelp>
+ <format>0x0-0x0FFFFF</format>
+ <description>Traffic class, 'inherit' or hex value</description>
+ </valueHelp>
+ <constraint>
+ <regex>(0x){0,1}(0?[0-9A-Fa-f]{1,2})</regex>
+ </constraint>
+ <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
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 <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/>.
+
+# 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 <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/>.
+
+# 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 <http://www.gnu.org/licenses/>.
+
+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', '<unset>')
+ 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)