diff --git a/data/templates/frr/mld.frr.j2 b/data/templates/frr/mld.frr.j2 new file mode 100644 index 00000000000..7352480400a --- /dev/null +++ b/data/templates/frr/mld.frr.j2 @@ -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 %} +! diff --git a/interface-definitions/protocols-mld.xml.in b/interface-definitions/protocols-mld.xml.in new file mode 100644 index 00000000000..b1b1e10f839 --- /dev/null +++ b/interface-definitions/protocols-mld.xml.in @@ -0,0 +1,95 @@ + + + + + + + + Multicast Listener Discovery (MLD) + + + + + MLD interface + + + + + + + + MLD join multicast group + + ipv6 + Multicast group address + + + + + + + + + Source address + + ipv6 + Source address + + + + + + + + + + + + MLD version + + 1 2 + + + 1 + MLD version 1 + + + 2 + MLD version 2 + + + + + + + + + MLD query interval + + u32:1-65535 + Query interval in seconds + + + + + + + + + MLD max query response time + + u32:1-65535 + Query response value in deci-seconds + + + + + + + + + + + + + diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py index 91154799506..e09c7d86d73 100644 --- a/python/vyos/utils/process.py +++ b/python/vyos/utils/process.py @@ -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 diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py index f6097e28244..4351890253d 100755 --- a/src/conf_mode/protocols_igmp.py +++ b/src/conf_mode/protocols_igmp.py @@ -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): diff --git a/src/conf_mode/protocols_mld.py b/src/conf_mode/protocols_mld.py new file mode 100755 index 00000000000..238eaff0c97 --- /dev/null +++ b/src/conf_mode/protocols_mld.py @@ -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 . + +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)