GCC Code Coverage Report


Directory: ./
File: MDNSManager.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 64 80 80.0%
Functions: 7 9 77.8%
Branches: 64 120 53.3%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2026 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 // The parts of mDNS that do not depend on which library is doing the talking:
14 // the service name, what counts as a newly discovered host, and what is done
15 // with one. The library-specific halves are in MDNSBackendAvahi.cpp and
16 // MDNSBackendBonjour.cpp; MDNSBackend.h picks between them.
17
18 #include "fpp-pch.h"
19
20 #include "Warnings.h" // WarningHolder -- needed directly for NOPCH builds
21
22 #include "MDNSBackend.h"
23 #include "MDNSManager.h"
24
25 #include "EPollManager.h"
26 #include "MultiSync.h"
27 #include "common.h"
28 #include "log.h"
29 #include "ping.h"
30
31 #include <net/if.h>
32 #include <sys/time.h>
33 #include <cerrno>
34 #include <cstdio>
35 #include <cstring>
36 #include <dirent.h>
37 #include <unistd.h>
38 #include <vector>
39 #ifdef PLATFORM_OSX
40 #include <net/if_dl.h>
41 #include <ifaddrs.h>
42 #endif
43
44 MDNSManager MDNSManager::INSTANCE;
45
46 2346 MDNSManager::MDNSManager() = default;
47
48 458 MDNSManager::~MDNSManager() {
49 458 Cleanup();
50 458 }
51
52 // The MAC of the first real interface, in the form the _wled._tcp "mac=" TXT
53 // record wants: lowercase hex, no separators. Empty when none could be read,
54 // in which case the record is published empty rather than omitted -- WLED apps
55 // key off the field being there. Both backends advertise the same record, and
56 // finding the address is a per-platform job rather than a per-library one, so
57 // it lives here rather than in either backend.
58 19 std::string MDNSPrimaryMacAddress() {
59 #ifdef PLATFORM_OSX
60 // There is no /sys on macOS, and the interface list is long: a Mac readily
61 // has a dozen link-layer interfaces (awdl, llw, anpi, bridge, ap, the
62 // Thunderbolt ens) that are "up" and carry a MAC while being no use to
63 // anyone. Taking the first of those gets a placeholder such as
64 // 02:00:00:00:00:00. The one worth advertising is the interface that
65 // actually holds an address, since that is the one a WLED app will talk
66 // to, so collect those names first and match the link layer to them.
67 std::set<std::string> addressed;
68 struct ifaddrs* ifap = nullptr;
69 if (getifaddrs(&ifap) != 0) {
70 return "";
71 }
72 for (struct ifaddrs* ifa = ifap; ifa; ifa = ifa->ifa_next) {
73 if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET || !ifa->ifa_name) {
74 continue;
75 }
76 if ((ifa->ifa_flags & IFF_LOOPBACK) || !(ifa->ifa_flags & IFF_UP)) {
77 continue;
78 }
79 addressed.insert(ifa->ifa_name);
80 }
81
82 std::string mac;
83 for (struct ifaddrs* ifa = ifap; ifa && mac.empty(); ifa = ifa->ifa_next) {
84 if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_LINK || !ifa->ifa_name) {
85 continue;
86 }
87 if (addressed.find(ifa->ifa_name) == addressed.end()) {
88 continue;
89 }
90 auto* sdl = (struct sockaddr_dl*)ifa->ifa_addr;
91 if (sdl->sdl_alen != 6) {
92 continue;
93 }
94 const unsigned char* a = (const unsigned char*)LLADDR(sdl);
95 if (!(a[0] | a[1] | a[2] | a[3] | a[4] | a[5])) {
96 continue;
97 }
98 // macOS hides hardware addresses from a process without the
99 // entitlement for them and hands back 02:00:00:00:00:00 instead --
100 // from getifaddrs() and from sysctl(NET_RT_IFLIST) alike, for every
101 // interface (/sbin/ifconfig still prints the real one, which makes
102 // this look like a bug in the code rather than the policy it is).
103 // Reporting it would give every Mac running FPP the same identity in
104 // the WLED apps that read this field, which is worse than reporting
105 // none: an absent value is already what Linux publishes when it cannot
106 // find an address. If a real one is ever wanted here it needs IOKit
107 // (IOEthernetInterface/IOMACAddress), not the socket APIs.
108 static const unsigned char MASKED[6] = { 0x02, 0, 0, 0, 0, 0 };
109 if (memcmp(a, MASKED, 6) == 0) {
110 continue;
111 }
112 char buf[13];
113 snprintf(buf, sizeof(buf), "%02x%02x%02x%02x%02x%02x",
114 a[0], a[1], a[2], a[3], a[4], a[5]);
115 mac = buf;
116 }
117 freeifaddrs(ifap);
118 if (mac.empty()) {
119 LogDebug(VB_SYNC, "MDNS: no hardware address available on this platform; advertising none\n");
120 }
121 return mac;
122 #else
123 // Pick the first non-loopback interface with a real MAC. Modern Debian
124 // uses predictable names like "enxXX..." and "wlxXX...", so enumerate
125 // /sys/class/net rather than guessing at them.
126 19 std::string mac;
127
2/4
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
19 if (DIR* d = opendir("/sys/class/net")) {
128 struct dirent* e;
129
2/4
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 76 times.
✗ Branch 3 not taken.
76 while ((e = readdir(d)) != nullptr) {
130
12/18
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 38 times.
✓ Branch 2 taken 38 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 38 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 19 times.
✓ Branch 7 taken 19 times.
✓ Branch 8 taken 38 times.
✓ Branch 9 taken 38 times.
✓ Branch 10 taken 38 times.
✓ Branch 11 taken 38 times.
✓ Branch 12 taken 57 times.
✓ Branch 13 taken 19 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
152 if (e->d_name[0] == '.' || std::string(e->d_name) == "lo") {
131 57 continue;
132 }
133 char path[256];
134 19 snprintf(path, sizeof(path), "/sys/class/net/%s/address", e->d_name);
135
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 FILE* f = fopen(path, "r");
136
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (!f) {
137 ✗ continue;
138 }
139 19 char buf[32] = {};
140 19 bool got = false;
141
2/4
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
19 if (fgets(buf, sizeof(buf), f)) {
142
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 std::string s(buf);
143
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 s.erase(s.find_last_not_of(" \t\r\n") + 1);
144
4/8
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 19 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 19 times.
✗ Branch 7 not taken.
19 if (!s.empty() && s != "00:00:00:00:00:00") {
145
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 mac = s;
146 19 got = true;
147 }
148 19 }
149
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 fclose(f);
150
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 if (got) {
151 19 break;
152 }
153 }
154
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 closedir(d);
155 }
156 19 std::string flat;
157
2/2
✓ Branch 0 taken 323 times.
✓ Branch 1 taken 19 times.
342 for (char c : mac) {
158
2/2
✓ Branch 0 taken 228 times.
✓ Branch 1 taken 95 times.
323 if (c != ':') {
159
1/2
✓ Branch 0 taken 228 times.
✗ Branch 1 not taken.
228 flat.push_back(std::tolower(c));
160 }
161 }
162 19 return flat;
163 #endif
164 19 }
165
166 // ── MDNSManager implementation ──────────────────────────────────────────
167
168 1885 void MDNSManager::Initialize(std::map<int, std::function<bool(int)>>& callbacks) {
169
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
1885 if (m_running)
170 ✗ return;
171
172 // Initialize service name from hostname
173 char hostname[256];
174
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
1885 if (gethostname(hostname, sizeof(hostname)) != 0) {
175 ✗ strncpy(hostname, "fppd", sizeof(hostname) - 1);
176 ✗ hostname[sizeof(hostname) - 1] = '\0';
177 }
178
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 m_serviceName = hostname;
179
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 m_baseServiceName = m_serviceName;
180
181
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 StartBackend();
182 }
183
184 2325 void MDNSManager::Cleanup() {
185 2325 StopBackend();
186 2325 }
187
188 #if !HAVE_AVAHI && !HAVE_BONJOUR
189 // Neither library is present. Discovery still works through the MultiSync
190 // ping and the HTTP scan; only the mDNS half is missing. These are the same
191 // entry points the real backends define, so the link is satisfied either way.
192 void MDNSManager::StartBackend() {
193 LogWarn(VB_SYNC, "No mDNS library available (Avahi or Bonjour); MDNS disabled\n");
194 }
195 void MDNSManager::StopBackend() { m_running = false; }
196 void MDNSManager::SetServiceBrowser(void* sb) {}
197 void MDNSManager::SetWLEDServiceBrowser(void* sb) {}
198 void MDNSManager::RegisterService(void* client) {}
199 void MDNSManager::PickAlternativeServiceName() {}
200 void MDNSManager::WithdrawService() {}
201 void MDNSManager::ResetServiceName() {}
202 void MDNSManager::ScheduleServiceNameRetry() {}
203 void MDNSManager::RetryBaseServiceName() {}
204 void MDNSManager::CancelServiceNameRetry() {}
205 void MDNSManager::NoteClientRunning() {}
206 void MDNSManager::NoteClientDown() {}
207 #endif
208
209 ✗ int MDNSManager::registerCallback(std::function<void(const std::string&)>&& callback) {
210 ✗ std::unique_lock<std::recursive_mutex> lk(m_callbackLock);
211 ✗ int id = m_nextCallbackId++;
212 ✗ m_callbacks[id] = std::move(callback);
213 ✗ return id;
214 ✗ }
215
216 ✗ void MDNSManager::removeCallback(int id) {
217 ✗ std::unique_lock<std::recursive_mutex> lk(m_callbackLock);
218 ✗ m_callbacks.erase(id);
219 ✗ }
220
221 75 void MDNSManager::HandleResolveIP(const std::string& ip, bool isWled) {
222 75 bool isNew = false;
223 {
224
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 std::unique_lock<std::recursive_mutex> lk(m_callbackLock);
225 // Anything on the LAN can announce addresses, one after another, and
226 // each new one costs a ping and an HTTP probe; past far more devices
227 // than a network has, an unseen address is left to other discovery.
228
6/8
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 33 times.
✓ Branch 5 taken 42 times.
✓ Branch 6 taken 33 times.
✓ Branch 7 taken 42 times.
75 if (m_knownHosts.size() < 4096 && m_knownHosts.insert(ip).second) {
229 33 isNew = true;
230 }
231 75 }
232
233
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 33 times.
75 if (!isNew)
234 49 return;
235
236 // Avahi resolves this host's own advertisement to 127.0.0.1, so we see
237 // ourselves here on every browse. MultiSync rejects loopback anyway; skip
238 // it before spending a ping and a detached HTTP probe on it.
239
3/4
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 26 times.
33 if (IsLoopbackAddress(ip)) {
240
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 LogDebug(VB_SYNC, "MDNS ignoring loopback resolve for %s\n", ip.c_str());
241 7 return;
242 }
243
244
3/4
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 LogDebug(VB_SYNC, "MDNS discovered new host %s%s\n", ip.c_str(), isWled ? " (wled)" : "");
245
246 // ping and notify MultiSync. The FPP discover-ping is a harmless no-op for a
247 // pure WLED node (it doesn't speak the protocol), and FPP nodes advertise
248 // _wled._tcp too, so we always send it regardless of which browser found the
249 // host - that way an FPP node still gets discovered if the _wled._tcp
250 // resolve happens to win the race against _fppd._udp.
251
3/6
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26 times.
✗ Branch 5 not taken.
26 PingManager::INSTANCE.ping(ip, 1000, [ip](int result) {
252 26 LogDebug(VB_SYNC, "MDNS ping %s result %d\n", ip.c_str(), result);
253 26 });
254
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 MultiSync::INSTANCE.PingSingleRemote(ip.c_str(), 1);
255
256 // Always probe over HTTP as well. The FPP discover-ping does not carry the
257 // remote's System UUID, so without this probe an FPP peer's UUID stays empty
258 // in the systems list - HTTP controller detection populates it (needed for
259 // device identification). This runs only once per newly discovered host.
260 // Harmless for a pure WLED node; detection just identifies it as WLED.
261 // PingSingleRemoteViaHTTP returns as soon as the request is queued and calls
262 // UpdateSystem itself from the completion, so there is nothing to keep off
263 // this thread and no detached thread needed to do it.
264
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 MultiSync::INSTANCE.PingSingleRemoteViaHTTP(ip);
265
266 // Take a snapshot of callbacks while holding the lock, then invoke them without the lock
267 // to avoid deadlock/iterator invalidation if callbacks modify m_callbacks
268 26 std::vector<std::function<void(const std::string&)>> callbacksSnapshot;
269 {
270
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 std::unique_lock<std::recursive_mutex> lk(m_callbackLock);
271
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 for (auto const& pair : m_callbacks) {
272 ✗ callbacksSnapshot.push_back(pair.second);
273 }
274 26 }
275
276 // Invoke callbacks without holding the lock
277
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 for (auto const& callback : callbacksSnapshot) {
278 ✗ callback(ip);
279 }
280 26 }
281