[Linux] DnssdImpl: rework avahi implementation (#26397)

* InetInterface: add IsLoopback() to InterfaceIterator and InterfaceAddressIterator

Required for improving dns-sd avahi based implementation

* [Linux] DnssdImpl: rework avahi implementation

This commit fixes two problems with the previous avahi based dns-sd implementation:

- Publishing more than one service at the same time did not work.
  This needs to be possible e.g. when a node is commissioned into multiple fabrics.
  The previous implementation falsely assumed that additional services can be added
  to already committed (=published) AvahiEntryGroup, which is not the case.
  An AvahiEntryGroup can only publish multiple services ALL AT ONCE.
  The new implementation creates a new AvahiEntryGroup per service, on demand.

- The previous implementation took ownership of the platform-global default hostname,
  (by overwriting it). This is not a good idea because the default hostname is usually
  of relevance for other non-matter services on a given Linux platform.
  The new implementation establishes the matter-mandated MAC-derived hostname separately
  and explicitly adds interface addresses.

* DnssdImpl.cpp: avoid shadowing local vars to prevent warning/error

* DnssdImpl.cpp: make work without INET_CONFIG_ENABLE_IPV4

* DnssdImpl.cpp: fix missing error variable assignment in SuccessOrExit()

(found by code-lints)
diff --git a/src/inet/InetInterface.cpp b/src/inet/InetInterface.cpp
index 7c09c9f..8560dd6 100644
--- a/src/inet/InetInterface.cpp
+++ b/src/inet/InetInterface.cpp
@@ -562,6 +562,11 @@
     return (GetFlags() & IFF_UP) != 0;
 }
 
+bool InterfaceIterator::IsLoopback()
+{
+    return (GetFlags() & IFF_LOOPBACK) != 0;
+}
+
 bool InterfaceIterator::SupportsMulticast()
 {
     return (GetFlags() & IFF_MULTICAST) != 0;
@@ -706,6 +711,11 @@
     return HasCurrent() && (mCurAddr->ifa_flags & IFF_UP) != 0;
 }
 
