diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/interfaces-loopback.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/le_cert.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/protocols_bgp.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/system-option.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 42 | ||||
-rw-r--r-- | src/etc/sysctl.d/30-vyos-router.conf | 7 | ||||
-rwxr-xr-x | src/migration-scripts/interfaces/19-to-20 | 130 | ||||
-rwxr-xr-x | src/migration-scripts/interfaces/20-to-21 | 130 | ||||
-rwxr-xr-x | src/op_mode/ikev2_profile_generator.py | 171 | ||||
-rwxr-xr-x | src/op_mode/show_dhcp.py | 7 | ||||
-rwxr-xr-x | src/op_mode/show_dhcpv6.py | 6 | ||||
-rwxr-xr-x | src/services/vyos-configd | 7 | ||||
-rw-r--r-- | src/systemd/isc-dhcp-server.service | 6 |
13 files changed, 359 insertions, 168 deletions
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py index 30a27abb4..193334443 100755 --- a/src/conf_mode/interfaces-loopback.py +++ b/src/conf_mode/interfaces-loopback.py @@ -45,8 +45,8 @@ def generate(loopback): return None def apply(loopback): - l = LoopbackIf(loopback['ifname']) - if 'deleted' in loopback.keys(): + l = LoopbackIf(**loopback) + if 'deleted' in loopback: l.remove() else: l.update(loopback) diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py index 755c89966..6e169a3d5 100755 --- a/src/conf_mode/le_cert.py +++ b/src/conf_mode/le_cert.py @@ -22,6 +22,7 @@ from vyos.config import Config from vyos import ConfigError from vyos.util import cmd from vyos.util import call +from vyos.util import is_systemd_service_running from vyos import airbag airbag.enable() @@ -87,8 +88,7 @@ def generate(cert): # certbot will attempt to reload nginx, even with 'certonly'; # start nginx if not active - ret = call('systemctl is-active --quiet nginx.service') - if ret: + if not is_systemd_service_running('nginx.service'): call('systemctl start nginx.service') request_certbot(cert) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 1a2fabded..9ecfd07fe 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -57,6 +57,11 @@ def get_config(config=None): if not conf.exists(base): bgp.update({'deleted' : ''}) + if not vrf: + # We are running in the default VRF context, thus we can not delete + # our main BGP instance if there are dependent BGP VRF instances. + bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], + key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) return bgp # We also need some additional information from the config, prefix-lists @@ -96,6 +101,11 @@ def verify_remote_as(peer_config, bgp_config): def verify(bgp): if not bgp or 'deleted' in bgp: + if 'dependent_vrfs' in bgp: + for vrf, vrf_options in bgp['dependent_vrfs'].items(): + if dict_search('protocols.bgp', vrf_options) != None: + raise ConfigError('Cannot delete default BGP instance, ' \ + 'dependent VRF instance(s) exist!') return None if 'local_as' not in bgp: diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index 454611c55..55cf6b142 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -24,6 +24,7 @@ from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render from vyos.util import cmd +from vyos.util import is_systemd_service_running from vyos.validate import is_addr_assigned from vyos.xml import defaults from vyos import ConfigError @@ -114,7 +115,7 @@ def apply(options): if 'performance' in options: cmd('systemctl restart tuned.service') # wait until daemon has started before sending configuration - while (int(os.system('systemctl is-active --quiet tuned.service')) != 0): + while (not is_systemd_service_running('tuned.service')): sleep(0.250) cmd('tuned-adm profile network-{performance}'.format(**options)) else: diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index f1c6b216b..11ff12e94 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -49,13 +49,14 @@ airbag.enable() dhcp_wait_attempts = 2 dhcp_wait_sleep = 1 -swanctl_dir = '/etc/swanctl' -ipsec_conf = '/etc/ipsec.conf' -ipsec_secrets = '/etc/ipsec.secrets' -charon_conf = '/etc/strongswan.d/charon.conf' -charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' -interface_conf = '/etc/strongswan.d/interfaces_use.conf' -swanctl_conf = f'{swanctl_dir}/swanctl.conf' +swanctl_dir = '/etc/swanctl' +ipsec_conf = '/etc/ipsec.conf' +ipsec_secrets = '/etc/ipsec.secrets' +charon_conf = '/etc/strongswan.d/charon.conf' +charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' +charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf' +interface_conf = '/etc/strongswan.d/interfaces_use.conf' +swanctl_conf = f'{swanctl_dir}/swanctl.conf' default_install_routes = 'yes' @@ -110,6 +111,12 @@ def get_config(config=None): ipsec['remote_access']['connection'][rw] = dict_merge(default_values, ipsec['remote_access']['connection'][rw]) + if 'remote_access' in ipsec and 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']: + default_values = defaults(base + ['remote-access', 'radius', 'server']) + for server in ipsec['remote_access']['radius']['server']: + ipsec['remote_access']['radius']['server'][server] = dict_merge(default_values, + ipsec['remote_access']['radius']['server'][server]) + ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface']) @@ -243,6 +250,11 @@ def verify(ipsec): if 'ike_group' in ra_conf: if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']: raise ConfigError(f"Invalid ike-group on {name} remote-access config") + + ike = ra_conf['ike_group'] + if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2': + raise ConfigError('IPSec remote-access connections requires IKEv2!') + else: raise ConfigError(f"Missing ike-group on {name} remote-access config") @@ -263,13 +275,19 @@ def verify(ipsec): if 'pre_shared_secret' not in ra_conf['authentication']: raise ConfigError(f"Missing pre-shared-key on {name} remote-access config") + + if 'client_mode' in ra_conf['authentication']: + if ra_conf['authentication']['client_mode'] == 'eap-radius': + if 'radius' not in ipsec['remote_access'] or 'server' not in ipsec['remote_access']['radius'] or len(ipsec['remote_access']['radius']['server']) == 0: + raise ConfigError('RADIUS authentication requires at least one server') + if 'pool' in ra_conf: if 'dhcp' in ra_conf['pool'] and len(ra_conf['pool']) > 1: raise ConfigError(f'Can not use both DHCP and a predefined address pool for "{name}"!') for pool in ra_conf['pool']: if pool == 'dhcp': - if dict_search('options.remote_access.dhcp.server', ipsec) == None: + if dict_search('remote_access.dhcp.server', ipsec) == None: raise ConfigError('IPSec DHCP server is not configured!') elif 'pool' not in ipsec['remote_access'] or pool not in ipsec['remote_access']['pool']: @@ -297,6 +315,10 @@ def verify(ipsec): if v4_addr_and_exclude or v6_addr_and_exclude: raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!') + if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']: + for server, server_config in ipsec['remote_access']['radius']['server'].items(): + if 'key' not in server_config: + raise ConfigError(f'Missing RADIUS secret key for server "{server}"') if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']: for peer, peer_conf in ipsec['site_to_site']['peer'].items(): @@ -449,7 +471,8 @@ def generate(ipsec): cleanup_pki_files() if not ipsec: - for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf, interface_conf, swanctl_conf]: + for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf, + charon_radius_conf, interface_conf, swanctl_conf]: if os.path.isfile(config_file): os.unlink(config_file) render(charon_conf, 'ipsec/charon.tmpl', {'install_routes': default_install_routes}) @@ -518,6 +541,7 @@ def generate(ipsec): render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', ipsec) render(charon_conf, 'ipsec/charon.tmpl', ipsec) render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', ipsec) + render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.tmpl', ipsec) render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', ipsec) render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', ipsec) diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index 8265e12dc..e03d3a29c 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -72,6 +72,12 @@ net.ipv4.conf.default.send_redirects=1 # Increase size of buffer for netlink net.core.rmem_max=2097152 +# Remove IPv4 and IPv6 routes from forward information base when link goes down +net.ipv4.conf.all.ignore_routes_with_linkdown=1 +net.ipv4.conf.default.ignore_routes_with_linkdown=1 +net.ipv6.conf.all.ignore_routes_with_linkdown=1 +net.ipv6.conf.default.ignore_routes_with_linkdown=1 + # Enable packet forwarding for IPv6 net.ipv6.conf.all.forwarding=1 @@ -81,6 +87,7 @@ net.ipv6.route.max_size = 262144 # Do not forget IPv6 addresses when a link goes down net.ipv6.conf.default.keep_addr_on_down=1 net.ipv6.conf.all.keep_addr_on_down=1 +net.ipv6.route.skip_notify_on_dev_down=1 # Default value of 20 seems to interfere with larger OSPF and VRRP setups net.ipv4.igmp_max_memberships = 512 diff --git a/src/migration-scripts/interfaces/19-to-20 b/src/migration-scripts/interfaces/19-to-20 index 06e07572f..e96663e54 100755 --- a/src/migration-scripts/interfaces/19-to-20 +++ b/src/migration-scripts/interfaces/19-to-20 @@ -18,62 +18,6 @@ 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!") @@ -85,57 +29,29 @@ if __name__ == '__main__': 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) + 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: diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21 index e96663e54..06e07572f 100755 --- a/src/migration-scripts/interfaces/20-to-21 +++ b/src/migration-scripts/interfaces/20-to-21 @@ -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/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py index 4ff37341c..d45525431 100755 --- a/src/op_mode/ikev2_profile_generator.py +++ b/src/op_mode/ikev2_profile_generator.py @@ -19,17 +19,99 @@ import argparse from jinja2 import Template from sys import exit from socket import getfqdn +from cryptography.x509.oid import NameOID from vyos.config import Config -from vyos.template import render_to_string -from cryptography.x509.oid import NameOID from vyos.pki import load_certificate +from vyos.template import render_to_string +from vyos.util import ask_input + +# Apple profiles only support one IKE/ESP encryption cipher and hash, whereas +# VyOS comes with a multitude of different proposals for a connection. +# +# We take all available proposals from the VyOS CLI and ask the user which one +# he would like to get enabled in his profile - thus there is limited possibility +# to select a proposal that is not supported on the connection profile. +# +# IOS supports IKE-SA encryption algorithms: +# - DES +# - 3DES +# - AES-128 +# - AES-256 +# - AES-128-GCM +# - AES-256-GCM +# - ChaCha20Poly1305 +# +vyos2apple_cipher = { + '3des' : '3DES', + 'aes128' : 'AES-128', + 'aes256' : 'AES-256', + 'aes128gcm128' : 'AES-128-GCM', + 'aes256gcm128' : 'AES-256-GCM', + 'chacha20poly1305' : 'ChaCha20Poly1305', +} + +# Windows supports IKE-SA encryption algorithms: +# - DES3 +# - AES128 +# - AES192 +# - AES256 +# - GCMAES128 +# - GCMAES192 +# - GCMAES256 +# +vyos2windows_cipher = { + '3des' : 'DES3', + 'aes128' : 'AES128', + 'aes192' : 'AES192', + 'aes256' : 'AES256', + 'aes128gcm128' : 'GCMAES128', + 'aes192gcm128' : 'GCMAES192', + 'aes256gcm128' : 'GCMAES256', +} + +# IOS supports IKE-SA integrity algorithms: +# - SHA1-96 +# - SHA1-160 +# - SHA2-256 +# - SHA2-384 +# - SHA2-512 +# +vyos2apple_integrity = { + 'sha1' : 'SHA1-96', + 'sha1_160' : 'SHA1-160', + 'sha256' : 'SHA2-256', + 'sha384' : 'SHA2-384', + 'sha512' : 'SHA2-512', +} + +# Windows supports IKE-SA integrity algorithms: +# - SHA1-96 +# - SHA1-160 +# - SHA2-256 +# - SHA2-384 +# - SHA2-512 +# +vyos2windows_integrity = { + 'sha1' : 'SHA196', + 'sha256' : 'SHA256', + 'aes128gmac' : 'GCMAES128', + 'aes192gmac' : 'GCMAES192', + 'aes256gmac' : 'GCMAES256', +} + +# IOS 14.2 and later do no support dh-group 1,2 and 5. Supported DH groups would +# be: 14, 15, 16, 17, 18, 19, 20, 21, 31 +ios_supported_dh_groups = ['14', '15', '16', '17', '18', '19', '20', '21', '31'] +# Windows 10 only allows a limited set of DH groups +windows_supported_dh_groups = ['1', '2', '14', '24'] parser = argparse.ArgumentParser() -parser.add_argument("--connection", action="store", help="IPsec IKEv2 remote-access connection name from CLI", required=True) -parser.add_argument("--remote", action="store", help="VPN connection remote-address where the client will connect to", required=True) -parser.add_argument("--profile", action="store", help="IKEv2 profile name used in the profile list on the device") -parser.add_argument("--name", action="store", help="VPN connection name as seen in the VPN application later") +parser.add_argument('--os', const='all', nargs='?', choices=['ios', 'windows'], help='Operating system used for config generation', required=True) +parser.add_argument("--connection", action="store", help='IPsec IKEv2 remote-access connection name from CLI', required=True) +parser.add_argument("--remote", action="store", help='VPN connection remote-address where the client will connect to', required=True) +parser.add_argument("--profile", action="store", help='IKEv2 profile name used in the profile list on the device') +parser.add_argument("--name", action="store", help='VPN connection name as seen in the VPN application later') args = parser.parse_args() ipsec_base = ['vpn', 'ipsec'] @@ -43,7 +125,7 @@ profile_name = 'VyOS IKEv2 Profile' if args.profile: profile_name = args.profile -vpn_name = 'VyOS IKEv2 Profile' +vpn_name = 'VyOS IKEv2 VPN' if args.name: vpn_name = args.name @@ -73,7 +155,76 @@ data['ca_cn'] = ca_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].v data['cert_cn'] = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value data['ca_cert'] = conf.return_value(pki_base + ['ca', ca_name, 'certificate']) -data['esp_proposal'] = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'], key_mangling=('-', '_'), get_first_key=True) -data['ike_proposal'] = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'], key_mangling=('-', '_'), get_first_key=True) +esp_proposals = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'], + key_mangling=('-', '_'), get_first_key=True) +ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'], + key_mangling=('-', '_'), get_first_key=True) + + +# This script works only for Apple iOS/iPadOS and Windows. Both operating systems +# have different limitations thus we load the limitations based on the operating +# system used. + +vyos2client_cipher = vyos2apple_cipher if args.os == 'ios' else vyos2windows_cipher; +vyos2client_integrity = vyos2apple_integrity if args.os == 'ios' else vyos2windows_integrity; +supported_dh_groups = ios_supported_dh_groups if args.os == 'ios' else windows_supported_dh_groups; + +# Create a dictionary containing client conform IKE settings +ike = {} +count = 1 +for _, proposal in ike_proposal.items(): + if {'dh_group', 'encryption', 'hash'} <= set(proposal): + if (proposal['encryption'] in set(vyos2client_cipher) and + proposal['hash'] in set(vyos2client_integrity) and + proposal['dh_group'] in set(supported_dh_groups)): + + # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme + proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ] + proposal['hash'] = vyos2client_integrity[ proposal['hash'] ] + + ike.update( { str(count) : proposal } ) + count += 1 + +# Create a dictionary containing Apple conform ESP settings +esp = {} +count = 1 +for _, proposal in esp_proposals.items(): + if {'encryption', 'hash'} <= set(proposal): + if proposal['encryption'] in set(vyos2client_cipher) and proposal['hash'] in set(vyos2client_integrity): + # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme + proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ] + proposal['hash'] = vyos2client_integrity[ proposal['hash'] ] + + esp.update( { str(count) : proposal } ) + count += 1 +try: + if len(ike) > 1: + # Propare the input questions for the user + tmp = '\n' + for number, options in ike.items(): + tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}, DH group {options["dh_group"]}\n' + tmp += '\nSelect one of the above IKE groups: ' + data['ike_encryption'] = ike[ ask_input(tmp, valid_responses=list(ike)) ] + else: + data['ike_encryption'] = ike['1'] + + if len(esp) > 1: + tmp = '\n' + for number, options in esp.items(): + tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}\n' + tmp += '\nSelect one of the above ESP groups: ' + data['esp_encryption'] = esp[ ask_input(tmp, valid_responses=list(esp)) ] + else: + data['esp_encryption'] = esp['1'] + +except KeyboardInterrupt: + exit("Interrupted") -print(render_to_string('ipsec/ios_profile.tmpl', data)) +print('\n\n==== <snip> ====') +if args.os == 'ios': + print(render_to_string('ipsec/ios_profile.tmpl', data)) + print('==== </snip> ====\n') + print('Save the XML from above to a new file named "vyos.mobileconfig" and E-Mail it to your phone.') +elif args.os == 'windows': + print(render_to_string('ipsec/windows_profile.tmpl', data)) + print('==== </snip> ====\n') diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index ff1e3cc56..4df275e04 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-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 @@ -27,8 +27,7 @@ from datetime import datetime from isc_dhcp_leases import Lease, IscDhcpLeases from vyos.config import Config -from vyos.util import call - +from vyos.util import is_systemd_service_running lease_file = "/config/dhcpd.leases" pool_key = "shared-networkname" @@ -217,7 +216,7 @@ if __name__ == '__main__': exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if call('systemctl -q is-active isc-dhcp-server.service') != 0: + if not is_systemd_service_running('isc-dhcp-server.service'): print("WARNING: DHCP server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index f70f04298..1f987ff7b 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-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 @@ -27,7 +27,7 @@ from datetime import datetime from isc_dhcp_leases import Lease, IscDhcpLeases from vyos.config import Config -from vyos.util import call +from vyos.util import is_systemd_service_running lease_file = "/config/dhcpdv6.leases" pool_key = "shared-networkname" @@ -202,7 +202,7 @@ if __name__ == '__main__': exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if call('systemctl -q is-active isc-dhcp-server6.service') != 0: + if not is_systemd_service_running('isc-dhcp-server6.service'): print("WARNING: DHCPv6 server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 6f770b696..670b6e66a 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -133,8 +133,7 @@ def explicit_print(path, mode, msg): logger.critical("error explicit_print") def run_script(script, config, args) -> int: - if args: - script.argv = args + script.argv = args config.set_level([]) try: c = script.get_config(config) @@ -208,7 +207,7 @@ def process_node_data(config, data) -> int: return R_ERROR_DAEMON script_name = None - args = None + args = [] res = re.match(r'^(VYOS_TAGNODE_VALUE=[^/]+)?.*\/([^/]+).py(.*)', data) if res.group(1): @@ -221,7 +220,7 @@ def process_node_data(config, data) -> int: return R_ERROR_DAEMON if res.group(3): args = res.group(3).split() - args.insert(0, f'{script_name}.py') + args.insert(0, f'{script_name}.py') if script_name not in include_set: return R_PASS diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service index 9aa70a7cc..a7d86e69c 100644 --- a/src/systemd/isc-dhcp-server.service +++ b/src/systemd/isc-dhcp-server.service @@ -14,10 +14,10 @@ Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhc PIDFile=/run/dhcp-server/dhcpd.pid ExecStartPre=/bin/sh -ec '\ touch ${LEASE_FILE}; \ -chown dhcpd:nogroup ${LEASE_FILE}* ; \ +chown dhcpd:vyattacfg ${LEASE_FILE}* ; \ chmod 664 ${LEASE_FILE}* ; \ -/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' -ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} +/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' +ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} Restart=always [Install] |