Skip to content

Commit

Permalink
T5518: Add MLD protocol support
Browse files Browse the repository at this point in the history
Currently VyOS has protocol igmp option to enable IGMP querier and reports through FRR's pimd.

I would like to add protocol mld for IPv6 as well since FRR's IPv6 multicast functionality has significantly improved.

Enabling MLD querier and reports on a VyOS router will allow us to turn on MLD snooping on layer-3 switches.
  • Loading branch information
vfreex committed Aug 27, 2023
1 parent d3edda2 commit 6a0ae6c
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 2 deletions.
41 changes: 41 additions & 0 deletions data/templates/frr/mld.frr.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
!
{% for iface in old_ifaces %}
interface {{ iface }}
{% for group in old_ifaces[iface].gr_join %}
{% if old_ifaces[iface].gr_join[group] %}
{% for source in old_ifaces[iface].gr_join[group] %}
no ipv6 mld join {{ group }} {{ source }}
{% endfor %}
{% else %}
no ipv6 mld join {{ group }}
{% endif %}
{% endfor %}
no ipv6 mld
!
{% endfor %}
{% for interface, interface_config in ifaces.items() %}
interface {{ interface }}
{% if interface_config.version %}
ipv6 mld version {{ interface_config.version }}
{% else %}
{# IGMP default version 3 #}
ipv6 mld
{% endif %}
{% if interface_config.query_interval %}
ipv6 mld query-interval {{ interface_config.query_interval }}
{% endif %}
{% if interface_config.query_max_resp_time %}
ipv6 mld query-max-response-time {{ interface_config.query_max_resp_time }}
{% endif %}
{% for group in interface_config.gr_join %}
{% if ifaces[iface].gr_join[group] %}
{% for source in ifaces[iface].gr_join[group] %}
ipv6 mld join {{ group }} {{ source }}
{% endfor %}
{% else %}
ipv6 mld join {{ group }}
{% endif %}
{% endfor %}
!
{% endfor %}
!
95 changes: 95 additions & 0 deletions interface-definitions/protocols-mld.xml.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?xml version="1.0"?>
<!-- Multicast Listener Discovery (MLD) configuration -->
<interfaceDefinition>
<node name="protocols">
<children>
<node name="mld" owner="${vyos_conf_scripts_dir}/protocols_mld.py">
<properties>
<help>Multicast Listener Discovery (MLD)</help>
</properties>
<children>
<tagNode name="interface">
<properties>
<help>MLD interface</help>
<completionHelp>
<script>${vyos_completion_dir}/list_interfaces</script>
</completionHelp>
</properties>
<children>
<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>
</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>
</tagNode>
</children>
</node>
</children>
</node>
</interfaceDefinition>
2 changes: 1 addition & 1 deletion python/vyos/utils/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
return code, out

def call(command, flag='', shell=None, input=None, timeout=None, env=None,
stdout=PIPE, stderr=PIPE, decode='utf-8'):
stdout=None, stderr=None, decode='utf-8'):
"""
A wrapper around popen, which print the stdout and
will return the error code of a command
Expand Down
2 changes: 1 addition & 1 deletion src/conf_mode/protocols_igmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def verify(igmp):
# Check, is this multicast group
for intfc in igmp['ifaces']:
for gr_addr in igmp['ifaces'][intfc]['gr_join']:
if IPv4Address(gr_addr) < IPv4Address('224.0.0.0'):
if not IPv4Address(gr_addr).is_multicast:
raise ConfigError(gr_addr + " not a multicast group")

def generate(igmp):
Expand Down
125 changes: 125 additions & 0 deletions src/conf_mode/protocols_mld.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
#
# Copyright (C) 2020-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 os

from ipaddress import IPv6Address
from sys import exit

from vyos import ConfigError
from vyos.config import Config
from vyos.utils.process import process_named_running
from vyos.utils.process import call
from vyos.template import render
from signal import SIGTERM

from vyos import airbag
airbag.enable()

# Required to use the full path to pim6d, in another case daemon will not be started
pimd_cmd = f'/usr/lib/frr/pim6d -d -F traditional --daemon -A 127.0.0.1'

config_file = r'/tmp/mld.frr'

def get_config(config=None):
if config:
conf = config
else:
conf = Config()
mld_conf = {
'mld_conf' : False,
'old_ifaces' : {},
'ifaces' : {}
}
if not (conf.exists('protocols mld') or conf.exists_effective('protocols mld')):
return None

if conf.exists('protocols mld'):
mld_conf['mld_conf'] = True

conf.set_level('protocols mld')

# # Get interfaces
for iface in conf.list_effective_nodes('interface'):
mld_conf['old_ifaces'].update({
iface : {
'version' : conf.return_effective_value('interface {0} version'.format(iface)),
'query_interval' : conf.return_effective_value('interface {0} query-interval'.format(iface)),
'query_max_resp_time' : conf.return_effective_value('interface {0} query-max-response-time'.format(iface)),
'gr_join' : {}
}
})
for gr_join in conf.list_effective_nodes('interface {0} join'.format(iface)):
mld_conf['old_ifaces'][iface]['gr_join'][gr_join] = conf.return_effective_values('interface {0} join {1} source'.format(iface, gr_join))

for iface in conf.list_nodes('interface'):
mld_conf['ifaces'].update({
iface : {
'version' : conf.return_value('interface {0} version'.format(iface)),
'query_interval' : conf.return_value('interface {0} query-interval'.format(iface)),
'query_max_resp_time' : conf.return_value('interface {0} query-max-response-time'.format(iface)),
'gr_join' : {}
}
})
for gr_join in conf.list_nodes('interface {0} join'.format(iface)):
mld_conf['ifaces'][iface]['gr_join'][gr_join] = conf.return_values('interface {0} join {1} source'.format(iface, gr_join))

return mld_conf

def verify(mld):
if mld is None:
return None

if mld['mld_conf']:
# Check interfaces
if not mld['ifaces']:
raise ConfigError(f"MLD require defined interfaces!")
# Check, is this multicast group
for intfc in mld['ifaces']:
for gr_addr in mld['ifaces'][intfc]['gr_join']:
if not IPv6Address(gr_addr).is_multicast:
raise ConfigError(gr_addr + " not a multicast group")

def generate(mld):
if mld is None:
return None

render(config_file, 'frr/mld.frr.j2', mld)
return None

def apply(mld):
if mld is None:
return None
pim_pid = process_named_running('pim6d')
if mld['mld_conf']:
if not pim_pid:
call(pimd_cmd)
if os.path.exists(config_file):
call(f'vtysh -d pim6d -f {config_file}')
os.remove(config_file)
elif pim_pid:
os.kill(int(pim_pid), SIGTERM)
return None

if __name__ == '__main__':
try:
c = get_config()
verify(c)
generate(c)
apply(c)
except ConfigError as e:
print(e)
exit(1)

0 comments on commit 6a0ae6c

Please sign in to comment.