From 27a67bd05fb0b01e905cdb3cae7cc6241d293656 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 8 Jun 2026 08:59:03 -0400 Subject: [PATCH 1/2] Add MAC address to device info - Auto-detects primary interface on ESP32 - Attempts to make a best guess on host builds - Overridable by the user config - Makes all device information fields optional in SendspinClientconfig --- CMakeLists.txt | 2 +- cmake/sources.cmake | 2 + docs/integration-guide.md | 13 +-- include/sendspin/config.h | 20 +++-- src/client.cpp | 5 ++ src/esp/network_info.cpp | 76 ++++++++++++++++++ src/host/network_info.cpp | 156 ++++++++++++++++++++++++++++++++++++ src/platform/network_info.h | 41 ++++++++++ src/protocol.cpp | 3 + src/protocol_messages.h | 1 + tests/CMakeLists.txt | 1 + tests/test_network_info.cpp | 32 ++++++++ tests/test_protocol.cpp | 40 +++++++++ 13 files changed, 380 insertions(+), 12 deletions(-) create mode 100644 src/esp/network_info.cpp create mode 100644 src/host/network_info.cpp create mode 100644 src/platform/network_info.h create mode 100644 tests/test_network_info.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bd1962c..c235bea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,7 @@ if(ESP_IDF_BUILD) # Assemble source list from core + enabled roles (driven by Kconfig) set(SENDSPIN_ALL_SOURCES ${SENDSPIN_CORE_SOURCES} ${SENDSPIN_ESP_SOURCES}) - set(SENDSPIN_REQUIRES esp_http_server esp_timer esp_ringbuf pthread mbedtls bblanchon__arduinojson) + set(SENDSPIN_REQUIRES esp_http_server esp_netif esp_timer esp_ringbuf pthread mbedtls bblanchon__arduinojson) set(SENDSPIN_COMPILE_DEFS "") if(CONFIG_SENDSPIN_ENABLE_PLAYER) diff --git a/cmake/sources.cmake b/cmake/sources.cmake index e437d78..d32f30c 100644 --- a/cmake/sources.cmake +++ b/cmake/sources.cmake @@ -71,6 +71,7 @@ function(sendspin_get_sources BASE_DIR) ${BASE_DIR}/src/esp/server_connection.cpp ${BASE_DIR}/src/esp/client_connection.cpp ${BASE_DIR}/src/esp/ws_server.cpp + ${BASE_DIR}/src/esp/network_info.cpp PARENT_SCOPE ) @@ -80,6 +81,7 @@ function(sendspin_get_sources BASE_DIR) ${BASE_DIR}/src/host/ws_server.cpp ${BASE_DIR}/src/host/server_connection.cpp ${BASE_DIR}/src/host/client_connection.cpp + ${BASE_DIR}/src/host/network_info.cpp PARENT_SCOPE ) diff --git a/docs/integration-guide.md b/docs/integration-guide.md index 9921139..4803548 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -42,9 +42,9 @@ SendspinClient::set_log_level(LogLevel::INFO); SendspinClientConfig config; config.client_id = "my-device-mac-addr"; // Unique identifier (e.g., MAC address) config.name = "Living Room Speaker"; // Friendly display name -config.product_name = "My Speaker"; // Device product name -config.manufacturer = "My Company"; // Manufacturer name -config.software_version = "1.0.0"; // Software version string +config.product_name = "My Speaker"; // Device product name (optional) +config.manufacturer = "My Company"; // Manufacturer name (optional) +config.software_version = "1.0.0"; // Software version string (optional) SendspinClient client(std::move(config)); ``` @@ -692,9 +692,10 @@ Main client configuration passed to the `SendspinClient` constructor. |---|---|---|---| | `client_id` | `std::string` | — | Unique client identifier (e.g., MAC address) | | `name` | `std::string` | — | Friendly display name shown in the Sendspin UI | -| `product_name` | `std::string` | — | Device product name | -| `manufacturer` | `std::string` | — | Manufacturer name (e.g., `"ESPHome"`) | -| `software_version` | `std::string` | — | Software version string | +| `product_name` | `std::optional` | unset | Device product name; sent in `client/hello` only when set | +| `manufacturer` | `std::optional` | unset | Manufacturer name (e.g., `"ESPHome"`); sent in `client/hello` only when set | +| `software_version` | `std::optional` | unset | Software version string; sent in `client/hello` only when set | +| `mac_address` | `std::optional` | auto-detected | MAC address of the network interface, lowercase colon-separated (e.g., `"aa:bb:cc:dd:ee:ff"`), sent in `client/hello`. Left unset, the library auto-detects it. ESP-IDF uses the default network interface (Wi-Fi or Ethernet). Host uses a best-effort from the active routable interface. Set explicitly to override (recommended on multi-homed hosts). | | `httpd_psram_stack` | `bool` | `false` | Allocate HTTP server task stack in PSRAM (ESP-IDF only) | | `httpd_priority` | `unsigned` | `17` | FreeRTOS priority for the HTTP server task (ESP-IDF only) | | `websocket_priority` | `unsigned` | `5` | FreeRTOS priority for the WebSocket client task (ESP-IDF only) | diff --git a/include/sendspin/config.h b/include/sendspin/config.h index 9d659e9..241dead 100644 --- a/include/sendspin/config.h +++ b/include/sendspin/config.h @@ -34,11 +34,21 @@ namespace sendspin { /// @brief Configuration for a SendspinClient instance /// Filled in by the platform (e.g., ESPHome) before calling start_server() struct SendspinClientConfig { - std::string client_id; ///< Unique client identifier (e.g., MAC address) - std::string name; ///< Friendly display name - std::string product_name; ///< Device product name - std::string manufacturer; ///< Manufacturer name (e.g., "ESPHome") - std::string software_version; ///< Software version string + std::string client_id; ///< Unique client identifier (e.g., MAC address) + std::string name; ///< Friendly display name + + std::optional product_name{}; ///< Device product name (optional) + std::optional manufacturer{}; ///< Manufacturer name, e.g., "ESPHome" (optional) + std::optional software_version{}; ///< Software version string (optional) + + /// @brief MAC address of the network interface the connection is opened on. + /// Sent in the client/hello device_info object. Must be lowercase colon-separated + /// form (e.g., "aa:bb:cc:dd:ee:ff"). When left unset, the library auto-detects it: + /// from the default network interface (Wi-Fi or Ethernet) on ESP-IDF, and best-effort + /// from the active routable interface on host. Set this explicitly to override the + /// detected value (recommended on multi-homed hosts where detection may pick the wrong + /// interface). + std::optional mac_address{}; bool httpd_psram_stack{false}; ///< Allocate httpd task stack in PSRAM (ESP-IDF only) diff --git a/src/client.cpp b/src/client.cpp index 4288668..674c03b 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -20,6 +20,7 @@ #include "platform/json_arena.h" #include "platform/logging.h" #include "platform/memory.h" +#include "platform/network_info.h" #include "platform/shadow_slot.h" #include "platform/thread_safe_queue.h" #include "platform/time.h" @@ -444,6 +445,10 @@ std::string SendspinClient::build_hello_message() { device_info.product_name = this->config_.product_name; device_info.manufacturer = this->config_.manufacturer; device_info.software_version = this->config_.software_version; + // Use the explicitly configured MAC when provided; otherwise fall back to platform detection + // (reliable on ESP, best-effort on host). Leaves the field absent if neither is available. + device_info.mac_address = + this->config_.mac_address ? this->config_.mac_address : platform_get_interface_mac(); msg.device_info = device_info; msg.version = 1; diff --git a/src/esp/network_info.cpp b/src/esp/network_info.cpp new file mode 100644 index 0000000..05d8821 --- /dev/null +++ b/src/esp/network_info.cpp @@ -0,0 +1,76 @@ +// Copyright 2026 Sendspin Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// @file network_info.cpp +/// @brief ESP-IDF MAC detection: MAC of the default network interface (Wi-Fi or Ethernet) + +#include "platform/network_info.h" + +#include "platform/logging.h" +#include +#include + +#include +#include + +namespace sendspin { + +static const char* const TAG = "sendspin.network_info"; + +namespace { + +/// @brief Formats six MAC octets as lowercase colon-separated text, or nullopt if all zero. +std::optional format_mac(const uint8_t* mac) { + bool all_zero = true; + for (int i = 0; i < 6; i++) { + if (mac[i] != 0) { + all_zero = false; + break; + } + } + if (all_zero) { + return std::nullopt; // emulators/unprovisioned efuse can report a zeroed MAC + } + + char buf[18]; + std::snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); + return std::string(buf); +} + +} // namespace + +std::optional platform_get_interface_mac() { + uint8_t mac[6] = {0}; + + // Prefer the MAC of the default network interface — the one outbound connections route over. + // This resolves to the correct address whether the device is on Wi-Fi or Ethernet. + esp_netif_t* netif = esp_netif_get_default_netif(); + if (netif != nullptr && esp_netif_get_mac(netif, mac) == ESP_OK) { + if (std::optional detected = format_mac(mac); detected.has_value()) { + return detected; + } + } + + // Fallback: factory Wi-Fi STA address from efuse. This does not require esp_wifi/esp_netif to + // be initialized, so it still yields a stable identity if no default interface is up yet. + const esp_err_t err = esp_read_mac(mac, ESP_MAC_WIFI_STA); + if (err != ESP_OK) { + SS_LOGW(TAG, "MAC detection failed: %s", esp_err_to_name(err)); + return std::nullopt; + } + return format_mac(mac); +} + +} // namespace sendspin diff --git a/src/host/network_info.cpp b/src/host/network_info.cpp new file mode 100644 index 0000000..0a5d0e1 --- /dev/null +++ b/src/host/network_info.cpp @@ -0,0 +1,156 @@ +// Copyright 2026 Sendspin Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// @file network_info.cpp +/// @brief Host MAC detection: best-effort lookup of an active interface that carries a routable IP + +#include "platform/network_info.h" + +#include "platform/logging.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#if defined(__APPLE__) +#include // sockaddr_dl, LLADDR +#else +#include // sockaddr_ll +#endif + +namespace sendspin { + +static const char* const TAG = "sendspin.network_info"; + +namespace { + +/// @brief Formats six MAC octets as lowercase colon-separated text, or nullopt if all zero. +std::optional format_mac(const uint8_t* mac) { + bool all_zero = true; + for (int i = 0; i < 6; i++) { + if (mac[i] != 0) { + all_zero = false; + break; + } + } + if (all_zero) { + return std::nullopt; + } + + char buf[18]; + std::snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); + return std::string(buf); +} + +/// @brief True if `sa` is a routable (non-loopback, non-link-local) IPv4/IPv6 address. +bool is_routable_ip(const struct sockaddr* sa) { + if (sa == nullptr) { + return false; + } + if (sa->sa_family == AF_INET) { + const auto* in = reinterpret_cast(sa); + const uint32_t addr = ntohl(in->sin_addr.s_addr); + const uint8_t first = static_cast(addr >> 24); + const uint16_t first_two = static_cast(addr >> 16); + if (first == 127) { + return false; // loopback 127.0.0.0/8 + } + if (first_two == 0xA9FE) { + return false; // link-local 169.254.0.0/16 + } + return true; + } + if (sa->sa_family == AF_INET6) { + const auto* in6 = reinterpret_cast(sa); + if (IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr) || IN6_IS_ADDR_LINKLOCAL(&in6->sin6_addr)) { + return false; + } + return true; + } + return false; +} + +} // namespace + +std::optional platform_get_interface_mac() { + struct ifaddrs* ifaddr = nullptr; + if (getifaddrs(&ifaddr) != 0 || ifaddr == nullptr) { + SS_LOGW(TAG, "getifaddrs failed; cannot auto-detect MAC address"); + return std::nullopt; + } + + // First pass: collect interfaces that carry a routable IP. The MAC of such an interface is far + // more likely to be the one a connection actually uses than an arbitrary up-and-running + // interface (which on desktops includes bridges, VPN/utun, and AWDL links with no real route). + std::set routable_ifaces; + for (struct ifaddrs* ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_name != nullptr && is_routable_ip(ifa->ifa_addr)) { + routable_ifaces.insert(ifa->ifa_name); + } + } + + std::optional result; + for (struct ifaddrs* ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == nullptr || ifa->ifa_name == nullptr) { + continue; + } + // Skip loopback and interfaces that are not up and running. + if ((ifa->ifa_flags & IFF_LOOPBACK) != 0) { + continue; + } + if ((ifa->ifa_flags & (IFF_UP | IFF_RUNNING)) != (IFF_UP | IFF_RUNNING)) { + continue; + } + // Only consider interfaces that also have a routable IP address. + if (routable_ifaces.find(ifa->ifa_name) == routable_ifaces.end()) { + continue; + } + +#if defined(__APPLE__) + if (ifa->ifa_addr->sa_family != AF_LINK) { + continue; + } + auto* sdl = reinterpret_cast(ifa->ifa_addr); + if (sdl->sdl_alen != 6) { + continue; + } + result = format_mac(reinterpret_cast(LLADDR(sdl))); +#else + if (ifa->ifa_addr->sa_family != AF_PACKET) { + continue; + } + auto* sll = reinterpret_cast(ifa->ifa_addr); + if (sll->sll_halen != 6) { + continue; + } + result = format_mac(sll->sll_addr); +#endif + if (result.has_value()) { + SS_LOGD(TAG, "auto-detected MAC %s on interface %s", result->c_str(), ifa->ifa_name); + break; + } + } + + freeifaddrs(ifaddr); + return result; +} + +} // namespace sendspin diff --git a/src/platform/network_info.h b/src/platform/network_info.h new file mode 100644 index 0000000..92f551d --- /dev/null +++ b/src/platform/network_info.h @@ -0,0 +1,41 @@ +// Copyright 2026 Sendspin Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// @file network_info.h +/// @brief Platform-abstracted detection of the local network interface MAC address + +#pragma once + +#include +#include + +namespace sendspin { + +/// @brief Best-effort lookup of the local MAC address used for Sendspin connections. +/// +/// Returned as lowercase colon-separated form (e.g., "aa:bb:cc:dd:ee:ff"), suitable +/// for the `client/hello` `device_info.mac_address` field. Returns std::nullopt when no +/// suitable interface can be determined. +/// +/// Platform behaviour: +/// - ESP-IDF: returns the MAC of the default network interface, so it resolves correctly whether +/// the device is on Wi-Fi or Ethernet. Falls back to the factory Wi-Fi STA MAC if no default +/// interface is up yet. +/// - Host: returns the MAC of the first active non-loopback interface that also carries a routable +/// (non-link-local) IP address. This is a heuristic (the "primary" interface), not necessarily +/// the interface a given connection is bound to; on multi-homed hosts it may differ. Prefer +/// setting SendspinClientConfig::mac_address explicitly when the exact interface matters. +std::optional platform_get_interface_mac(); + +} // namespace sendspin diff --git a/src/protocol.cpp b/src/protocol.cpp index b2b6355..6d583ee 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -742,6 +742,9 @@ std::string format_client_hello_message(const ClientHelloMessage* msg) { if (info.software_version.has_value()) { root["payload"]["device_info"]["software_version"] = info.software_version.value(); } + if (info.mac_address.has_value()) { + root["payload"]["device_info"]["mac_address"] = info.mac_address.value(); + } } root["payload"]["version"] = msg->version; JsonArray supported_roles_list = root["payload"]["supported_roles"].to(); diff --git a/src/protocol_messages.h b/src/protocol_messages.h index ca6a900..eda51c4 100644 --- a/src/protocol_messages.h +++ b/src/protocol_messages.h @@ -207,6 +207,7 @@ struct DeviceInfoObject { std::optional product_name{}; std::optional manufacturer{}; std::optional software_version{}; + std::optional mac_address{}; }; // --- player_role.h --- diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 46952e8..d419e90 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,6 +23,7 @@ add_executable(sendspin_tests test_audio_stream_info.cpp test_time_filter.cpp test_protocol.cpp + test_network_info.cpp ) # Reach the library's private headers (protocol_messages.h, time_filter.h, ...). diff --git a/tests/test_network_info.cpp b/tests/test_network_info.cpp new file mode 100644 index 0000000..9c2c372 --- /dev/null +++ b/tests/test_network_info.cpp @@ -0,0 +1,32 @@ +// Copyright 2026 Sendspin Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "platform/network_info.h" +#include + +#include + +using namespace sendspin; + +// Host MAC detection is environment-dependent (it may legitimately find no routable interface in a +// sandboxed CI runner), so this asserts the contract rather than a specific value: the result is +// either absent or a well-formed lowercase colon-separated MAC. +TEST(NetworkInfo, InterfaceMacIsWellFormedOrAbsent) { + const std::optional mac = platform_get_interface_mac(); + if (!mac.has_value()) { + GTEST_SKIP() << "no routable interface detected in this environment"; + } + const std::regex mac_re("^[0-9a-f]{2}(:[0-9a-f]{2}){5}$"); + EXPECT_TRUE(std::regex_match(*mac, mac_re)) << "malformed MAC: " << *mac; +} diff --git a/tests/test_protocol.cpp b/tests/test_protocol.cpp index 79501a5..a9bcb02 100644 --- a/tests/test_protocol.cpp +++ b/tests/test_protocol.cpp @@ -308,3 +308,43 @@ TEST(Protocol, FormatClientCommandNoArgs) { EXPECT_FALSE(doc["payload"]["controller"]["volume"].is()); EXPECT_FALSE(doc["payload"]["controller"]["mute"].is()); } + +// Every device_info identity field (product_name, manufacturer, software_version, mac_address) +// is optional and serialized only when present. +TEST(Protocol, FormatClientHelloDeviceInfoFieldsPresent) { + ClientHelloMessage msg; + msg.client_id = "abc"; + msg.name = "Speaker"; + msg.version = 1; + DeviceInfoObject info{}; + info.product_name = "Speaker Pro"; + info.manufacturer = "ESPHome"; + info.software_version = "1.2.3"; + info.mac_address = "aa:bb:cc:dd:ee:ff"; + msg.device_info = info; + + const std::string out = format_client_hello_message(&msg); + + JsonDocument doc; + ASSERT_FALSE(deserializeJson(doc, out)); + EXPECT_STREQ(doc["payload"]["device_info"]["product_name"], "Speaker Pro"); + EXPECT_STREQ(doc["payload"]["device_info"]["manufacturer"], "ESPHome"); + EXPECT_STREQ(doc["payload"]["device_info"]["software_version"], "1.2.3"); + EXPECT_STREQ(doc["payload"]["device_info"]["mac_address"], "aa:bb:cc:dd:ee:ff"); +} + +// Unset optional identity fields must not emit their keys. +TEST(Protocol, FormatClientHelloDeviceInfoFieldsAbsent) { + ClientHelloMessage msg; + msg.client_id = "abc"; + msg.name = "Speaker"; + msg.version = 1; + msg.device_info = DeviceInfoObject{}; + + JsonDocument doc; + ASSERT_FALSE(deserializeJson(doc, format_client_hello_message(&msg))); + EXPECT_FALSE(doc["payload"]["device_info"]["product_name"].is()); + EXPECT_FALSE(doc["payload"]["device_info"]["manufacturer"].is()); + EXPECT_FALSE(doc["payload"]["device_info"]["software_version"].is()); + EXPECT_FALSE(doc["payload"]["device_info"]["mac_address"].is()); +} From ff43f83be62580b6e1743e36ea83d548ad698bac Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 8 Jun 2026 09:09:24 -0400 Subject: [PATCH 2/2] Clang-tidy fixes --- src/esp/network_info.cpp | 5 ++++- src/host/network_info.cpp | 18 +++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/esp/network_info.cpp b/src/esp/network_info.cpp index 05d8821..75cf9f7 100644 --- a/src/esp/network_info.cpp +++ b/src/esp/network_info.cpp @@ -30,6 +30,9 @@ static const char* const TAG = "sendspin.network_info"; namespace { +/// @brief Size of a formatted MAC string buffer: "aa:bb:cc:dd:ee:ff" plus null terminator. +constexpr size_t MAC_STR_BUF_SIZE = 18; + /// @brief Formats six MAC octets as lowercase colon-separated text, or nullopt if all zero. std::optional format_mac(const uint8_t* mac) { bool all_zero = true; @@ -43,7 +46,7 @@ std::optional format_mac(const uint8_t* mac) { return std::nullopt; // emulators/unprovisioned efuse can report a zeroed MAC } - char buf[18]; + char buf[MAC_STR_BUF_SIZE]; std::snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); return std::string(buf); diff --git a/src/host/network_info.cpp b/src/host/network_info.cpp index 0a5d0e1..c66b5d2 100644 --- a/src/host/network_info.cpp +++ b/src/host/network_info.cpp @@ -41,6 +41,13 @@ static const char* const TAG = "sendspin.network_info"; namespace { +/// @brief Size of a formatted MAC string buffer: "aa:bb:cc:dd:ee:ff" plus null terminator. +constexpr size_t MAC_STR_BUF_SIZE = 18; +/// @brief First octet of the IPv4 loopback range 127.0.0.0/8. +constexpr uint8_t IPV4_LOOPBACK_FIRST_OCTET = 127; +/// @brief High 16 bits of the IPv4 link-local range 169.254.0.0/16. +constexpr uint16_t IPV4_LINK_LOCAL_PREFIX = 0xA9FE; + /// @brief Formats six MAC octets as lowercase colon-separated text, or nullopt if all zero. std::optional format_mac(const uint8_t* mac) { bool all_zero = true; @@ -54,7 +61,7 @@ std::optional format_mac(const uint8_t* mac) { return std::nullopt; } - char buf[18]; + char buf[MAC_STR_BUF_SIZE]; std::snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); return std::string(buf); @@ -70,20 +77,17 @@ bool is_routable_ip(const struct sockaddr* sa) { const uint32_t addr = ntohl(in->sin_addr.s_addr); const uint8_t first = static_cast(addr >> 24); const uint16_t first_two = static_cast(addr >> 16); - if (first == 127) { + if (first == IPV4_LOOPBACK_FIRST_OCTET) { return false; // loopback 127.0.0.0/8 } - if (first_two == 0xA9FE) { + if (first_two == IPV4_LINK_LOCAL_PREFIX) { return false; // link-local 169.254.0.0/16 } return true; } if (sa->sa_family == AF_INET6) { const auto* in6 = reinterpret_cast(sa); - if (IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr) || IN6_IS_ADDR_LINKLOCAL(&in6->sin6_addr)) { - return false; - } - return true; + return !IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr) && !IN6_IS_ADDR_LINKLOCAL(&in6->sin6_addr); } return false; }