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)