diff options
Diffstat (limited to 'src')
23 files changed, 812 insertions, 76 deletions
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 1118143e4..7b3afa058 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -47,12 +47,6 @@ def get_config(config=None): base = ['interfaces', 'l2tpv3'] l2tpv3 = get_interface_dict(conf, base) - # L2TPv3 is "special" the default MTU is 1488 - update accordingly - # as the config_level is already st in get_interface_dict() - we can use [] - tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) - if 'mtu' not in tmp: - l2tpv3['mtu'] = '1488' - # To delete an l2tpv3 interface we need the current tunnel and session-id if 'deleted' in l2tpv3: tmp = leaf_node_changed(conf, ['tunnel-id']) diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 2c8367ff3..bfebed7e4 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -49,14 +49,6 @@ def get_config(config=None): base = ['interfaces', 'macsec'] macsec = get_interface_dict(conf, base) - # MACsec is "special" the default MTU is 1460 - update accordingly - # as the config_level is already st in get_interface_dict() - we can use [] - tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) - if 'mtu' not in tmp: - # base MTU for MACsec is 1468 bytes, but we leave room for 802.1ad and - # 802.1q VLAN tags, thus the limit is 1460 bytes. - macsec['mtu'] = '1460' - # Check if interface has been removed if 'deleted' in macsec: source_interface = conf.return_effective_value(['source-interface']) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index e4a6a5ec1..ee6f05fcd 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -17,6 +17,7 @@ import os import re +from glob import glob from sys import exit from ipaddress import IPv4Address from ipaddress import IPv4Network @@ -488,14 +489,9 @@ def apply(openvpn): # Do some cleanup when OpenVPN is disabled/deleted if 'deleted' in openvpn or 'disable' in openvpn: - # cleanup old configuration files - cleanup = [] - cleanup.append(cfg_file.format(**openvpn)) - cleanup.append(openvpn['auth_user_pass_file']) - - for file in cleanup: - if os.path.isfile(file): - os.unlink(file) + for cleanup_file in glob(f'/run/openvpn/{interface}.*'): + if os.path.isfile(cleanup_file): + os.unlink(cleanup_file) if interface in interfaces(): VTunIf(interface).remove() diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index c31e49574..f49792a7a 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -43,12 +43,6 @@ def get_config(config=None): base = ['interfaces', 'pppoe'] pppoe = get_interface_dict(conf, base) - # PPPoE is "special" the default MTU is 1492 - update accordingly - # as the config_level is already st in get_interface_dict() - we can use [] - tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) - if 'mtu' not in tmp: - pppoe['mtu'] = '1492' - return pppoe def verify(pppoe): diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index d2fcf3121..f03bc9d5d 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -57,12 +57,6 @@ def get_config(config=None): base = ['interfaces', 'tunnel'] tunnel = get_interface_dict(conf, base) - # Wireguard is "special" the default MTU is 1420 - update accordingly - # as the config_level is already st in get_interface_dict() - we can use [] - tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) - if 'mtu' not in tmp: - tunnel['mtu'] = '1476' - tmp = leaf_node_changed(conf, ['encapsulation']) if tmp: tunnel.update({'encapsulation_changed': {}}) diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 04e258fcf..9a6d72772 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -42,12 +42,6 @@ def get_config(config=None): base = ['interfaces', 'vxlan'] vxlan = get_interface_dict(conf, base) - # VXLAN is "special" the default MTU is 1492 - update accordingly - # as the config_level is already st in get_interface_dict() - we can use [] - tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) - if 'mtu' not in tmp: - vxlan['mtu'] = '1450' - return vxlan def verify(vxlan): diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 3e6320f02..024ab8f59 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -46,12 +46,6 @@ def get_config(config=None): base = ['interfaces', 'wireguard'] wireguard = get_interface_dict(conf, base) - # Wireguard is "special" the default MTU is 1420 - update accordingly - # as the config_level is already st in get_interface_dict() - we can use [] - tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) - if 'mtu' not in tmp: - wireguard['mtu'] = '1420' - # Mangle private key - it has a default so its always valid wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 2d98cb11b..dae958774 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -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 @@ -26,6 +26,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render +from vyos.template import is_ip_network from vyos.util import cmd from vyos.util import check_kmod from vyos.util import dict_search @@ -68,9 +69,9 @@ def verify_rule(config, err_msg): 'ports can only be specified when protocol is '\ 'either tcp, udp or tcp_udp!') - if '/' in (dict_search('translation.address', config) or []): + if is_ip_network(dict_search('translation.address', config)): raise ConfigError(f'{err_msg}\n' \ - 'Cannot use ports with an IPv4net type translation address as it\n' \ + 'Cannot use ports with an IPv4 network as translation address as it\n' \ 'statically maps a whole network of addresses onto another\n' \ 'network of addresses') @@ -147,7 +148,7 @@ def verify(nat): addr = dict_search('translation.address', config) if addr != None: - if addr != 'masquerade': + if addr != 'masquerade' and not is_ip_network(addr): for ip in addr.split('-'): if not is_addr_assigned(ip): print(f'WARNING: IP address {ip} does not exist on the system!') diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py new file mode 100755 index 000000000..dc3cd550c --- /dev/null +++ b/src/conf_mode/nat66.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import jmespath +import json +import os + +from sys import exit +from netifaces import interfaces + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.util import cmd +from vyos.util import check_kmod +from vyos.util import dict_search +from vyos.template import is_ipv6 +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +k_mod = ['nft_nat', 'nft_chain_nat'] + +iptables_nat_config = '/tmp/vyos-nat66-rules.nft' +ndppd_config = '/run/ndppd/ndppd.conf' + +def get_handler(json, chain, target): + """ Get nftable rule handler number of given chain/target combination. + Handler is required when adding NAT66/Conntrack helper targets """ + for x in json: + if x['chain'] != chain: + continue + if x['target'] != target: + continue + return x['handle'] + + return None + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['nat66'] + nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # T2665: we must add the tagNode defaults individually until this is + # moved to the base class + for direction in ['source', 'destination']: + if direction in nat: + default_values = defaults(base + [direction, 'rule']) + if 'rule' in nat[direction]: + for rule in nat[direction]['rule']: + nat[direction]['rule'][rule] = dict_merge(default_values, + nat[direction]['rule'][rule]) + + # read in current nftable (once) for further processing + tmp = cmd('nft -j list table ip6 raw') + nftable_json = json.loads(tmp) + + # condense the full JSON table into a list with only relevand informations + pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}' + condensed_json = jmespath.search(pattern, nftable_json) + + if not conf.exists(base): + nat['helper_functions'] = 'remove' + nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT','NAT_CONNTRACK') + nat['deleted'] = '' + return nat + + # check if NAT66 connection tracking helpers need to be set up - this has to + # be done only once + if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'): + nat['helper_functions'] = 'add' + + # Retrieve current table handler positions + nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT','VYATTA_CT_OUTPUT_HOOK') + else: + nat['helper_functions'] = 'has' + + return nat + +def verify(nat): + if not nat or 'deleted' in nat: + # no need to verify the CLI as NAT66 is going to be deactivated + return None + + if 'helper_functions' in nat and nat['helper_functions'] != 'has': + if not (nat['pre_ct_conntrack'] or nat['out_ct_conntrack']): + raise Exception('could not determine nftable ruleset handlers') + + if dict_search('source.rule', nat): + for rule, config in dict_search('source.rule', nat).items(): + err_msg = f'Source NAT66 configuration error in rule {rule}:' + if 'outbound_interface' not in config: + raise ConfigError(f'{err_msg}\n' \ + 'outbound-interface not specified') + else: + if config['outbound_interface'] not in interfaces(): + print(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') + + prefix = dict_search('translation.prefix', config) + if prefix != None: + if not is_ipv6(prefix): + raise ConfigError(f'Warning: IPv6 prefix {prefix} is not a valid address prefix') + + prefix = dict_search('source.prefix', config) + if prefix != None: + if not is_ipv6(prefix): + raise ConfigError(f'{err_msg} source-prefix not specified') + + if dict_search('destination.rule', nat): + for rule, config in dict_search('destination.rule', nat).items(): + err_msg = f'Destination NAT66 configuration error in rule {rule}:' + + if 'inbound_interface' not in config: + raise ConfigError(f'{err_msg}\n' \ + 'inbound-interface not specified') + else: + if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): + print(f'WARNING: rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') + + return None + +def generate(nat): + render(iptables_nat_config, 'firewall/nftables-nat66.tmpl', nat, permission=0o755) + render(ndppd_config, 'proxy-ndp/ndppd.conf.tmpl', nat, permission=0o755) + return None + +def apply(nat): + if not nat: + return None + cmd(f'{iptables_nat_config}') + if 'deleted' in nat or not dict_search('source.rule', nat): + cmd('systemctl stop ndppd') + if os.path.isfile(ndppd_config): + os.unlink(ndppd_config) + else: + cmd('systemctl restart ndppd') + if os.path.isfile(iptables_nat_config): + os.unlink(iptables_nat_config) + + return None + +if __name__ == '__main__': + try: + check_kmod(k_mod) + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index de0148b2f..41d89e03b 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -14,6 +14,8 @@ # 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 @@ -28,12 +30,25 @@ from vyos import airbag airbag.enable() config_file = r'/tmp/bgp.frr' - -def get_config(): - conf = Config() +frr_daemon = 'bgpd' + +DEBUG = os.path.exists('/tmp/bgp.debug') +if DEBUG: + import logging + lg = logging.getLogger("vyos.frr") + lg.setLevel(logging.DEBUG) + ch = logging.StreamHandler() + lg.addHandler(ch) + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base = ['protocols', 'bgp'] bgp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # Bail out early if configuration tree does not exist if not conf.exists(base): return bgp @@ -78,8 +93,13 @@ def verify(bgp): if neighbor == 'neighbor': # remote-as must be either set explicitly for the neighbor # or for the entire peer-group - if 'remote_as' not in peer_config: - if 'peer_group' not in peer_config or 'remote_as' not in asn_config['peer_group'][peer_config['peer_group']]: + if 'interface' in peer_config: + if 'remote_as' not in peer_config['interface']: + if 'peer_group' not in peer_config['interface'] or 'remote_as' not in asn_config['peer_group'][ peer_config['interface']['peer_group'] ]: + raise ConfigError('Remote AS must be set for neighbor or peer-group!') + + elif 'remote_as' not in peer_config: + if 'peer_group' not in peer_config or 'remote_as' not in asn_config['peer_group'][ peer_config['peer_group'] ]: raise ConfigError('Remote AS must be set for neighbor or peer-group!') for afi in ['ipv4_unicast', 'ipv6_unicast']: @@ -94,7 +114,7 @@ def verify(bgp): if tmp not in afi_config['prefix_list']: # bail out early continue - # get_config_dict() mangles all '-' characters to '_' this is legitim, thus all our + # get_config_dict() mangles all '-' characters to '_' this is legitimate, thus all our # compares will run on '_' as also '_' is a valid name for a prefix-list prefix_list = afi_config['prefix_list'][tmp].replace('-', '_') if afi == 'ipv4_unicast': @@ -113,6 +133,16 @@ def verify(bgp): if dict_search(f'policy.route_map.{route_map}', asn_config) == None: raise ConfigError(f'route-map "{route_map}" used for "{tmp}" does not exist!') + # Throw an error if a peer group is not configured for allow range + for prefix in dict_search('listen.range', asn_config) or []: + # we can not use dict_search() here as prefix contains dots ... + if 'peer_group' not in asn_config['listen']['range'][prefix]: + raise ConfigError(f'Listen range for prefix "{prefix}" has no peer group configured.') + else: + peer_group = asn_config['listen']['range'][prefix]['peer_group'] + # the peer group must also exist + if not dict_search(f'peer_group.{peer_group}', asn_config): + raise ConfigError(f'Peer-group "{peer_group}" for listen range "{prefix}" does not exist!') return None @@ -135,25 +165,32 @@ def generate(bgp): def apply(bgp): # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(daemon='bgpd') + frr_cfg.load_configuration(frr_daemon) frr_cfg.modify_section(f'router bgp \S+', '') frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['new_frr_config']) - frr_cfg.commit_configuration(daemon='bgpd') + + # Debugging + if DEBUG: + from pprint import pprint + print('') + print('--------- DEBUGGING ----------') + pprint(dir(frr_cfg)) + print('Existing config:\n') + for line in frr_cfg.original_config: + print(line) + print(f'Replacement config:\n') + print(f'{bgp["new_frr_config"]}') + print(f'Modified config:\n') + print(f'{frr_cfg}') + + frr_cfg.commit_configuration(frr_daemon) # If FRR config is blank, rerun the blank commit x times due to frr-reload # behavior/bug not properly clearing out on one commit. if bgp['new_frr_config'] == '': for a in range(5): - frr_cfg.commit_configuration(daemon='bgpd') + frr_cfg.commit_configuration(frr_daemon) - # Debugging - ''' - print('') - print('--------- DEBUGGING ----------') - print(f'Existing config:\n{frr_cfg["original_config"]}\n\n') - print(f'Replacement config:\n{bgp["new_frr_config"]}\n\n') - print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n') - ''' return None diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py new file mode 100755 index 000000000..7c0ffbd27 --- /dev/null +++ b/src/conf_mode/protocols_ospf.py @@ -0,0 +1,163 @@ +#!/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_route_maps +from vyos.template import render +from vyos.template import render_to_string +from vyos.util import call +from vyos.util import dict_search +from vyos.xml import defaults +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/ospf.frr' +frr_daemon = 'ospfd' + +DEBUG = os.path.exists('/tmp/ospf.debug') +if DEBUG: + import logging + lg = logging.getLogger("vyos.frr") + lg.setLevel(logging.DEBUG) + ch = logging.StreamHandler() + lg.addHandler(ch) + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['protocols', 'ospf'] + ospf = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # Bail out early if configuration tree does not exist + if not conf.exists(base): + return ospf + + # 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) + + # We have to cleanup the default dict, as default values could enable features + # which are not explicitly enabled on the CLI. Example: default-information + # originate comes with a default metric-type of 2, which will enable the + # entire default-information originate tree, even when not set via CLI so we + # need to check this first and probably drop that key. + if dict_search('default_information.originate', ospf) is None: + del default_values['default_information'] + if dict_search('area.area_type.nssa', ospf) is None: + del default_values['area']['area_type']['nssa'] + if 'mpls_te' not in ospf: + del default_values['mpls_te'] + for protocol in ['bgp', 'connected', 'kernel', 'rip', 'static']: + if dict_search(f'redistribute.{protocol}', ospf) is None: + del default_values['redistribute'][protocol] + # XXX: T2665: we currently have no nice way for defaults under tag nodes, + # clean them out and add them manually :( + del default_values['neighbor'] + del default_values['area']['virtual_link'] + + # merge in remaining default values + ospf = dict_merge(default_values, ospf) + + if 'neighbor' in ospf: + default_values = defaults(base + ['neighbor']) + for neighbor in ospf['neighbor']: + ospf['neighbor'][neighbor] = dict_merge(default_values, ospf['neighbor'][neighbor]) + + if 'area' in ospf: + default_values = defaults(base + ['area', 'virtual-link']) + for area, area_config in ospf['area'].items(): + if 'virtual_link' in area_config: + print(default_values) + for virtual_link in area_config['virtual_link']: + ospf['area'][area]['virtual_link'][virtual_link] = dict_merge( + default_values, ospf['area'][area]['virtual_link'][virtual_link]) + + # We also need some additional information from the config, prefix-lists + # and route-maps for instance. They will be used in verify() + base = ['policy'] + tmp = conf.get_config_dict(base, key_mangling=('-', '_')) + # Merge policy dict into OSPF dict + ospf = dict_merge(tmp, ospf) + + return ospf + +def verify(ospf): + if not ospf: + return None + + verify_route_maps(ospf) + return None + +def generate(ospf): + if not ospf: + ospf['new_frr_config'] = '' + return None + + # render(config) not needed, its only for debug + render(config_file, 'frr/ospf.frr.tmpl', ospf) + ospf['new_frr_config'] = render_to_string('frr/ospf.frr.tmpl', ospf) + + return None + +def apply(ospf): + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + frr_cfg.load_configuration(frr_daemon) + frr_cfg.modify_section('router ospf', '') + frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['new_frr_config']) + + # Debugging + if DEBUG: + from pprint import pprint + print('') + print('--------- DEBUGGING ----------') + pprint(dir(frr_cfg)) + print('Existing config:\n') + for line in frr_cfg.original_config: + print(line) + print(f'Replacement config:\n') + print(f'{ospf["new_frr_config"]}') + print(f'Modified config:\n') + print(f'{frr_cfg}') + + frr_cfg.commit_configuration(frr_daemon) + + # If FRR config is blank, rerun the blank commit x times due to frr-reload + # behavior/bug not properly clearing out on one commit. + if ospf['new_frr_config'] == '': + for a in range(5): + frr_cfg.commit_configuration(frr_daemon) + + 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_ospfv3.py b/src/conf_mode/protocols_ospfv3.py new file mode 100755 index 000000000..2fb600056 --- /dev/null +++ b/src/conf_mode/protocols_ospfv3.py @@ -0,0 +1,123 @@ +#!/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_route_maps +from vyos.template import render +from vyos.template import render_to_string +from vyos.util import call +from vyos.util import dict_search +from vyos.xml import defaults +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/ospfv3.frr' +frr_daemon = 'ospf6d' + +DEBUG = os.path.exists('/tmp/ospfv3.debug') +if DEBUG: + import logging + lg = logging.getLogger("vyos.frr") + lg.setLevel(logging.DEBUG) + ch = logging.StreamHandler() + lg.addHandler(ch) + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['protocols', 'ospfv3'] + ospfv3 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # Bail out early if configuration tree does not exist + if not conf.exists(base): + return ospfv3 + + # We also need some additional information from the config, prefix-lists + # and route-maps for instance. They will be used in verify() + base = ['policy'] + tmp = conf.get_config_dict(base, key_mangling=('-', '_')) + # Merge policy dict into OSPF dict + ospfv3 = dict_merge(tmp, ospfv3) + + return ospfv3 + +def verify(ospfv3): + if not ospfv3: + return None + + verify_route_maps(ospfv3) + return None + +def generate(ospfv3): + if not ospfv3: + ospfv3['new_frr_config'] = '' + return None + + # render(config) not needed, its only for debug + render(config_file, 'frr/ospfv3.frr.tmpl', ospfv3) + ospfv3['new_frr_config'] = render_to_string('frr/ospfv3.frr.tmpl', ospfv3) + + return None + +def apply(ospfv3): + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + frr_cfg.load_configuration(frr_daemon) + frr_cfg.modify_section('router ospf6', '') + frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospfv3['new_frr_config']) + + # Debugging + if DEBUG: + from pprint import pprint + print('') + print('--------- DEBUGGING ----------') + pprint(dir(frr_cfg)) + print('Existing config:\n') + for line in frr_cfg.original_config: + print(line) + print(f'Replacement config:\n') + print(f'{ospfv3["new_frr_config"]}') + print(f'Modified config:\n') + print(f'{frr_cfg}') + + frr_cfg.commit_configuration(frr_daemon) + + # If FRR config is blank, re-run the blank commit x times due to frr-reload + # behavior/bug not properly clearing out on one commit. + if ospfv3['new_frr_config'] == '': + for a in range(5): + frr_cfg.commit_configuration(frr_daemon) + + 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/ssh.py b/src/conf_mode/ssh.py index 8eeb0a7c1..67724b043 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.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 @@ -17,6 +17,8 @@ import os from sys import exit +from syslog import syslog +from syslog import LOG_INFO from vyos.config import Config from vyos.configdict import dict_merge @@ -31,6 +33,10 @@ airbag.enable() config_file = r'/run/sshd/sshd_config' systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf' +key_rsa = '/etc/ssh/ssh_host_rsa_key' +key_dsa = '/etc/ssh/ssh_host_dsa_key' +key_ed25519 = '/etc/ssh/ssh_host_ed25519_key' + def get_config(config=None): if config: conf = config @@ -66,6 +72,18 @@ def generate(ssh): return None + # This usually happens only once on a fresh system, SSH keys need to be + # freshly generted, one per every system! + if not os.path.isfile(key_rsa): + syslog(LOG_INFO, 'SSH RSA host key not found, generating new key!') + call(f'ssh-keygen -q -N "" -t rsa -f {key_rsa}') + if not os.path.isfile(key_dsa): + syslog(LOG_INFO, 'SSH DSA host key not found, generating new key!') + call(f'ssh-keygen -q -N "" -t dsa -f {key_dsa}') + if not os.path.isfile(key_ed25519): + syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!') + call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}') + render(config_file, 'ssh/sshd_config.tmpl', ssh) render(systemd_override, 'ssh/override.conf.tmpl', ssh) # Reload systemd manager configuration diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index 910c14474..454611c55 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -87,10 +87,10 @@ def apply(options): # Ctrl-Alt-Delete action if os.path.exists(systemd_action_file): os.unlink(systemd_action_file) - if 'ctrl_alt_del' in options: - if options['ctrl_alt_del'] == 'reboot': + if 'ctrl_alt_delete' in options: + if options['ctrl_alt_delete'] == 'reboot': os.symlink('/lib/systemd/system/reboot.target', systemd_action_file) - elif options['ctrl_alt_del'] == 'poweroff': + elif options['ctrl_alt_delete'] == 'poweroff': os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file) # Configure HTTP client @@ -104,11 +104,11 @@ def apply(options): os.unlink(ssh_config) # Reboot system on kernel panic + timeout = '0' + if 'reboot_on_panic' in options: + timeout = '60' with open('/proc/sys/kernel/panic', 'w') as f: - if 'reboot_on_panic' in options: - f.write('60') - else: - f.write('0') + f.write(timeout) # tuned - performance tuning if 'performance' in options: diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf new file mode 100644 index 000000000..8265e12dc --- /dev/null +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -0,0 +1,98 @@ +# +# VyOS specific sysctl settings, see sysctl.conf (5) for information. +# + +# Panic on OOPS +kernel.panic_on_oops=1 + +# Timeout before rebooting on panic +kernel.panic=60 + +# Send all core files to /var/core/core.program.pid.time +kernel.core_pattern=/var/core/core-%e-%p-%t + +# ARP configuration +# arp_filter - allow multiple network interfaces on same subnet +# arp_announce - avoid local addresses no on target's subnet +# arp_ignore - reply only if target IP is local_address on the interface + +# arp_filter defaults to 1 so set all to 0 so vrrp interfaces can override it. +net.ipv4.conf.all.arp_filter=0 + +# https://phabricator.vyos.net/T300 +net.ipv4.conf.all.arp_ignore=0 + +net.ipv4.conf.all.arp_announce=2 + +# Enable packet forwarding for IPv4 +net.ipv4.ip_forward=1 + +# if a primary address is removed from an interface promote the +# secondary address if available +net.ipv4.conf.all.promote_secondaries=1 + +# Ignore ICMP broadcasts sent to broadcast/multicast +net.ipv4.icmp_echo_ignore_broadcasts=1 + +# Ignore bogus ICMP errors +net.ipv4.icmp_ignore_bogus_error_responses=1 + +# Send ICMP responses with primary address of exiting interface +net.ipv4.icmp_errors_use_inbound_ifaddr=1 + +# Log packets with impossible addresses to kernel log +net.ipv4.conf.all.log_martians=1 + +# Do not ignore all ICMP ECHO requests by default +net.ipv4.icmp_echo_ignore_all=0 + +# Disable source validation by default +net.ipv4.conf.all.rp_filter=0 +net.ipv4.conf.default.rp_filter=0 + +# Enable tcp syn-cookies by default +net.ipv4.tcp_syncookies=1 + +# Disable accept_redirects by default for any interface +net.ipv4.conf.all.accept_redirects=0 +net.ipv4.conf.default.accept_redirects=0 +net.ipv6.conf.all.accept_redirects=0 +net.ipv6.conf.default.accept_redirects=0 + +# Disable accept_source_route by default +net.ipv4.conf.all.accept_source_route=0 +net.ipv4.conf.default.accept_source_route=0 +net.ipv6.conf.all.accept_source_route=0 +net.ipv6.conf.default.accept_source_route=0 + +# Enable send_redirects by default +net.ipv4.conf.all.send_redirects=1 +net.ipv4.conf.default.send_redirects=1 + +# Increase size of buffer for netlink +net.core.rmem_max=2097152 + +# Enable packet forwarding for IPv6 +net.ipv6.conf.all.forwarding=1 + +# Increase route table limit +net.ipv6.route.max_size = 262144 + +# Do not forget IPv6 addresses when a link goes down +net.ipv6.conf.default.keep_addr_on_down=1 +net.ipv6.conf.all.keep_addr_on_down=1 + +# Default value of 20 seems to interfere with larger OSPF and VRRP setups +net.ipv4.igmp_max_memberships = 512 + +# Enable conntrack helper by default +net.netfilter.nf_conntrack_helper=1 + +# Increase default garbage collection thresholds +net.ipv4.neigh.default.gc_thresh1 = 1024 +net.ipv4.neigh.default.gc_thresh2 = 4096 +net.ipv4.neigh.default.gc_thresh3 = 8192 +# +net.ipv6.neigh.default.gc_thresh1 = 1024 +net.ipv6.neigh.default.gc_thresh2 = 4096 +net.ipv6.neigh.default.gc_thresh3 = 8192 diff --git a/src/etc/udev/rules.d/42-qemu-usb.rules b/src/etc/udev/rules.d/42-qemu-usb.rules new file mode 100644 index 000000000..a79543df7 --- /dev/null +++ b/src/etc/udev/rules.d/42-qemu-usb.rules @@ -0,0 +1,14 @@ +# +# Enable autosuspend for qemu emulated usb hid devices. +# +# Note that there are buggy qemu versions which advertise remote +# wakeup support but don't actually implement it correctly. This +# is the reason why we need a match for the serial number here. +# The serial number "42" is used to tag the implementations where +# remote wakeup is working. +# +# Gerd Hoffmann <kraxel@xxxxxxxxxx> + +ACTION=="add", SUBSYSTEM=="usb", ATTR{product}=="QEMU USB Mouse", ATTR{serial}=="42", TEST=="power/control", ATTR{power/control}="auto" +ACTION=="add", SUBSYSTEM=="usb", ATTR{product}=="QEMU USB Tablet", ATTR{serial}=="42", TEST=="power/control", ATTR{power/control}="auto" +ACTION=="add", SUBSYSTEM=="usb", ATTR{product}=="QEMU USB Keyboard", ATTR{serial}=="42", TEST=="power/control", ATTR{power/control}="auto" diff --git a/src/etc/udev/rules.d/63-hyperv-vf-net.rules b/src/etc/udev/rules.d/63-hyperv-vf-net.rules new file mode 100644 index 000000000..b4dcb5a39 --- /dev/null +++ b/src/etc/udev/rules.d/63-hyperv-vf-net.rules @@ -0,0 +1,5 @@ +ATTR{[dmi/id]sys_vendor}!="Microsoft Corporation", GOTO="end_hyperv_nic" + +ACTION=="add", SUBSYSTEM=="net", DRIVERS=="hv_pci", NAME="vf_%k" + +LABEL="end_hyperv_nic" diff --git a/src/etc/udev/rules.d/64-vyos-vmware-net.rules b/src/etc/udev/rules.d/64-vyos-vmware-net.rules new file mode 100644 index 000000000..66a4a069b --- /dev/null +++ b/src/etc/udev/rules.d/64-vyos-vmware-net.rules @@ -0,0 +1,14 @@ +ATTR{[dmi/id]sys_vendor}!="VMware, Inc.", GOTO="end_vmware_nic" + +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet0", ENV{VYOS_IFNAME}="eth0" +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet1", ENV{VYOS_IFNAME}="eth1" +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet2", ENV{VYOS_IFNAME}="eth2" +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet3", ENV{VYOS_IFNAME}="eth3" +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet4", ENV{VYOS_IFNAME}="eth4" +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet5", ENV{VYOS_IFNAME}="eth5" +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet6", ENV{VYOS_IFNAME}="eth6" +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet7", ENV{VYOS_IFNAME}="eth7" +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet8", ENV{VYOS_IFNAME}="eth8" +ACTION=="add", SUBSYSTEM=="net", ATTRS{label}=="Ethernet9", ENV{VYOS_IFNAME}="eth9" + +LABEL="end_vmware_nic" diff --git a/src/etc/udev/rules.d/65-vyatta-net.rules b/src/etc/udev/rules.d/65-vyatta-net.rules new file mode 100644 index 000000000..2b48c1213 --- /dev/null +++ b/src/etc/udev/rules.d/65-vyatta-net.rules @@ -0,0 +1,26 @@ +# These rules use vyatta_net_name to persistently name network interfaces +# per "hwid" association in the Vyatta configuration file. + +ACTION!="add", GOTO="vyatta_net_end" +SUBSYSTEM!="net", GOTO="vyatta_net_end" + +# ignore the interface if a name has already been set +NAME=="?*", GOTO="vyatta_net_end" + +# Do name change for ethernet and wireless devices only +KERNEL!="eth*|wlan*", GOTO="vyatta_net_end" + +# ignore "secondary" monitor interfaces of mac80211 drivers +KERNEL=="wlan*", ATTRS{type}=="803", GOTO="vyatta_net_end" + +# If using VyOS predefined names +ENV{VYOS_IFNAME}!="eth*", GOTO="end_vyos_predef_names" + +DRIVERS=="?*", PROGRAM="vyatta_net_name %k $attr{address} $env{VYOS_IFNAME}", NAME="%c", GOTO="vyatta_net_end" + +LABEL="end_vyos_predef_names" + +# ignore interfaces without a driver link like bridges and VLANs +DRIVERS=="?*", PROGRAM="vyatta_net_name %k $attr{address}", NAME="%c" + +LABEL="vyatta_net_end" diff --git a/src/migration-scripts/nat66/0-to-1 b/src/migration-scripts/nat66/0-to-1 new file mode 100755 index 000000000..74d64c07b --- /dev/null +++ b/src/migration-scripts/nat66/0-to-1 @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sys import argv,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() + +config = ConfigTree(config_file) + +def merge_npt(config,base,rule): + merge_base = ['nat66','source','rule',rule] + # Configure migration functions + if config.exists(base + ['description']): + tmp = config.return_value(base + ['description']) + config.set(merge_base + ['description'],value=tmp) + + if config.exists(base + ['disable']): + tmp = config.return_value(base + ['disable']) + config.set(merge_base + ['disable'],value=tmp) + + if config.exists(base + ['outbound-interface']): + tmp = config.return_value(base + ['outbound-interface']) + config.set(merge_base + ['outbound-interface'],value=tmp) + + if config.exists(base + ['source','prefix']): + tmp = config.return_value(base + ['source','prefix']) + config.set(merge_base + ['source','prefix'],value=tmp) + + if config.exists(base + ['translation','prefix']): + tmp = config.return_value(base + ['translation','prefix']) + config.set(merge_base + ['translation','prefix'],value=tmp) + +if not config.exists(['nat', 'nptv6']): + # Nothing to do + exit(0) + +for rule in config.list_nodes(['nat', 'nptv6', 'rule']): + base = ['nat', 'nptv6', 'rule', rule] + # Merge 'nat nptv6' to 'nat66 source' + merge_npt(config,base,rule) + +# Delete the original NPT configuration +config.delete(['nat','nptv6']); + +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/systemd/dropbear@.service b/src/systemd/dropbear@.service index 606a7ea6d..a3fde5708 100644 --- a/src/systemd/dropbear@.service +++ b/src/systemd/dropbear@.service @@ -4,6 +4,7 @@ Requires=dropbearkey.service Wants=conserver-server.service ConditionPathExists=/run/conserver/conserver.cf After=dropbearkey.service vyos-router.service conserver-server.service +StartLimitIntervalSec=0 [Service] Type=forking @@ -11,4 +12,6 @@ ExecStartPre=/usr/bin/bash -c '/usr/bin/systemctl set-environment PORT=$(cli-she ExecStart=-/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -c "/usr/bin/console %I" -P /run/conserver/dropbear.%I.pid -p ${PORT} PIDFile=/run/conserver/dropbear.%I.pid KillMode=process -Restart=on-failure +Restart=always +RestartSec=10 +RuntimeDirectoryPreserve=yes diff --git a/src/systemd/ndppd.service b/src/systemd/ndppd.service new file mode 100644 index 000000000..5790d37f1 --- /dev/null +++ b/src/systemd/ndppd.service @@ -0,0 +1,15 @@ +[Unit] +Description=NDP Proxy Daemon +After=vyos-router.service +ConditionPathExists=/run/ndppd/ndppd.conf +StartLimitIntervalSec=0 + +[Service] +Type=forking +ExecStart=/usr/sbin/ndppd -d -p /run/ndppd/ndppd.pid -c /run/ndppd/ndppd.conf +PIDFile=/run/ndppd/ndppd.pid +Restart=on-failure +RestartSec=20 + +[Install] +WantedBy=multi-user.target diff --git a/src/tests/test_template.py b/src/tests/test_template.py index 544755692..7800d007f 100644 --- a/src/tests/test_template.py +++ b/src/tests/test_template.py @@ -93,3 +93,22 @@ class TestVyOSTemplate(TestCase): self.assertEqual(vyos.template.dec_ip('2001:db8::b/64', '10'), '2001:db8::1') self.assertEqual(vyos.template.dec_ip('2001:db8::f', '5'), '2001:db8::a') + def test_is_network(self): + self.assertFalse(vyos.template.is_ip_network('192.0.2.0')) + self.assertFalse(vyos.template.is_ip_network('192.0.2.1/24')) + self.assertTrue(vyos.template.is_ip_network('192.0.2.0/24')) + + self.assertFalse(vyos.template.is_ip_network('2001:db8::')) + self.assertFalse(vyos.template.is_ip_network('2001:db8::ffff')) + self.assertTrue(vyos.template.is_ip_network('2001:db8::/48')) + self.assertTrue(vyos.template.is_ip_network('2001:db8:1000::/64')) + + def test_is_network(self): + self.assertTrue(vyos.template.compare_netmask('10.0.0.0/8', '20.0.0.0/8')) + self.assertTrue(vyos.template.compare_netmask('10.0.0.0/16', '20.0.0.0/16')) + self.assertFalse(vyos.template.compare_netmask('10.0.0.0/8', '20.0.0.0/16')) + self.assertFalse(vyos.template.compare_netmask('10.0.0.1', '20.0.0.0/16')) + + self.assertTrue(vyos.template.compare_netmask('2001:db8:1000::/48', '2001:db8:2000::/48')) + self.assertTrue(vyos.template.compare_netmask('2001:db8:1000::/64', '2001:db8:2000::/64')) + self.assertFalse(vyos.template.compare_netmask('2001:db8:1000::/48', '2001:db8:2000::/64')) |