diff options
Diffstat (limited to 'src')
45 files changed, 2306 insertions, 73 deletions
diff --git a/src/completion/list_disks.py b/src/completion/list_disks.py index ff1135e23..0aa872abb 100755 --- a/src/completion/list_disks.py +++ b/src/completion/list_disks.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 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 @@ -23,11 +23,20 @@ parser.add_argument("-e", "--exclude", type=str, help="Exclude specified device args = parser.parse_args() disks = set() -with open('/proc/partitions') as partitions_file: - for line in partitions_file: - fields = line.strip().split() - if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': - disks.add(fields[3]) +with open('/proc/partitions') as f: + table = f.read() + +for line in table.splitlines()[1:]: + fields = line.strip().split() + # probably an empty line at the top + if len(fields) == 0: + continue + disks.add(fields[3]) + +if 'loop0' in disks: + disks.remove('loop0') +if 'sr0' in disks: + disks.remove('sr0') if args.exclude: disks.remove(args.exclude) diff --git a/src/completion/list_protocols.sh b/src/completion/list_protocols.sh new file mode 100755 index 000000000..e9d50a70f --- /dev/null +++ b/src/completion/list_protocols.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +grep -v '^#' /etc/protocols | awk 'BEGIN {ORS=""} {if ($3) {print TRS $1; TRS=" "}}' diff --git a/src/completion/list_sysctl_parameters.sh b/src/completion/list_sysctl_parameters.sh new file mode 100755 index 000000000..c111716bb --- /dev/null +++ b/src/completion/list_sysctl_parameters.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# +# 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/>. + +declare -a vals +eval "vals=($(/sbin/sysctl -N -a))" +echo ${vals[@]} +exit 0 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) diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook new file mode 100644 index 000000000..36edf04f3 --- /dev/null +++ b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +import os +import sys + +from vyos.util import call + +IPSEC_CONF="/etc/ipsec.conf" +IPSEC_SECRETS="/etc/ipsec.secrets" + +def getlines(file): + with open(file, 'r') as f: + return f.readlines() + +def writelines(file, lines): + with open(file, 'w') as f: + f.writelines(lines) + +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) + found = False + to_match = f'# dhcp:{interface}' + + for i, line in enumerate(conf_lines): + if line.find(to_match) > 0: + conf_lines[i] = line.replace(old_ip, new_ip) + found = True + + for i, line in enumerate(secrets_lines): + if line.find(to_match) > 0: + 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') diff --git a/src/etc/ipsec.d/key-pair.template b/src/etc/ipsec.d/key-pair.template new file mode 100644 index 000000000..56be97516 --- /dev/null +++ b/src/etc/ipsec.d/key-pair.template @@ -0,0 +1,67 @@ +[ req ] + default_bits = 2048 + default_keyfile = privkey.pem + distinguished_name = req_distinguished_name + string_mask = utf8only + attributes = req_attributes + dirstring_type = nobmp +# SHA-1 is deprecated, so use SHA-2 instead. + default_md = sha256 +# Extension to add when the -x509 option is used. + x509_extensions = v3_ca + +[ req_distinguished_name ] + countryName = Country Name (2 letter code) + countryName_min = 2 + countryName_max = 2 + ST = State Name + localityName = Locality Name (eg, city) + organizationName = Organization Name (eg, company) + organizationalUnitName = Organizational Unit Name (eg, department) + commonName = Common Name (eg, Device hostname) + commonName_max = 64 + emailAddress = Email Address + emailAddress_max = 40 +[ req_attributes ] + challengePassword = A challenge password (optional) + challengePassword_min = 4 + challengePassword_max = 20 +[ v3_ca ] + subjectKeyIdentifier=hash + authorityKeyIdentifier=keyid:always,issuer:always + basicConstraints = critical, CA:true + keyUsage = critical, digitalSignature, cRLSign, keyCertSign +[ v3_intermediate_ca ] +# Extensions for a typical intermediate CA (`man x509v3_config`). + subjectKeyIdentifier = hash + authorityKeyIdentifier = keyid:always,issuer + basicConstraints = critical, CA:true, pathlen:0 + keyUsage = critical, digitalSignature, cRLSign, keyCertSign +[ usr_cert ] +# Extensions for client certificates (`man x509v3_config`). + basicConstraints = CA:FALSE + nsCertType = client, email + nsComment = "OpenSSL Generated Client Certificate" + subjectKeyIdentifier = hash + authorityKeyIdentifier = keyid,issuer + keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment + extendedKeyUsage = clientAuth, emailProtection +[ server_cert ] +# Extensions for server certificates (`man x509v3_config`). + basicConstraints = CA:FALSE + nsCertType = server + nsComment = "OpenSSL Generated Server Certificate" + subjectKeyIdentifier = hash + authorityKeyIdentifier = keyid,issuer:always + keyUsage = critical, digitalSignature, keyEncipherment + extendedKeyUsage = serverAuth +[ crl_ext ] +# Extension for CRLs (`man x509v3_config`). + authorityKeyIdentifier=keyid:always +[ ocsp ] +# Extension for OCSP signing certificates (`man ocsp`). + basicConstraints = CA:FALSE + subjectKeyIdentifier = hash + authorityKeyIdentifier = keyid,issuer + keyUsage = critical, digitalSignature + extendedKeyUsage = critical, OCSPSigning diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down new file mode 100755 index 000000000..0e1cd7753 --- /dev/null +++ b/src/etc/ipsec.d/vti-up-down @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +## 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 + +import os +import sys + +from vyos.util import call, get_interface_config, get_interface_address + +def get_dhcp_address(interface): + addr = get_interface_address(interface) + if not addr: + return None + if len(addr['addr_info']) == 0: + return None + return addr['addr_info'][0]['local'] + +if __name__ == '__main__': + verb = os.getenv('PLUTO_VERB') + connection = os.getenv('PLUTO_CONNECTION') + interface = sys.argv[1] + dhcp_interface = sys.argv[2] + + print(f'vti-up-down: start: {verb} {connection} {interface}') + + if verb in ['up-client', 'up-host']: + call('sudo ip route delete default table 220') + + vti_link = get_interface_config(interface) + + if not vti_link: + print('vti-up-down: interface not found') + sys.exit(0) + + vti_link_up = (vti_link['operstate'] == 'UP' if 'operstate' in vti_link else False) + + if verb in ['up-client', 'up-host']: + if not vti_link_up: + if dhcp_interface != 'no': + local_ip = get_dhcp_address(dhcp_interface) + call(f'sudo ip tunnel change {interface} local {local_ip}') + call(f'sudo ip link set {interface} up') + elif verb in ['down-client', 'down-host']: + if vti_link_up: + call(f'sudo ip link set {interface} down') + + print('vti-up-down: finish')
\ No newline at end of file diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py new file mode 100755 index 000000000..74c45f2f6 --- /dev/null +++ b/src/etc/opennhrp/opennhrp-script.py @@ -0,0 +1,136 @@ +#!/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 pprint import pprint +import os +import re +import sys +import vici + +from vyos.util import cmd +from vyos.util import process_named_running + +NHRP_CONFIG="/etc/opennhrp/opennhrp.conf" + +def parse_type_ipsec(interface): + with open(NHRP_CONFIG, 'r') as f: + lines = f.readlines() + match = rf'^interface {interface} #(hub|spoke)(?:\s([\w-]+))?$' + for line in lines: + m = re.match(match, line) + if m: + return m[1], m[2] + return None, None + +def vici_initiate(conn, child_sa, src_addr, dest_addr): + try: + session = vici.Session() + logs = session.initiate({ + 'ike': conn, + 'child': child_sa, + 'timeout': '-1', + 'my-host': src_addr, + 'other-host': dest_addr + }) + for log in logs: + message = log['msg'].decode('ascii') + print('INIT LOG:', message) + return True + except: + return None + +def vici_terminate(conn, child_sa, src_addr, dest_addr): + try: + session = vici.Session() + logs = session.terminate({ + 'ike': conn, + 'child': child_sa, + 'timeout': '-1', + 'my-host': src_addr, + 'other-host': dest_addr + }) + for log in logs: + message = log['msg'].decode('ascii') + print('TERM LOG:', message) + return True + except: + return None + +def iface_up(interface): + cmd(f'sudo ip route flush proto 42 dev {interface}') + cmd(f'sudo ip neigh flush dev {interface}') + +def peer_up(dmvpn_type, conn): + src_addr = os.getenv('NHRP_SRCADDR') + src_nbma = os.getenv('NHRP_SRCNBMA') + dest_addr = os.getenv('NHRP_DESTADDR') + dest_nbma = os.getenv('NHRP_DESTNBMA') + dest_mtu = os.getenv('NHRP_DESTMTU') + + if dest_mtu: + args = cmd(f'sudo ip route get {dest_nbma} from {src_nbma}') + cmd(f'sudo ip route add {args} proto 42 mtu {dest_mtu}') + + if conn and dmvpn_type == 'spoke' and process_named_running('charon'): + vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma) + vici_initiate(conn, 'dmvpn', src_nbma, dest_nbma) + +def peer_down(dmvpn_type, conn): + src_nbma = os.getenv('NHRP_SRCNBMA') + dest_nbma = os.getenv('NHRP_DESTNBMA') + + if conn and dmvpn_type == 'spoke' and process_named_running('charon'): + vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma) + + cmd(f'sudo ip route del {dest_nbma} src {src_nbma} proto 42') + +def route_up(interface): + dest_addr = os.getenv('NHRP_DESTADDR') + dest_prefix = os.getenv('NHRP_DESTPREFIX') + next_hop = os.getenv('NHRP_NEXTHOP') + + cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 via {next_hop} dev {interface}') + cmd('sudo ip route flush cache') + +def route_down(interface): + dest_addr = os.getenv('NHRP_DESTADDR') + dest_prefix = os.getenv('NHRP_DESTPREFIX') + + cmd(f'sudo ip route del {dest_addr}/{dest_prefix} proto 42') + cmd('sudo ip route flush cache') + +if __name__ == '__main__': + action = sys.argv[1] + interface = os.getenv('NHRP_INTERFACE') + dmvpn_type, profile_name = parse_type_ipsec(interface) + + dmvpn_conn = None + + if profile_name: + dmvpn_conn = f'dmvpn-{profile_name}-{interface}' + + if action == 'interface-up': + iface_up(interface) + elif action == 'peer-register': + pass + elif action == 'peer-up': + peer_up(dmvpn_type, dmvpn_conn) + elif action == 'peer-down': + peer_down(dmvpn_type, dmvpn_conn) + elif action == 'route-up': + route_up(interface) + elif action == 'route-down': + route_down(interface) diff --git a/src/etc/systemd/system/conntrackd.service.d/override.conf b/src/etc/systemd/system/conntrackd.service.d/override.conf new file mode 100644 index 000000000..eb611e0d9 --- /dev/null +++ b/src/etc/systemd/system/conntrackd.service.d/override.conf @@ -0,0 +1,8 @@ +[Unit] +After= +After=vyos-router.service +ConditionPathExists=/run/conntrackd/conntrackd.conf + +[Service] +ExecStart= +ExecStart=/usr/sbin/conntrackd -C /run/conntrackd/conntrackd.conf diff --git a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py index dc751c45c..ec33906ba 100755 --- a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py +++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.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 @@ -15,48 +15,47 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys -import syslog as sl +import syslog from vyos.config import Config from vyos import ConfigError from vyos.util import run - def get_config(): c = Config() interfaces = dict() for intf in c.list_effective_nodes('interfaces ethernet'): # skip interfaces that are disabled or is configured for dhcp - check_disable = "interfaces ethernet {} disable".format(intf) - check_dhcp = "interfaces ethernet {} address dhcp".format(intf) + check_disable = f'interfaces ethernet {intf} disable' + check_dhcp = f'interfaces ethernet {intf} address dhcp' if c.exists_effective(check_disable): continue # get addresses configured on the interface intf_addresses = c.return_effective_values( - "interfaces ethernet {} address".format(intf) - ) + f'interfaces ethernet {intf} address') interfaces[intf] = [addr.strip("'") for addr in intf_addresses] return interfaces - def apply(config): + syslog.openlog(ident='ether-resume', logoption=syslog.LOG_PID, + facility=syslog.LOG_INFO) + for intf, addresses in config.items(): # bring the interface up - cmd = ["ip", "link", "set", "dev", intf, "up"] - sl.syslog(sl.LOG_NOTICE, " ".join(cmd)) + cmd = f'ip link set dev {intf} up' + syslog.syslog(cmd) run(cmd) # add configured addresses to interface for addr in addresses: - if addr == "dhcp": - cmd = ["dhclient", intf] + if addr == 'dhcp': + cmd = ['dhclient', intf] else: - cmd = ["ip", "address", "add", addr, "dev", intf] - sl.syslog(sl.LOG_NOTICE, " ".join(cmd)) + cmd = f'ip address add {addr} dev {intf}' + syslog.syslog(cmd) run(cmd) - if __name__ == '__main__': try: config = get_config() diff --git a/src/helpers/vyos-vrrp-conntracksync.sh b/src/helpers/vyos-vrrp-conntracksync.sh new file mode 100755 index 000000000..4501aa63e --- /dev/null +++ b/src/helpers/vyos-vrrp-conntracksync.sh @@ -0,0 +1,154 @@ +#!/bin/sh +# +# (C) 2008 by Pablo Neira Ayuso <pablo@netfilter.org> +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +# +# Description: +# +# This is the script for primary-backup setups for keepalived +# (http://www.keepalived.org). You may adapt it to make it work with other +# high-availability managers. +# +# Modified by : Mohit Mehta <mohit@vyatta.com> +# Slight modifications were made to this script for running with Vyatta +# The original script came from 0.9.14 debian conntrack-tools package +# +# + +CONNTRACKD_BIN=/usr/sbin/conntrackd +CONNTRACKD_LOCK=/var/lock/conntrack.lock +CONNTRACKD_CONFIG=/etc/conntrackd/conntrackd.conf +FACILITY=daemon +LEVEL=notice +TAG=conntrack-tools +LOGCMD="logger -t $TAG -p $FACILITY.$LEVEL" +VRRP_GRP="VRRP sync-group [$2]" +FAILOVER_STATE="/var/run/vyatta-conntrackd-failover-state" + +$LOGCMD "vyatta-vrrp-conntracksync invoked at `date`" + + +if [ ! -e $FAILOVER_STATE ]; then + mkdir -p /var/run + touch $FAILOVER_STATE +fi + +case "$1" in + master) + echo MASTER at `date` > $FAILOVER_STATE + $LOGCMD "`uname -n` transitioning to MASTER state for $VRRP_GRP" + # + # commit the external cache into the kernel table + # + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -c + if [ $? -eq 1 ] + then + $LOGCMD "ERROR: failed to invoke conntrackd -c" + fi + + # + # commit the expect entries to the kernel + # + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -c exp + if [ $? -eq 1 ] + then + $LOGCMD "ERROR: failed to invoke conntrackd -ce exp" + fi + + # + # flush the internal and the external caches + # + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -f + if [ $? -eq 1 ] + then + $LOGCMD "ERROR: failed to invoke conntrackd -f" + fi + + # + # resynchronize my internal cache to the kernel table + # + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -R + if [ $? -eq 1 ] + then + $LOGCMD "ERROR: failed to invoke conntrackd -R" + fi + + # + # send a bulk update to backups + # + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -B + if [ $? -eq 1 ] + then + $LOGCMD "ERROR: failed to invoke conntrackd -B" + fi + ;; + backup) + echo BACKUP at `date` > $FAILOVER_STATE + $LOGCMD "`uname -n` transitioning to BACKUP state for $VRRP_GRP" + # + # is conntrackd running? request some statistics to check it + # + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -s + if [ $? -eq 1 ] + then + # + # something's wrong, do we have a lock file? + # + if [ -f $CONNTRACKD_LOCK ] + then + $LOGCMD "WARNING: conntrackd was not cleanly stopped." + $LOGCMD "If you suspect that it has crashed:" + $LOGCMD "1) Enable coredumps" + $LOGCMD "2) Try to reproduce the problem" + $LOGCMD "3) Post the coredump to netfilter-devel@vger.kernel.org" + rm -f $CONNTRACKD_LOCK + fi + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -d + if [ $? -eq 1 ] + then + $LOGCMD "ERROR: cannot launch conntrackd" + exit 1 + fi + fi + # + # shorten kernel conntrack timers to remove the zombie entries. + # + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -t + if [ $? -eq 1 ] + then + $LOGCMD "ERROR: failed to invoke conntrackd -t" + fi + + # + # request resynchronization with master firewall replica (if any) + # Note: this does nothing in the alarm approach. + # + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -n + if [ $? -eq 1 ] + then + $LOGCMD "ERROR: failed to invoke conntrackd -n" + fi + ;; + fault) + echo FAULT at `date` > $FAILOVER_STATE + $LOGCMD "`uname -n` transitioning to FAULT state for $VRRP_GRP" + # + # shorten kernel conntrack timers to remove the zombie entries. + # + $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -t + if [ $? -eq 1 ] + then + $LOGCMD "ERROR: failed to invoke conntrackd -t" + fi + ;; + *) + echo UNKNOWN at `date` > $FAILOVER_STATE + $LOGCMD "ERROR: `uname -n` unknown state transition for $VRRP_GRP" + echo "Usage: vyatta-vrrp-conntracksync.sh {master|backup|fault}" + exit 1 + ;; +esac + +exit 0 diff --git a/src/migration-scripts/conntrack-sync/1-to-2 b/src/migration-scripts/conntrack-sync/1-to-2 new file mode 100755 index 000000000..ebbd8c35a --- /dev/null +++ b/src/migration-scripts/conntrack-sync/1-to-2 @@ -0,0 +1,66 @@ +#!/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/>. + +# VyOS 1.2 crux allowed configuring a lower or upper case loglevel. This +# is no longer supported as the input data is validated and will lead to +# an error. If user specifies an upper case logleve, make it lowercase + +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 = ['service', 'conntrack-sync'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) +else: + base_accept_proto = base + ['accept-protocol'] + if config.exists(base_accept_proto): + tmp = config.return_value(base_accept_proto) + config.delete(base_accept_proto) + for protocol in tmp.split(','): + config.set(base_accept_proto, value=protocol, replace=False) + + base_ignore_addr = base + ['ignore-address', 'ipv4'] + if config.exists(base_ignore_addr): + tmp = config.return_values(base_ignore_addr) + config.delete(base_ignore_addr) + for address in tmp: + config.set(base + ['ignore-address'], value=address, replace=False) + + # we no longer support cluster mode + base_cluster = base + ['failover-mechanism', 'cluster'] + if config.exists(base_cluster): + config.delete(base_cluster) + + 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)) + exit(1) diff --git a/src/migration-scripts/interfaces/5-to-6 b/src/migration-scripts/interfaces/5-to-6 index 1291751d8..ae79c1d1b 100755 --- a/src/migration-scripts/interfaces/5-to-6 +++ b/src/migration-scripts/interfaces/5-to-6 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# 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 @@ -55,6 +55,16 @@ def copy_rtradv(c, old_base, interface): min_max = interval.split('-')[0] c.set(new_base + ['interval', min_max], value=tmp) + # cleanup boolean nodes in individual route + route_base = new_base + ['route'] + if c.exists(route_base): + for route in config.list_nodes(route_base): + if c.exists(route_base + [route, 'remove-route']): + tmp = c.return_value(route_base + [route, 'remove-route']) + c.delete(route_base + [route, 'remove-route']) + if tmp == 'false': + c.set(route_base + [route, 'no-remove-route']) + # cleanup boolean nodes in individual prefix prefix_base = new_base + ['prefix'] if c.exists(prefix_base): diff --git a/src/migration-scripts/ntp/0-to-1 b/src/migration-scripts/ntp/0-to-1 index 9c66f3109..294964580 100755 --- a/src/migration-scripts/ntp/0-to-1 +++ b/src/migration-scripts/ntp/0-to-1 @@ -17,7 +17,7 @@ with open(file_name, 'r') as f: config = ConfigTree(config_file) -if not config.exists(['system', 'ntp']): +if not config.exists(['system', 'ntp', 'server']): # Nothing to do sys.exit(0) else: diff --git a/src/migration-scripts/system/20-to-21 b/src/migration-scripts/system/20-to-21 new file mode 100755 index 000000000..ad41be646 --- /dev/null +++ b/src/migration-scripts/system/20-to-21 @@ -0,0 +1,57 @@ +#!/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, argv +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 = ['system', 'sysctl'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +for all_custom in ['all', 'custom']: + if config.exists(base + [all_custom]): + for key in config.list_nodes(base + [all_custom]): + tmp = config.return_value(base + [all_custom, key, 'value']) + config.set(base + ['parameter', key, 'value'], value=tmp) + config.set_tag(base + ['parameter']) + config.delete(base + [all_custom]) + +for ipv4_param in ['net.ipv4.igmp_max_memberships', 'net.ipv4.ipfrag_time']: + if config.exists(base + [ipv4_param]): + tmp = config.return_value(base + [ipv4_param]) + config.set(base + ['parameter', ipv4_param, 'value'], value=tmp) + config.set_tag(base + ['parameter']) + config.delete(base + [ipv4_param]) + +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)) + exit(1) diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py new file mode 100755 index 000000000..66ecf8439 --- /dev/null +++ b/src/op_mode/conntrack_sync.py @@ -0,0 +1,136 @@ +#!/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 +import syslog +import xmltodict + +from argparse import ArgumentParser +from vyos.configquery import CliShellApiConfigQuery +from vyos.util import cmd +from vyos.util import run +from vyos.template import render_to_string + +conntrackd_bin = '/usr/sbin/conntrackd' +conntrackd_config = '/run/conntrackd/conntrackd.conf' + +parser = ArgumentParser(description='Conntrack Sync') +group = parser.add_mutually_exclusive_group() +group.add_argument('--restart', help='Restart connection tracking synchronization service', action='store_true') +group.add_argument('--reset-cache-internal', help='Reset internal cache', action='store_true') +group.add_argument('--reset-cache-external', help='Reset external cache', action='store_true') +group.add_argument('--show-internal', help='Show internal (main) tracking cache', action='store_true') +group.add_argument('--show-external', help='Show external (main) tracking cache', action='store_true') +group.add_argument('--show-internal-expect', help='Show internal (expect) tracking cache', action='store_true') +group.add_argument('--show-external-expect', help='Show external (expect) tracking cache', action='store_true') + +def is_configured(): + """ Check if conntrack-sync service is configured """ + config = CliShellApiConfigQuery() + if not config.exists(['service', 'conntrack-sync']): + print('Service conntrackd-sync not configured!') + exit(1) + +def send_bulk_update(): + """ send bulk update of internal-cache to other systems """ + tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -B') + if tmp > 0: + print('ERROR: failed to send bulk update to other conntrack-sync systems') + +def request_sync(): + """ request resynchronization with other systems """ + tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -n') + if tmp > 0: + print('ERROR: failed to request resynchronization of external cache') + +def flush_cache(direction): + """ flush conntrackd cache (internal or external) """ + if direction not in ['internal', 'external']: + raise ValueError() + tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -f {direction}') + if tmp > 0: + print('ERROR: failed to clear {direction} cache') + +def xml_to_stdout(xml): + out = [] + for line in xml.splitlines(): + if line == '\n': + continue + parsed = xmltodict.parse(line) + out.append(parsed) + + print(render_to_string('conntrackd/conntrackd.op-mode.tmpl', {'data' : out})) + +if __name__ == '__main__': + args = parser.parse_args() + syslog.openlog(ident='conntrack-tools', logoption=syslog.LOG_PID, + facility=syslog.LOG_INFO) + + if args.restart: + is_configured() + + syslog.syslog('Restarting conntrack sync service...') + cmd('systemctl restart conntrackd.service') + # request resynchronization with other systems + request_sync() + # send bulk update of internal-cache to other systems + send_bulk_update() + + elif args.reset_cache_external: + is_configured() + syslog.syslog('Resetting external cache of conntrack sync service...') + + # flush the external cache + flush_cache('external') + # request resynchronization with other systems + request_sync() + + elif args.reset_cache_internal: + is_configured() + syslog.syslog('Resetting internal cache of conntrack sync service...') + # flush the internal cache + flush_cache('internal') + + # request resynchronization of internal cache with kernel conntrack table + tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -R') + if tmp > 0: + print('ERROR: failed to resynchronize internal cache with kernel conntrack table') + + # send bulk update of internal-cache to other systems + send_bulk_update() + + elif args.show_external or args.show_internal or args.show_external_expect or args.show_internal_expect: + is_configured() + opt = '' + if args.show_external: + opt = '-e ct' + elif args.show_external_expect: + opt = '-e expect' + elif args.show_internal: + opt = '-i ct' + elif args.show_internal_expect: + opt = '-i expect' + + if args.show_external or args.show_internal: + print('Main Table Entries:') + else: + print('Expect Table Entries:') + out = cmd(f'sudo {conntrackd_bin} -C {conntrackd_config} {opt} -x') + xml_to_stdout(out) + + else: + parser.print_help() + exit(1) diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py index 962943896..263a3b6a5 100755 --- a/src/op_mode/dynamic_dns.py +++ b/src/op_mode/dynamic_dns.py @@ -36,6 +36,10 @@ update-status: {{ entry.status }} """ def show_status(): + # A ddclient status file must not always exist + if not os.path.exists(cache_file): + sys.exit(0) + data = { 'hosts': [] } @@ -61,11 +65,10 @@ def show_status(): if ip: outp['ip'] = ip.split(',')[0] - if 'atime=' in line: - atime = line.split('atime=')[1] - if atime: - tmp = atime.split(',')[0] - outp['time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(int(tmp, base=10))) + if 'mtime=' in line: + mtime = line.split('mtime=')[1] + if mtime: + outp['time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(int(mtime.split(',')[0], base=10))) if 'status=' in line: status = line.split('status=')[1] diff --git a/src/op_mode/monitor_bandwidth_test.sh b/src/op_mode/monitor_bandwidth_test.sh index 6da0291c5..900223bca 100755 --- a/src/op_mode/monitor_bandwidth_test.sh +++ b/src/op_mode/monitor_bandwidth_test.sh @@ -26,5 +26,5 @@ elif [[ $(dig $1 AAAA +short | grep -v '\.$' | wc -l) -gt 0 ]]; then OPT="-V" fi -/usr/bin/iperf $OPT -c $1 +/usr/bin/iperf $OPT -c $1 $2 diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py index ef9fe618c..c3cd25186 100755 --- a/src/op_mode/openconnect-control.py +++ b/src/op_mode/openconnect-control.py @@ -58,7 +58,7 @@ def main(): is_ocserv_configured() if args.action == "restart": - run("systemctl restart ocserv") + run("sudo systemctl restart ocserv.service") sys.exit(0) elif args.action == "show_sessions": show_sessions() diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py index 68cff61c8..4b7e40d1f 100755 --- a/src/op_mode/show_nat_rules.py +++ b/src/op_mode/show_nat_rules.py @@ -34,8 +34,8 @@ if args.source or args.destination: tmp = json.loads(tmp) format_nat66_rule = '{0: <10} {1: <50} {2: <50} {3: <10}' - print(format_nat66_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface")) - print(format_nat66_rule.format("----", "------" if args.source else "-----------", "-----------", "------------------" if args.source else "-----------------")) + print(format_nat_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface")) + print(format_nat_rule.format("----", "------" if args.source else "-----------", "-----------", "------------------" if args.source else "-----------------")) data_json = jmespath.search('nftables[?rule].rule[?chain]', tmp) for idx in range(0, len(data_json)): @@ -86,7 +86,7 @@ if args.source or args.destination: else: tran_addr = dict_search('snat.addr' if args.source else 'dnat.addr', data['expr'][3]) - print(format_nat66_rule.format(rule, srcdest, tran_addr, interface)) + print(format_nat_rule.format(rule, srcdest, tran_addr, interface)) exit(0) else: diff --git a/src/op_mode/vpn_ike_sa.py b/src/op_mode/vpn_ike_sa.py new file mode 100755 index 000000000..28da9f8dc --- /dev/null +++ b/src/op_mode/vpn_ike_sa.py @@ -0,0 +1,68 @@ +#!/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 +import re +import vici + +ike_sa_peer_prefix = """\ +Peer ID / IP Local ID / IP +------------ -------------""" + +ike_sa_tunnel_prefix = """ + + State IKEVer Encrypt Hash D-H Group NAT-T A-Time L-Time + ----- ------ ------- ---- --------- ----- ------ ------""" + +def s(byte_string): + return str(byte_string, 'utf-8') + +def ike_sa(peer, nat): + session = vici.Session() + sas = session.list_sas() + peers = [] + for conn in sas: + for name, sa in conn.items(): + if peer and not name.startswith('peer-' + peer): + continue + if name.startswith('peer-') and name in peers: + continue + if nat and 'nat-local' not in sa: + continue + peers.append(name) + remote_str = f'{s(sa["remote-host"])} {s(sa["remote-id"])}' if s(sa['remote-id']) != '%any' else s(sa["remote-host"]) + local_str = f'{s(sa["local-host"])} {s(sa["local-id"])}' if s(sa['local-id']) != '%any' else s(sa["local-host"]) + print(ike_sa_peer_prefix) + 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' + 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' + atime = s(sa['established']) if 'established' in sa else '0' + ltime = s(sa['rekey-time']) if 'rekey_time' in sa else '0' + print(ike_sa_tunnel_prefix) + print(' %-6s %-6s %-12s %-13s %-14s %-6s %-7s %-7s\n' % (state, version, encryption, integrity, dh_group, natt, atime, ltime)) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--peer', help='Peer name', required=False) + parser.add_argument('--nat', help='NAT Traversal', required=False) + + args = parser.parse_args() + + ike_sa(args.peer, args.nat)
\ No newline at end of file diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py new file mode 100755 index 000000000..434186abb --- /dev/null +++ b/src/op_mode/vpn_ipsec.py @@ -0,0 +1,206 @@ +#!/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 re +import struct +import sys +import argparse +from subprocess import TimeoutExpired + +from vyos.util import ask_yes_no, call, cmd, process_named_running +from Crypto.PublicKey.RSA import importKey + +RSA_LOCAL_KEY_PATH = '/config/ipsec.d/rsa-keys/localhost.key' +RSA_LOCAL_PUB_PATH = '/etc/ipsec.d/certs/localhost.pub' +RSA_KEY_PATHS = ['/config/auth', '/config/ipsec.d/rsa-keys'] + +X509_CONFIG_PATH = '/etc/ipsec.d/key-pair.template' +X509_PATH = '/config/auth/' + +IPSEC_CONF = '/etc/ipsec.conf' +SWANCTL_CONF = '/etc/swanctl.conf' + +def migrate_to_vyatta_key(path): + with open(path, 'r') as f: + key = importKey(f.read()) + e = key.e.to_bytes((key.e.bit_length() + 7) // 8, 'big') + n = key.n.to_bytes((key.n.bit_length() + 7) // 8, 'big') + return '0s' + str(base64.b64encode(struct.pack('B', len(e)) + e + n), 'ascii') + return None + +def find_rsa_keys(): + keys = [] + for path in RSA_KEY_PATHS: + if not os.path.exists(path): + continue + for filename in os.listdir(path): + full_path = os.path.join(path, filename) + if os.path.isfile(full_path) and full_path.endswith(".key"): + keys.append(full_path) + return keys + +def show_rsa_keys(): + for key_path in find_rsa_keys(): + print('Private key: ' + os.path.basename(key_path)) + print('Public key: ' + migrate_to_vyatta_key(key_path) + '\n') + +def generate_rsa_key(bits = 2192): + if (bits < 16 or bits > 4096) or bits % 16 != 0: + print('Invalid bit length') + return + + if os.path.exists(RSA_LOCAL_KEY_PATH): + if not ask_yes_no("A local RSA key file already exists and will be overwritten. Continue?"): + return + + print(f'Generating rsa-key to {RSA_LOCAL_KEY_PATH}') + + directory = os.path.dirname(RSA_LOCAL_KEY_PATH) + call(f'sudo mkdir -p {directory}') + result = call(f'sudo /usr/bin/openssl genrsa -out {RSA_LOCAL_KEY_PATH} {bits}') + + if result != 0: + print(f'Could not generate RSA key: {result}') + return + + call(f'sudo /usr/bin/openssl rsa -inform PEM -in {RSA_LOCAL_KEY_PATH} -pubout -out {RSA_LOCAL_PUB_PATH}') + + print('Your new local RSA key has been generated') + print('The public portion of the key is:\n') + print(migrate_to_vyatta_key(RSA_LOCAL_KEY_PATH)) + +def generate_x509_pair(name): + if os.path.exists(X509_PATH + name): + if not ask_yes_no("A certificate request with this name already exists and will be overwritten. Continue?"): + return + + result = os.system(f'openssl req -new -nodes -keyout {X509_PATH}{name}.key -out {X509_PATH}{name}.csr -config {X509_CONFIG_PATH}') + + if result != 0: + print(f'Could not generate x509 key-pair: {result}') + return + + print('Private key and certificate request has been generated') + print(f'CSR: {X509_PATH}{name}.csr') + 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))$' + matches = [] + with open(IPSEC_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 + if return_all or (result[2] == suffix): + matches.append(result[1]) + return matches + +def reset_peer(peer, tunnel): + if not peer: + print('Invalid peer, aborting') + return + + conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all')) + + if not conns: + print('Tunnel(s) not found, aborting') + return + + result = True + for conn in conns: + try: + call(f'sudo /usr/sbin/ipsec down {conn}', timeout = 10) + call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10) + except TimeoutExpired as e: + print(f'Timed out while resetting {conn}') + result = False + + + print('Peer reset result: ' + ('success' if result else 'failed')) + +def get_profile_connection(profile, tunnel = None): + search = rf'(dmvpn-{profile}-[\w]+)' if tunnel == 'all' else rf'(dmvpn-{profile}-{tunnel})' + with open(SWANCTL_CONF, 'r') as f: + for line in f.readlines(): + result = re.search(search, line) + if result: + return result[1] + return None + +def reset_profile(profile, tunnel): + if not profile: + print('Invalid profile, aborting') + return + + if not tunnel: + print('Invalid tunnel, aborting') + return + + conn = get_profile_connection(profile) + + if not conn: + print('Profile not found, aborting') + return + + call(f'sudo /usr/sbin/ipsec down {conn}') + result = call(f'sudo /usr/sbin/ipsec up {conn}') + + print('Profile reset result: ' + ('success' if result == 0 else 'failed')) + +def debug_peer(peer, tunnel): + if not peer or peer == "all": + call('sudo /usr/sbin/ipsec statusall') + return + + if not tunnel or tunnel == 'all': + tunnel = '' + + conn = get_peer_connection(peer, tunnel) + + if not conn: + print('Peer not found, aborting') + return + + call(f'sudo /usr/sbin/ipsec statusall | grep {conn}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--action', help='Control action', required=True) + parser.add_argument('--bits', help='Bits for rsa-key', required=False) + parser.add_argument('--name', help='Name for x509 key-pair, peer for reset', required=False) + parser.add_argument('--tunnel', help='Specific tunnel of peer', required=False) + + args = parser.parse_args() + + if args.action == 'rsa-key': + bits = int(args.bits) if args.bits else 2192 + generate_rsa_key(bits) + elif args.action == 'rsa-key-show': + show_rsa_keys() + elif args.action == 'x509': + if not args.name: + print('Invalid name for key-pair, aborting.') + sys.exit(0) + generate_x509_pair(args.name) + elif args.action == 'reset-peer': + reset_peer(args.name, args.tunnel) + elif args.action == "reset-profile": + reset_profile(args.name, args.tunnel) + elif args.action == "vpn-debug": + debug_peer(args.name, args.tunnel) diff --git a/src/systemd/opennhrp.service b/src/systemd/opennhrp.service new file mode 100644 index 000000000..70235f89d --- /dev/null +++ b/src/systemd/opennhrp.service @@ -0,0 +1,13 @@ +[Unit] +Description=OpenNHRP +After=vyos-router.service +ConditionPathExists=/run/opennhrp/opennhrp.conf +StartLimitIntervalSec=0 + +[Service] +Type=forking +ExecStart=/usr/sbin/opennhrp -d -v -a /run/opennhrp.socket -c /run/opennhrp/opennhrp.conf -s /etc/opennhrp/opennhrp-script.py -p /run/opennhrp.pid +ExecReload=/usr/bin/kill -HUP $MAINPID +PIDFile=/run/opennhrp.pid +Restart=on-failure +RestartSec=20 diff --git a/src/validators/ipv4 b/src/validators/ipv4 new file mode 100755 index 000000000..53face090 --- /dev/null +++ b/src/validators/ipv4 @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4 $1 diff --git a/src/validators/ipv4-multicast b/src/validators/ipv4-multicast new file mode 100755 index 000000000..e5cbc9532 --- /dev/null +++ b/src/validators/ipv4-multicast @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-multicast $1 diff --git a/src/validators/ipv6-exclude b/src/validators/ipv6-exclude new file mode 100755 index 000000000..893eeab09 --- /dev/null +++ b/src/validators/ipv6-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv6 "${arg:1}" diff --git a/src/validators/ipv6-multicast b/src/validators/ipv6-multicast new file mode 100755 index 000000000..66cd90c9c --- /dev/null +++ b/src/validators/ipv6-multicast @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv6-multicast $1 diff --git a/src/validators/ipv6-range b/src/validators/ipv6-range new file mode 100755 index 000000000..033b6461b --- /dev/null +++ b/src/validators/ipv6-range @@ -0,0 +1,16 @@ +#!/usr/bin/python3 + +import sys +import re +from vyos.template import is_ipv6 + +if __name__ == '__main__': + if len(sys.argv)>1: + ipv6_range = sys.argv[1] + # Regex for ipv6-ipv6 https://regexr.com/ + if re.search('([a-f0-9:]+:+)+[a-f0-9]+-([a-f0-9:]+:+)+[a-f0-9]+', ipv6_range): + for tmp in ipv6_range.split('-'): + if not is_ipv6(tmp): + sys.exit(1) + + sys.exit(0) diff --git a/src/validators/ipv6-range-exclude b/src/validators/ipv6-range-exclude new file mode 100755 index 000000000..912b55ae3 --- /dev/null +++ b/src/validators/ipv6-range-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv6-range "${arg:1}" diff --git a/src/validators/sysctl b/src/validators/sysctl new file mode 100755 index 000000000..9b5bba3e1 --- /dev/null +++ b/src/validators/sysctl @@ -0,0 +1,24 @@ +#!/bin/sh +# +# 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/>. + +declare -a array +eval "array=($(/sbin/sysctl -N -a))" + +if [[ ! " ${array[@]} " =~ " $1 " ]]; then + # passed sysctl option is invalid + exit 1 +fi +exit 0 |