diff options
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-x | src/conf_mode/conntrack.py | 140 | ||||
-rwxr-xr-x | src/conf_mode/conntrack_sync.py | 135 | ||||
-rwxr-xr-x | src/conf_mode/dhcp_server.py | 45 | ||||
-rwxr-xr-x | src/conf_mode/firewall.py | 73 | ||||
-rwxr-xr-x | src/conf_mode/flow_accounting_conf.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-bonding.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-dummy.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-pseudo-ethernet.py | 7 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-tunnel.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-vti.py | 97 | ||||
-rwxr-xr-x | src/conf_mode/protocols_isis.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/protocols_nhrp.py | 122 | ||||
-rwxr-xr-x | src/conf_mode/service_router-advert.py | 12 | ||||
-rwxr-xr-x | src/conf_mode/system_sysctl.py | 73 | ||||
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 358 | ||||
-rwxr-xr-x | src/conf_mode/vpn_rsa-keys.py | 111 |
16 files changed, 1162 insertions, 40 deletions
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py new file mode 100755 index 000000000..4e6e39c0f --- /dev/null +++ b/src/conf_mode/conntrack.py @@ -0,0 +1,140 @@ +#!/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 os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.util import cmd +from vyos.util import run +from vyos.util import process_named_running +from vyos.util import dict_search +from vyos.template import render +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf' +sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf' + +# Every ALG (Application Layer Gateway) consists of either a Kernel Object +# also called a Kernel Module/Driver or some rules present in iptables +module_map = { + 'ftp' : { + 'ko' : ['nf_nat_ftp', 'nf_conntrack_ftp'], + }, + 'h323' : { + 'ko' : ['nf_nat_h323', 'nf_conntrack_h323'], + }, + 'nfs' : { + 'iptables' : ['VYATTA_CT_HELPER --table raw --proto tcp --dport 111 --jump CT --helper rpc', + 'VYATTA_CT_HELPER --table raw --proto udp --dport 111 --jump CT --helper rpc'], + }, + 'pptp' : { + 'ko' : ['nf_nat_pptp', 'nf_conntrack_pptp'], + }, + 'sip' : { + 'ko' : ['nf_nat_sip', 'nf_conntrack_sip'], + }, + 'sqlnet' : { + 'iptables' : ['VYATTA_CT_HELPER --table raw --proto tcp --dport 1521 --jump CT --helper tns', + 'VYATTA_CT_HELPER --table raw --proto tcp --dport 1525 --jump CT --helper tns', + 'VYATTA_CT_HELPER --table raw --proto tcp --dport 1536 --jump CT --helper tns'], + }, + 'tftp' : { + 'ko' : ['nf_nat_tftp', 'nf_conntrack_tftp'], + }, +} + +def resync_conntrackd(): + tmp = run('/usr/libexec/vyos/conf_mode/conntrack_sync.py') + if tmp > 0: + print('ERROR: error restarting conntrackd!') + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['system', 'conntrack'] + + conntrack = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=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) + conntrack = dict_merge(default_values, conntrack) + + return conntrack + +def verify(conntrack): + return None + +def generate(conntrack): + render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.tmpl', conntrack) + render(sysctl_file, 'conntrack/sysctl.conf.tmpl', conntrack) + + return None + +def apply(conntrack): + # Depending on the enable/disable state of the ALG (Application Layer Gateway) + # modules we need to either insmod or rmmod the helpers. + for module, module_config in module_map.items(): + if dict_search(f'modules.{module}.disable', conntrack) != None: + if 'ko' in module_config: + for mod in module_config['ko']: + # Only remove the module if it's loaded + if os.path.exists(f'/sys/module/{mod}'): + cmd(f'rmmod {mod}') + if 'iptables' in module_config: + for rule in module_config['iptables']: + print(f'iptables --delete {rule}') + cmd(f'iptables --delete {rule}') + else: + if 'ko' in module_config: + for mod in module_config['ko']: + cmd(f'modprobe {mod}') + if 'iptables' in module_config: + for rule in module_config['iptables']: + # Only install iptables rule if it does not exist + tmp = run(f'iptables --check {rule}') + if tmp > 0: + cmd(f'iptables --insert {rule}') + + + if process_named_running('conntrackd'): + # Reload conntrack-sync daemon to fetch new sysctl values + resync_conntrackd() + + # We silently ignore all errors + # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080 + cmd(f'sysctl -f {sysctl_file}') + + 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/conntrack_sync.py b/src/conf_mode/conntrack_sync.py new file mode 100755 index 000000000..7f22fa2dd --- /dev/null +++ b/src/conf_mode/conntrack_sync.py @@ -0,0 +1,135 @@ +#!/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 os + +from sys import exit +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configverify import verify_interface_exists +from vyos.util import call +from vyos.util import dict_search +from vyos.util import process_named_running +from vyos.util import read_file +from vyos.util import run +from vyos.template import render +from vyos.template import get_ipv4 +from vyos.validate import is_addr_assigned +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_file = '/run/conntrackd/conntrackd.conf' + +def resync_vrrp(): + tmp = run('/usr/libexec/vyos/conf_mode/vrrp.py') + if tmp > 0: + print('ERROR: error restarting VRRP daemon!') + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['service', 'conntrack-sync'] + if not conf.exists(base): + return None + + conntrack = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=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) + conntrack = dict_merge(default_values, conntrack) + + conntrack['hash_size'] = read_file('/sys/module/nf_conntrack/parameters/hashsize') + conntrack['table_size'] = read_file('/proc/sys/net/netfilter/nf_conntrack_max') + + conntrack['vrrp'] = conf.get_config_dict(['high-availability', 'vrrp', 'sync-group'], + get_first_key=True) + + return conntrack + +def verify(conntrack): + if not conntrack: + return None + + if 'interface' not in conntrack: + raise ConfigError('Interface not defined!') + + for interface in conntrack['interface']: + verify_interface_exists(interface) + # Interface must not only exist, it must also carry an IP address + if len(get_ipv4(interface)) < 1: + raise ConfigError(f'Interface {interface} requires an IP address!') + + if 'expect_sync' in conntrack: + if len(conntrack['expect_sync']) > 1 and 'all' in conntrack['expect_sync']: + raise ConfigError('Cannot configure all with other protocol') + + if 'listen_address' in conntrack: + address = conntrack['listen_address'] + if not is_addr_assigned(address): + raise ConfigError(f'Specified listen-address {address} not assigned to any interface!') + + vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack) + if vrrp_group == None: + raise ConfigError(f'No VRRP sync-group defined!') + if vrrp_group not in conntrack['vrrp']: + raise ConfigError(f'VRRP sync-group {vrrp_group} not configured!') + + return None + +def generate(conntrack): + if not conntrack: + if os.path.isfile(config_file): + os.unlink(config_file) + return None + + render(config_file, 'conntrackd/conntrackd.conf.tmpl', conntrack) + + return None + +def apply(conntrack): + if not conntrack: + # Failover mechanism daemon should be indicated that it no longer needs + # to execute conntrackd actions on transition. This is only required + # once when conntrackd is stopped and taken out of service! + if process_named_running('conntrackd'): + resync_vrrp() + + call('systemctl stop conntrackd.service') + return None + + # Failover mechanism daemon should be indicated that it needs to execute + # conntrackd actions on transition. This is only required once when conntrackd + # is started the first time! + if not process_named_running('conntrackd'): + resync_vrrp() + + call('systemctl restart conntrackd.service') + 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/dhcp_server.py b/src/conf_mode/dhcp_server.py index 84a8736e8..cdee72e09 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -18,6 +18,8 @@ import os from ipaddress import ip_address from ipaddress import ip_network +from netaddr import IPAddress +from netaddr import IPRange from sys import exit from vyos.config import Config @@ -25,6 +27,7 @@ from vyos.configdict import dict_merge from vyos.template import render from vyos.util import call from vyos.util import dict_search +from vyos.util import run from vyos.validate import is_subnet_connected from vyos.validate import is_addr_assigned from vyos.xml import defaults @@ -162,8 +165,7 @@ def verify(dhcp): # Check if DHCP address range is inside configured subnet declaration if 'range' in subnet_config: - range_start = [] - range_stop = [] + networks = [] for range, range_config in subnet_config['range'].items(): if not {'start', 'stop'} <= set(range_config): raise ConfigError(f'DHCP range "{range}" start and stop address must be defined!') @@ -178,18 +180,16 @@ def verify(dhcp): raise ConfigError(f'DHCP range "{range}" stop address must be greater or equal\n' \ 'to the ranges start address!') - # Range start address must be unique - if range_config['start'] in range_start: - raise ConfigError('Conflicting DHCP lease range: Pool start\n' \ - 'address "{start}" defined multipe times!'.format(range_config)) + for network in networks: + start = range_config['start'] + stop = range_config['stop'] + if start in network: + raise ConfigError(f'Range "{range}" start address "{start}" already part of another range!') + if stop in network: + raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another range!') - # Range stop address must be unique - if range_config['stop'] in range_start: - raise ConfigError('Conflicting DHCP lease range: Pool stop\n' \ - 'address "{stop}" defined multipe times!'.format(range_config)) - - range_start.append(range_config['start']) - range_stop.append(range_config['stop']) + tmp = IPRange(range_config['start'], range_config['stop']) + networks.append(tmp) if 'failover' in subnet_config: for key in ['local_address', 'peer_address', 'name', 'status']: @@ -272,10 +272,25 @@ def generate(dhcp): if not dhcp or 'disable' in dhcp: return None - # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters - # we can pass to ISC DHCPd + # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw + # parameters we can pass to ISC DHCPd + tmp_file = '/tmp/dhcpd.conf' + render(tmp_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp, + formater=lambda _: _.replace(""", '"')) + # XXX: as we have the ability for a user to pass in "raw" options via VyOS + # CLI (see T3544) we now ask ISC dhcpd to test the newly rendered + # configuration + tmp = run(f'/usr/sbin/dhcpd -4 -q -t -cf {tmp_file}') + if tmp > 0: + if os.path.exists(tmp_file): + os.unlink(tmp_file) + raise ConfigError('Configuration file errors encountered - check your options!') + + # Now that we know that the newly rendered configuration is "good" we can + # render the "real" configuration render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp, formater=lambda _: _.replace(""", '"')) + return None def apply(dhcp): diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py new file mode 100755 index 000000000..8e6ce5b14 --- /dev/null +++ b/src/conf_mode/firewall.py @@ -0,0 +1,73 @@ +#!/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 os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import node_changed +from vyos.configdict import leaf_node_changed +from vyos.template import render +from vyos.util import call +from vyos import ConfigError +from vyos import airbag +from pprint import pprint +airbag.enable() + + +def get_config(config=None): + + if config: + conf = config + else: + conf = Config() + base = ['nfirewall'] + firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + pprint(firewall) + return firewall + +def verify(firewall): + # bail out early - looks like removal from running config + if not firewall: + return None + + return None + +def generate(firewall): + if not firewall: + return None + + return None + +def apply(firewall): + if not firewall: + return None + + 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/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 0727b47a8..9cae29481 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -43,7 +43,7 @@ uacctd_conf_path = '/etc/pmacct/uacctd.conf' iptables_nflog_table = 'raw' iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK' egress_iptables_nflog_table = 'mangle' -egress_iptables_nflog_chain = 'POSTROUTING' +egress_iptables_nflog_chain = 'FORWARD' # helper functions # check if node exists and return True if this is true diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 1a549f27d..431d65f1f 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -83,6 +83,9 @@ def get_config(config=None): tmp = leaf_node_changed(conf, ['mode']) if tmp: bond.update({'shutdown_required': {}}) + tmp = leaf_node_changed(conf, ['lacp-rate']) + if tmp: bond.update({'shutdown_required': {}}) + # determine which members have been removed interfaces_removed = leaf_node_changed(conf, ['member', 'interface']) if interfaces_removed: diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index 44fc9cb9e..55c783f38 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -14,8 +14,6 @@ # 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 @@ -42,7 +40,7 @@ def get_config(config=None): return dummy def verify(dummy): - if 'deleted' in dummy.keys(): + if 'deleted' in dummy: verify_bridge_delete(dummy) return None @@ -58,7 +56,7 @@ def apply(dummy): d = DummyIf(dummy['ifname']) # Remove dummy interface - if 'deleted' in dummy.keys(): + if 'deleted' in dummy: d.remove() else: d.update(dummy) diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 34a054837..945a2ea9c 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -24,6 +24,7 @@ from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_source_interface from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_mtu_parent from vyos.ifconfig import MACVLANIf from vyos import ConfigError @@ -45,6 +46,9 @@ def get_config(config=None): mode = leaf_node_changed(conf, ['mode']) if mode: peth.update({'mode_old' : mode}) + if 'source_interface' in peth: + peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'], + peth['source_interface']) return peth def verify(peth): @@ -55,9 +59,10 @@ def verify(peth): verify_source_interface(peth) verify_vrf(peth) verify_address(peth) - + verify_mtu_parent(peth, peth['parent']) # use common function to verify VLAN configuration verify_vlan_config(peth) + return None def generate(peth): diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 4e6c8a9ab..1575c83ef 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -109,6 +109,14 @@ def verify(tunnel): if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: raise ConfigError('Can not disable PMTU discovery for given encapsulation') + if dict_search('parameters.ip.ignore_df', tunnel) != None: + if tunnel['encapsulation'] not in ['gretap']: + 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!') + + def generate(tunnel): return None diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py new file mode 100755 index 000000000..6ff23ae59 --- /dev/null +++ b/src/conf_mode/interfaces-vti.py @@ -0,0 +1,97 @@ +#!/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/>. + +from netifaces import interfaces +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.ifconfig import VTIIf +from vyos.util import dict_search +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', 'vti'] + vti = get_interface_dict(conf, base) + + # VTI is more then an interface - we retrieve the "real" configuration from + # the IPsec peer configuration which binds this VTI + conf.set_level([]) + vti['ipsec'] = conf.get_config_dict(['vpn', 'ipsec', 'site-to-site', 'peer'], + key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + for peer, peer_config in vti['ipsec'].items(): + if dict_search('vti.bind', peer_config) == vti['ifname']: + vti['remote'] = peer + if 'local_address' in peer_config: + vti['source_address'] = peer_config['local_address'] + # we also need to "calculate" a per vti individual key + base = 0x900000 + vti['key'] = base + int(vti['ifname'].lstrip('vti')) + + return vti + +def verify(vti): + if 'deleted' in vti: + return None + + ifname = vti['ifname'] + found = False + for peer, peer_config in vti['ipsec'].items(): + if dict_search('vti.bind', peer_config) == ifname: + found = True + # we can now stop processing the for loop + break + if not found: + tmp = vti['ifname'] + raise ConfigError(f'Interface "{ifname}" not referenced in any VPN configuration!') + + return None + +def generate(vti): + return None + +def apply(vti): + if vti['ifname'] in interfaces(): + # Always delete the VTI interface in advance + VTIIf(**vti).remove() + + if 'deleted' not in vti: + tmp = VTIIf(**vti) + tmp.update(vti) + + 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/protocols_isis.py b/src/conf_mode/protocols_isis.py index ef21e0055..c3a444f16 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -128,9 +128,11 @@ def verify(isis): raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!') # If md5 and plaintext-password set at the same time - if 'area_password' in isis: - if {'md5', 'plaintext_password'} <= set(isis['encryption']): - raise ConfigError('Can not use both md5 and plaintext-password for ISIS area-password!') + for password in ['area_password', 'domain_password']: + if password in isis: + if {'md5', 'plaintext_password'} <= set(isis[password]): + tmp = password.replace('_', '-') + raise ConfigError(f'Can use either md5 or plaintext-password for {tmp}!') # If one param from delay set, but not set others if 'spf_delay_ietf' in isis: diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py new file mode 100755 index 000000000..12dacdba0 --- /dev/null +++ b/src/conf_mode/protocols_nhrp.py @@ -0,0 +1,122 @@ +#!/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/>. + +from vyos.config import Config +from vyos.configdict import node_changed +from vyos.template import render +from vyos.util import process_named_running +from vyos.util import run +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +opennhrp_conf = '/run/opennhrp/opennhrp.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['protocols', 'nhrp'] + + nhrp = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + nhrp['del_tunnels'] = node_changed(conf, base + ['tunnel'], key_mangling=('-', '_')) + + if not conf.exists(base): + return nhrp + + nhrp['if_tunnel'] = conf.get_config_dict(['interfaces', 'tunnel'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + nhrp['profile_map'] = {} + profile = conf.get_config_dict(['vpn', 'ipsec', 'profile'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + for name, profile_conf in profile.items(): + if 'bind' in profile_conf and 'tunnel' in profile_conf['bind']: + interfaces = profile_conf['bind']['tunnel'] + if isinstance(interfaces, str): + interfaces = [interfaces] + for interface in interfaces: + nhrp['profile_map'][interface] = name + + return nhrp + +def verify(nhrp): + if 'tunnel' in nhrp: + for name, nhrp_conf in nhrp['tunnel'].items(): + if not nhrp['if_tunnel'] or name not in nhrp['if_tunnel']: + raise ConfigError(f'Tunnel interface "{name}" does not exist') + + tunnel_conf = nhrp['if_tunnel'][name] + + if 'encapsulation' not in tunnel_conf or tunnel_conf['encapsulation'] != 'gre': + raise ConfigError(f'Tunnel "{name}" is not an mGRE tunnel') + + if 'remote' in tunnel_conf: + raise ConfigError(f'Tunnel "{name}" cannot have a remote address defined') + + if 'map' in nhrp_conf: + for map_name, map_conf in nhrp_conf['map'].items(): + if 'nbma_address' not in map_conf: + raise ConfigError(f'nbma-address missing on map {map_name} on tunnel {name}') + + if 'dynamic_map' in nhrp_conf: + for map_name, map_conf in nhrp_conf['dynamic_map'].items(): + if 'nbma_domain_name' not in map_conf: + raise ConfigError(f'nbma-domain-name missing on dynamic-map {map_name} on tunnel {name}') + return None + +def generate(nhrp): + render(opennhrp_conf, 'nhrp/opennhrp.conf.tmpl', nhrp) + return None + +def apply(nhrp): + if 'tunnel' in nhrp: + for tunnel, tunnel_conf in nhrp['tunnel'].items(): + if 'source_address' in tunnel_conf: + chain = f'VYOS_NHRP_{tunnel}_OUT_HOOK' + source_address = tunnel_conf['source_address'] + + chain_exists = run(f'sudo iptables --check {chain} -j RETURN') == 0 + if not chain_exists: + run(f'sudo iptables --new {chain}') + run(f'sudo iptables --append {chain} -p gre -s {source_address} -d 224.0.0.0/4 -j DROP') + run(f'sudo iptables --append {chain} -j RETURN') + run(f'sudo iptables --insert OUTPUT 2 -j {chain}') + + for tunnel in nhrp['del_tunnels']: + chain = f'VYOS_NHRP_{tunnel}_OUT_HOOK' + chain_exists = run(f'sudo iptables --check {chain} -j RETURN') == 0 + if chain_exists: + run(f'sudo iptables --delete OUTPUT -j {chain}') + run(f'sudo iptables --flush {chain}') + run(f'sudo iptables --delete-chain {chain}') + + action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop' + run(f'systemctl {action} opennhrp') + 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_router-advert.py b/src/conf_mode/service_router-advert.py index 65eb11ce3..9afcdd63e 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2019 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 @@ -40,11 +40,14 @@ def get_config(config=None): # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_interface_values = defaults(base + ['interface']) - # we deal with prefix defaults later on + # we deal with prefix, route defaults later on if 'prefix' in default_interface_values: del default_interface_values['prefix'] + if 'route' in default_interface_values: + del default_interface_values['route'] default_prefix_values = defaults(base + ['interface', 'prefix']) + default_route_values = defaults(base + ['interface', 'route']) if 'interface' in rtradv: for interface in rtradv['interface']: @@ -56,6 +59,11 @@ def get_config(config=None): rtradv['interface'][interface]['prefix'][prefix] = dict_merge( default_prefix_values, rtradv['interface'][interface]['prefix'][prefix]) + if 'route' in rtradv['interface'][interface]: + for route in rtradv['interface'][interface]['route']: + rtradv['interface'][interface]['route'][route] = dict_merge( + default_route_values, rtradv['interface'][interface]['route'][route]) + if 'name_server' in rtradv['interface'][interface]: # always use a list when dealing with nameservers - eases the template generation if isinstance(rtradv['interface'][interface]['name_server'], str): diff --git a/src/conf_mode/system_sysctl.py b/src/conf_mode/system_sysctl.py new file mode 100755 index 000000000..4f16d1ed6 --- /dev/null +++ b/src/conf_mode/system_sysctl.py @@ -0,0 +1,73 @@ +#!/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 os + +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import cmd +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_file = r'/run/sysctl/99-vyos-sysctl.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['system', 'sysctl'] + if not conf.exists(base): + return None + + sysctl = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + return sysctl + +def verify(sysctl): + return None + +def generate(sysctl): + if not sysctl: + if os.path.isfile(config_file): + os.unlink(config_file) + return None + + render(config_file, 'system/sysctl.conf.tmpl', sysctl) + return None + +def apply(sysctl): + if not sysctl: + return None + + # We silently ignore all errors + # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080 + cmd(f'sysctl -f {config_file}') + 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/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 969266c30..4efedd995 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# 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 @@ -17,51 +17,383 @@ import os from sys import exit +from time import sleep 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 render 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 from vyos import ConfigError from vyos import airbag -from pprint import pprint airbag.enable() +authby_translate = { + 'pre-shared-secret': 'secret', + 'rsa': 'rsasig', + 'x509': 'rsasig' +} +default_pfs = 'dh-group2' +pfs_translate = { + 'dh-group1': 'modp768', + 'dh-group2': 'modp1024', + 'dh-group5': 'modp1536', + 'dh-group14': 'modp2048', + 'dh-group15': 'modp3072', + 'dh-group16': 'modp4096', + 'dh-group17': 'modp6144', + 'dh-group18': 'modp8192', + 'dh-group19': 'ecp256', + 'dh-group20': 'ecp384', + 'dh-group21': 'ecp512', + 'dh-group22': 'modp1024s160', + 'dh-group23': 'modp2048s224', + 'dh-group24': 'modp2048s256', + 'dh-group25': 'ecp192', + 'dh-group26': 'ecp224', + 'dh-group27': 'ecp224bp', + 'dh-group28': 'ecp256bp', + 'dh-group29': 'ecp384bp', + 'dh-group30': 'ecp512bp', + 'dh-group31': 'curve25519', + 'dh-group32': 'curve448' +} + +any_log_modes = [ + 'dmn', 'mgr', 'ike', 'chd','job', 'cfg', 'knl', 'net', 'asn', + 'enc', 'lib', 'esp', 'tls', 'tnc', 'imc', 'imv', 'pts' +] + +ike_ciphers = {} +esp_ciphers = {} + +mark_base = 0x900000 + +CA_PATH = "/etc/ipsec.d/cacerts/" +CRL_PATH = "/etc/ipsec.d/crls/" + +DHCP_BASE = "/var/lib/dhcp/dhclient" + +LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] +X509_PATH = '/config/auth/' + def get_config(config=None): if config: conf = config else: conf = Config() - base = ['vpn', 'nipsec'] + base = ['vpn', 'ipsec'] if not conf.exists(base): return None # retrieve common dictionary keys - ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + 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') + ipsec['rsa_keys'] = conf.get_config_dict(['vpn', 'rsa-keys'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + default_ike_pfs = None + + if 'ike_group' in ipsec: + for group, ike_conf in ipsec['ike_group'].items(): + if 'proposal' in ike_conf: + ciphers = [] + for i in ike_conf['proposal']: + proposal = ike_conf['proposal'][i] + enc = proposal['encryption'] if 'encryption' in proposal else None + hash = proposal['hash'] if 'hash' in proposal else None + pfs = ('dh-group' + proposal['dh_group']) if 'dh_group' in proposal else default_pfs + + if not default_ike_pfs: + default_ike_pfs = pfs + + if enc and hash: + ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}") + ike_ciphers[group] = ','.join(ciphers) + '!' + + if 'esp_group' in ipsec: + for group, esp_conf in ipsec['esp_group'].items(): + pfs = esp_conf['pfs'] if 'pfs' in esp_conf else 'enable' + + if pfs == 'disable': + pfs = None + + if pfs == 'enable': + pfs = default_ike_pfs + + if 'proposal' in esp_conf: + ciphers = [] + for i in esp_conf['proposal']: + proposal = esp_conf['proposal'][i] + enc = proposal['encryption'] if 'encryption' in proposal else 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) + '!' + return ipsec +def get_rsa_local_key(ipsec): + return dict_search('local_key.file', ipsec['rsa_keys']) + +def verify_rsa_local_key(ipsec): + file = get_rsa_local_key(ipsec) + + if not file: + return False + + for path in LOCAL_KEY_PATHS: + full_path = os.path.join(path, file) + if os.path.exists(full_path): + return full_path + + return False + +def verify_rsa_key(ipsec, key_name): + return dict_search(f'rsa_key_name.{key_name}.rsa_key', ipsec['rsa_keys']) + def verify(ipsec): if not ipsec: return None + if 'ipsec_interfaces' in ipsec and 'interface' in ipsec['ipsec_interfaces']: + interfaces = ipsec['ipsec_interfaces']['interface'] + if isinstance(interfaces, str): + interfaces = [interfaces] + + for ifname in interfaces: + verify_interface_exists(ifname) + + if 'profile' in ipsec: + for profile, profile_conf in ipsec['profile'].items(): + if 'esp_group' in profile_conf: + if 'esp_group' not in ipsec or profile_conf['esp_group'] not in ipsec['esp_group']: + raise ConfigError(f"Invalid esp-group on {profile} profile") + else: + raise ConfigError(f"Missing esp-group on {profile} profile") + + if 'ike_group' in profile_conf: + if 'ike_group' not in ipsec or profile_conf['ike_group'] not in ipsec['ike_group']: + raise ConfigError(f"Invalid ike-group on {profile} profile") + else: + raise ConfigError(f"Missing ike-group on {profile} profile") + + if 'authentication' not in profile_conf: + raise ConfigError(f"Missing authentication on {profile} profile") + + if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']: + for peer, peer_conf in ipsec['site_to_site']['peer'].items(): + has_default_esp = False + if 'default_esp_group' in peer_conf: + has_default_esp = True + if 'esp_group' not in ipsec or peer_conf['default_esp_group'] not in ipsec['esp_group']: + raise ConfigError(f"Invalid esp-group on site-to-site peer {peer}") + + if 'ike_group' in peer_conf: + if 'ike_group' not in ipsec or peer_conf['ike_group'] not in ipsec['ike_group']: + raise ConfigError(f"Invalid ike-group on site-to-site peer {peer}") + else: + raise ConfigError(f"Missing ike-group on site-to-site peer {peer}") + + if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']: + raise ConfigError(f"Missing authentication on site-to-site peer {peer}") + + if peer_conf['authentication']['mode'] == 'x509': + if 'x509' not in peer_conf['authentication']: + raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}") + + if 'key' not in peer_conf['authentication']['x509']: + raise ConfigError(f"Missing x509 key on site-to-site peer {peer}") + + if 'ca_cert_file' not in peer_conf['authentication']['x509'] or 'cert_file' not in peer_conf['authentication']['x509']: + raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}") + + if 'file' not in peer_conf['authentication']['x509']['key']: + raise ConfigError(f"Missing x509 key file on site-to-site peer {peer}") + + for key in ['ca_cert_file', 'cert_file', 'crl_file']: + if key in peer_conf['authentication']['x509']: + path = os.path.join(X509_PATH, peer_conf['authentication']['x509'][key]) + if not os.path.exists(path): + raise ConfigError(f"File not found for {key} on site-to-site peer {peer}") + + key_path = os.path.join(X509_PATH, peer_conf['authentication']['x509']['key']['file']) + if not os.path.exists(key_path): + raise ConfigError(f"Private key not found on site-to-site peer {peer}") + + if peer_conf['authentication']['mode'] == 'rsa': + if not verify_rsa_local_key(ipsec): + raise ConfigError(f"Invalid key on rsa-keys local-key") + + if 'rsa_key_name' not in peer_conf['authentication']: + raise ConfigError(f"Missing rsa-key-name on site-to-site peer {peer}") + + if not verify_rsa_key(ipsec, peer_conf['authentication']['rsa_key_name']): + raise ConfigError(f"Invalid rsa-key-name on site-to-site peer {peer}") + + if 'local_address' not in peer_conf and 'dhcp_interface' not in peer_conf: + raise ConfigError(f"Missing local-address or dhcp-interface on site-to-site peer {peer}") + + if 'dhcp_interface' in peer_conf: + dhcp_interface = peer_conf['dhcp_interface'] + + verify_interface_exists(dhcp_interface) + + 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() + if not address: + raise ConfigError(f"Failed to get address from dhcp-interface on site-to-site peer {peer}") + + if 'vti' in peer_conf: + if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf: + raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}") + + if 'bind' in peer_conf['vti']: + vti_interface = peer_conf['vti']['bind'] + if not os.path.exists(f'/sys/class/net/{vti_interface}'): + raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!') + + if 'vti' not in peer_conf and 'tunnel' not in peer_conf: + raise ConfigError(f"No VTI or tunnel specified on site-to-site peer {peer}") + + if 'tunnel' in peer_conf: + for tunnel, tunnel_conf in peer_conf['tunnel'].items(): + if 'esp_group' not in tunnel_conf and not has_default_esp: + raise ConfigError(f"Missing esp-group on tunnel {tunnel} for site-to-site peer {peer}") + + esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group'] + + if esp_group_name not in ipsec['esp_group']: + raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}") + + esp_group = ipsec['esp_group'][esp_group_name] + + if 'mode' in esp_group and esp_group['mode'] == 'transport': + if 'protocol' in tunnel_conf and ((peer in ['any', '0.0.0.0']) or ('local_address' not in peer_conf or peer_conf['local_address'] in ['any', '0.0.0.0'])): + raise ConfigError(f"Fixed local-address or peer required when a protocol is defined with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}") + + if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']): + raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}") + def generate(ipsec): - if not ipsec: - return None + data = {} - return ipsec + if ipsec: + 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_conf['authentication']['mode'] == 'x509': + ca_cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['ca_cert_file']) + call(f'cp -f {ca_cert_file} {CA_PATH}') + + if 'crl_file' in peer_conf['authentication']['x509']: + crl_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['crl_file']) + call(f'cp -f {crl_file} {CRL_PATH}') + + local_ip = '' + 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() + + 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: + 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) + + if not local_prefix or not remote_prefix: + continue + + passthrough = cidr_fit(local_prefix, remote_prefix) + data['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough + + if 'logging' in ipsec and 'log_modes' in ipsec['logging']: + modes = ipsec['logging']['log_modes'] + level = ipsec['logging']['log_level'] if 'log_level' in ipsec['logging'] else '1' + if isinstance(modes, str): + modes = [modes] + if 'any' in modes: + modes = any_log_modes + data['charondebug'] = f' {level}, '.join(modes) + ' ' + level + + render("/etc/ipsec.conf", "ipsec/ipsec.conf.tmpl", data) + render("/etc/ipsec.secrets", "ipsec/ipsec.secrets.tmpl", data) + render("/etc/strongswan.d/interfaces_use.conf", "ipsec/interfaces_use.conf.tmpl", data) + render("/etc/swanctl/swanctl.conf", "ipsec/swanctl.conf.tmpl", data) + +def resync_l2tp(ipsec): + if ipsec and not ipsec['l2tp_exists']: + return + + tmp = run('/usr/libexec/vyos/conf_mode/ipsec-settings.py') + if tmp > 0: + print('ERROR: failed to reapply L2TP IPSec settings!') + +def resync_nhrp(ipsec): + if ipsec and not ipsec['nhrp_exists']: + return + + tmp = run('/usr/libexec/vyos/conf_mode/protocols_nhrp.py') + if tmp > 0: + print('ERROR: failed to reapply NHRP settings!') def apply(ipsec): if not ipsec: - return None + call('sudo /usr/sbin/ipsec stop') + else: + should_start = ('profile' in ipsec or dict_search('site_to_site.peer', ipsec)) + + if not process_named_running('charon') and should_start: + args = f'--auto-update {ipsec["auto_update"]}' if 'auto_update' in ipsec else '' + call(f'sudo /usr/sbin/ipsec start {args}') + elif not should_start: + call('sudo /usr/sbin/ipsec stop') + elif ipsec['interface_change']: + call('sudo /usr/sbin/ipsec restart') + else: + call('sudo /usr/sbin/ipsec rereadall') + call('sudo /usr/sbin/ipsec reload') + + if should_start: + sleep(2) # Give charon enough time to start + call('sudo /usr/sbin/swanctl -q') + + resync_l2tp(ipsec) + resync_nhrp(ipsec) - pprint(ipsec) +def get_mark(vti_interface): + vti_num = int(vti_interface.lstrip('vti')) + return mark_base + vti_num if __name__ == '__main__': try: - c = get_config() - verify(c) - generate(c) - apply(c) + ipsec = get_config() + verify(ipsec) + generate(ipsec) + apply(ipsec) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/vpn_rsa-keys.py b/src/conf_mode/vpn_rsa-keys.py new file mode 100755 index 000000000..6cf7eba6e --- /dev/null +++ b/src/conf_mode/vpn_rsa-keys.py @@ -0,0 +1,111 @@ +#!/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 base64 +import os +import struct + +from sys import exit + +from vyos.config import Config +from vyos.util import call +from vyos import ConfigError +from vyos import airbag +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' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['vpn', 'rsa-keys'] + if not conf.exists(base): + return None + + return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + +def verify(conf): + if not conf: + return + + if 'local_key' in conf and 'file' in conf['local_key']: + local_key = conf['local_key']['file'] + if not local_key: + raise ConfigError(f'Invalid local-key') + + if not get_local_key(local_key): + raise ConfigError(f'File not found for local-key: {local_key}') + +def get_local_key(local_key): + for path in LOCAL_KEY_PATHS: + full_path = os.path.join(path, local_key) + if os.path.exists(full_path): + return full_path + return False + +def generate(conf): + if not conf: + return + + 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 /usr/bin/openssl rsa -in {local_key_path} -pubout -out {LOCAL_OUTPUT}') + + if 'rsa_key_name' in conf: + for key_name, key_conf in conf['rsa_key_name'].items(): + if 'rsa_key' not in key_conf: + continue + + remote_key = key_conf['rsa_key'] + + if remote_key[:2] == "0s": # Vyatta format + remote_key = migrate_from_vyatta_key(remote_key) + 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: + f.write(remote_key) + +def migrate_from_vyatta_key(data): + data = base64.b64decode(data[2:]) + length = struct.unpack('B', data[:1])[0] + e = int.from_bytes(data[1:1+length], 'big') + n = int.from_bytes(data[1+length:], 'big') + pubkey = construct((n, e)) + return pubkey.exportKey(format='PEM') + +def apply(conf): + if not conf: + return + + call('sudo /usr/sbin/ipsec rereadall') + call('sudo /usr/sbin/ipsec reload') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) |