Files
MicrOBU/obu-firmware/test/host/test_chain.c
T

731 lines
33 KiB
C
Raw Normal View History

// Host-side chain test for obu-firmware. See README.md in this folder.
//
// Builds frames with the firmware's own TX code (geonet_wrap_shb -> dot11p_build_frame) and parses
// them back with its own RX code (gn_unwrap_its), all compiled from ../../main unmodified.
//
// Two kinds of check, on purpose. The round trip proves TX and RX agree with each other. The byte
// checks at fixed offsets prove they agree with EN 302 636-4-1, and only those catch a mistake made
// the same way on both sides: the missing 4-byte SHB field (fixed 2026-08-13) round-tripped fine
// between two ESP32s and was wrong against every other station. So expected values here come from
// the standard, vanetza's serializers and real captures - never from reading geonet.c.
//
// Every frame handed to gn_unwrap_its is first copied so that its last byte sits right before a
// no-access page (test_util.c): reading even one byte past a frame crashes the test instead of
// passing quietly. MinGW has no AddressSanitizer; this covers what matters for a radio parser.
//
// Usage: test_chain [out.pcap]
// With a path, also writes every test frame to a pcap (linktype 105, bare 802.11) so a second,
// independent parser can read them: ../pcap_gn_tally.py, or Wireshark. The GeoBroadcast and
// signed frames carry stub payloads/signatures; their headers are real.
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dot11p.h"
#include "geonet.h"
#include "gn_unwrap.h"
#include "serial_link.h" // SERIAL_LINK_MAX_PAYLOAD only; serial_link.c itself needs ESP-IDF
#include "test_util.h"
// Same buffer sizes as tx_radio_task in main.c. Keep them in step with it.
#define GN_BUF_LEN (SERIAL_LINK_MAX_PAYLOAD + 64)
#define FRAME_BUF_LEN (SERIAL_LINK_MAX_PAYLOAD + 192)
// ---- Checks ---------------------------------------------------------------------------------
static int s_checks;
static int s_failures;
static void check(bool ok, int line, const char *fmt, ...)
{
s_checks++;
if (ok) {
return;
}
s_failures++;
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "FAIL line %d [%s]: ", line, tu_context());
vfprintf(stderr, fmt, ap);
fputc('\n', stderr);
va_end(ap);
}
#define CHECK(cond, ...) check((cond), __LINE__, __VA_ARGS__)
// ---- Test data ------------------------------------------------------------------------------
static uint16_t rd16(const uint8_t *p)
{
return (uint16_t)((p[0] << 8) | p[1]);
}
static uint32_t rd32(const uint8_t *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}
// The app's reference CAM: the expected bytes of CamEncodeGoldenTest.kt (stationID 999999), which
// asn1tools decodes and re-encodes byte-identically. The chain treats it as opaque bytes. Not taken
// from the old captures on purpose: our own CAMs there predate the 2026-08-20 yawRateConfidence fix
// and do not decode.
static const uint8_t k_cam[] = {
0x02, 0x02, 0x00, 0x0f, 0x42, 0x3f, 0x37, 0x00, 0x40, 0x2a, 0xb2, 0x15, 0xaf, 0x6e, 0x28, 0x64,
0x77, 0xdf, 0xff, 0xff, 0xfc, 0x23, 0xb7, 0x74, 0x3e, 0x00, 0x27, 0xff, 0xc0, 0xd0, 0xfe, 0x01,
0x18, 0x32, 0x93, 0x37, 0xfe, 0xeb, 0xff, 0xf6, 0x00, 0x00, 0x00,
};
#define CAM_LEN ((int)sizeof k_cam)
static const uint8_t k_mac[6] = {0x02, 0x11, 0x22, 0x33, 0x44, 0x55};
static const uint8_t k_bcast[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t k_llc_snap_gn[8] = {0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x89, 0x47};
// The 8 bytes that follow the 802.11 frame in every frame recorded through the ESP32-C5's
// promiscuous RX (these from capture_20260817_171055.pcap). See gn_unwrap.h, "Payload bounds".
static const uint8_t k_rx_trailer[8] = {0xc8, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00};
// Header lengths from EN 302 636-4-1 / IEEE 802.11, used to compute every offset below.
enum {
MAC_HDR = 24, QOS_CTRL = 2, LLC_SNAP = 8, GN_BASIC = 4, GN_COMMON = 8,
SHB_EXT = 28, // Source Position Vector (24) + DCC-MCO / reserved (4)
GBC_EXT = 44, // SN (2) + reserved (2) + SO PV (24) + area (12) + reserved (4)
BTP_B = 4,
GN_COMMON_PAYLOAD_LEN_FIELD = 4, // offset of the payload-length field in the Common Header
};
// Where the ITS payload starts in a non-QoS SHB frame.
#define SHB_PAYLOAD_OFFSET (MAC_HDR + LLC_SNAP + GN_BASIC + GN_COMMON + SHB_EXT + BTP_B)
static gn_lpv_t test_lpv(void)
{
gn_lpv_t lpv;
memset(&lpv, 0, sizeof lpv);
memcpy(lpv.mac, k_mac, sizeof lpv.mac);
lpv.station_type = 2; // cyclist
lpv.pai = true;
lpv.tst_ms = 0x89ABCDEFu;
lpv.lat_tenmicrodeg = 535546667; // the bench, 53.5546667 N
lpv.lon_tenmicrodeg = -10022389; // negative on purpose: the sign has to survive
lpv.speed_cms = 1234;
lpv.heading_decideg = 2700;
return lpv;
}
// The TX path exactly as tx_radio_task runs it. Returns the frame length, or <= 0 on failure.
static int build(const uint8_t *payload, int len, const gn_lpv_t *lpv, uint16_t port, bool qos,
uint8_t *frame, size_t frame_size)
{
static uint8_t gn[GN_BUF_LEN];
int gn_len = geonet_wrap_shb(payload, len, lpv, port, gn, sizeof gn);
if (gn_len <= 0) {
return gn_len;
}
return dot11p_build_frame(gn, gn_len, lpv->mac, frame, frame_size, qos);
}
// Every prefix of a frame, each ending against the guard page. Up to the end of the BTP-B header
// nothing may be accepted; a frame cut inside the payload is accepted as truncated with what
// arrived; from `payload_end` on, the payload is exactly the declared one whatever follows it.
static void sweep(const char *name, const uint8_t *f, int len, int payload_start, int payload_end)
{
for (int n = 0; n <= len; n++) {
tu_set_context("truncation sweep, %s, %d of %d bytes", name, n, len);
const uint8_t *g = tu_guarded(f, n);
gn_rx_t rx;
const bool ok = gn_unwrap_its(g, n, &rx);
if (n <= payload_start) {
CHECK(!ok, "%s cut to %d bytes was accepted", name, n);
} else if (n < payload_end) {
CHECK(ok && rx.truncated && rx.payload == g + payload_start &&
rx.payload_len == n - payload_start,
"%s cut to %d bytes: ok=%d truncated=%d payload_len=%d", name, n, ok,
ok && rx.truncated, ok ? rx.payload_len : -1);
} else {
CHECK(ok && !rx.truncated && rx.payload == g + payload_start &&
rx.payload_len == payload_end - payload_start,
"%s at %d bytes: ok=%d truncated=%d payload_len=%d", name, n, ok,
ok && rx.truncated, ok ? rx.payload_len : -1);
}
}
}
// ---- SHB: layout, position vector, bounds ---------------------------------------------------
// Every byte of a CAM frame against the standard, then back through the RX path.
static void test_shb_layout(bool qos)
{
tu_set_context("SHB layout, qos=%d", qos);
const gn_lpv_t lpv = test_lpv();
uint8_t f[FRAME_BUF_LEN];
const int len = build(k_cam, CAM_LEN, &lpv, 2001, qos, f, sizeof f);
const int llc = MAC_HDR + (qos ? QOS_CTRL : 0);
const int basic = llc + LLC_SNAP;
const int common = basic + GN_BASIC;
const int shb = common + GN_COMMON;
const int btp = shb + SHB_EXT;
const int pay = btp + BTP_B;
CHECK(len == pay + CAM_LEN, "frame length %d, expected %d", len, pay + CAM_LEN);
if (len != pay + CAM_LEN) {
return;
}
// 802.11 MAC header
CHECK(f[0] == (qos ? 0x88 : 0x08) && f[1] == 0x00, "frame control %02x %02x", f[0], f[1]);
CHECK(memcmp(f + 4, k_bcast, 6) == 0, "addr1 must be broadcast");
CHECK(memcmp(f + 10, k_mac, 6) == 0, "addr2 must be the pseudonym");
CHECK(memcmp(f + 16, k_bcast, 6) == 0, "addr3 (BSSID) must be the OCB wildcard");
if (qos) {
CHECK(f[24] == 0 && f[25] == 0, "QoS control %02x %02x", f[24], f[25]);
}
CHECK(memcmp(f + llc, k_llc_snap_gn, 8) == 0, "LLC/SNAP with EtherType 0x8947");
// GN Basic Header
CHECK(f[basic + 0] == 0x11, "basic: version 1 + NextHeader 1 (Common, unsecured), got %02x",
f[basic + 0]);
CHECK(f[basic + 1] == 0x00, "basic: reserved, got %02x", f[basic + 1]);
// Multiplier 1 in the upper 6 bits, base 1 (= 1 s) in the lower 2: 1 s, what every real station
// in the recordings sends its CAMs with. Was 0x83 = 3200 s until 2026-09-11.
CHECK(f[basic + 2] == 0x05, "basic: lifetime 0x05 (1 s), got 0x%02x", f[basic + 2]);
CHECK(f[basic + 3] == 1, "basic: remaining hop limit 1, got %d", f[basic + 3]);
// GN Common Header
CHECK(f[common + 0] == 0x20, "common: NextHeader 2 (BTP-B), got %02x", f[common + 0]);
CHECK(f[common + 1] == 0x50, "common: HeaderType 5 (TSB) / subtype 0 (single hop), got %02x",
f[common + 1]);
CHECK(f[common + 2] == 0x02, "common: traffic class TC-ID 2 (DP2, CAM), got %02x", f[common + 2]);
CHECK(f[common + 3] == 0x80, "common: flags = mobile, got %02x", f[common + 3]);
CHECK(rd16(f + common + 4) == BTP_B + CAM_LEN,
"common: payload length must count BTP-B + payload only, got %d", rd16(f + common + 4));
CHECK(f[common + 6] == 1, "common: maximum hop limit 1, got %d", f[common + 6]);
CHECK(f[common + 7] == 0, "common: reserved, got %02x", f[common + 7]);
// SHB extended header: Source Position Vector, then 4 bytes DCC-MCO / reserved
// GN_ADDR: M flag bit 15, station type bits 14..10 (vanetza geonet/address.cpp), MID.
CHECK(rd16(f + shb) == (2u << 10), "GN_ADDR: M=0, station type 2, got %04x", rd16(f + shb));
CHECK(memcmp(f + shb + 2, k_mac, 6) == 0, "GN_ADDR MID must equal the 802.11 source address");
CHECK(rd32(f + shb + 8) == 0x89ABCDEFu, "TST, got %08lx", (unsigned long)rd32(f + shb + 8));
CHECK((int32_t)rd32(f + shb + 12) == 535546667, "latitude, got %ld",
(long)(int32_t)rd32(f + shb + 12));
CHECK((int32_t)rd32(f + shb + 16) == -10022389, "longitude, got %ld",
(long)(int32_t)rd32(f + shb + 16));
CHECK(rd16(f + shb + 20) == (0x8000 | 1234), "PAI bit 15 + speed 1234, got %04x",
rd16(f + shb + 20));
CHECK(rd16(f + shb + 22) == 2700, "heading, got %d", rd16(f + shb + 22));
CHECK(rd32(f + shb + 24) == 0, "DCC-MCO / reserved: present and zero, got %08lx",
(unsigned long)rd32(f + shb + 24));
// BTP-B, payload
CHECK(rd16(f + btp) == 2001, "BTP-B destination port, got %d", rd16(f + btp));
CHECK(rd16(f + btp + 2) == 0, "BTP-B destination port info, got %d", rd16(f + btp + 2));
CHECK(memcmp(f + pay, k_cam, (size_t)CAM_LEN) == 0, "payload bytes");
// ...and back through the RX path
const uint8_t *g = tu_guarded(f, len);
gn_rx_t rx;
const bool ok = gn_unwrap_its(g, len, &rx);
CHECK(ok, "gn_unwrap_its rejected our own frame");
if (ok) {
CHECK(rx.btp_dest_port == 2001, "RX port %d", rx.btp_dest_port);
CHECK(!rx.has_geo_area, "RX reports a geo area for an SHB frame");
CHECK(!rx.signed_unverified, "RX reports an unsecured frame as signed");
CHECK(!rx.truncated, "RX reports a whole frame as truncated");
CHECK(rx.payload == g + pay, "RX payload offset %d, expected %d", (int)(rx.payload - g), pay);
CHECK(rx.payload_len == CAM_LEN, "RX payload length %d, expected %d", rx.payload_len, CAM_LEN);
CHECK(rx.payload_len == CAM_LEN && memcmp(rx.payload, k_cam, (size_t)CAM_LEN) == 0,
"RX payload bytes differ from what was sent");
}
tu_pcap_add(f, len);
}
// The Source Position Vector's packed fields at their edges.
static void test_pv_encoding(void)
{
// Offsets inside the GeoNetworking packet (no 802.11 header): SO PV starts after Basic + Common.
const int pv = GN_BASIC + GN_COMMON;
uint8_t gn[GN_BUF_LEN];
// Speed is 15-bit signed (geonet.h), clamped rather than wrapped; PAI is bit 15.
static const struct { int16_t speed; bool pai; uint16_t expect; } speeds[] = {
{0, false, 0x0000}, {1234, true, 0x84D2}, {16383, false, 0x3FFF},
{16384, false, 0x3FFF}, {32767, false, 0x3FFF}, {-100, false, 0x7F9C},
{-16384, false, 0x4000}, {-32768, true, 0xC000},
};
for (size_t i = 0; i < sizeof speeds / sizeof speeds[0]; i++) {
tu_set_context("PV speed %d pai %d", speeds[i].speed, speeds[i].pai);
gn_lpv_t lpv = test_lpv();
lpv.speed_cms = speeds[i].speed;
lpv.pai = speeds[i].pai;
const int n = geonet_wrap_shb(k_cam, CAM_LEN, &lpv, 2001, gn, sizeof gn);
CHECK(n > 0 && rd16(gn + pv + 20) == speeds[i].expect, "speed %d pai %d: got %04x, expected %04x",
speeds[i].speed, speeds[i].pai, rd16(gn + pv + 20), speeds[i].expect);
}
// Heading is 0..3599 tenths of a degree, wrapped.
static const struct { uint16_t in, expect; } headings[] = {
{0, 0}, {2700, 2700}, {3599, 3599}, {3600, 0}, {3601, 1}, {65535, 735},
};
for (size_t i = 0; i < sizeof headings / sizeof headings[0]; i++) {
tu_set_context("PV heading %d", headings[i].in);
gn_lpv_t lpv = test_lpv();
lpv.heading_decideg = headings[i].in;
const int n = geonet_wrap_shb(k_cam, CAM_LEN, &lpv, 2001, gn, sizeof gn);
CHECK(n > 0 && rd16(gn + pv + 22) == headings[i].expect, "heading %d: got %d, expected %d",
headings[i].in, rd16(gn + pv + 22), headings[i].expect);
}
// Station type has 5 bits; anything larger must not spill into the M flag.
static const struct { uint8_t in; uint16_t expect; } types[] = {
{2, 0x0800}, {15, 0x3C00}, {0xFF, 0x7C00},
};
for (size_t i = 0; i < sizeof types / sizeof types[0]; i++) {
tu_set_context("PV station type %d", types[i].in);
gn_lpv_t lpv = test_lpv();
lpv.station_type = types[i].in;
const int n = geonet_wrap_shb(k_cam, CAM_LEN, &lpv, 2001, gn, sizeof gn);
CHECK(n > 0 && rd16(gn + pv) == types[i].expect, "station type %d: GN_ADDR %04x, expected %04x",
types[i].in, rd16(gn + pv), types[i].expect);
}
}
// Output buffers: an exact fit works and writes nothing past its end; one byte less is refused.
static void test_bounds(void)
{
const gn_lpv_t lpv = test_lpv();
const int gn_len = GN_BASIC + GN_COMMON + SHB_EXT + BTP_B + CAM_LEN;
tu_set_context("geonet_wrap_shb bounds");
CHECK(geonet_wrap_shb(k_cam, CAM_LEN, &lpv, 2001, tu_guard_buf(gn_len), (size_t)gn_len) == gn_len,
"geonet_wrap_shb: exact-size buffer must fit");
CHECK(geonet_wrap_shb(k_cam, CAM_LEN, &lpv, 2001, tu_guard_buf(gn_len - 1), (size_t)gn_len - 1) == -1,
"geonet_wrap_shb: one byte short must be refused");
uint8_t gn[GN_BUF_LEN];
geonet_wrap_shb(k_cam, CAM_LEN, &lpv, 2001, gn, sizeof gn);
for (int qos = 0; qos <= 1; qos++) {
tu_set_context("dot11p_build_frame bounds, qos=%d", qos);
const int frame_len = MAC_HDR + (qos ? QOS_CTRL : 0) + LLC_SNAP + gn_len;
CHECK(dot11p_build_frame(gn, gn_len, k_mac, tu_guard_buf(frame_len), (size_t)frame_len, qos) ==
frame_len,
"dot11p_build_frame qos=%d: exact-size buffer must fit", qos);
CHECK(dot11p_build_frame(gn, gn_len, k_mac, tu_guard_buf(frame_len - 1), (size_t)frame_len - 1,
qos) == -1,
"dot11p_build_frame qos=%d: one byte short must be refused", qos);
}
}
// The largest CAM the serial link can carry must survive the whole chain in main.c's buffers.
static void test_max_payload(void)
{
static uint8_t big[SERIAL_LINK_MAX_PAYLOAD];
for (int i = 0; i < SERIAL_LINK_MAX_PAYLOAD; i++) {
big[i] = (uint8_t)(i * 7 + 3);
}
for (int qos = 0; qos <= 1; qos++) {
tu_set_context("max payload %d, qos=%d", SERIAL_LINK_MAX_PAYLOAD, qos);
const gn_lpv_t lpv = test_lpv();
uint8_t f[FRAME_BUF_LEN];
const int hdr = MAC_HDR + (qos ? QOS_CTRL : 0) + LLC_SNAP + GN_BASIC + GN_COMMON + SHB_EXT + BTP_B;
const int len = build(big, SERIAL_LINK_MAX_PAYLOAD, &lpv, 2001, qos, f, sizeof f);
CHECK(len == hdr + SERIAL_LINK_MAX_PAYLOAD, "qos=%d: frame length %d, expected %d", qos, len,
hdr + SERIAL_LINK_MAX_PAYLOAD);
if (len <= 0) {
continue;
}
gn_rx_t rx;
const uint8_t *g = tu_guarded(f, len);
const bool ok = gn_unwrap_its(g, len, &rx);
CHECK(ok && !rx.truncated && rx.payload_len == SERIAL_LINK_MAX_PAYLOAD &&
memcmp(rx.payload, big, SERIAL_LINK_MAX_PAYLOAD) == 0,
"qos=%d: max-size payload did not round-trip", qos);
tu_pcap_add(f, len);
}
}
// ---- GeoBroadcast ---------------------------------------------------------------------------
// A GeoBroadcast DENM frame laid out by hand from EN 302 636-4-1 clause 9.8.5, since the firmware
// has no GBC builder and real RSUs send DENM this way. QoS Data, lifetime 0x79 (30 s) and the
// 44-byte extended header all as seen from the CiT One in the recordings.
static const uint8_t k_rsu_mac[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE};
static const uint8_t k_denm_stub[] = {0x02, 0x01, 0x00, 0x00, 0x30, 0x39, 0xDE, 0xAD, 0xBE,
0xEF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
#define DENM_STUB_LEN ((int)sizeof k_denm_stub)
#define GBC_PAYLOAD_OFFSET (MAC_HDR + QOS_CTRL + LLC_SNAP + GN_BASIC + GN_COMMON + GBC_EXT + BTP_B)
static uint8_t *put(uint8_t *p, const void *src, int n)
{
memcpy(p, src, (size_t)n);
return p + n;
}
static uint8_t *put16(uint8_t *p, uint16_t v)
{
*p++ = (uint8_t)(v >> 8);
*p++ = (uint8_t)v;
return p;
}
static uint8_t *put32(uint8_t *p, uint32_t v)
{
p = put16(p, (uint16_t)(v >> 16));
return put16(p, (uint16_t)v);
}
static int build_gbc(uint8_t subtype, int32_t area_lat, int32_t area_lon, uint16_t dist_a, uint8_t *f)
{
uint8_t *p = f;
// 802.11 QoS Data: FC, duration, addr1..3, sequence control, QoS control
p = put16(p, 0x8800);
p = put16(p, 0);
p = put(p, k_bcast, 6);
p = put(p, k_rsu_mac, 6);
p = put(p, k_bcast, 6);
p = put16(p, 0x1000);
p = put16(p, 0);
p = put(p, k_llc_snap_gn, 8);
// Basic: version 1 / NH common, reserved, lifetime 30 s, RHL 10
const uint8_t basic[4] = {0x11, 0x00, 0x79, 10};
p = put(p, basic, 4);
// Common: NH BTP-B, HT 4 (GBC) / subtype = area shape, TC 1, flags 0 (stationary), PL, MHL, reserved
const uint8_t common[4] = {0x20, (uint8_t)(0x40 | subtype), 0x01, 0x00};
p = put(p, common, 4);
p = put16(p, (uint16_t)(BTP_B + DENM_STUB_LEN));
*p++ = 10;
*p++ = 0;
// GBC extended header: SN, reserved, SO PV (GN_ADDR with station type 15 = RSU, MID, TST, lat,
// lon, PAI/speed, heading), area centre lat/lon, DistanceA, DistanceB, angle, reserved
p = put16(p, 0x1234);
p = put16(p, 0);
p = put16(p, 15u << 10);
p = put(p, k_rsu_mac, 6);
p = put32(p, 1000);
p = put32(p, 535540100);
p = put32(p, 100220100);
p = put16(p, 0);
p = put16(p, 0);
p = put32(p, (uint32_t)area_lat);
p = put32(p, (uint32_t)area_lon);
p = put16(p, dist_a);
p = put16(p, subtype ? 40 : 0);
p = put16(p, subtype ? 900 : 0);
p = put16(p, 0);
// BTP-B: DENM
p = put16(p, 2002);
p = put16(p, 0);
p = put(p, k_denm_stub, DENM_STUB_LEN);
return (int)(p - f);
}
static void test_gbc_rx(void)
{
static const struct { uint8_t subtype; int32_t lat, lon; uint16_t dist_a; } areas[] = {
{0, 535540000, 100220000, 250}, // circle around the bench
{1, -338600000, 1512100000, 1000}, // rectangle; southern and far-eastern: signs and range
{2, 535540000, -100220000, 65535}, // ellipse; western, largest DistanceA
};
for (size_t i = 0; i < sizeof areas / sizeof areas[0]; i++) {
tu_set_context("GBC subtype %d", areas[i].subtype);
uint8_t f[256];
const int len = build_gbc(areas[i].subtype, areas[i].lat, areas[i].lon, areas[i].dist_a, f);
CHECK(len == GBC_PAYLOAD_OFFSET + DENM_STUB_LEN, "GBC frame length %d", len);
gn_rx_t rx;
const uint8_t *g = tu_guarded(f, len);
const bool ok = gn_unwrap_its(g, len, &rx);
CHECK(ok, "GBC subtype %d rejected", areas[i].subtype);
if (ok) {
CHECK(rx.btp_dest_port == 2002, "GBC port %d", rx.btp_dest_port);
CHECK(rx.has_geo_area, "GBC area missing");
CHECK(!rx.signed_unverified && !rx.truncated, "GBC flags signed=%d truncated=%d",
rx.signed_unverified, rx.truncated);
CHECK(rx.geo_area_lat_tenmicrodeg == areas[i].lat, "area lat %ld, expected %ld",
(long)rx.geo_area_lat_tenmicrodeg, (long)areas[i].lat);
CHECK(rx.geo_area_lon_tenmicrodeg == areas[i].lon, "area lon %ld, expected %ld",
(long)rx.geo_area_lon_tenmicrodeg, (long)areas[i].lon);
CHECK(rx.geo_area_distance_a_m == areas[i].dist_a, "DistanceA %d, expected %d",
rx.geo_area_distance_a_m, areas[i].dist_a);
CHECK(rx.payload == g + GBC_PAYLOAD_OFFSET && rx.payload_len == DENM_STUB_LEN &&
memcmp(rx.payload, k_denm_stub, (size_t)DENM_STUB_LEN) == 0,
"GBC payload offset %d length %d", (int)(rx.payload - g), rx.payload_len);
}
tu_pcap_add(f, len);
}
}
// ---- What gets rejected, and where the payload ends -----------------------------------------
// One-byte changes to a good CAM frame: what must be dropped, and what must still pass.
static void test_rejections(void)
{
const gn_lpv_t lpv = test_lpv();
uint8_t good[FRAME_BUF_LEN];
const int len = build(k_cam, CAM_LEN, &lpv, 2001, false, good, sizeof good);
// Offsets into that non-QoS frame: 802.11 0, LLC/SNAP 24, Basic 32, Common 36, SHB 44, BTP-B 72.
static const struct {
const char *what;
int offset;
uint8_t value;
bool accept;
uint16_t port;
} cases[] = {
{"management frame (beacon)", 0, 0x80, false, 0},
{"WDS (ToDS and FromDS)", 1, 0x03, false, 0},
{"LLC DSAP not 0xAA", 24, 0xAB, false, 0},
{"EtherType not 0x8947", 30, 0x08, false, 0},
{"Basic NextHeader 2 with no security envelope", 32, 0x12, false, 0},
{"Basic NextHeader 0 (any)", 32, 0x10, false, 0},
{"Common NextHeader 1 (BTP-A)", 36, 0x10, false, 0},
{"Beacon (HeaderType 1)", 37, 0x10, false, 0},
{"GeoUnicast (HeaderType 2)", 37, 0x20, false, 0},
{"multi-hop TSB (HeaderType 5, subtype 1)", 37, 0x51, false, 0},
{"BTP port 2003 (MAPEM, not accepted yet)", 73, 0xD3, false, 0},
{"BTP port 2018 (VAM, not accepted yet)", 73, 0xE2, false, 0},
{"BTP port 2002 (DENM)", 73, 0xD2, true, 2002},
{"BTP port 2004 (SPATEM)", 73, 0xD4, true, 2004},
};
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) {
tu_set_context("mutation: %s", cases[i].what);
uint8_t f[FRAME_BUF_LEN];
memcpy(f, good, (size_t)len);
f[cases[i].offset] = cases[i].value;
gn_rx_t rx;
const bool ok = gn_unwrap_its(tu_guarded(f, len), len, &rx);
CHECK(ok == cases[i].accept, "%s: %s", cases[i].what, ok ? "accepted" : "rejected");
if (ok && cases[i].accept) {
CHECK(rx.btp_dest_port == cases[i].port, "%s: port %d", cases[i].what, rx.btp_dest_port);
}
}
}
// The Common Header's payload length, not the end of the frame, decides where the message stops.
static void test_payload_length(void)
{
const gn_lpv_t lpv = test_lpv();
uint8_t good[FRAME_BUF_LEN];
const int len = build(k_cam, CAM_LEN, &lpv, 2001, false, good, sizeof good);
const int pl = MAC_HDR + LLC_SNAP + GN_BASIC + GN_COMMON_PAYLOAD_LEN_FIELD;
static const struct { const char *what; uint16_t value; bool accept; bool truncated; int payload_len; }
cases[] = {
{"one byte less than sent", BTP_B + CAM_LEN - 1, true, false, CAM_LEN - 1},
{"BTP-B header only, no payload", BTP_B, false, false, 0},
{"shorter than the BTP-B header", BTP_B - 1, false, false, 0},
{"zero", 0, false, false, 0},
{"far more than arrived", 0xFFFF, true, true, CAM_LEN},
};
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) {
tu_set_context("Common payload length: %s", cases[i].what);
uint8_t f[FRAME_BUF_LEN];
memcpy(f, good, (size_t)len);
f[pl] = (uint8_t)(cases[i].value >> 8);
f[pl + 1] = (uint8_t)cases[i].value;
gn_rx_t rx;
const uint8_t *g = tu_guarded(f, len);
const bool ok = gn_unwrap_its(g, len, &rx);
CHECK(ok == cases[i].accept, "%s: %s", cases[i].what, ok ? "accepted" : "rejected");
if (ok && cases[i].accept) {
CHECK(rx.truncated == cases[i].truncated && rx.payload == g + SHB_PAYLOAD_OFFSET &&
rx.payload_len == cases[i].payload_len,
"%s: truncated=%d payload_len=%d", cases[i].what, rx.truncated, rx.payload_len);
}
}
}
// The 8 bytes the chip's promiscuous RX appends to every frame must not reach the phone.
static void test_trailer(void)
{
uint8_t f[FRAME_BUF_LEN];
for (int qos = 0; qos <= 1; qos++) {
tu_set_context("RX trailer after an SHB frame, qos=%d", qos);
const gn_lpv_t lpv = test_lpv();
const int len = build(k_cam, CAM_LEN, &lpv, 2001, qos, f, sizeof f);
memcpy(f + len, k_rx_trailer, sizeof k_rx_trailer);
gn_rx_t rx;
const bool ok = gn_unwrap_its(tu_guarded(f, len + 8), len + 8, &rx);
CHECK(ok && !rx.truncated && rx.payload_len == CAM_LEN &&
memcmp(rx.payload, k_cam, (size_t)CAM_LEN) == 0,
"qos=%d: payload_len %d with the trailer, expected %d", qos, ok ? rx.payload_len : -1,
CAM_LEN);
}
tu_set_context("RX trailer after a GBC frame");
const int len = build_gbc(0, 535540000, 100220000, 250, f);
memcpy(f + len, k_rx_trailer, sizeof k_rx_trailer);
gn_rx_t rx;
const bool ok = gn_unwrap_its(tu_guarded(f, len + 8), len + 8, &rx);
CHECK(ok && !rx.truncated && rx.payload_len == DENM_STUB_LEN,
"GBC: payload_len %d with the trailer, expected %d", ok ? rx.payload_len : -1, DENM_STUB_LEN);
}
// ---- Secured (TS 103 097) -------------------------------------------------------------------
// Stand-in for what follows the inner packet in a real signed frame: headerInfo, signer and
// signature, 94 bytes with a digest signer. Its content is never read.
#define SIG_STUB_LEN 94
static int put_coer_length(int len, uint8_t *out)
{
if (len < 0x80) {
out[0] = (uint8_t)len;
return 1;
}
if (len <= 0xFF) {
out[0] = 0x81;
out[1] = (uint8_t)len;
return 2;
}
out[0] = 0x82;
out[1] = (uint8_t)(len >> 8);
out[2] = (uint8_t)len;
return 3;
}
// A secured CAM frame shaped like the recorded ones: our own SHB packet with the Basic Header's
// NextHeader set to 2 and everything after the Basic Header wrapped as `prefix` + COER length +
// inner packet, then the signature stand-in. Sets where the ITS payload starts and ends.
static int build_secured(const uint8_t *prefix, int prefix_len, const uint8_t *payload,
int payload_len, uint8_t *f, int *payload_start, int *payload_end)
{
const gn_lpv_t lpv = test_lpv();
uint8_t plain[FRAME_BUF_LEN];
const int plain_len = build(payload, payload_len, &lpv, 2001, false, plain, sizeof plain);
const int basic = MAC_HDR + LLC_SNAP;
const int inner = basic + GN_BASIC; // the Common Header onward
const int inner_len = plain_len - inner;
int n = inner;
memcpy(f, plain, (size_t)inner);
f[basic] = (uint8_t)((f[basic] & 0xF0) | 2); // Basic Header NextHeader: secured
memcpy(f + n, prefix, (size_t)prefix_len);
n += prefix_len;
n += put_coer_length(inner_len, f + n);
memcpy(f + n, plain + inner, (size_t)inner_len);
*payload_start = n + GN_COMMON + SHB_EXT + BTP_B;
n += inner_len;
*payload_end = n;
for (int i = 0; i < SIG_STUB_LEN; i++) {
f[n++] = (uint8_t)(0xA5 ^ i);
}
return n;
}
static const uint8_t k_signed_prefix[] = {0x03, 0x81, 0x00, 0x40, 0x03, 0x80};
static const uint8_t k_unsecured_prefix[] = {0x03, 0x80};
static void test_secured(void)
{
static uint8_t big[300];
for (int i = 0; i < (int)sizeof big; i++) {
big[i] = (uint8_t)(i * 13 + 1);
}
static const struct { const char *what; bool is_signed; int payload_len; } cases[] = {
{"signed, short length form", true, CAM_LEN}, // inner packet 83 bytes
{"signed, 1-byte long length form", true, 100}, // 140
{"signed, 2-byte long length form", true, 300}, // 340
{"top-level unsecuredData", false, CAM_LEN},
};
for (size_t c = 0; c < sizeof cases / sizeof cases[0]; c++) {
tu_set_context("secured: %s", cases[c].what);
const uint8_t *payload = cases[c].payload_len == CAM_LEN ? k_cam : big;
uint8_t f[FRAME_BUF_LEN];
int start, end;
const int len = cases[c].is_signed
? build_secured(k_signed_prefix, (int)sizeof k_signed_prefix, payload,
cases[c].payload_len, f, &start, &end)
: build_secured(k_unsecured_prefix, (int)sizeof k_unsecured_prefix, payload,
cases[c].payload_len, f, &start, &end);
gn_rx_t rx;
const uint8_t *g = tu_guarded(f, len);
const bool ok = gn_unwrap_its(g, len, &rx);
CHECK(ok, "%s: rejected", cases[c].what);
if (ok) {
CHECK(rx.signed_unverified == cases[c].is_signed, "%s: signed_unverified %d",
cases[c].what, rx.signed_unverified);
CHECK(!rx.truncated && rx.btp_dest_port == 2001, "%s: truncated %d port %d",
cases[c].what, rx.truncated, rx.btp_dest_port);
CHECK(rx.payload == g + start && rx.payload_len == cases[c].payload_len &&
memcmp(rx.payload, payload, (size_t)cases[c].payload_len) == 0,
"%s: payload offset %d length %d, expected %d / %d", cases[c].what,
(int)(rx.payload - g), rx.payload_len, start, cases[c].payload_len);
}
tu_pcap_add(f, len);
sweep(cases[c].what, f, len, start, end);
}
// One-byte changes to the signed, short-form frame. `env` is where the envelope starts.
uint8_t good[FRAME_BUF_LEN];
int start, end;
const int len = build_secured(k_signed_prefix, (int)sizeof k_signed_prefix, k_cam, CAM_LEN,
good, &start, &end);
const int env = MAC_HDR + LLC_SNAP + GN_BASIC;
const int inner = env + (int)sizeof k_signed_prefix + 1; // + one length byte
static const struct { const char *what; int offset; uint8_t value; } bad[] = {
{"legacy envelope, protocolVersion 2", 0, 0x02},
{"encryptedData", 1, 0x82},
{"hashId not a one-byte value", 2, 0x80},
{"no data, only extDataHash (preamble 0x20)", 3, 0x20},
{"inner protocolVersion 2", 4, 0x02},
{"nested signedData", 5, 0x81},
{"length form with no length bytes (0x80)", 6, 0x80},
{"length form with 3 length bytes (0x83)", 6, 0x83},
{"envelope too short for the Common Header", 6, 0x05},
};
for (size_t i = 0; i < sizeof bad / sizeof bad[0]; i++) {
tu_set_context("secured mutation: %s", bad[i].what);
uint8_t f[FRAME_BUF_LEN];
memcpy(f, good, (size_t)len);
f[env + bad[i].offset] = bad[i].value;
gn_rx_t rx;
CHECK(!gn_unwrap_its(tu_guarded(f, len), len, &rx), "%s: accepted", bad[i].what);
}
tu_set_context("secured mutation: inner packet claims more than its envelope holds");
uint8_t f[FRAME_BUF_LEN];
memcpy(f, good, (size_t)len);
f[inner + GN_COMMON_PAYLOAD_LEN_FIELD + 1]++; // Common Header payload length, low byte
gn_rx_t rx;
CHECK(!gn_unwrap_its(tu_guarded(f, len), len, &rx), "payload overrunning its envelope: accepted");
}
static void test_truncation(void)
{
const gn_lpv_t lpv = test_lpv();
uint8_t f[FRAME_BUF_LEN];
for (int qos = 0; qos <= 1; qos++) {
const int len = build(k_cam, CAM_LEN, &lpv, 2001, qos, f, sizeof f);
const int start = MAC_HDR + (qos ? QOS_CTRL : 0) + LLC_SNAP + GN_BASIC + GN_COMMON + SHB_EXT + BTP_B;
sweep(qos ? "SHB QoS" : "SHB", f, len, start, len);
}
const int len = build_gbc(0, 535540000, 100220000, 250, f);
sweep("GBC", f, len, GBC_PAYLOAD_OFFSET, len);
}
int main(int argc, char **argv)
{
tu_install_crash_handler();
tu_guard_init();
if (argc > 1 && !tu_pcap_open(argv[1])) {
fprintf(stderr, "cannot write %s\n", argv[1]);
return 2;
}
test_shb_layout(false);
test_shb_layout(true);
test_pv_encoding();
test_bounds();
test_max_payload();
test_gbc_rx();
test_rejections();
test_payload_length();
test_trailer();
test_secured();
test_truncation();
tu_pcap_close();
printf("test_chain: %d checks, %d failed\n", s_checks, s_failures);
return s_failures ? 1 : 0;
}