Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1079,21 +1079,6 @@ test-wolfguard-loopback-ubsan: CFLAGS+=-fsanitize=undefined -fno-sanitize-recove
test-wolfguard-loopback-ubsan: LDFLAGS+=-fsanitize=undefined $(UNIT_LIBS)
test-wolfguard-loopback-ubsan: clean-test-wolfguard-loopback build/test/test-wolfguard-loopback

# wolfGuard benchmark
bench-wolfguard: build/test/bench-wolfguard

build/test/bench-wolfguard: src/test/bench_wolfguard.c
@mkdir -p build/test/
@echo "[CC] bench_wolfguard.c"
@$(CC) $(CFLAGS) -O2 $(WOLFGUARD_CFLAGS) \
-c src/test/bench_wolfguard.c -o build/test/bench_wolfguard.o
@echo "[LD] $@"
@$(CC) build/test/bench_wolfguard.o -o $@ \
$(LDFLAGS) -lwolfssl

clean-bench-wolfguard:
@rm -f build/test/bench-wolfguard build/test/bench_wolfguard.o

# wolfGuard interop test (wolfIP <-> kernel wolfGuard via TUN)
test-wolfguard-interop: build/test/test-wolfguard-interop
Comment thread
gasbytes marked this conversation as resolved.

Expand All @@ -1117,7 +1102,6 @@ clean-test-wolfguard-interop:
unit-wolfguard unit-wolfguard-asan unit-wolfguard-ubsan clean-unit-wolfguard \
test-wolfguard-loopback test-wolfguard-loopback-asan test-wolfguard-loopback-ubsan \
clean-test-wolfguard-loopback \
bench-wolfguard clean-bench-wolfguard \
test-wolfguard-interop clean-test-wolfguard-interop

cppcheck:
Expand Down
21 changes: 13 additions & 8 deletions docs/wolfguard_howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ switch. The pre-wired Makefile targets build and exercise it:
```sh
make unit-wolfguard # unit tests
make test-wolfguard-loopback # two-stack loopback integration test
make bench-wolfguard # micro-benchmarks
make test-wolfguard-interop # interop binary (driven by the script in §8)
```

