diff options
Diffstat (limited to 'src')
44 files changed, 1134 insertions, 540 deletions
diff --git a/src/completion/list_bgp_peer_groups.sh b/src/completion/list_bgp_peer_groups.sh new file mode 100755 index 000000000..4503d608f --- /dev/null +++ b/src/completion/list_bgp_peer_groups.sh @@ -0,0 +1,23 @@ +#!/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/>. + +# Return BGP peer-groups from CLI + +declare -a vals +eval "bgp_as=$(cli-shell-api listNodes protocols bgp)" +eval "vals=($(cli-shell-api listNodes protocols bgp $bgp_as peer-group))" + +echo -n ${vals[@]} +exit 0 diff --git a/src/conf_mode/interfaces-erspan.py b/src/conf_mode/interfaces-erspan.py index 2d65b834c..97ae3cf55 100755 --- a/src/conf_mode/interfaces-erspan.py +++ b/src/conf_mode/interfaces-erspan.py @@ -48,7 +48,7 @@ def get_config(config=None): conf = Config() base = ['interfaces', 'erspan'] erspan = get_interface_dict(conf, base) - + tmp = leaf_node_changed(conf, ['encapsulation']) if tmp: erspan.update({'encapsulation_changed': {}}) @@ -58,30 +58,30 @@ def get_config(config=None): def verify(erspan): if 'deleted' in erspan: return None - + if 'encapsulation' not in erspan: raise ConfigError('Unable to detect the following ERSPAN tunnel encapsulation'\ '{ifname}!'.format(**erspan)) verify_mtu_ipv6(erspan) verify_tunnel(erspan) - + key = dict_search('parameters.ip.key',erspan) if key == None: raise ConfigError('parameters.ip.key is mandatory for ERSPAN tunnel') - + def generate(erspan): return None def apply(erspan): - if 'deleted' in erspan or 'encapsulation_changed' in erspan: - if erspan['ifname'] in interfaces(): - tmp = Interface(erspan['ifname']) - tmp.remove() - if 'deleted' in erspan: - return None - + if 'deleted' in erspan or 'encapsulation_changed' in erspan: + if erspan['ifname'] in interfaces(): + tmp = Interface(erspan['ifname']) + tmp.remove() + if 'deleted' in erspan: + return None + dispatch = { 'erspan': ERSpanIf, 'ip6erspan': ER6SpanIf @@ -90,14 +90,8 @@ def apply(erspan): # We need to re-map the tunnel encapsulation proto to a valid interface class encap = erspan['encapsulation'] klass = dispatch[encap] - - conf = deepcopy(erspan) - - conf.update(klass.get_config()) - - del conf['ifname'] - - erspan_tunnel = klass(erspan['ifname'],**conf) + + erspan_tunnel = klass(**erspan) erspan_tunnel.change_options() erspan_tunnel.update(erspan) diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index e7f0cd6a5..378f400b8 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -30,6 +30,7 @@ from vyos.configverify import verify_mtu from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf +from vyos.ethtool import Ethtool from vyos.ifconfig import EthernetIf from vyos.template import render from vyos.util import call @@ -76,10 +77,37 @@ def verify(ethernet): verify_mirror(ethernet) # verify offloading capabilities - if 'offload' in ethernet and 'rps' in ethernet['offload']: + if dict_search('offload.rps', ethernet) != None: if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'): raise ConfigError('Interface does not suport RPS!') + driver = EthernetIf(ifname).get_driver_name() + # T3342 - Xen driver requires special treatment + if driver == 'vif': + if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None: + raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\ + 'for MTU size larger then 1500 bytes') + + ethtool = Ethtool(ifname) + if 'ring_buffer' in ethernet: + max_rx = ethtool.get_rx_buffer() + if not max_rx: + raise ConfigError('Driver does not support RX ring-buffer configuration!') + + max_tx = ethtool.get_tx_buffer() + if not max_tx: + raise ConfigError('Driver does not support TX ring-buffer configuration!') + + rx = dict_search('ring_buffer.rx', ethernet) + if rx and int(rx) > int(max_rx): + raise ConfigError(f'Driver only supports a maximum RX ring-buffer '\ + f'size of "{max_rx}" bytes!') + + tx = dict_search('ring_buffer.tx', ethernet) + if tx and int(tx) > int(max_tx): + raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\ + f'size of "{max_tx}" bytes!') + # XDP requires multiple TX queues if 'xdp' in ethernet: queues = glob(f'/sys/class/net/{ifname}/queues/tx-*') diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index 979a5612e..2a63b60aa 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -72,18 +72,8 @@ def apply(geneve): g.remove() if 'deleted' not in geneve: - # This is a special type of interface which needs additional parameters - # when created using iproute2. Instead of passing a ton of arguments, - # use a dictionary provided by the interface class which holds all the - # options necessary. - conf = GeneveIf.get_config() - - # Assign GENEVE instance configuration parameters to config dict - conf['vni'] = geneve['vni'] - conf['remote'] = geneve['remote'] - # Finally create the new interface - g = GeneveIf(geneve['ifname'], **conf) + g = GeneveIf(**geneve) g.update(geneve) return None diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 7b3afa058..9b6ddd5aa 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -34,7 +34,6 @@ airbag.enable() k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6'] - def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -65,15 +64,15 @@ def verify(l2tpv3): interface = l2tpv3['ifname'] - for key in ['local_ip', 'remote_ip', 'tunnel_id', 'peer_tunnel_id', + for key in ['source_address', 'remote', 'tunnel_id', 'peer_tunnel_id', 'session_id', 'peer_session_id']: if key not in l2tpv3: tmp = key.replace('_', '-') - raise ConfigError(f'L2TPv3 {tmp} must be configured!') + raise ConfigError(f'Missing mandatory L2TPv3 option: "{tmp}"!') - if not is_addr_assigned(l2tpv3['local_ip']): - raise ConfigError('L2TPv3 local-ip address ' - '"{local_ip}" is not configured!'.format(**l2tpv3)) + if not is_addr_assigned(l2tpv3['source_address']): + raise ConfigError('L2TPv3 source-address address "{source_address}" ' + 'not configured on any interface!'.format(**l2tpv3)) verify_mtu_ipv6(l2tpv3) verify_address(l2tpv3) @@ -83,34 +82,16 @@ def generate(l2tpv3): return None def apply(l2tpv3): - # This is a special type of interface which needs additional parameters - # when created using iproute2. Instead of passing a ton of arguments, - # use a dictionary provided by the interface class which holds all the - # options necessary. - conf = L2TPv3If.get_config() - # Check if L2TPv3 interface already exists if l2tpv3['ifname'] in interfaces(): # L2TPv3 is picky when changing tunnels/sessions, thus we can simply # always delete it first. - conf['session_id'] = l2tpv3['session_id'] - conf['tunnel_id'] = l2tpv3['tunnel_id'] - l = L2TPv3If(l2tpv3['ifname'], **conf) + l = L2TPv3If(**l2tpv3) l.remove() if 'deleted' not in l2tpv3: - conf['peer_tunnel_id'] = l2tpv3['peer_tunnel_id'] - conf['local_port'] = l2tpv3['source_port'] - conf['remote_port'] = l2tpv3['destination_port'] - conf['encapsulation'] = l2tpv3['encapsulation'] - conf['local_address'] = l2tpv3['local_ip'] - conf['remote_address'] = l2tpv3['remote_ip'] - conf['session_id'] = l2tpv3['session_id'] - conf['tunnel_id'] = l2tpv3['tunnel_id'] - conf['peer_session_id'] = l2tpv3['peer_session_id'] - # Finally create the new interface - l = L2TPv3If(l2tpv3['ifname'], **conf) + l = L2TPv3If(**l2tpv3) l.update(l2tpv3) return None diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index bfebed7e4..eab69f36e 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -115,17 +115,9 @@ def apply(macsec): os.unlink(wpa_suppl_conf.format(**macsec)) else: - # This is a special type of interface which needs additional parameters - # when created using iproute2. Instead of passing a ton of arguments, - # use a dictionary provided by the interface class which holds all the - # options necessary. - conf = MACsecIf.get_config() - conf['source_interface'] = macsec['source_interface'] - conf['security_cipher'] = macsec['security']['cipher'] - # It is safe to "re-create" the interface always, there is a sanity # check that the interface will only be create if its non existent - i = MACsecIf(macsec['ifname'], **conf) + i = MACsecIf(**macsec) i.update(macsec) call('systemctl restart wpa_supplicant-macsec@{source_interface}' diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index ee6f05fcd..4afb85526 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -502,10 +502,7 @@ def apply(openvpn): # existed - nevertheless, spawn new OpenVPN process call(f'systemctl start openvpn@{interface}.service') - conf = VTunIf.get_config() - conf['device_type'] = openvpn['device_type'] - - o = VTunIf(interface, **conf) + o = VTunIf(**openvpn) o.update(openvpn) return None diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index ddbef56ac..34a054837 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -75,19 +75,9 @@ def apply(peth): if 'mode_old' in peth: MACVLANIf(peth['ifname']).remove() - # This is a special type of interface which needs additional parameters - # when created using iproute2. Instead of passing a ton of arguments, - # use a dictionary provided by the interface class which holds all the - # options necessary. - conf = MACVLANIf.get_config() - - # Assign MACVLAN instance configuration parameters to config dict - conf['source_interface'] = peth['source_interface'] - conf['mode'] = peth['mode'] - # It is safe to "re-create" the interface always, there is a sanity check # that the interface will only be create if its non existent - p = MACVLANIf(peth['ifname'], **conf) + p = MACVLANIf(**peth) p.update(peth) return None diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 034bd6dd1..cab94a5b0 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -31,16 +31,10 @@ from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vrf from vyos.configverify import verify_tunnel from vyos.ifconfig import Interface -from vyos.ifconfig import GREIf -from vyos.ifconfig import GRETapIf -from vyos.ifconfig import IPIPIf -from vyos.ifconfig import IP6GREIf -from vyos.ifconfig import IPIP6If -from vyos.ifconfig import IP6IP6If -from vyos.ifconfig import SitIf -from vyos.ifconfig import Sit6RDIf +from vyos.ifconfig import TunnelIf from vyos.template import is_ipv4 from vyos.template import is_ipv6 +from vyos.util import get_interface_config from vyos.util import dict_search from vyos import ConfigError from vyos import airbag @@ -79,8 +73,8 @@ def verify(tunnel): return None if 'encapsulation' not in tunnel: - raise ConfigError('Must configure the tunnel encapsulation for '\ - '{ifname}!'.format(**tunnel)) + error = 'Must configure encapsulation for "{ifname}"!' + raise ConfigError(error.format(**tunnel)) verify_mtu_ipv6(tunnel) verify_address(tunnel) @@ -103,68 +97,26 @@ def generate(tunnel): return None def apply(tunnel): - if 'deleted' in tunnel or 'encapsulation_changed' in tunnel: - if tunnel['ifname'] in interfaces(): - tmp = Interface(tunnel['ifname']) + interface = tunnel['ifname'] + # If a gretap tunnel is already existing we can not "simply" change local or + # remote addresses. This returns "Operation not supported" by the Kernel. + # There is no other solution to destroy and recreate the tunnel. + encap = '' + remote = '' + tmp = get_interface_config(interface) + if tmp: + encap = dict_search('linkinfo.info_kind', tmp) + remote = dict_search('linkinfo.info_data.remote', tmp) + + if ('deleted' in tunnel or 'encapsulation_changed' in tunnel or + encap in ['gretap', 'ip6gretap'] or remote in ['any']): + if interface in interfaces(): + tmp = Interface(interface) tmp.remove() if 'deleted' in tunnel: return None - dispatch = { - 'gre': GREIf, - 'gre-bridge': GRETapIf, - 'ipip': IPIPIf, - 'ipip6': IPIP6If, - 'ip6ip6': IP6IP6If, - 'ip6gre': IP6GREIf, - 'sit': SitIf, - } - - # We need to re-map the tunnel encapsulation proto to a valid interface class - encap = tunnel['encapsulation'] - klass = dispatch[encap] - - # This is a special type of interface which needs additional parameters - # when created using iproute2. Instead of passing a ton of arguments, - # use a dictionary provided by the interface class which holds all the - # options necessary. - conf = klass.get_config() - - # Copy/re-assign our dictionary values to values understood by the - # derived _Tunnel classes - mapping = { - # this : get_config() - 'local_ip' : 'local', - 'remote_ip' : 'remote', - 'source_interface' : 'dev', - 'parameters.ip.ttl' : 'ttl', - 'parameters.ip.tos' : 'tos', - 'parameters.ip.key' : 'key', - 'parameters.ipv6.encaplimit' : 'encaplimit' - } - - # Add additional IPv6 options if tunnel is IPv6 aware - if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: - mappingv6 = { - # this : get_config() - 'parameters.ipv6.encaplimit' : 'encaplimit', - 'parameters.ipv6.flowlabel' : 'flowlabel', - 'parameters.ipv6.hoplimit' : 'hoplimit', - 'parameters.ipv6.tclass' : 'flowlabel' - } - mapping.update(mappingv6) - - for our_key, their_key in mapping.items(): - if dict_search(our_key, tunnel) and their_key in conf: - conf[their_key] = dict_search(our_key, tunnel) - - if dict_search('parameters.ip.no_pmtu_discovery', tunnel) != None: - if 'pmtudisc' in conf['raw']: - conf['raw'].remove('pmtudisc') - conf['raw'].append('nopmtudisc') - - tun = klass(tunnel['ifname'], **conf) - tun.change_options() + tun = TunnelIf(**tunnel) tun.update(tunnel) return None diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 9a6d72772..8e6247a30 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -90,19 +90,8 @@ def apply(vxlan): v.remove() if 'deleted' not in vxlan: - # This is a special type of interface which needs additional parameters - # when created using iproute2. Instead of passing a ton of arguments, - # use a dictionary provided by the interface class which holds all the - # options necessary. - conf = VXLANIf.get_config() - - # Assign VXLAN instance configuration parameters to config dict - for tmp in ['vni', 'group', 'source_address', 'source_interface', 'remote', 'port']: - if tmp in vxlan: - conf[tmp] = vxlan[tmp] - # Finally create the new interface - v = VXLANIf(vxlan['ifname'], **conf) + v = VXLANIf(**vxlan) v.update(vxlan) return None diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index b25fcd4e0..7b3de6e8a 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -255,17 +255,8 @@ def apply(wifi): if 'deleted' in wifi: WiFiIf(interface).remove() else: - # This is a special type of interface which needs additional parameters - # when created using iproute2. Instead of passing a ton of arguments, - # use a dictionary provided by the interface class which holds all the - # options necessary. - conf = WiFiIf.get_config() - - # Assign WiFi instance configuration parameters to config dict - conf['phy'] = wifi['physical_device'] - # Finally create the new interface - w = WiFiIf(interface, **conf) + w = WiFiIf(**wifi) w.update(wifi) # Enable/Disable interface - interface is always placed in diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index dc3cd550c..e2bd6417d 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -79,8 +79,10 @@ def get_config(config=None): if not conf.exists(base): nat['helper_functions'] = 'remove' + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER') nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') - nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT','NAT_CONNTRACK') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') nat['deleted'] = '' return nat @@ -90,8 +92,10 @@ def get_config(config=None): nat['helper_functions'] = 'add' # Retrieve current table handler positions + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE') 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') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK') else: nat['helper_functions'] = 'has' @@ -116,10 +120,10 @@ def verify(nat): 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') + addr = dict_search('translation.address', config) + if addr != None: + if addr != 'masquerade' and not is_ipv6(addr): + raise ConfigError(f'Warning: IPv6 address {addr} is not a valid address') prefix = dict_search('source.prefix', config) if prefix != None: diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index d1e551cad..a43eed504 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.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 @@ -17,191 +17,97 @@ import os from sys import exit -from copy import deepcopy from vyos.config import Config +from vyos.configdict import dict_merge from vyos.template import is_ipv6 -from vyos.template import render +from vyos.template import render_to_string from vyos.util import call from vyos.validate import is_ipv6_link_local +from vyos.xml import defaults from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() -config_file = r'/tmp/bfd.frr' - -default_config_data = { - 'new_peers': [], - 'old_peers' : [] -} - -# get configuration for BFD peer from proposed or effective configuration -def get_bfd_peer_config(peer, conf_mode="proposed"): - conf = Config() - conf.set_level('protocols bfd peer {0}'.format(peer)) - - bfd_peer = { - 'remote': peer, - 'shutdown': False, - 'src_if': '', - 'src_addr': '', - 'multiplier': '3', - 'rx_interval': '300', - 'tx_interval': '300', - 'multihop': False, - 'echo_interval': '', - 'echo_mode': False, - } - - # Check if individual peer is disabled - if conf_mode == "effective" and conf.exists_effective('shutdown'): - bfd_peer['shutdown'] = True - if conf_mode == "proposed" and conf.exists('shutdown'): - bfd_peer['shutdown'] = True - - # Check if peer has a local source interface configured - if conf_mode == "effective" and conf.exists_effective('source interface'): - bfd_peer['src_if'] = conf.return_effective_value('source interface') - if conf_mode == "proposed" and conf.exists('source interface'): - bfd_peer['src_if'] = conf.return_value('source interface') - - # Check if peer has a local source address configured - this is mandatory for IPv6 - if conf_mode == "effective" and conf.exists_effective('source address'): - bfd_peer['src_addr'] = conf.return_effective_value('source address') - if conf_mode == "proposed" and conf.exists('source address'): - bfd_peer['src_addr'] = conf.return_value('source address') - - # Tell BFD daemon that we should expect packets with TTL less than 254 - # (because it will take more than one hop) and to listen on the multihop - # port (4784) - if conf_mode == "effective" and conf.exists_effective('multihop'): - bfd_peer['multihop'] = True - if conf_mode == "proposed" and conf.exists('multihop'): - bfd_peer['multihop'] = True - - # Configures the minimum interval that this system is capable of receiving - # control packets. The default value is 300 milliseconds. - if conf_mode == "effective" and conf.exists_effective('interval receive'): - bfd_peer['rx_interval'] = conf.return_effective_value('interval receive') - if conf_mode == "proposed" and conf.exists('interval receive'): - bfd_peer['rx_interval'] = conf.return_value('interval receive') - - # The minimum transmission interval (less jitter) that this system wants - # to use to send BFD control packets. - if conf_mode == "effective" and conf.exists_effective('interval transmit'): - bfd_peer['tx_interval'] = conf.return_effective_value('interval transmit') - if conf_mode == "proposed" and conf.exists('interval transmit'): - bfd_peer['tx_interval'] = conf.return_value('interval transmit') - - # Configures the detection multiplier to determine packet loss. The remote - # transmission interval will be multiplied by this value to determine the - # connection loss detection timer. The default value is 3. - if conf_mode == "effective" and conf.exists_effective('interval multiplier'): - bfd_peer['multiplier'] = conf.return_effective_value('interval multiplier') - if conf_mode == "proposed" and conf.exists('interval multiplier'): - bfd_peer['multiplier'] = conf.return_value('interval multiplier') - - # Configures the minimal echo receive transmission interval that this system is capable of handling - if conf_mode == "effective" and conf.exists_effective('interval echo-interval'): - bfd_peer['echo_interval'] = conf.return_effective_value('interval echo-interval') - if conf_mode == "proposed" and conf.exists('interval echo-interval'): - bfd_peer['echo_interval'] = conf.return_value('interval echo-interval') - - # Enables or disables the echo transmission mode - if conf_mode == "effective" and conf.exists_effective('echo-mode'): - bfd_peer['echo_mode'] = True - if conf_mode == "proposed" and conf.exists('echo-mode'): - bfd_peer['echo_mode'] = True - - return bfd_peer - -def get_config(): - bfd = deepcopy(default_config_data) - conf = Config() - if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')): - return None +def get_config(config=None): + if config: + conf = config else: - conf.set_level('protocols bfd') - - # as we have to use vtysh to talk to FRR we also need to know - # which peers are gone due to a config removal - thus we read in - # all peers (active or to delete) - for peer in conf.list_effective_nodes('peer'): - bfd['old_peers'].append(get_bfd_peer_config(peer, "effective")) - - for peer in conf.list_nodes('peer'): - bfd['new_peers'].append(get_bfd_peer_config(peer)) - - # find deleted peers - set_new_peers = set(conf.list_nodes('peer')) - set_old_peers = set(conf.list_effective_nodes('peer')) - bfd['deleted_peers'] = set_old_peers - set_new_peers + conf = Config() + base = ['protocols', 'bfd'] + bfd = conf.get_config_dict(base, get_first_key=True) + + # Bail out early if configuration tree does not exist + if not conf.exists(base): + return bfd + + # We have gathered the dict representation of the CLI, but there are + # default options which we need to update into the dictionary retrived. + # XXX: T2665: we currently have no nice way for defaults under tag + # nodes, thus we load the defaults "by hand" + default_values = defaults(base + ['peer']) + if 'peer' in bfd: + for peer in bfd['peer']: + bfd['peer'][peer] = dict_merge(default_values, bfd['peer'][peer]) + + if 'profile' in bfd: + for profile in bfd['profile']: + bfd['profile'][profile] = dict_merge(default_values, bfd['profile'][profile]) return bfd def verify(bfd): - if bfd is None: + if not bfd: return None - # some variables to use later - conf = Config() - - for peer in bfd['new_peers']: - # IPv6 link local peers require an explicit local address/interface - if is_ipv6_link_local(peer['remote']): - if not (peer['src_if'] and peer['src_addr']): - raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting') - - # IPv6 peers require an explicit local address - if is_ipv6(peer['remote']): - if not peer['src_addr']: - raise ConfigError('BFD IPv6 peers require explicit local address setting') - - # multihop require source address - if peer['multihop'] and not peer['src_addr']: - raise ConfigError('Multihop require source address') - - # multihop and echo-mode cannot be used together - if peer['multihop'] and peer['echo_mode']: - raise ConfigError('Multihop and echo-mode cannot be used together') - - # multihop doesn't accept interface names - if peer['multihop'] and peer['src_if']: - raise ConfigError('Multihop and source interface cannot be used together') - - # echo interval can be configured only with enabled echo-mode - if peer['echo_interval'] != '' and not peer['echo_mode']: - raise ConfigError('echo-interval can be configured only with enabled echo-mode') - - # check if we deleted peers are not used in configuration - if conf.exists('protocols bgp'): - bgp_as = conf.list_nodes('protocols bgp')[0] - - # check BGP neighbors - for peer in bfd['deleted_peers']: - if conf.exists('protocols bgp {0} neighbor {1} bfd'.format(bgp_as, peer)): - raise ConfigError('Cannot delete BFD peer {0}: it is used in BGP configuration'.format(peer)) - if conf.exists('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer)): - peer_group = conf.return_value('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer)) - if conf.exists('protocols bgp {0} peer-group {1} bfd'.format(bgp_as, peer_group)): - raise ConfigError('Cannot delete BFD peer {0}: it belongs to BGP peer-group {1} with enabled BFD'.format(peer, peer_group)) + if 'peer' in bfd: + for peer, peer_config in bfd['peer'].items(): + # IPv6 link local peers require an explicit local address/interface + if is_ipv6_link_local(peer): + if 'source' not in peer_config or len(peer_config['source'] < 2): + raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting') + + # IPv6 peers require an explicit local address + if is_ipv6(peer): + if 'source' not in peer_config or 'address' not in peer_config['source']: + raise ConfigError('BFD IPv6 peers require explicit local address setting') + + if 'multihop' in peer_config: + # multihop require source address + if 'source' not in peer_config or 'address' not in peer_config['source']: + raise ConfigError('BFD multihop require source address') + + # multihop and echo-mode cannot be used together + if 'echo_mode' in peer_config: + raise ConfigError('Multihop and echo-mode cannot be used together') + + # multihop doesn't accept interface names + if 'source' in peer_config and 'interface' in peer_config['source']: + raise ConfigError('Multihop and source interface cannot be used together') return None def generate(bfd): - if bfd is None: + if not bfd: + bfd['new_frr_config'] = '' return None - render(config_file, 'frr/bfd.frr.tmpl', bfd) - return None + bfd['new_frr_config'] = render_to_string('frr/bfd.frr.tmpl', bfd) def apply(bfd): - if bfd is None: - return None - - call("vtysh -d bfdd -f " + config_file) - if os.path.exists(config_file): - os.remove(config_file) + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + frr_cfg.load_configuration() + frr_cfg.modify_section('^bfd', '') + frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bfd['new_frr_config']) + frr_cfg.commit_configuration() + + # 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 bfd['new_frr_config'] == '': + for a in range(5): + frr_cfg.commit_configuration() return None diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 54352460c..43ca37f9d 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -17,12 +17,15 @@ import os from sys import exit +from sys import argv from vyos.config import Config from vyos.configdict import dict_merge +from vyos.template import is_ip from vyos.template import render_to_string from vyos.util import call from vyos.util import dict_search +from vyos.validate import is_addr_assigned from vyos import ConfigError from vyos import frr from vyos import airbag @@ -35,11 +38,24 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'bgp'] + + vrf = None + if len(argv) > 1: + vrf = argv[1] + + base_path = ['protocols', 'bgp'] + + # eqivalent of the C foo ? 'a' : 'b' statement + base = vrf and ['vrf', 'name', vrf, 'protocols', 'bgp'] or base_path bgp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Bail out early if configuration tree does not exist + # Assign the name of our VRF context. This MUST be done before the return + # statement below, else on deletion we will delete the default instance + # instead of the VRF instance. + if vrf: bgp.update({'vrf' : vrf}) + if not conf.exists(base): + bgp.update({'deleted' : ''}) return bgp # We also need some additional information from the config, @@ -54,15 +70,44 @@ def get_config(config=None): return bgp +def verify_remote_as(peer_config, asn_config): + if 'remote_as' in peer_config: + return peer_config['remote_as'] + + if 'peer_group' in peer_config: + peer_group_name = peer_config['peer_group'] + tmp = dict_search(f'peer_group.{peer_group_name}.remote_as', asn_config) + if tmp: return tmp + + if 'interface' in peer_config: + if 'remote_as' in peer_config['interface']: + return peer_config['interface']['remote_as'] + + if 'peer_group' in peer_config['interface']: + peer_group_name = peer_config['interface']['peer_group'] + tmp = dict_search(f'peer_group.{peer_group_name}.remote_as', asn_config) + if tmp: return tmp + + return None + def verify(bgp): if not bgp: return None - # Check if declared more than one ASN - if len(bgp) > 1: - raise ConfigError('Only one BGP AS number can be defined!') + # FRR bgpd only supports one Autonomous System Number, verify this! + asn = 0 + for key in bgp: + if key.isnumeric(): + asn +=1 + if asn > 1: + raise ConfigError('Only one BGP AS number can be defined!') for asn, asn_config in bgp.items(): + # Workaround for https://phabricator.vyos.net/T1711 + # We also have a vrf, and deleted key now - so we can only veriy "numbers" + if not asn.isnumeric(): + continue + # Common verification for both peer-group and neighbor statements for neighbor in ['neighbor', 'peer_group']: # bail out early if there is no neighbor or peer-group statement @@ -75,22 +120,29 @@ def verify(bgp): # Check if the configure peer-group exists if 'peer_group' in peer_config: peer_group = peer_config['peer_group'] - if peer_group not in asn_config['peer_group']: + if 'peer_group' not in asn_config or peer_group not in asn_config['peer_group']: raise ConfigError(f'Specified peer-group "{peer_group}" for '\ f'neighbor "{neighbor}" does not exist!') - # Some checks can/must only be done on a neighbor and nor a peer-group + # ttl-security and ebgp-multihop can't be used in the same configration + if 'ebgp_multihop' in peer_config and 'ttl_security' in peer_config: + raise ConfigError('You can\'t set both ebgp-multihop and ttl-security hops') + + # Check spaces in the password + if 'password' in peer_config and ' ' in peer_config['password']: + raise ConfigError('You can\'t use spaces in the password') + + # Some checks can/must only be done on a neighbor and not a peer-group if neighbor == 'neighbor': # remote-as must be either set explicitly for the neighbor # or for the entire 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!') + if not verify_remote_as(peer_config, asn_config): + raise ConfigError(f'Neighbor "{peer}" remote-as must be set!') - 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!') + # Only checks for ipv4 and ipv6 neighbors + # Check if neighbor address is assigned as system interface address + if is_ip(peer) and is_addr_assigned(peer): + raise ConfigError(f'Can\'t configure local address as neighbor "{peer}"') for afi in ['ipv4_unicast', 'ipv6_unicast', 'l2vpn_evpn']: # Bail out early if address family is not configured @@ -127,10 +179,10 @@ def verify(bgp): if 'remote_as' in peer_config and asn != peer_config['remote_as']: raise ConfigError('route-reflector-client only supported for iBGP peers') else: - peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', asn_config) - if 'peer_group' in peer_config and peer_group_as != None and peer_group_as != asn: - raise ConfigError('route-reflector-client only supported for iBGP peers') - + if 'peer_group' in peer_config: + peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', asn_config) + if peer_group_as != None and peer_group_as != asn: + raise ConfigError('route-reflector-client only supported for iBGP peers') # Throw an error if a peer group is not configured for allow range for prefix in dict_search('listen.range', asn_config) or []: @@ -146,7 +198,7 @@ def verify(bgp): return None def generate(bgp): - if not bgp: + if not bgp or 'deleted' in bgp: bgp['new_frr_config'] = '' return None @@ -154,6 +206,8 @@ def generate(bgp): # of the config dict asn = list(bgp.keys())[0] bgp[asn]['asn'] = asn + if 'vrf' in bgp: + bgp[asn]['vrf'] = bgp['vrf'] bgp['new_frr_config'] = render_to_string('frr/bgp.frr.tmpl', bgp[asn]) return None @@ -162,7 +216,13 @@ def apply(bgp): # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(f'^router bgp \d+$', '') + + if 'vrf' in bgp: + vrf = bgp['vrf'] + frr_cfg.modify_section(f'^router bgp \d+ vrf {vrf}$', '') + else: + frr_cfg.modify_section('^router bgp \d+$', '') + frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 2ce0ab530..ef2aeda7f 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -17,14 +17,17 @@ import os from sys import exit +from sys import argv from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configdict import node_changed from vyos.configverify import verify_route_maps from vyos.configverify import verify_interface_exists from vyos.template import render_to_string from vyos.util import call from vyos.util import dict_search +from vyos.util import get_interface_config from vyos.xml import defaults from vyos import ConfigError from vyos import frr @@ -38,16 +41,33 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'ospf'] + + vrf = None + if len(argv) > 1: + vrf = argv[1] + + base_path = ['protocols', 'ospf'] + + # eqivalent of the C foo ? 'a' : 'b' statement + base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospf'] or base_path ospf = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # Assign the name of our VRF context. This MUST be done before the return + # statement below, else on deletion we will delete the default instance + # instead of the VRF instance. + if vrf: ospf['vrf'] = vrf + # Bail out early if configuration tree does not exist if not conf.exists(base): + ospf.update({'deleted' : ''}) 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) + # XXX: Note that we can not call defaults(base), as defaults does not work + # on an instance of a tag node. As we use the exact same CLI definition for + # both the non-vrf and vrf version this is absolutely safe! + default_values = defaults(base_path) # We have to cleanup the default dict, as default values could enable features # which are not explicitly enabled on the CLI. Example: default-information @@ -60,7 +80,7 @@ def get_config(config=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']: + for protocol in ['bgp', 'connected', 'isis', '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, @@ -99,6 +119,14 @@ def get_config(config=None): ospf['interface'][interface] = dict_merge(default_values, ospf['interface'][interface]) + # As we no re-use this Python handler for both VRF and non VRF instances for + # OSPF we need to find out if any interfaces changed so properly adjust + # the FRR configuration and not by acctident change interfaces from a + # different VRF. + interfaces_removed = node_changed(conf, base + ['interface']) + if interfaces_removed: + ospf['interface_removed'] = list(interfaces_removed) + # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify() base = ['policy'] @@ -121,12 +149,22 @@ def verify(ospf): # time. FRR will only activate the last option set via CLI. if {'hello_multiplier', 'dead_interval'} <= set(ospf['interface'][interface]): raise ConfigError(f'Can not use hello-multiplier and dead-interval ' \ - f'concurrently for "{interface}"!') + f'concurrently for {interface}!') + + if 'vrf' in ospf: + # If interface specific options are set, we must ensure that the + # interface is bound to our requesting VRF. Due to the VyOS/Vyatta + # priorities the interface is bound to the VRF after creation of + # the VRF itself, and before any routing protocol is configured. + vrf = ospf['vrf'] + tmp = get_interface_config(interface) + if 'master' not in tmp or tmp['master'] != vrf: + raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!') return None def generate(ospf): - if not ospf: + if not ospf or 'deleted' in ospf: ospf['new_frr_config'] = '' return None @@ -137,8 +175,19 @@ 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(r'interface \S+', '') - frr_cfg.modify_section('^router ospf$', '') + + if 'vrf' in ospf: + vrf = ospf['vrf'] + frr_cfg.modify_section(f'^router ospf vrf {vrf}$', '') + else: + frr_cfg.modify_section('^router ospf$', '') + + for key in ['interface', 'interface_removed']: + if key not in ospf: + continue + for interface in ospf[key]: + frr_cfg.modify_section(f'^interface {interface}$', '') + frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index 6c3aaf426..6f068b196 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -23,6 +23,7 @@ from vyos.configdict import dict_merge from vyos.configverify import verify_route_maps from vyos.template import render_to_string from vyos.util import call +from vyos.ifconfig import Interface from vyos.xml import defaults from vyos import ConfigError from vyos import frr @@ -57,6 +58,14 @@ def verify(ospfv3): return None verify_route_maps(ospfv3) + + if 'interface' in ospfv3: + for ifname, if_config in ospfv3['interface'].items(): + if 'ifmtu' in if_config: + mtu = Interface(ifname).get_mtu() + if int(if_config['ifmtu']) > int(mtu): + raise ConfigError(f'OSPFv3 ifmtu cannot go beyond physical MTU of "{mtu}"') + return None def generate(ospfv3): @@ -71,7 +80,8 @@ 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.modify_section(r'^interface \S+', '') + frr_cfg.modify_section('^router ospf6$', '') frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospfv3['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 5d101b33e..51b4acfc8 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -17,6 +17,7 @@ import os from sys import exit +from sys import argv from vyos.config import Config from vyos.template import render_to_string @@ -34,8 +35,19 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'static'] + + vrf = None + if len(argv) > 1: + vrf = argv[1] + + base_path = ['protocols', 'static'] + # eqivalent of the C foo ? 'a' : 'b' statement + base = vrf and ['vrf', 'name', vrf, 'protocols', 'static'] or base_path static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # Assign the name of our VRF context + if vrf: static['vrf'] = vrf + return static def verify(static): @@ -50,8 +62,14 @@ def apply(static): # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(r'^ip route .*', '') - frr_cfg.modify_section(r'^ipv6 route .*', '') + + if 'vrf' in static: + vrf = static['vrf'] + frr_cfg.modify_section(f'^vrf {vrf}$', '') + else: + frr_cfg.modify_section(r'^ip route .*', '') + frr_cfg.modify_section(r'^ipv6 route .*', '') + frr_cfg.add_before(r'(interface .*|line vty)', static['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) diff --git a/src/conf_mode/protocols_vrf.py b/src/conf_mode/protocols_vrf.py deleted file mode 100755 index 227e7d5e1..000000000 --- a/src/conf_mode/protocols_vrf.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/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_to_string -from vyos.util import call -from vyos import ConfigError -from vyos import frr -from vyos import airbag -airbag.enable() - -frr_daemon = 'staticd' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['protocols', 'vrf'] - vrf = conf.get_config_dict(base, key_mangling=('-', '_')) - return vrf - -def verify(vrf): - - return None - -def generate(vrf): - vrf['new_frr_config'] = render_to_string('frr/vrf.frr.tmpl', vrf) - return None - -def apply(vrf): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(r'vrf \S+', '') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', vrf['new_frr_config']) - 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 vrf['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/service_console-server.py b/src/conf_mode/service_console-server.py index 0e5fc75b0..51050e702 100755 --- a/src/conf_mode/service_console-server.py +++ b/src/conf_mode/service_console-server.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,7 @@ import os from sys import exit +from psutil import process_iter from vyos.config import Config from vyos.configdict import dict_merge @@ -25,7 +26,8 @@ from vyos.util import call from vyos.xml import defaults from vyos import ConfigError -config_file = r'/run/conserver/conserver.cf' +config_file = '/run/conserver/conserver.cf' +dropbear_systemd_file = '/etc/systemd/system/dropbear@{port}.service.d/override.conf' def get_config(config=None): if config: @@ -59,14 +61,19 @@ def verify(proxy): if not proxy: return None + processes = process_iter(['name', 'cmdline']) if 'device' in proxy: - for device in proxy['device']: - if 'speed' not in proxy['device'][device]: - raise ConfigError(f'Serial port speed must be defined for "{device}"!') + for device, device_config in proxy['device'].items(): + for process in processes: + if 'agetty' in process.name() and device in process.cmdline(): + raise ConfigError(f'Port "{device}" already provides a '\ + 'console used by "system console"!') + + if 'speed' not in device_config: + raise ConfigError(f'Port "{device}" requires speed to be set!') - if 'ssh' in proxy['device'][device]: - if 'port' not in proxy['device'][device]['ssh']: - raise ConfigError(f'SSH port must be defined for "{device}"!') + if 'ssh' in device_config and 'port' not in device_config['ssh']: + raise ConfigError(f'Port "{device}" requires SSH port to be set!') return None @@ -75,9 +82,22 @@ def generate(proxy): return None render(config_file, 'conserver/conserver.conf.tmpl', proxy) + if 'device' in proxy: + for device, device_config in proxy['device'].items(): + if 'ssh' not in device_config: + continue + + tmp = { + 'device' : device, + 'port' : device_config['ssh']['port'], + } + render(dropbear_systemd_file.format(**tmp), + 'conserver/dropbear@.service.tmpl', tmp) + return None def apply(proxy): + call('systemctl daemon-reload') call('systemctl stop dropbear@*.service conserver-server.service') if not proxy: @@ -88,10 +108,11 @@ def apply(proxy): call('systemctl restart conserver-server.service') if 'device' in proxy: - for device in proxy['device']: - if 'ssh' in proxy['device'][device]: - port = proxy['device'][device]['ssh']['port'] - call(f'systemctl restart dropbear@{device}.service') + for device, device_config in proxy['device'].items(): + if 'ssh' not in device_config: + continue + port = device_config['ssh']['port'] + call(f'systemctl restart dropbear@{port}.service') return None diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py index 8dfae348a..cbbd2e0bc 100755 --- a/src/conf_mode/service_webproxy.py +++ b/src/conf_mode/service_webproxy.py @@ -123,9 +123,6 @@ def verify(proxy): ldap_auth = dict_search('authentication.method', proxy) == 'ldap' for address, config in proxy['listen_address'].items(): - if not is_addr_assigned(address): - raise ConfigError( - f'listen-address "{address}" not assigned on any interface!') if ldap_auth and 'disable_transparent' not in config: raise ConfigError('Authentication can not be configured when ' \ 'proxy is in transparent mode') diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index b17818797..33a546bd3 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -17,9 +17,8 @@ import os import re -from fileinput import input as replace_in_file from vyos.config import Config -from vyos.util import call +from vyos.util import call, read_file, write_file from vyos.template import render from vyos import ConfigError, airbag airbag.enable() @@ -98,15 +97,27 @@ def generate(console): if not os.path.isfile(grub_config): return None - # stdin/stdout are redirected in replace_in_file(), thus print() is fine + lines = read_file(grub_config).split('\n') + p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$') - for line in replace_in_file(grub_config, inplace=True): + write = False + newlines = [] + for line in lines: if line.startswith('serial --unit'): - line = f'serial --unit=0 --speed={speed}\n' + newline = f'serial --unit=0 --speed={speed}' elif p.match(line): - line = '{},{}{}\n'.format(p.search(line)[1], speed, p.search(line)[2]) + newline = '{},{}{}'.format(p.search(line)[1], speed, p.search(line)[2]) + else: + newline = line + + if newline != line: + write = True + + newlines.append(newline) + newlines.append('') - print(line, end='') + if write: + write_file(grub_config, '\n'.join(newlines)) return None diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index 4510dd3e7..680a80859 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -75,6 +75,7 @@ def get_config(config=None): group["backup_script"] = config.return_value("transition-script backup") group["fault_script"] = config.return_value("transition-script fault") group["stop_script"] = config.return_value("transition-script stop") + group["script_mode_force"] = config.exists("transition-script mode-force") if config.exists("no-preempt"): group["preempt"] = False @@ -183,6 +184,11 @@ def verify(data): if isinstance(pa, IPv4Address): raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"])) + # Warn the user about the deprecated mode-force option + if group['script_mode_force']: + print("""Warning: "transition-script mode-force" VRRP option is deprecated and will be removed in VyOS 1.4.""") + print("""It's no longer necessary, so you can safely remove it from your config now.""") + # Disallow same VRID on multiple interfaces _groups = sorted(vrrp_groups, key=(lambda x: x["interface"])) count = len(_groups) - 1 diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper index d1161e704..fc035766b 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper +++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper @@ -1,5 +1,8 @@ # redefine ip command to use FRR when it is available +# default route distance +IF_METRIC=${IF_METRIC:-210} + # get status of FRR function frr_alive () { /usr/lib/frr/watchfrr.sh all_status @@ -15,11 +18,12 @@ function frr_alive () { # convert ip route command to vtysh function iptovtysh () { # prepare variables for vtysh command - local VTYSH_DISTANCE="210" - local VTYSH_TAG="210" + local VTYSH_ACTION=$3 local VTYSH_NETADDR="" local VTYSH_GATEWAY="" local VTYSH_DEV="" + local VTYSH_TAG="210" + local VTYSH_DISTANCE="" # convert default route to 0.0.0.0/0 if [ "$4" == "default" ] ; then VTYSH_NETADDR="0.0.0.0/0" @@ -30,26 +34,32 @@ function iptovtysh () { if [[ ! $VTYSH_NETADDR =~ ^.*/[[:digit:]]+$ ]] ; then VTYSH_NETADDR="$VTYSH_NETADDR/32" fi + shift 4 # get gateway address - if [ "$5" == "via" ] ; then - VTYSH_GATEWAY=$6 + if [ "$1" == "via" ] ; then + VTYSH_GATEWAY=$2 + shift 2 fi # get device name - if [ "$5" == "dev" ]; then - VTYSH_DEV=$6 - elif [ "$7" == "dev" ]; then - VTYSH_DEV=$8 + if [ "$1" == "dev" ]; then + VTYSH_DEV=$2 + shift 2 + fi + # get distance + if [ "$1" == "metric" ]; then + VTYSH_DISTANCE=$2 + shift 2 fi # Add route to VRF routing table - local VTYSH_VRF_NAME=$(basename /sys/class/net/$VTYSH_DEV/upper_* | sed -e 's/upper_//') - if [ -n $VTYSH_VRF_NAME ]; then + local VTYSH_VRF_NAME=$(/usr/sbin/ip link show dev $VTYSH_DEV | sed -nre '1s/.* master ([^ ]*) .*/\1/p') + if /usr/sbin/ip -d link show dev $VTYSH_DEV | grep -q "vrf_slave"; then VTYSH_VRF="vrf $VTYSH_VRF_NAME" fi VTYSH_CMD="ip route $VTYSH_NETADDR $VTYSH_GATEWAY $VTYSH_DEV tag $VTYSH_TAG $VTYSH_DISTANCE $VTYSH_VRF" # delete route if the command is "del" - if [ "$3" == "del" ] ; then + if [ "$VTYSH_ACTION" == "del" ] ; then VTYSH_CMD="no $VTYSH_CMD" fi logmsg info "Converted vtysh command: \"$VTYSH_CMD\"" diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index b768e1ae5..edb7c7b27 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -13,6 +13,8 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then $hostsd_client --delete-name-servers --tag "dhcp-${interface}" hostsd_changes=y + if_metric="$IF_METRIC" + # try to delete default ip route for router in $old_routers; do # check if we are bound to a VRF @@ -21,8 +23,10 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then vrf="vrf $vrf_name" fi - logmsg info "Deleting default route: via $router dev ${interface} ${vrf}" - ip -4 route del default via $router dev ${interface} ${vrf} + logmsg info "Deleting default route: via $router dev ${interface} ${if_metric:+metric $if_metric} ${vrf}" + ip -4 route del default via $router dev ${interface} ${if_metric:+metric $if_metric} ${vrf} + + if_metric=$((if_metric+1)) done # delete rfc3442 routes diff --git a/src/migration-scripts/interfaces/19-to-20 b/src/migration-scripts/interfaces/19-to-20 new file mode 100755 index 000000000..e96663e54 --- /dev/null +++ b/src/migration-scripts/interfaces/19-to-20 @@ -0,0 +1,61 @@ +#!/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 sys import argv +from sys import exit +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + 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) + + for type in ['tunnel', 'l2tpv3']: + base = ['interfaces', type] + if not config.exists(base): + # Nothing to do + continue + + for interface in config.list_nodes(base): + # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap + encap_path = base + [interface, 'encapsulation'] + if type == 'tunnel' and config.exists(encap_path): + tmp = config.return_value(encap_path) + if tmp == 'gre-bridge': + config.set(encap_path, value='gretap') + + # Migrate "interface tunnel|l2tpv3 <interface> local-ip" to source-address + # Migrate "interface tunnel|l2tpv3 <interface> remote-ip" to remote + local_ip_path = base + [interface, 'local-ip'] + if config.exists(local_ip_path): + config.rename(local_ip_path, 'source-address') + + remote_ip_path = base + [interface, 'remote-ip'] + if config.exists(remote_ip_path): + config.rename(remote_ip_path, 'remote') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/nat/4-to-5 b/src/migration-scripts/nat/4-to-5 index dda191719..b791996e2 100755 --- a/src/migration-scripts/nat/4-to-5 +++ b/src/migration-scripts/nat/4-to-5 @@ -36,9 +36,15 @@ if not config.exists(['nat']): exit(0) else: for direction in ['source', 'destination']: + # If a node doesn't exist, we obviously have nothing to do. if not config.exists(['nat', direction]): continue + # However, we also need to handle the case when a 'source' or 'destination' sub-node does exist, + # but there are no rules under it. + if not config.list_nodes(['nat', direction]): + continue + for rule in config.list_nodes(['nat', direction, 'rule']): base = ['nat', direction, 'rule', rule] diff --git a/src/migration-scripts/nat66/0-to-1 b/src/migration-scripts/nat66/0-to-1 index 74d64c07b..83b421926 100755 --- a/src/migration-scripts/nat66/0-to-1 +++ b/src/migration-scripts/nat66/0-to-1 @@ -49,7 +49,7 @@ def merge_npt(config,base,rule): if config.exists(base + ['translation','prefix']): tmp = config.return_value(base + ['translation','prefix']) - config.set(merge_base + ['translation','prefix'],value=tmp) + config.set(merge_base + ['translation','address'],value=tmp) if not config.exists(['nat', 'nptv6']): # Nothing to do diff --git a/src/migration-scripts/quagga/6-to-7 b/src/migration-scripts/quagga/6-to-7 index f7aca0d2b..25cf5eebd 100755 --- a/src/migration-scripts/quagga/6-to-7 +++ b/src/migration-scripts/quagga/6-to-7 @@ -17,14 +17,17 @@ # - T3037, BGP address-family ipv6-unicast capability dynamic does not exist in # FRR, there is only a base, per neighbor dynamic capability, migrate config -import sys +from sys import argv +from sys import exit from vyos.configtree import ConfigTree +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 -if (len(sys.argv) < 2): +if (len(argv) < 2): print("Must specify file name!") - sys.exit(1) + exit(1) -file_name = sys.argv[1] +file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() @@ -34,7 +37,7 @@ config = ConfigTree(config_file) if not config.exists(base): # Nothing to do - sys.exit(0) + exit(0) # Check if BGP is actually configured and obtain the ASN asn_list = config.list_nodes(base) @@ -55,30 +58,59 @@ if asn_list: config.delete(send_comm_path) cap_dynamic = False + peer_group = None for afi in ['ipv4-unicast', 'ipv6-unicast']: - afi_path = bgp_base + [neighbor_type, neighbor, 'address-family', afi, 'capability', 'dynamic'] - if config.exists(afi_path): + afi_path = bgp_base + [neighbor_type, neighbor, 'address-family', afi] + # Exit loop early if AFI does not exist + if not config.exists(afi_path): + continue + + cap_path = afi_path + ['capability', 'dynamic'] + if config.exists(cap_path): cap_dynamic = True - config.delete(afi_path) + config.delete(cap_path) + + # We have now successfully migrated the address-family + # specific dynamic capability to the neighbor/peer-group + # level. If this has been the only option under the + # address-family nodes, we can clean them up by checking if + # no other nodes are left under that tree and if so, delete + # the parent. + # + # We walk from the most inner node to the most outer one. + cleanup = -1 + while len(config.list_nodes(cap_path[:cleanup])) == 0: + config.delete(cap_path[:cleanup]) + cleanup -= 1 + + peer_group_path = afi_path + ['peer-group'] + if config.exists(peer_group_path): + if ((is_ipv4(neighbor) and afi == 'ipv4-unicast') or + (is_ipv6(neighbor) and afi == 'ipv6-unicast')): + peer_group = config.return_value(peer_group_path) + + config.delete(peer_group_path) - # We have now successfully migrated the address-family specific - # dynamic capability to the neighbor/peer-group level. If this - # has been the only option under the address-family nodes, we - # can clean them up by checking if no other nodes are left under - # that tree and if so, delete the parent. + # We have now successfully migrated the address-family + # specific peer-group to the neighbor level. If this has + # been the only option under the address-family nodes, we + # can clean them up by checking if no other nodes are left + # under that tree and if so, delete the parent. # # We walk from the most inner node to the most outer one. cleanup = -1 - while len(config.list_nodes(afi_path[:cleanup])) == 0: - config.delete(afi_path[:cleanup]) + while len(config.list_nodes(peer_group_path[:cleanup])) == 0: + config.delete(peer_group_path[:cleanup]) cleanup -= 1 if cap_dynamic: config.set(bgp_base + [neighbor_type, neighbor, 'capability', 'dynamic']) + if peer_group: + config.set(bgp_base + [neighbor_type, neighbor, 'peer-group'], value=peer_group) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) - sys.exit(1) + exit(1) diff --git a/src/migration-scripts/quagga/8-to-9 b/src/migration-scripts/quagga/8-to-9 new file mode 100755 index 000000000..15c44924f --- /dev/null +++ b/src/migration-scripts/quagga/8-to-9 @@ -0,0 +1,61 @@ +#!/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/>. + +# - T3391: Migrate "maximum-paths" setting from "protocols bgp asn maximum-paths" +# under the IPv4 address-family tree. Reason is we currently have no way in +# configuring this for IPv6 address-family. This mimics the FRR configuration. + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + +if (len(argv) < 2): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['protocols', 'bgp'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# Check if BGP is actually configured and obtain the ASN +asn_list = config.list_nodes(base) +if asn_list: + # There's always just one BGP node, if any + bgp_base = base + [asn_list[0]] + + maximum_paths = bgp_base + ['maximum-paths'] + if config.exists(maximum_paths): + for bgp_type in ['ebgp', 'ibgp']: + if config.exists(maximum_paths + [bgp_type]): + new_base = bgp_base + ['address-family', 'ipv4-unicast', 'maximum-paths'] + config.set(new_base) + config.copy(maximum_paths + [bgp_type], new_base + [bgp_type]) + config.delete(maximum_paths) + +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/vrf/1-to-2 b/src/migration-scripts/vrf/1-to-2 new file mode 100755 index 000000000..20128e957 --- /dev/null +++ b/src/migration-scripts/vrf/1-to-2 @@ -0,0 +1,61 @@ +#!/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/>. + +# - T3344: migrate routing options from "protocols vrf" to "vrf <name> protocols" + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + +if (len(argv) < 2): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['protocols', 'vrf'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +vrf_base = ['vrf', 'name'] +config.set(vrf_base) +config.set_tag(vrf_base) + +# Copy all existing static routes to the new base node under "vrf name <name> protocols static" +for vrf in config.list_nodes(base): + static_base = base + [vrf, 'static'] + if not config.exists(static_base): + continue + + new_static_base = vrf_base + [vrf, 'protocols'] + config.set(new_static_base) + config.copy(static_base, new_static_base + ['static']) + +# Now delete the old configuration +config.delete(base) + +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/ppp-server-ctrl.py b/src/op_mode/ppp-server-ctrl.py index 171107b4a..8a4e846b2 100755 --- a/src/op_mode/ppp-server-ctrl.py +++ b/src/op_mode/ppp-server-ctrl.py @@ -59,7 +59,10 @@ def main(): output, err = popen(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][args.proto]) + args.action + ses_pattern, stderr=DEVNULL, decode='utf-8') if not err: - print(output) + try: + print(output) + except BrokenPipeError: + sys.exit(0) else: print("{} server is not running".format(args.proto)) diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index 256c86d2a..39e5dc7ac 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -30,7 +30,7 @@ from vyos.util import cmd # interfaces = Sections.reserved() -interfaces = ['eno', 'ens', 'enp', 'enx', 'eth', 'vmnet', 'lo', 'tun', 'wan', 'pppoe', 'pppoa', 'adsl'] +interfaces = ['eno', 'ens', 'enp', 'enx', 'eth', 'vmnet', 'lo', 'tun', 'wan', 'pppoe'] glob_ifnames = '/sys/class/net/({})*'.format('|'.join(interfaces)) @@ -177,7 +177,7 @@ def run_show_intf(ifnames, iftypes, vif, vrrp): out = cmd(f'ip addr show {interface.ifname}') out = re.sub(f'^\d+:\s+','',out) - if re.search("link/tunnel6", out): + if re.search('link/tunnel6', out): tunnel = cmd(f'ip -6 tun show {interface.ifname}') # tun0: ip/ipv6 remote ::2 local ::1 encaplimit 4 hoplimit 64 tclass inherit flowlabel inherit (flowinfo 0x00000000) tunnel = re.sub('.*encap', 'encap', tunnel) diff --git a/src/op_mode/show_nat66_rules.py b/src/op_mode/show_nat66_rules.py new file mode 100755 index 000000000..cbab2d03b --- /dev/null +++ b/src/op_mode/show_nat66_rules.py @@ -0,0 +1,74 @@ +#!/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 jmespath +import json + +from argparse import ArgumentParser +from jinja2 import Template +from sys import exit +from vyos.util import cmd +from vyos.util import dict_search + +parser = ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true") +group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true") +args = parser.parse_args() + +if args.source or args.destination: + tmp = cmd('sudo nft -j list table ip6 nat') + tmp = json.loads(tmp) + + format_nat66_rule = '%-10s %-50s %-50s %-10s' + print(format_nat66_rule % ("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface")) + print(format_nat66_rule % ("----", "------" 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)): + data = data_json[idx] + comment = data['comment'] + chain = data['chain'] + if not (args.source and chain == 'POSTROUTING') or (not args.source and chain == 'PREROUTING'): + exit(0) + interface = dict_search('match.right', data['expr'][0]) + srcdest = dict_search('match.right.prefix.addr', data['expr'][2]) + if srcdest: + addr_tmp = dict_search('match.right.prefix.len', data['expr'][2]) + if addr_tmp: + srcdest = srcdest + '/' + str(addr_tmp) + else: + srcdest = dict_search('match.right', data['expr'][2]) + tran_addr = dict_search('snat.addr.prefix.addr' if args.source else 'dnat.addr.prefix.addr', data['expr'][3]) + if tran_addr: + addr_tmp = dict_search('snat.addr.prefix.len' if args.source else 'dnat.addr.prefix.len', data['expr'][3]) + if addr_tmp: + srcdest = srcdest + '/' + str(addr_tmp) + else: + if 'masquerade' in data['expr'][3]: + tran_addr = 'masquerade' + elif 'log' in data['expr'][3]: + continue + else: + tran_addr = dict_search('snat.addr' if args.source else 'dnat.addr', data['expr'][3]) + + print(format_nat66_rule % (comment, srcdest, tran_addr, interface)) + + exit(0) +else: + parser.print_help() + exit(1) + diff --git a/src/op_mode/show_nat66_statistics.py b/src/op_mode/show_nat66_statistics.py new file mode 100755 index 000000000..0f0b05978 --- /dev/null +++ b/src/op_mode/show_nat66_statistics.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 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 + +from argparse import ArgumentParser +from jinja2 import Template +from sys import exit +from vyos.util import cmd + +OUT_TMPL_SRC=""" +rule pkts bytes interface +---- ---- ----- --------- +{% for r in output %} +{% if r.comment %} +{% set packets = r.counter.packets %} +{% set bytes = r.counter.bytes %} +{% set interface = r.interface %} +{# remove rule comment prefix #} +{% set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') | replace(' tcp_udp', '') %} +{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }} +{% endif %} +{% endfor %} +""" + +parser = ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true") +group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true") +args = parser.parse_args() + +if args.source or args.destination: + tmp = cmd('sudo nft -j list table ip6 nat') + tmp = json.loads(tmp) + + source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" + destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" + data = { + 'output' : jmespath.search(source if args.source else destination, tmp), + 'direction' : 'source' if args.source else 'destination' + } + + tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True) + print(tmpl.render(data)) + exit(0) +else: + parser.print_help() + exit(1) + diff --git a/src/op_mode/show_nat66_translations.py b/src/op_mode/show_nat66_translations.py new file mode 100755 index 000000000..045d64065 --- /dev/null +++ b/src/op_mode/show_nat66_translations.py @@ -0,0 +1,204 @@ +#!/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/>. + +''' +show nat translations +''' + +import os +import sys +import ipaddress +import argparse +import xmltodict + +from vyos.util import popen +from vyos.util import DEVNULL + +conntrack = '/usr/sbin/conntrack' + +verbose_format = "%-20s %-18s %-20s %-18s" +normal_format = "%-20s %-20s %-4s %-8s %s" + + +def headers(verbose, pipe): + if verbose: + return verbose_format % ('Pre-NAT src', 'Pre-NAT dst', 'Post-NAT src', 'Post-NAT dst') + return normal_format % ('Pre-NAT', 'Post-NAT', 'Prot', 'Timeout', 'Type' if pipe else '') + + +def command(srcdest, proto, ipaddr): + command = f'{conntrack} -o xml -L -f ipv6' + + if proto: + command += f' -p {proto}' + + if srcdest == 'source': + command += ' -n' + if ipaddr: + command += f' --orig-src {ipaddr}' + if srcdest == 'destination': + command += ' -g' + if ipaddr: + command += f' --orig-dst {ipaddr}' + + return command + + +def run(command): + xml, code = popen(command,stderr=DEVNULL) + if code: + sys.exit('conntrack failed') + return xml + + +def content(xmlfile): + xml = '' + with open(xmlfile,'r') as r: + xml += r.read() + return xml + + +def pipe(): + xml = '' + while True: + line = sys.stdin.readline() + xml += line + if '</conntrack>' in line: + break + + sys.stdin = open('/dev/tty') + return xml + + +def process(data, stats, protocol, pipe, verbose, flowtype=''): + if not data: + return + + parsed = xmltodict.parse(data) + + print(headers(verbose, pipe)) + + # to help the linter to detect typos + ORIGINAL = 'original' + REPLY = 'reply' + INDEPENDANT = 'independent' + SPORT = 'sport' + DPORT = 'dport' + SRC = 'src' + DST = 'dst' + + for rule in parsed['conntrack']['flow']: + src, dst, sport, dport, proto = {}, {}, {}, {}, {} + packet_count, byte_count = {}, {} + timeout, use = 0, 0 + + rule_type = rule.get('type', '') + + for meta in rule['meta']: + # print(meta) + direction = meta['@direction'] + + if direction in (ORIGINAL, REPLY): + if 'layer3' in meta: + l3 = meta['layer3'] + src[direction] = l3[SRC] + dst[direction] = l3[DST] + + if 'layer4' in meta: + l4 = meta['layer4'] + sp = l4.get(SPORT, '') + dp = l4.get(DPORT, '') + if sp: + sport[direction] = sp + if dp: + dport[direction] = dp + proto[direction] = l4.get('@protoname','') + + if stats and 'counters' in meta: + packet_count[direction] = meta['packets'] + byte_count[direction] = meta['bytes'] + continue + + if direction == INDEPENDANT: + timeout = meta['timeout'] + use = meta['use'] + continue + + in_src = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if ORIGINAL in sport else src[ORIGINAL] + in_dst = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if ORIGINAL in dport else dst[ORIGINAL] + + # inverted the the perl code !!? + out_dst = '%s:%s' % (dst[REPLY], dport[REPLY]) if REPLY in dport else dst[REPLY] + out_src = '%s:%s' % (src[REPLY], sport[REPLY]) if REPLY in sport else src[REPLY] + + if flowtype == 'source': + v = ORIGINAL in sport and REPLY in dport + f = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if v else src[ORIGINAL] + t = '%s:%s' % (dst[REPLY], dport[REPLY]) if v else dst[REPLY] + else: + v = ORIGINAL in dport and REPLY in sport + f = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if v else dst[ORIGINAL] + t = '%s:%s' % (src[REPLY], sport[REPLY]) if v else src[REPLY] + + # Thomas: I do not believe proto should be an option + p = proto.get('original', '') + if protocol and p != protocol: + continue + + if verbose: + msg = verbose_format % (in_src, in_dst, out_dst, out_src) + p = f'{p}: ' if p else '' + msg += f'\n {p}{f} ==> {t}' + msg += f' timeout: {timeout}' if timeout else '' + msg += f' use: {use} ' if use else '' + msg += f' type: {rule_type}' if rule_type else '' + print(msg) + else: + print(normal_format % (f, t, p, timeout, rule_type if rule_type else '')) + + if stats: + for direction in ('original', 'reply'): + if direction in packet_count: + print(' %-8s: packets %s, bytes %s' % direction, packet_count[direction], byte_count[direction]) + + +def main(): + parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__) + parser.add_argument('--verbose', help='provide more details about the flows', action='store_true') + parser.add_argument('--proto', help='filter by protocol', default='', type=str) + parser.add_argument('--file', help='read the conntrack xml from a file', type=str) + parser.add_argument('--stats', help='add usage statistics', action='store_true') + parser.add_argument('--type', help='NAT type (source, destination)', required=True, type=str) + parser.add_argument('--ipaddr', help='source ip address to filter on', type=ipaddress.ip_address) + parser.add_argument('--pipe', help='read conntrack xml data from stdin', action='store_true') + + arg = parser.parse_args() + + if arg.type not in ('source', 'destination'): + sys.exit('Unknown NAT type!') + + if arg.pipe: + process(pipe(), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) + elif arg.file: + process(content(arg.file), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) + else: + try: + process(run(command(arg.type, arg.proto, arg.ipaddr)), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) + except: + pass + +if __name__ == '__main__': + main() diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py new file mode 100755 index 000000000..0ddb7ddd4 --- /dev/null +++ b/src/op_mode/show_nat_rules.py @@ -0,0 +1,74 @@ +#!/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 jmespath +import json + +from argparse import ArgumentParser +from jinja2 import Template +from sys import exit +from vyos.util import cmd +from vyos.util import dict_search + +parser = ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true") +group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true") +args = parser.parse_args() + +if args.source or args.destination: + tmp = cmd('sudo nft -j list table ip nat') + tmp = json.loads(tmp) + + format_nat66_rule = '%-10s %-50s %-50s %-10s' + print(format_nat66_rule % ("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface")) + print(format_nat66_rule % ("----", "------" 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)): + data = data_json[idx] + comment = data['comment'] + chain = data['chain'] + if not (args.source and chain == 'POSTROUTING') or (not args.source and chain == 'PREROUTING'): + exit(0) + interface = dict_search('match.right', data['expr'][0]) + srcdest = dict_search('match.right.prefix.addr', data['expr'][1]) + if srcdest: + addr_tmp = dict_search('match.right.prefix.len', data['expr'][1]) + if addr_tmp: + srcdest = srcdest + '/' + str(addr_tmp) + else: + srcdest = dict_search('match.right', data['expr'][1]) + tran_addr = dict_search('snat.addr.prefix.addr' if args.source else 'dnat.addr.prefix.addr', data['expr'][3]) + if tran_addr: + addr_tmp = dict_search('snat.addr.prefix.len' if args.source else 'dnat.addr.prefix.len', data['expr'][3]) + if addr_tmp: + srcdest = srcdest + '/' + str(addr_tmp) + else: + if 'masquerade' in data['expr'][3]: + tran_addr = 'masquerade' + elif 'log' in data['expr'][3]: + continue + else: + tran_addr = dict_search('snat.addr' if args.source else 'dnat.addr', data['expr'][3]) + + print(format_nat66_rule % (comment, srcdest, tran_addr, interface)) + + exit(0) +else: + parser.print_help() + exit(1) + diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py index 482993d06..c568c8305 100755 --- a/src/op_mode/show_nat_statistics.py +++ b/src/op_mode/show_nat_statistics.py @@ -44,7 +44,7 @@ group.add_argument("--destination", help="Show statistics for configured destina args = parser.parse_args() if args.source or args.destination: - tmp = cmd('sudo nft -j list table nat') + tmp = cmd('sudo nft -j list table ip nat') tmp = json.loads(tmp) source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" diff --git a/src/op_mode/vtysh_wrapper.sh b/src/op_mode/vtysh_wrapper.sh new file mode 100755 index 000000000..47d88330b --- /dev/null +++ b/src/op_mode/vtysh_wrapper.sh @@ -0,0 +1,4 @@ +#!/bin/sh +declare -a tmp +tmp=$@ +vtysh -c "$tmp" diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 5b1ab1f1f..1e60e53df 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -25,6 +25,7 @@ import logging import signal import importlib.util import zmq +from contextlib import redirect_stdout, redirect_stderr from vyos.defaults import directories from vyos.configsource import ConfigSourceString, ConfigSourceError @@ -33,6 +34,8 @@ from vyos import ConfigError CFG_GROUP = 'vyattacfg' +script_stdout_log = '/tmp/vyos-configd-script-stdout' + debug = True logger = logging.getLogger(__name__) @@ -59,7 +62,8 @@ configd_env_unset_file = os.path.join(directories['data'], 'vyos-configd-env-uns # sourced on entering config session configd_env_file = '/etc/default/vyos-configd-env' -session_tty = None +session_out = None +session_mode = None def key_name_from_file_name(f): return os.path.splitext(f)[0] @@ -104,33 +108,30 @@ conf_mode_scripts = dict(zip(imports, modules)) exclude_set = {key_name_from_file_name(f) for f in filenames if f not in include} include_set = {key_name_from_file_name(f) for f in filenames if f in include} -def explicit_print(t, m): - try: - with open(t, 'w') as f: - f.write(m) - f.write("\n") - f.flush() - except Exception: - pass def run_script(script, config) -> int: config.set_level([]) try: - c = script.get_config(config) - script.verify(c) - script.generate(c) - script.apply(c) + with open(session_out, session_mode) as f, redirect_stdout(f): + with redirect_stderr(f): + c = script.get_config(config) + script.verify(c) + script.generate(c) + script.apply(c) except ConfigError as e: logger.critical(e) - explicit_print(session_tty, str(e)) + with open(session_out, session_mode) as f, redirect_stdout(f): + print(f"{e}\n") return R_ERROR_COMMIT - except Exception: + except Exception as e: + logger.critical(e) return R_ERROR_DAEMON return R_SUCCESS def initialization(socket): - global session_tty + global session_out + global session_mode # Reset config strings: active_string = '' session_string = '' @@ -158,9 +159,15 @@ def initialization(socket): logger.debug(f"config session pid is {pid_string}") try: - session_tty = os.readlink(f"/proc/{pid_string}/fd/1") + session_out = os.readlink(f"/proc/{pid_string}/fd/1") + session_mode = 'w' except FileNotFoundError: - session_tty = None + session_out = None + + # if not a 'live' session, for example on boot, write to file + if not session_out or '/dev/pts' not in session_out: + session_out = script_stdout_log + session_mode = 'a' try: configsource = ConfigSourceString(running_config_text=active_string, diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh index a062dc810..49e53d7e1 100755 --- a/src/system/on-dhcp-event.sh +++ b/src/system/on-dhcp-event.sh @@ -21,21 +21,20 @@ client_mac=$4 domain=$5 hostsd_client="/usr/bin/vyos-hostsd-client" -if [ -z "$client_name" ]; then - logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" - client_name=$(echo "client-"$client_mac | tr : -) -fi - -if [ "$domain" == "..YYZ!" ]; then - client_fqdn_name=$client_name - client_search_expr=$client_name -else - client_fqdn_name=$client_name.$domain - client_search_expr="$client_name\\.$domain" -fi - case "$action" in commit) # add mapping for new lease + if [ -z "$client_name" ]; then + logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" + client_name=$(echo "client-"$client_mac | tr : -) + fi + + if [ "$domain" == "..YYZ!" ]; then + client_fqdn_name=$client_name + client_search_expr=$client_name + else + client_fqdn_name=$client_name.$domain + client_search_expr="$client_name\\.$domain" + fi $hostsd_client --add-hosts "$client_fqdn_name,$client_ip" --tag "dhcp-server-$client_ip" --apply exit 0 ;; diff --git a/src/systemd/dropbear@.service b/src/systemd/dropbear@.service index a3fde5708..acf926af9 100644 --- a/src/systemd/dropbear@.service +++ b/src/systemd/dropbear@.service @@ -8,9 +8,8 @@ StartLimitIntervalSec=0 [Service] Type=forking -ExecStartPre=/usr/bin/bash -c '/usr/bin/systemctl set-environment PORT=$(cli-shell-api returnActiveValue service console-server device "%I" ssh port)' -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 +ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -P /run/dropbear/dropbear.%I.pid -p %I +PIDFile=/run/dropbear/dropbear.%I.pid KillMode=process Restart=always RestartSec=10 diff --git a/src/tests/test_util.py b/src/tests/test_util.py index f7405cbde..22bc085c5 100644 --- a/src/tests/test_util.py +++ b/src/tests/test_util.py @@ -17,11 +17,7 @@ from unittest import TestCase from vyos.util import mangle_dict_keys - class TestVyOSUtil(TestCase): - def setUp(self): - pass - def test_key_mangline(self): data = {"foo-bar": {"baz-quux": None}} expected_data = {"foo_bar": {"baz_quux": None}} diff --git a/src/validators/fqdn b/src/validators/fqdn index 66276c093..a4027e4ca 100755 --- a/src/validators/fqdn +++ b/src/validators/fqdn @@ -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 @@ -17,8 +17,7 @@ import re import sys -# pattern copied from: https://www.regextester.com/103452 -pattern = "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)" +pattern = '[A-Za-z0-9][-.A-Za-z0-9]*' if __name__ == '__main__': if len(sys.argv) != 2: diff --git a/src/validators/interface-name b/src/validators/interface-name index 32cd42fbd..72e9fd54a 100755 --- a/src/validators/interface-name +++ b/src/validators/interface-name @@ -17,7 +17,7 @@ import re import sys -pattern = '^(br|bond|dum|en|eth|gnv|peth|pppoe|tun|vti|vtun|vxlan|wg|wlan)[0-9]+|lo$' +pattern = '^(bond|br|dum|en|ersp|eth|gnv|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|vti|vtun|vxlan|wg|wlan|wlm)[0-9]+(.\d+)?|lo$' if __name__ == '__main__': if len(sys.argv) != 2: |