diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78471e8c..4d24a5aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -546,6 +546,72 @@ jobs: path: build/iwyu/iwyu-output.txt retention-days: 1 + # NET=LWIP twin of analyze-tidy-freertos-plustcp. Same cpputest-freertos image + # and tidy preset, but SOLIDSYSLOG_FREERTOS_NET=LWIP so the configure exercises + # the lwIP wiring path (Platform/PlusTcp library target dropped; the LwipRaw + # adapter pack is the active FreeRTOS networking backend). Guards the NET=LWIP + # configure + the LwipRaw tree / Tests under clang-tidy as a named required check. + analyze-tidy-freertos-lwip: + runs-on: ubuntu-latest + container: + image: ghcr.io/davidcozens/cpputest-freertos:sha-a0c1c0a + options: --user root + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: safe.directory + GIT_CONFIG_VALUE_0: '*' + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Configure + run: cmake --preset tidy -DSOLIDSYSLOG_FREERTOS_NET=LWIP + + - name: Build with clang-tidy + shell: bash + run: set -o pipefail && cmake --build --preset tidy 2>&1 | tee build/tidy/clang-tidy-output.txt + + - name: Upload clang-tidy output + if: success() || failure() + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: clang-tidy-report-freertos-lwip + path: build/tidy/clang-tidy-output.txt + retention-days: 1 + + # NET=LWIP twin of analyze-iwyu-freertos-plustcp — see that lane for the + # clang-19 override rationale and analyze-tidy-freertos-lwip for the NET scope. + # Advisory per S24.13. + analyze-iwyu-freertos-lwip: + runs-on: ubuntu-latest + container: + image: ghcr.io/davidcozens/cpputest-freertos:sha-a0c1c0a + options: --user root + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: safe.directory + GIT_CONFIG_VALUE_0: '*' + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Configure + run: cmake --preset iwyu -DSOLIDSYSLOG_FREERTOS_NET=LWIP -DCMAKE_C_COMPILER=clang-19 -DCMAKE_CXX_COMPILER=clang++-19 + + # Advisory per S24.13 — see analyze-iwyu above for rationale. + - name: Run include-what-you-use (advisory) + shell: bash + continue-on-error: true + run: set -o pipefail && cmake --build --preset iwyu --target iwyu 2>&1 | tee build/iwyu/iwyu-output.txt + + - name: Upload iwyu output + if: success() || failure() + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: iwyu-report-freertos-lwip + path: build/iwyu/iwyu-output.txt + retention-days: 1 + analyze-format: runs-on: ubuntu-latest container: @@ -1014,11 +1080,11 @@ jobs: path: Bdd/features/steps/solidsyslog_tunables.py retention-days: 1 - # Advisory (S28.07) — promoted to a required check in S28.11. Proves the + # Required as of S28.11 (in the `summary` needs list). Proves the # Platform/LwipRaw tree cross-builds for FreeRTOS/ARM under - # SOLIDSYSLOG_FREERTOS_NET=LWIP with zero PlusTcp dependency. Deliberately - # absent from the `summary` needs list and the branch-protection required - # set so a break here does not block merges yet. No QEMU run. + # SOLIDSYSLOG_FREERTOS_NET=LWIP with zero PlusTcp dependency (the symbol + # assertion below enforces that). No QEMU run. Adding it to the GitHub + # branch-protection required-checks set is a separate manual step (David). build-freertos-target-lwip: runs-on: ubuntu-latest permissions: @@ -1120,8 +1186,10 @@ jobs: if: failure() run: docker compose -f ci/docker-compose.bdd.yml logs --no-color - # Advisory (S28.09): not in summary.needs, so a failure does not block - # merges while the lwIP netif beds in. S28.11 promotes it to required. + # Required as of S28.11 (in the `summary` needs list): runs the lwIP BDD + # target on QEMU against the syslog-ng oracle over UDP / TCP / TLS / mTLS. + # Adding it to the GitHub branch-protection required-checks set is a + # separate manual step (David). bdd-freertos-qemu-lwip: needs: build-freertos-target-lwip runs-on: ubuntu-latest @@ -1176,7 +1244,7 @@ jobs: summary: if: always() && github.event_name == 'pull_request' - needs: [build-linux-gcc, build-linux-tunable-override, build-linux-clang, sanitize-linux-gcc, coverage-linux-gcc, analyze-tidy, analyze-tidy-freertos-plustcp, analyze-cppcheck, analyze-format, analyze-iwyu, analyze-iwyu-freertos-plustcp, bdd-linux-syslog-ng, build-windows-msvc, bdd-windows-otel, integration-linux-openssl, integration-linux-mbedtls, integration-windows-openssl, build-freertos-host-tdd-plustcp, build-freertos-target-plustcp, bdd-freertos-qemu-plustcp] + needs: [build-linux-gcc, build-linux-tunable-override, build-linux-clang, sanitize-linux-gcc, coverage-linux-gcc, analyze-tidy, analyze-tidy-freertos-plustcp, analyze-tidy-freertos-lwip, analyze-cppcheck, analyze-format, analyze-iwyu, analyze-iwyu-freertos-plustcp, analyze-iwyu-freertos-lwip, bdd-linux-syslog-ng, build-windows-msvc, bdd-windows-otel, integration-linux-openssl, integration-linux-mbedtls, integration-windows-openssl, build-freertos-host-tdd-plustcp, build-freertos-target-plustcp, bdd-freertos-qemu-plustcp, build-freertos-target-lwip, bdd-freertos-qemu-lwip] runs-on: ubuntu-latest permissions: contents: read @@ -1274,6 +1342,11 @@ jobs: "name": "bdd-freertos-qemu-plustcp", "pattern": "**/junit-bdd-freertos-qemu-plustcp/TESTS-*.xml" }, + { + "id": "junit", + "name": "bdd-freertos-qemu-lwip", + "pattern": "**/junit-bdd-freertos-qemu-lwip/TESTS-*.xml" + }, { "id": "junit", "name": "build-windows-msvc", diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_LwipRawTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_LwipRawTcp.c new file mode 100644 index 00000000..88820d2a --- /dev/null +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_LwipRawTcp.c @@ -0,0 +1,422 @@ +/* MbedTLS-over-LwipRawTcpStream BDD TLS sender (S28.11). + * + * Composes: + * - SolidSyslogLwipRawTcpStream inner TCP transport (lwIP Raw API, NO_SYS=0) + * - SolidSyslogMbedTlsStream TLS over the injected Stream + * - SolidSyslogStreamSender RFC 6587 octet-counting framing + * + * Network-backend twin of BddTargetTlsSender_MbedTls_PlusTcpTcp.c: identical + * mbedTLS init / entropy / DRBG / baked-PEM machinery, with the FreeRTOS-Plus-TCP + * inner stream swapped for the OS-agnostic LwipRaw adapter. The mbedTLS adapter + * takes pre-built handles (mbedtls_ctr_drbg, mbedtls_x509_crt, mbedtls_pk_context) + * rather than file paths, because MBEDTLS_FS_IO is disabled in the integrator + * config — there is no host path reachable from QEMU. The demo CA / client cert / + * client key PEMs travel as `static const` arrays in rodata, baked at CMake-time + * by xxd -i from Bdd/syslog-ng/tls/ ca.pem / client.pem / client.key. The arrays + * are parsed once on first BddTargetTlsSender_Create call. + * + * Entropy + CTR_DRBG also live in this TU rather than in main.c so all + * mbedTLS-specific state is one file's responsibility. The entropy source + * is deliberately weak — see DemoEntropySource — and an audit-trail + * WARNING is emitted via printf on first init. + */ + +#include "BddTargetTlsSender.h" + +#include "BddTargetMtlsConfig.h" +#include "BddTargetSwitchConfig.h" +#include "BddTargetTlsConfig.h" +#include "SolidSyslogLwipRawAddress.h" +#include "SolidSyslogLwipRawTcpStream.h" +#include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogNullSender.h" +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamSender.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "BddBakedCaPem.h" +#include "BddBakedClientCertPem.h" +#include "BddBakedClientKeyPem.h" + +struct SolidSyslogResolver; + +static struct SolidSyslogStream* underlyingStream; +static struct SolidSyslogStream* tlsStream; +static struct SolidSyslogAddress* address; +static struct SolidSyslogSender* sender; + +/* Entropy + CTR_DRBG live for the lifetime of the BDD target; one-shot init + * gated on `mbedTlsInitialised`. mbedTLS structs are zero-initialised by + * static storage; mbedtls_*_init is still called explicitly because the + * library treats post-init / post-free as the same state. */ +static mbedtls_entropy_context entropy; +static mbedtls_ctr_drbg_context drbg; +static bool mbedTlsInitialised; + +/* mbedtls_x509_crt_parse / mbedtls_pk_parse_key require NUL-terminated PEM + * input. xxd -i doesn't append a NUL, so we hold a +1-sized copy here and + * NUL-terminate at parse time. The cost is one extra byte per PEM in BSS; + * cheaper than the shell pipeline that would be needed to bake the NUL at + * CMake time. */ +static unsigned char caPemBuf[sizeof(bdd_baked_ca_pem) + 1U]; +static unsigned char clientCertPemBuf[sizeof(bdd_baked_client_cert_pem) + 1U]; +static unsigned char clientKeyPemBuf[sizeof(bdd_baked_client_key_pem) + 1U]; + +static mbedtls_x509_crt caChain; +static mbedtls_x509_crt clientCertChain; +static mbedtls_pk_context clientKey; + +/* mbedTLS allocates its per-SSL-context IN/OUT buffers (~6 KiB combined) and + * handshake state (~10 KiB) via libc calloc — on this FreeRTOS target that + * funnels through newlib's tiny syscall heap (~4 KiB, see Common/Syscalls.c), + * which can't satisfy a single TLS context. Redirect to pvPortMalloc so + * mbedTLS allocates from the 96 KiB FreeRTOS heap_4 region instead — the + * standard FreeRTOS+mbedTLS integration. Gated on MBEDTLS_PLATFORM_MEMORY in + * mbedtls_user_config.h. The zero-fill mirrors libc calloc's contract. */ +static void* FreeRtosMbedTlsCalloc(size_t nmemb, size_t size) +{ + void* result = NULL; + /* Guard the multiplication against wrap BEFORE multiplying: a maliciously + * huge request must not underestimate the allocation size, or pvPortMalloc + * would hand back a too-small buffer the caller then writes past. With size + * already known non-zero, nmemb <= SIZE_MAX / size proves nmemb * size fits. */ + if ((nmemb != 0U) && (size != 0U) && (nmemb <= (SIZE_MAX / size))) + { + size_t bytes = nmemb * size; + result = pvPortMalloc(bytes); + if (result != NULL) + { + memset(result, 0, bytes); + } + } + return result; +} + +static void FreeRtosMbedTlsFree(void* ptr) +{ + if (ptr != NULL) + { + vPortFree(ptr); + } +} + +/* PSA crypto's randomness hook (gated on MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG in + * the user config). mbedTLS 3.6's TLS 1.3 path drives PSA crypto, and PSA's + * built-in entropy collector returns PSA_ERROR_INSUFFICIENT_ENTROPY on + * MBEDTLS_NO_PLATFORM_ENTROPY targets — bypass it by feeding PSA from the + * same CTR_DRBG the classic mbedTLS API already uses. The DRBG must be + * seeded before psa_crypto_init() so the first PSA crypto operation can + * draw bytes — EnsureMbedTlsInitialised below enforces that ordering. */ +psa_status_t mbedtls_psa_external_get_random( + mbedtls_psa_external_random_context_t* context, + uint8_t* output, + size_t outputSize, + size_t* outputLength +) +{ + (void) context; + if (mbedtls_ctr_drbg_random(&drbg, output, outputSize) != 0) + { + return PSA_ERROR_GENERIC_ERROR; + } + *outputLength = outputSize; + return PSA_SUCCESS; +} + +/* Demo-only entropy: XOR the FreeRTOS tick count, a per-call counter, and + * the destination address (which varies per call) into each output byte. + * Quality is intentionally terrible — QEMU has no real source — and the + * "demo-only entropy" printf emit at the end of EnsureMbedTlsInitialised + * makes that explicit. Real integrators on bare-metal would replace this + * with TRNG / HSM bytes. */ +static int DemoEntropySource(void* data, unsigned char* output, size_t len, size_t* olen) +{ + (void) data; + static uint32_t counter = 0U; + for (size_t i = 0; i < len; i++) + { + counter++; + uint32_t mix = (uint32_t) xTaskGetTickCount() ^ counter ^ (uint32_t) (uintptr_t) &output[i]; + output[i] = (unsigned char) ((mix >> ((i % 4U) * 8U)) & 0xFFU); + } + *olen = len; + return 0; +} + +static void RtosSleep(int milliseconds) +{ + /* Same rounding rule as the CmsdkUart sleep in main.c — sub-tick requests + * must still block the task, otherwise vTaskDelay(0) just yields. */ + TickType_t ticks = pdMS_TO_TICKS((TickType_t) milliseconds); + if ((milliseconds > 0) && (ticks == 0U)) + { + ticks = 1U; + } + vTaskDelay(ticks); +} + +/* Idempotent: safe to call from BddTargetTlsSender_Create on every invocation + * even though the first call is the only one that does real work. mbedTLS + * state for entropy/DRBG/cert/key lives at file scope and survives across + * connect/disconnect cycles. + * + * Each major step emits a printf diagnostic and yields one tick to the + * FreeRTOS scheduler. Under QEMU mps2-an385 the DRBG seed + cert/key parses + * can each take several seconds (mbedTLS does serious crypto work — RSA key + * parse, ECDHE primes, ASN.1 walks); without the yields, lower-priority tasks + * would starve until init finishes, and without the diagnostic prints the + * boot would appear to hang. */ +static void EnsureMbedTlsInitialised(void) +{ + if (mbedTlsInitialised) + { + return; + } + + /* Boot diagnostics via printf rather than SolidSyslog_Error because main's + * error handler installation happens AFTER BddTargetTlsSender_Create — + * the default no-op handler would otherwise swallow these. */ + (void) printf("[mbedtls] init entropy + DRBG seed (slow under QEMU)\r\n"); + vTaskDelay(1U); + + /* Redirect mbedTLS allocations to the FreeRTOS heap before any + * mbedtls_*_init runs. Must come first — once an ssl_setup runs against + * the default libc calloc and fails, the failure mode is heap exhaustion + * inside newlib's 4 KiB syscall heap, not a recoverable error. */ + mbedtls_platform_set_calloc_free(FreeRtosMbedTlsCalloc, FreeRtosMbedTlsFree); + + mbedtls_entropy_init(&entropy); + /* Registered as MBEDTLS_ENTROPY_SOURCE_STRONG even though the demo + * randomness is intentionally terrible. The STRONG/WEAK label is + * checked structurally by `mbedtls_entropy_func`, which requires at + * least MBEDTLS_ENTROPY_BLOCK_SIZE bytes of strong contribution per + * call — without any strong source registered, every + * `mbedtls_ctr_drbg_seed` returns ENTROPY_SOURCE_FAILED (-0x0034) + * after looping 256 times trying to satisfy the threshold. The + * "demo-only entropy" notice printed at the end of this function is + * the real quality assertion; real integrators replace this with a + * TRNG / HSM source. */ + mbedtls_entropy_add_source( + &entropy, + DemoEntropySource, + NULL, + MBEDTLS_ENTROPY_BLOCK_SIZE, + MBEDTLS_ENTROPY_SOURCE_STRONG + ); + + mbedtls_ctr_drbg_init(&drbg); + static const unsigned char personalization[] = "solidsyslog-freertos-bdd"; + int drbgSeedRc = + mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, personalization, sizeof(personalization) - 1U); + if (drbgSeedRc != 0) + { + (void + ) printf("[mbedtls] ctr_drbg_seed FAILED rc=-0x%04x; TLS slot will be unusable\r\n", (unsigned) -drbgSeedRc); + return; + } + vTaskDelay(1U); + + /* mbedTLS 3.6 routes TLS 1.3 cryptography through PSA, so psa_crypto_init() + * must succeed before the first handshake or mbedtls_ssl_handshake returns + * MBEDTLS_ERR_ERROR_GENERIC_ERROR (-0x0001) at the first state transition. + * MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG in the user config disables PSA's + * built-in entropy collector (which would otherwise return + * PSA_ERROR_INSUFFICIENT_ENTROPY on this no-platform-entropy target), so + * the init only succeeds once the DRBG is seeded above — that's why this + * sits after the seed step. Idempotent across subsequent calls. */ + psa_status_t psaRc = psa_crypto_init(); + if (psaRc != PSA_SUCCESS) + { + (void) printf("[mbedtls] psa_crypto_init FAILED rc=%d; TLS slot will be unusable\r\n", (int) psaRc); + return; + } + + (void) printf("[mbedtls] parsing CA chain\r\n"); + + memcpy(caPemBuf, bdd_baked_ca_pem, sizeof(bdd_baked_ca_pem)); + caPemBuf[sizeof(bdd_baked_ca_pem)] = '\0'; + mbedtls_x509_crt_init(&caChain); + int caParseRc = mbedtls_x509_crt_parse(&caChain, caPemBuf, sizeof(caPemBuf)); + if (caParseRc != 0) + { + (void + ) printf("[mbedtls] CA chain parse FAILED rc=-0x%04x; TLS slot will be unusable\r\n", (unsigned) -caParseRc); + return; + } + vTaskDelay(1U); + + (void) printf("[mbedtls] parsing client cert chain\r\n"); + + memcpy(clientCertPemBuf, bdd_baked_client_cert_pem, sizeof(bdd_baked_client_cert_pem)); + clientCertPemBuf[sizeof(bdd_baked_client_cert_pem)] = '\0'; + mbedtls_x509_crt_init(&clientCertChain); + int clientCertParseRc = mbedtls_x509_crt_parse(&clientCertChain, clientCertPemBuf, sizeof(clientCertPemBuf)); + if (clientCertParseRc != 0) + { + (void) printf( + "[mbedtls] client cert parse FAILED rc=-0x%04x; mTLS will be unusable\r\n", + (unsigned) -clientCertParseRc + ); + return; + } + vTaskDelay(1U); + + (void) printf("[mbedtls] parsing client key (RSA — slowest step)\r\n"); + + memcpy(clientKeyPemBuf, bdd_baked_client_key_pem, sizeof(bdd_baked_client_key_pem)); + clientKeyPemBuf[sizeof(bdd_baked_client_key_pem)] = '\0'; + mbedtls_pk_init(&clientKey); + int clientKeyParseRc = mbedtls_pk_parse_key( + &clientKey, + clientKeyPemBuf, + sizeof(clientKeyPemBuf), + NULL, + 0U, + mbedtls_ctr_drbg_random, + &drbg + ); + if (clientKeyParseRc != 0) + { + (void) printf( + "[mbedtls] client key parse FAILED rc=-0x%04x; mTLS will be unusable\r\n", + (unsigned) -clientKeyParseRc + ); + return; + } + vTaskDelay(1U); + + /* Audit trail: every cold boot of this target announces the demo-only + * entropy explicitly. Integrators porting this off the BDD target should + * see this and replace DemoEntropySource with TRNG before shipping. The + * `mbedTlsInitialised = true` latch happens only after every fail-able + * step above has succeeded — partial-init state would silently degrade + * later handshakes into confusing "internal" errors. */ + (void) printf("[mbedtls] init complete. WARNING: demo-only entropy " + "(xTaskGetTickCount + per-call counter). Not for production.\r\n"); + + mbedTlsInitialised = true; +} + +/* Dispatch the StreamSender's endpoint+version callbacks based on whether + * the harness most recently selected `tls` or `mtls` over the UART. tls + * and mtls share BDD_TARGET_SWITCH_TLS (and therefore one TLS stream / one + * TCP socket / one mbedTLS context); only the destination port (6514 vs + * 6515) differs at Connect time. The mTLS port's syslog-ng listener + * peer-verifies the client cert that the wrapper wires unconditionally + * below, and the plain-TLS port's listener accepts it as optional-untrusted + * — so the same client identity works on both ports. */ +static void DispatchEndpoint(struct SolidSyslogEndpoint* endpoint) +{ + if (BddTargetSwitchConfig_IsMtlsMode()) + { + BddTargetMtlsConfig_GetEndpoint(endpoint); + } + else + { + BddTargetTlsConfig_GetEndpoint(endpoint); + } +} + +static uint32_t DispatchEndpointVersion(void) +{ + return BddTargetSwitchConfig_IsMtlsMode() ? BddTargetMtlsConfig_GetEndpointVersion() + : BddTargetTlsConfig_GetEndpointVersion(); +} + +struct SolidSyslogSender* BddTargetTlsSender_Create(struct SolidSyslogResolver* resolver, bool mtls) +{ + /* `mtls` is honoured for cross-platform contract uniformity but does not + * gate cert wiring on FreeRTOS — both TLS and mTLS BDD scenarios share + * one Switching slot here, and `set transport mtls` arrives over the UART + * AFTER this Create call has run. Wiring the client identity + * unconditionally lets the dispatcher above flip ports at runtime without + * needing a sender re-create. */ + (void) mtls; + + EnsureMbedTlsInitialised(); + if (!mbedTlsInitialised) + { + /* EnsureMbedTlsInitialised already printed a [mbedtls] ... FAILED + * diagnostic explaining which step tripped. Returning the shared + * NullSender here keeps the bad-setup contract intact — the + * SwitchingSender's tls slot drops messages cleanly rather than + * failing opaquely later inside MbedTlsStream_Open, and the + * statics below stay NULL so BddTargetTlsSender_Destroy can + * detect the short-circuit. */ + return SolidSyslogNullSender_Get(); + } + + /* Inner byte transport: lwIP Raw API TCP stream. RtosSleep drives the + * bounded synchronous-connect spin; the connect timeout comes from the + * SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS tunable (GetConnectTimeoutMs NULL). + * All lwIP-core touches inside the adapter are marshalled onto the tcpip + * thread via the SolidSyslogLwipRaw_SetMarshal hop main.c installs. */ + static struct SolidSyslogLwipRawTcpStreamConfig underlyingStreamConfig; + underlyingStreamConfig = (struct SolidSyslogLwipRawTcpStreamConfig) {0}; + underlyingStreamConfig.GetConnectTimeoutMs = NULL; + underlyingStreamConfig.ConnectTimeoutContext = NULL; + underlyingStreamConfig.Sleep = RtosSleep; + underlyingStream = SolidSyslogLwipRawTcpStream_Create(&underlyingStreamConfig); + + static struct SolidSyslogMbedTlsStreamConfig tlsStreamConfig; + tlsStreamConfig = (struct SolidSyslogMbedTlsStreamConfig) {0}; + tlsStreamConfig.Transport = underlyingStream; + tlsStreamConfig.Sleep = RtosSleep; + tlsStreamConfig.Rng = &drbg; + tlsStreamConfig.CaChain = &caChain; + /* Plain-TLS and mTLS share one SNI on this oracle (CN/SAN = "syslog-ng"), + * so either *_GetServerName accessor returns the same string. Use the + * TLS one to make the equivalence explicit. */ + tlsStreamConfig.ServerName = BddTargetTlsConfig_GetServerName(); + tlsStreamConfig.ClientCertChain = &clientCertChain; + tlsStreamConfig.ClientKey = &clientKey; + tlsStream = SolidSyslogMbedTlsStream_Create(&tlsStreamConfig); + + address = SolidSyslogLwipRawAddress_Create(); + + static struct SolidSyslogStreamSenderConfig senderConfig; + senderConfig = (struct SolidSyslogStreamSenderConfig) {0}; + senderConfig.Resolver = resolver; + senderConfig.Stream = tlsStream; + senderConfig.Address = address; + senderConfig.Endpoint = DispatchEndpoint; + senderConfig.EndpointVersion = DispatchEndpointVersion; + sender = SolidSyslogStreamSender_Create(&senderConfig); + + return sender; +} + +void BddTargetTlsSender_Destroy(void) +{ + /* If EnsureMbedTlsInitialised failed, Create short-circuited to the + * shared NullSender and never assigned the file-scope statics — there + * is nothing to release. The pool-backed Destroy helpers tolerate + * a NULL handle but skipping makes the no-op explicit. */ + if (sender == NULL) + { + return; + } + SolidSyslogStreamSender_Destroy(sender); + SolidSyslogLwipRawAddress_Destroy(address); + SolidSyslogMbedTlsStream_Destroy(tlsStream); + SolidSyslogLwipRawTcpStream_Destroy(underlyingStream); + + /* Entropy / DRBG / parsed certs survive across Destroy → Create cycles to + * avoid re-seeding on every reconnect. Real teardown only happens at + * process exit, which the FreeRTOS target never reaches. */ +} diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c index e6c64bc6..11638dcb 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c @@ -90,19 +90,17 @@ static mbedtls_pk_context clientKey; static void* FreeRtosMbedTlsCalloc(size_t nmemb, size_t size) { void* result = NULL; - if ((nmemb != 0U) && (size != 0U)) + /* Guard the multiplication against wrap BEFORE multiplying: a maliciously + * huge request must not underestimate the allocation size, or pvPortMalloc + * would hand back a too-small buffer the caller then writes past. With size + * already known non-zero, nmemb <= SIZE_MAX / size proves nmemb * size fits. */ + if ((nmemb != 0U) && (size != 0U) && (nmemb <= (SIZE_MAX / size))) { size_t bytes = nmemb * size; - /* Detect the multiplication wrap so a maliciously huge request can't - * underestimate the allocation size — pvPortMalloc would then hand - * back a too-small buffer that the caller writes past. */ - if ((bytes / nmemb) == size) + result = pvPortMalloc(bytes); + if (result != NULL) { - result = pvPortMalloc(bytes); - if (result != NULL) - { - memset(result, 0, bytes); - } + memset(result, 0, bytes); } } return result; diff --git a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt index 0bf56b11..6abfd030 100644 --- a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt +++ b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt @@ -29,6 +29,13 @@ if(NOT LWIP_DIR) "which sets this to /opt/lwip.") endif() +set(MBEDTLS_SOURCE_DIR "$ENV{MBEDTLS_DIR}") +if(NOT EXISTS "${MBEDTLS_SOURCE_DIR}/CMakeLists.txt") + message(FATAL_ERROR + "MBEDTLS_DIR not set or invalid. Use the cpputest-freertos-cross " + "container, which sets this to /opt/mbedtls.") +endif() + # lwIP ships its source lists as *_SRCS variables via Filelists.cmake. For # NO_SYS=0 we consume the IPv4 core set plus the api/ set (tcpip.c, api_lib, # api_msg, …) — the tcpip thread, timeouts, and tcpip_callback marshal hop @@ -102,6 +109,85 @@ target_include_directories(solid_syslog_freertos_lwip_upstream PRIVATE ${LWIP_CONTRIB_FREERTOS_DIR}/include # arch/sys_arch.h ) +# --- mbedTLS upstream as a subproject ---------------------------------------- +# +# Same pattern as Bdd/Targets/FreeRtos/CMakeLists.txt: bring mbedTLS in via +# add_subdirectory(EXCLUDE_FROM_ALL), force-disable its programs/tests, and +# strip our strict warning surface from the upstream targets so a future +# mbedTLS bump doesn't break our build for diagnostic reasons. clang-tidy / +# cppcheck are also disabled per-target — they would otherwise apply our +# .clang-tidy ruleset to upstream code. +set(ENABLE_PROGRAMS OFF CACHE BOOL "Disable mbedTLS demo programs in our build" FORCE) +set(ENABLE_TESTING OFF CACHE BOOL "Disable mbedTLS upstream tests in our build" FORCE) +set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "Upstream mbedTLS warnings are not errors in our build" FORCE) +add_subdirectory(${MBEDTLS_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}/_mbedtls EXCLUDE_FROM_ALL) + +# MBEDTLS_USER_CONFIG_FILE is consumed by mbedTLS's headers via `#include`, +# so the macro value must be a quoted C string literal. CMake-quote with +# `\"` to survive the shell -> compiler hop. +set(MBEDTLS_USER_CONFIG_HEADER + "\"${CMAKE_CURRENT_SOURCE_DIR}/mbedtls_user_config.h\"" +) + +foreach(_mbedtls_upstream_target IN ITEMS mbedtls mbedx509 mbedcrypto everest p256m) + if(TARGET ${_mbedtls_upstream_target}) + target_compile_options(${_mbedtls_upstream_target} PRIVATE -w) + target_compile_definitions(${_mbedtls_upstream_target} PRIVATE + MBEDTLS_USER_CONFIG_FILE=${MBEDTLS_USER_CONFIG_HEADER} + ) + set_target_properties(${_mbedtls_upstream_target} PROPERTIES + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + C_CPPCHECK "" + CXX_CPPCHECK "" + ) + endif() +endforeach() + +# --- Bake demo PEMs into the ELF ------------------------------------------- +# +# QEMU has no path to the host filesystem, and the integrator config disables +# MBEDTLS_FS_IO, so the demo CA + client identity travel as `static const` +# arrays in rodata. xxd -i emits the array shape we want; mbedTLS's PEM parser +# additionally requires a NUL-terminated input, which the wrapper TU adds at +# parse time by copying into a +1-sized buffer (cheaper than a shell pipeline +# to append the NUL at bake time, and keeps this CMake step simple). + +set(BAKED_PEMS_DIR "${CMAKE_CURRENT_BINARY_DIR}/baked_pems") +file(MAKE_DIRECTORY ${BAKED_PEMS_DIR}) + +function(bake_pem_into_header input_pem output_header symbol) + add_custom_command( + OUTPUT ${output_header} + COMMAND bash -c "xxd -i -n '${symbol}' '${input_pem}' > '${output_header}'" + DEPENDS ${input_pem} + COMMENT "Baking ${input_pem} -> ${output_header}" + VERBATIM + ) +endfunction() + +bake_pem_into_header( + ${CMAKE_SOURCE_DIR}/Bdd/syslog-ng/tls/ca.pem + ${BAKED_PEMS_DIR}/BddBakedCaPem.h + bdd_baked_ca_pem +) +bake_pem_into_header( + ${CMAKE_SOURCE_DIR}/Bdd/syslog-ng/tls/client.pem + ${BAKED_PEMS_DIR}/BddBakedClientCertPem.h + bdd_baked_client_cert_pem +) +bake_pem_into_header( + ${CMAKE_SOURCE_DIR}/Bdd/syslog-ng/tls/client.key + ${BAKED_PEMS_DIR}/BddBakedClientKeyPem.h + bdd_baked_client_key_pem +) + +set(BAKED_PEM_HEADERS + ${BAKED_PEMS_DIR}/BddBakedCaPem.h + ${BAKED_PEMS_DIR}/BddBakedClientCertPem.h + ${BAKED_PEMS_DIR}/BddBakedClientKeyPem.h +) + # --- Executable --------------------------------------------------------------- # # Project code only. Inherits the full project warning set from the top-level @@ -130,10 +216,17 @@ add_executable(SolidSyslogBddTargetLwip ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutexMessages.c ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutexStatic.c ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosSysUpTime.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamMessages.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetMtlsConfig.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetSwitchConfig.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetTlsConfig.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_LwipRawTcp.c + ${BAKED_PEM_HEADERS} $ ) @@ -158,6 +251,9 @@ target_include_directories(SolidSyslogBddTargetLwip PRIVATE ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source # SolidSyslogLwipRaw*Private.h ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Interface # SolidSyslogFreeRtos{Mutex,SysUpTime}.h ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source # SolidSyslogFreeRtosMutexPrivate.h + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Interface # SolidSyslogMbedTlsStream.h + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source # SolidSyslogMbedTlsStreamPrivate.h + ${BAKED_PEMS_DIR} # BddBaked*.h (xxd -i output) ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common # BddTargetInteractive.h, BddTargetSwitchConfig.h, … ${FREERTOS_KERNEL_PATH}/include ${FREERTOS_PORT_DIR} # portmacro.h @@ -167,6 +263,14 @@ target_include_directories(SolidSyslogBddTargetLwip PRIVATE target_link_libraries(SolidSyslogBddTargetLwip PRIVATE ${PROJECT_NAME} # libSolidSyslog.a (cross-compiled) + mbedtls mbedx509 mbedcrypto # mbedTLS upstream (subproject, cross-compiled) +) + +# Platform/MbedTls/Source/*.c include ; they MUST see the same +# user config as the mbedTLS library itself, otherwise struct sizes and the +# enabled-feature bitset diverge between the consumer and the library. +target_compile_definitions(SolidSyslogBddTargetLwip PRIVATE + MBEDTLS_USER_CONFIG_FILE=${MBEDTLS_USER_CONFIG_HEADER} ) target_link_options(SolidSyslogBddTargetLwip PRIVATE diff --git a/Bdd/Targets/FreeRtosLwip/main.c b/Bdd/Targets/FreeRtosLwip/main.c index 8e4b45b8..f70e2647 100644 --- a/Bdd/Targets/FreeRtosLwip/main.c +++ b/Bdd/Targets/FreeRtosLwip/main.c @@ -11,10 +11,12 @@ * drains over UDP, and BddTargetInteractive drives `send N` / `set ` / * `quit` over the QEMU -serial stdio UART. * - * Scope is UDP (S28.09) + TCP (S28.10). The SwitchingSender carries a real UDP - * sender (LwipRawDatagram) and a real octet-framed TCP sender (StreamSender over - * LwipRawTcpStream); the TLS slot still resolves to the shared NullSender (drops - * on the floor) until S28.11 wires the LwipRaw TLS sender. + * Scope is UDP (S28.09) + TCP (S28.10) + TLS/mTLS (S28.11). The SwitchingSender + * carries a real UDP sender (LwipRawDatagram), a real octet-framed TCP sender + * (StreamSender over LwipRawTcpStream), and a real TLS sender (StreamSender over + * SolidSyslogMbedTlsStream over a second LwipRawTcpStream) — see + * BddTargetTlsSender_MbedTls_LwipRawTcp.c. tls and mtls share the one TLS slot; + * the destination port (6514 vs 6515) is dispatched at Connect time. * * Static IPv4 (10.0.2.15) on the QEMU slirp network, host reachable at the * slirp gateway 10.0.2.2 — numeric, because slirp has no route to the docker @@ -27,7 +29,10 @@ #include "BddTargetInteractive.h" #include "BddTargetIps.h" #include "BddTargetLanguage.h" +#include "BddTargetMtlsConfig.h" #include "BddTargetSwitchConfig.h" +#include "BddTargetTlsConfig.h" +#include "BddTargetTlsSender.h" #include "SolidSyslog.h" #include "SolidSyslogAtomicCounter.h" @@ -45,7 +50,6 @@ #include "SolidSyslogLwipRawTcpStream.h" #include "SolidSyslogMetaSd.h" #include "SolidSyslogMutex.h" -#include "SolidSyslogNullSender.h" #include "SolidSyslogNullStore.h" #include "SolidSyslogOriginSd.h" #include "SolidSyslogPrival.h" @@ -77,12 +81,13 @@ /* Unprivileged mirror of SOLIDSYSLOG_UDP_DEFAULT_PORT (514) for BDD listeners. */ #define BDD_TARGET_UDP_PORT 5514U -/* UDP-only interactive task: BddTargetInteractive's 2048-byte line + name - * frames plus SolidSyslog_Log's two SOLIDSYSLOG_MAX_MESSAGE_SIZE formatter - * frames and newlib printf. *40 (20 KB) matches the empirical pre-TLS budget - * the +TCP target recorded for the same 2048-byte line buffer. heap_4 (96 KB) - * absorbs it alongside the lwIP tcpip / RX tasks. */ -#define INTERACTIVE_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 40U) +/* Interactive task: BddTargetInteractive's 2048-byte line + name frames plus + * SolidSyslog_Log's two SOLIDSYSLOG_MAX_MESSAGE_SIZE formatter frames and + * newlib printf, *and* the one-shot mbedTLS init (DRBG seed, RSA key parse, + * x509 walks) that BddTargetTlsSender_Create drives on this task at startup. + * *48 (24 KB) matches the +TCP target's post-TLS budget for the same work; + * heap_4 (96 KB) absorbs it alongside the lwIP tcpip / RX tasks. */ +#define INTERACTIVE_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 48U) #define SERVICE_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 16U) static char appName[49] = "SolidSyslogBddTarget"; @@ -484,6 +489,20 @@ static void InteractiveTask(void* argument) * first send is not lost to a cache miss (see WarmUpGatewayArp). */ WarmUpGatewayArp(); + /* Pin the TLS / mTLS destination host to the slirp gateway 10.0.2.2 (the + * same numeric path UDP / TCP take) rather than the "syslog-ng" docker DNS + * alias the shared BddTargetTlsConfig defaults to: SolidSyslogLwipRawResolver + * is numeric-only (ipaddr_aton, no DNS), so a hostname would fail to resolve + * and the TLS StreamSender would never connect. ServerName for SNI / cert + * verification stays "syslog-ng" (the cert's subject); without that the + * handshake fails because the syslog-ng cert isn't issued for 10.0.2.2. + * Mirrors Bdd/Targets/FreeRtos/main.c (the +TCP target's PlusTcpResolver + * ignores the host, so this only bites the numeric lwIP resolver). */ + BddTargetTlsConfig_SetHost("10.0.2.2"); + BddTargetTlsConfig_SetServerName("syslog-ng"); + BddTargetMtlsConfig_SetHost("10.0.2.2"); + BddTargetMtlsConfig_SetServerName("syslog-ng"); + resolver = SolidSyslogLwipRawResolver_Create(); datagram = SolidSyslogLwipRawDatagram_Create(); udpAddress = SolidSyslogLwipRawAddress_Create(); @@ -519,13 +538,22 @@ static void InteractiveTask(void* argument) }; tcpSender = SolidSyslogStreamSender_Create(&tcpConfig); - /* SwitchingSender lets `set transport ` flip transport at - * runtime. UDP and TCP are wired; TLS routes to the shared NullSender (drop - * on the floor) until S28.11 wires the LwipRaw TLS sender. */ + /* TLS slot: SolidSyslogMbedTlsStream over a second LwipRawTcpStream, with + * the demo CA / client cert / client key baked into the ELF. The same slot + * serves both @tls (port 6514) and @mtls (port 6515) scenarios — the + * wrapper wires the client cert unconditionally and its StreamSender + * Endpoint callback dispatches on BddTargetSwitchConfig_IsMtlsMode() at + * Connect time. The `false` argument is honoured for cross-platform + * signature uniformity but ignored on FreeRTOS, where the harness flips + * mode over the UART after the prompt is already up. */ + struct SolidSyslogSender* tlsSender = BddTargetTlsSender_Create(resolver, false); + + /* SwitchingSender lets `set transport ` flip transport at + * runtime. UDP, TCP, and TLS/mTLS are all wired. */ static struct SolidSyslogSender* inners[BDD_TARGET_SWITCH_COUNT]; inners[BDD_TARGET_SWITCH_UDP] = udpSender; inners[BDD_TARGET_SWITCH_TCP] = tcpSender; - inners[BDD_TARGET_SWITCH_TLS] = SolidSyslogNullSender_Get(); + inners[BDD_TARGET_SWITCH_TLS] = tlsSender; struct SolidSyslogSwitchingSenderConfig switchConfig = { .Senders = inners, .SenderCount = BDD_TARGET_SWITCH_COUNT, @@ -634,6 +662,11 @@ static void TeardownAll(void) SolidSyslogFreeRtosMutex_Destroy(lifecycleMutex); lifecycleMutex = NULL; SolidSyslogSwitchingSender_Destroy(switchingSender); + /* BddTargetTlsSender owns the inner MbedTlsStream + LwipRawTcpStream pool + * slots and its own StreamSender slot, so release it before the plain-TCP + * tcpSender / tcpStream below — roughly reverse order of Create keeps the + * dependency graph clean even though the pool allocator is order-insensitive. */ + BddTargetTlsSender_Destroy(); SolidSyslogUdpSender_Destroy(udpSender); SolidSyslogStreamSender_Destroy(tcpSender); SolidSyslogLwipRawTcpStream_Destroy(tcpStream); diff --git a/Bdd/Targets/FreeRtosLwip/mbedtls_user_config.h b/Bdd/Targets/FreeRtosLwip/mbedtls_user_config.h new file mode 100644 index 00000000..1dbcf4f0 --- /dev/null +++ b/Bdd/Targets/FreeRtosLwip/mbedtls_user_config.h @@ -0,0 +1,104 @@ +/* mbedTLS integrator overrides for the FreeRTOS + lwIP QEMU BDD target. + * + * Network-backend twin of Bdd/Targets/FreeRtos/mbedtls_user_config.h — the + * mbedTLS feature set is transport-agnostic, so the two configs are identical + * except for the include guard and the transport references in comments. Kept + * as a separate file (rather than #include of the +TCP one) so each target's + * config is self-contained and independently tunable. + * + * mbedTLS supports an optional integrator config layered on top of its + * built-in default via `-DMBEDTLS_USER_CONFIG_FILE="path"`. Anything we + * #define here adds to the default; anything we #undef removes from it. + * + * The defaults that mbedTLS picks for a generic build assume a Unix or + * Windows host — they pull /dev/urandom for entropy, use fopen for cert + * loading, and call BSD sockets directly. The Cortex-M3 / FreeRTOS QEMU + * BDD target has none of those, so we strip them and rely on the integrator + * (BddTargetTlsSender_MbedTls_LwipRawTcp.c) to wire entropy, transport, and + * cert handles via DI. + * + * Anything not touched here keeps mbedTLS's default — including the cipher + * suite set, RSA, ECC curves, SHA, AES, the PEM parser, x509 parsing, the + * CTR-DRBG implementation, etc. Trimming further is a binary-size exercise. + */ + +#ifndef BDD_TARGET_FREERTOS_LWIP_MBEDTLS_USER_CONFIG_H +#define BDD_TARGET_FREERTOS_LWIP_MBEDTLS_USER_CONFIG_H + +/* Don't compile entropy_poll.c's Unix/Windows code path — mbedTLS would + * otherwise #error on "Platform entropy sources only work on Unix and + * Windows". BddTargetTlsSender_MbedTls_LwipRawTcp.c provides a weak entropy + * callback via mbedtls_entropy_add_source. The "demo-only entropy" caveat is + * documented in the integrator guide. */ +#define MBEDTLS_NO_PLATFORM_ENTROPY + +/* No filesystem from mbedTLS's point of view. PEMs are baked into the ELF + * via xxd -i and parsed via mbedtls_x509_crt_parse / _pk_parse_key against + * the in-memory buffer, so MBEDTLS_FS_IO is unused dead code and a link + * hazard against newlib stubs. */ +#undef MBEDTLS_FS_IO + +/* No BSD sockets — the transport is injected as a SolidSyslogStream + * (LwipRawTcpStream) and bridged into mbedTLS via mbedtls_ssl_set_bio + * callbacks. MBEDTLS_NET_C would otherwise pull in . */ +#undef MBEDTLS_NET_C + +/* No host clock. Cortex-M3 has no wall-clock; mbedTLS's cert-validity-date + * check is skipped when this is off, which is fine for BDD with baked certs + * carrying validity 20240101–20990101. Production integrators with an RTC + * should turn MBEDTLS_HAVE_TIME[_DATE] back on. */ +#undef MBEDTLS_HAVE_TIME +#undef MBEDTLS_HAVE_TIME_DATE + +/* Disable mbedTLS's own threading primitive layer. The library runs on the + * service task only — concurrent access to the ssl_context is not in scope + * for this target. Per [[project-mbedtls-coexistence-contract]] the library + * must never install threading hooks. */ +#undef MBEDTLS_THREADING_C +#undef MBEDTLS_THREADING_PTHREAD + +/* PSA's "internal trusted storage on filesystem" requires MBEDTLS_FS_IO, + * which we just disabled. We don't use the PSA API surface anyway — the + * adapter is built on the classic mbedTLS API (mbedtls_ssl_*, mbedtls_x509_*, + * mbedtls_pk_*, mbedtls_ctr_drbg_*). */ +#undef MBEDTLS_PSA_ITS_FILE_C +#undef MBEDTLS_PSA_CRYPTO_STORAGE_C + +/* mbedTLS's timing.c uses gettimeofday / clock_gettime — Unix/Windows only. + * The adapter manages its own bounded handshake retry budget via the + * injected Sleep callback, so MBEDTLS_TIMING_C is unused. */ +#undef MBEDTLS_TIMING_C + +/* Route mbedTLS allocations through a runtime-installed calloc/free pair. By + * default mbedTLS calls libc calloc/free, which on this target funnels through + * newlib into the small 4 KiB syscall heap in Bdd/Targets/FreeRtos/Common/ + * Syscalls.c (shared with this target) — far too small for mbedTLS's + * per-context allocations (IN/OUT buffers plus handshake state run ~10–20 KiB). + * Enabling MBEDTLS_PLATFORM_MEMORY lets BddTargetTlsSender_MbedTls_LwipRawTcp.c + * call mbedtls_platform_set_calloc_free(...) to redirect those allocations to + * pvPortMalloc, which uses the 96 KiB heap_4 region — the textbook + * FreeRTOS+mbedTLS integration. */ +#define MBEDTLS_PLATFORM_MEMORY + +/* Route PSA crypto's randomness through an integrator-supplied callback rather + * than PSA's internal entropy pool. mbedTLS 3.6's TLS 1.3 path is built on PSA, + * so psa_crypto_init() must succeed before any TLS 1.3 handshake — and the + * default PSA entropy collector returns PSA_ERROR_INSUFFICIENT_ENTROPY + * (-148) on platforms with no real entropy source (which is us, with + * MBEDTLS_NO_PLATFORM_ENTROPY defined above). With this define, PSA never + * tries to seed itself; the integrator provides mbedtls_psa_external_get_random + * (see BddTargetTlsSender_MbedTls_LwipRawTcp.c) which feeds the same CTR_DRBG + * the classic API uses, so PSA and the classic API share one entropy chain. */ +#define MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG + +/* Shrink the TLS record buffers from the 16 KiB default. The BDD syslog-ng + * oracle's server cert + chain fits in 4 KiB IN comfortably, and the BDD + * messages we send fit in 2 KiB OUT. Cuts ~28 KiB off the per-context + * footprint — important when the FreeRTOS heap also has to satisfy the lwIP + * tcpip / RX tasks, the SolidSyslog Service task, and the interactive task + * all at once. Embedded integrators replicating this footprint should re-tune + * both knobs against their peer's largest TLS record. */ +#define MBEDTLS_SSL_IN_CONTENT_LEN 4096 +#define MBEDTLS_SSL_OUT_CONTENT_LEN 2048 + +#endif /* BDD_TARGET_FREERTOS_LWIP_MBEDTLS_USER_CONFIG_H */ diff --git a/DEVLOG.md b/DEVLOG.md index 59a04cb5..e5aa063e 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,215 @@ # Dev Log +## 2026-05-30 — S28.11 FreeRtosLwip TLS/mTLS (mbedTLS over LwipRawTcpStream) + +Wiring complete; **both plain TLS and mutual TLS are green** on the lwIP BDD +lane at main's original `lwipopts.h` sizing. The "mTLS blocker" recorded in the +WIP draft of this entry turned out to be a **measurement artifact in the +verification harness, not a defect** — see Verification. + +### Decisions + +- **The mTLS/TLS sender is a transport-swap clone of the +TCP variant.** + `Bdd/Targets/Common/BddTargetTlsSender_MbedTls_LwipRawTcp.c` is + `..._PlusTcpTcp.c` with `SolidSyslogPlusTcpTcpStream` → `SolidSyslogLwipRawTcpStream` + (created with `{GetConnectTimeoutMs=NULL, Sleep=RtosSleep}`) and + `SolidSyslogPlusTcpAddress` → `SolidSyslogLwipRawAddress`. All the mbedTLS + init (entropy/DRBG/PSA, baked PEMs, the tls/mtls endpoint dispatcher) is + transport-agnostic and copied verbatim. CMake gained the mbedTLS subproject + + PEM-baking blocks (mirroring `Bdd/Targets/FreeRtos/CMakeLists.txt`), and + `FreeRtosLwip/mbedtls_user_config.h` is the +TCP config with a fresh guard. + +- **The resolver/host bug — silent, and the first real blocker.** `BddTargetTlsConfig` + defaults the TLS/mTLS host to `"syslog-ng"` (a DNS name; it doubles as the SNI). + `SolidSyslogLwipRawResolver` is numeric-only (`ipaddr_aton`, no DNS), so it + rejected the name, the `StreamSender` never connected, and **nothing was logged**. + Fix mirrors the +TCP target: pin the host to the numeric slirp gateway `10.0.2.2` + in `main.c` while keeping `ServerName="syslog-ng"` for SNI/cert verification. The + +TCP target dodges this because its `PlusTcpResolver` ignores the host. This silent + failure cost real debugging time — see Deferred (resolve-failure logging). + +- **Nagle disabled on the lwIP TCP pcb (Tier-2, `tcp_nagle_disable`) — the actual + fix.** Packet capture proved a mid-handshake send-stall: the client sent + ClientHello + CCS, then the Certificate/CertVerify/Finished flight was accepted + by `tcp_write` (returned OK, sndbuf healthy) but **never put on the wire** — + Nagle held the sub-MSS segments waiting for an ACK that the peer only sends + per-flight. With `TCP_NODELAY` the full flight (and the app-data record) goes + out and is ACK'd. `TCP_NODELAY` is the correct default for this small-record + request/response workload anyway. Driven by a new unit test + (`OpenDisablesNagleOnPcb`) asserting `TF_NODELAY` on the pcb after Open. + +- **`lwipopts.h` reverted to main's minimum — the provisional bump was + unnecessary.** During the WIP investigation the counts were bumped + 16/16/16 → 48/48/32 and `TCP_WND`/`TCP_SND_BUF` 4→6×MSS, on the belief that + lower values left mTLS "flaky". That flakiness observation was the same + measurement artifact (see Verification). Re-tested this session at main's + original `16/16/16 + 4×MSS`: mTLS delivered **5/5** in the corrected manual + sweep, and the full lwIP BDD lane (`@tls`, `@mtls`, TLS 1.3, TCP→TLS switch) + is green. The bump is reverted; the file is now identical to `main`. The + `tcp_write`-`ERR_MEM`-tears-down-the-connection coupling is real, but a single + mutual-TLS handshake fits the 4×MSS budget comfortably. + +### Verification + +- Cross ELF builds clean under the strict bar (no PlusTcp symbols; mbedTLS + + TLS-sender symbols present). `Tests/Lwip` 57 tests green (added the Nagle test). +- **Plain TLS over lwIP: PASS.** After the Nagle fix the encrypted record is + delivered, ACK'd, and logged. +- **mTLS over lwIP: PASS.** The full lwIP BDD lane reports `13 features passed, + 0 failed`; `mtls_transport` junit shows `status="passed" skipped="0"`, and the + delivered record (`Hello from FreeRTOS lwIP`) lands in `received_mtls.log`. + Confirmed deterministic (5/5 manual fresh-boot runs) via syslog-ng's own + `source(s_mtls)` processed counter. +- **The WIP "mTLS FAIL (unsolved)" was a harness artifact.** The manual probe + loop (`mtls_loop.sh`) did `rm -f received_mtls.log` before each run *while + syslog-ng held the file open* — syslog-ng kept writing to the now-unlinked + inode, so the line-count check always read 0. The real BDD harness counts a + baseline→delta on the file (no `rm`), which is why `bdd-freertos-qemu-lwip` was + **green in CI the whole time**. The earlier `strace` "`recvmsg`→`EAGAIN`" + evidence was captured before the Nagle fix was built into the ELF. Lesson: + never reset a log file the server has open — truncate-in-place or baseline the + offset. + +### Deferred + +- **Resolve-failure logging — still queued (not implemented).** The + numeric-resolver-rejects-a-hostname path is silent today (cost real debugging + time via the resolver/host bug above). Agreed approach: add a + `SolidSyslog_Error` on resolve failure, TDD'd (a test provokes the silent + failure and asserts the error fires). Surfaced for David's call on whether to + fold it into this PR (interactive TDD) or take it as a follow-up. + +--- + +## 2026-05-29 — S28.10 FreeRtosLwip TCP (octet-framed StreamSender over LwipRawTcpStream) + +### Decisions + +- **Mostly wiring — but TCP did not come entirely "for free".** The conditional + #473 asked whether TCP fell out of S28.09; it didn't quite. Plain delivery and + runtime UDP→TCP switching were pure wiring (a `SolidSyslogStreamSender` over + `SolidSyslogLwipRawTcpStream` in the SwitchingSender's TCP slot, sharing the + resolver + endpoint callbacks; `RtosSleep` already matched + `SolidSyslogSleepFunction`; the adapter sources were already compiled in). But + `tcp_reconnect` exposed a real Tier-2 gap (below), so S28.10 earned its keep. + +- **`LwipRawTcpStream_Send` now fails on a peer close (the reconnect fix).** + Root cause of the `tcp_reconnect` failure: `StreamSender` only reconnects when a + `Send` returns false (or the endpoint version changes) — it never `Read`s. On a + graceful server FIN (syslog-ng config-reload closes the connection), lwIP's + `RecvCallback(NULL)` set `Errored` but left the pcb non-NULL, and `Send` guarded + only on `IsOpen` (pcb != NULL) — so `tcp_write` kept returning `ERR_OK` into a + doomed half-closed connection and the sender never reconnected. Fix: a new + intent-named predicate `LwipRawTcpStream_IsWritable` (`IsOpen && !Errored`) gates + `Send`, so a post-FIN send fails, `StreamSender` closes + reconnects on the next + message, and the recovered message is delivered. `Read` deliberately still runs + while `Errored` (it must drain queued bytes then close on EOF). Driven red→green + by a new `Tests/Lwip` unit test (`SendReturnsFalseAfterPeerFin`); this matches + FreeRTOS-Plus-TCP's send-side closed-socket detection, which is why the +TCP lane + already passed the same scenario. + +- **Lane scope: `(@udp or @tcp)`, TLS still excluded.** The lwIP lane filter went + from `@udp and not @tcp …` to `(@udp or @tcp) and not @tls and not @mtls and not + @store`. That enables `tcp_transport`, `tcp_reconnect`, and the UDP→TCP + `switching_transport` scenario; `tcp_singletask` (`@windows_wip`) and the TCP→TLS + switch (`@tls`) stay excluded. TLS lands in S28.11. + +- **Local-run gotcha recorded.** Any `cmake` configure regenerates the in-tree + `Bdd/features/steps/solidsyslog_tunables.py`; an intermediate host `debug` build + (default 2048) clobbered the lwIP target's 512, so a stray oracle run *ran* the + `@requires_message_size_1500` `udp_mtu` scenario (which must skip on a 512-byte + target) and failed spuriously. Re-running the cross-lwip configure restored 512 + and the lane is green. CI is immune — each lane downloads its own + `bdd-tunables-` artifact. + +### Verification + +- Oracle (`ci/docker-compose.bdd.yml`, lwIP pair): **11 features / 28 scenarios / + 116 steps passed, 0 failed** — `tcp_transport`, `tcp_reconnect`, and UDP→TCP + switch green alongside the full UDP set; TLS/mTLS/store/1500-MTU correctly + skipped. `Tests/Lwip/SolidSyslogLwipRawTcpStreamTest` 56 tests green. Tree + clang-format clean. + +--- + +## 2026-05-29 — S28.09 FreeRtosLwip LAN9118 netif + NO_SYS=0 UDP on QEMU + +### Decisions + +- **First-packet ARP drop was a PBUF_REF lifetime bug, fixed with a + bring-up ARP warm-up, not by switching to PBUF_RAM.** `LwipRawDatagram` + sends zero-copy (PBUF_REF over the integrator's transient marshal + buffer). When the first datagram hit an ARP cache miss, lwIP queued + the pbuf for post-resolution transmission — but the referenced buffer + was freed the moment `_SendTo` returned, so the queued seqId=1 went + out as garbage / was dropped while seqId=2 (cache now warm) delivered. + Rather than pay a copy on every send, `main.c`'s `WarmUpGatewayArp` + issues `etharp_request(gw)` at netif bring-up and polls + `etharp_find_addr` (marshalled off the tcpip thread) until the gateway + is resolved before the logging pipeline goes live. pcap now shows + gratuitous ARP → gw ARP request → reply → UDP seqId=1 → seqId=2 in + order. This keeps the zero-copy contract intact and pushes the + one-time resolution cost to setup. + +- **Vendored Arm smsc9220 LAN9118 driver isolated from clang-format.** + `Bdd/Targets/FreeRtosLwip/netif/smsc9220/` carries its Apache-2.0 + upstream headers and a `DisableFormat: true` `.clang-format` so the + `analyze-format` lane won't reflow third-party source. The + hand-written netif glue (`netif/EthernetIf.c`) is ours and stays + under the project style. + +- **NO_SYS=0 with the tcpip thread; lwIP calls marshalled via S28.06's + `_SetMarshal`.** `lwipopts.h` runs the full tcpip thread + contrib + `sys_arch.c`. `main` calls `tcpip_init` pre-scheduler; netif bring-up + runs from the interactive task via `tcpip_callback` because + `smsc9220_init` calls `vTaskDelay` and must run post-scheduler. The + Datagram/TcpStream adapters route through `LwipTcpipMarshal` (installed + with `SolidSyslogLwipRaw_SetMarshal`). The synchronous-marshal contract + requires the callback's results to be ready when the marshal returns; + lwIP `tcpip_callback` only blocks until the work is *queued*, so it + cannot satisfy that on its own. `LWIP_TCPIP_CORE_LOCKING` is enabled, so + the marshal runs the callback in the caller's task context under a + `LOCK_TCPIP_CORE`/`UNLOCK_TCPIP_CORE` pair — unconditionally synchronous, + independent of task priority, no per-send mailbox message. The ARP + warm-up query reuses the same marshal, so its `resolved` flag is written + before it is read. (PR #476 review #4: an earlier revision relied on + `tcpip_callback` + `TCPIP_THREAD_PRIO` preemption, which worked but left + an implicit "marshalling tasks must be below the tcpip thread" invariant; + the core-lock pair removes it.) + +- **Oracle parity confirms the UDP path.** Slice 4 ran the + `behave-freertos-lwip` + `syslog-ng-freertos-lwip` compose pair in + WSL (no docker-in-docker in the freertos-target container). Result: + 8 features / 25 scenarios / 96 steps passed, 0 failed — a strict + subset of the +TCP lane's scenarios under the same library config, + so parity was expected and observed. Delivery truth comes from the + syslog-ng oracle's "receives a message with priority/hostname/PROCID" + assertions, not an ad-hoc host listener (slirp doesn't reliably + deliver the NATed datagram to a host-loopback listener even though + it's correct on the wire). + +- **Advisory CI lane only this story.** `bdd-freertos-qemu-lwip` runs + UDP-only (`@udp and not @tcp/@tls/@mtls/@store`) with + `@freertoslwipwip` as the per-scenario escape hatch, and is NOT in + `summary.needs` — promotion to a required gate is S28.11's job. + +### Deferred + +- **Slice 6 (LwipRaw TCP into the SwitchingSender)** — left for S28.10. + S28.09 is scoped to the netif + UDP; the SwitchingSender's TCP/TLS + slots stay wired to `NullSender` here. Pulling TCP in would be scope + creep across a story boundary. + +- **`build-freertos-target-lwip` → required-gate promotion** — stays + advisory until S28.11. + +### Open questions + +- Does any other lwIP zero-copy send site (a future TCP path) carry the + same transient-buffer-lifetime assumption the UDP warm-up now papers + over at the ARP layer, or is gateway warm-up sufficient because + established TCP connections never re-resolve mid-stream? + ## 2026-05-29 — S28.07 FreeRtosLwip cross link-probe The real S28.07 (#471) turned out to be a CI/build-infra story, **not** the @@ -12957,132 +13167,3 @@ MISRA rule — different category, doesn't set precedent. every untriaged style finding becomes a hard gate. Worth E24 discussion. -## 2026-05-29 — S28.10 FreeRtosLwip TCP (octet-framed StreamSender over LwipRawTcpStream) - -### Decisions - -- **Mostly wiring — but TCP did not come entirely "for free".** The conditional - #473 asked whether TCP fell out of S28.09; it didn't quite. Plain delivery and - runtime UDP→TCP switching were pure wiring (a `SolidSyslogStreamSender` over - `SolidSyslogLwipRawTcpStream` in the SwitchingSender's TCP slot, sharing the - resolver + endpoint callbacks; `RtosSleep` already matched - `SolidSyslogSleepFunction`; the adapter sources were already compiled in). But - `tcp_reconnect` exposed a real Tier-2 gap (below), so S28.10 earned its keep. - -- **`LwipRawTcpStream_Send` now fails on a peer close (the reconnect fix).** - Root cause of the `tcp_reconnect` failure: `StreamSender` only reconnects when a - `Send` returns false (or the endpoint version changes) — it never `Read`s. On a - graceful server FIN (syslog-ng config-reload closes the connection), lwIP's - `RecvCallback(NULL)` set `Errored` but left the pcb non-NULL, and `Send` guarded - only on `IsOpen` (pcb != NULL) — so `tcp_write` kept returning `ERR_OK` into a - doomed half-closed connection and the sender never reconnected. Fix: a new - intent-named predicate `LwipRawTcpStream_IsWritable` (`IsOpen && !Errored`) gates - `Send`, so a post-FIN send fails, `StreamSender` closes + reconnects on the next - message, and the recovered message is delivered. `Read` deliberately still runs - while `Errored` (it must drain queued bytes then close on EOF). Driven red→green - by a new `Tests/Lwip` unit test (`SendReturnsFalseAfterPeerFin`); this matches - FreeRTOS-Plus-TCP's send-side closed-socket detection, which is why the +TCP lane - already passed the same scenario. - -- **Lane scope: `(@udp or @tcp)`, TLS still excluded.** The lwIP lane filter went - from `@udp and not @tcp …` to `(@udp or @tcp) and not @tls and not @mtls and not - @store`. That enables `tcp_transport`, `tcp_reconnect`, and the UDP→TCP - `switching_transport` scenario; `tcp_singletask` (`@windows_wip`) and the TCP→TLS - switch (`@tls`) stay excluded. TLS lands in S28.11. - -- **Local-run gotcha recorded.** Any `cmake` configure regenerates the in-tree - `Bdd/features/steps/solidsyslog_tunables.py`; an intermediate host `debug` build - (default 2048) clobbered the lwIP target's 512, so a stray oracle run *ran* the - `@requires_message_size_1500` `udp_mtu` scenario (which must skip on a 512-byte - target) and failed spuriously. Re-running the cross-lwip configure restored 512 - and the lane is green. CI is immune — each lane downloads its own - `bdd-tunables-` artifact. - -### Verification - -- Oracle (`ci/docker-compose.bdd.yml`, lwIP pair): **11 features / 28 scenarios / - 116 steps passed, 0 failed** — `tcp_transport`, `tcp_reconnect`, and UDP→TCP - switch green alongside the full UDP set; TLS/mTLS/store/1500-MTU correctly - skipped. `Tests/Lwip/SolidSyslogLwipRawTcpStreamTest` 56 tests green. Tree - clang-format clean. - ---- - -## 2026-05-29 — S28.09 FreeRtosLwip LAN9118 netif + NO_SYS=0 UDP on QEMU - -### Decisions - -- **First-packet ARP drop was a PBUF_REF lifetime bug, fixed with a - bring-up ARP warm-up, not by switching to PBUF_RAM.** `LwipRawDatagram` - sends zero-copy (PBUF_REF over the integrator's transient marshal - buffer). When the first datagram hit an ARP cache miss, lwIP queued - the pbuf for post-resolution transmission — but the referenced buffer - was freed the moment `_SendTo` returned, so the queued seqId=1 went - out as garbage / was dropped while seqId=2 (cache now warm) delivered. - Rather than pay a copy on every send, `main.c`'s `WarmUpGatewayArp` - issues `etharp_request(gw)` at netif bring-up and polls - `etharp_find_addr` (marshalled off the tcpip thread) until the gateway - is resolved before the logging pipeline goes live. pcap now shows - gratuitous ARP → gw ARP request → reply → UDP seqId=1 → seqId=2 in - order. This keeps the zero-copy contract intact and pushes the - one-time resolution cost to setup. - -- **Vendored Arm smsc9220 LAN9118 driver isolated from clang-format.** - `Bdd/Targets/FreeRtosLwip/netif/smsc9220/` carries its Apache-2.0 - upstream headers and a `DisableFormat: true` `.clang-format` so the - `analyze-format` lane won't reflow third-party source. The - hand-written netif glue (`netif/EthernetIf.c`) is ours and stays - under the project style. - -- **NO_SYS=0 with the tcpip thread; lwIP calls marshalled via S28.06's - `_SetMarshal`.** `lwipopts.h` runs the full tcpip thread + contrib - `sys_arch.c`. `main` calls `tcpip_init` pre-scheduler; netif bring-up - runs from the interactive task via `tcpip_callback` because - `smsc9220_init` calls `vTaskDelay` and must run post-scheduler. The - Datagram/TcpStream adapters route through `LwipTcpipMarshal` (installed - with `SolidSyslogLwipRaw_SetMarshal`). The synchronous-marshal contract - requires the callback's results to be ready when the marshal returns; - lwIP `tcpip_callback` only blocks until the work is *queued*, so it - cannot satisfy that on its own. `LWIP_TCPIP_CORE_LOCKING` is enabled, so - the marshal runs the callback in the caller's task context under a - `LOCK_TCPIP_CORE`/`UNLOCK_TCPIP_CORE` pair — unconditionally synchronous, - independent of task priority, no per-send mailbox message. The ARP - warm-up query reuses the same marshal, so its `resolved` flag is written - before it is read. (PR #476 review #4: an earlier revision relied on - `tcpip_callback` + `TCPIP_THREAD_PRIO` preemption, which worked but left - an implicit "marshalling tasks must be below the tcpip thread" invariant; - the core-lock pair removes it.) - -- **Oracle parity confirms the UDP path.** Slice 4 ran the - `behave-freertos-lwip` + `syslog-ng-freertos-lwip` compose pair in - WSL (no docker-in-docker in the freertos-target container). Result: - 8 features / 25 scenarios / 96 steps passed, 0 failed — a strict - subset of the +TCP lane's scenarios under the same library config, - so parity was expected and observed. Delivery truth comes from the - syslog-ng oracle's "receives a message with priority/hostname/PROCID" - assertions, not an ad-hoc host listener (slirp doesn't reliably - deliver the NATed datagram to a host-loopback listener even though - it's correct on the wire). - -- **Advisory CI lane only this story.** `bdd-freertos-qemu-lwip` runs - UDP-only (`@udp and not @tcp/@tls/@mtls/@store`) with - `@freertoslwipwip` as the per-scenario escape hatch, and is NOT in - `summary.needs` — promotion to a required gate is S28.11's job. - -### Deferred - -- **Slice 6 (LwipRaw TCP into the SwitchingSender)** — left for S28.10. - S28.09 is scoped to the netif + UDP; the SwitchingSender's TCP/TLS - slots stay wired to `NullSender` here. Pulling TCP in would be scope - creep across a story boundary. - -- **`build-freertos-target-lwip` → required-gate promotion** — stays - advisory until S28.11. - -### Open questions - -- Does any other lwIP zero-copy send site (a future TCP path) carry the - same transient-buffer-lifetime assumption the UDP warm-up now papers - over at the ARP layer, or is gateway warm-up sufficient because - established TCP connections never re-resolve mid-stream? - diff --git a/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c b/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c index 6c3f0f85..21fa45a3 100644 --- a/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c +++ b/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c @@ -217,6 +217,16 @@ static struct tcp_pcb* LwipRawTcpStream_OpenAndConfigurePcb(struct SolidSyslogLw if (pcb != NULL) { ip_set_option(pcb, SOF_KEEPALIVE); + /* Disable Nagle. The syslog client writes small, latency-sensitive + * records (octet-framed messages, and — when an upper TLS layer is + * stacked on this stream — multi-segment handshake flights) and never + * pipelines a second write behind an unacked one. With Nagle on, lwIP + * holds a sub-MSS segment until the previous one is ACKed; a TLS + * handshake flight (e.g. a client certificate) then stalls mid-exchange + * waiting for an ACK that the peer only sends after it has the whole + * flight — a deadlock observed against syslog-ng over mutual TLS. + * TCP_NODELAY is the right default for this request/response workload. */ + tcp_nagle_disable(pcb); tcp_arg(pcb, self); tcp_recv(pcb, LwipRawTcpStream_RecvCallback); tcp_sent(pcb, LwipRawTcpStream_SentCallback); diff --git a/Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp b/Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp index 5478932d..bdf250c8 100644 --- a/Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp +++ b/Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp @@ -310,6 +310,17 @@ TEST(SolidSyslogLwipRawTcpStream, OpenSetsKeepaliveOnPcb) CHECK((LwipTcpFake_LastTcpNewReturned()->so_options & SOF_KEEPALIVE) != 0); } +// Nagle is disabled (TF_NODELAY set) so small, un-pipelined writes — octet-framed +// records, and the multi-segment handshake flights of a stacked TLS layer — go out +// immediately instead of being held until the previous segment is ACKed (which +// deadlocks a TLS handshake mid-flight against a peer that ACKs per-flight). +TEST(SolidSyslogLwipRawTcpStream, OpenDisablesNagleOnPcb) +{ + SolidSyslogStream_Open(stream, address); + + CHECK((LwipTcpFake_LastTcpNewReturned()->flags & TF_NODELAY) != 0); +} + TEST(SolidSyslogLwipRawTcpStream, OpenRegistersTcpArgRecvErrSentCallbacks) { SolidSyslogStream_Open(stream, address); diff --git a/ci/docker-compose.bdd.yml b/ci/docker-compose.bdd.yml index 09b3776c..5c7080b0 100644 --- a/ci/docker-compose.bdd.yml +++ b/ci/docker-compose.bdd.yml @@ -123,13 +123,13 @@ services: # solidsyslog_user_tunables.h changes. command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @rtc and not @windows_wip and (@udp or @tcp or @tls or @mtls)' Bdd/features/ - # FreeRTOS + lwIP pair (S28.09 UDP, S28.10 TCP). Same shape as the freertos - # (+TCP) pair — its own syslog-ng on its own bridge network, behave sharing the - # netns so QEMU's slirp gateway 10.0.2.2 NATs to the pair's loopback where - # syslog-ng listens on 0.0.0.0:5514. The only differences are EXAMPLE_BINARY - # (the lwIP ELF) and the tag filter: this advisory lane runs UDP + TCP — - # TLS / mTLS / store land in later E28 stories, and @freertoslwipwip is the - # per-scenario escape hatch while the netif is bedding in. + # FreeRTOS + lwIP pair (S28.09 UDP, S28.10 TCP, S28.11 TLS/mTLS). Same shape as + # the freertos (+TCP) pair — its own syslog-ng on its own bridge network, behave + # sharing the netns so QEMU's slirp gateway 10.0.2.2 NATs to the pair's loopback + # where syslog-ng listens on 0.0.0.0:5514 (and 6514 tls / 6515 mtls). The only + # differences are EXAMPLE_BINARY (the lwIP ELF) and the tag filter: this lane + # runs UDP + TCP + TLS + mTLS — only @store lands in a later E28 story, and + # @freertoslwipwip is the per-scenario escape hatch while the netif is bedding in. syslog-ng-freertos-lwip: image: balabit/syslog-ng:4.8.2 volumes: @@ -178,7 +178,7 @@ services: # ELF rides the existing freertos driver — only the binary path differs. - BDD_TARGET=freertos - EXAMPLE_BINARY=build/freertos-cross-lwip/Bdd/Targets/FreeRtosLwip/SolidSyslogBddTargetLwip.elf - command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @freertoslwipwip and not @rtc and not @windows_wip and (@udp or @tcp) and not @tls and not @mtls and not @store' Bdd/features/ + command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @freertoslwipwip and not @rtc and not @windows_wip and (@udp or @tcp or @tls or @mtls) and not @store' Bdd/features/ volumes: bdd-output-linux: