[PATCH usteer 2/2] remote: initialize the socket before sending the first update

Jan Leon jan at leoncode.de
Tue Sep 8 08:11:52 PDT 2026


Skip sends on unregistered sockets and handle descriptor zero correctly.
Add a socket lifecycle regression test and English test documentation.

Signed-off-by: Jan Leon <jan at leoncode.de>
---
 CMakeLists.txt        |  6 +++
 remote.c              | 14 ++++---
 tests/README.md       | 78 +++++++++++++++++++------------------
 tests/remote-socket.c | 89 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 144 insertions(+), 43 deletions(-)
 create mode 100644 tests/remote-socket.c

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2d9bae5..a7b8d84 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -52,6 +52,12 @@ IF(BUILD_TESTING)
   TARGET_COMPILE_OPTIONS(test-remote-message PRIVATE -UNDEBUG)
   TARGET_LINK_LIBRARIES(test-remote-message ubox)
   ADD_TEST(NAME remote-message COMMAND test-remote-message)
+  ADD_EXECUTABLE(test-remote-socket tests/remote-socket.c remote-message.c)
+  TARGET_INCLUDE_DIRECTORIES(test-remote-socket PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+  TARGET_COMPILE_OPTIONS(test-remote-socket PRIVATE -UNDEBUG -ffunction-sections -fdata-sections)
+  SET_TARGET_PROPERTIES(test-remote-socket PROPERTIES LINK_FLAGS "-Wl,--gc-sections")
+  TARGET_LINK_LIBRARIES(test-remote-socket ubox)
+  ADD_TEST(NAME remote-socket COMMAND test-remote-socket)
 ENDIF()
 
 SET(CMAKE_INSTALL_PREFIX /usr)
diff --git a/remote.c b/remote.c
index 5424bdd..6ee4447 100644
--- a/remote.c
+++ b/remote.c
@@ -37,7 +37,7 @@
 #include "node.h"
 
 static uint32_t local_id;
-static struct uloop_fd remote_fd;
+static struct uloop_fd remote_fd = { .fd = -1 };
 static struct uloop_timeout remote_timer;
 static struct uloop_timeout reload_timer;
 
@@ -556,6 +556,10 @@ static void interface_send_msg(struct interface *iface, struct blob_attr *data)
 	size_t limit;
 	int ret;
 
+	/* Node notifications can arrive before the remote socket is ready. */
+	if (!remote_fd.registered)
+		return;
+
 	snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", interface_name(iface));
 	if (ioctl(remote_fd.fd, SIOCGIFMTU, &ifr) < 0) {
 		MSG(FATAL, "Cannot read MTU for %s: %s\n", interface_name(iface), strerror(errno));
@@ -770,7 +774,7 @@ static int usteer_create_v6_socket() {
 
 static void usteer_reload_timer(struct uloop_timeout *t) {
 	/* Remove uloop descriptor */
-	if (remote_fd.fd && remote_fd.registered) {
+	if (remote_fd.fd >= 0) {
 		uloop_fd_delete(&remote_fd);
 		close(remote_fd.fd);
 	}
@@ -794,11 +798,11 @@ int usteer_interface_init(void)
 	if (usteer_init_local_id())
 		return -1;
 
-	remote_timer.cb = usteer_send_update_timer;
-	remote_timer.cb(&remote_timer);
-
 	reload_timer.cb = usteer_reload_timer;
 	reload_timer.cb(&reload_timer);
 
+	remote_timer.cb = usteer_send_update_timer;
+	remote_timer.cb(&remote_timer);
+
 	return 0;
 }
diff --git a/tests/README.md b/tests/README.md
index df71be5..c9f4ba7 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,6 +1,6 @@
-# Begrenzte Remote-Updates testen
+# Testing bounded remote updates
 
-Mit den normalen Usteer-Buildabhängigkeiten:
+With the regular Usteer build dependencies installed:
 
 ```sh
 cmake -S . -B build-test -DBUILD_TESTING=ON
@@ -8,45 +8,47 @@ cmake --build build-test
 ctest --test-dir build-test --output-on-failure
 ```
 
-Für ASan/UBSan zusätzlich beim Konfigurieren:
+For ASan/UBSan, also pass these options when configuring:
 
 ```sh
 -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
 -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
 ```
 
-Der Test nutzt den unveränderten Parser aus `parse.c`. Er prüft die
-byteidentische Weitergabe kleiner Nachrichten, eine Grenze exakt auf und
-knapp unter der Nachrichtengröße, 1.001 Paketgrenzen von 200 bis 1.200 Byte
-mit jeweils 300 Stationen auf zwei APs sowie einen dritten leeren AP.
-Stationsdaten dürfen weder fehlen noch doppelt vorkommen. Metadaten,
-unbekannte AP-Felder und der Eingabepuffer müssen unverändert bleiben.
-Weitere Fälle: unteilbare übergroße Metadaten im letzten AP, übergroße
-Stationsdatensätze, zu kleine MTUs, reine Host-Updates und Sendefehler.
-Zwei Tests senden und empfangen die Nachrichten über lokale IPv4-/IPv6-
-UDP-Sockets; IPv6-Loopback muss in der Testumgebung verfügbar sein.
-
-## Protokoll und Grenzen
-
-Die Aufteilung verwendet ausschließlich bestehende Nachrichtenfelder.
-Jeder Teil enthält dieselben Host-Metadaten und dieselbe Sequenznummer des
-logischen Updates. Der bestehende Empfänger verwendet die Sequenznummer
-nicht zur Duplikatunterdrückung und aktualisiert Stationsdatensätze einzeln;
-fehlende Stationen werden nicht wegen eines Teilupdates gelöscht.
-Der Patch benötigt keine neue Empfangslogik und keinen gleichzeitigen
-Versionswechsel aller Gegenstellen. Dies ist durch Parser- und Datentests
-gestützt, ersetzt aber keinen Mehr-AP-Betriebstest.
-
-Das UDP-Nutzlastlimit ist höchstens 1.200 Byte und bei kleineren lokalen
-Interface-MTUs zusätzlich um IPv4-/IPv6- und UDP-Header reduziert. Das
-ist keine allgemeine Path-MTU-Ermittlung für Tunnel oder fremde Routen.
-Die gesamte Nachricht wird vor dem ersten Teil auf Teilbarkeit geprüft.
-Ein einzelner zu großer Metadatenblock oder Stationsdatensatz führt zu
-`EMSGSIZE` und einem protokollierten Sendefehler, nicht zu Abschneiden oder
-einem Rückfall auf übergroße Datagramme. Ein späterer Socket-Sendefehler
-kann wie gewöhnlicher UDP-Verlust ein unvollständig empfangenes Update
-hinterlassen; die nächste periodische Aktualisierung erfolgt weiterhin.
-
-Der Patch soll IP-Fragmentierung der Usteer-Updates auf den vorgesehenen
-LAN-Interfaces vermeiden. Er ist kein Nachweis einer Behebung sonstiger
-WLAN-, Shelly- oder HomePod-Probleme.
+The test uses the unchanged parser from `parse.c`. It checks byte-identical
+forwarding of small messages, limits exactly at and just below the message
+size, and 1,001 payload limits from 200 to 1,200 bytes with 300 stations
+across two nodes plus a third, empty node. Station records must not be lost
+or duplicated. Metadata, unknown node fields and the input buffer must be
+preserved. Further cases cover indivisible oversized metadata in the last
+node, oversized station records, small MTUs, host-only updates and send
+errors. Two tests send and receive messages through actual IPv4/IPv6
+loopback UDP sockets; IPv6 loopback must be available in the test environment.
+
+`remote-socket` also exercises the actual send path with mocked system
+calls: no MTU query before socket registration or after removal, valid
+descriptor 0 and a controlled MTU query failure. The initial periodic send
+is started only after socket initialization.
+
+## Protocol and limitations
+
+Splitting uses only existing message fields. Each chunk contains the same
+host metadata and sequence number as the logical update. The existing
+receiver does not use the sequence number to suppress duplicates and
+updates station records individually; missing stations are not deleted
+because an update contains only a subset. No new receive logic or
+simultaneous upgrade of all peers is required. Parser and data tests
+support this, but do not replace a multi-AP deployment test.
+
+The UDP payload limit is at most 1,200 bytes and is reduced further for
+smaller local interface MTUs, accounting for IPv4/IPv6 and UDP headers.
+This is not general path-MTU discovery for tunnels or routed networks.
+The entire message is checked for splittability before sending any chunk.
+An indivisible oversized metadata block or station record produces
+`EMSGSIZE` and a logged send error, not truncation or an oversized fallback.
+A later socket send error can still leave a partially received update,
+as with ordinary UDP loss; subsequent periodic updates continue normally.
+
+The patch is intended to avoid IP fragmentation of Usteer updates on the
+intended LAN interfaces. It does not establish a fix for unrelated radio
+or client connectivity problems.
diff --git a/tests/remote-socket.c b/tests/remote-socket.c
new file mode 100644
index 0000000..b7ea8ef
--- /dev/null
+++ b/tests/remote-socket.c
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#define _GNU_SOURCE
+#include <assert.h>
+#include <stdarg.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+
+static int mock_ioctl(int fd, unsigned long request, ...);
+static ssize_t mock_sendmsg(int fd, const struct msghdr *msg, int flags);
+static ssize_t mock_sendto(int fd, const void *data, size_t len, int flags,
+			   const struct sockaddr *addr, socklen_t addrlen);
+#define ioctl mock_ioctl
+#define sendmsg mock_sendmsg
+#define sendto mock_sendto
+#include "../remote.c"
+#undef ioctl
+#undef sendmsg
+#undef sendto
+
+struct usteer_config config;
+static unsigned int mtu_queries, sends, errors;
+static bool fail_mtu;
+
+void debug_msg(int level, const char *func, int line, const char *format, ...)
+{
+	errors++;
+}
+
+static int mock_ioctl(int fd, unsigned long request, ...)
+{
+	struct ifreq *ifr;
+	va_list ap;
+
+	assert(fd == 0 && request == SIOCGIFMTU);
+	mtu_queries++;
+	if (fail_mtu) {
+		errno = ENODEV;
+		return -1;
+	}
+	va_start(ap, request);
+	ifr = va_arg(ap, struct ifreq *);
+	ifr->ifr_mtu = 1500;
+	va_end(ap);
+	return 0;
+}
+
+static ssize_t mock_sendmsg(int fd, const struct msghdr *msg, int flags)
+{
+	assert(fd == 0 && msg->msg_iovlen == 1);
+	sends++;
+	return msg->msg_iov[0].iov_len;
+}
+
+static ssize_t mock_sendto(int fd, const void *data, size_t len, int flags,
+			   const struct sockaddr *addr, socklen_t addrlen)
+{
+	assert(0);
+	return -1;
+}
+
+int main(void)
+{
+	struct interface iface = {};
+
+	iface.node.avl.key = "test0";
+	blob_buf_init(&buf, 0);
+	blob_put_int32(&buf, APMSG_ID, 1);
+	blob_put_int32(&buf, APMSG_SEQ, 1);
+	blob_put(&buf, APMSG_NODES, NULL, 0);
+	assert(remote_fd.fd == -1 && !remote_fd.registered);
+	interface_send_msg(&iface, buf.head);
+	assert(!mtu_queries && !sends && !errors);
+	/* Descriptor zero is valid after registration, but not before it. */
+	remote_fd.fd = 0;
+	interface_send_msg(&iface, buf.head);
+	assert(!mtu_queries && !sends && !errors);
+	remote_fd.registered = true;
+	interface_send_msg(&iface, buf.head);
+	assert(mtu_queries == 1 && sends == 1 && !errors);
+	fail_mtu = true;
+	interface_send_msg(&iface, buf.head);
+	assert(mtu_queries == 2 && sends == 1 && errors == 1);
+	remote_fd.registered = false;
+	interface_send_msg(&iface, buf.head);
+	assert(mtu_queries == 2 && sends == 1 && errors == 1);
+	blob_buf_free(&buf);
+	puts("remote-socket: all tests passed");
+	return 0;
+}





More information about the openwrt-devel mailing list