Skip to content

Commit 633a053

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,084 B (+704 on the previous stage) RAM +37,452 B (+28,280) mbedTLS peak +14,660 B (35,992 absolute) Log stack +680 B (unchanged) Service +3,768 B (+2,808) Only 632 bytes of the RAM is the library. The rest is a second concurrent session upstream: the mbedTLS pool grows 32 -> 53 KiB and the service seam 2 -> 8 KiB. Likewise the flash, which measures the code that drives TLS rather than TLS itself — a device already speaking mTLS was carrying mbedTLS long before SolidSyslog arrived. Both resizes were forced by the device and both failed loudly. The pool was sized for the broker session alone, so the handshake could not allocate: MbedTlsStream category 0x0402, repeatedly, and nothing reached the collector. Given room, the handshake then overflowed the service seam, which takes the device down before it can report — so neither figure can be read from the run that fails. The pool follows this device's existing rule, the measured peak times 1.5 rounded up to the next KiB. The service seam is generous for now and comes back with the rest at the end. The TLS stream takes the trust anchor and DRBG as handles at create time, not paths or PEM, so the device's crypto is initialised before the scheduler starts. ServerName is checked against the collector's certificate. The drain window covers a connect and handshake rather than a datagram. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ec68eb2 commit 633a053

8 files changed

Lines changed: 80 additions & 47 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;StdAtomic;FreeRtos;FatFs" CACHE STRING "" FORCE)
42+
set(SOLIDSYSLOG_PLATFORMS "LwipRaw;StdAtomic;FreeRtos;FatFs;MbedTls" CACHE STRING "" FORCE)
4343

4444
include(FetchContent)
4545
FetchContent_Declare(SolidSyslog
@@ -167,7 +167,7 @@ target_include_directories(baseline PRIVATE
167167
# library, or context struct sizes diverge between consumer and library.
168168
target_compile_definitions(baseline PRIVATE MBEDTLS_USER_CONFIG_FILE=${MBEDTLS_USER_CONFIG_HEADER})
169169

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

172172
target_link_options(baseline PRIVATE
173173
-mcpu=cortex-m3 -mthumb

‎README.md‎

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,44 @@ 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-
Add the `ip` PARAM to the origin element, sourced from the same interface address the HOSTNAME field
16-
reports.
15+
Wrap the byte stream in TLS, layered over the TCP stream from the previous stage. The device
16+
verifies the collector against a trust anchor it already holds, so records can be read only by that
17+
collector and cannot be altered in transit. The collector is authenticated to the device; the device
18+
is not yet authenticated to the collector.
1719

1820
```c
19-
struct SolidSyslogOriginSdConfig originConfig = {
20-
/* ... as the previous stage ... */
21-
.GetIpCount = SyslogOriginIpCount,
22-
.GetIpAt = SyslogOriginIpAt,
21+
struct SolidSyslogMbedTlsStreamConfig tlsConfig = {
22+
.Transport = SolidSyslogLwipRawTcpStream_Create(&tcpConfig),
23+
.Sleep = SyslogSleep,
24+
.Rng = DeviceCertStore_Rng(),
25+
.CaChain = DeviceCertStore_CaChain(),
26+
.ServerName = SYSLOG_COLLECTOR_HOST,
2327
};
24-
```
2528

26-
```text
27-
... [origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"] device started
29+
.Stream = SolidSyslogMbedTlsStream_Create(&tlsConfig),
2830
```
2931

30-
A relay or NAT between the device and the collector rewrites the address the collector observes.
31-
`ip` is what the device says about itself, and that survives the hop.
32+
`ServerName` is checked against the collector's certificate, so it has to match a name that
33+
certificate carries. The trust anchor and the DRBG are passed as handles rather than paths or PEM,
34+
so the device's crypto is initialised before the scheduler starts.
3235

33-
The PARAM is repeatable, so the library asks for a count and then one value per index rather than
34-
taking a single string. This device has one address and returns one, and returns none before the
35-
interface has an address — a count of zero omits the PARAM rather than emitting an empty one.
36+
A second concurrent session has to be paid for upstream. The mbedTLS allocator and the task that
37+
carries the handshake both need sizing for it; both fail loudly when they are not, and neither can
38+
be sized from the run that fails.
3639

37-
`SyslogFields_IpAddress` becomes the single place that reads the address, and HOSTNAME formats the
38-
same string through it. Two fields that must agree now cannot disagree.
40+
**When you need it.** If the log path crosses a network you do not control, or if someone reading
41+
records in transit would learn something they should not. Also if the device needs to know it is
42+
talking to the real collector rather than to whatever answered on that address.
3943

40-
**When you need it.** If anything sits between the device and the collector — a relay, a gateway, or
41-
NAT — and the source address the collector sees can no longer be trusted to identify the device.
44+
> If your device does not already run TLS, the library and its trust material will dominate
45+
> everything on this page. This device already holds a TLS session for its own broker, so what this
46+
> stage adds is the adapter and a second session.
4247
4348
<!-- STAGE-COST:START (generated by scripts/gen-cost-table.py — do not edit by hand) -->
4449

45-
**Cost above baseline: Flash +12,380 B, RAM +9,172 B.**
50+
**Cost above baseline: Flash +13,084 B, RAM +37,452 B.**
4651

4752
<!-- STAGE-COST:END -->
4853

@@ -68,6 +73,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage.
6873
| File store | records that survive a failed send, spooled to disk with a checksum at rest | +11,584 | +9,128 |
6974
| Origin | the device named in the record itself, not inferred from the source address | +11,988 | +9,172 |
7075
| Origin address | the device's own address in the record, which a relay or NAT between it and the collector cannot rewrite | +12,380 | +9,172 |
76+
| TLS | a collector the device authenticates, and records no longer readable on the wire | +13,084 | +37,452 |
7177

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

‎app/AppConfig.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* beneath, and neither fits the FreeRTOS floor. Sized generously here and
1616
* tightened against measured high-water marks once the pipeline is complete. */
1717
#define LOG_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 4U)
18-
#define SERVICE_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 4U)
18+
#define SERVICE_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 16U)
1919
#define LOG_TASK_PRIORITY (tskIDLE_PRIORITY + 1U)
2020
#define SERVICE_TASK_PRIORITY (tskIDLE_PRIORITY + 1U)
2121

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

3636
#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
/* Depth enough to absorb a burst while the sender is busy, without sizing for a
5356
* backlog the store is there to hold. */
@@ -133,11 +136,20 @@ void Syslog_Start(void)
133136

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

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

‎measurements/stages.tsv‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ time-quality Time quality a timestamp the collector knows how far to trust, and
2222
file-store File store records that survive a failed send, spooled to disk with a checksum at rest
2323
origin Origin the device named in the record itself, not inferred from the source address
2424
origin-ip Origin address the device's own address in the record, which a relay or NAT between it and the collector cannot rewrite
25+
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,362744
4+
flash_data,648
5+
static_bss,147996
6+
heap_used,4440
7+
mbedtls_peak,35992
8+
mbedtls_free,18280
9+
lwip_mem_free,7576
10+
lwip_pbufs_free,14
11+
stack_log,800
12+
stack_service,3820
13+
stack_harness,2848

‎run-report.md‎

Lines changed: 18 additions & 18 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,362048,349992,12056
14-
[report] flash_data,640,316,324
15-
[report] static_bss,119724,110876,8848
13+
[report] flash_text,362744,349992,12752
14+
[report] flash_data,648,316,332
15+
[report] static_bss,147996,110876,37120
1616
[report] heap_used,4440,4440,0
17-
[report] mbedtls_peak,21260,21332,-72
18-
[report] mbedtls_free,11508,11436,72
17+
[report] mbedtls_peak,36108,21332,14776
18+
[report] mbedtls_free,18164,11436,6728
1919
[report] lwip_mem_free,7576,7576,0
2020
[report] lwip_pbufs_free,13,14,-1
2121
[report] stack_log,800,120,680
22-
[report] stack_service,1012,52,960
22+
[report] stack_service,3820,52,3768
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-
362040 648 119724 482412 75c6c /w/build/baseline-cross/baseline.elf
32+
362736 656 147996 511388 7cd9c /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-08-16T07:24:12.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-08-16T07:24:12+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
50+
wire <134>1 2026-08-16T09:14:51.440000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="244"][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-08-16T09:14:51+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="244"][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: 362048 (expected 362048, Δ0)
58-
OK flash_data: 640 (expected 640, Δ0)
59-
OK static_bss: 119724 (expected 119724, Δ0)
57+
OK flash_text: 362744 (expected 362744, Δ0)
58+
OK flash_data: 648 (expected 648, Δ0)
59+
OK static_bss: 147996 (expected 147996, Δ0)
6060
OK heap_used: 4440 (expected 4440, Δ0)
61-
OK mbedtls_peak: 21260 (expected 21256, Δ4)
62-
OK mbedtls_free: 11508 (expected 11512, Δ4)
61+
OK mbedtls_peak: 36108 (expected 35992, Δ116)
62+
OK mbedtls_free: 18164 (expected 18280, Δ116)
6363
OK lwip_mem_free: 7576 (expected 7576, Δ0)
64-
OK lwip_pbufs_free: 13 (expected 13, Δ0)
64+
OK lwip_pbufs_free: 13 (expected 14, Δ1)
6565
OK stack_log: 800 (expected 800, Δ0)
66-
OK stack_service: 1012 (expected 1012, Δ0)
66+
OK stack_service: 3820 (expected 3820, Δ0)
6767
OK stack_harness: 2848 (expected 2848, Δ0)
6868
```
6969

0 commit comments

Comments
 (0)