changeset 423:0a5896d64aaa

wip, network discovery tested
author Louis Opter <kalessin@kalessin.fr>
date Wed, 06 Jan 2016 11:59:33 +0100
parents e262a6cceb20
children ebee80bcca5a
files network_discovery.patch
diffstat 1 files changed, 1141 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/network_discovery.patch	Tue Jan 05 16:36:04 2016 +0100
+++ b/network_discovery.patch	Wed Jan 06 11:59:33 2016 +0100
@@ -1,5 +1,5 @@
 # HG changeset patch
-# Parent  722eaeb15d6f8eeffa3750987c72e9e1c0146215
+# Parent  1d70e191bfd598d0eaf23b87368acb704e764d57
 Properly broadcast LIFX discovery packets on all networks (closes GH-2)
 
 It does solve the issue but only is half the solution I'd like to have.
@@ -49,11 +49,11 @@
  #include <stdarg.h>
  #include <stdbool.h>
  #include <stdint.h>
-@@ -51,16 +54,35 @@
+@@ -51,16 +54,37 @@
  };
  
  static bool
-+lgtd_lifx_broadcast_send_packet(void *pkt,
++lgtd_lifx_broadcast_send_packet(const void *pkt,
 +                                int pkt_sz,
 +                                const struct sockaddr *addr,
 +                                ev_socklen_t addrlen)
@@ -68,7 +68,9 @@
 +    } while (nbytes == -1 && EVUTIL_SOCKET_ERROR() == EINTR);
 +
 +    if (nbytes != pkt_sz) {
-+        lgtd_warn("couldn't broadcast LIFX discovery packet on %s", addr_str);
++        void (*warnfn)(const char *fmt, ...) = nbytes == -1 ?
++            lgtd_warn : lgtd_warnx;
++        warnfn("couldn't broadcast LIFX discovery packet on %s", addr_str);
 +        return false;
 +    }
 +
@@ -86,12 +88,12 @@
 -        .sin_port = htons(LGTD_LIFX_PROTOCOL_PORT),
 -        .sin_zero = { 0 }
 -    };
-+    uint16_t lifx_port = htons(LGTD_LIFX_PROTOCOL_PORT);
++    const uint16_t lifx_port = htons(LGTD_LIFX_PROTOCOL_PORT);
 +
      struct lgtd_lifx_packet_header get_pan_gateway;
      lgtd_lifx_wire_setup_header(
          &get_pan_gateway,
-@@ -70,31 +92,57 @@
+@@ -70,31 +94,50 @@
          LGTD_LIFX_GET_PAN_GATEWAY
      );
  