+bool InterfaceAddressIterator::IsLoopback()
+{
+    return HasCurrent() && (mCurAddr->ifa_flags & IFF_LOOPBACK) != 0;
+}
+
 bool InterfaceAddressIterator::SupportsMulticast()
 {
     return HasCurrent() && (mCurAddr->ifa_flags & IFF_MULTICAST) != 0;
diff --git a/src/inet/InetInterface.h b/src/inet/InetInterface.h
index 8a18229..9603ba8 100644
--- a/src/inet/InetInterface.h
+++ b/src/inet/InetInterface.h
@@ -311,6 +311,14 @@
     bool IsUp();
 
     /**
+     * Returns whether the current network interface is a loopback interface
+     *
+     * @return  \c true if current network interface is a loopback interface, \c false
+     *          if not, or if the iterator is positioned beyond the end of the list.
+     */
+    bool IsLoopback();
+
+    /**
      * Returns whether the current network interface supports multicast.
      *
      * @return  \c true if current network interface supports multicast, \c false
@@ -505,6 +513,14 @@
     bool IsUp();
 
     /**
+     * Returns whether the current network interface is a loopback interface
+     *
+     * @return  \c true if current network interface is a loopback interface, \c false
+     *          if not, or if the iterator is positioned beyond the end of the list.
+     */
+    bool IsLoopback();
+
+    /**
      * Returns whether the network interface associated with the current interface address supports multicast.
      *
      * @return  \c true if multicast is supported, \c false if not, or
diff --git a/src/platform/Linux/DnssdImpl.cpp b/src/platform/Linux/DnssdImpl.cpp
index eac8003..289ae41 100644
--- a/src/platform/Linux/DnssdImpl.cpp
+++ b/src/platform/Linux/DnssdImpl.cpp
@@ -332,7 +332,7 @@
 
     VerifyOrExit(initCallback != nullptr, error = CHIP_ERROR_INVALID_ARGUMENT);
     VerifyOrExit(errorCallback != nullptr, error = CHIP_ERROR_INVALID_ARGUMENT);
-    VerifyOrExit(mClient == nullptr && mGroup == nullptr, error = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit(mClient == nullptr && mPublishedGroups.empty(), error = CHIP_ERROR_INCORRECT_STATE);
     mInitCallback       = initCallback;
     mErrorCallback      = errorCallback;
     mAsyncReturnContext = context;
@@ -346,11 +346,7 @@
 
 void MdnsAvahi::Shutdown()
 {
-    if (mGroup)
-    {
-        avahi_entry_group_free(mGroup);
-        mGroup = nullptr;
-    }
+    StopPublish();
     if (mClient)
     {
         avahi_client_free(mClient);
@@ -361,19 +357,11 @@
 CHIP_ERROR MdnsAvahi::SetHostname(const char * hostname)
 {
     CHIP_ERROR error = CHIP_NO_ERROR;
-    int avahiRet;
 
     VerifyOrExit(mClient != nullptr, error = CHIP_ERROR_INCORRECT_STATE);
-    avahiRet = avahi_client_set_host_name(mClient, hostname);
-    if (avahiRet == AVAHI_ERR_ACCESS_DENIED)
-    {
-        ChipLogError(DeviceLayer, "Cannot set hostname on this system, continue anyway...");
-    }
-    else if (avahiRet != AVAHI_OK && avahiRet != AVAHI_ERR_NO_CHANGE)
-    {
-        error = CHIP_ERROR_INTERNAL;
-    }
-
+    // Note: we do no longer set the primary hostname here, as other services
+    // on the platform might not be happy with the matter mandated hostname.
+    // Instead, we'll establish our own hostname when needed (see PublishService())
 exit:
     return error;
 }
@@ -390,16 +378,8 @@
     case AVAHI_CLIENT_S_RUNNING:
         ChipLogProgress(DeviceLayer, "Avahi client registered");
         mClient = client;
-        mGroup  = avahi_entry_group_new(client, HandleGroupState, this);
-        if (mGroup == nullptr)
-        {
-            ChipLogError(DeviceLayer, "Failed to create avahi group: %s", avahi_strerror(avahi_client_errno(client)));
-            mInitCallback(mAsyncReturnContext, CHIP_ERROR_OPEN_FAILED);
-        }
-        else
-        {
-            mInitCallback(mAsyncReturnContext, CHIP_NO_ERROR);
-        }
+        // no longer create groups here, but on a by-service basis in PublishService()
+        mInitCallback(mAsyncReturnContext, CHIP_NO_ERROR);
         break;
     case AVAHI_CLIENT_FAILURE:
         ChipLogError(DeviceLayer, "Avahi client failure");
@@ -408,22 +388,8 @@
     case AVAHI_CLIENT_S_COLLISION:
     case AVAHI_CLIENT_S_REGISTERING:
         ChipLogProgress(DeviceLayer, "Avahi re-register required");
-        if (mGroup != nullptr)
-        {
-            avahi_entry_group_reset(mGroup);
-            avahi_entry_group_free(mGroup);
-        }
-        mGroup = avahi_entry_group_new(client, HandleGroupState, this);
-        mPublishedServices.clear();
-        if (mGroup == nullptr)
-        {
-            ChipLogError(DeviceLayer, "Failed to create avahi group: %s", avahi_strerror(avahi_client_errno(client)));
-            mErrorCallback(mAsyncReturnContext, CHIP_ERROR_OPEN_FAILED);
-        }
-        else
-        {
-            mErrorCallback(mAsyncReturnContext, CHIP_ERROR_FORCED_RESET);
-        }
+        StopPublish();
+        mErrorCallback(mAsyncReturnContext, CHIP_ERROR_FORCED_RESET);
         break;
     case AVAHI_CLIENT_CONNECTING:
         ChipLogProgress(DeviceLayer, "Avahi connecting");
@@ -449,7 +415,7 @@
         break;
     case AVAHI_ENTRY_GROUP_FAILURE:
         ChipLogError(DeviceLayer, "Avahi group internal failure %s",
-                     avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(mGroup))));
+                     avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(group))));
         mErrorCallback(mAsyncReturnContext, CHIP_ERROR_INTERNAL);
         break;
     case AVAHI_ENTRY_GROUP_UNCOMMITED:
@@ -462,50 +428,130 @@
 {
     std::ostringstream keyBuilder;
     std::string key;
-    std::string type       = GetFullType(service.mType, service.mProtocol);
-    CHIP_ERROR error       = CHIP_NO_ERROR;
-    AvahiStringList * text = nullptr;
+    std::string type = GetFullType(service.mType, service.mProtocol);
+    std::string matterHostname;
+    CHIP_ERROR error          = CHIP_NO_ERROR;
+    AvahiStringList * text    = nullptr;
+    AvahiEntryGroup * group   = nullptr;
+    const char * mainHostname = nullptr;
     AvahiIfIndex interface =
         service.mInterface.IsPresent() ? static_cast<AvahiIfIndex>(service.mInterface.GetPlatformInterface()) : AVAHI_IF_UNSPEC;
+    AvahiProtocol protocol = ToAvahiProtocol(service.mAddressType);
 
     keyBuilder << service.mName << "." << type << service.mPort << "." << interface;
     key = keyBuilder.str();
     ChipLogProgress(DeviceLayer, "PublishService %s", key.c_str());
-
-    if (mPublishedServices.find(key) == mPublishedServices.end())
+    auto publishedgroups_it = mPublishedGroups.find(key);
+    if (publishedgroups_it != mPublishedGroups.end())
     {
-        SuccessOrExit(error = MakeAvahiStringListFromTextEntries(service.mTextEntries, service.mTextEntrySize, &text));
-
-        mPublishedServices.emplace(key);
-        VerifyOrExit(avahi_entry_group_add_service_strlst(mGroup, interface, ToAvahiProtocol(service.mAddressType),
-                                                          static_cast<AvahiPublishFlags>(0), service.mName, type.c_str(), nullptr,
-                                                          nullptr, service.mPort, text) == 0,
-                     error = CHIP_ERROR_INTERNAL);
-        for (size_t i = 0; i < service.mSubTypeSize; i++)
+        // same service was already published, we need to de-publish it first
+        int avahiRet = avahi_entry_group_free(publishedgroups_it->second);
+        if (avahiRet != AVAHI_OK)
         {
-            std::ostringstream sstream;
-
-            sstream << service.mSubTypes[i] << "._sub." << type;
-
-            VerifyOrExit(avahi_entry_group_add_service_subtype(mGroup, interface, ToAvahiProtocol(service.mAddressType),
-                                                               static_cast<AvahiPublishFlags>(0), service.mName, type.c_str(),
-                                                               nullptr, sstream.str().c_str()) == 0,
-                         error = CHIP_ERROR_INTERNAL);
+            ChipLogError(DeviceLayer, "Cannot remove avahi group: %s", avahi_strerror(avahiRet));
+            ExitNow(error = CHIP_ERROR_INTERNAL);
         }
+        mPublishedGroups.erase(publishedgroups_it);
+    }
+
+    // create fresh group
+    group = avahi_entry_group_new(mClient, HandleGroupState, this);
+    VerifyOrExit(group != nullptr, error = CHIP_ERROR_INTERNAL);
+
+    // establish the host name (separately from avahi's default host name that the platform might have,
+    // unless it matches the matter hostname)
+    mainHostname = avahi_client_get_host_name(mClient);
+    if (strcmp(mainHostname, service.mHostName) == 0)
+    {
+        // main host name is correct, we can use it
+        matterHostname = std::string(mainHostname) + ".local";
     }
     else
     {
-        SuccessOrExit(error = MakeAvahiStringListFromTextEntries(service.mTextEntries, service.mTextEntrySize, &text));
-
-        VerifyOrExit(avahi_entry_group_update_service_txt_strlst(mGroup, interface, ToAvahiProtocol(service.mAddressType),
-                                                                 static_cast<AvahiPublishFlags>(0), service.mName, type.c_str(),
-                                                                 nullptr, text) == 0,
-                     error = CHIP_ERROR_INTERNAL);
+        // we need to establish a matter hostname separately from the platform's default hostname
+        char b[chip::Inet::IPAddress::kMaxStringLength];
+        SuccessOrExit(error = service.mInterface.GetInterfaceName(b, chip::Inet::IPAddress::kMaxStringLength));
+        ChipLogDetail(DeviceLayer, "Using addresses from interface id=%d name=%s", service.mInterface.GetPlatformInterface(), b);
+        matterHostname = std::string(service.mHostName) + ".local";
+        // find addresses to publish
+        for (chip::Inet::InterfaceAddressIterator addr_it; addr_it.HasCurrent(); addr_it.Next())
+        {
+            // only specific interface?
+            if (service.mInterface.IsPresent() && addr_it.GetInterfaceId() != service.mInterface)
+            {
+                continue;
+            }
+            if (addr_it.IsUp())
+            {
+                if (addr_it.IsLoopback())
+                {
+                    // do not advertise loopback interface addresses
+                    continue;
+                }
+                chip::Inet::IPAddress addr;
+                if ((addr_it.GetAddress(addr) == CHIP_NO_ERROR) &&
+                    ((service.mAddressType == chip::Inet::IPAddressType::kAny) ||
+                     (addr.IsIPv6() && service.mAddressType == chip::Inet::IPAddressType::kIPv6)
+#if INET_CONFIG_ENABLE_IPV4
+                     || (addr.IsIPv4() && service.mAddressType == chip::Inet::IPAddressType::kIPv4)
+#endif
+                         ))
+                {
+                    VerifyOrExit(addr.ToString(b) != nullptr, error = CHIP_ERROR_INTERNAL);
+                    AvahiAddress a;
+                    VerifyOrExit(avahi_address_parse(b, AVAHI_PROTO_UNSPEC, &a) != nullptr, error = CHIP_ERROR_INTERNAL);
+                    AvahiIfIndex thisinterface = static_cast<AvahiIfIndex>(addr_it.GetInterfaceId().GetPlatformInterface());
+                    // Note: NO_REVERSE publish flag is needed because otherwise we can't have more than one hostname
+                    //   for reverse resolving IP addresses back to hostnames
+                    VerifyOrExit(avahi_entry_group_add_address(group,                        // group
+                                                               thisinterface,                // interface
+                                                               ToAvahiProtocol(addr.Type()), // protocol
+                                                               AVAHI_PUBLISH_NO_REVERSE,     // publish flags
+                                                               matterHostname.c_str(),       // hostname
+                                                               &a                            // address
+                                                               ) == 0,
+                                 error = CHIP_ERROR_INTERNAL);
+                }
+            }
+        }
     }
 
-    VerifyOrExit(avahi_entry_group_commit(mGroup) == 0, error = CHIP_ERROR_INTERNAL);
+    // create the service
+    SuccessOrExit(error = MakeAvahiStringListFromTextEntries(service.mTextEntries, service.mTextEntrySize, &text));
+
+    VerifyOrExit(avahi_entry_group_add_service_strlst(group, interface, protocol,        // group, interface, protocol
+                                                      static_cast<AvahiPublishFlags>(0), // publish flags
+                                                      service.mName,                     // service name
+                                                      type.c_str(),                      // type
+                                                      nullptr,                           // domain
+                                                      matterHostname.c_str(),            // host
+                                                      service.mPort,                     // port
+                                                      text) == 0,                        // TXT records StringList
+                 error = CHIP_ERROR_INTERNAL);
+
+    // add the subtypes
+    for (size_t i = 0; i < service.mSubTypeSize; i++)
+    {
+        std::ostringstream sstream;
+
+        sstream << service.mSubTypes[i] << "._sub." << type;
+
+        VerifyOrExit(avahi_entry_group_add_service_subtype(group, interface, protocol, static_cast<AvahiPublishFlags>(0),
+                                                           service.mName, type.c_str(), nullptr, sstream.str().c_str()) == 0,
+                     error = CHIP_ERROR_INTERNAL);
+    }
+    VerifyOrExit(avahi_entry_group_commit(group) == 0, error = CHIP_ERROR_INTERNAL);
+
+    // group is now published, pass it to the service map
+    mPublishedGroups[key] = group;
+    group                 = nullptr;
 
 exit:
+    if (group != nullptr)
+    {
+        avahi_entry_group_free(group);
+    }
+
     if (text != nullptr)
     {
         avahi_string_list_free(text);
@@ -521,6 +567,8 @@
     }
     else
     {
+        ChipLogError(DeviceLayer, "PublishService failed: %s",
+                     mClient ? avahi_strerror(avahi_client_errno(mClient)) : "no mClient");
         callback(context, nullptr, nullptr, error);
     }
 
@@ -530,12 +578,19 @@
 CHIP_ERROR MdnsAvahi::StopPublish()
 {
     CHIP_ERROR error = CHIP_NO_ERROR;
-    mPublishedServices.clear();
-    if (mGroup)
+    for (const auto & group : mPublishedGroups)
     {
-        VerifyOrExit(avahi_entry_group_reset(mGroup) == 0, error = CHIP_ERROR_INTERNAL);
+        if (group.second)
+        {
+            int avahiRet = avahi_entry_group_free(group.second);
+            if (avahiRet != AVAHI_OK)
+            {
+                ChipLogError(DeviceLayer, "Error freeing avahi group: %s", avahi_strerror(avahiRet));
+                error = CHIP_ERROR_INTERNAL;
+            }
+        }
     }
-exit:
+    mPublishedGroups.clear();
     return error;
 }
 
diff --git a/src/platform/Linux/DnssdImpl.h b/src/platform/Linux/DnssdImpl.h
index e711180..9bad9e8 100644
--- a/src/platform/Linux/DnssdImpl.h
+++ b/src/platform/Linux/DnssdImpl.h
@@ -23,7 +23,6 @@
 #include <chrono>
 #include <map>
 #include <memory>
-#include <set>
 #include <string>
 #include <vector>
 
@@ -142,7 +141,7 @@
         uint8_t mAttempts = 0;
     };
 
-    MdnsAvahi() : mClient(nullptr), mGroup(nullptr) {}
+    MdnsAvahi() : mClient(nullptr) {}
     static MdnsAvahi sInstance;
 
     static void HandleClientState(AvahiClient * client, AvahiClientState state, void * context);
@@ -163,9 +162,8 @@
     DnssdAsyncReturnCallback mErrorCallback;
     void * mAsyncReturnContext;
 
-    std::set<std::string> mPublishedServices;
     AvahiClient * mClient;
-    AvahiEntryGroup * mGroup;
+    std::map<std::string, AvahiEntryGroup *> mPublishedGroups;
     Poller mPoller;
 };