face: implement IPv6 UDP multicast transport

Change-Id: Ib6ab956354dbbba00694c7949fa9ee4639579879
Refs: #4222
diff --git a/daemon/face/multicast-udp-transport.cpp b/daemon/face/multicast-udp-transport.cpp
index 86324d3..8912759 100644
--- a/daemon/face/multicast-udp-transport.cpp
+++ b/daemon/face/multicast-udp-transport.cpp
@@ -27,6 +27,8 @@
 #include "socket-utils.hpp"
 #include "udp-protocol.hpp"
 
+#include <boost/functional/hash.hpp>
+
 #ifdef __linux__
 #include <cerrno>       // for errno
 #include <cstring>      // for std::strerror()
@@ -39,8 +41,7 @@
 NFD_LOG_INCLASS_2TEMPLATE_SPECIALIZATION_DEFINE(DatagramTransport, MulticastUdpTransport::protocol,
                                                 Multicast, "MulticastUdpTransport");
 
-MulticastUdpTransport::MulticastUdpTransport(const protocol::endpoint& localEndpoint,
-                                             const protocol::endpoint& multicastGroup,
+MulticastUdpTransport::MulticastUdpTransport(const protocol::endpoint& multicastGroup,
                                              protocol::socket&& recvSocket,
                                              protocol::socket&& sendSocket,
                                              ndn::nfd::LinkType linkType)
@@ -48,12 +49,12 @@
   , m_multicastGroup(multicastGroup)
   , m_sendSocket(std::move(sendSocket))
 {
-  this->setLocalUri(FaceUri(localEndpoint));
+  this->setLocalUri(FaceUri(m_sendSocket.local_endpoint()));
   this->setRemoteUri(FaceUri(multicastGroup));
   this->setScope(ndn::nfd::FACE_SCOPE_NON_LOCAL);
   this->setPersistency(ndn::nfd::FACE_PERSISTENCY_PERMANENT);
   this->setLinkType(linkType);
-  this->setMtu(udp::computeMtu(localEndpoint));
+  this->setMtu(udp::computeMtu(m_sendSocket.local_endpoint()));
 
   protocol::socket::send_buffer_size sendBufferSizeOption;
   boost::system::error_code error;
@@ -117,16 +118,25 @@
 
   sock.open(multicastGroup.protocol());
   sock.set_option(protocol::socket::reuse_address(true));
-  sock.bind(protocol::endpoint(multicastGroup.protocol(), multicastGroup.port()));
 
   if (multicastGroup.address().is_v4()) {
     BOOST_ASSERT(localAddress.is_v4());
+    sock.bind(multicastGroup);
     sock.set_option(boost::asio::ip::multicast::join_group(multicastGroup.address().to_v4(),
                                                            localAddress.to_v4()));
   }
   else {
-    // IPv6 multicast is not supported
-    BOOST_ASSERT(false);
+    BOOST_ASSERT(localAddress.is_v6());
+    sock.set_option(boost::asio::ip::v6_only(true));
+#ifdef WITH_TESTS
+    // To simplify unit tests, we bind to the "any" IPv6 address if the supplied multicast
+    // address lacks a scope id. Calling bind() without a scope id would otherwise fail.
+    if (multicastGroup.address().to_v6().scope_id() == 0)
+      sock.bind(protocol::endpoint(boost::asio::ip::address_v6::any(), multicastGroup.port()));
+    else
+#endif
+      sock.bind(multicastGroup);
+    sock.set_option(boost::asio::ip::multicast::join_group(multicastGroup.address().to_v6()));
   }
 
 #ifdef __linux__
@@ -149,6 +159,7 @@
 void
 MulticastUdpTransport::openTxSocket(protocol::socket& sock,
                                     const protocol::endpoint& localEndpoint,
+                                    const shared_ptr<const ndn::net::NetworkInterface>& netif,
                                     bool enableLoopback)
 {
   BOOST_ASSERT(!sock.is_open());
@@ -156,15 +167,17 @@
   sock.open(localEndpoint.protocol());
   sock.set_option(protocol::socket::reuse_address(true));
   sock.set_option(boost::asio::ip::multicast::enable_loopback(enableLoopback));
-  sock.bind(localEndpoint);
 
   if (localEndpoint.address().is_v4()) {
+    sock.bind(localEndpoint);
     if (!localEndpoint.address().is_unspecified())
       sock.set_option(boost::asio::ip::multicast::outbound_interface(localEndpoint.address().to_v4()));
   }
   else {
-    // IPv6 multicast is not supported
-    BOOST_ASSERT(false);
+    sock.set_option(boost::asio::ip::v6_only(true));
+    sock.bind(localEndpoint);
+    if (netif)
+      sock.set_option(boost::asio::ip::multicast::outbound_interface(netif->getIndex()));
   }
 }
 
@@ -172,11 +185,17 @@
 Transport::EndpointId
 DatagramTransport<boost::asio::ip::udp, Multicast>::makeEndpointId(const protocol::endpoint& ep)
 {
-  // IPv6 multicast is not supported
-  BOOST_ASSERT(ep.address().is_v4());
-
-  return (static_cast<uint64_t>(ep.port()) << 32) |
-          static_cast<uint64_t>(ep.address().to_v4().to_ulong());
+  if (ep.address().is_v4()) {
+    return (static_cast<uint64_t>(ep.port()) << 32) |
+            static_cast<uint64_t>(ep.address().to_v4().to_ulong());
+  }
+  else {
+    size_t seed = 0;
+    const auto& addrBytes = ep.address().to_v6().to_bytes();
+    boost::hash_range(seed, addrBytes.begin(), addrBytes.end());
+    boost::hash_combine(seed, ep.port());
+    return seed;
+  }
 }
 
 } // namespace face
diff --git a/daemon/face/multicast-udp-transport.hpp b/daemon/face/multicast-udp-transport.hpp
index e655f4a..ba7bcbe 100644
--- a/daemon/face/multicast-udp-transport.hpp
+++ b/daemon/face/multicast-udp-transport.hpp
@@ -59,14 +59,12 @@
 
   /**
    * \brief Creates a UDP-based transport for multicast communication
-   * \param localEndpoint local endpoint
    * \param multicastGroup multicast group
-   * \param recvSocket socket used to receive packets
+   * \param recvSocket socket used to receive multicast packets
    * \param sendSocket socket used to send to the multicast group
-   * \param linkType either LINK_TYPE_MULTI_ACCESS or LINK_TYPE_AD_HOC
+   * \param linkType either `ndn::nfd::LINK_TYPE_MULTI_ACCESS` or `ndn::nfd::LINK_TYPE_AD_HOC`
    */
-  MulticastUdpTransport(const protocol::endpoint& localEndpoint,
-                        const protocol::endpoint& multicastGroup,
+  MulticastUdpTransport(const protocol::endpoint& multicastGroup,
                         protocol::socket&& recvSocket,
                         protocol::socket&& sendSocket,
                         ndn::nfd::LinkType linkType);
@@ -83,6 +81,7 @@
   static void
   openTxSocket(protocol::socket& sock,
                const protocol::endpoint& localEndpoint,
+               const shared_ptr<const ndn::net::NetworkInterface>& netif = nullptr,
                bool enableLoopback = false);
 
 private:
diff --git a/daemon/face/udp-factory.cpp b/daemon/face/udp-factory.cpp
index c9cab22..2024cd6 100644
--- a/daemon/face/udp-factory.cpp
+++ b/daemon/face/udp-factory.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2017,  Regents of the University of California,
+ * Copyright (c) 2014-2018,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -68,6 +68,8 @@
   //   mcast yes
   //   mcast_group 224.0.23.170
   //   mcast_port 56363
+  //   mcast_group_v6 ff02::1234
+  //   mcast_port_v6 56363
   //   mcast_ad_hoc no
   //   whitelist
   //   {
@@ -126,6 +128,22 @@
       else if (key == "mcast_port") {
         mcastConfig.group.port(ConfigFile::parseNumber<uint16_t>(pair, "face_system.udp"));
       }
+      else if (key == "mcast_group_v6") {
+        const std::string& valueStr = value.get_value<std::string>();
+        boost::system::error_code ec;
+        mcastConfig.groupV6.address(ndn::ip::addressV6FromString(valueStr, ec));
+        if (ec) {
+          BOOST_THROW_EXCEPTION(ConfigFile::Error("face_system.udp.mcast_group_v6: '" +
+                                valueStr + "' cannot be parsed as an IPv6 address"));
+        }
+        else if (!mcastConfig.groupV6.address().is_multicast()) {
+          BOOST_THROW_EXCEPTION(ConfigFile::Error("face_system.udp.mcast_group_v6: '" +
+                                valueStr + "' is not a multicast address"));
+        }
+      }
+      else if (key == "mcast_port_v6") {
+        mcastConfig.groupV6.port(ConfigFile::parseNumber<uint16_t>(pair, "face_system.udp"));
+      }
       else if (key == "mcast_ad_hoc") {
         bool wantAdHoc = ConfigFile::parseYesNo(pair, "face_system.udp");
         mcastConfig.linkType = wantAdHoc ? ndn::nfd::LINK_TYPE_AD_HOC : ndn::nfd::LINK_TYPE_MULTI_ACCESS;
@@ -181,6 +199,7 @@
   if (m_mcastConfig.isEnabled != mcastConfig.isEnabled) {
     if (mcastConfig.isEnabled) {
       NFD_LOG_INFO("enabling multicast on " << mcastConfig.group);
+      NFD_LOG_INFO("enabling multicast on " << mcastConfig.groupV6);
     }
     else {
       NFD_LOG_INFO("disabling multicast");
@@ -191,9 +210,13 @@
       NFD_LOG_WARN("Cannot change ad hoc setting on existing faces");
     }
     if (m_mcastConfig.group != mcastConfig.group) {
-      NFD_LOG_INFO("changing multicast group from " << m_mcastConfig.group <<
+      NFD_LOG_INFO("changing IPv4 multicast group from " << m_mcastConfig.group <<
                    " to " << mcastConfig.group);
     }
+    if (m_mcastConfig.groupV6 != mcastConfig.groupV6) {
+      NFD_LOG_INFO("changing IPv6 multicast group from " << m_mcastConfig.groupV6 <<
+                   " to " << mcastConfig.groupV6);
+    }
     if (m_mcastConfig.netifPredicate != mcastConfig.netifPredicate) {
       NFD_LOG_INFO("changing whitelist/blacklist");
     }
@@ -262,15 +285,11 @@
   if (it != m_channels.end())
     return it->second;
 
-  if (localEndpoint.address().is_multicast()) {
-    BOOST_THROW_EXCEPTION(Error("createChannel is only for unicast channels. The provided endpoint "
-                                "is multicast. Use createMulticastFace to create a multicast face"));
-  }
-
   // check if the endpoint is already used by a multicast face
   if (m_mcastFaces.find(localEndpoint) != m_mcastFaces.end()) {
-    BOOST_THROW_EXCEPTION(Error("Cannot create the requested UDP unicast channel, local "
-                                "endpoint is already allocated for a UDP multicast face"));
+    BOOST_THROW_EXCEPTION(Error("Cannot create UDP channel on " +
+                                boost::lexical_cast<std::string>(localEndpoint) +
+                                ", endpoint already allocated for a UDP multicast face"));
   }
 
   auto channel = std::make_shared<UdpChannel>(localEndpoint, idleTimeout);
@@ -286,126 +305,148 @@
 }
 
 shared_ptr<Face>
-UdpFactory::createMulticastFace(const udp::Endpoint& localEndpoint,
-                                const udp::Endpoint& multicastEndpoint,
-                                const shared_ptr<const ndn::net::NetworkInterface>& netif)
+UdpFactory::createMulticastFace(const shared_ptr<const net::NetworkInterface>& netif,
+                                const ip::address& localAddress,
+                                const udp::Endpoint& multicastEndpoint)
 {
   BOOST_ASSERT(multicastEndpoint.address().is_multicast());
-  BOOST_ASSERT(localEndpoint.port() == multicastEndpoint.port());
 
-  // check if the local and multicast endpoints are already in use for a multicast face
-  auto it = m_mcastFaces.find(localEndpoint);
+  udp::Endpoint localEp(localAddress, multicastEndpoint.port());
+  BOOST_ASSERT(localEp.protocol() == multicastEndpoint.protocol());
+
+  auto mcastEp = multicastEndpoint;
+  if (mcastEp.address().is_v6()) {
+    // in IPv6, a scope id on the multicast address is always required
+    auto mcastAddress = mcastEp.address().to_v6();
+    mcastAddress.scope_id(netif->getIndex());
+    mcastEp.address(mcastAddress);
+  }
+
+  // check if the local endpoint is already used by another multicast face
+  auto it = m_mcastFaces.find(localEp);
   if (it != m_mcastFaces.end()) {
-    if (it->second->getRemoteUri() == FaceUri(multicastEndpoint))
+    if (it->second->getRemoteUri() == FaceUri(mcastEp))
       return it->second;
     else
-      BOOST_THROW_EXCEPTION(Error("Cannot create the requested UDP multicast face, local "
-                                  "endpoint is already allocated for a UDP multicast face "
-                                  "on a different multicast group"));
+      BOOST_THROW_EXCEPTION(Error("Cannot create UDP multicast face on " +
+                                  boost::lexical_cast<std::string>(localEp) +
+                                  ", endpoint already allocated for a different UDP multicast face"));
   }
 
   // check if the local endpoint is already used by a unicast channel
-  if (m_channels.find(localEndpoint) != m_channels.end()) {
-    BOOST_THROW_EXCEPTION(Error("Cannot create the requested UDP multicast face, local "
-                                "endpoint is already allocated for a UDP unicast channel"));
-  }
-
-  if (localEndpoint.address().is_v6() || multicastEndpoint.address().is_v6()) {
-    BOOST_THROW_EXCEPTION(Error("IPv6 multicast is not supported yet. Please provide an IPv4 "
-                                "address"));
+  if (m_channels.find(localEp) != m_channels.end()) {
+    BOOST_THROW_EXCEPTION(Error("Cannot create UDP multicast face on " +
+                                boost::lexical_cast<std::string>(localEp) +
+                                ", endpoint already allocated for a UDP channel"));
   }
 
   ip::udp::socket rxSock(getGlobalIoService());
-  MulticastUdpTransport::openRxSocket(rxSock, multicastEndpoint, localEndpoint.address(), netif);
+  MulticastUdpTransport::openRxSocket(rxSock, mcastEp, localAddress, netif);
   ip::udp::socket txSock(getGlobalIoService());
-  MulticastUdpTransport::openTxSocket(txSock, localEndpoint);
+  MulticastUdpTransport::openTxSocket(txSock, udp::Endpoint(localAddress, 0), netif);
 
   auto linkService = make_unique<GenericLinkService>();
-  auto transport = make_unique<MulticastUdpTransport>(localEndpoint, multicastEndpoint,
-                                                      std::move(rxSock), std::move(txSock),
+  auto transport = make_unique<MulticastUdpTransport>(mcastEp, std::move(rxSock), std::move(txSock),
                                                       m_mcastConfig.linkType);
   auto face = make_shared<Face>(std::move(linkService), std::move(transport));
 
-  m_mcastFaces[localEndpoint] = face;
-  connectFaceClosedSignal(*face, [this, localEndpoint] { m_mcastFaces.erase(localEndpoint); });
+  m_mcastFaces[localEp] = face;
+  connectFaceClosedSignal(*face, [this, localEp] { m_mcastFaces.erase(localEp); });
 
   return face;
 }
 
 static ndn::optional<ip::address>
-getV4Address(const net::NetworkInterface& netif)
+pickAddress(const net::NetworkInterface& netif, net::AddressFamily af)
 {
   for (const auto& na : netif.getNetworkAddresses()) {
-    if (na.getFamily() == net::AddressFamily::V4 && na.getScope() != net::AddressScope::NOWHERE) {
+    if (na.getFamily() == af &&
+        (na.getScope() == net::AddressScope::LINK || na.getScope() == net::AddressScope::GLOBAL)) {
       return na.getIp();
     }
   }
   return ndn::nullopt;
 }
 
-shared_ptr<Face>
+std::vector<shared_ptr<Face>>
 UdpFactory::applyMcastConfigToNetif(const shared_ptr<const net::NetworkInterface>& netif)
 {
   BOOST_ASSERT(netif != nullptr);
 
   if (!m_mcastConfig.isEnabled) {
-    return nullptr;
+    return {};
   }
 
   if (!netif->isUp()) {
-    NFD_LOG_DEBUG("Not creating multicast face on " << netif->getName() << ": netif is down");
-    return nullptr;
+    NFD_LOG_DEBUG("Not creating multicast faces on " << netif->getName() << ": netif is down");
+    return {};
+  }
+
+  if (netif->isLoopback()) {
+    NFD_LOG_DEBUG("Not creating multicast faces on " << netif->getName() << ": netif is loopback");
+    return {};
   }
 
   if (!netif->canMulticast()) {
-    NFD_LOG_DEBUG("Not creating multicast face on " << netif->getName() << ": netif cannot multicast");
-    return nullptr;
-  }
-
-  auto address = getV4Address(*netif);
-  if (!address) {
-    NFD_LOG_DEBUG("Not creating multicast face on " << netif->getName() << ": no IPv4 address");
-    // keep an eye on new addresses
-    m_netifConns[netif->getIndex()].addrAddConn =
-      netif->onAddressAdded.connectSingleShot(bind(&UdpFactory::applyMcastConfigToNetif, this, netif));
-    return nullptr;
+    NFD_LOG_DEBUG("Not creating multicast faces on " << netif->getName() << ": netif cannot multicast");
+    return {};
   }
 
   if (!m_mcastConfig.netifPredicate(*netif)) {
-    NFD_LOG_DEBUG("Not creating multicast face on " << netif->getName() << ": rejected by whitelist/blacklist");
-    return nullptr;
+    NFD_LOG_DEBUG("Not creating multicast faces on " << netif->getName() << ": rejected by whitelist/blacklist");
+    return {};
   }
 
-  NFD_LOG_DEBUG("Creating multicast face on " << netif->getName());
-
-  udp::Endpoint localEndpoint(*address, m_mcastConfig.group.port());
-  auto face = this->createMulticastFace(localEndpoint, m_mcastConfig.group, netif);
-  if (face->getId() == INVALID_FACEID) {
-    // new face: register with forwarding
-    this->addFace(face);
+  std::vector<ip::address> addrs;
+  for (auto af : {net::AddressFamily::V4, net::AddressFamily::V6}) {
+    auto addr = pickAddress(*netif, af);
+    if (addr)
+      addrs.push_back(*addr);
   }
 
-  return face;
+  if (addrs.empty()) {
+    NFD_LOG_DEBUG("Not creating multicast faces on " << netif->getName() << ": no viable IP address");
+    // keep an eye on new addresses
+    m_netifConns[netif->getIndex()].addrAddConn =
+      netif->onAddressAdded.connect(bind(&UdpFactory::applyMcastConfigToNetif, this, netif));
+    return {};
+  }
+
+  NFD_LOG_DEBUG("Creating multicast faces on " << netif->getName());
+
+  std::vector<shared_ptr<Face>> faces;
+  for (const auto& addr : addrs) {
+    auto face = this->createMulticastFace(netif, addr,
+                                          addr.is_v4() ? m_mcastConfig.group : m_mcastConfig.groupV6);
+    if (face->getId() == INVALID_FACEID) {
+      // new face: register with forwarding
+      this->addFace(face);
+    }
+    faces.push_back(std::move(face));
+  }
+
+  return faces;
 }
 
 void
 UdpFactory::applyMcastConfig(const FaceSystem::ConfigContext& context)
 {
   // collect old faces
-  std::set<shared_ptr<Face>> oldFaces;
-  boost::copy(m_mcastFaces | boost::adaptors::map_values, std::inserter(oldFaces, oldFaces.end()));
+  std::set<shared_ptr<Face>> facesToClose;
+  boost::copy(m_mcastFaces | boost::adaptors::map_values,
+              std::inserter(facesToClose, facesToClose.end()));
 
   // create faces if requested by config
   for (const auto& netif : netmon->listNetworkInterfaces()) {
-    auto face = this->applyMcastConfigToNetif(netif);
-    if (face != nullptr) {
+    auto facesToKeep = this->applyMcastConfigToNetif(netif);
+    for (const auto& face : facesToKeep) {
       // don't destroy face
-      oldFaces.erase(face);
+      facesToClose.erase(face);
     }
   }
 
   // destroy old faces that are not needed in new configuration
-  for (const auto& face : oldFaces) {
+  for (const auto& face : facesToClose) {
     face->close();
   }
 }
diff --git a/daemon/face/udp-factory.hpp b/daemon/face/udp-factory.hpp
index 3f644c4..1153a7a 100644
--- a/daemon/face/udp-factory.hpp
+++ b/daemon/face/udp-factory.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2017,  Regents of the University of California,
+ * Copyright (c) 2014-2018,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -99,30 +99,31 @@
    *
    * The face will join the specified multicast group.
    *
-   * If this method is called twice with the same pair of endpoints, only one
+   * If this method is called twice with the same set of arguments, only one
    * face will be created. The second call will just return the existing face.
    *
    * If a UDP channel, unicast face, or multicast face already exists on the
-   * same local endpoint, the creation fails and an exception is thrown.
+   * same combination of local address and multicast port, the creation fails
+   * and an exception is thrown.
    *
-   * \param localEndpoint the local endpoint
-   * \param multicastEndpoint the multicast endpoint
    * \param netif the network interface to which the face will be bound
+   * \param localAddress the local IP address to which the face will be bound
+   * \param multicastEndpoint the multicast endpoint (multicast group and port number)
    *
    * \return always a valid shared pointer to the created face;
    *         an exception is thrown if the face cannot be created.
    * \throw UdpFactory::Error
    */
   shared_ptr<Face>
-  createMulticastFace(const udp::Endpoint& localEndpoint,
-                      const udp::Endpoint& multicastEndpoint,
-                      const shared_ptr<const ndn::net::NetworkInterface>& netif);
+  createMulticastFace(const shared_ptr<const ndn::net::NetworkInterface>& netif,
+                      const boost::asio::ip::address& localAddress,
+                      const udp::Endpoint& multicastEndpoint);
 
 private:
-  /** \brief Create UDP multicast face on \p netif if needed by \p m_mcastConfig
-   *  \return new or existing face, or nullptr if no face should be created
+  /** \brief Create UDP multicast faces on \p netif if needed by \p m_mcastConfig
+   *  \return list of faces (just created or already existing) on \p netif
    */
-  shared_ptr<Face>
+  std::vector<shared_ptr<Face>>
   applyMcastConfigToNetif(const shared_ptr<const ndn::net::NetworkInterface>& netif);
 
   /** \brief Create and destroy UDP multicast faces according to \p m_mcastConfig
@@ -137,6 +138,7 @@
   {
     bool isEnabled = false;
     udp::Endpoint group = udp::getDefaultMulticastGroup();
+    udp::Endpoint groupV6 = udp::getDefaultMulticastGroupV6();
     ndn::nfd::LinkType linkType = ndn::nfd::LINK_TYPE_MULTI_ACCESS;
     NetworkInterfacePredicate netifPredicate;
   };
diff --git a/daemon/face/udp-protocol.hpp b/daemon/face/udp-protocol.hpp
index f716658..85a6a5f 100644
--- a/daemon/face/udp-protocol.hpp
+++ b/daemon/face/udp-protocol.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
- * Copyright (c) 2014-2017,  Regents of the University of California,
+/*
+ * Copyright (c) 2014-2018,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -38,15 +38,22 @@
 ssize_t
 computeMtu(const Endpoint& localEndpoint);
 
-/** \return default multicast group: 224.0.23.170:56363
+/** \return default IPv4 multicast group: 224.0.23.170:56363
  */
 inline Endpoint
 getDefaultMulticastGroup()
 {
-  // 224.0.23.170:56363
   return {boost::asio::ip::address_v4(0xE00017AA), 56363};
 }
 
+/** \return default IPv6 multicast group: [FF02::1234]:56363
+ */
+inline Endpoint
+getDefaultMulticastGroupV6()
+{
+  return {boost::asio::ip::address_v6::from_string("FF02::1234"), 56363};
+}
+
 } // namespace udp
 } // namespace nfd