Expand Down Expand Up @@ -189,10 +188,10 @@ void wolfguard_destroy(struct wg_device *dev);
```

- `wolfguard_init` zeroes `dev`, initialises its RNG, configures `wg_if_idx` as
the `wg0` L3 interface (and sets its MTU to `LINK_MTU - 60` to leave room for
the outer IP/UDP/WireGuard overhead), opens the outer UDP socket, binds it to
`listen_port`, and registers the RX callback. Returns `0` on success, `-1` on
failure.
the `wg0` L3 interface (and sets its MTU to `WG_IF_MTU`, derived so that a
full-size inner packet still fits in one outer UDP datagram after padding and
encapsulation), opens the outer UDP socket, binds it to `listen_port`, and
registers the RX callback. Returns `0` on success, `-1` on failure.
- `wolfguard_set_private_key` stores the 32-byte private key and derives the
device's 65-byte public key (`wg_pubkey_from_private`). It must be called
before adding peers; calling it again rotates the identity and drops live
Expand Down Expand Up @@ -407,6 +406,12 @@ wolfIP stacks.
- **It will not talk to stock WireGuard.** Expected — wolfGuard uses the FIPS
suite (P-256 / AES-GCM / SHA-256) and is interoperable only with other
wolfGuard peers (kernel module or another wolfIP instance).
- **Inner MTU surprises.** `wolfguard_init` sets the `wg0` MTU to
`LINK_MTU - 60` to reserve the outer IP/UDP/WireGuard overhead; size inner
payloads accordingly.
- **Inner MTU surprises.** `wolfguard_init` sets the `wg0` MTU to `WG_IF_MTU`
(1454 with the usual `LINK_MTU` of 1536), leaving a 1440-byte inner IP budget
and 1412 bytes of UDP payload; size inner payloads accordingly. The
reservation is not a flat header sum: the plaintext is padded up to a 16-byte
multiple before encryption, the outer IP payload is capped at 1500 rather
than `LINK_MTU`, and wolfIP subtracts a 14-byte link header from every
interface MTU including this one. Raising the `wg0` MTU past `WG_IF_MTU`
black-holes the largest packets rather than failing loudly, since the outer
`sendto()` rejects them after wolfIP has already accepted them.
305 changes: 305 additions & 0 deletions src/test/test_wolfguard_loopback.c
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,297 @@ START_TEST(test_multi_peer)
}
END_TEST

/*
* MTU encapsulation budget
*
* Structural companion to the sweep below, this test doesn't send/receive any
* traffic, it just checks the arithmetic that
* wolfguard_init() has to get right.
*/
START_TEST(test_mtu_encap_fits_transport)
{
uint64_t now;
int wg0_ip_mtu; /* largest inner IP packet wolfIP accepts on wg0 */
int outer_udp_max; /* largest outer UDP payload the phys iface carries */
int wg_overhead; /* WG data header + auth tag */
int carry_cap; /* largest inner IP packet that survives encapsulation */

setup_loopback_stacks(&now);

wg_overhead = (int)(sizeof(struct wg_msg_data) + WG_AUTHTAG_LEN);
wg0_ip_mtu = (int)wolfIP_ip_mtu(&stack_a, TEST_WG_IF);
outer_udp_max = (int)wolfIP_ip_mtu(&stack_a, TEST_PHYS_IF)
- (IP_HEADER_LEN + UDP_HEADER_LEN);
/* Padding rounds the inner length up to a 16-byte multiple, so the largest
* inner packet that still fits is the budget rounded *down* to one. */
carry_cap = outer_udp_max - wg_overhead;
carry_cap -= carry_cap % 16;

ck_assert_int_gt(carry_cap, 0);
ck_assert_msg(wg0_ip_mtu <= carry_cap,
"wg0 advertises a %d-byte IP MTU but only %d bytes of inner "
"IP survive encapsulation: pad16(%d) + %d = %d, over the "
"%d-byte outer UDP budget. Inner packets of %d..%d bytes "
"are accepted and then silently dropped.",
wg0_ip_mtu, carry_cap,
wg0_ip_mtu, wg_overhead,
(int)wg_pad_len((size_t)wg0_ip_mtu) + wg_overhead,
outer_udp_max,
carry_cap + 1, wg0_ip_mtu);

teardown_stacks();
}
END_TEST

/*
* MTU boundary sweep
*
* Sweeps the inner UDP payload across the wg0 MTU in 1-byte steps.
*
* The contract under test is that wg0 must not advertise capacity the outer
* transport cannot carry: every payload wolfIP accepts on wg0 has to arrive
* intact at the far end, and nothing beyond that may arrive at all. Both
* halves matter. A one-sided "never exceeds the MTU" check passes happily
* while the tunnel black-holes the top of its own advertised range, which is
* precisely the failure this test exists to catch, so the boundary is pinned
* from both directions.
*
* Nothing here is hardcoded to a particular LINK_MTU: the accept cap is read
* back from wolfIP and the carry cap is recomputed from the wire format, so
* the test tracks whatever MTU wolfguard_init() actually installs.
*/
START_TEST(test_mtu_boundary_sweep)
{
uint64_t now;
int app_sock_a, app_sock_b;
struct wolfIP_sockaddr_in bind_addr, dst_addr;
uint8_t sndbuf[LINK_MTU];
const int anchor = 1000; /* known-deliverable (cf. flood) */
/* Read back from the live stack once it is up, never hardcoded. */
int accept_cap; /* largest app payload wolfIP accepts on wg0 */
int top; /* sweep ceiling, just past the advertised MTU */
int max_delivered = -1;
int anchor_delivered = 0;
int first_gap = -1; /* first accepted-but-lost size */
int over_delivered = -1; /* first past-cap size delivered */
int p, i, ret;

setup_loopback_stacks(&now);

accept_cap = (int)wolfIP_ip_mtu(&stack_a, TEST_WG_IF)
- (IP_HEADER_LEN + UDP_HEADER_LEN);
top = accept_cap + 16;
ck_assert_int_gt(accept_cap, anchor);
ck_assert_int_lt(top, (int)sizeof(sndbuf));

/* B listens on 7777, A binds a source port on 9999 */
app_sock_b = wolfIP_sock_socket(&stack_b, AF_INET, SOCK_DGRAM, 0);
ck_assert_int_ge(app_sock_b, 0);
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_port = ee16(7777);
bind_addr.sin_addr.s_addr = ee32(MAKE_IP4(10,0,0,2));
ck_assert_int_ge(wolfIP_sock_bind(&stack_b, app_sock_b,
(struct wolfIP_sockaddr *)&bind_addr, sizeof(bind_addr)), 0);
wolfIP_register_callback(&stack_b, app_sock_b, app_udp_callback, &stack_b);

app_sock_a = wolfIP_sock_socket(&stack_a, AF_INET, SOCK_DGRAM, 0);
ck_assert_int_ge(app_sock_a, 0);
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_port = ee16(9999);
bind_addr.sin_addr.s_addr = ee32(MAKE_IP4(10,0,0,1));
ck_assert_int_ge(wolfIP_sock_bind(&stack_a, app_sock_a,
(struct wolfIP_sockaddr *)&bind_addr, sizeof(bind_addr)), 0);

memset(&dst_addr, 0, sizeof(dst_addr));
dst_addr.sin_family = AF_INET;
dst_addr.sin_port = ee16(7777);
dst_addr.sin_addr.s_addr = ee32(MAKE_IP4(10,0,0,2));

/* Bring the session up with a small packet so the sweep tests
* the pure data-plane MTU path, not a handshake+size interaction. */
for (i = 0; i < 64; i++)
sndbuf[i] = (uint8_t)(i & 0xff);
ret = wolfIP_sock_sendto(&stack_a, app_sock_a, sndbuf, 64, 0,
(const struct wolfIP_sockaddr *)&dst_addr,
sizeof(dst_addr));
ck_assert_int_ge(ret, 0);
pump_stacks(&now, 200, 10);
ck_assert_int_gt(app_recv_count, 0);
ck_assert_ptr_nonnull(wg_dev_a.peers[0].keypairs.current);

/* Sweep the inner payload up to just past the tunnel MTU, one byte at a
* time. Timers are frozen (step_ms = 0) so a single session persists, a
* live clock would trip the spec's stale-receive rekey, since B is a pure
* sink that never replies. */
for (p = anchor; p <= top; p++) {
for (i = 0; i < p; i++)
sndbuf[i] = (uint8_t)((i * 31 + p) & 0xff);

app_recv_count = 0;
app_recv_len = 0;

ret = wolfIP_sock_sendto(&stack_a, app_sock_a, sndbuf, p, 0,
(const struct wolfIP_sockaddr *)&dst_addr,
sizeof(dst_addr));
/* wolfIP does not fragment, so the accept/reject split has to land
* exactly on the wg0 MTU. */
if (p <= accept_cap)
ck_assert_msg(ret >= 0,
"sendto rejected a %d-byte payload, at or below the "
"%d-byte wg0 cap", p, accept_cap);
else
ck_assert_msg(ret < 0,
"sendto accepted a %d-byte payload, above the "
"%d-byte wg0 cap", p, accept_cap);

pump_stacks(&now, 20, 0);

if (app_recv_count > 0) {
/* Any delivered packet must be intact: exact length and bytes.
* This is what catches padding / truncation / buffer bugs. */
ck_assert_int_eq(app_recv_len, p);
for (i = 0; i < p; i++)
ck_assert_uint_eq(app_recv_buf[i],
(uint8_t)((i * 31 + p) & 0xff));
if (p == anchor)
anchor_delivered = 1;
if (p > accept_cap && over_delivered < 0)
over_delivered = p;
max_delivered = p;
}
else if (p <= accept_cap && first_gap < 0) {
first_gap = p;
}
}

/* Pin the boundary from both sides.
*
* Below the cap: everything wg0 accepted has to arrive. A gap means the
* interface is advertising an MTU its own transport cannot carry, and the
* packets in that band vanish with no error and no ICMP, the black hole a
* one-sided "never exceeds the MTU" check would sail straight past.
*
* Above the cap: nothing may arrive. wolfIP has no fragmentation, so an
* over-MTU delivery would mean truncation or a buffer overrun. */
ck_assert_int_eq(anchor_delivered, 1);
ck_assert_msg(first_gap < 0,
"%d-byte payload accepted by wg0 (cap %d) but never "
"delivered: the top %d bytes of the advertised MTU are a "
"black hole", first_gap, accept_cap,
accept_cap - first_gap + 1);
ck_assert_msg(over_delivered < 0,
"%d-byte payload delivered above the %d-byte wg0 cap",
over_delivered, accept_cap);
ck_assert_int_eq(max_delivered, accept_cap);

wolfIP_sock_close(&stack_a, app_sock_a);
wolfIP_sock_close(&stack_b, app_sock_b);
teardown_stacks();
}
END_TEST

/*
* Sustained flood
* This test floods 256 packets to stress the transport path. Each packet
* is uniquely tagged, exercising the replay-counter sliding window implemented
* by wg_counter_validate().
* */
START_TEST(test_sustained_flood)
{
uint64_t now;
int app_sock_a, app_sock_b;
struct wolfIP_sockaddr_in bind_addr, dst_addr;
uint8_t sndbuf[1024];
const int N = 256; /* crosses several replay-bitmap words */
const int payload_len = 1000;
int i, j, ret, delivered = 0;

setup_loopback_stacks(&now);

app_sock_b = wolfIP_sock_socket(&stack_b, AF_INET, SOCK_DGRAM, 0);
ck_assert_int_ge(app_sock_b, 0);
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_port = ee16(7777);
bind_addr.sin_addr.s_addr = ee32(MAKE_IP4(10,0,0,2));
ck_assert_int_ge(wolfIP_sock_bind(&stack_b, app_sock_b,
(struct wolfIP_sockaddr *)&bind_addr, sizeof(bind_addr)), 0);
wolfIP_register_callback(&stack_b, app_sock_b, app_udp_callback, &stack_b);

app_sock_a = wolfIP_sock_socket(&stack_a, AF_INET, SOCK_DGRAM, 0);
ck_assert_int_ge(app_sock_a, 0);
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_port = ee16(9999);
bind_addr.sin_addr.s_addr = ee32(MAKE_IP4(10,0,0,1));
ck_assert_int_ge(wolfIP_sock_bind(&stack_a, app_sock_a,
(struct wolfIP_sockaddr *)&bind_addr, sizeof(bind_addr)), 0);

memset(&dst_addr, 0, sizeof(dst_addr));
dst_addr.sin_family = AF_INET;
dst_addr.sin_port = ee16(7777);
dst_addr.sin_addr.s_addr = ee32(MAKE_IP4(10,0,0,2));

/* Establish the session before flooding. */
for (j = 0; j < payload_len; j++)
sndbuf[j] = (uint8_t)(j & 0xff);
ret = wolfIP_sock_sendto(&stack_a, app_sock_a, sndbuf, payload_len, 0,
(const struct wolfIP_sockaddr *)&dst_addr,
sizeof(dst_addr));
ck_assert_int_ge(ret, 0);
pump_stacks(&now, 200, 10);
ck_assert_int_gt(app_recv_count, 0);
ck_assert_ptr_nonnull(wg_dev_a.peers[0].keypairs.current);

/* This is the flood logic, where each
* packet carries its sequence number in the first two bytes so
* delivery, ordering, and integrity are checked per packet. */
for (i = 0; i < N; i++) {
for (j = 0; j < payload_len; j++)
sndbuf[j] = (uint8_t)((j + i) & 0xff);
sndbuf[0] = (uint8_t)(i & 0xff);
sndbuf[1] = (uint8_t)((i >> 8) & 0xff);

app_recv_count = 0;
app_recv_len = 0;

ret = wolfIP_sock_sendto(&stack_a, app_sock_a, sndbuf, payload_len, 0,
(const struct wolfIP_sockaddr *)&dst_addr,
sizeof(dst_addr));
ck_assert_int_ge(ret, 0);
/* Freeze timers (step_ms = 0): keeps a single session for the whole
* flood so the replay counter advances monotonically. With a live
* clock the spec's stale-receive rekey would fire (B never replies),
* resetting the counter mid-flood. */
pump_stacks(&now, 16, 0);

if (app_recv_count > 0) {
ck_assert_int_eq(app_recv_len, payload_len);
ck_assert_uint_eq(app_recv_buf[0], (uint8_t)(i & 0xff));
ck_assert_uint_eq(app_recv_buf[1], (uint8_t)((i >> 8) & 0xff));
delivered++;
}
}

/* Nearly all delivered (small slack for pump-timing stragglers). */
ck_assert_int_ge(delivered, N - 4);

/* Receiver's replay window advanced across the whole flood, crossing many
* 32-bit bitmap words in wg_counter_validate without false rejections. */
ck_assert_ptr_nonnull(wg_dev_b.peers[0].keypairs.current);
ck_assert_uint_ge(wg_dev_b.peers[0].keypairs.current->receiving_counter_max,
(uint64_t)(N - 4));
ck_assert_uint_gt(wg_dev_a.peers[0].tx_bytes,
(uint64_t)(N - 4) * (uint64_t)payload_len);

wolfIP_sock_close(&stack_a, app_sock_a);
wolfIP_sock_close(&stack_b, app_sock_b);
teardown_stacks();
}
END_TEST

/*
* Test suite assembly
* */
Expand Down Expand Up @@ -1027,6 +1318,20 @@ static Suite *wolfguard_integration_suite(void)
tcase_add_test(tc, test_multi_peer);
suite_add_tcase(s, tc);

/* MTU boundary: encapsulation budget, then a 1-byte payload sweep
* across the wg0 MTU */
tc = tcase_create("mtu_boundary");
tcase_set_timeout(tc, 120);
tcase_add_test(tc, test_mtu_encap_fits_transport);
tcase_add_test(tc, test_mtu_boundary_sweep);
suite_add_tcase(s, tc);

/* Sustained flood: replay-window advance under volume */
tc = tcase_create("flood");
tcase_set_timeout(tc, 120);
tcase_add_test(tc, test_sustained_flood);
suite_add_tcase(s, tc);

return s;
}

Expand Down
Loading
Loading