@@ -108,7 +110,7 @@
 -    if (nbytes == sizeof(get_pan_gateway)) {
 -        if (event_del(lgtd_lifx_broadcast_endpoint.write_ev)) {
 -            lgtd_err(1, "can't setup events");
-+    bool ok = true;
++    bool ok = false;
 +    struct ifaddrs *ifaddrs = NULL;
 +    if (getifaddrs(&ifaddrs)) {
 +        struct sockaddr_in lifx_bcast_addr = {
@@ -130,21 +132,14 @@
 +        );
 +    } else {
 +        for (struct ifaddrs *ifa = ifaddrs; ifa; ifa = ifa->ifa_next) {
++            // NOTE: IPv6 doesn't implement broadcast
 +            if (ifa->ifa_broadaddr != NULL
 +                && (ifa->ifa_flags & IFF_BROADCAST)
++                && ifa->ifa_broadaddr->sa_family == AF_INET
 +                && ifa->ifa_netmask != NULL) {
-+                ev_socklen_t addrlen;
 +                struct sockaddr *addr = ifa->ifa_broadaddr;
-+                if (addr->sa_family == AF_INET) {
-+                    ((struct sockaddr_in *)addr)->sin_port = lifx_port;
-+                    addrlen = sizeof(struct sockaddr_in);
-+                } else if (addr->sa_family == AF_INET6) {
-+                    // TODO: add support for IPv6 on the receive path.
-+                    ((struct sockaddr_in6 *)addr)->sin6_port = lifx_port;
-+                    addrlen = sizeof(struct sockaddr_in6);
-+                } else {
-+                    continue;
-+                }
++                ((struct sockaddr_in *)addr)->sin_port = lifx_port;
++                ev_socklen_t addrlen = sizeof(struct sockaddr_in);
 +                bool sent = lgtd_lifx_broadcast_send_packet(
 +                    &get_pan_gateway, sizeof(get_pan_gateway), addr, addrlen
 +                );
@@ -171,3 +166,1131 @@
  }
  
  static void
+diff --git a/lifx/bulb.c b/lifx/bulb.c
+--- a/lifx/bulb.c
++++ b/lifx/bulb.c
+@@ -274,9 +274,9 @@
+ 
+ void
+ lgtd_lifx_bulb_set_ip_state(struct lgtd_lifx_bulb *bulb,
+-                      enum lgtd_lifx_bulb_ips ip_id,
+-                      const struct lgtd_lifx_ip_state *state,
+-                      lgtd_time_mono_t received_at)
++                            enum lgtd_lifx_bulb_ips ip_id,
++                            const struct lgtd_lifx_ip_state *state,
++                            lgtd_time_mono_t received_at)
+ {
+     assert(bulb);
+     assert(state);
+diff --git a/lifx/tagging.h b/lifx/tagging.h
+--- a/lifx/tagging.h
++++ b/lifx/tagging.h
+@@ -17,6 +17,8 @@
+ 
+ #pragma once
+ 
++struct lgtd_lifx_gateway;
++
+ extern struct lgtd_lifx_tag_list lgtd_lifx_tags;
+ 
+ struct lgtd_lifx_site {
+diff --git a/tests/core/mock_event2.h b/tests/core/mock_event2.h
+--- a/tests/core/mock_event2.h
++++ b/tests/core/mock_event2.h
+@@ -210,3 +210,21 @@
+     return 0;
+ }
+ #endif
++
++#ifndef MOCKED_EVUTIL_CLOSESOCKET
++int
++evutil_closesocket(evutil_socket_t sock)
++{
++    (void)sock;
++    return -1;
++}
++#endif
++
++#ifndef MOCKED_EVUTIL_MAKE_LISTEN_SOCKET_REUSEABLE
++int
++evutil_make_listen_socket_reuseable(evutil_socket_t sock)
++{
++    (void)sock;
++    return -1;
++}
++#endif
+diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c
+--- a/tests/core/tests_utils.c
++++ b/tests/core/tests_utils.c
+@@ -213,3 +213,39 @@
+ 
+     free(path);
+ }
++
++void
++lgtd_tests_check_sockaddr_in(const struct sockaddr *addr,
++                             int addrlen,
++                             int expected_family,
++                             uint32_t expected_addr,
++                             uint16_t expected_port)
++{
++    if (!addr) {
++        lgtd_errx(1, "missing addr");
++    }
++
++    if (addr->sa_family != expected_family) {
++        lgtd_errx(
++            1, "got address of type %d (expected %d)",
++            addr->sa_family, expected_family
++        );
++    }
++    const struct sockaddr_in *inaddr = (const struct sockaddr_in *)addr;
++    if (inaddr->sin_addr.s_addr != expected_addr) {
++        lgtd_errx(
++            1, "got address %#x (expected %#x)",
++            inaddr->sin_addr.s_addr, expected_addr
++        );
++    }
++    uint16_t port = ntohs(inaddr->sin_port);
++    if (port != expected_port) {
++        lgtd_errx(1, "got port %hu (expected %u)", port, expected_port);
++    }
++    if (addrlen != -1 && addrlen != sizeof(*inaddr)) {
++        lgtd_errx(
++            1, "got invalid addrlen %u (expected %ju)",
++            addrlen, sizeof(*inaddr)
++        );
++    }
++}
+diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h
+--- a/tests/core/tests_utils.h
++++ b/tests/core/tests_utils.h
+@@ -1,6 +1,7 @@
+ #pragma once
+ 
+ struct bufferevent;
++struct sockaddr;
+ 
+ static inline bool
+ lgtd_tests_lifx_header_has_flags(const struct lgtd_lifx_packet_header *hdr,
+@@ -32,6 +33,12 @@
+     return true;
+ }
+ 
++static inline void
++lgtd_tests_hr(void)
++{
++    puts("----");
++}
++
+ char *lgtd_tests_make_temp_dir(void);
+ void lgtd_tests_remove_temp_dir(char *);
+ 
+@@ -44,3 +51,4 @@
+                                                 int);
+ struct lgtd_listen *lgtd_tests_insert_mock_listener(const char *, uint16_t);
+ struct lgtd_client *lgtd_tests_insert_mock_client(struct bufferevent *);
++void lgtd_tests_check_sockaddr_in(const struct sockaddr *, int, int, uint32_t, uint16_t);
+diff --git a/tests/lifx/broadcast/CMakeLists.txt b/tests/lifx/broadcast/CMakeLists.txt
+new file mode 100644
+--- /dev/null
++++ b/tests/lifx/broadcast/CMakeLists.txt
+@@ -0,0 +1,21 @@
++INCLUDE_DIRECTORIES(
++    ${CMAKE_CURRENT_SOURCE_DIR}
++    ${CMAKE_CURRENT_BINARY_DIR}
++)
++
++ADD_CORE_LIBRARY(
++    test_lifx_broadcast STATIC
++    ${LIGHTSD_SOURCE_DIR}/core/stats.c
++    ${LIGHTSD_SOURCE_DIR}/core/utils.c
++    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
++    ${CMAKE_CURRENT_SOURCE_DIR}/../../core/tests_utils.c
++)
++
++FUNCTION(ADD_BROADCAST_TEST TEST_SOURCE)
++    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_broadcast)
++ENDFUNCTION()
++
++FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
++FOREACH(TEST ${TESTS})
++    ADD_BROADCAST_TEST(${TEST})
++ENDFOREACH()
+diff --git a/tests/lifx/broadcast/test_broadcast_send_packet.c b/tests/lifx/broadcast/test_broadcast_send_packet.c
+new file mode 100644
+--- /dev/null
++++ b/tests/lifx/broadcast/test_broadcast_send_packet.c
+@@ -0,0 +1,146 @@
++#include <sys/socket.h>
++
++ssize_t mock_sendto(int, const void *, size_t, int,
++                    const struct sockaddr *, socklen_t);
++
++#define sendto(socket, buffer, length, flags, dest_addr, dest_len) \
++    mock_sendto(socket, buffer, length, flags, dest_addr, dest_len)
++
++#include "broadcast.c"
++
++#include "mock_bulb.h"
++#include "mock_event2.h"
++#include "mock_gateway.h"
++#include "mock_log.h"
++#include "mock_tagging.h"
++#include "mock_wire_proto.h"
++
++#include "tests_utils.h"
++
++enum { MOCK_SOCKET_FD = 32 };
++static const char MOCK_PKT[] = "fake packet";
++static const struct sockaddr_in MOCK_ADDR = {
++    .sin_family = AF_INET,
++    .sin_addr = { INADDR_BROADCAST },
++    .sin_port = htons(LGTD_LIFX_PROTOCOL_PORT),
++    .sin_zero = { 0 }
++};
++
++enum mock_sendto_calls {
++    TEST_SENDTO_OK = 0,
++    TEST_SENDTO_PARTIAL,
++    TEST_SENDTO_EINTR,
++    TEST_SENDTO_EINTR_OK,
++    TEST_SENDTO_ERROR,
++};
++
++static int mock_sendto_call_count = 0;
++
++ssize_t
++mock_sendto(int socket,
++            const void *buffer,
++            size_t length,
++            int flags,
++            const struct sockaddr *dest_addr,
++            socklen_t dest_len)
++{
++    if (socket != MOCK_SOCKET_FD) {
++        lgtd_errx(
++            1, "got socket=%jd (expected %d)", (intmax_t)socket, MOCK_SOCKET_FD
++        );
++    }
++
++    if (length != sizeof(MOCK_PKT)) {
++        lgtd_errx(
++            1, "got length=%ju (expected %ju)",
++            (uintmax_t)length, sizeof(MOCK_PKT)
++        );
++    }
++
++    if (strcmp((const char *)buffer, MOCK_PKT)) {
++        lgtd_errx(
++            1, "got buffer [%.*s] (expected [%s])",
++            (int)length, buffer, MOCK_PKT
++        );
++    }
++
++    if (flags) {
++        lgtd_errx(1, "got flags=%#x (expected 0x0)", flags);
++    }
++
++    lgtd_tests_check_sockaddr_in(
++        dest_addr, dest_len, AF_INET, INADDR_BROADCAST, LGTD_LIFX_PROTOCOL_PORT
++    );
++
++    switch (mock_sendto_call_count++) {
++    case TEST_SENDTO_OK:
++        return length;
++    case TEST_SENDTO_PARTIAL:
++        return length - 1;
++    case TEST_SENDTO_EINTR:
++        errno = EINTR;
++        return -1;
++    case TEST_SENDTO_EINTR_OK:
++        return length;
++    case TEST_SENDTO_ERROR:
++        errno = EIO;
++        return -1;
++    default:
++        lgtd_errx(1, "sendto was called too many times");
++    }
++}
++
++int
++main(void)
++{
++    lgtd_lifx_broadcast_endpoint.socket = MOCK_SOCKET_FD;
++    const struct sockaddr *addr = (const struct sockaddr *)&MOCK_ADDR;
++
++    bool ok;
++
++    // ok
++    ok = lgtd_lifx_broadcast_send_packet(
++        MOCK_PKT, sizeof(MOCK_PKT), addr, sizeof(MOCK_ADDR)
++    );
++    if (mock_sendto_call_count != 1) {
++        lgtd_errx(1, "sendto wasn't called");
++    }
++    if (!ok) {
++        lgtd_errx(1, "broadcast_send_packet returned false (expected true)");
++    }
++
++    // partial
++    ok = lgtd_lifx_broadcast_send_packet(
++        MOCK_PKT, sizeof(MOCK_PKT), addr, sizeof(MOCK_ADDR)
++    );
++    if (mock_sendto_call_count != 2) {
++        lgtd_errx(1, "sendto wasn't called");
++    }
++    if (ok) {
++        lgtd_errx(1, "broadcast_send_packet returned true (expected false)");
++    }
++
++    // eintr
++    ok = lgtd_lifx_broadcast_send_packet(
++        MOCK_PKT, sizeof(MOCK_PKT), addr, sizeof(MOCK_ADDR)
++    );
++    if (mock_sendto_call_count != 4) {
++        lgtd_errx(1, "sendto wasn't called");
++    }
++    if (!ok) {
++        lgtd_errx(1, "broadcast_send_packet returned false (expected true)");
++    }
++
++    // error
++    ok = lgtd_lifx_broadcast_send_packet(
++        MOCK_PKT, sizeof(MOCK_PKT), addr, sizeof(MOCK_ADDR)
++    );
++    if (mock_sendto_call_count != 5) {
++        lgtd_errx(1, "sendto wasn't called");
++    }
++    if (ok) {
++        lgtd_errx(1, "broadcast_send_packet returned true (expected false)");
++    }
++
++    return 0;
++}
+diff --git a/tests/lifx/broadcast/test_broadcast_write_callback.c b/tests/lifx/broadcast/test_broadcast_write_callback.c
+new file mode 100644
+--- /dev/null
++++ b/tests/lifx/broadcast/test_broadcast_write_callback.c
+@@ -0,0 +1,374 @@
++#include <sys/types.h>
++#include <sys/socket.h>
++#include <ifaddrs.h>
++
++int mock_getifaddrs(struct ifaddrs **);
++void mock_freeifaddrs(struct ifaddrs *);
++ssize_t mock_sendto(int, const void *, size_t, int,
++                    const struct sockaddr *, socklen_t);
++
++#define getifaddrs(ifap) mock_getifaddrs(ifap)
++#define freeifaddrs(ifp) mock_freeifaddrs(ifp)
++#define sendto(socket, buffer, length, flags, dest_addr, dest_len) \
++    mock_sendto(socket, buffer, length, flags, dest_addr, dest_len)
++
++#include "broadcast.c"
++
++#include "mock_bulb.h"
++#define MOCKED_EVENT_DEL
++#include "mock_event2.h"
++#include "mock_gateway.h"
++#include "mock_log.h"
++#include "mock_tagging.h"
++#define MOCKED_LGTD_LIFX_WIRE_SETUP_HEADER
++#include "mock_wire_proto.h"
++
++#include "tests_utils.h"
++
++enum { MOCK_SOCKET_FD = 32 };
++static struct event *MOCK_WRITE_EV = (struct event *)0x7061726973;
++static const struct sockaddr_in LIFX_BROADCAST_ADDR = {
++    .sin_family = AF_INET,
++    .sin_addr = { INADDR_BROADCAST },
++    .sin_port = htons(LGTD_LIFX_PROTOCOL_PORT),
++    .sin_zero = { 0 }
++};
++
++// /24
++#define TEST_IPV4_NETMASK_ADDR_24 htonl(0xffffff00)
++static struct sockaddr_in TEST_IPV4_NETMASK_SOCKADDR_24 = {
++    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_NETMASK_ADDR_24 }
++};
++
++// /16
++#define TEST_IPV4_NETMASK_ADDR_16 htonl(0xffff0000)
++static struct sockaddr_in TEST_IPV4_NETMASK_SOCKADDR_16 = {
++    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_NETMASK_ADDR_16 }
++};
++
++// 192.168.42.255
++#define TEST_IPV4_BROADCAST_ADDR_CLASS_C htonl(0xc0a82aff)
++static struct sockaddr_in TEST_IPV4_BROADCAST_SOCKADDR_CLASS_C = {
++    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_BROADCAST_ADDR_CLASS_C }
++};
++
++// 10.10.255.255
++#define TEST_IPV4_BROADCAST_ADDR_CLASS_A htonl(0x0a0affff)
++static struct sockaddr_in TEST_IPV4_BROADCAST_SOCKADDR_CLASS_A = {
++    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_BROADCAST_ADDR_CLASS_A }
++};
++
++// 82.66.148.158
++#define TEST_IPV4_UNICAST_ADDR_ROUTABLE htonl(0x5242949e)
++static struct sockaddr_in TEST_IPV4_UNICAST_SOCKADDR_ROUTABLE = {
++    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_UNICAST_ADDR_ROUTABLE }
++};
++
++static struct sockaddr_in6 TEST_IPV6_NETMASK_SOCKADDR_64 = {
++    .sin6_family = AF_INET6
++};
++static struct sockaddr_in6 TEST_IPV6_UNICAST_SOCKADDR = {
++    .sin6_family = AF_INET6
++};
++
++static struct ifaddrs MOCK_IFSOCKADDR_IPV4_BROADCAST_SOCKADDR_CLASS_C = {
++    .ifa_flags = IFF_BROADCAST,
++    .ifa_broadaddr = (struct sockaddr *)&TEST_IPV4_BROADCAST_SOCKADDR_CLASS_C,
++    .ifa_netmask = (struct sockaddr *)&TEST_IPV4_NETMASK_SOCKADDR_24,
++    .ifa_next = NULL
++};
++static struct ifaddrs MOCK_IFSOCKADDR_IPV4_BROADCAST_SOCKADDR_CLASS_A = {
++    .ifa_flags = IFF_BROADCAST,
++    .ifa_broadaddr = (struct sockaddr *)&TEST_IPV4_BROADCAST_SOCKADDR_CLASS_A,
++    .ifa_netmask = (struct sockaddr *)&TEST_IPV4_NETMASK_SOCKADDR_16,
++    .ifa_next = &MOCK_IFSOCKADDR_IPV4_BROADCAST_SOCKADDR_CLASS_C
++};
++static struct ifaddrs MOCK_IFSOCKADDR_IPV4_UNICAST_SOCKADDR_ROUTABLE = {
++    .ifa_dstaddr = (struct sockaddr *)&TEST_IPV4_UNICAST_SOCKADDR_ROUTABLE,
++    .ifa_netmask = (struct sockaddr *)&TEST_IPV4_NETMASK_SOCKADDR_24,
++    .ifa_next = &MOCK_IFSOCKADDR_IPV4_BROADCAST_SOCKADDR_CLASS_A
++};
++static struct ifaddrs MOCK_IFSOCKADDR_IPV6_UNICAST_SOCKADDR = {
++    .ifa_dstaddr = (struct sockaddr *)&TEST_IPV6_UNICAST_SOCKADDR,
++    .ifa_netmask = (struct sockaddr *)&TEST_IPV6_NETMASK_SOCKADDR_64,
++    .ifa_next = &MOCK_IFSOCKADDR_IPV4_UNICAST_SOCKADDR_ROUTABLE
++};
++
++static int event_del_call_count = 0;
++
++int
++event_del(struct event *ev)
++{
++    if (ev != MOCK_WRITE_EV) {
++        lgtd_errx(
++            1, "event_del received invalid event=%p (expected %p)",
++            ev, MOCK_WRITE_EV
++        );
++    }
++
++    event_del_call_count++;
++
++    return 0;
++}
++
++static int mock_getifaddrs_call_count = 0;
++
++int
++mock_getifaddrs(struct ifaddrs **ifap)
++{
++    if (!ifap) {
++        lgtd_errx(1, "ifap souldn't be NULL");
++    }
++
++    *ifap = &MOCK_IFSOCKADDR_IPV6_UNICAST_SOCKADDR;
++
++    mock_getifaddrs_call_count++;
++
++    return 0;
++}
++
++static int mock_freeifaddrs_call_count = 0;
++
++void
++mock_freeifaddrs(struct ifaddrs *ifp)
++{
++    if (ifp != &MOCK_IFSOCKADDR_IPV6_UNICAST_SOCKADDR) {
++        lgtd_errx(
++            1, "got ifp = %p (expected %p)",
++            ifp, &MOCK_IFSOCKADDR_IPV6_UNICAST_SOCKADDR
++        );
++    }
++
++    mock_freeifaddrs_call_count++;
++}
++
++static int mock_lifx_wire_setup_header_call_count = 0;
++
++const struct lgtd_lifx_packet_info *
++lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *hdr,
++                            enum lgtd_lifx_target_type target_type,
++                            union lgtd_lifx_target target,
++                            const uint8_t *site,
++                            enum lgtd_lifx_packet_type packet_type)
++{
++    if (!hdr) {
++        lgtd_errx(1, "missing header");
++    }
++
++    if (target_type != LGTD_LIFX_TARGET_ALL_DEVICES) {
++        lgtd_errx(
++            1, "got target type %d (expected %d)",
++            target_type, LGTD_LIFX_TARGET_ALL_DEVICES
++        );
++    }
++
++    if (memcmp(&target, &LGTD_LIFX_UNSPEC_TARGET, sizeof(target))) {
++        lgtd_errx(
++            1, "got unexpected target (expected LGTD_LIFX_UNSPEC_TARGET)"
++        );
++    }
++
++    if (site) {
++        lgtd_errx(1, "got unexpected site %p (expected NULL)", site);
++    }
++
++    if (packet_type != LGTD_LIFX_GET_PAN_GATEWAY) {
++        lgtd_errx(
++            1, "got unexpected packet type %d (expected %d)",
++            packet_type, LGTD_LIFX_GET_PAN_GATEWAY
++        );
++    }
++
++    mock_lifx_wire_setup_header_call_count++;
++
++    return NULL;
++}
++
++static int mock_sendto_call_count = 0;
++
++enum mock_sendto_calls {
++    // 1st test case everything ok
++    TEST_OK_SENDTO_OK_ADDR_CLASS_A = 0,
++    TEST_OK_SENDTO_OK_ADDR_CLASS_C,
++    // 2nd test case one failure
++    TEST_PARTIAL_SENDTO_OK_ADDR_CLASS_A,
++    TEST_PARTIAL_SENDTO_ERROR_ADDR_CLASS_C,
++    // 3rd test case all failures
++    TEST_FAIL_SENDTO_ERROR_ADDR_CLASS_A,
++    TEST_FAIL_SENDTO_ERROR_ADDR_CLASS_C
++};
++
++ssize_t
++mock_sendto(int socket,
++            const void *buffer,
++            size_t length,
++            int flags,
++            const struct sockaddr *dest_addr,
++            socklen_t dest_len)
++{
++    if (socket != MOCK_SOCKET_FD) {
++        lgtd_errx(
++            1, "got socket=%jd (expected %d)", (intmax_t)socket, MOCK_SOCKET_FD
++        );
++    }
++
++    if (!buffer) {
++        lgtd_errx(1, "missing buffer");
++    }
++
++    if (length != sizeof(struct lgtd_lifx_packet_header)) {
++        lgtd_errx(
++            1, "got buffer length=%ju (expected %ju)",
++            (uintmax_t)length, sizeof(struct lgtd_lifx_packet_header)
++        );
++    }
++
++    if (flags) {
++        lgtd_errx(1, "got flags=%#x (expected 0x0)", flags);
++    }
++
++    switch (mock_sendto_call_count++) {
++    case TEST_PARTIAL_SENDTO_OK_ADDR_CLASS_A:
++    case TEST_OK_SENDTO_OK_ADDR_CLASS_A:
++        lgtd_tests_check_sockaddr_in(
++            dest_addr,
++            dest_len,
++            AF_INET,
++            TEST_IPV4_BROADCAST_ADDR_CLASS_A,
++            LGTD_LIFX_PROTOCOL_PORT
++        );
++        return length;
++    case TEST_OK_SENDTO_OK_ADDR_CLASS_C:
++        lgtd_tests_check_sockaddr_in(
++            dest_addr,
++            dest_len,
++            AF_INET,
++            TEST_IPV4_BROADCAST_ADDR_CLASS_C,
++            LGTD_LIFX_PROTOCOL_PORT
++        );
++        return length;
++    case TEST_PARTIAL_SENDTO_ERROR_ADDR_CLASS_C:
++    case TEST_FAIL_SENDTO_ERROR_ADDR_CLASS_C:
++        lgtd_tests_check_sockaddr_in(
++            dest_addr,
++            dest_len,
++            AF_INET,
++            TEST_IPV4_BROADCAST_ADDR_CLASS_C,
++            LGTD_LIFX_PROTOCOL_PORT
++        );
++        return length - 1;
++    case TEST_FAIL_SENDTO_ERROR_ADDR_CLASS_A:
++        lgtd_tests_check_sockaddr_in(
++            dest_addr,
++            dest_len,
++            AF_INET,
++            TEST_IPV4_BROADCAST_ADDR_CLASS_A,
++            LGTD_LIFX_PROTOCOL_PORT
++        );
++        return length - 1;
++    default:
++        lgtd_errx(1, "mock_sendto called too many times");
++    }
++}
++
++int
++main(void)
++{
++    lgtd_lifx_broadcast_endpoint.socket = MOCK_SOCKET_FD;
++    lgtd_lifx_broadcast_endpoint.write_ev = MOCK_WRITE_EV;
++
++    bool ok;
++
++    // getifaddrs ok
++    ok = lgtd_lifx_broadcast_handle_write();
++    if (!ok) {
++        lgtd_errx(1, "write callback returned false (expected true)");
++    }
++    if (mock_lifx_wire_setup_header_call_count != 1) {
++        lgtd_errx(
++            1, "mock_lifx_wire_setup_header_call_count=%d (expected 1)",
++            mock_lifx_wire_setup_header_call_count
++        );
++    }
++    if (mock_sendto_call_count != 2) {
++        lgtd_errx(
++            1, "mock_sendto_call_count=%d (expected 2)",
++            mock_sendto_call_count
++        );
++    }
++    if (event_del_call_count != 1) {
++        lgtd_errx(
++            1, "event_del_call_count=%d (expected 1)", event_del_call_count
++        );
++    }
++    if (mock_freeifaddrs_call_count != 1) {
++        lgtd_errx(
++            1, "freeifaddrs_call_count=%d (expected 1)",
++            mock_freeifaddrs_call_count
++        );
++    }
++
++    lgtd_tests_hr();
++
++    // getifaddrs ok, one send fails
++    ok = lgtd_lifx_broadcast_handle_write();
++    if (!ok) {
++        lgtd_errx(1, "write callback returned false (expected true)");
++    }
++    if (mock_lifx_wire_setup_header_call_count != 2) {
++        lgtd_errx(
++            1, "mock_lifx_wire_setup_header_call_count=%d (expected 2)",
++            mock_lifx_wire_setup_header_call_count
++        );
++    }
++    if (mock_sendto_call_count != 4) {
++        lgtd_errx(
++            1, "mock_sendto_call_count=%d (expected 4)",
++            mock_sendto_call_count
++        );
++    }
++    if (event_del_call_count != 2) {
++        lgtd_errx(
++            1, "event_del_call_count=%d (expected 2)", event_del_call_count
++        );
++    }
++    if (mock_freeifaddrs_call_count != 2) {
++        lgtd_errx(
++            1, "freeifaddrs_call_count=%d (expected 2)",
++            mock_freeifaddrs_call_count
++        );
++    }
++
++    lgtd_tests_hr();
++
++    // getifaddrs ok, all sends fail
++    ok = lgtd_lifx_broadcast_handle_write();
++    if (ok) {
++        lgtd_errx(1, "write callback returned true (expected false)");
++    }
++    if (mock_lifx_wire_setup_header_call_count != 3) {
++        lgtd_errx(
++            1, "mock_lifx_wire_setup_header_call_count=%d (expected 2)",
++            mock_lifx_wire_setup_header_call_count
++        );
++    }
++    if (mock_sendto_call_count != 6) {
++        lgtd_errx(
++            1, "mock_sendto_call_count=%d (expected 6)",
++            mock_sendto_call_count
++        );
++    }
++    if (event_del_call_count != 2) {
++        lgtd_errx(
++            1, "event_del_call_count=%d (expected 2)", event_del_call_count
++        );
++    }
++    if (mock_freeifaddrs_call_count != 3) {
++        lgtd_errx(
++            1, "freeifaddrs_call_count=%d (expected 3)",
++            mock_freeifaddrs_call_count
++        );
++    }
++
++    return 0;
++}
+diff --git a/tests/lifx/broadcast/test_broadcast_write_callback_getifaddrs_fails.c b/tests/lifx/broadcast/test_broadcast_write_callback_getifaddrs_fails.c
+new file mode 100644
+--- /dev/null
++++ b/tests/lifx/broadcast/test_broadcast_write_callback_getifaddrs_fails.c
+@@ -0,0 +1,221 @@
++#include <sys/types.h>
++#include <sys/socket.h>
++#include <ifaddrs.h>
++
++int mock_getifaddrs(struct ifaddrs **);
++void mock_freeifaddrs(struct ifaddrs *);
++ssize_t mock_sendto(int, const void *, size_t, int, const struct sockaddr *, socklen_t);
++
++#define getifaddrs(ifap) mock_getifaddrs(ifap)
++#define freeifaddrs(ifp) mock_freeifaddrs(ifp)
++#define sendto(socket, buffer, length, flags, dest_addr, dest_len) \
++    mock_sendto(socket, buffer, length, flags, dest_addr, dest_len)
++
++#include "broadcast.c"
++
++#include "mock_bulb.h"
++#define MOCKED_EVENT_DEL
++#include "mock_event2.h"
++#include "mock_gateway.h"
++#include "mock_log.h"
++#include "mock_tagging.h"
++#define MOCKED_LGTD_LIFX_WIRE_SETUP_HEADER
++#include "mock_wire_proto.h"
++
++#include "tests_utils.h"
++
++enum { MOCK_SOCKET_FD = 32 };
++static struct event *MOCK_WRITE_EV = (struct event *)0x7061726973;
++static const struct sockaddr_in LIFX_BROADCAST_ADDR = {
++    .sin_family = AF_INET,
++    .sin_addr = { INADDR_BROADCAST },
++    .sin_port = htons(LGTD_LIFX_PROTOCOL_PORT),
++    .sin_zero = { 0 }
++};
++
++static int event_del_call_count = 0;
++
++int
++event_del(struct event *ev)
++{
++    if (ev != MOCK_WRITE_EV) {
++        lgtd_errx(
++            1, "event_del received invalid event=%p (expected %p)",
++            ev, MOCK_WRITE_EV
++        );
++    }
++
++    event_del_call_count++;
++
++    return 0;
++}
++
++int
++mock_getifaddrs(struct ifaddrs **ifap)
++{
++    if (!ifap) {
++        lgtd_errx(1, "ifap souldn't be NULL");
++    }
++
++    errno = ENOSYS;
++
++    return -1;
++}
++
++void
++mock_freeifaddrs(struct ifaddrs *ifp)
++{
++    (void)ifp;
++
++    lgtd_errx(1, "freeifaddrs shouldn't have been called");
++}
++
++static int mock_lifx_wire_setup_header_call_count = 0;
++
++const struct lgtd_lifx_packet_info *
++lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *hdr,
++                            enum lgtd_lifx_target_type target_type,
++                            union lgtd_lifx_target target,
++                            const uint8_t *site,
++                            enum lgtd_lifx_packet_type packet_type)
++{
++    if (!hdr) {
++        lgtd_errx(1, "missing header");
++    }
++
++    if (target_type != LGTD_LIFX_TARGET_ALL_DEVICES) {
++        lgtd_errx(
++            1, "got target type %d (expected %d)",
++            target_type, LGTD_LIFX_TARGET_ALL_DEVICES
++        );
++    }
++
++    if (memcmp(&target, &LGTD_LIFX_UNSPEC_TARGET, sizeof(target))) {
++        lgtd_errx(
++            1, "got unexpected target (expected LGTD_LIFX_UNSPEC_TARGET)"
++        );
++    }
++
++    if (site) {
++        lgtd_errx(1, "got unexpected site %p (expected NULL)", site);
++    }
++
++    if (packet_type != LGTD_LIFX_GET_PAN_GATEWAY) {
++        lgtd_errx(
++            1, "got unexpected packet type %d (expected %d)",
++            packet_type, LGTD_LIFX_GET_PAN_GATEWAY
++        );
++    }
++
++    mock_lifx_wire_setup_header_call_count++;
++
++    return NULL;
++}
++
++enum mock_sendto_calls {
++    MOCK_SENDTO_OK = 0,
++    MOCK_SENDTO_ERROR,
++};
++
++static int mock_sendto_call_count = 0;
++
++ssize_t
++mock_sendto(int socket,
++            const void *buffer,
++            size_t length,
++            int flags,
++            const struct sockaddr *dest_addr,
++            socklen_t dest_len)
++{
++    if (socket != MOCK_SOCKET_FD) {
++        lgtd_errx(
++            1, "got socket=%jd (expected %d)", (intmax_t)socket, MOCK_SOCKET_FD
++        );
++    }
++
++    if (!buffer) {
++        lgtd_errx(1, "missing buffer");
++    }
++
++    if (length != sizeof(struct lgtd_lifx_packet_header)) {
++        lgtd_errx(
++            1, "got buffer length=%ju (expected %ju)",
++            (uintmax_t)length, sizeof(struct lgtd_lifx_packet_header)
++        );
++    }
++
++    if (flags) {
++        lgtd_errx(1, "got flags=%#x (expected 0x0)", flags);
++    }
++
++    lgtd_tests_check_sockaddr_in(
++        dest_addr, dest_len, AF_INET, INADDR_BROADCAST, LGTD_LIFX_PROTOCOL_PORT
++    );
++
++    switch (mock_sendto_call_count++) {
++    case MOCK_SENDTO_OK:
++        return length;
++    case MOCK_SENDTO_ERROR:
++        errno = EIO;
++        return -1;
++    default:
++        lgtd_errx(1, "sendto was called too many times");
++    }
++}
++
++int
++main(void)
++{
++    lgtd_lifx_broadcast_endpoint.socket = MOCK_SOCKET_FD;
++    lgtd_lifx_broadcast_endpoint.write_ev = MOCK_WRITE_EV;
++
++    bool ok;
++
++    // getifaddrs fails
++    ok = lgtd_lifx_broadcast_handle_write();
++    if (!ok) {
++        lgtd_errx(1, "write callback returned false (expected true)");
++    }
++    if (mock_lifx_wire_setup_header_call_count != 1) {
++        lgtd_errx(
++            1, "mock_lifx_wire_setup_header_call_count=%d (expected 1)",
++            mock_lifx_wire_setup_header_call_count
++        );
++    }
++    if (mock_sendto_call_count != 1) {
++        lgtd_errx(
++            1, "mock_sendto_call_count=%d (expected 1)",
++            mock_sendto_call_count
++        );
++    }
++    if (event_del_call_count != 1) {
++        lgtd_errx(
++            1, "event_del_call_count=%d (expected 1)", event_del_call_count
++        );
++    }
++
++    // getifaddrs & lgtd_lifx_broadcast_send_packet fail
++    ok = lgtd_lifx_broadcast_handle_write();
++    if (ok) {
++        lgtd_errx(1, "write callback returned true (expected false)");
++    }
++    if (mock_lifx_wire_setup_header_call_count != 2) {
++        lgtd_errx(
++            1, "mock_lifx_wire_setup_header_call_count=%d (expected 2)",
++            mock_lifx_wire_setup_header_call_count
++        );
++    }
++    if (mock_sendto_call_count != 2) {
++        lgtd_errx(
++            1, "mock_sendto_call_count=%d (expected 2)",
++            mock_sendto_call_count
++        );
++    }
++    if (event_del_call_count != 1) {
++        lgtd_errx(
++            1, "event_del_call_count=%d (expected 1)", event_del_call_count
++        );
++    }
++
++    return 0;
++}
+diff --git a/tests/lifx/mock_bulb.h b/tests/lifx/mock_bulb.h
+new file mode 100644
+--- /dev/null
++++ b/tests/lifx/mock_bulb.h
+@@ -0,0 +1,138 @@
++#pragma once
++
++#ifndef MOCKED_LGTD_LIFX_BULB_GET
++struct lgtd_lifx_bulb *
++lgtd_lifx_bulb_get(const uint8_t *addr)
++{
++    (void)addr;
++    return NULL;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_OPEN
++struct lgtd_lifx_bulb *
++lgtd_lifx_bulb_open(struct lgtd_lifx_gateway *gw, const uint8_t *addr)
++{
++    (void)gw;
++    (void)addr;
++    return NULL;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_CLOSE
++void
++lgtd_lifx_bulb_close(struct lgtd_lifx_bulb *bulb)
++{
++    (void)bulb;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_HAS_LABEL
++bool
++lgtd_lifx_bulb_has_label(const struct lgtd_lifx_bulb *bulb,
++                         const char *label)
++{
++    (void)bulb;
++    (void)label;
++    return false;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_SET_LIGHT_STATE
++void
++lgtd_lifx_bulb_set_light_state(struct lgtd_lifx_bulb *bulb,
++                               const struct lgtd_lifx_light_state *state,
++                               lgtd_time_mono_t received_at)
++{
++    (void)bulb;
++    (void)state;
++    (void)received_at;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_SET_POWER_STATE
++void
++lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *bulb, uint16_t power)
++{
++    (void)bulb;
++    (void)power;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_SET_TAGS
++void
++lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *bulb, uint64_t tags)
++{
++    (void)bulb;
++    (void)tags;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_SET_IP_STATE
++void
++lgtd_lifx_bulb_set_ip_state(struct lgtd_lifx_bulb *bulb,
++                            enum lgtd_lifx_bulb_ips ip_id,
++                            const struct lgtd_lifx_ip_state *state,
++                            lgtd_time_mono_t received_at)
++{
++    (void)bulb;
++    (void)ip_id;
++    (void)state;
++    (void)received_at;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_SET_IP_FIRMWARE_INFO
++void
++lgtd_lifx_bulb_set_ip_firmware_info(struct lgtd_lifx_bulb *bulb,
++                                    enum lgtd_lifx_bulb_ips ip_id,
++                                    const struct lgtd_lifx_ip_firmware_info *info,
++                                    lgtd_time_mono_t received_at)
++{
++    (void)bulb;
++    (void)ip_id;
++    (void)info;
++    (void)received_at;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_SET_PRODUCT_INFO
++void
++lgtd_lifx_bulb_set_product_info(struct lgtd_lifx_bulb *bulb,
++                                const struct lgtd_lifx_product_info *info)
++{
++    (void)bulb;
++    (void)info;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_SET_RUNTIME_INFO
++void
++lgtd_lifx_bulb_set_runtime_info(struct lgtd_lifx_bulb *bulb,
++                                const struct lgtd_lifx_runtime_info *info,
++                                lgtd_time_mono_t received_at)
++{
++    (void)bulb;
++    (void)info;
++    (void)received_at;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_SET_LABEL
++void
++lgtd_lifx_bulb_set_label(struct lgtd_lifx_bulb *bulb,
++                         const char label[LGTD_LIFX_LABEL_SIZE])
++{
++    (void)bulb;
++    (void)label;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_BULB_SET_AMBIENT_LIGHT
++void
++lgtd_lifx_bulb_set_ambient_light(struct lgtd_lifx_bulb *bulb, float illuminance)
++{
++    (void)bulb;
++    (void)illuminance;
++}
++#endif
+diff --git a/tests/lifx/mock_tagging.h b/tests/lifx/mock_tagging.h
+new file mode 100644
+--- /dev/null
++++ b/tests/lifx/mock_tagging.h
+@@ -0,0 +1,55 @@
++#pragma once
++
++#include "lifx/tagging.h"
++
++struct lgtd_lifx_tag_list lgtd_lifx_tags =
++    LIST_HEAD_INITIALIZER(&lgtd_lifx_tags);
++
++#ifndef MOCKED_LGTD_LIFX_TAGGING_FIND_TAG
++struct lgtd_lifx_tag *
++lgtd_lifx_tagging_find_tag(const char *tag_label)
++{
++    (void)tag_label;
++    return NULL;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_TAGGING_ALLOCATE_TAG
++struct lgtd_lifx_tag *
++lgtd_lifx_tagging_allocate_tag(const char *tag_label)
++{
++    (void)tag_label;
++    return NULL;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_TAGGING_DEALLOCATE_TAG
++void
++lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *tag)
++{
++    (void)tag;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_TAGGING_INCREF
++struct lgtd_lifx_tag *
++lgtd_lifx_tagging_incref(const char *tag_label,
++                         struct lgtd_lifx_gateway *gw,
++                         int tag_id)
++{
++    (void)tag_label;
++    (void)gw;
++    (void)tag_id;
++    return NULL;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_TAGGING_DECREF
++void
++lgtd_lifx_tagging_decref(struct lgtd_lifx_tag *tag,
++                         struct lgtd_lifx_gateway *gw)
++{
++    (void)tag;
++    (void)gw;
++}
++#endif
+diff --git a/tests/lifx/wire_proto/CMakeLists.txt b/tests/lifx/wire_proto/CMakeLists.txt
+--- a/tests/lifx/wire_proto/CMakeLists.txt
++++ b/tests/lifx/wire_proto/CMakeLists.txt
+@@ -4,12 +4,12 @@
+ )
+ 
+ ADD_CORE_LIBRARY(
+-    test_lifx_wire_proto_core STATIC
++    test_lifx_wire_proto STATIC
+     ${LIGHTSD_SOURCE_DIR}/core/utils.c
+ )
+ 
+ FUNCTION(ADD_WIRE_PROTO_TEST TEST_SOURCE)
+-    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto_core)
++    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto)
+ ENDFUNCTION()
+ 
+ FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")