Skip to content
Merged
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,8 @@ live under `Core/Interface/`; platform-specific helpers (the `SolidSyslogPosix*`
| `SolidSyslogNullStream.h` | Any code installing a no-op stream slot (Open/Close are no-ops, Send returns `true` to drop on the floor so the Store does not fill with undeliverables, Read returns `0` for would-block so the caller does not tear the connection down) | `SolidSyslogNullStream_Get` |
| `SolidSyslogPlusTcpTcpStream.h` | System setup code on FreeRTOS targets using FreeRTOS-Plus-TCP for TCP | `SolidSyslogPlusTcpTcpStream_Create(void)`, `_Destroy(base)` (wraps `FreeRTOS_socket` / `FreeRTOS_connect` / `FreeRTOS_send` / `FreeRTOS_recv` / `FreeRTOS_closesocket`; bounded blocking connect via `SO_SNDTIMEO=200ms`, cleared post-connect so Send/Read are non-blocking). Instance struct lives in a library-internal static pool (E11); default pool size 2 to support the future TLS-via-mbedTLS + plain-TCP pair (S08.07). Pool-exhaustion fallback is the shared `SolidSyslogNullStream`. |
| `SolidSyslogPlusTcpTcpStreamErrors.h` | Any code installing an error handler that wants to react to PlusTcpTcpStream-specific events (pointer-identity match on `PlusTcpTcpStreamErrorSource`, switch on `enum SolidSyslogPlusTcpTcpStreamErrors`) | `enum SolidSyslogPlusTcpTcpStreamErrors` (`PLUSTCPTCPSTREAM_ERROR_*` codes + `PLUSTCPTCPSTREAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource PlusTcpTcpStreamErrorSource`. Integrators ignore if not handling errors per source. |
| `SolidSyslogLwipRawTcpStream.h` | System setup code on **any target using lwIP Raw API** wiring a TCP sender (or a byte transport for `SolidSyslogTlsStream` / `SolidSyslogMbedTlsStream`) | `SolidSyslogLwipRawTcpStreamConfig` (`GetConnectTimeoutMs` + context + required `Sleep`), `SolidSyslogLwipRawTcpStream_Create(config)`, `_Destroy(base)` (wraps `tcp_new` / `tcp_connect` / `tcp_write` (`TCP_WRITE_FLAG_COPY`) / `tcp_output` / `tcp_recv` / `tcp_recved` / `tcp_close` / `tcp_abort`; integrator-supplied Sleep drives the bounded synchronous-Open spin loop; sets `SOF_KEEPALIVE` on every pcb; bounded RX pbuf queue via `SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE`; encapsulates lwIP's `tcp_close`-after-`tcp_err` use-after-free rule). Instance struct lives in a library-internal static pool (E11); default pool size 2 to support the TLS-over-plain-TCP pair. Pool-exhaustion and bad-config (`NULL` config or `NULL` Sleep) fallback is the shared `SolidSyslogNullStream`. See [`docs/integrating-lwip.md`](docs/integrating-lwip.md) for the full integrator guide. |
| `SolidSyslogLwipRawTcpStreamErrors.h` | Any code installing an error handler that wants to react to LwipRawTcpStream-specific events (pointer-identity match on `LwipRawTcpStreamErrorSource`, switch on `enum SolidSyslogLwipRawTcpStreamErrors`) | `enum SolidSyslogLwipRawTcpStreamErrors` (`LWIPRAWTCPSTREAM_ERROR_*` codes + `LWIPRAWTCPSTREAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource LwipRawTcpStreamErrorSource`. Integrators ignore if not handling errors per source. |
| `SolidSyslogWinsockTcpStreamErrors.h` | Any code installing an error handler that wants to react to WinsockTcpStream-specific events (pointer-identity match on `WinsockTcpStreamErrorSource`, switch on `enum SolidSyslogWinsockTcpStreamErrors`) | `enum SolidSyslogWinsockTcpStreamErrors` (`WINSOCKTCPSTREAM_ERROR_*` codes + `WINSOCKTCPSTREAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource WinsockTcpStreamErrorSource`. Integrators ignore if not handling errors per source. |
| `SolidSyslogUdpSender.h` | System setup code using UDP transport | `SolidSyslogUdpSenderConfig` (resolver, datagram, address, endpoint, endpointVersion — the integrator supplies one platform Address handle per sender from `SolidSyslog{Posix,Winsock,PlusTcp}Address_Create`), `SolidSyslogUdpSender_Create(config)`, `_Destroy(sender)`. Instance struct lives in a library-internal static pool (E11); `_Destroy` takes the handle returned by `_Create`. Pool-exhaustion and bad-config (including NULL Address) fallback is the shared `SolidSyslogNullSender`. |
| `SolidSyslogUdpSenderErrors.h` | Any code installing an error handler that wants to react to UdpSender-specific events (pointer-identity match on `UdpSenderErrorSource`, switch on `enum SolidSyslogUdpSenderErrors`) | `enum SolidSyslogUdpSenderErrors` (`UDPSENDER_ERROR_*` codes + `UDPSENDER_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource UdpSenderErrorSource`. Integrators ignore if not handling errors per source. |
Expand Down
16 changes: 8 additions & 8 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,12 @@ endif()
# time. CI builds PLUSTCP and LWIP in isolation; BOTH is a dev-container
# convenience.
#
# S28.03 lands the first slice of the lwIP backend (Address + Resolver,
# literal IPv4 only). LWIP and BOTH configure but the backend is still
# partial — Datagram/TcpStream/BDD arrive in S28.04-S28.06. Platform/LwipRaw
# is OS-agnostic by construction (wraps lwIP only, no FreeRTOS coupling)
# and is gated separately on LWIP_PATH below; this option only controls
# which adapter pack the FreeRTOS BDD target wires up.
# S28.03-S28.05 land the OS-agnostic slices (Address, Resolver, Datagram,
# TcpStream). LWIP and BOTH configure but the backend is still partial —
# BDD arrives in S28.06. Platform/LwipRaw is OS-agnostic by construction
# (wraps lwIP only, no FreeRTOS coupling) and is gated separately on
# LWIP_PATH below; this option only controls which adapter pack the
# FreeRTOS BDD target wires up.
set(SOLIDSYSLOG_FREERTOS_NET "PLUSTCP" CACHE STRING
"FreeRTOS networking backend: PLUSTCP (default), LWIP, or BOTH")
set_property(CACHE SOLIDSYSLOG_FREERTOS_NET PROPERTY STRINGS PLUSTCP LWIP BOTH)
Expand All @@ -169,12 +169,12 @@ endif()
if(SOLIDSYSLOG_FREERTOS_NET STREQUAL "LWIP")
message(STATUS
"SOLIDSYSLOG_FREERTOS_NET=LWIP: lwIP backend partial — "
"Address+Resolver+Datagram only; TcpStream/BDD arrive in S28.05-S28.06.")
"Address+Resolver+Datagram+TcpStream only; BDD arrives in S28.06.")
endif()
if(SOLIDSYSLOG_FREERTOS_NET STREQUAL "BOTH")
message(STATUS
"SOLIDSYSLOG_FREERTOS_NET=BOTH: lwIP backend partial — "
"Address+Resolver+Datagram only; TcpStream/BDD arrive in S28.05-S28.06.")
"Address+Resolver+Datagram+TcpStream only; BDD arrives in S28.06.")
endif()

# Build-time tunables (E21: Port-Time Configurability). Integrators override
Expand Down
63 changes: 63 additions & 0 deletions Core/Interface/SolidSyslogTunablesDefaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,69 @@
#error "SOLIDSYSLOG_LWIP_RAW_DATAGRAM_POOL_SIZE must be >= 1"
#endif

/*
* Number of SolidSyslogLwipRawTcpStream instances the library's internal
* static pool can simultaneously hold. Each instance carries a struct tcp_pcb
* pointer, the Connected / Errored flags, and a bounded RX pbuf ring sized
* by SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE.
*
* Default 2 — matches SOLIDSYSLOG_POSIX_TCP_STREAM_POOL_SIZE and the other
* TCP-stream-backend defaults so the canonical TLS-over-plain-TCP pair does
* not silently fall back to NullStream on the second Create. Bump via
* SOLIDSYSLOG_USER_TUNABLES_FILE if more are needed.
*
* Floor: 1. Sub-floor values rejected at compile time.
*/
#ifndef SOLIDSYSLOG_LWIP_RAW_TCP_STREAM_POOL_SIZE
#define SOLIDSYSLOG_LWIP_RAW_TCP_STREAM_POOL_SIZE 2U
#endif

#if SOLIDSYSLOG_LWIP_RAW_TCP_STREAM_POOL_SIZE < 1
#error "SOLIDSYSLOG_LWIP_RAW_TCP_STREAM_POOL_SIZE must be >= 1"
#endif

/*
* Period (milliseconds) the SolidSyslogLwipRawTcpStream bounded-connect
* spin loop sleeps between polls of the lwIP-side connected_cb flag.
* Each iteration calls the integrator-injected SolidSyslogSleepFunction
* so the loop never busy-waits — under NO_SYS=1 the integrator's Sleep
* implementation ticks sys_check_timeouts and drives RX; under NO_SYS=0
* it yields to the tcpip thread (vTaskDelay or equivalent).
*
* Default 10 ms gives 20 polls inside the default 200 ms connect deadline.
*
* Floor: 1 ms. Sub-floor values rejected at compile time.
*/
#ifndef SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS
#define SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS 10U
#endif

#if SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS < 1
#error "SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS must be >= 1"
#endif

/*
* Maximum number of struct pbuf* the SolidSyslogLwipRawTcpStream RX queue
* holds before backpressuring lwIP. Bounds the *count* of queued pbufs,
* not their byte volume — lwIP's TCP_WND and MEMP_NUM_PBUF cap upstream
* receive bytes; this knob caps how many segment-sized pbufs can pile up
* behind a slow Stream_Read drain before the tcp_recv callback returns
* non-ERR_OK so lwIP retains the pbuf and replays the callback later.
*
* Default 8 — sized for the typical mTLS handshake flight (ServerHello +
* Certificate + ServerKeyExchange + ServerHelloDone is 2-4 segments; 8
* leaves margin for cert chains and renegotiation traffic).
*
* Floor: 1. Sub-floor values rejected at compile time.
*/
#ifndef SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE
#define SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE 8U
#endif

#if SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE < 1
#error "SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE must be >= 1"
#endif

/*
* Number of SolidSyslogPlusTcpTcpStream instances the library's
* internal static pool can simultaneously hold. Each instance carries
Expand Down
148 changes: 148 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,153 @@
# Dev Log

## 2026-05-28 — S28.05 SolidSyslogLwipRawTcpStream

Fifth story in E28 (#465, parent #439). TCP stream adapter for the
lwIP Raw API, plus the `docs/integrating-lwip.md` guide that's been
deferred since S28.03 landed the first OS-agnostic slice. The new
ground here is bridging lwIP's callback-driven Raw TCP API to
SolidSyslog's synchronous `Stream_Open`/`Send`/`Read`/`Close`
contract — a richer problem than the Datagram side because TCP's
`tcp_connect` is asynchronous and the receive path delivers
arbitrary-sized pbufs that the wrapper has to buffer until
`Stream_Read` drains them.

### Design decisions

The full set landed in the issue body up front (#465); five key calls.

1. **`tcp_write` uses `TCP_WRITE_FLAG_COPY`.** Deliberate divergence
from S28.04's Datagram-side `PBUF_REF`. UDP is safe with REF
because `udp_sendto` is either synchronous or `etharp_query`
clones the payload on queue. TCP `tcp_write` defers transmission
indefinitely and depends on retransmits + peer ACKs — the
caller's buffer would have to live well past `Stream_Send`
return, which the synchronous contract doesn't allow. COPY costs
one `memcpy` per send; syslog records are small, well within
budget. `tcp_output` is called after every successful `tcp_write`
to nudge transmission; `ERR_MEM` from `tcp_output` is
queued-not-failed (lwIP owns the bytes, retries on next `tcp_tmr`).

2. **Synchronous Open via spin-with-injected-sleep.** `tcp_connect`
returns immediately; the registered `connected_cb` fires when the
SYN/SYN-ACK exchange completes. The wrapper spins on a
`Connected` flag bounded by the existing per-instance
`GetConnectTimeoutMs` getter (the S12.17 pattern), sleeping
`SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS` (default 10 ms) per
iteration via the integrator-supplied `SolidSyslogSleepFunction`.
Semaphore-wakeup ruled out — under `NO_SYS=1` lwIP's callback
runs in the same thread as `Stream_Open`, so there's no other
thread to post a semaphore from; the spin+sleep pattern is the
only shape that covers both `NO_SYS=1` and `NO_SYS=0` with one
code path. Integrator's Sleep ticks `sys_check_timeouts` + RX
under `NO_SYS=1`; under `NO_SYS=0` it just yields.

3. **`tcp_close`-after-`tcp_err` gotcha encapsulated.** lwIP's
`tcp_err` callback fires *after* lwIP has already released the
pcb — calling `tcp_close` on it would be a use-after-free. The
wrapper's `tcp_err` handler nulls `self->Pcb`; `Close` checks
`Pcb != NULL` before calling `tcp_close`. Integrators never see
the rule. Documented as background only in the integrator
guide ("don't poke at lwIP pcbs through the abstraction").

4. **Bounded RX queue draining `tcp_recv`.** Fixed ring of
`struct pbuf*` pointers sized by `SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE`
(default 8 — sized for the typical mTLS handshake flight).
`tcp_recv` callback enqueues; returns `ERR_MEM` when full so
lwIP retains the pbuf and replays (flow-control). `Stream_Read`
drains bytes from the head pbuf, calls `tcp_recved(pcb, n)` to
ACK back to lwIP's window, `pbuf_free`s the head when fully
drained. Peer FIN (`tcp_recv` with `p == NULL`) sets `Errored`
but lets queued bytes drain first — Read returns `-1` only once
the queue is empty AND Errored, and closes internally per the
Stream contract at that point.

5. **`SOF_KEEPALIVE` set in `Open`.** Matches `SolidSyslogPosixTcpStream`.
The idle/intvl/cnt values come from the integrator's lwipopts.h
(`TCP_KEEPIDLE_DEFAULT` etc.); the wrapper just opts the pcb in.
No-op if `LWIP_TCP_KEEPALIVE=0` — documented as recommended-on
in the integrator guide.

### Commit shape

Four commits + one mid-story refactor:

- **Commit 1 — Pool plumbing + tunables + 13 tests.** Three-TU split
(Static.c / Messages.c / .c with empty vtable / Private.h struct).
Three new tunables (`POOL_SIZE` = 2 to match the other TCP-stream
backends, `CONNECT_POLL_MS` = 10, `RX_QUEUE_SIZE` = 8). NULL
config or NULL Sleep → silent NullStream fallback (sibling pattern;
only POOL_EXHAUSTED + UNKNOWN_DESTROY reported).
- **Commit 2 — Open/Close lifecycle + connect callback bridge.**
`LwipTcpFake` covering `tcp_new` / `tcp_arg` / `tcp_recv` /
`tcp_err` / `tcp_sent` / `tcp_connect` / `tcp_close` / `tcp_abort`
with captured callbacks tests can drive. 22 new tests.
- **Commit 3 — Send / Read / RX queue + tcp_err handling.** The
COPY-flag Send path, ERR_MEM-after-write success branch, RX-ring
drain with `tcp_recved` ACK, peer-FIN-after-drain → -1, RX-queue
drain on Close/Destroy as a leak invariant. 20 new tests.
- **Refactor — test cleanups.** David spotted three repetitions
worth lifting. `sendBytes()` / `readBytes()` helpers on the test
base (matches the Datagram precedent). `pushIncomingPbuf` returns
the `err_t` and only bumps the leak counter on `ERR_OK`, encoding
the ownership transfer in one place. `CHECK_FORWARDED_PCB` macro
for the recurring "this lwIP call received our pcb" intent. ~20
lines lighter; one inline test setup collapses to 3 lines.
- **Commit 4 — Housekeeping.** Integrator guide, CLAUDE.md rows,
CMake STATUS strings + the `:155` comment (deferred PR #464 nit),
this DEVLOG entry, pre-PR three-step.

### Test-fixture pattern that carried over

S28.04's `TEST_BASE` + two `TEST_GROUP_BASE` split (Created-only vs
Open-already) transferred 1:1. New convenience: shared `sendBuffer`
+ `readBuffer` fields with `sendBytes(len=1)` / `readBytes(cap=N)`
helpers — same shape as the Datagram tests' `sendBytes` from S28.04.
Leak invariants in shared teardown: `LwipTcpFake_OutstandingPcbCount()`
and `LwipPbufFake_OutstandingPbufCount()` both pinned to zero,
exactly mirroring the Datagram precedent.

The novel piece is the `LwipTcpFake`'s default behaviour for
`tcp_connect`: synchronously invokes the registered `connected_cb`
with `ERR_OK` before returning. That gives the happy-path Open tests
a single line of work (`stream = …Create + Open` in the Connected
group's setup) and surfaces the spin loop's Sleep-call-count as 0 on
the happy path — pinned by `OpenHappyPathDoesNotSleep`. Tests that
need to exercise the timeout / errored-cb paths call
`LwipTcpFake_SetConnectCallbackFires(false)` /
`SetConnectCallbackResult(ERR_RST)`.

### New tunables, new error source

- `SOLIDSYSLOG_LWIP_RAW_TCP_STREAM_POOL_SIZE` default `2U` — matches
every other TCP-stream backend; covers the TLS-over-plain-TCP
pair.
- `SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS` default `10U` — 20
polls inside the default 200 ms connect deadline.
- `SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE` default `8U` — sized for
the typical mTLS handshake flight (ServerHello + Certificate +
ServerKeyExchange + ServerHelloDone is 2–4 segments; 8 leaves
margin).
- `LwipRawTcpStreamErrorSource` with `POOL_EXHAUSTED` /
`UNKNOWN_DESTROY`, matching the other TCP-stream backends.

### `docs/integrating-lwip.md`

The deferred integrator guide picks up the lwipopts.h expectations
both S28.04 (ARP_QUEUEING) and S28.05 (LWIP_TCP_KEEPALIVE) surfaced,
plus the `NO_SYS=1` vs `NO_SYS=0` threading rule and the
`tcpip_callback()` marshalling convention. Worked example for the
bare-metal main loop shape; per-adapter design notes documenting the
pbuf-strategy split (UDP REF, TCP COPY) so future readers don't
re-litigate the inconsistency.

### Next up

S28.06 — `Bdd/Targets/FreeRtosLwip/` + three new CI jobs
(`build-freertos-host-tdd-lwip`, `build-freertos-target-lwip`,
`bdd-freertos-qemu-lwip`). Mirrors the existing FreeRTOS BDD target
but with the lwIP adapters in place of PlusTcp.

## 2026-05-27 — S28.04 SolidSyslogLwipRawDatagram

Fourth story in E28 (#463, parent #439). UDP datagram adapter for the
Expand Down
27 changes: 27 additions & 0 deletions Platform/LwipRaw/Interface/SolidSyslogLwipRawTcpStream.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#ifndef SOLIDSYSLOGLWIPRAWTCPSTREAM_H
#define SOLIDSYSLOGLWIPRAWTCPSTREAM_H

#include "ExternC.h"
#include "SolidSyslogSleep.h"
#include "SolidSyslogTcpConnectTimeoutFunction.h"

EXTERN_C_BEGIN

struct SolidSyslogStream;

struct SolidSyslogLwipRawTcpStreamConfig
{
SolidSyslogTcpConnectTimeoutFunction
GetConnectTimeoutMs; /* NULL → use SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS tunable */
void* ConnectTimeoutContext; /* passed through to GetConnectTimeoutMs; NULL is fine */
SolidSyslogSleepFunction
Sleep; /* required — drives the bounded-connect spin; NULL config falls back to NullStream */
};

struct SolidSyslogStream* SolidSyslogLwipRawTcpStream_Create(const struct SolidSyslogLwipRawTcpStreamConfig* config
);
void SolidSyslogLwipRawTcpStream_Destroy(struct SolidSyslogStream * base);

EXTERN_C_END

#endif /* SOLIDSYSLOGLWIPRAWTCPSTREAM_H */
21 changes: 21 additions & 0 deletions Platform/LwipRaw/Interface/SolidSyslogLwipRawTcpStreamErrors.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef SOLIDSYSLOGLWIPRAWTCPSTREAMERRORS_H
#define SOLIDSYSLOGLWIPRAWTCPSTREAMERRORS_H

#include "ExternC.h"

EXTERN_C_BEGIN

struct SolidSyslogErrorSource;

enum SolidSyslogLwipRawTcpStreamErrors
{
LWIPRAWTCPSTREAM_ERROR_POOL_EXHAUSTED,
LWIPRAWTCPSTREAM_ERROR_UNKNOWN_DESTROY,
LWIPRAWTCPSTREAM_ERROR_MAX
};

extern const struct SolidSyslogErrorSource LwipRawTcpStreamErrorSource;

EXTERN_C_END

#endif /* SOLIDSYSLOGLWIPRAWTCPSTREAMERRORS_H */
Loading
Loading