Skip to content

Commit 7fc4655

Browse files
DavidCozensclaude
andcommitted
feat: send over TLS instead of plain TCP
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) <noreply@anthropic.com>
1 parent 23bfd9c commit 7fc4655

8 files changed

Lines changed: 62 additions & 31 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ set(LWIP_CONTRIB_FREERTOS_DIR "${LWIP_DIR}/contrib/ports/freertos")
3939
# link target — only the header-configured packs below do.
4040
# https://docs.cososo.co.uk/solid-syslog/getting-started/#path-a--cmake-consumer
4141
# Pinned to a commit until there is a release tag to pin to.
42-
set(SOLIDSYSLOG_PLATFORMS "LwipRaw;Atomics;FreeRtos;FatFs" CACHE STRING "" FORCE)
42+
set(SOLIDSYSLOG_PLATFORMS "LwipRaw;Atomics;FreeRtos;FatFs;MbedTls" CACHE STRING "" FORCE)
4343

4444
# Compile-time limits. Every tunable is #ifndef-guarded, so this file only needs
4545
# the ones this device wants changed.
@@ -171,7 +171,7 @@ target_include_directories(baseline PRIVATE
171171
# library, or context struct sizes diverge between consumer and library.
172172
target_compile_definitions(baseline PRIVATE MBEDTLS_USER_CONFIG_FILE=${MBEDTLS_USER_CONFIG_HEADER})
173173

174-
target_link_libraries(baseline PRIVATE mbedtls mbedx509 mbedcrypto SolidSyslog SolidSyslog::LwipRaw SolidSyslog::FreeRtos SolidSyslog::FatFs)
174+
target_link_libraries(baseline PRIVATE mbedtls mbedx509 mbedcrypto SolidSyslog SolidSyslog::LwipRaw SolidSyslog::FreeRtos SolidSyslog::FatFs SolidSyslog::MbedTls)
175175

176176
target_link_options(baseline PRIVATE
177177
-mcpu=cortex-m3 -mthumb

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ It builds on a baseline that simulates the sort of device you might be adding th
1010
measures itself: see [docs/baseline.md](docs/baseline.md) for what the baseline is, how the
1111
figures are made, and how to run it.
1212

13-
## This stage — Origin address
13+
## This stage — TLS
1414

15-
`origin` gains the `ip` PARAM, read from the same interface address `HOSTNAME` reports. A relay or
16-
NAT between the device and the collector rewrites what the collector observes; this is what the
17-
device says about itself, and it survives the hop.
15+
Records are encrypted in transit, and the collector is authenticated before anything is sent to it.
16+
The sender wraps its TCP stream in mbedTLS, moves to 6514, and checks the collector against the
17+
trust anchor the device already holds.
18+
19+
Until now the audit trail crossed the network in cleartext — readable, and alterable, by anything
20+
on the path.
1821

1922
<!-- STAGE-COST:START (generated by scripts/gen-cost-table.py — do not edit by hand) -->
2023

21-
**Cost above baseline: Flash +12,388 B, RAM +7,384 B.**
24+
**Cost above baseline: Flash +13,076 B, RAM +35,664 B.**
2225

2326
<!-- STAGE-COST:END -->
2427

@@ -47,6 +50,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage.
4750
| Larger cap | headroom for the grown record, so full-width counters cannot push it into truncation | +11,980 | +9,440 |
4851
| Smaller ring | most of the cap rise given back, now the store rather than the ring holds a backlog | +11,980 | +7,384 |
4952
| 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 |
53+
| TLS | a collector the device authenticates, and records no longer readable on the wire | +13,076 | +35,664 |
5054

5155
*Deltas are bytes above the baseline, which is itself Flash 350,308 B, RAM 111,192 B.*
5256

app/AppConfig.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* it. Whatever deepens a seam grows it here and is charged for it. The reported
1515
* figure is high-water usage, which does not depend on the allocation. */
1616
#define LOG_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 4U)
17-
#define SERVICE_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 4U)
17+
#define SERVICE_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 16U)
1818
#define LOG_TASK_PRIORITY (tskIDLE_PRIORITY + 1U)
1919
#define SERVICE_TASK_PRIORITY (tskIDLE_PRIORITY + 1U)
2020

@@ -30,6 +30,6 @@
3030
* headroom, not spare capacity: buffer_alloc hands out contiguous space, so a
3131
* buffer only a little over the peak fails on fragmentation rather than on
3232
* capacity. Applied again wherever more is asked of mbedTLS. */
33-
#define SIMULATED_APP_MBEDTLS_HEAP_BYTES (32 * 1024)
33+
#define SIMULATED_APP_MBEDTLS_HEAP_BYTES (53 * 1024)
3434

3535
#endif /* APP_CONFIG_H */

app/main.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ static void HarnessTask(void* parameters)
7070
* before the figures are taken. What arrived is the collector's word. */
7171
bool logged = LogTask_EmitOnce(5000U);
7272
(void) printf("[device] first record logged: %s\n", logged ? "yes" : "FAILED");
73-
vTaskDelay(pdMS_TO_TICKS(500U));
73+
/* Long enough for the TLS negotiation, not just the send. */
74+
vTaskDelay(pdMS_TO_TICKS(3000U));
7475

7576
(void) Measure_Report();
7677

app/syslog/Syslog.c

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
/* See Syslog.h.
22
*
3-
* A TCP stream over lwIP behind a circular buffer: Log enqueues and returns, and
4-
* the service task drains and sends. The mutex is what makes those two sides
3+
* A TLS stream over lwIP TCP behind a circular buffer: Log enqueues and returns,
4+
* and the service task drains and sends. The mutex is what makes those two sides
55
* safe on different tasks.
66
*
77
* Unlike a header field, an SD PARAM has no NILVALUE: an unset one is omitted
88
* entirely rather than written as "-". */
99

1010
#include "Syslog.h"
1111

12+
#include "DeviceCertStore.h"
13+
1214
#include "SolidSyslogBlockStore.h"
1315
#include "SolidSyslogCircularBuffer.h"
1416
#include "SolidSyslogConfig.h"
@@ -23,6 +25,7 @@
2325
#include "SolidSyslogLwipRawMarshal.h"
2426
#include "SolidSyslogLwipRawResolver.h"
2527
#include "SolidSyslogLwipRawTcpStream.h"
28+
#include "SolidSyslogMbedTlsStream.h"
2629
#include "SolidSyslogMetaSd.h"
2730
#include "SolidSyslogOriginSd.h"
2831
#include "SolidSyslogSdValue.h"
@@ -47,7 +50,7 @@
4750
* the resolver numeric-only — no DNS, so no LWIP_DNS and no DNS resolver
4851
* component to compile. */
4952
#define SYSLOG_COLLECTOR_HOST "10.0.2.2"
50-
#define SYSLOG_COLLECTOR_PORT ((uint16_t) 5601U)
53+
#define SYSLOG_COLLECTOR_PORT ((uint16_t) 6514U)
5154

5255
/* Absorbs records logged while the service task is busy sending. Many devices can
5356
* reduce this further: the store holds the backlog, so the ring only has to cover
@@ -134,11 +137,20 @@ void Syslog_Start(void)
134137

135138
struct SolidSyslogLwipRawTcpStreamConfig tcpConfig = {.Sleep = SyslogSleep};
136139

140+
/* ServerName must match the name in the collector's certificate. */
141+
struct SolidSyslogMbedTlsStreamConfig tlsConfig = {
142+
.Transport = SolidSyslogLwipRawTcpStream_Create(&tcpConfig),
143+
.Sleep = SyslogSleep,
144+
.Rng = DeviceCertStore_Rng(),
145+
.CaChain = DeviceCertStore_CaChain(),
146+
.ServerName = SYSLOG_COLLECTOR_HOST,
147+
};
148+
137149
/* No EndpointVersion — this collector never moves, so the sender resolves
138150
* once and pins it. */
139151
struct SolidSyslogStreamSenderConfig senderConfig = {
140152
.Resolver = SolidSyslogLwipRawResolver_Create(),
141-
.Stream = SolidSyslogLwipRawTcpStream_Create(&tcpConfig),
153+
.Stream = SolidSyslogMbedTlsStream_Create(&tlsConfig),
142154
.Address = SolidSyslogLwipRawAddress_Create(),
143155
.Endpoint = CollectorEndpoint,
144156
};

measurements/stages.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ origin Origin the device named in the record itself, not inferred from the sourc
2525
cap-rise Larger cap headroom for the grown record, so full-width counters cannot push it into truncation
2626
buffer-halve Smaller ring most of the cap rise given back, now the store rather than the ring holds a backlog
2727
origin-ip Origin address the device's own address in the record, which a relay or NAT between it and the collector cannot rewrite
28+
tls TLS a collector the device authenticates, and records no longer readable on the wire

measurements/tls.csv

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# tls figures (bytes) — captured by scripts/run.sh (CAPTURE=1).
2+
# The device reads measurements/Baseline.csv as its frozen baseline and reports current-minus-Baseline.
3+
flash_text,362736
4+
flash_data,648
5+
static_bss,146208
6+
heap_used,4440
7+
mbedtls_peak,36056
8+
mbedtls_free,18216
9+
lwip_mem_free,7576
10+
lwip_pbufs_free,13
11+
stack_log,832
12+
stack_service,3852
13+
stack_harness,2848

run-report.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# solid-syslog-example — run (origin-ip)
1+
# solid-syslog-example — run (tls)
22

33
## Device (self-measured)
44

@@ -10,16 +10,16 @@
1010
[device] first record logged: yes
1111
[report] --- SolidSyslog cost above baseline (simulated existing application) ---
1212
[report] key,current,baseline,used_above_baseline
13-
[report] flash_text,362056,349992,12064
14-
[report] flash_data,640,316,324
15-
[report] static_bss,117936,110876,7060
13+
[report] flash_text,362736,349992,12744
14+
[report] flash_data,648,316,332
15+
[report] static_bss,146208,110876,35332
1616
[report] heap_used,4440,4440,0
17-
[report] mbedtls_peak,21280,21332,-52
18-
[report] mbedtls_free,11488,11436,52
17+
[report] mbedtls_peak,36056,21332,14724
18+
[report] mbedtls_free,18216,11436,6780
1919
[report] lwip_mem_free,7576,7576,0
2020
[report] lwip_pbufs_free,13,14,-1
2121
[report] stack_log,832,120,712
22-
[report] stack_service,1044,52,992
22+
[report] stack_service,3852,52,3800
2323
[report] stack_harness,2848,2840,8
2424
[report] --- end ---
2525
[device] ready
@@ -29,7 +29,7 @@
2929

3030
```text
3131
text data bss dec hex filename
32-
362048 648 117936 480632 75578 /w/build/baseline-cross/baseline.elf
32+
362728 656 146208 509592 7c698 /w/build/baseline-cross/baseline.elf
3333
```
3434

3535
## Listeners (proved before the device ran)
@@ -47,23 +47,23 @@
4747
## Collector (syslog-ng) received
4848

4949
```text
50-
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
51-
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
50+
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
51+
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
5252
```
5353

54-
## Self-check (vs measurements/origin-ip.csv)
54+
## Self-check (vs measurements/tls.csv)
5555

5656
```text
57-
OK flash_text: 362056 (expected 362056, Δ0)
58-
OK flash_data: 640 (expected 640, Δ0)
59-
OK static_bss: 117936 (expected 117936, Δ0)
57+
OK flash_text: 362736 (expected 362736, Δ0)
58+
OK flash_data: 648 (expected 648, Δ0)
59+
OK static_bss: 146208 (expected 146208, Δ0)
6060
OK heap_used: 4440 (expected 4440, Δ0)
61-
OK mbedtls_peak: 21280 (expected 21280, Δ0)
62-
OK mbedtls_free: 11488 (expected 11488, Δ0)
61+
OK mbedtls_peak: 36056 (expected 36056, Δ0)
62+
OK mbedtls_free: 18216 (expected 18216, Δ0)
6363
OK lwip_mem_free: 7576 (expected 7576, Δ0)
6464
OK lwip_pbufs_free: 13 (expected 13, Δ0)
6565
OK stack_log: 832 (expected 832, Δ0)
66-
OK stack_service: 1044 (expected 1044, Δ0)
66+
OK stack_service: 3852 (expected 3852, Δ0)
6767
OK stack_harness: 2848 (expected 2848, Δ0)
6868
```
6969

0 commit comments

Comments
 (0)