-
Notifications
You must be signed in to change notification settings - Fork 335
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Currently VyOS has `protocol igmp` option to enable IGMP querier and reports through FRR's pimd. I would like to add support for IPv6 as well since FRR's IPv6 multicast functionality has significantly improved. Enabling both MLD and IGMP on a VyOS router will allow us to turn on multicast snooping on layer-3 switches in dual-stack networks. Example commands: ``` // Enable on interface eth0 set protocols pimv6 interface eth0 // Explicitly join multicast group ff18::1234 on interface eth1 set protocols pimv6 interface eth1 mld join ff18::1234 // Explicitly join source-specific multicast group ff38::5678 with source address 2001:db8::1 on interface eth1 set protocols pimv6 interface eth1 mld join ff38::5678 source 2001:db8::1 ```
- Loading branch information
Showing
6 changed files
with
366 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
! | ||
{% if interface is vyos_defined %} | ||
{% for iface, iface_config in interface.items() %} | ||
interface {{ iface }} | ||
{% if iface_config.mld is vyos_defined and iface_config.mld.disable is not vyos_defined %} | ||
ipv6 mld | ||
{% if iface_config.mld.version is vyos_defined %} | ||
ipv6 mld version {{ iface_config.mld.version }} | ||
{% endif %} | ||
{% if iface_config.mld.query_interval is vyos_defined %} | ||
ipv6 mld query-interval {{ iface_config.mld.query_interval }} | ||
{% endif %} | ||
{% if iface_config.mld.query_max_resp_time is vyos_defined %} | ||
ipv6 mld query-max-response-time {{ iface_config.mld.query_max_resp_time }} | ||
{% endif %} | ||
{% if iface_config.mld.join is vyos_defined %} | ||
{% for group, group_config in iface_config.mld.join.items() %} | ||
{% if group_config.source is vyos_defined %} | ||
{% for source in group_config.source %} | ||
ipv6 mld join {{ group }} {{ source }} | ||
{% endfor %} | ||
{% else %} | ||
ipv6 mld join {{ group }} | ||
{% endif %} | ||
{% endfor %} | ||
{% endif %} | ||
{% endif %} | ||
exit | ||
! | ||
{% endfor %} | ||
! | ||
{% endif %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<?xml version="1.0"?> | ||
<!-- Protocol Independent Multicast for IPv6 (PIMv6) configuration --> | ||
<interfaceDefinition> | ||
<node name="protocols"> | ||
<children> | ||
<node name="pimv6" owner="${vyos_conf_scripts_dir}/protocols_pimv6.py"> | ||
<properties> | ||
<help>Protocol Independent Multicast for IPv6 (PIMv6)</help> | ||
<priority>400</priority> | ||
</properties> | ||
<children> | ||
<tagNode name="interface"> | ||
<properties> | ||
<help>PIMv6 interface</help> | ||
<completionHelp> | ||
<script>${vyos_completion_dir}/list_interfaces</script> | ||
</completionHelp> | ||
</properties> | ||
<children> | ||
<node name="mld"> | ||
<properties> | ||
<help>Multicast Listener Discovery (MLD)</help> | ||
</properties> | ||
<children> | ||
#include <include/generic-disable-node.xml.i> | ||
<tagNode name="join"> | ||
<properties> | ||
<help>MLD join multicast group</help> | ||
<valueHelp> | ||
<format>ipv6</format> | ||
<description>Multicast group address</description> | ||
</valueHelp> | ||
<constraint> | ||
<validator name="ipv6-address"/> | ||
</constraint> | ||
</properties> | ||
<children> | ||
<leafNode name="source"> | ||
<properties> | ||
<help>Source address</help> | ||
<valueHelp> | ||
<format>ipv6</format> | ||
<description>Source address</description> | ||
</valueHelp> | ||
<constraint> | ||
<validator name="ipv6-address"/> | ||
</constraint> | ||
<multi/> | ||
</properties> | ||
</leafNode> | ||
</children> | ||
</tagNode> | ||
<leafNode name="version"> | ||
<properties> | ||
<help>MLD version</help> | ||
<completionHelp> | ||
<list>1 2</list> | ||
</completionHelp> | ||
<valueHelp> | ||
<format>1</format> | ||
<description>MLD version 1</description> | ||
</valueHelp> | ||
<valueHelp> | ||
<format>2</format> | ||
<description>MLD version 2</description> | ||
</valueHelp> | ||
<constraint> | ||
<validator name="numeric" argument="--range 1-2"/> | ||
</constraint> | ||
</properties> | ||
<defaultValue>2</defaultValue> | ||
</leafNode> | ||
<leafNode name="query-interval"> | ||
<properties> | ||
<help>MLD query interval</help> | ||
<valueHelp> | ||
<format>u32:1-65535</format> | ||
<description>Query interval in seconds</description> | ||
</valueHelp> | ||
<constraint> | ||
<validator name="numeric" argument="--range 1-65535"/> | ||
</constraint> | ||
</properties> | ||
</leafNode> | ||
<leafNode name="query-max-response-time"> | ||
<properties> | ||
<help>MLD max query response time</help> | ||
<valueHelp> | ||
<format>u32:1-65535</format> | ||
<description>Query response value in deci-seconds</description> | ||
</valueHelp> | ||
<constraint> | ||
<validator name="numeric" argument="--range 1-65535"/> | ||
</constraint> | ||
</properties> | ||
</leafNode> | ||
</children> | ||
</node> | ||
</children> | ||
</tagNode> | ||
</children> | ||
</node> | ||
</children> | ||
</node> | ||
</interfaceDefinition> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
#!/usr/bin/env python3 | ||
# | ||
# Copyright (C) 2023 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 unittest | ||
|
||
from base_vyostest_shim import VyOSUnitTestSHIM | ||
from vyos.configsession import ConfigSessionError | ||
from vyos.ifconfig import Section | ||
from vyos.utils.process import process_named_running | ||
|
||
PROCESS_NAME = 'pim6d' | ||
base_path = ['protocols', 'pimv6'] | ||
|
||
|
||
class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): | ||
def tearDown(self): | ||
# Check for running process | ||
self.assertTrue(process_named_running(PROCESS_NAME)) | ||
self.cli_delete(base_path) | ||
self.cli_commit() | ||
|
||
def test_pimv6_01_defaults(self): | ||
# commit changes | ||
self.cli_set(base_path) | ||
self.cli_commit() | ||
|
||
interfaces = Section.interfaces('ethernet') | ||
|
||
# Verify FRR pim6d configuration | ||
for interface in interfaces: | ||
config = self.getFRRconfig( | ||
f'interface {interface}', daemon=PROCESS_NAME) | ||
self.assertIn(f'interface {interface}', config) | ||
self.assertNotIn(f' ipv6 mld', config) | ||
|
||
def test_pimv6_02_mld_simple(self): | ||
# commit changes | ||
interfaces = Section.interfaces('ethernet') | ||
|
||
for interface in interfaces: | ||
self.cli_set(base_path + ['interface', interface]) | ||
|
||
self.cli_commit() | ||
|
||
# Verify FRR pim6d configuration | ||
for interface in interfaces: | ||
config = self.getFRRconfig( | ||
f'interface {interface}', daemon=PROCESS_NAME) | ||
self.assertIn(f'interface {interface}', config) | ||
self.assertIn(f' ipv6 mld', config) | ||
self.assertNotIn(f' ipv6 mld version 1', config) | ||
|
||
# Change to MLD version 1 | ||
for interface in interfaces: | ||
self.cli_set(base_path + ['interface', | ||
interface, 'mld', 'version', '1']) | ||
|
||
self.cli_commit() | ||
|
||
# Verify FRR pim6d configuration | ||
for interface in interfaces: | ||
config = self.getFRRconfig( | ||
f'interface {interface}', daemon=PROCESS_NAME) | ||
self.assertIn(f'interface {interface}', config) | ||
self.assertIn(f' ipv6 mld', config) | ||
self.assertIn(f' ipv6 mld version 1', config) | ||
|
||
def test_pimv6_03_mld_join(self): | ||
# commit changes | ||
interfaces = Section.interfaces('ethernet') | ||
|
||
# Use an invalid multiple group address | ||
for interface in interfaces: | ||
self.cli_set(base_path + ['interface', | ||
interface, 'mld', 'join', 'fd00::1234']) | ||
|
||
with self.assertRaises(ConfigSessionError): | ||
self.cli_commit() | ||
self.cli_delete(base_path + ['interface']) | ||
|
||
# Use a valid multiple group address | ||
for interface in interfaces: | ||
self.cli_set(base_path + ['interface', | ||
interface, 'mld', 'join', 'ff18::1234']) | ||
|
||
self.cli_commit() | ||
|
||
# Verify FRR pim6d configuration | ||
for interface in interfaces: | ||
config = self.getFRRconfig( | ||
f'interface {interface}', daemon=PROCESS_NAME) | ||
self.assertIn(f'interface {interface}', config) | ||
self.assertIn(f' ipv6 mld join ff18::1234', config) | ||
|
||
# Join a source-specific multicast group | ||
for interface in interfaces: | ||
self.cli_set(base_path + ['interface', interface, | ||
'mld', 'join', 'ff38::5678', '2001:db8::5678']) | ||
|
||
self.cli_commit() | ||
|
||
# Verify FRR pim6d configuration | ||
for interface in interfaces: | ||
config = self.getFRRconfig( | ||
f'interface {interface}', daemon=PROCESS_NAME) | ||
self.assertIn(f'interface {interface}', config) | ||
self.assertIn(f' ipv6 mld join ff38::5678 2001:db8::5678', config) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main(verbosity=2) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
#!/usr/bin/env python3 | ||
# | ||
# Copyright (C) 2023 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 ipaddress import IPv6Address | ||
from sys import exit | ||
from typing import Optional | ||
|
||
from vyos import ConfigError, airbag, frr | ||
from vyos.config import Config, ConfigDict | ||
from vyos.configdict import node_changed | ||
from vyos.configverify import verify_interface_exists | ||
from vyos.template import render_to_string | ||
|
||
airbag.enable() | ||
|
||
|
||
def get_config(config: Optional[Config] = None): | ||
if config: | ||
conf = config | ||
else: | ||
conf = Config() | ||
base = ['protocols', 'pimv6'] | ||
pimv6 = conf.get_config_dict(base, key_mangling=('-', '_'), | ||
get_first_key=True, with_recursive_defaults=True) | ||
|
||
# FRR has VRF support for different routing daemons. As interfaces belong | ||
# to VRFs - or the global VRF, we need to check for changed interfaces so | ||
# that they will be properly rendered for the FRR config. Also this eases | ||
# removal of interfaces from the running configuration. | ||
interfaces_removed = node_changed(conf, base + ['interface']) | ||
if interfaces_removed: | ||
pimv6['interface_removed'] = list(interfaces_removed) | ||
|
||
return pimv6 | ||
|
||
|
||
def verify(pimv6: Optional[ConfigDict]): | ||
if pimv6 is None: | ||
return | ||
|
||
for interface, interface_config in pimv6.get('interface', {}).items(): | ||
verify_interface_exists(interface) | ||
if 'mld' in interface_config: | ||
mld = interface_config['mld'] | ||
for group in mld.get('join', {}).keys(): | ||
# Validate multicast group address | ||
if not IPv6Address(group).is_multicast: | ||
raise ConfigError(f"{group} is not a multicast group") | ||
|
||
|
||
def generate(pimv6: Optional[ConfigDict]): | ||
if pimv6 is None: | ||
return | ||
|
||
pimv6['new_frr_config'] = render_to_string('frr/pim6d.frr.j2', pimv6) | ||
|
||
|
||
def apply(pimv6: Optional[ConfigDict]): | ||
if pimv6 is None: | ||
return | ||
|
||
pim6_daemon = 'pim6d' | ||
|
||
# Save original configuration prior to starting any commit actions | ||
frr_cfg = frr.FRRConfig() | ||
|
||
frr_cfg.load_configuration(pim6_daemon) | ||
|
||
for key in ['interface', 'interface_removed']: | ||
if key not in pimv6: | ||
continue | ||
for interface in pimv6[key]: | ||
frr_cfg.modify_section( | ||
f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) | ||
|
||
if 'new_frr_config' in pimv6: | ||
frr_cfg.add_before(frr.default_add_before, pimv6['new_frr_config']) | ||
frr_cfg.commit_configuration(pim6_daemon) | ||
|
||
|
||
if __name__ == '__main__': | ||
try: | ||
c = get_config() | ||
verify(c) | ||
generate(c) | ||
apply(c) | ||
except ConfigError as e: | ||
print(e) | ||
exit(1) |