| 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 |