diff --git a/data/configd-include.json b/data/configd-include.json index 2711a29b82b..0f46e9ddae3 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -26,6 +26,7 @@ "interfaces-wireguard.py", "interfaces-wireless.py", "interfaces-wirelessmodem.py", +"interfaces-tinc.py", "ipsec-settings.py", "lldp.py", "nat.py", @@ -60,4 +61,4 @@ "vrf.py", "vrrp.py", "vyos_cert.py" -] \ No newline at end of file +] diff --git a/data/templates/tinc/hosts_config.tmpl b/data/templates/tinc/hosts_config.tmpl new file mode 100644 index 00000000000..b103300f6e4 --- /dev/null +++ b/data/templates/tinc/hosts_config.tmpl @@ -0,0 +1,8 @@ +{% for prefix in subnets %} +Subnet = {{ prefix }} +{% endfor %} +{% for addr in local_address %} +Address = {{ addr }} +{% endfor %} +Port = {{ port }} + diff --git a/data/templates/tinc/tinc-down.tmpl b/data/templates/tinc/tinc-down.tmpl new file mode 100644 index 00000000000..b2598e6d606 --- /dev/null +++ b/data/templates/tinc/tinc-down.tmpl @@ -0,0 +1,2 @@ +#!/bin/sh +ip link set {{ ifname }} down diff --git a/data/templates/tinc/tinc-up.tmpl b/data/templates/tinc/tinc-up.tmpl new file mode 100644 index 00000000000..3f56c61cc91 --- /dev/null +++ b/data/templates/tinc/tinc-up.tmpl @@ -0,0 +1,5 @@ +#!/bin/sh +{% for addr in address %} +ip addr add dev {{ ifname }} local {{ addr }} +{% endfor %} +ip link set {{ ifname }} up diff --git a/data/templates/tinc/tinc.conf.tmpl b/data/templates/tinc/tinc.conf.tmpl new file mode 100644 index 00000000000..77a90ef8a7f --- /dev/null +++ b/data/templates/tinc/tinc.conf.tmpl @@ -0,0 +1,108 @@ +Name = {{ node_name }} +Interface = {{ ifname }} +Mode = {{ device.mode }} +Compression = {{ compression_level }} +Cipher = {{ encryption.cipher }} +Digest = {{ encryption.digset }} +{% if resolve_hostname %} +Hostnames = yes +{% else %} +Hostnames = no +{% endif %} +PrivateKeyFile = {{ private_keyfile }} +Broadcast = {{ broadcast_type }} +{% if disable_resolve_hostname %} +DecrementTTL = no +{% else %} +DecrementTTL = yes +{% endif %} +{% if direct_only %} +DirectOnly = yes +{% else %} +DirectOnly = no +{% endif %} +Forwarding = {{ forwarding_option }} +{% if iff_One_Queue %} +IffOneQueue = yes +{% else %} +IffOneQueue = no +{% endif %} +KeyExpire = {{ key_expire }} +{% if local_discovery %} +LocalDiscovery = yes +{% else %} +LocalDiscovery = no +{% endif %} +MACExpire = {{ mac_expire}} +MaxTimeout = {{ max_timeout }} +PingInterval = {{ ping_interval }} +PingTimeout = {{ ping_timeout }} +{% if priority_inheritance %} +PriorityInheritance = yes +{% else %} +PriorityInheritance = no +{% endif %} +ProcessPriority = {{ priority }} +ReplayWindow = {{ replay_window }} +{% if strict_subnets %} +StrictSubnets = yes +{% else %} +StrictSubnets = no +{% endif %} +{% if tunnel_server %} +TunnelServer = yes +{% else %} +TunnelServer = no +{% endif %} +{% if clamp_mss %} +ClampMSS = yes +{% else %} +ClampMSS = no +{% endif %} +{% if indirect_data %} +IndirectData = yes +{% else %} +IndirectData = no +{% endif %} +MACLength = {{ mac_length }} +PMTU = {{ mtu }} +{% if disable_PMTU_Discovery %} +PMTUDiscovery = no +{% else %} +PMTUDiscovery = yes +{% endif %} +{% if TCP_Only %} +TCPonly = yes +{% else %} +TCPonly = no +{% endif %} +DeviceType = {{ device.type }} +{% if udp_rcv_buf %} +UDPRcvBuf = {{ udp_rcv_buf }} +{% endif %} +{% if udp_snd_buf %} +UDPSndBuf = {{ udp_snd_buf }} +{% endif %} +{% if proxy and proxy.type %} +{% if proxy.type == 'socks5' %} +Proxy = {{ proxy.type }} {{ proxy.address }} {{ proxy.port }} {{ proxy.username }} { proxy.password }} +{% elif proxy.type == 'socks4' %} +Proxy = {{ proxy.type }} {{ proxy.address }} {{ proxy.port }}{{ proxy.username }} +{% elif proxy.type == 'http' %} +Proxy = {{ proxy.type }} {{ proxy.address }} {{ proxy.port }} +{% elif proxy.type == 'exec' %} +Proxy = {{ proxy.type }} {{ proxy.exec }} +{% endif %} +{% endif %} +{% if connect %} +ConnectTo = {{ connect }} +{% endif %} +{% if bind_address %} +BindToAddress = {{ bind_address }} +{% endif %} +{% if bind_interface %} +BindToInterface = {{ bind_interface }} +{% endif %} +{% if graph_dump_file %} +GraphDumpFile = {{ graph_dump_file }} +{% endif %} diff --git a/debian/control b/debian/control index ebcfc6c43ca..9e6076eeadb 100644 --- a/debian/control +++ b/debian/control @@ -112,7 +112,8 @@ Depends: wireguard-tools, wireguard-modules, wireless-regdb, - wpasupplicant (>= 0.6.7) + wpasupplicant (>= 0.6.7), + tinc Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything diff --git a/interface-definitions/interfaces-tinc.xml.in b/interface-definitions/interfaces-tinc.xml.in new file mode 100644 index 00000000000..93c39677bf0 --- /dev/null +++ b/interface-definitions/interfaces-tinc.xml.in @@ -0,0 +1,476 @@ + + + + + + + Tinc VPN Tunnel Interface + 460 + + ^tinc[0-9]+$ + + Tinc VPN tunnel interface must be named vtincN + + tincN + Tinc VPN interface name + + + + + + Local Node Name options(require) + + + + + Declare network segment + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + + + + + Bind To Address + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + + + + + IP address + + ipv4 + IPv4 address + + + ipv6 + IPv6 address + + + host + Host + + + + + + + + + + Port Option + + + + + 655 + + + + Bind To Interface + + + + + + + + Connect To Peer Node Name + + + #include + #include + #include + #include + #include + #include + #include + + + Data Encryption settings + + + + + Standard Data Encryption Algorithm(default:aes-256-cbc) + + aes-256-cbc + + + + UDP Digest settings(default:sha256) + + sha256 + + + + + + Virtual Device settings + + + + + Tinc VPN interface device type + + dummy raw_socket multicast tun tap + + + dummy + Use a dummy interface. No packets are ever read or written to a virtual network device. Useful for testing, or when setting up a node that only forwards packets for other nodes. + + + raw_socket + Open a raw socket, and bind it to a pre-existing Interface (eth0 by default). All packets are read from this interface. Packets received for the local node are written to the raw socket. However, at least on Linux, the operating system does not process IP packets destined for the local host + + + multicast + Open a multicast UDP socket and bind it to the address and port (separated by spaces) and optionally a TTL value specified using Device. Packets are read from and written to this multicast socket. This can be used to connect to UML, QEMU or KVM instances listening on the same multicast address. Do NOT connect multiple tinc daemons to the same multicast address, this will very likely cause routing loops. Also note that this can cause decrypted VPN packets to be sent out on a real network if misconfigured + + + tun + Set type to tun. Depending on the platform, this can either be with or without an address family header (see below) + + + (dummy|raw_socket|multicast|tun) + + + tap + + + + Tinc VPN interface device mode + + switch hub + + + switch + switch device + + + hub + hub device + + + (switch|hub) + + + router + + + + #include + #include + #include + #include + + + This option selects the way broadcast packets are sent to other daemons. NOTE: all nodes in a VPN must use the same Broadcast mode, otherwise routing loops can form + + no mst direct + + + no + Broadcast packets are never sent to other nodes. + + + mst + Broadcast packets are sent and forwarded via the VPN’s Minimum Spanning Tree. This ensures broadcast packets reach all nodes + + + (no|mst) + + + direct + + + + Disable Decrement TTL + + + + + + Only carry out peer-to-peer direct communication, discard data packets that need to be forwarded + + + + + + This option selects the way indirect packets are forwarded + + off internal kernel + + + off + Incoming packets that are not meant for the local node, but which should be forwarded to another node, are dropped + + + internal + Incoming packets that are meant for another node are forwarded by tinc internally(default) + + + kernel + Incoming packets are always sent to the TUN/TAP device, even if the packets are not for the local node. This is less efficient, but allows the kernel to apply its routing and firewall rules on them, and can also help debugging + + + (off|internal|kernel) + + + internal + + + + Dump network graph files, which can be read with graphviz + + + + + Resolve IP addresses (real and VPN) + + + + + + Set IFF_ONE_QUEUE flag on TUN/TAP devices + + + + + + Key expiration time(seconds,default:3600) + + 3600 + + + + Enable Local Discovery + + + + + + MAC expiration time(seconds,default:600) + + 600 + + + + Max Timeout(seconds,default:900) + + 900 + + + + Ping Interval(seconds,default:60) + + 60 + + + + Ping Timeout(seconds,default:5) + + 5 + + + + Enable Priority Inheritance + + + + + + Private Key File,This is the full path name of the RSA private key file that was generated by ‘tincd --generate-keys’. It must be a full path, not a relative directory(require) + + + + + The directory where the network interface stores host certificates(require) + + + + + Priority Option + + low normal high + + + low + Low Priority + + + normal + normal Priority(default) + + + high + high Priority + + + (low|normal|high) + + + normal + + + + Proxy settings + + + + + Proxy Type + + socks4 socks5 http exec + + + socks4 + socks4 Proxy + + + socks5 + socks4 Proxy + + + http + http Proxy + + + exec + Executes the given command which should set up the outgoing connection. The environment variables NAME, NODE, REMOTEADDRES and REMOTEPORT are available + + + (socks4|socks5|http|exec) + + + + + + Proxy Address + + + + + + + + Proxy Port + + + + + + + + Proxy Username + + + + + Proxy Password + + + + + Executes the given command which should set up the outgoing connection. The environment variables NAME, NODE, REMOTEADDRES and REMOTEPORT are available + + + + + + + ReplayWindow(Bytes,default:16) + + 16 + + + + When this option is enabled tinc will only use Subnet statements which are present in the host config files in the local /etc/tinc/netname/hosts/ directory. Subnets learned via connections to other nodes and which are not present in the local host config files are ignored + + + + + + When this option is enabled tinc will no longer forward information between other tinc daemons, and will only allow connections with nodes for which host config files are present in the local /etc/tinc/netname/hosts/ directory, Setting this options also implicitly sets StrictSubnets + + + + + + Sets the socket receive buffer size for the UDP socket, in bytes. If unset, the default buffer size will be used by the operating system(Bytes) + + + + + Sets the socket send buffer size for the UDP socket, in bytes. If unset, the default buffer size will be used by the operating system(Bytes) + + + + + Enable Clamp MSS + + + + + + The tinc daemons other than those specified by ConnectTo can directly establish a connection with you + + + + + + Disable PMTU Discovery Option + + + + + + Only use TCP connection + + + + + + Compression Level Option(default:9) + + 9 + + + + MAC Length(Bytes,default:4)) + + 4 + + + + MTU(Bytes,default:1514) + + 1514 + + + + + + diff --git a/op-mode-definitions/show-interfaces-tinc.xml b/op-mode-definitions/show-interfaces-tinc.xml new file mode 100644 index 00000000000..e0f82d8386a --- /dev/null +++ b/op-mode-definitions/show-interfaces-tinc.xml @@ -0,0 +1,41 @@ + + + + + + + + + Show Tinc VPN interface information + + + + + Show detailed Tinc VPN interface information + + ${vyos_op_scripts_dir}/show_interfaces.py --intf-type=tinc --action=show + + + + + + Show Tinc VPN interface information + + + + + ${vyos_op_scripts_dir}/show_interfaces.py --intf=$4 + + + + Show summary of specified Tinc VPN interface information + + ${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief + + + + + + + + diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index 9cd8d44c16d..363deaced14 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -29,6 +29,7 @@ from vyos.ifconfig.vxlan import VXLANIf from vyos.ifconfig.wireguard import WireGuardIf from vyos.ifconfig.vtun import VTunIf +from vyos.ifconfig.tinc import TincIf from vyos.ifconfig.vti import VTIIf from vyos.ifconfig.pppoe import PPPoEIf from vyos.ifconfig.tunnel import GREIf diff --git a/python/vyos/ifconfig/tinc.py b/python/vyos/ifconfig/tinc.py new file mode 100644 index 00000000000..c7f216d4fe8 --- /dev/null +++ b/python/vyos/ifconfig/tinc.py @@ -0,0 +1,45 @@ +# Copyright 2020 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 . + +from vyos.ifconfig.interface import Interface + +@Interface.register +class TincIf(Interface): + default = { + 'type': 'tinc', + } + definition = { + **Interface.definition, + **{ + 'section': 'tinc', + 'prefixes': ['tinc'], + 'bridgeable': True, + 'eternal': '(tinc)[0-9]+$', + 'bondable': True, + 'broadcast': True + }, + } + + def update(self, 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. + super().update(config) + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/smoketest/scripts/cli/test_interfaces_tinc.py b/smoketest/scripts/cli/test_interfaces_tinc.py new file mode 100755 index 00000000000..0fc1aabb056 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_tinc.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 +# 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 unittest + +from base_interfaces_test import BasicInterfaceTest +from vyos.ifconfig import Section + +base_path = ['interfaces','tinc'] + +class TaskTincVPN(unittest.TestCase): + def setUp(self): + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + self.session = ConfigSession(os.getpid()) + self.session.delete(base_path) + + def tearDown(self): + self.session.delete(base_path) + self.session.commit() + def test_ndp_proxy(self): + self.session.set(base_path + ['node-name'],'test1') + self.session.set(base_path + ['hosts-dir'],'/tmp/test/hosts') + self.session.set(base_path + ['private-keyfile'],'/tmp/test/test.key') + self.session.set(base_path + ['address'],'192.168.20.1/24') + self.session.set(base_path + ['subnets'],'192.168.20.0/24') + # check validate() - outbound-interface must be defined + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.assertEqual(True) + +if __name__ == '__main__': + unittest.main() diff --git a/src/conf_mode/interfaces-tinc.py b/src/conf_mode/interfaces-tinc.py new file mode 100755 index 00000000000..856a08ce261 --- /dev/null +++ b/src/conf_mode/interfaces-tinc.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 +# 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, sys + +from sys import exit +from copy import deepcopy + +from netifaces import interfaces +from vyos.configdict import get_interface_dict,leaf_node_changed +from vyos.config import Config +from netifaces import interfaces +from time import sleep +from vyos.ifconfig import VTincIf +from vyos import ConfigError +from vyos.util import call +from vyos.xml import defaults +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.configverify import verify_address +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_mtu +from vyos.configverify import verify_mtu_ipv6 +from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf + +from vyos import airbag + +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['interfaces', 'tinc'] + + tinc = get_interface_dict(conf, base) + + return tinc + +def verify(tinc): + #bail out early - looks like removal from running config + if tinc is None: + return None + if 'deleted' in tinc or 'disable' in tinc: + return None + if 'address' not in tinc: + raise ConfigError('address must be set') + if 'private_keyfile' not in tinc: + raise ConfigError('private-keyfile must be set') + if 'hosts_dir' not in tinc: + raise ConfigError('private-keyfile must be set') + if 'node_name' not in tinc: + raise ConfigError('node_name must be set') + if 'connect' in tinc: + node_name=tinc['node_name'] + connect_peer_node_name=tinc['connect'] + if node_name == connect_peer_node_name: + raise ConfigError('The local node name("{local_node}") and the remote target connection node name("{remote_node}") cannot match'.format(local_node=node_name,remote_node=connect_peer_node_name)) + + if 'proxy' in tinc: + if 'address' not in tinc['proxy']: + raise ConfigError('proxy.address must be set') + if 'port' not in tinc['proxy']: + raise ConfigError('proxy.port must be set') + if 'type' not in tinc['proxy']: + raise ConfigError('proxy.type must be set') + if tinc['proxy']['type'] == 'socks5': + if 'password' not in tinc['proxy']: + raise ConfigError('proxy.password must be set') + if tinc['proxy']['type'] == 'socks4' or tinc['proxy']['type'] == 'socks5': + if 'username' not in tinc['proxy']: + raise ConfigError('proxy.username must be set') + if tinc['proxy']['type'] == 'exec': + if 'exec' not in tinc: + raise ConfigError('proxy.exec must be set') + + verify_dhcpv6(tinc) + verify_address(tinc) + verify_vrf(tinc) + + if {'is_bond_member', 'mac'} <= set(tinc): + print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" ' + f'is a member of bond "{is_bond_member}"'.format(**tinc)) + + return None + +def generate(tinc): + if tinc is None: + return None + if 'deleted' in tinc or 'disable' in tinc: + return None + interface = tinc['ifname'] + network = tinc['ifname'] + node_name=tinc['node_name'] + private_keyfile = tinc['private_keyfile'] + public_keyfile = f'/tmp/tinc_{node_name}.key' + config_root_dir='/config/tinc' + config_network_dir=f'/config/tinc/{interface}' + config_hosts_dir=tinc['hosts_dir'] + system_hosts_dir=f'/etc/tinc/{network}/hosts' + tinc_root_dir=f'/etc/tinc' + tinc_network_dir=f'/etc/tinc/{network}' + tinc_main_config=f'{tinc_network_dir}/tinc.conf' + tinc_up=f'{tinc_network_dir}/tinc-up' + tinc_down=f'{tinc_network_dir}/tinc-down' + tinc_host_local_peer_config=f'{config_hosts_dir}/{node_name}' + if tinc: + if not os.path.exists(tinc_network_dir): + os.makedirs(tinc_network_dir, 0o644 ) + if not os.path.exists(system_hosts_dir): + if os.path.exists(config_hosts_dir): + os.symlink(config_hosts_dir,system_hosts_dir) + else: + os.makedirs(config_hosts_dir, 0o644 ) + os.symlink(config_hosts_dir,system_hosts_dir) + elif not os.path.islink(system_hosts_dir): + os.remove(system_hosts_dir) + os.makedirs(config_hosts_dir,0o644) + os.symlink(config_hosts_dir,system_hosts_dir) + os.chdir(tinc_network_dir) + call(f'openssl genrsa -out {private_keyfile} 4096') + call(f'openssl rsa -in {private_keyfile} -pubout -out {public_keyfile}') + render(tinc_main_config, 'tinc/tinc.conf.tmpl', tinc) + render(tinc_up, 'tinc/tinc-up.tmpl', tinc) + render(tinc_down, 'tinc/tinc-down.tmpl', tinc) + render(tinc_host_local_peer_config, 'tinc/hosts_config.tmpl', tinc) + call(f'cat {public_keyfile} >> {tinc_host_local_peer_config} ') + call(f'chmod a+x {tinc_up}') + call(f'chmod a+x {tinc_down}') + + return None + +def apply(tinc): + if tinc is None: + return None + interface = tinc['ifname'] + if 'deleted' in tinc or 'disable' in tinc: + network = tinc['ifname'] + call(f'systemctl stop tinc@{network}') + else: + network = tinc['ifname'] + call(f'systemctl restart tinc@{network}') + + # sleep 250ms + sleep(0.250) + try: + o = VTincIf(interface) + #update interface description used e.g. within SNMP + o.update(tinc) + except: + pass + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1)