[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