diff --git a/data/templates/frr/bfd.frr.tmpl b/data/templates/frr/bfd.frr.tmpl index 9e5ad3379..c618efdc6 100644 --- a/data/templates/frr/bfd.frr.tmpl +++ b/data/templates/frr/bfd.frr.tmpl @@ -1,22 +1,25 @@ ! bfd {% for peer in old_peers %} no peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} {% endfor %} ! {% for peer in new_peers %} peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} detect-multiplier {{ peer.multiplier }} receive-interval {{ peer.rx_interval }} transmit-interval {{ peer.tx_interval }} {% if peer.echo_mode %} echo-mode {% endif %} +{% if peer.minimum_ttl %} + minimum-ttl {{ peer.minimum_ttl }} +{% endif %} {% if peer.echo_interval != '' %} echo-interval {{ peer.echo_interval }} {% endif %} {% if not peer.shutdown %}no {% endif %}shutdown {% endfor %} ! diff --git a/interface-definitions/protocols-bfd.xml.in b/interface-definitions/protocols-bfd.xml.in index 8900e7955..0423ebcb2 100644 --- a/interface-definitions/protocols-bfd.xml.in +++ b/interface-definitions/protocols-bfd.xml.in @@ -1,140 +1,152 @@ <?xml version="1.0"?> <!-- Bidirectional Forwarding Detection (BFD) configuration --> <interfaceDefinition> <node name="protocols"> <children> <node name="bfd" owner="${vyos_conf_scripts_dir}/protocols_bfd.py"> <properties> <help>Bidirectional Forwarding Detection (BFD)</help> <priority>820</priority> </properties> <children> <tagNode name="peer"> <properties> <help>Configures a new BFD peer to listen and talk to</help> <valueHelp> <format>ipv4</format> <description>BFD peer IPv4 address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>BFD peer IPv6 address</description> </valueHelp> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> </constraint> </properties> <children> <node name="source"> <properties> <help>Bind listener to specified interface/address, mandatory for IPv6</help> </properties> <children> <leafNode name="interface"> <properties> <help>Local interface to bind our peer listener to</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> </properties> </leafNode> <leafNode name="address"> <properties> <help>Local address to bind our peer listener to</help> <valueHelp> <format>ipv4</format> <description>Local IPv4 address used to connect to the peer</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>Local IPv6 address used to connect to the peer</description> </valueHelp> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> </constraint> </properties> </leafNode> </children> </node> <node name="interval"> <properties> <help>Configure timer intervals</help> </properties> <children> <leafNode name="receive"> <properties> <help>Minimum interval of receiving control packets</help> <valueHelp> <format>10-60000</format> <description>Interval in milliseconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 10-60000"/> </constraint> </properties> </leafNode> <leafNode name="transmit"> <properties> <help>Minimum interval of transmitting control packets</help> <valueHelp> <format>10-60000</format> <description>Interval in milliseconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 10-60000"/> </constraint> </properties> </leafNode> <leafNode name="multiplier"> <properties> <help>Multiplier to determine packet loss</help> <valueHelp> <format>2-255</format> <description>Remote transmission interval will be multiplied by this value</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 2-255"/> </constraint> </properties> </leafNode> <leafNode name="echo-interval"> <properties> <help>Echo receive transmission interval</help> <valueHelp> <format>10-60000</format> <description>The minimal echo receive transmission interval that this system is capable of handling</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 10-60000"/> </constraint> </properties> </leafNode> </children> </node> <leafNode name="shutdown"> <properties> <help>Disable this peer</help> <valueless/> </properties> </leafNode> <leafNode name="multihop"> <properties> <help>Allow this BFD peer to not be directly connected</help> <valueless/> </properties> </leafNode> + <leafNode name="minimum-ttl"> + <properties> + <help>Expect packets with at least this TTL</help> + <valueHelp> + <format>u32:1-254</format> + <description>Minimum TTL expected</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-254"/> + </constraint> + </properties> + </leafNode> <leafNode name="echo-mode"> <properties> <help>Enables the echo transmission mode</help> <valueless/> </properties> </leafNode> </children> </tagNode> </children> </node> </children> </node> </interfaceDefinition> diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index 46a2bdcfa..7839aee12 100755 --- a/smoketest/scripts/cli/test_protocols_bfd.py +++ b/smoketest/scripts/cli/test_protocols_bfd.py @@ -1,127 +1,132 @@ #!/usr/bin/env python3 # # Copyright (C) 2021 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import process_named_running PROCESS_NAME = 'bfdd' base_path = ['protocols', 'bfd'] dum_if = 'dum1001' neighbor_config = { '192.0.2.10' : { 'intv_rx' : '500', 'intv_tx' : '600', 'multihop' : '', + 'minimum_ttl': '50', 'source_addr': '192.0.2.254', }, '192.0.2.20' : { 'echo_mode' : '', 'intv_echo' : '100', 'intv_mult' : '111', 'intv_rx' : '222', 'intv_tx' : '333', 'shutdown' : '', 'source_intf': dum_if, }, '2001:db8::a' : { 'source_addr': '2001:db8::1', 'source_intf': dum_if, }, '2001:db8::b' : { 'source_addr': '2001:db8::1', 'multihop' : '', }, } def getFRRconfig(): return cmd('vtysh -c "show run" | sed -n "/^bfd/,/^!/p"') def getBFDPeerconfig(peer): return cmd(f'vtysh -c "show run" | sed -n "/^ {peer}/,/^!/p"') class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): def setUp(self): self.cli_set(['interfaces', 'dummy', dum_if, 'address', '192.0.2.1/24']) self.cli_set(['interfaces', 'dummy', dum_if, 'address', '2001:db8::1/64']) def tearDown(self): self.cli_delete(['interfaces', 'dummy', dum_if]) self.cli_delete(base_path) self.cli_commit() # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) def test_bfd_simple(self): for peer, peer_config in neighbor_config.items(): if 'echo_mode' in peer_config: self.cli_set(base_path + ['peer', peer, 'echo-mode']) if 'intv_echo' in peer_config: self.cli_set(base_path + ['peer', peer, 'interval', 'echo-interval', peer_config["intv_echo"]]) if 'intv_mult' in peer_config: self.cli_set(base_path + ['peer', peer, 'interval', 'multiplier', peer_config["intv_mult"]]) if 'intv_rx' in peer_config: self.cli_set(base_path + ['peer', peer, 'interval', 'receive', peer_config["intv_rx"]]) if 'intv_tx' in peer_config: self.cli_set(base_path + ['peer', peer, 'interval', 'transmit', peer_config["intv_tx"]]) if 'multihop' in peer_config: self.cli_set(base_path + ['peer', peer, 'multihop']) + if 'minimum_ttl' in peer_config: + self.cli_set(base_path + ['peer', peer, 'minimum-ttl', peer_config["minimum_ttl"]]) if 'shutdown' in peer_config: self.cli_set(base_path + ['peer', peer, 'shutdown']) if 'source_addr' in peer_config: self.cli_set(base_path + ['peer', peer, 'source', 'address', peer_config["source_addr"]]) if 'source_intf' in peer_config: self.cli_set(base_path + ['peer', peer, 'source', 'interface', peer_config["source_intf"]]) # commit changes self.cli_commit() # Verify FRR bgpd configuration frrconfig = getFRRconfig() for peer, peer_config in neighbor_config.items(): tmp = f'peer {peer}' if 'multihop' in peer_config: tmp += f' multihop' if 'source_addr' in peer_config: tmp += f' local-address {peer_config["source_addr"]}' if 'source_intf' in peer_config: tmp += f' interface {peer_config["source_intf"]}' self.assertIn(tmp, frrconfig) peerconfig = getBFDPeerconfig(tmp) if 'echo_mode' in peer_config: self.assertIn(f' echo-mode', peerconfig) + if 'minimum_ttl' in peer_config: + self.assertIn(f' minimum-ttl {peer_config["minimum_ttl"]}', peerconfig) if 'intv_echo' in peer_config: self.assertIn(f' echo-interval {peer_config["intv_echo"]}', peerconfig) if 'intv_mult' in peer_config: self.assertIn(f' detect-multiplier {peer_config["intv_mult"]}', peerconfig) if 'intv_rx' in peer_config: self.assertIn(f' receive-interval {peer_config["intv_rx"]}', peerconfig) if 'intv_tx' in peer_config: self.assertIn(f' transmit-interval {peer_config["intv_tx"]}', peerconfig) if 'shutdown' in peer_config: self.assertIn(f' shutdown', peerconfig) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index d1e551cad..19076068d 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -1,216 +1,226 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-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 os from sys import exit from copy import deepcopy from vyos.config import Config from vyos.template import is_ipv6 from vyos.template import render from vyos.util import call from vyos.validate import is_ipv6_link_local from vyos import ConfigError 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, + 'minimum_ttl': None, } # 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 + # Enables or disables the echo transmission mode + if conf_mode == "effective" and conf.exists_effective('minimum-ttl'): + bfd_peer['minimum_ttl'] = conf.return_effective_value('minimum-ttl') + if conf_mode == "proposed" and conf.exists('minimum-ttl'): + bfd_peer['minimum_ttl'] = conf.return_value('minimum-ttl') + 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 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 return bfd def verify(bfd): if bfd is None: 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') + if peer['minimum_ttl'] != None and peer['multihop'] != True: + raise ConfigError('Minimum TTL is only available for multihop BFD sessions!') + # 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)) return None def generate(bfd): if bfd is None: return None render(config_file, 'frr/bfd.frr.tmpl', bfd) return None 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) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)