731 lines
33 KiB
C
731 lines
33 KiB
C
// 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;
|
||
|
|
}
|