diff options
Diffstat (limited to 'src')
27 files changed, 763 insertions, 350 deletions
diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py index 5efdb6a2f..21b47f42a 100755 --- a/src/conf_mode/containers.py +++ b/src/conf_mode/containers.py @@ -75,7 +75,7 @@ def get_config(config=None): base = ['container'] container = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) + get_first_key=True, no_tag_node_value_mangle=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 3675db73b..6c4c6c95b 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -22,6 +22,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configverify import verify_authentication from vyos.configverify import verify_source_interface from vyos.configverify import verify_vrf from vyos.configverify import verify_mtu_ipv6 @@ -51,6 +52,7 @@ def verify(pppoe): return None verify_source_interface(pppoe) + verify_authentication(pppoe) verify_vrf(pppoe) verify_mtu_ipv6(pppoe) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 1575c83ef..294da8ef9 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -114,7 +114,7 @@ def verify(tunnel): raise ConfigError('Option ignore-df can only be used on GRETAP tunnels!') if dict_search('parameters.ip.no_pmtu_discovery', tunnel) == None: - raise ConfigError('Option ignore-df path MTU discovery to be disabled!') + raise ConfigError('Option ignore-df requires path MTU discovery to be disabled!') def generate(tunnel): diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py deleted file mode 100755 index 976953b31..000000000 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/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 <http://www.gnu.org/licenses/>. - -import os - -from sys import exit - -from vyos.config import Config -from vyos.configdict import get_interface_dict -from vyos.configverify import verify_vrf -from vyos.template import render -from vyos.util import call -from vyos.util import check_kmod -from vyos.util import find_device_file -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -k_mod = ['option', 'usb_wwan', 'usbserial'] - -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', 'wirelessmodem'] - wwan = get_interface_dict(conf, base) - - return wwan - -def verify(wwan): - if 'deleted' in wwan: - return None - - if not 'apn' in wwan: - raise ConfigError('No APN configured for "{ifname}"'.format(**wwan)) - - if not 'device' in wwan: - raise ConfigError('Physical "device" must be configured') - - # we can not use isfile() here as Linux device files are no regular files - # thus the check will return False - dev_path = find_device_file(wwan['device']) - if dev_path is None or not os.path.exists(dev_path): - raise ConfigError('Device "{device}" does not exist'.format(**wwan)) - - verify_vrf(wwan) - - return None - -def generate(wwan): - # set up configuration file path variables where our templates will be - # rendered into - ifname = wwan['ifname'] - config_wwan = f'/etc/ppp/peers/{ifname}' - config_wwan_chat = f'/etc/ppp/peers/chat.{ifname}' - script_wwan_pre_up = f'/etc/ppp/ip-pre-up.d/1010-vyos-wwan-{ifname}' - script_wwan_ip_up = f'/etc/ppp/ip-up.d/1010-vyos-wwan-{ifname}' - script_wwan_ip_down = f'/etc/ppp/ip-down.d/1010-vyos-wwan-{ifname}' - - config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up, - script_wwan_ip_up, script_wwan_ip_down] - - # Always hang-up WWAN connection prior generating new configuration file - call(f'systemctl stop ppp@{ifname}.service') - - if 'deleted' in wwan: - # Delete PPP configuration files - for file in config_files: - if os.path.exists(file): - os.unlink(file) - - else: - wwan['device'] = find_device_file(wwan['device']) - - # Create PPP configuration files - render(config_wwan, 'wwan/peer.tmpl', wwan) - # Create PPP chat script - render(config_wwan_chat, 'wwan/chat.tmpl', wwan) - - # generated script file must be executable - - # Create script for ip-pre-up.d - render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', - wwan, permission=0o755) - # Create script for ip-up.d - render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', - wwan, permission=0o755) - # Create script for ip-down.d - render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', - wwan, permission=0o755) - - return None - -def apply(wwan): - if 'deleted' in wwan: - # bail out early - return None - - if not 'disable' in wwan: - # "dial" WWAN connection - call('systemctl start ppp@{ifname}.service'.format(**wwan)) - - return None - -if __name__ == '__main__': - try: - check_kmod(k_mod) - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py new file mode 100755 index 000000000..31c599145 --- /dev/null +++ b/src/conf_mode/interfaces-wwan.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020-2021 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 + +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_authentication +from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_vrf +from vyos.ifconfig import WWANIf +from vyos.util import cmd +from vyos.util import dict_search +from vyos.template import render +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', 'wwan'] + wwan = get_interface_dict(conf, base) + + return wwan + +def verify(wwan): + if 'deleted' in wwan: + return None + + ifname = wwan['ifname'] + if not 'apn' in wwan: + raise ConfigError(f'No APN configured for "{ifname}"!') + + verify_interface_exists(ifname) + verify_authentication(wwan) + verify_vrf(wwan) + + return None + +def generate(wwan): + return None + +def apply(wwan): + # we only need the modem number. wwan0 -> 0, wwan1 -> 1 + modem = wwan['ifname'].lstrip('wwan') + base_cmd = f'mmcli --modem {modem}' + # Number of bearers is limited - always disconnect first + cmd(f'{base_cmd} --simple-disconnect') + + w = WWANIf(wwan['ifname']) + if 'deleted' in wwan or 'disable' in wwan: + w.remove() + return None + + ip_type = 'ipv4' + slaac = dict_search('ipv6.address.autoconf', wwan) != None + if 'address' in wwan: + if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac): + ip_type = 'ipv4v6' + elif 'dhcpv6' in wwan['address'] or slaac: + ip_type = 'ipv6' + elif 'dhcp' in wwan['address']: + ip_type = 'ipv4' + + options = f'ip-type={ip_type},apn=' + wwan['apn'] + if 'authentication' in wwan: + options += ',user={user},password={password}'.format(**wwan['authentication']) + + command = f'{base_cmd} --simple-connect="{options}"' + cmd(command) + w.update(wwan) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py index 729518c96..c920920ed 100755 --- a/src/conf_mode/service_mdns-repeater.py +++ b/src/conf_mode/service_mdns-repeater.py @@ -16,10 +16,12 @@ import os +from json import loads from sys import exit from netifaces import ifaddresses, interfaces, AF_INET from vyos.config import Config +from vyos.ifconfig.vrrp import VRRP from vyos.template import render from vyos.util import call from vyos import ConfigError @@ -27,6 +29,7 @@ from vyos import airbag airbag.enable() config_file = r'/etc/default/mdns-repeater' +vrrp_running_file = '/run/mdns_vrrp_active' def get_config(config=None): if config: @@ -35,6 +38,9 @@ def get_config(config=None): conf = Config() base = ['service', 'mdns', 'repeater'] mdns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + if mdns: + mdns['vrrp_exists'] = conf.exists('high-availability vrrp') return mdns def verify(mdns): @@ -60,6 +66,18 @@ def verify(mdns): return None +# Get VRRP states from interfaces, returns only interfaces where state is MASTER +def get_vrrp_master(interfaces): + json_data = loads(VRRP.collect('json')) + for group in json_data: + if 'data' in group: + if 'ifp_ifname' in group['data']: + iface = group['data']['ifp_ifname'] + state = group['data']['state'] # 2 = Master + if iface in interfaces and state != 2: + interfaces.remove(iface) + return interfaces + def generate(mdns): if not mdns: return None @@ -68,6 +86,12 @@ def generate(mdns): print('Warning: mDNS repeater will be deactivated because it is disabled') return None + if mdns['vrrp_exists'] and 'vrrp_disable' in mdns: + mdns['interface'] = get_vrrp_master(mdns['interface']) + + if len(mdns['interface']) < 2: + return None + render(config_file, 'mdns-repeater/mdns-repeater.tmpl', mdns) return None @@ -76,7 +100,21 @@ def apply(mdns): call('systemctl stop mdns-repeater.service') if os.path.exists(config_file): os.unlink(config_file) + + if os.path.exists(vrrp_running_file): + os.unlink(vrrp_running_file) else: + if 'vrrp_disable' not in mdns and os.path.exists(vrrp_running_file): + os.unlink(vrrp_running_file) + + if mdns['vrrp_exists'] and 'vrrp_disable' in mdns: + if not os.path.exists(vrrp_running_file): + os.mknod(vrrp_running_file) # vrrp script looks for this file to update mdns repeater + + if len(mdns['interface']) < 2: + call('systemctl stop mdns-repeater.service') + return None + call('systemctl restart mdns-repeater.service') return None diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 4efedd995..433c51e7e 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -23,10 +23,11 @@ from vyos.config import Config from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists from vyos.ifconfig import Interface +from vyos.template import ip_from_cidr from vyos.template import render +from vyos.validate import is_ipv6_link_local from vyos.util import call from vyos.util import dict_search -from vyos.util import get_interface_address from vyos.util import process_named_running from vyos.util import run from vyos.util import cidr_fit @@ -35,9 +36,9 @@ from vyos import airbag airbag.enable() authby_translate = { - 'pre-shared-secret': 'secret', - 'rsa': 'rsasig', - 'x509': 'rsasig' + 'pre-shared-secret': 'psk', + 'rsa': 'pubkey', + 'x509': 'pubkey' } default_pfs = 'dh-group2' pfs_translate = { @@ -73,12 +74,18 @@ any_log_modes = [ ike_ciphers = {} esp_ciphers = {} +dhcp_wait_attempts = 2 +dhcp_wait_sleep = 1 + mark_base = 0x900000 -CA_PATH = "/etc/ipsec.d/cacerts/" -CRL_PATH = "/etc/ipsec.d/crls/" +CERT_PATH="/etc/swanctl/x509/" +KEY_PATH="/etc/swanctl/private/" +CA_PATH = "/etc/swanctl/x509ca/" +CRL_PATH = "/etc/swanctl/x509crl/" DHCP_BASE = "/var/lib/dhcp/dhclient" +DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_waiting" LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] X509_PATH = '/config/auth/' @@ -96,6 +103,7 @@ def get_config(config=None): ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + ipsec['dhcp_no_address'] = {} ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces', 'interface']) ipsec['l2tp_exists'] = conf.exists('vpn l2tp remote-access ipsec-settings ') ipsec['nhrp_exists'] = conf.exists('protocols nhrp tunnel') @@ -119,7 +127,7 @@ def get_config(config=None): if enc and hash: ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}") - ike_ciphers[group] = ','.join(ciphers) + '!' + ike_ciphers[group] = ','.join(ciphers) if 'esp_group' in ipsec: for group, esp_conf in ipsec['esp_group'].items(): @@ -139,7 +147,7 @@ def get_config(config=None): hash = proposal['hash'] if 'hash' in proposal else None if enc and hash: ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}") - esp_ciphers[group] = ','.join(ciphers) + '!' + esp_ciphers[group] = ','.join(ciphers) return ipsec @@ -162,6 +170,15 @@ def verify_rsa_local_key(ipsec): def verify_rsa_key(ipsec, key_name): return dict_search(f'rsa_key_name.{key_name}.rsa_key', ipsec['rsa_keys']) +def get_dhcp_address(iface): + addresses = Interface(iface).get_addr() + if not addresses: + return None + for address in addresses: + if not is_ipv6_link_local(address): + return ip_from_cidr(address) + return None + def verify(ipsec): if not ipsec: return None @@ -252,9 +269,17 @@ def verify(ipsec): if not os.path.exists(f'{DHCP_BASE}_{dhcp_interface}.conf'): raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}") - address = Interface(dhcp_interface).get_addr() + address = get_dhcp_address(dhcp_interface) + count = 0 + while not address and count < dhcp_wait_attempts: + address = get_dhcp_address(dhcp_interface) + count += 1 + sleep(dhcp_wait_sleep) + if not address: - raise ConfigError(f"Failed to get address from dhcp-interface on site-to-site peer {peer}") + ipsec['dhcp_no_address'][peer] = dhcp_interface + print(f"Failed to get address from dhcp-interface on site-to-site peer {peer} -- skipped") + continue if 'vti' in peer_conf: if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf: @@ -291,16 +316,28 @@ def generate(ipsec): data = {} if ipsec: + if ipsec['dhcp_no_address']: + with open(DHCP_HOOK_IFLIST, 'w') as f: + f.write(" ".join(ipsec['dhcp_no_address'].values())) + data = ipsec data['authby'] = authby_translate data['ciphers'] = {'ike': ike_ciphers, 'esp': esp_ciphers} data['marks'] = {} data['rsa_local_key'] = verify_rsa_local_key(ipsec) - data['x509_path'] = X509_PATH if 'site_to_site' in data and 'peer' in data['site_to_site']: for peer, peer_conf in ipsec['site_to_site']['peer'].items(): + if peer in ipsec['dhcp_no_address']: + continue + if peer_conf['authentication']['mode'] == 'x509': + cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['cert_file']) + call(f'cp -f {cert_file} {CERT_PATH}') + + key_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['key']['file']) + call(f'cp -f {key_file} {KEY_PATH}') + ca_cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['ca_cert_file']) call(f'cp -f {ca_cert_file} {CA_PATH}') @@ -312,22 +349,29 @@ def generate(ipsec): if 'local_address' in peer_conf: local_ip = peer_conf['local_address'] elif 'dhcp_interface' in peer_conf: - local_ip = Interface(peer_conf['dhcp_interface']).get_addr() + local_ip = get_dhcp_address(peer_conf['dhcp_interface']) data['site_to_site']['peer'][peer]['local_address'] = local_ip if 'vti' in peer_conf and 'bind' in peer_conf['vti']: vti_interface = peer_conf['vti']['bind'] data['marks'][vti_interface] = get_mark(vti_interface) - else: + + if 'tunnel' in peer_conf: for tunnel, tunnel_conf in peer_conf['tunnel'].items(): - local_prefix = dict_search('local.prefix', tunnel_conf) - remote_prefix = dict_search('remote.prefix', tunnel_conf) + local_prefixes = dict_search('local.prefix', tunnel_conf) + remote_prefixes = dict_search('remote.prefix', tunnel_conf) - if not local_prefix or not remote_prefix: + if not local_prefixes or not remote_prefixes: continue - passthrough = cidr_fit(local_prefix, remote_prefix) + passthrough = [] + + for local_prefix in local_prefixes: + for remote_prefix in remote_prefixes: + if cidr_fit(local_prefix, remote_prefix): + passthrough.append(local_prefix) + data['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough if 'logging' in ipsec and 'log_modes' in ipsec['logging']: diff --git a/src/conf_mode/vpn_rsa-keys.py b/src/conf_mode/vpn_rsa-keys.py index 6cf7eba6e..c6ff369ad 100755 --- a/src/conf_mode/vpn_rsa-keys.py +++ b/src/conf_mode/vpn_rsa-keys.py @@ -29,7 +29,8 @@ from Crypto.PublicKey.RSA import construct airbag.enable() LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] -LOCAL_OUTPUT = '/etc/ipsec.d/certs/localhost.pub' +LOCAL_OUTPUT = '/etc/swanctl/pubkey/localhost.pub' +LOCAL_KEY_OUTPUT = '/etc/swanctl/private/localhost.key' def get_config(config=None): if config: @@ -68,6 +69,7 @@ def generate(conf): if 'local_key' in conf and 'file' in conf['local_key']: local_key = conf['local_key']['file'] local_key_path = get_local_key(local_key) + call(f'sudo cp -f {local_key_path} {LOCAL_KEY_OUTPUT}') call(f'sudo /usr/bin/openssl rsa -in {local_key_path} -pubout -out {LOCAL_OUTPUT}') if 'rsa_key_name' in conf: @@ -82,7 +84,7 @@ def generate(conf): else: remote_key = bytes('-----BEGIN PUBLIC KEY-----\n' + remote_key + '\n-----END PUBLIC KEY-----\n', 'utf-8') - with open(f'/etc/ipsec.d/certs/{key_name}.pub', 'wb') as f: + with open(f'/etc/swanctl/pubkey/{key_name}.pub', 'wb') as f: f.write(remote_key) def migrate_from_vyatta_key(data): diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index a39da8991..936561edc 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -141,7 +141,7 @@ def apply(vrf): # set the default VRF global behaviour bind_all = '0' - if 'bind_to_all' in vrf: + if 'bind-to-all' in vrf: bind_all = '1' call(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') call(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') diff --git a/src/etc/cron.hourly/vyos-logrotate-hourly b/src/etc/cron.hourly/vyos-logrotate-hourly new file mode 100755 index 000000000..f4f56a9c2 --- /dev/null +++ b/src/etc/cron.hourly/vyos-logrotate-hourly @@ -0,0 +1,4 @@ +#!/bin/sh + +test -x /usr/sbin/logrotate || exit 0 +/usr/sbin/logrotate /etc/logrotate.conf diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook index 36edf04f3..a7a9a2ce6 100644..100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook @@ -1,12 +1,44 @@ -#!/usr/bin/env python3 +#!/bin/bash +# +# Copyright (C) 2021 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 sys +if [ "$reason" == "REBOOT" ] || [ "$reason" == "EXPIRE" ]; then + exit 0 +fi + +DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_waiting" + +if [ -f $DHCP_HOOK_IFLIST ] && [ "$reason" == "BOUND" ]; then + if grep -qw $interface $DHCP_HOOK_IFLIST; then + sudo rm $DHCP_HOOK_IFLIST + sudo python3 /usr/libexec/vyos/conf_mode/vpn_ipsec.py + exit 0 + fi +fi + +if [ "$old_ip_address" == "$new_ip_address" ] && [ "$reason" == "BOUND" ]; then + exit 0 +fi +python3 - <<PYEND +import os +import re from vyos.util import call +from vyos.util import cmd -IPSEC_CONF="/etc/ipsec.conf" -IPSEC_SECRETS="/etc/ipsec.secrets" +SWANCTL_CONF="/etc/swanctl/swanctl.conf" def getlines(file): with open(file, 'r') as f: @@ -16,17 +48,25 @@ def writelines(file, lines): with open(file, 'w') as f: f.writelines(lines) +def ipsec_down(ip_address): + # This prevents the need to restart ipsec and kill all active connections, only the stale connection is closed + status = cmd('sudo ipsec statusall') + connection_name = None + for line in status.split("\n"): + if line.find(ip_address) > 0: + regex_match = re.search(r'(peer_[^:\[]+)', line) + if regex_match: + connection_name = regex_match[1] + break + if connection_name: + call(f'sudo ipsec down {connection_name}') + if __name__ == '__main__': interface = os.getenv('interface') new_ip = os.getenv('new_ip_address') old_ip = os.getenv('old_ip_address') - reason = os.getenv('reason') - - if (old_ip == new_ip and reason != 'BOUND') or reason in ['REBOOT', 'EXPIRE']: - sys.exit(0) - conf_lines = getlines(IPSEC_CONF) - secrets_lines = getlines(IPSEC_SECRETS) + conf_lines = getlines(SWANCTL_CONF) found = False to_match = f'# dhcp:{interface}' @@ -40,7 +80,9 @@ if __name__ == '__main__': secrets_lines[i] = line.replace(old_ip, new_ip) if found: - writelines(IPSEC_CONF, conf_lines) - writelines(IPSEC_SECRETS, secrets_lines) - call('sudo /usr/sbin/ipsec rereadall') - call('sudo /usr/sbin/ipsec reload') + writelines(SWANCTL_CONF, conf_lines) + ipsec_down(old_ip) + call('sudo ipsec rereadall') + call('sudo ipsec reload') + call('sudo swanctl -q') +PYEND
\ No newline at end of file diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down index 0e1cd7753..2b66dd9e6 100755 --- a/src/etc/ipsec.d/vti-up-down +++ b/src/etc/ipsec.d/vti-up-down @@ -1,4 +1,18 @@ #!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. ## Script called up strongswan to bring the vti interface up/down based on the state of the IPSec tunnel. ## Called as vti_up_down vti_intf_name diff --git a/src/etc/systemd/system/ModemManager.service.d/override.conf b/src/etc/systemd/system/ModemManager.service.d/override.conf new file mode 100644 index 000000000..07a18460e --- /dev/null +++ b/src/etc/systemd/system/ModemManager.service.d/override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/ModemManager --filter-policy=strict --log-level=INFO --log-timestamps --log-journal diff --git a/src/etc/udev/rules.d/99-vyos-wwan.rules b/src/etc/udev/rules.d/99-vyos-wwan.rules deleted file mode 100644 index 67f30a3dd..000000000 --- a/src/etc/udev/rules.d/99-vyos-wwan.rules +++ /dev/null @@ -1,11 +0,0 @@ -ACTION!="add|change", GOTO="mbim_to_qmi_rules_end" - -SUBSYSTEM!="usb", GOTO="mbim_to_qmi_rules_end" - -# ignore any device with only one configuration -ATTR{bNumConfigurations}=="1", GOTO="mbim_to_qmi_rules_end" - -# force Sierra Wireless MC7710 to configuration #1 -ATTR{idVendor}=="1199",ATTR{idProduct}=="68a2",ATTR{bConfigurationValue}="1" - -LABEL="mbim_to_qmi_rules_end" diff --git a/src/migration-scripts/interfaces/18-to-19 b/src/migration-scripts/interfaces/18-to-19 index 06e07572f..a12c4a6cd 100755 --- a/src/migration-scripts/interfaces/18-to-19 +++ b/src/migration-scripts/interfaces/18-to-19 @@ -14,65 +14,31 @@ # 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 + from sys import argv from sys import exit -from vyos.configtree import ConfigTree - -def migrate_ospf(config, path, interface): - path = path + ['ospf'] - if config.exists(path): - new_base = ['protocols', 'ospf', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ip ospf" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -def migrate_ospfv3(config, path, interface): - path = path + ['ospfv3'] - if config.exists(path): - new_base = ['protocols', 'ospfv3', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - # if "ipv6 ospfv3" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -def migrate_rip(config, path, interface): - path = path + ['rip'] - if config.exists(path): - new_base = ['protocols', 'rip', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ip rip" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) +from vyos.configtree import ConfigTree -def migrate_ripng(config, path, interface): - path = path + ['ripng'] - if config.exists(path): - new_base = ['protocols', 'ripng', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) +def replace_nat_interfaces(config, old, new): + if not config.exists(['nat']): + return + for direction in ['destination', 'source']: + conf_direction = ['nat', direction, 'rule'] + if not config.exists(conf_direction): + return + for rule in config.list_nodes(conf_direction): + conf_rule = conf_direction + [rule] + if config.exists(conf_rule + ['inbound-interface']): + tmp = config.return_value(conf_rule + ['inbound-interface']) + if tmp == old: + config.set(conf_rule + ['inbound-interface'], value=new) + if config.exists(conf_rule + ['outbound-interface']): + tmp = config.return_value(conf_rule + ['outbound-interface']) + if tmp == old: + config.set(conf_rule + ['outbound-interface'], value=new) - # if "ipv6 ripng" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) if __name__ == '__main__': if (len(argv) < 1): @@ -80,62 +46,58 @@ if __name__ == '__main__': exit(1) file_name = argv[1] + with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) - - # - # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" - # - for type in config.list_nodes(['interfaces']): - for interface in config.list_nodes(['interfaces', type]): - ip_base = ['interfaces', type, interface, 'ip'] - ipv6_base = ['interfaces', type, interface, 'ipv6'] - migrate_rip(config, ip_base, interface) - migrate_ripng(config, ipv6_base, interface) - migrate_ospf(config, ip_base, interface) - migrate_ospfv3(config, ipv6_base, interface) - - vif_path = ['interfaces', type, interface, 'vif'] - if config.exists(vif_path): - for vif in config.list_nodes(vif_path): - vif_ip_base = vif_path + [vif, 'ip'] - vif_ipv6_base = vif_path + [vif, 'ipv6'] - ifname = f'{interface}.{vif}' - - migrate_rip(config, vif_ip_base, ifname) - migrate_ripng(config, vif_ipv6_base, ifname) - migrate_ospf(config, vif_ip_base, ifname) - migrate_ospfv3(config, vif_ipv6_base, ifname) - - - vif_s_path = ['interfaces', type, interface, 'vif-s'] - if config.exists(vif_s_path): - for vif_s in config.list_nodes(vif_s_path): - vif_s_ip_base = vif_s_path + [vif_s, 'ip'] - vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] - - # vif-c interfaces MUST be migrated before their parent vif-s - # interface as the migrate_*() functions delete the path! - vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] - if config.exists(vif_c_path): - for vif_c in config.list_nodes(vif_c_path): - vif_c_ip_base = vif_c_path + [vif_c, 'ip'] - vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] - ifname = f'{interface}.{vif_s}.{vif_c}' - - migrate_rip(config, vif_c_ip_base, ifname) - migrate_ripng(config, vif_c_ipv6_base, ifname) - migrate_ospf(config, vif_c_ip_base, ifname) - migrate_ospfv3(config, vif_c_ipv6_base, ifname) - - - ifname = f'{interface}.{vif_s}' - migrate_rip(config, vif_s_ip_base, ifname) - migrate_ripng(config, vif_s_ipv6_base, ifname) - migrate_ospf(config, vif_s_ip_base, ifname) - migrate_ospfv3(config, vif_s_ipv6_base, ifname) + base = ['interfaces', 'wirelessmodem'] + if not config.exists(base): + # Nothing to do + exit(0) + + new_base = ['interfaces', 'wwan'] + config.set(new_base) + config.set_tag(new_base) + for old_interface in config.list_nodes(base): + # convert usb0b1.3p1.2 device identifier and extract 1.3 usb bus id + usb = config.return_value(base + [old_interface, 'device']) + device = usb.split('b')[-1] + busid = device.split('p')[0] + for new_interface in os.listdir('/sys/class/net'): + # we are only interested in interfaces starting with wwan + if not new_interface.startswith('wwan'): + continue + device = os.readlink(f'/sys/class/net/{new_interface}/device') + device = device.split(':')[0] + if busid in device: + config.copy(base + [old_interface], new_base + [new_interface]) + replace_nat_interfaces(config, old_interface, new_interface) + + config.delete(base) + + # Now that we have copied the old wirelessmodem interfaces to wwan + # we can start to migrate also individual config items. + for interface in config.list_nodes(new_base): + # we do no longer need the USB device name + config.delete(new_base + [interface, 'device']) + # set/unset DNS configuration + dns = new_base + [interface, 'no-peer-dns'] + if config.exists(dns): + config.delete(dns) + else: + config.set(['system', 'name-servers-dhcp'], value=interface, replace=False) + + # Backup distance is now handled by DHCP option "default-route-distance" + distance = dns = new_base + [interface, 'backup', 'distance'] + old_default_distance = '10' + if config.exists(distance): + old_default_distance = config.return_value(distance) + config.delete(distance) + config.set(new_base + [interface, 'dhcp-options', 'default-route-distance'], value=old_default_distance) + + # the new wwan interface use regular IP addressing + config.set(new_base + [interface, 'address'], value='dhcp') try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/19-to-20 b/src/migration-scripts/interfaces/19-to-20 index e96663e54..06e07572f 100755 --- a/src/migration-scripts/interfaces/19-to-20 +++ b/src/migration-scripts/interfaces/19-to-20 @@ -18,6 +18,62 @@ from sys import argv from sys import exit from vyos.configtree import ConfigTree +def migrate_ospf(config, path, interface): + path = path + ['ospf'] + if config.exists(path): + new_base = ['protocols', 'ospf', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ip ospf" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_ospfv3(config, path, interface): + path = path + ['ospfv3'] + if config.exists(path): + new_base = ['protocols', 'ospfv3', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ipv6 ospfv3" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_rip(config, path, interface): + path = path + ['rip'] + if config.exists(path): + new_base = ['protocols', 'rip', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ip rip" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_ripng(config, path, interface): + path = path + ['ripng'] + if config.exists(path): + new_base = ['protocols', 'ripng', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ipv6 ripng" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + if __name__ == '__main__': if (len(argv) < 1): print("Must specify file name!") @@ -29,29 +85,57 @@ if __name__ == '__main__': config = ConfigTree(config_file) - for type in ['tunnel', 'l2tpv3']: - base = ['interfaces', type] - if not config.exists(base): - # Nothing to do - continue - - for interface in config.list_nodes(base): - # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap - encap_path = base + [interface, 'encapsulation'] - if type == 'tunnel' and config.exists(encap_path): - tmp = config.return_value(encap_path) - if tmp == 'gre-bridge': - config.set(encap_path, value='gretap') - - # Migrate "interface tunnel|l2tpv3 <interface> local-ip" to source-address - # Migrate "interface tunnel|l2tpv3 <interface> remote-ip" to remote - local_ip_path = base + [interface, 'local-ip'] - if config.exists(local_ip_path): - config.rename(local_ip_path, 'source-address') - - remote_ip_path = base + [interface, 'remote-ip'] - if config.exists(remote_ip_path): - config.rename(remote_ip_path, 'remote') + # + # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" + # + for type in config.list_nodes(['interfaces']): + for interface in config.list_nodes(['interfaces', type]): + ip_base = ['interfaces', type, interface, 'ip'] + ipv6_base = ['interfaces', type, interface, 'ipv6'] + migrate_rip(config, ip_base, interface) + migrate_ripng(config, ipv6_base, interface) + migrate_ospf(config, ip_base, interface) + migrate_ospfv3(config, ipv6_base, interface) + + vif_path = ['interfaces', type, interface, 'vif'] + if config.exists(vif_path): + for vif in config.list_nodes(vif_path): + vif_ip_base = vif_path + [vif, 'ip'] + vif_ipv6_base = vif_path + [vif, 'ipv6'] + ifname = f'{interface}.{vif}' + + migrate_rip(config, vif_ip_base, ifname) + migrate_ripng(config, vif_ipv6_base, ifname) + migrate_ospf(config, vif_ip_base, ifname) + migrate_ospfv3(config, vif_ipv6_base, ifname) + + + vif_s_path = ['interfaces', type, interface, 'vif-s'] + if config.exists(vif_s_path): + for vif_s in config.list_nodes(vif_s_path): + vif_s_ip_base = vif_s_path + [vif_s, 'ip'] + vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] + + # vif-c interfaces MUST be migrated before their parent vif-s + # interface as the migrate_*() functions delete the path! + vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] + if config.exists(vif_c_path): + for vif_c in config.list_nodes(vif_c_path): + vif_c_ip_base = vif_c_path + [vif_c, 'ip'] + vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] + ifname = f'{interface}.{vif_s}.{vif_c}' + + migrate_rip(config, vif_c_ip_base, ifname) + migrate_ripng(config, vif_c_ipv6_base, ifname) + migrate_ospf(config, vif_c_ip_base, ifname) + migrate_ospfv3(config, vif_c_ipv6_base, ifname) + + + ifname = f'{interface}.{vif_s}' + migrate_rip(config, vif_s_ip_base, ifname) + migrate_ripng(config, vif_s_ipv6_base, ifname) + migrate_ospf(config, vif_s_ip_base, ifname) + migrate_ospfv3(config, vif_s_ipv6_base, ifname) try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21 index d1ec2ad3e..e96663e54 100755 --- a/src/migration-scripts/interfaces/20-to-21 +++ b/src/migration-scripts/interfaces/20-to-21 @@ -14,47 +14,48 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported -# having a VTI interface in the CLI but no IPSec configuration - drop VTI -# configuration if this is the case for VyOS 1.4 - -import sys +from sys import argv +from sys import exit from vyos.configtree import ConfigTree if __name__ == '__main__': - if (len(sys.argv) < 1): + if (len(argv) < 1): print("Must specify file name!") - sys.exit(1) - - file_name = sys.argv[1] + exit(1) + file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) - base = ['interfaces', 'vti'] - if not config.exists(base): - # Nothing to do - sys.exit(0) - - ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] - for interface in config.list_nodes(base): - found = False - if config.exists(ipsec_base): - for peer in config.list_nodes(ipsec_base): - if config.exists(ipsec_base + [peer, 'vti', 'bind']): - tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind']) - if tmp == interface: - # Interface was found and we no longer need to search - # for it in our IPSec peers - found = True - break - if not found: - config.delete(base + [interface]) + + for type in ['tunnel', 'l2tpv3']: + base = ['interfaces', type] + if not config.exists(base): + # Nothing to do + continue + + for interface in config.list_nodes(base): + # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap + encap_path = base + [interface, 'encapsulation'] + if type == 'tunnel' and config.exists(encap_path): + tmp = config.return_value(encap_path) + if tmp == 'gre-bridge': + config.set(encap_path, value='gretap') + + # Migrate "interface tunnel|l2tpv3 <interface> local-ip" to source-address + # Migrate "interface tunnel|l2tpv3 <interface> remote-ip" to remote + local_ip_path = base + [interface, 'local-ip'] + if config.exists(local_ip_path): + config.rename(local_ip_path, 'source-address') + + remote_ip_path = base + [interface, 'remote-ip'] + if config.exists(remote_ip_path): + config.rename(remote_ip_path, 'remote') try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) - sys.exit(1) + exit(1) diff --git a/src/migration-scripts/interfaces/21-to-22 b/src/migration-scripts/interfaces/21-to-22 new file mode 100755 index 000000000..d1ec2ad3e --- /dev/null +++ b/src/migration-scripts/interfaces/21-to-22 @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported +# having a VTI interface in the CLI but no IPSec configuration - drop VTI +# configuration if this is the case for VyOS 1.4 + +import sys +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + + file_name = sys.argv[1] + + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + base = ['interfaces', 'vti'] + if not config.exists(base): + # Nothing to do + sys.exit(0) + + ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] + for interface in config.list_nodes(base): + found = False + if config.exists(ipsec_base): + for peer in config.list_nodes(ipsec_base): + if config.exists(ipsec_base + [peer, 'vti', 'bind']): + tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind']) + if tmp == interface: + # Interface was found and we no longer need to search + # for it in our IPSec peers + found = True + break + if not found: + config.delete(base + [interface]) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/policy/0-to-1 b/src/migration-scripts/policy/0-to-1 new file mode 100755 index 000000000..7134920ad --- /dev/null +++ b/src/migration-scripts/policy/0-to-1 @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# T3631: route-map: migrate "set extcommunity-rt" and "set extcommunity-soo" +# to "set extcommunity rt|soo" to match FRR syntax + + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['policy', 'route-map'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + + +for route_map in config.list_nodes(base): + if not config.exists(base + [route_map, 'rule']): + continue + for rule in config.list_nodes(base + [route_map, 'rule']): + base_rule = base + [route_map, 'rule', rule] + + if config.exists(base_rule + ['set', 'extcommunity-rt']): + tmp = config.return_value(base_rule + ['set', 'extcommunity-rt']) + config.delete(base_rule + ['set', 'extcommunity-rt']) + config.set(base_rule + ['set', 'extcommunity', 'rt'], value=tmp) + + + if config.exists(base_rule + ['set', 'extcommunity-soo']): + tmp = config.return_value(base_rule + ['set', 'extcommunity-soo']) + config.delete(base_rule + ['set', 'extcommunity-soo']) + config.set(base_rule + ['set', 'extcommunity', 'soo'], value=tmp) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py index 29b430d53..924a889db 100755 --- a/src/op_mode/ping.py +++ b/src/op_mode/ping.py @@ -50,6 +50,11 @@ options = { 'type': '<seconds>', 'help': 'Number of seconds before ping exits' }, + 'do-not-fragment': { + 'ping': '{command} -M dont', + 'type': 'noarg', + 'help': 'Set DF-bit flag to 1 for no fragmentation' + }, 'flood': { 'ping': 'sudo {command} -f', 'type': 'noarg', @@ -227,4 +232,4 @@ if __name__ == '__main__': # print(f'{command} {host}') os.system(f'{command} {host}') - +
\ No newline at end of file diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py new file mode 100755 index 000000000..249dda2a5 --- /dev/null +++ b/src/op_mode/show_wwan.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 argparse + +from sys import exit +from vyos.util import cmd + +parser = argparse.ArgumentParser() +parser.add_argument("--model", help="Get module model", action="store_true") +parser.add_argument("--revision", help="Get module revision", action="store_true") +parser.add_argument("--capabilities", help="Get module capabilities", action="store_true") +parser.add_argument("--imei", help="Get module IMEI/ESN/MEID", action="store_true") +parser.add_argument("--imsi", help="Get module IMSI", action="store_true") +parser.add_argument("--msisdn", help="Get module MSISDN", action="store_true") +parser.add_argument("--sim", help="Get SIM card status", action="store_true") +parser.add_argument("--signal", help="Get current RF signal info", action="store_true") +parser.add_argument("--firmware", help="Get current RF signal info", action="store_true") + +required = parser.add_argument_group('Required arguments') +required.add_argument("--interface", help="WWAN interface name, e.g. wwan0", required=True) + +def qmi_cmd(device, command, silent=False): + tmp = cmd(f'qmicli --device={device} --device-open-proxy {command}') + tmp = tmp.replace(f'[{cdc}] ', '') + if not silent: + # skip first line as this only holds the info headline + for line in tmp.splitlines()[1:]: + print(line.lstrip()) + return tmp + +if __name__ == '__main__': + args = parser.parse_args() + + # remove the WWAN prefix from the interface, required for the CDC interface + if_num = args.interface.replace('wwan','') + cdc = f'/dev/cdc-wdm{if_num}' + + if args.model: + qmi_cmd(cdc, '--dms-get-model') + elif args.capabilities: + qmi_cmd(cdc, '--dms-get-capabilities') + qmi_cmd(cdc, '--dms-get-band-capabilities') + elif args.revision: + qmi_cmd(cdc, '--dms-get-revision') + elif args.imei: + qmi_cmd(cdc, '--dms-get-ids') + elif args.imsi: + qmi_cmd(cdc, '--dms-uim-get-imsi') + elif args.msisdn: + qmi_cmd(cdc, '--dms-get-msisdn') + elif args.sim: + qmi_cmd(cdc, '--uim-get-card-status') + elif args.signal: + qmi_cmd(cdc, '--nas-get-signal-info') + qmi_cmd(cdc, '--nas-get-rf-band-info') + elif args.firmware: + tmp = qmi_cmd(cdc, '--dms-get-manufacturer', silent=True) + if 'Sierra Wireless' in tmp: + qmi_cmd(cdc, '--dms-swi-get-current-firmware') + else: + qmi_cmd(cdc, '--dms-get-software-version') + else: + parser.print_help() + exit(1) diff --git a/src/op_mode/vpn_ike_sa.py b/src/op_mode/vpn_ike_sa.py index 28da9f8dc..622498a7f 100755 --- a/src/op_mode/vpn_ike_sa.py +++ b/src/op_mode/vpn_ike_sa.py @@ -36,9 +36,9 @@ def ike_sa(peer, nat): peers = [] for conn in sas: for name, sa in conn.items(): - if peer and not name.startswith('peer-' + peer): + if peer and not name.startswith('peer_' + peer): continue - if name.startswith('peer-') and name in peers: + if name.startswith('peer_') and name in peers: continue if nat and 'nat-local' not in sa: continue @@ -49,7 +49,9 @@ def ike_sa(peer, nat): print('%-39s %-39s' % (remote_str, local_str)) state = 'up' if 'state' in sa and s(sa['state']) == 'ESTABLISHED' else 'down' version = 'IKEv' + s(sa['version']) - encryption = f'{s(sa["encr-alg"])}_{s(sa["encr-keysize"])}' if 'encr-alg' in sa else 'n/a' + encryption = f'{s(sa["encr-alg"])}' if 'encr-alg' in sa else 'n/a' + if 'encr-keysize' in sa: + encryption += '_' + s(sa["encr-keysize"]) integrity = s(sa['integ-alg']) if 'integ-alg' in sa else 'n/a' dh_group = s(sa['dh-group']) if 'dh-group' in sa else 'n/a' natt = 'yes' if 'nat-local' in sa and s(sa['nat-local']) == 'yes' else 'no' diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py index 434186abb..582b5ef95 100755 --- a/src/op_mode/vpn_ipsec.py +++ b/src/op_mode/vpn_ipsec.py @@ -100,13 +100,13 @@ def generate_x509_pair(name): print(f'Private key: {X509_PATH}{name}.key') def get_peer_connections(peer, tunnel, return_all = False): - search = rf'^conn (peer-{peer}-(tunnel-[\d]+|vti))$' + search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*' matches = [] - with open(IPSEC_CONF, 'r') as f: + with open(SWANCTL_CONF, 'r') as f: for line in f.readlines(): result = re.match(search, line) if result: - suffix = f'tunnel-{tunnel}' if tunnel.isnumeric() else tunnel + suffix = f'tunnel_{tunnel}' if tunnel.isnumeric() else tunnel if return_all or (result[2] == suffix): matches.append(result[1]) return matches @@ -171,13 +171,14 @@ def debug_peer(peer, tunnel): if not tunnel or tunnel == 'all': tunnel = '' - conn = get_peer_connection(peer, tunnel) + conn = get_peer_connections(peer, tunnel) - if not conn: + if not conns: print('Peer not found, aborting') return - call(f'sudo /usr/sbin/ipsec statusall | grep {conn}') + for conn in conns: + call(f'sudo /usr/sbin/ipsec statusall | grep {conn}') if __name__ == '__main__': parser = argparse.ArgumentParser() diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 8069d7146..cbf321dc8 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -32,6 +32,9 @@ from fastapi.responses import HTMLResponse from fastapi.exceptions import RequestValidationError from fastapi.routing import APIRoute from pydantic import BaseModel, StrictStr, validator +from starlette.datastructures import FormData, MutableHeaders +from starlette.formparsers import FormParser, MultiPartParser +from multipart.multipart import parse_options_header import vyos.config @@ -236,6 +239,35 @@ class MultipartRequest(Request): ERR_PATH_NOT_LIST_OF_STR = False offending_command = {} exception = None + + @property + def orig_headers(self): + self._orig_headers = super().headers + return self._orig_headers + + @property + def headers(self): + self._headers = super().headers.mutablecopy() + self._headers['content-type'] = 'application/json' + return self._headers + + async def form(self) -> FormData: + if not hasattr(self, "_form"): + assert ( + parse_options_header is not None + ), "The `python-multipart` library must be installed to use form parsing." + content_type_header = self.orig_headers.get("Content-Type") + content_type, options = parse_options_header(content_type_header) + if content_type == b"multipart/form-data": + multipart_parser = MultiPartParser(self.orig_headers, self.stream()) + self._form = await multipart_parser.parse() + elif content_type == b"application/x-www-form-urlencoded": + form_parser = FormParser(self.orig_headers, self.stream()) + self._form = await form_parser.parse() + else: + self._form = FormData() + return self._form + async def body(self) -> bytes: if not hasattr(self, "_body"): forms = {} diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index 7e2076820..3b4330e9b 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -37,6 +37,8 @@ logs_handler_syslog.setFormatter(logs_format) logger.addHandler(logs_handler_syslog) logger.setLevel(logging.DEBUG) +mdns_running_file = '/run/mdns_vrrp_active' +mdns_update_command = 'sudo /usr/libexec/vyos/conf_mode/service_mdns-repeater.py' # class for all operations class KeepalivedFifo: @@ -121,6 +123,9 @@ class KeepalivedFifo: logger.info("{} {} changed state to {}".format(n_type, n_name, n_state)) # check and run commands for VRRP instances if n_type == 'INSTANCE': + if os.path.exists(mdns_running_file): + cmd(mdns_update_command) + if n_name in self.vrrp_config['vrrp_groups'] and n_state in self.vrrp_config['vrrp_groups'][n_name]: n_script = self.vrrp_config['vrrp_groups'][n_name].get(n_state) if n_script: @@ -128,6 +133,9 @@ class KeepalivedFifo: # check and run commands for VRRP sync groups # currently, this is not available in VyOS CLI if n_type == 'GROUP': + if os.path.exists(mdns_running_file): + cmd(mdns_update_command) + if n_name in self.vrrp_config['sync_groups'] and n_state in self.vrrp_config['sync_groups'][n_name]: n_script = self.vrrp_config['sync_groups'][n_name].get(n_state) if n_script: diff --git a/src/validators/interface-name b/src/validators/interface-name index 5bac671b1..105815eee 100755 --- a/src/validators/interface-name +++ b/src/validators/interface-name @@ -20,7 +20,7 @@ import re from sys import argv from sys import exit -pattern = '^(bond|br|dum|en|ersp|eth|gnv|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|vti|vtun|vxlan|wg|wlan|wlm)[0-9]+(.\d+)?|lo$' +pattern = '^(bond|br|dum|en|ersp|eth|gnv|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|lo$' if __name__ == '__main__': if len(argv) != 2: diff --git a/src/validators/vrf-name b/src/validators/vrf-name index 7b6313888..c78a80776 100755 --- a/src/validators/vrf-name +++ b/src/validators/vrf-name @@ -34,7 +34,7 @@ if __name__ == '__main__': exit(1) pattern = "^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|" \ - "vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wlm)\d+(\.\d+(v.+)?)?$).*$" + "vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wwan)\d+(\.\d+(v.+)?)?$).*$" if not re.match(pattern, vrf): exit(1) |