From 7fc465516d66e728ac97896ef4b84d423efccd5e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 29 Jul 2026 11:35:56 +0100 Subject: [PATCH] feat: send over TLS instead of plain TCP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An mbedTLS stream wrapping the lwIP TCP stream, and the collector moves to 6514. Server authentication only: the device verifies the collector against the trust anchor it already holds and presents nothing of its own. Flash +13,076 B (+688 on the previous stage) RAM +35,664 B (+28,280) mbedTLS peak +14,724 B (36,056 absolute) Log stack +712 B (unchanged) Service +3,800 B (+2,808) Flash is the number worth reading. Adding TLS to this device costs well under a kilobyte, not the ~150 KB mbedTLS occupies, because a device that already speaks mTLS was carrying mbedTLS long before SolidSyslog arrived. What is measured here is the code that drives TLS, not TLS itself. It is also smaller than it would have been as a single hop from UDP: the TCP stream underneath was already linked and paid for. The RAM accounts for itself exactly, and only 632 bytes of it is the library: mbedTLS pool, 32 -> 53 KiB +21,504 service seam, 2 -> 8 KiB +6,144 stream and session objects +632 =+28,280 Both of those resizes were forced by the device, in that order, and both failed loudly first. The pool was sized for the broker session alone, so the handshake could not allocate: "MbedTlsStream category 0x0402", eighty-four times, and nothing reached the collector. Given room, the handshake then overflowed the service seam: "FATAL: stack overflow in task service", which takes the device down before it can report, so neither figure can be read from the run that fails. Both are then sized from a run that completes, by the rules the baseline already uses. The pool is the measured peak times 1.5 rounded up to the next KiB — 36,092 gives 53 KiB, and the margin is fragmentation headroom rather than spare capacity, because buffer_alloc hands out contiguous space. The seam is twice the measured high-water of 3,852, and configMINIMAL_STACK_SIZE * 15 falls 24 bytes short of that, so it takes 16. A peak that moves ~100 bytes run to run can put the first rule either side of a KiB boundary; 53 or 54 both hold it. The service stack carries the handshake, which is why it nearly quadruples while the log stack does not move at all. A task that calls Log is unaffected by the transport underneath it. The TLS stream takes the trust anchor and DRBG as handles at create time, not paths or PEM, so SimulatedExistingApp_StartCrypto() has to run before the scheduler. The drain window covers a connect and handshake rather than a datagram, sized against the library's 5 s handshake budget. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 4 ++-- README.md | 14 +++++++++----- app/AppConfig.h | 4 ++-- app/main.c | 3 ++- app/syslog/Syslog.c | 20 ++++++++++++++++---- measurements/stages.tsv | 1 + measurements/tls.csv | 13 +++++++++++++ run-report.md | 34 +++++++++++++++++----------------- 8 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 measurements/tls.csv diff --git a/CMakeLists.txt b/CMakeLists.txt index 060ef6f..f53a3cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ set(LWIP_CONTRIB_FREERTOS_DIR "${LWIP_DIR}/contrib/ports/freertos") # link target — only the header-configured packs below do. # https://docs.cososo.co.uk/solid-syslog/getting-started/#path-a--cmake-consumer # Pinned to a commit until there is a release tag to pin to. -set(SOLIDSYSLOG_PLATFORMS "LwipRaw;Atomics;FreeRtos;FatFs" CACHE STRING "" FORCE) +set(SOLIDSYSLOG_PLATFORMS "LwipRaw;Atomics;FreeRtos;FatFs;MbedTls" CACHE STRING "" FORCE) # Compile-time limits. Every tunable is #ifndef-guarded, so this file only needs # the ones this device wants changed. @@ -171,7 +171,7 @@ target_include_directories(baseline PRIVATE # library, or context struct sizes diverge between consumer and library. target_compile_definitions(baseline PRIVATE MBEDTLS_USER_CONFIG_FILE=${MBEDTLS_USER_CONFIG_HEADER}) -target_link_libraries(baseline PRIVATE mbedtls mbedx509 mbedcrypto SolidSyslog SolidSyslog::LwipRaw SolidSyslog::FreeRtos SolidSyslog::FatFs) +target_link_libraries(baseline PRIVATE mbedtls mbedx509 mbedcrypto SolidSyslog SolidSyslog::LwipRaw SolidSyslog::FreeRtos SolidSyslog::FatFs SolidSyslog::MbedTls) target_link_options(baseline PRIVATE -mcpu=cortex-m3 -mthumb diff --git a/README.md b/README.md index e0a66c3..8a52d94 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,18 @@ It builds on a baseline that simulates the sort of device you might be adding th measures itself: see [docs/baseline.md](docs/baseline.md) for what the baseline is, how the figures are made, and how to run it. -## This stage — Origin address +## This stage — TLS -`origin` gains the `ip` PARAM, read from the same interface address `HOSTNAME` reports. A relay or -NAT between the device and the collector rewrites what the collector observes; this is what the -device says about itself, and it survives the hop. +Records are encrypted in transit, and the collector is authenticated before anything is sent to it. +The sender wraps its TCP stream in mbedTLS, moves to 6514, and checks the collector against the +trust anchor the device already holds. + +Until now the audit trail crossed the network in cleartext — readable, and alterable, by anything +on the path. -**Cost above baseline: Flash +12,388 B, RAM +7,384 B.** +**Cost above baseline: Flash +13,076 B, RAM +35,664 B.** @@ -47,6 +50,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage. | Larger cap | headroom for the grown record, so full-width counters cannot push it into truncation | +11,980 | +9,440 | | Smaller ring | most of the cap rise given back, now the store rather than the ring holds a backlog | +11,980 | +7,384 | | Origin address | the device's own address in the record, which a relay or NAT between it and the collector cannot rewrite | +12,388 | +7,384 | +| TLS | a collector the device authenticates, and records no longer readable on the wire | +13,076 | +35,664 | *Deltas are bytes above the baseline, which is itself Flash 350,308 B, RAM 111,192 B.* diff --git a/app/AppConfig.h b/app/AppConfig.h index e2efca5..1b241d4 100644 --- a/app/AppConfig.h +++ b/app/AppConfig.h @@ -14,7 +14,7 @@ * it. Whatever deepens a seam grows it here and is charged for it. The reported * figure is high-water usage, which does not depend on the allocation. */ #define LOG_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 4U) -#define SERVICE_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 4U) +#define SERVICE_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 16U) #define LOG_TASK_PRIORITY (tskIDLE_PRIORITY + 1U) #define SERVICE_TASK_PRIORITY (tskIDLE_PRIORITY + 1U) @@ -30,6 +30,6 @@ * headroom, not spare capacity: buffer_alloc hands out contiguous space, so a * buffer only a little over the peak fails on fragmentation rather than on * capacity. Applied again wherever more is asked of mbedTLS. */ -#define SIMULATED_APP_MBEDTLS_HEAP_BYTES (32 * 1024) +#define SIMULATED_APP_MBEDTLS_HEAP_BYTES (53 * 1024) #endif /* APP_CONFIG_H */ diff --git a/app/main.c b/app/main.c index e42240a..a8fae3f 100644 --- a/app/main.c +++ b/app/main.c @@ -70,7 +70,8 @@ static void HarnessTask(void* parameters) * before the figures are taken. What arrived is the collector's word. */ bool logged = LogTask_EmitOnce(5000U); (void) printf("[device] first record logged: %s\n", logged ? "yes" : "FAILED"); - vTaskDelay(pdMS_TO_TICKS(500U)); + /* Long enough for the TLS negotiation, not just the send. */ + vTaskDelay(pdMS_TO_TICKS(3000U)); (void) Measure_Report(); diff --git a/app/syslog/Syslog.c b/app/syslog/Syslog.c index 522148e..d5d6d0e 100644 --- a/app/syslog/Syslog.c +++ b/app/syslog/Syslog.c @@ -1,7 +1,7 @@ /* See Syslog.h. * - * A TCP stream over lwIP behind a circular buffer: Log enqueues and returns, and - * the service task drains and sends. The mutex is what makes those two sides + * A TLS stream over lwIP TCP behind a circular buffer: Log enqueues and returns, + * and the service task drains and sends. The mutex is what makes those two sides * safe on different tasks. * * Unlike a header field, an SD PARAM has no NILVALUE: an unset one is omitted @@ -9,6 +9,8 @@ #include "Syslog.h" +#include "DeviceCertStore.h" + #include "SolidSyslogBlockStore.h" #include "SolidSyslogCircularBuffer.h" #include "SolidSyslogConfig.h" @@ -23,6 +25,7 @@ #include "SolidSyslogLwipRawMarshal.h" #include "SolidSyslogLwipRawResolver.h" #include "SolidSyslogLwipRawTcpStream.h" +#include "SolidSyslogMbedTlsStream.h" #include "SolidSyslogMetaSd.h" #include "SolidSyslogOriginSd.h" #include "SolidSyslogSdValue.h" @@ -47,7 +50,7 @@ * the resolver numeric-only — no DNS, so no LWIP_DNS and no DNS resolver * component to compile. */ #define SYSLOG_COLLECTOR_HOST "10.0.2.2" -#define SYSLOG_COLLECTOR_PORT ((uint16_t) 5601U) +#define SYSLOG_COLLECTOR_PORT ((uint16_t) 6514U) /* Absorbs records logged while the service task is busy sending. Many devices can * reduce this further: the store holds the backlog, so the ring only has to cover @@ -134,11 +137,20 @@ void Syslog_Start(void) struct SolidSyslogLwipRawTcpStreamConfig tcpConfig = {.Sleep = SyslogSleep}; + /* ServerName must match the name in the collector's certificate. */ + struct SolidSyslogMbedTlsStreamConfig tlsConfig = { + .Transport = SolidSyslogLwipRawTcpStream_Create(&tcpConfig), + .Sleep = SyslogSleep, + .Rng = DeviceCertStore_Rng(), + .CaChain = DeviceCertStore_CaChain(), + .ServerName = SYSLOG_COLLECTOR_HOST, + }; + /* No EndpointVersion — this collector never moves, so the sender resolves * once and pins it. */ struct SolidSyslogStreamSenderConfig senderConfig = { .Resolver = SolidSyslogLwipRawResolver_Create(), - .Stream = SolidSyslogLwipRawTcpStream_Create(&tcpConfig), + .Stream = SolidSyslogMbedTlsStream_Create(&tlsConfig), .Address = SolidSyslogLwipRawAddress_Create(), .Endpoint = CollectorEndpoint, }; diff --git a/measurements/stages.tsv b/measurements/stages.tsv index dcd00e2..f23a587 100644 --- a/measurements/stages.tsv +++ b/measurements/stages.tsv @@ -25,3 +25,4 @@ origin Origin the device named in the record itself, not inferred from the sourc cap-rise Larger cap headroom for the grown record, so full-width counters cannot push it into truncation buffer-halve Smaller ring most of the cap rise given back, now the store rather than the ring holds a backlog origin-ip Origin address the device's own address in the record, which a relay or NAT between it and the collector cannot rewrite +tls TLS a collector the device authenticates, and records no longer readable on the wire diff --git a/measurements/tls.csv b/measurements/tls.csv new file mode 100644 index 0000000..6928588 --- /dev/null +++ b/measurements/tls.csv @@ -0,0 +1,13 @@ +# tls figures (bytes) — captured by scripts/run.sh (CAPTURE=1). +# The device reads measurements/Baseline.csv as its frozen baseline and reports current-minus-Baseline. +flash_text,362736 +flash_data,648 +static_bss,146208 +heap_used,4440 +mbedtls_peak,36056 +mbedtls_free,18216 +lwip_mem_free,7576 +lwip_pbufs_free,13 +stack_log,832 +stack_service,3852 +stack_harness,2848 diff --git a/run-report.md b/run-report.md index c894411..813ecdb 100644 --- a/run-report.md +++ b/run-report.md @@ -1,4 +1,4 @@ -# solid-syslog-example — run (origin-ip) +# solid-syslog-example — run (tls) ## Device (self-measured) @@ -10,16 +10,16 @@ [device] first record logged: yes [report] --- SolidSyslog cost above baseline (simulated existing application) --- [report] key,current,baseline,used_above_baseline -[report] flash_text,362056,349992,12064 -[report] flash_data,640,316,324 -[report] static_bss,117936,110876,7060 +[report] flash_text,362736,349992,12744 +[report] flash_data,648,316,332 +[report] static_bss,146208,110876,35332 [report] heap_used,4440,4440,0 -[report] mbedtls_peak,21280,21332,-52 -[report] mbedtls_free,11488,11436,52 +[report] mbedtls_peak,36056,21332,14724 +[report] mbedtls_free,18216,11436,6780 [report] lwip_mem_free,7576,7576,0 [report] lwip_pbufs_free,13,14,-1 [report] stack_log,832,120,712 -[report] stack_service,1044,52,992 +[report] stack_service,3852,52,3800 [report] stack_harness,2848,2840,8 [report] --- end --- [device] ready @@ -29,7 +29,7 @@ ```text text data bss dec hex filename - 362048 648 117936 480632 75578 /w/build/baseline-cross/baseline.elf + 362728 656 146208 509592 7c698 /w/build/baseline-cross/baseline.elf ``` ## Listeners (proved before the device ran) @@ -47,23 +47,23 @@ ## Collector (syslog-ng) received ```text -wire <134>1 2026-07-29T10:10:44.420000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="242"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"] device started -parsed PRIORITY=134 TIMESTAMP=2026-07-29T10:10:44+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="242"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"] MSG=device started +wire <134>1 2026-07-29T10:34:01.430000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="243"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"] device started +parsed PRIORITY=134 TIMESTAMP=2026-07-29T10:34:01+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="243"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"] MSG=device started ``` -## Self-check (vs measurements/origin-ip.csv) +## Self-check (vs measurements/tls.csv) ```text - OK flash_text: 362056 (expected 362056, Δ0) - OK flash_data: 640 (expected 640, Δ0) - OK static_bss: 117936 (expected 117936, Δ0) + OK flash_text: 362736 (expected 362736, Δ0) + OK flash_data: 648 (expected 648, Δ0) + OK static_bss: 146208 (expected 146208, Δ0) OK heap_used: 4440 (expected 4440, Δ0) - OK mbedtls_peak: 21280 (expected 21280, Δ0) - OK mbedtls_free: 11488 (expected 11488, Δ0) + OK mbedtls_peak: 36056 (expected 36056, Δ0) + OK mbedtls_free: 18216 (expected 18216, Δ0) OK lwip_mem_free: 7576 (expected 7576, Δ0) OK lwip_pbufs_free: 13 (expected 13, Δ0) OK stack_log: 832 (expected 832, Δ0) - OK stack_service: 1044 (expected 1044, Δ0) + OK stack_service: 3852 (expected 3852, Δ0) OK stack_harness: 2848 (expected 2848, Δ0) ```