diff --git a/interface-definitions/include/constraint/interface-name.xml.i b/interface-definitions/include/constraint/interface-name.xml.i index 3e7c4e667e..be0a4da17e 100644 --- a/interface-definitions/include/constraint/interface-name.xml.i +++ b/interface-definitions/include/constraint/interface-name.xml.i @@ -1,4 +1,4 @@ -(bond|br|dum|en|ersp|eth|gnv|ifb|ipoe|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|sstpc|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|lo +(bond|br|dum|en|ersp|eth|gnv|ifb|ipoe|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|sstpc|tun|utun|veth|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|lo diff --git a/interface-definitions/interfaces_utunnel.xml.in b/interface-definitions/interfaces_utunnel.xml.in new file mode 100644 index 0000000000..9c646074d0 --- /dev/null +++ b/interface-definitions/interfaces_utunnel.xml.in @@ -0,0 +1,56 @@ + + + + + + + Tunnel interface + 400 + + utun[0-9]+ + + tunnel interface must be named utunN + + utunN + Tunnel interface name + + + + #include + #include + #include + + + + Manage type + + external + + + external + Controlled by external program + + + (external) + + + + + + + Tunnel type + + + + + .{0,255} + + Tunnel type too long (limit 255 characters) + + + + + + + + diff --git a/interface-definitions/service_config-sync.xml.in b/interface-definitions/service_config-sync.xml.in index af4e8ed51c..c618edd3c5 100644 --- a/interface-definitions/service_config-sync.xml.in +++ b/interface-definitions/service_config-sync.xml.in @@ -178,6 +178,12 @@ + + + Custom tunnel interface + + + Virtual Ethernet interface diff --git a/op-mode-definitions/restart-utunnel.xml.in b/op-mode-definitions/restart-utunnel.xml.in new file mode 100644 index 0000000000..b5cbfcb4fc --- /dev/null +++ b/op-mode-definitions/restart-utunnel.xml.in @@ -0,0 +1,16 @@ + + + + + + + Restart a custom tunnel backend service + + interfaces utunnel + + + sudo ${vyos_op_scripts_dir}/utunnel.py restart --interface="$3" + + + + diff --git a/op-mode-definitions/show-interfaces-utunnel.xml.in b/op-mode-definitions/show-interfaces-utunnel.xml.in new file mode 100644 index 0000000000..9bb990565b --- /dev/null +++ b/op-mode-definitions/show-interfaces-utunnel.xml.in @@ -0,0 +1,20 @@ + + + + + + + + + Show specified custom tunnel interface information + + interfaces utunnel + + + ${vyos_op_scripts_dir}/utunnel.py show_status --interface="$4" + + + + + + diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index 206b2bba19..5cd158c6b7 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -39,3 +39,4 @@ from vyos.ifconfig.veth import VethIf from vyos.ifconfig.wwan import WWANIf from vyos.ifconfig.sstpc import SSTPCIf +from vyos.ifconfig.utunnel import CustomTunnelIf diff --git a/python/vyos/ifconfig/utunnel.py b/python/vyos/ifconfig/utunnel.py new file mode 100644 index 0000000000..e749462a05 --- /dev/null +++ b/python/vyos/ifconfig/utunnel.py @@ -0,0 +1,122 @@ +# Copyright 2019-2021 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . +import glob +import os +from pathlib import Path +import yaml + +from vyos.ifconfig import Interface +from vyos.ifconfig import Operational + +utunnel_config_directory = '/config/utunnels/' + + +def get_utunnel_config(tunnel_type): + config_file = os.path.join(utunnel_config_directory, '{}.yaml'.format(tunnel_type)) + + config = {} + if os.path.exists(config_file): + config = yaml.safe_load(open(config_file)) + + defaults = { + 'scripts': { + 'start': '', + 'stop': '', + 'update': '', + 'status': '', + }, + } + defaults.update(config) + + return defaults + + +def get_custom_tunnel_types() -> list[str]: + pattern = os.path.join(utunnel_config_directory, '*.yaml') + + types = [] + for file_path in glob.glob(pattern): + basename = Path(file_path).stem + types.append(basename) + + return sorted(types) + + +class CustomTunnelOperational(Operational): + + def get_tunnel_type(self): + from vyos.config import Config + + c = Config() + return c.return_effective_value(['interfaces', 'utunnel', self.config['ifname'], 'tunnel-type']) + + def start(self): + config = get_utunnel_config(self.get_tunnel_type()) + if config['scripts']['start']: + self._cmd(config['scripts']['start'].replace('{device}', self.ifname)) + + def stop(self): + config = get_utunnel_config(self.get_tunnel_type()) + if config['scripts']['stop']: + self._cmd(config['scripts']['stop'].replace('{device}', self.ifname)) + + def restart(self): + self.stop() + self.start() + + def update(self): + config = get_utunnel_config(self.get_tunnel_type()) + if config['scripts']['update']: + self._cmd(config['scripts']['update'].replace('{device}', self.ifname)) + + def show_status(self): + config = get_utunnel_config(self.get_tunnel_type()) + if config['scripts']['status']: + print(self._cmd(config['scripts']['status'].replace('{device}', self.ifname))) + + +@Interface.register +class CustomTunnelIf(Interface): + """ + A dummy interface for custom tunnels + """ + + OperationalClass = CustomTunnelOperational + + iftype = 'utunnel' + definition = { + **Interface.definition, + **{ + 'section': 'utunnel', + 'prefixes': ['utun', ], + 'eternal': 'utun[0-9]+$', + }, + } + + def _create(self): + # don't create this interface as it is managed outside + pass + + def _delete(self): + # don't create this interface as it is managed outside + pass + + def get_mac(self): + """ Get a synthetic MAC address. """ + return self.get_mac_synthetic() + + def update(self, config): + # don't perform any update + pass diff --git a/src/completion/list_utunnel_types.py b/src/completion/list_utunnel_types.py new file mode 100755 index 0000000000..7f6e329ee7 --- /dev/null +++ b/src/completion/list_utunnel_types.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 argparse +import glob +import os +import sys +from pathlib import Path + +directory = '/config/utunnels/' +pattern = os.path.join(directory, '*.yaml') + +parser = argparse.ArgumentParser(description='list available custom tunnel types') + + +def get_custom_tunnel_types() -> list[str]: + types = [] + for file_path in glob.glob(pattern): + basename = Path(file_path).stem + types.append(basename) + + return sorted(types) + + +if __name__ == '__main__': + args = parser.parse_args() + print("\n".join(get_custom_tunnel_types())) + sys.exit(0) diff --git a/src/conf_mode/interfaces_utunnel.py b/src/conf_mode/interfaces_utunnel.py new file mode 100755 index 0000000000..f7c5e762b8 --- /dev/null +++ b/src/conf_mode/interfaces_utunnel.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2024 yOS 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 . + +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configdict import is_node_changed +from vyos.configverify import verify_address +from vyos.ifconfig import Interface +from vyos.ifconfig import CustomTunnelIf +from vyos.utils.dict import dict_search +from vyos.utils.network import get_interface_config +from vyos.utils.network import interface_exists +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least + the interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'utunnel'] + ifname, utunnel = get_interface_dict(conf, base) + + return utunnel + + +def verify(utunnel): + if 'deleted' in utunnel: + return None + + verify_address(utunnel) + + # todo: if tunnel_type has no related yaml definitions, throws a warning + + return None + + +def generate(utunnel): + return None + + +def apply(utunnel): + interface = utunnel['ifname'] + + intf = CustomTunnelIf(**utunnel) + + if 'disable' in utunnel or 'deleted' in utunnel: + # WireGuard only supports peer removal based on the configured public-key, + # by deleting the entire interface this is the shortcut instead of parsing + # out all peers and removing them one by one. + # + # Peer reconfiguration will always come with a short downtime while the + # WireGuard interface is recreated (see below) + + # call stop script + intf.operational.stop() + return None + + # for custom tunnels, if manage-type is external, nothing need to be done. + # tun.update(utunnel) + # Users should manage external programs by systemd + # intf.operational.start() + + return None + + +if __name__ == '__main__': + try: + c = get_config() + generate(c) + verify(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/op_mode/utunnel.py b/src/op_mode/utunnel.py new file mode 100755 index 0000000000..ca56e833cc --- /dev/null +++ b/src/op_mode/utunnel.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022-2023 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 sys +import typing + +import vyos.opmode + +from vyos.ifconfig import CustomTunnelIf +from vyos.configquery import ConfigTreeQuery + + +def _verify(func): + """Decorator checks if WireGuard interface config exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + interface = kwargs.get('interface') + if not config.exists(['interfaces', 'utunnel', interface]): + unconf_message = f'Custom Tunnel interface {interface} is not configured' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + + return _wrapper + + +@_verify +def restart(raw: bool, interface: str): + intf = CustomTunnelIf(interface) + return intf.operational.restart() + + +@_verify +def show_status(raw: bool, interface: str): + intf = CustomTunnelIf(interface) + return intf.operational.show_status() + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1)