You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Third source slice of the Platform/LwipRaw/ tree. Lands the TCP
stream class — SolidSyslogLwipRawTcpStream — wrapping lwIP Raw's tcp_new / tcp_connect / tcp_write / tcp_output / tcp_recv / tcp_recved / tcp_close / tcp_abort against the SolidSyslogStream vtable. Composes against the SolidSyslogLwipRawAddress landed in S28.03 and bridges lwIP's
callback Raw TCP API to SolidSyslog's synchronous Stream_Open / Send / Read / Close contract. Also lands docs/integrating-lwip.md covering the threading + lwipopts.h
expectations both this story and S28.04 surface (the natural home for
the tcp_close-after-tcp_err background and the ARP_QUEUEING
recovery note carried over from S28.04).
Platform-agnostic by design. Per the epic's framing, Platform/LwipRaw/Source/ wraps lwIP only — zero direct calls to any
host RTOS primitive. The synchronous-Open spin loop needs a bounded
sleep; that primitive is the existing SolidSyslogSleepFunction
typedef from Core/Interface/SolidSyslogSleep.h, injected via the
Create-time config. The integrator wires the platform-appropriate
impl (vTaskDelay under FreeRTOS, usleep under POSIX, a sys_check_timeouts-plus-RX pump under bare-metal NO_SYS=1) — Platform/LwipRaw/Source/ never sees vTaskDelay or any sibling.
Mirrors the three-TU-per-class shape established by E11 (Class.c
vtable + Initialise/Cleanup, ClassPrivate.h struct + private sigs,
ClassStatic.c pool + Create/Destroy + errors, ClassMessages.c
error-source registration). Pool exhaustion and bad-config (NULL
Sleep) resolve to the shared SolidSyslogNullStream. Core untouched
modulo the three new tunable defaults.
Design choices
1. tcp_write ownership — TCP_WRITE_FLAG_COPY set
tcp_write(pcb, data, len, flags) accepts two ownership flavours:
TCP_WRITE_FLAG_COPYset — lwIP copies data into its own
pbufs before returning. Caller buffer free immediately on return.
TCP_WRITE_FLAG_COPYclear — lwIP queues a reference to the
caller's buffer. The buffer must live until lwIP's sent callback
fires (after the peer ACKs, possibly after retransmits). Cannot be
honoured from a synchronous Stream_Send(buf, len) contract whose
lifetime ends at return.
We use COPY. This deliberately diverges from the S28.04 Datagram
class's PBUF_REF — because udp_sendto either ships synchronously or
lets etharp_query clone-on-queue, so the caller's buffer is safe in
both branches. tcp_write defers transmission and depends on
retransmits + ACKs; the buffer must outlive every code path the
caller cannot observe. COPY costs one memcpy per send; syslog
records are small and the cost is well within budget.
tcp_write only queues into pcb->snd_buf; tcp_output triggers
the actual transmission. We call both in succession on every Send.
If tcp_output returns ERR_MEM the data is still queued — lwIP
retries on the next tcp_tmr tick — and we treat this as
Send-success (lwIP owns the bytes, matching POSIX's "kernel accepted
the data into the send buffer" semantics).
2. Synchronous Open via spin-with-injected-sleep
tcp_connect(pcb, ip, port, connected_cb) returns immediately. The
TCP SYN/SYN-ACK exchange completes when connected_cb fires. Our SolidSyslogStream_Open(addr) contract is synchronous.
The wrapper spins on a Connected flag set by the registered connected_cb, bounded by the existing per-instance GetConnectTimeoutMs getter (the S12.17 pattern — default tunable SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS = 200 ms). Each iteration sleeps
for SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS (new tunable, default
10 ms) via the integrator-injected SolidSyslogSleepFunction so the
spin doesn't starve lwIP's timer / RX paths.
Semaphore-wakeup ruled out. Under NO_SYS=1 lwIP's connected_cb runs in the same thread as the caller's Open (the
integrator's main loop drives both), so there is no other thread to
post a semaphore from. Spin-with-injected-sleep covers both NO_SYS=1 (integrator's Sleep ticks lwIP timers + drives RX for
N ms) and NO_SYS=0 (Sleep yields to the tcpip thread via vTaskDelay / equivalent) with one code path.
Connect failure modes:
tcp_connect returns non-ERR_OK immediately → abort pcb, Open
returns false.
connected_cb fires with non-ERR_OK (RST received during
handshake, etc.) → set Errored, abort pcb, Open returns false.
Timeout expires before either callback fires → tcp_abort the
pcb (lwIP-idiomatic way to give up on an in-flight connect),
Open returns false.
3. RX queue draining the tcp_recv callback
tcp_recv(pcb, recv_cb) registers a callback that fires when a pbuf
arrives. Our Stream_Read is synchronous.
Shape:
A bounded struct pbuf* ring on the per-pcb adapter state, sized
by the new tunable SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE
(default 8 — sized for the typical mTLS handshake flight; the
bound is on the number of pbufs queued, not bytes; lwIP's TCP_WND and MEMP_NUM_PBUF already cap the upstream byte
volume).
The tcp_recv callback enqueues the incoming pbuf and returns ERR_OK. If the queue is full it returns non-ERR_OK so lwIP
keeps the pbuf and replays the callback later (lwIP's flow-control
hook).
Stream_Read dequeues bytes from the head pbuf, calls tcp_recved(pcb, n) to ACK back to lwIP's receive window. When
the head pbuf is fully drained, pbuf_frees it and advances.
Peer half-close: tcp_recv fires with p == NULL. We set the
Errored flag; next Stream_Read returns -1 (EOF — closes
internally per the Stream contract). Drains any queued bytes
first.
4. Peer FIN / RST — tcp_close-after-tcp_err gotcha encapsulated
lwIP's tcp_err callback fires for fatal errors (RST, OOM, ABRT)
and releases the pcb before calling our callback — calling tcp_close on the same pcb afterwards is a use-after-free.
We encapsulate the rule inside the wrapper:
The tcp_err_cb we register nulls self->Pcb and sets the
Errored flag.
Stream_Close checks if (self->Pcb != NULL) tcp_close(self->Pcb);
before calling tcp_close. Idempotent and safe whether the prior
failure was a peer RST (Pcb already null), a peer FIN (Pcb still
valid), or a clean shutdown.
Integrators never see this — docs/integrating-lwip.md mentions it
only as background ("the wrapper owns its pcb lifecycle — don't
poke at lwIP pcbs through the abstraction").
5. Keepalive — SOF_KEEPALIVE set on every pcb
Match SolidSyslogPosixTcpStream's behaviour (PlusTcp doesn't,
because FreeRTOS+TCP doesn't expose per-pcb keepalive). After tcp_new we set pcb->so_options |= SOF_KEEPALIVE. The idle /
interval / probe-count values are pulled from the integrator's
lwipopts.h (TCP_KEEPIDLE_DEFAULT / TCP_KEEPINTVL_DEFAULT / TCP_KEEPCNT_DEFAULT) — we don't override them.
If the integrator compiled lwIP with LWIP_TCP_KEEPALIVE=0 the SOF_KEEPALIVE bit is a no-op — the connection still works, just
without dead-peer detection. docs/integrating-lwip.md documents LWIP_TCP_KEEPALIVE=1 as a recommended-on lwipopts setting.
6. Threading — integrator marshals tcpip_callback() at the SolidSyslog boundary
Under NO_SYS=0, lwIP requires all Raw-API calls from outside the
tcpip thread to go via tcpip_callback(). The wrapper does not
marshal internally — that would force LWIP_TCPIP_CORE_LOCKING=1 on
every integrator and pull tcpip.c into the build.
The integrator marshals at the SolidSyslog API boundary instead
(their Service loop calls tcpip_callback(do_service) which calls
into our adapter on the tcpip thread). docs/integrating-lwip.md
documents this rule. Same shape as S28.04 Datagram.
7. Failure mapping to the SolidSyslogStream contract
Platform/LwipRaw/CMakeLists.txt adds the four new sources.
TcpStream struct shape (Private.h)
structSolidSyslogLwipRawTcpStream
{
structSolidSyslogStreamBase;
structSolidSyslogLwipRawTcpStreamConfigConfig;
structtcp_pcb*Pcb;
boolConnected; /* set by connected_cb on ERR_OK */boolErrored; /* latched until Close */structpbuf*RxQueue[SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE];
size_tRxQueueHead; /* index of next pbuf to drain */size_tRxQueueCount; /* number of pbufs currently queued */size_tRxHeadOffset; /* bytes already consumed from RxQueue[RxQueueHead] */
};
Sleep is required — the synchronous-Open spin loop has nowhere
to go without it. NULL Sleep is treated as bad config and Create
resolves to SolidSyslogNullStream (matches the pattern used by SolidSyslogUdpSender for NULL Address).
LwipTcpFake_OutstandingPcbCount (leak invariant — every tcp_new pcb reclaimed by tcp_close / tcp_abort / null-via- tcp_err).
Sources inline into the test exe via the existing LWIP_FAKE_SOURCES variable in Tests/Lwip/CMakeLists.txt.
Test file — Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp
TEST_BASE + TEST_GROUP_BASE split between "Created-only" and
"Open + Connected" groups. Mirrors Tests/FreeRtos/SolidSyslogPlusTcpTcpStreamTest.cpp
and the S28.04 Datagram test shape (leak invariants in shared
teardown, sendBytes-style helper). Note that test names are
indicative — exact phrasings settle during the red/green/refactor
loop, and the list may grow or shrink as the design lands.
Created-only group:
CreateReturnsNonNullStream
CreateWithNullSleepReturnsFallback (Null Object — calls to
fallback Send return false, Read returns -1, Close is no-op)
CreateZeroesPcbAndQueueState
Ten pool tests (mirror PlusTcpTcpStream's pool block 1:1 with
names substituted: FillPool / Overflow / ExhaustedReportsError /
FallbackVtableMethodsAreNoOps / four ConfigLock tests /
two Destroy-of-unknown / Destroy-of-stale)
New executable SolidSyslogLwipRawTcpStreamTest registered
alongside Address / Resolver / Datagram. Sources: test cpp + the
three TcpStream production TUs + the three Address production TUs.
Links LwipTcpFake + LwipPbufFake + ConfigLockFake +
ErrorHandlerFake + CppUTest.
misra_suppressions.txt
Expect at minimum misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:<line>
(SelfFromBase — same shape as every other adapter, falls under
D.002). If cppcheck-MISRA surfaces additional entries, each gets a
header pointing at the matching sibling-backend entry and a
cross-reference in docs/misra-deviations.md.
docs/integrating-lwip.md — new file
Covers:
NO_SYS=1 vs NO_SYS=0: positioning paragraphs. SolidSyslog
supports both; the differences are entirely in how the integrator
drives lwIP forward (main-loop pump vs tcpip thread).
tcpip_callback() threading rule (NO_SYS=0 only): all
Raw-API calls from outside the tcpip thread must be marshalled.
Integrator marshals at the SolidSyslog API boundary
(Service / Log entry), not inside the wrapper.
lwipopts.h expectations: LWIP_RAW=1, LWIP_UDP=1, LWIP_TCP=1, ARP_QUEUEING=1 (Datagram first-packet recovery —
the S28.04 footgun), LWIP_TCP_KEEPALIVE=1 recommended (with TCP_KEEPIDLE_DEFAULT / TCP_KEEPINTVL_DEFAULT / TCP_KEEPCNT_DEFAULT tuning), TCP_MSS / PBUF_POOL_SIZE sizing
notes, MEMP_NUM_TCP_PCB sizing for the pool default.
Sleep callback wiring for the TcpStream: integrator-supplied.
Examples: FreeRTOS vTaskDelay(pdMS_TO_TICKS(ms)); POSIX usleep((useconds_t) ms * 1000U); bare-metal "tick sys_check_timeouts() + drive RX for ms milliseconds".
Background notes: pbuf strategy differs UDP (REF) vs TCP
(COPY); the wrapper owns its pcb lifecycle so integrators never
call tcp_close directly through the abstraction.
CLAUDE.md updates
Two new rows in the public-headers table, slotted near the existing
LwipRaw rows:
Header
Audience
Provides
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 + Sleep), SolidSyslogLwipRawTcpStream_Create(config), _Destroy(base) (wraps tcp_new / tcp_connect / tcp_write (COPY) / tcp_output / tcp_recv / tcp_recved / tcp_close / tcp_abort; integrator-supplied Sleep drives the bounded connect spin; sets SOF_KEEPALIVE; bounded RX pbuf queue via SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE). Instance struct lives in a library-internal static pool (E11); default pool size 2 to support the future TLS-over-plain-TCP pair. Pool-exhaustion and bad-config fallback is the shared SolidSyslogNullStream.
SolidSyslogLwipRawTcpStreamErrors.h
Any code installing an error handler that wants to react to LwipRawTcpStream-specific events…
Update both message(STATUS) strings for SOLIDSYSLOG_FREERTOS_NET=LWIP and =BOTH: drop "TcpStream" from
the remaining list — only "BDD arrives in S28.06" left.
Update the matching comment block at CMakeLists.txt:155 so its
narrative tracks the STATUS strings (the deferred CodeRabbit nit
from PR feat: S28.04 SolidSyslogLwipRawDatagram #464).
Acceptance
cmake --preset freertos-cross -DSOLIDSYSLOG_FREERTOS_NET=LWIP
builds with the new TcpStream sources in the graph.
cmake --preset freertos-cross -DSOLIDSYSLOG_FREERTOS_NET=BOTH
builds and links both PlusTcp TcpStream and LwipRaw TcpStream
sources.
New host-side test SolidSyslogLwipRawTcpStreamTest passes on the cpputest-freertos image (the only one shipping lwIP today;
skipped under base cpputest / cpputest-clang per the LWIP_PATH guard locked in at S28.03).
100% line + branch coverage on the new sources.
All four PlusTcp host-tests (Address, Datagram, Resolver,
TcpStream) still pass under =PLUSTCP and =BOTH.
Parent epic: #439 (E28: lwIP Raw API support)
Third source slice of the
Platform/LwipRaw/tree. Lands the TCPstream class —
SolidSyslogLwipRawTcpStream— wrapping lwIP Raw'stcp_new/tcp_connect/tcp_write/tcp_output/tcp_recv/tcp_recved/tcp_close/tcp_abortagainst theSolidSyslogStreamvtable. Composes against theSolidSyslogLwipRawAddresslanded in S28.03 and bridges lwIP'scallback Raw TCP API to SolidSyslog's synchronous
Stream_Open / Send / Read / Closecontract. Also landsdocs/integrating-lwip.mdcovering the threading + lwipopts.hexpectations both this story and S28.04 surface (the natural home for
the
tcp_close-after-tcp_errbackground and theARP_QUEUEINGrecovery note carried over from S28.04).
Platform-agnostic by design. Per the epic's framing,
Platform/LwipRaw/Source/wraps lwIP only — zero direct calls to anyhost RTOS primitive. The synchronous-Open spin loop needs a bounded
sleep; that primitive is the existing
SolidSyslogSleepFunctiontypedef from
Core/Interface/SolidSyslogSleep.h, injected via theCreate-time config. The integrator wires the platform-appropriate
impl (
vTaskDelayunder FreeRTOS,usleepunder POSIX, asys_check_timeouts-plus-RX pump under bare-metalNO_SYS=1) —Platform/LwipRaw/Source/never seesvTaskDelayor any sibling.Mirrors the three-TU-per-class shape established by E11 (Class.c
vtable + Initialise/Cleanup, ClassPrivate.h struct + private sigs,
ClassStatic.c pool + Create/Destroy + errors, ClassMessages.c
error-source registration). Pool exhaustion and bad-config (NULL
Sleep) resolve to the shared
SolidSyslogNullStream. Core untouchedmodulo the three new tunable defaults.
Design choices
1.
tcp_writeownership —TCP_WRITE_FLAG_COPYsettcp_write(pcb, data, len, flags)accepts two ownership flavours:TCP_WRITE_FLAG_COPYset — lwIP copiesdatainto its ownpbufs before returning. Caller buffer free immediately on return.
TCP_WRITE_FLAG_COPYclear — lwIP queues a reference to thecaller's buffer. The buffer must live until lwIP's
sentcallbackfires (after the peer ACKs, possibly after retransmits). Cannot be
honoured from a synchronous
Stream_Send(buf, len)contract whoselifetime ends at return.
We use COPY. This deliberately diverges from the S28.04 Datagram
class's PBUF_REF — because
udp_sendtoeither ships synchronously orlets
etharp_queryclone-on-queue, so the caller's buffer is safe inboth branches.
tcp_writedefers transmission and depends onretransmits + ACKs; the buffer must outlive every code path the
caller cannot observe. COPY costs one
memcpyper send; syslogrecords are small and the cost is well within budget.
tcp_writeonly queues intopcb->snd_buf;tcp_outputtriggersthe actual transmission. We call both in succession on every Send.
If
tcp_outputreturnsERR_MEMthe data is still queued — lwIPretries on the next
tcp_tmrtick — and we treat this asSend-success (lwIP owns the bytes, matching POSIX's "kernel accepted
the data into the send buffer" semantics).
2. Synchronous Open via spin-with-injected-sleep
tcp_connect(pcb, ip, port, connected_cb)returns immediately. TheTCP SYN/SYN-ACK exchange completes when
connected_cbfires. OurSolidSyslogStream_Open(addr)contract is synchronous.The wrapper spins on a
Connectedflag set by the registeredconnected_cb, bounded by the existing per-instanceGetConnectTimeoutMsgetter (the S12.17 pattern — default tunableSOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS= 200 ms). Each iteration sleepsfor
SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS(new tunable, default10 ms) via the integrator-injected
SolidSyslogSleepFunctionso thespin doesn't starve lwIP's timer / RX paths.
Semaphore-wakeup ruled out. Under
NO_SYS=1lwIP'sconnected_cbruns in the same thread as the caller's Open (theintegrator's main loop drives both), so there is no other thread to
post a semaphore from. Spin-with-injected-sleep covers both
NO_SYS=1(integrator's Sleep ticks lwIP timers + drives RX forN ms) and
NO_SYS=0(Sleep yields to the tcpip thread viavTaskDelay/ equivalent) with one code path.Connect failure modes:
tcp_connectreturns non-ERR_OKimmediately → abort pcb, Openreturns false.
connected_cbfires with non-ERR_OK(RST received duringhandshake, etc.) → set Errored, abort pcb, Open returns false.
tcp_abortthepcb (lwIP-idiomatic way to give up on an in-flight connect),
Open returns false.
3. RX queue draining the
tcp_recvcallbacktcp_recv(pcb, recv_cb)registers a callback that fires when a pbufarrives. Our
Stream_Readis synchronous.Shape:
struct pbuf*ring on the per-pcb adapter state, sizedby the new tunable
SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE(default 8 — sized for the typical mTLS handshake flight; the
bound is on the number of pbufs queued, not bytes; lwIP's
TCP_WNDandMEMP_NUM_PBUFalready cap the upstream bytevolume).
tcp_recvcallback enqueues the incoming pbuf and returnsERR_OK. If the queue is full it returns non-ERR_OKso lwIPkeeps the pbuf and replays the callback later (lwIP's flow-control
hook).
Stream_Readdequeues bytes from the head pbuf, callstcp_recved(pcb, n)to ACK back to lwIP's receive window. Whenthe head pbuf is fully drained,
pbuf_frees it and advances.tcp_recvfires withp == NULL. We set theErrored flag; next
Stream_Readreturns -1 (EOF — closesinternally per the Stream contract). Drains any queued bytes
first.
4. Peer FIN / RST —
tcp_close-after-tcp_errgotcha encapsulatedlwIP's
tcp_errcallback fires for fatal errors (RST, OOM, ABRT)and releases the pcb before calling our callback — calling
tcp_closeon the same pcb afterwards is a use-after-free.We encapsulate the rule inside the wrapper:
tcp_err_cbwe register nullsself->Pcband sets theErrored flag.
Stream_Closechecksif (self->Pcb != NULL) tcp_close(self->Pcb);before calling
tcp_close. Idempotent and safe whether the priorfailure was a peer RST (Pcb already null), a peer FIN (Pcb still
valid), or a clean shutdown.
Integrators never see this —
docs/integrating-lwip.mdmentions itonly as background ("the wrapper owns its pcb lifecycle — don't
poke at lwIP pcbs through the abstraction").
5. Keepalive —
SOF_KEEPALIVEset on every pcbMatch
SolidSyslogPosixTcpStream's behaviour (PlusTcp doesn't,because FreeRTOS+TCP doesn't expose per-pcb keepalive). After
tcp_newwe setpcb->so_options |= SOF_KEEPALIVE. The idle /interval / probe-count values are pulled from the integrator's
lwipopts.h (
TCP_KEEPIDLE_DEFAULT/TCP_KEEPINTVL_DEFAULT/TCP_KEEPCNT_DEFAULT) — we don't override them.If the integrator compiled lwIP with
LWIP_TCP_KEEPALIVE=0theSOF_KEEPALIVEbit is a no-op — the connection still works, justwithout dead-peer detection.
docs/integrating-lwip.mddocumentsLWIP_TCP_KEEPALIVE=1as a recommended-on lwipopts setting.6. Threading — integrator marshals
tcpip_callback()at the SolidSyslog boundaryUnder
NO_SYS=0, lwIP requires all Raw-API calls from outside thetcpip thread to go via
tcpip_callback(). The wrapper does notmarshal internally — that would force
LWIP_TCPIP_CORE_LOCKING=1onevery integrator and pull
tcpip.cinto the build.The integrator marshals at the SolidSyslog API boundary instead
(their Service loop calls
tcpip_callback(do_service)which callsinto our adapter on the tcpip thread).
docs/integrating-lwip.mddocuments this rule. Same shape as S28.04 Datagram.
7. Failure mapping to the
SolidSyslogStreamcontractSolidSyslogStreamcontract (perCore/Interface/SolidSyslogStream.h):Opentrue/false; on failure underlying state is closedinternally, caller can Open again to retry.
Sendtrue only if the entire frame is accepted in one call; anyfailure closes internally.
Read> 0 bytes, = 0 would-block, < 0 EOF/error (closesinternally).
Closeidempotent.Mapping to lwIP:
SolidSyslogStreamresulttcp_newreturns NULLtcp_connectimmediate non-ERR_OKconnected_cbnon-ERR_OKtcp_writenon-ERR_OKtcp_outputERR_MEMafter successfultcp_writetcp_outputother non-ERR_OKtcp_errcallback firestcp_recvwithp == NULL)Scope
New files —
Platform/LwipRaw/Platform/LwipRaw/CMakeLists.txtadds the four new sources.TcpStream struct shape (Private.h)
Config struct (Interface.h)
Sleepis required — the synchronous-Open spin loop has nowhereto go without it. NULL Sleep is treated as bad config and
Createresolves to
SolidSyslogNullStream(matches the pattern used bySolidSyslogUdpSenderfor NULL Address).Public Create / Destroy
New tunables —
Core/Interface/SolidSyslogTunablesDefaults.hSOLIDSYSLOG_LWIP_RAW_TCP_STREAM_POOL_SIZE2USOLIDSYSLOG_POSIX_TCP_STREAM_POOL_SIZE/SOLIDSYSLOG_WINSOCK_TCP_STREAM_POOL_SIZE/SOLIDSYSLOG_PLUS_TCP_TCP_STREAM_POOL_SIZEdefaults — covers the TLS-over-plain-TCP pair.SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS10SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE8TCP_WND/MEMP_NUM_PBUFcap upstream byte volume). Sized for the typical mTLS handshake flight (ServerHello + Certificate + ServerKeyExchange + ServerHelloDone is 2-4 segments; 8 leaves margin).Test scaffolding —
Tests/Support/LwipFakes/Extends the LwipFakes tree added in S28.04 with one new fake —
LwipTcpFake.{c,h}— covering:tcp_new: counter, last-args, failure-injection (return NULL).tcp_arg: counter, last-args (captures theargwe register).tcp_err: counter, captures theerr_cbso tests can drive it.tcp_recv: counter, captures therecv_cbso tests can driveincoming pbufs.
tcp_sent: counter, captures thesent_cb(we register a no-opfor COPY mode but lwIP requires it set).
tcp_connect: counter, last-args, captures theconnected_cb,failure-injection (immediate non-
ERR_OK).tcp_write: counter, last-args, failure-injection.tcp_output: counter, last-pcb, failure-injection.tcp_recved: counter, last-args (window-update ACK).tcp_close: counter, last-pcb, failure-injection.tcp_abort: counter, last-pcb.LwipTcpFake_OutstandingPcbCount(leak invariant — everytcp_newpcb reclaimed bytcp_close/tcp_abort/ null-via-tcp_err).Sources inline into the test exe via the existing
LWIP_FAKE_SOURCESvariable inTests/Lwip/CMakeLists.txt.Test file —
Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cppTEST_BASE + TEST_GROUP_BASE split between "Created-only" and
"Open + Connected" groups. Mirrors
Tests/FreeRtos/SolidSyslogPlusTcpTcpStreamTest.cppand the S28.04 Datagram test shape (leak invariants in shared
teardown,
sendBytes-style helper). Note that test names areindicative — exact phrasings settle during the red/green/refactor
loop, and the list may grow or shrink as the design lands.
Created-only group:
CreateReturnsNonNullStreamCreateWithNullSleepReturnsFallback(Null Object — calls tofallback Send return false, Read returns -1, Close is no-op)
CreateZeroesPcbAndQueueStatenames substituted: FillPool / Overflow / ExhaustedReportsError /
FallbackVtableMethodsAreNoOps / four ConfigLock tests /
two Destroy-of-unknown / Destroy-of-stale)
OpenCallsTcpNewOpenReturnsFalseWhenTcpNewFailsOpenCallsTcpConnectOpenRegistersTcpArgRecvErrSentCallbacksOpenSetsKeepaliveOnPcbOpenReturnsTrueWhenConnectedCallbackFiresOkOpenReturnsFalseAndAbortsWhenConnectedCallbackFiresErroredOpenReturnsFalseAndAbortsOnTimeoutOpenSleepsBetweenPollsOpenRespectsRuntimeTunableConnectTimeoutSendBeforeOpenReturnsFalseReadBeforeOpenReturnsMinusOneCloseBeforeOpenIsNoOpDestroyBeforeOpenIsNoOpConnected group (Open called in setup):
SendCallsTcpWriteWithCopyFlagSendCallsTcpOutputAfterTcpWriteSendReturnsTrueOnTcpWriteOkSendReturnsFalseAndClosesOnTcpWriteFailsSendReturnsTrueWhenTcpOutputDefersWithErrMemSendReturnsFalseAndClosesOnTcpOutputOtherErrorReadReturnsZeroWhenQueueEmptyReadReturnsBytesFromHeadPbufReadCallsTcpRecvedAfterDrainReadAdvancesPastDrainedPbufReadDrainsBeforeReportingPeerFinReadReturnsMinusOneOnPeerFinAfterDrainReadReturnsMinusOneAfterTcpErrRecvCallbackBackpressuresWhenQueueFull(returns non-ERR_OK)TcpErrCallbackNullsPcbAndSetsErroredCloseAfterTcpErrDoesNotCallTcpClose(encapsulated gotcha,Decision 4)
CloseAfterPeerFinCallsTcpCloseCloseDrainsRxQueueCloseIsIdempotentDestroyClosesOpenPcbDestroyDrainsRxQueueOnCleanup(leak invariant)DestroyAfterCloseDoesNotCloseAgainTests/Lwip/CMakeLists.txtNew executable
SolidSyslogLwipRawTcpStreamTestregisteredalongside Address / Resolver / Datagram. Sources: test cpp + the
three TcpStream production TUs + the three Address production TUs.
Links
LwipTcpFake+LwipPbufFake+ ConfigLockFake +ErrorHandlerFake + CppUTest.
misra_suppressions.txtExpect at minimum
misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:<line>(SelfFromBase — same shape as every other adapter, falls under
D.002). If cppcheck-MISRA surfaces additional entries, each gets a
header pointing at the matching sibling-backend entry and a
cross-reference in
docs/misra-deviations.md.docs/integrating-lwip.md— new fileCovers:
NO_SYS=1vsNO_SYS=0: positioning paragraphs. SolidSyslogsupports both; the differences are entirely in how the integrator
drives lwIP forward (main-loop pump vs tcpip thread).
tcpip_callback()threading rule (NO_SYS=0only): allRaw-API calls from outside the tcpip thread must be marshalled.
Integrator marshals at the SolidSyslog API boundary
(Service / Log entry), not inside the wrapper.
lwipopts.hexpectations:LWIP_RAW=1,LWIP_UDP=1,LWIP_TCP=1,ARP_QUEUEING=1(Datagram first-packet recovery —the S28.04 footgun),
LWIP_TCP_KEEPALIVE=1recommended (withTCP_KEEPIDLE_DEFAULT/TCP_KEEPINTVL_DEFAULT/TCP_KEEPCNT_DEFAULTtuning),TCP_MSS/PBUF_POOL_SIZEsizingnotes,
MEMP_NUM_TCP_PCBsizing for the pool default.Examples: FreeRTOS
vTaskDelay(pdMS_TO_TICKS(ms)); POSIXusleep((useconds_t) ms * 1000U); bare-metal "ticksys_check_timeouts()+ drive RX for ms milliseconds".SOLIDSYSLOG_LWIP_RAW_*_POOL_SIZE,SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS(existing per-instancegetter),
SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS,SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE.(COPY); the wrapper owns its pcb lifecycle so integrators never
call
tcp_closedirectly through the abstraction.CLAUDE.md updates
Two new rows in the public-headers table, slotted near the existing
LwipRaw rows:
SolidSyslogLwipRawTcpStream.hSolidSyslogTlsStream/SolidSyslogMbedTlsStream)SolidSyslogLwipRawTcpStreamConfig(GetConnectTimeoutMs + context + Sleep),SolidSyslogLwipRawTcpStream_Create(config),_Destroy(base)(wrapstcp_new/tcp_connect/tcp_write(COPY) /tcp_output/tcp_recv/tcp_recved/tcp_close/tcp_abort; integrator-supplied Sleep drives the bounded connect spin; setsSOF_KEEPALIVE; bounded RX pbuf queue viaSOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE). Instance struct lives in a library-internal static pool (E11); default pool size 2 to support the future TLS-over-plain-TCP pair. Pool-exhaustion and bad-config fallback is the sharedSolidSyslogNullStream.SolidSyslogLwipRawTcpStreamErrors.henum SolidSyslogLwipRawTcpStreamErrors(LWIPRAWTCPSTREAM_ERROR_*+_MAX),LwipRawTcpStreamErrorSource.Top-level
CMakeLists.txthousekeepingmessage(STATUS)strings forSOLIDSYSLOG_FREERTOS_NET=LWIPand=BOTH: drop "TcpStream" fromthe remaining list — only "BDD arrives in S28.06" left.
CMakeLists.txt:155so itsnarrative tracks the STATUS strings (the deferred CodeRabbit nit
from PR feat: S28.04 SolidSyslogLwipRawDatagram #464).
Acceptance
cmake --preset freertos-cross -DSOLIDSYSLOG_FREERTOS_NET=LWIPbuilds with the new TcpStream sources in the graph.
cmake --preset freertos-cross -DSOLIDSYSLOG_FREERTOS_NET=BOTHbuilds and links both PlusTcp TcpStream and LwipRaw TcpStream
sources.
SolidSyslogLwipRawTcpStreamTestpasses on thecpputest-freertosimage (the only one shipping lwIP today;skipped under base
cpputest/cpputest-clangper theLWIP_PATHguard locked in at S28.03).TcpStream) still pass under
=PLUSTCPand=BOTH.analyze-format, analyze-iwyu — clean.
grep -rn 'LwipRaw' Core/returns only the existingS28.03/S28.04 tunable lines plus the three new ones added in this
story — no other Core touched.
docs/integrating-lwip.mdexists.CMakeLists.txt:155align — the deferred PR feat: S28.04 SolidSyslogLwipRawDatagram #464 nit is closed.
Out of scope
Bdd/Targets/FreeRtosLwip/+ three new CI jobs (host-tdd-lwip,target-lwip, qemu-lwip) — S28.06.
SolidSyslogLwipRawDnsResolver.epic scope.
tcpip_callback()from the wrapper(Decision 6 — integrator's responsibility).
SolidSyslogSemaphoreabstraction for the connect-wait(ruled out — would not cover
NO_SYS=1).Breaking changes
None — entirely new files behind two new headers.