diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fc6a2d..bb5f394 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,13 @@ jobs: - uses: mlugg/setup-zig@v2 with: - version: 0.15.1 + version: 0.16.0 + + - name: Build examples and benchmark + run: zig build - name: Run tests run: zig build test + + - name: Build optimized artifacts + run: zig build -Doptimize=ReleaseFast diff --git a/CHANGELOG.md b/CHANGELOG.md index a064793..bedb600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased +- Require Zig 0.16.0; replace public peer addresses with `std.Io.net.IpAddress` +- Retain the Linux io_uring backend with native-address conversion at syscall boundaries +- Use monotonic clocks for protocol deadlines and timing +- Update allocator examples, benchmark argument handling, OpenSSL process APIs and CI +- Vectorize full-block CCM CBC-MAC XOR; add independent boundary test vectors +- Add `--payload-only` benchmark filtering and document migration performance measurements + ## [0.7.2] - 2026-03-21 - Fix: Block1 reassembled body now reaches handler (#81) - Fix: Observe notifications use correct per-observer token (#82) diff --git a/README.md b/README.md index c5f68d3..974ee63 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ const std = @import("std"); const coap = @import("coap"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); var server = try coap.Server.init(gpa.allocator(), .{}, echo); @@ -66,7 +66,7 @@ const router = coap.Router(.{ }); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; var server = try coap.Server.init(gpa.allocator(), .{}, router.handler()); defer server.deinit(); try server.run(); @@ -99,7 +99,7 @@ const std = @import("std"); const coap = @import("coap"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); @@ -192,7 +192,7 @@ The request provides convenience accessors and the underlying packet: - `deferResponse()` — defer the response for async handling (separate response) - `observeResource(rid)` / `removeObserver(rid)` — observe registration - `packet` — the full parsed CoAP packet for advanced use -- `peer_address` — source address of the client (`std.net.Address`) +- `peer_address` — source address of the client (`std.Io.net.IpAddress`) - `arena` — per-request arena allocator (resets after handler returns) ### Response @@ -892,12 +892,23 @@ zig build bench -Doptimize=ReleaseFast ``` Filter flags: `--plain-only`, `--dtls-only`, `--con-only`, `--non-only`, -`--single-only`, `--multi-only`, `--ipv6`. Use `--help` for all options. +`--single-only`, `--multi-only`, `--payload-only <0|100|1000>`, `--ipv6`. +Use `--help` for all options. For performance comparisons, run one scenario +per invocation to avoid outstanding NON traffic affecting the next scenario's +server-side counters. See [the Zig 0.16 migration measurements](docs/ZIG_016_PERFORMANCE.md). ## Requirements - Linux (io_uring support, kernel 5.13+ for multishot recvmsg) -- Zig 0.15.1+ +- Zig 0.16.0 (the currently supported toolchain) + +### Migrating from Zig 0.15 + +The public peer address type is now `std.Io.net.IpAddress`. Use +`IpAddress.parse(host, port)` instead of `std.net.Address.parseIp`, access +IPv4/IPv6 values through `.ip4`/`.ip6`, and use `.getPort()` for ports. +The server still uses preallocated Linux io_uring operations; the server/client +initialization signatures and handler interface are unchanged. ## Roadmap diff --git a/bench/client.zig b/bench/client.zig index 4e52f8c..cdd51b5 100644 --- a/bench/client.zig +++ b/bench/client.zig @@ -4,7 +4,8 @@ /// sizes) and prints a compact summary table. Forks embedded echo servers /// as needed, grouped by (thread_count, use_dtls) to minimize restarts. const std = @import("std"); -const posix = std.posix; +const platform = @import("coap").platform; +const posix = platform; const coapz = @import("coapz"); const coap = @import("coap"); @@ -24,6 +25,7 @@ const SuiteConfig = struct { embedded_server: bool = true, thread_count: u16 = 0, // 0 = nproc count_override: ?u32 = null, + payload_filter: ?u16 = null, filter_plain: bool = true, filter_dtls: bool = true, filter_con: bool = true, @@ -83,12 +85,12 @@ const scenario_templates = [_]Scenario{ // ── Main ─────────────────────────────────────────────────────────────── -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +pub fn main(init: std.process.Init.Minimal) !void { + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - const config = parse_args(); + const config = parse_args(init.args); const cpu_count: u16 = if (config.thread_count > 0) config.thread_count else @@ -97,6 +99,9 @@ pub fn main() !void { var scenarios: [scenario_templates.len]?Scenario = .{null} ** scenario_templates.len; var total: u16 = 0; for (scenario_templates, 0..) |tmpl, i| { + if (config.payload_filter) |size| { + if (tmpl.payload_size != size) continue; + } if (tmpl.use_dtls and !config.filter_dtls) continue; if (!tmpl.use_dtls and !config.filter_plain) continue; if (tmpl.use_confirmable and !config.filter_con) continue; @@ -133,7 +138,8 @@ pub fn main() !void { .{ config.host, config.port, - cpu_count, cpu_count, + cpu_count, + cpu_count, config.window_size, fmtInt(&warmup_buf, config.warmup_count), count_str, @@ -167,7 +173,7 @@ pub fn main() !void { kill_server(&server_pid); const psk: ?coap.Psk = if (s.use_dtls) bench_psk else null; server_pid = try fork_server(port, srv_tc, psk, counters, config.bind_address); - std.Thread.sleep(150 * std.time.ns_per_ms); + platform.sleep(150 * std.time.ns_per_ms); current_group = group; } } @@ -297,7 +303,7 @@ fn run_scenario_plain_non( for (threads) |t| t.join(); // Small drain delay so server processes remaining packets. - std.Thread.sleep(50 * std.time.ns_per_ms); + platform.sleep(50 * std.time.ns_per_ms); var total_errors: u64 = main_errors; for (send_errors[0..extra]) |e| total_errors += e; @@ -376,7 +382,7 @@ fn run_scenario_plain_con( allocator.free(worker_results); } - const start = std.time.nanoTimestamp(); + const start = platform.nanoTimestamp(); for (0..extra) |j| { worker_results[j] = .{}; @@ -392,7 +398,7 @@ fn run_scenario_plain_con( for (threads) |t| t.join(); - const elapsed_ns = std.time.nanoTimestamp() - start; + const elapsed_ns = platform.nanoTimestamp() - start; return aggregate_plain(allocator, worker_results, elapsed_ns); } @@ -484,15 +490,22 @@ fn run_scenario_dtls( for (0..extra) |j| { worker_results[j] = .{}; threads[j] = try std.Thread.spawn(.{}, dtls_worker, .{ - allocator, config.host, port, window, s.payload_size, - s.use_confirmable, per_thread, config.warmup_count, &worker_results[j], + allocator, config.host, port, window, s.payload_size, + s.use_confirmable, per_thread, config.warmup_count, &worker_results[j], }); } worker_results[extra] = .{}; dtls_worker( - allocator, config.host, port, window, s.payload_size, - s.use_confirmable, per_thread + remainder, config.warmup_count, &worker_results[extra], + allocator, + config.host, + port, + window, + s.payload_size, + s.use_confirmable, + per_thread + remainder, + config.warmup_count, + &worker_results[extra], ); for (threads) |t| t.join(); @@ -573,7 +586,7 @@ fn dtls_worker_con( var total_sent: u32 = 0; var in_flight: u16 = 0; - const bench_start = std.time.nanoTimestamp(); + const bench_start = platform.nanoTimestamp(); while (received + errors < count) { while (in_flight < window and total_sent < count) { @@ -582,7 +595,7 @@ fn dtls_worker_con( total_sent += 1; continue; }; - timestamps[handle] = std.time.nanoTimestamp(); + timestamps[handle] = platform.nanoTimestamp(); total_sent += 1; in_flight += 1; sent += 1; @@ -609,7 +622,7 @@ fn dtls_worker_con( const ts = timestamps[completion.handle]; if (ts > 0) { - const latency = std.time.nanoTimestamp() - ts; + const latency = platform.nanoTimestamp() - ts; if (latency_count < count) { latencies[latency_count] = @intCast(@min(latency, std.math.maxInt(i64))); latency_count += 1; @@ -618,7 +631,7 @@ fn dtls_worker_con( } } - out.bench_elapsed_ns = std.time.nanoTimestamp() - bench_start; + out.bench_elapsed_ns = platform.nanoTimestamp() - bench_start; out.sent = sent; out.errors = errors; out.latencies = latencies; @@ -634,7 +647,7 @@ fn dtls_worker_non( var sent: u64 = 0; var errors: u64 = 0; - const bench_start = std.time.nanoTimestamp(); + const bench_start = platform.nanoTimestamp(); for (0..count) |_| { client.cast(.get, &.{}, payload) catch { @@ -644,7 +657,7 @@ fn dtls_worker_non( sent += 1; } - out.bench_elapsed_ns = std.time.nanoTimestamp() - bench_start; + out.bench_elapsed_ns = platform.nanoTimestamp() - bench_start; out.sent = sent; out.errors = errors; } @@ -757,7 +770,7 @@ fn run_bench( send_buf[4] = id_hi; send_buf[5] = id_lo; - timestamps[msg_id % window] = std.time.nanoTimestamp(); + timestamps[msg_id % window] = platform.nanoTimestamp(); _ = posix.send(fd, send_buf[0..template.len], 0) catch { result.errors += 1; @@ -800,7 +813,7 @@ fn run_bench( const slot = token % window; const ts = timestamps[slot]; if (ts > 0) { - const latency = std.time.nanoTimestamp() - ts; + const latency = platform.nanoTimestamp() - ts; result.latency_sum_ns += latency; if (latency < result.latency_min_ns) result.latency_min_ns = latency; if (latency > result.latency_max_ns) result.latency_max_ns = latency; @@ -818,10 +831,10 @@ fn run_bench( // ── Helpers ──────────────────────────────────────────────────────────── fn make_client_socket(host: []const u8, port: u16) !posix.socket_t { - const dest = try std.net.Address.parseIp(host, port); - const fd = try posix.socket(dest.any.family, posix.SOCK.DGRAM, 0); + const dest = try platform.parseIp(host, port); + const fd = try posix.socket(platform.family(dest), posix.SOCK.DGRAM, 0); errdefer posix.close(fd); - try posix.connect(fd, &dest.any, dest.getOsSockLen()); + try posix.connect(fd, dest); const buf_size = std.mem.toBytes(@as(c_int, 4 * 1024 * 1024)); posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.SNDBUF, &buf_size) catch {}; @@ -855,7 +868,7 @@ const ServerCounters = struct { }; fn counting_handler(counters: *ServerCounters, request: coap.Request) ?coap.Response { - const now: i64 = @truncate(std.time.nanoTimestamp()); + const now: i64 = @truncate(platform.nanoTimestamp()); _ = @atomicRmw(u64, &counters.count.raw, .Add, 1, .monotonic); // Set first_ns only on the first request (CAS from 0). _ = @cmpxchgWeak(i64, &counters.first_ns.raw, 0, now, .monotonic, .monotonic); @@ -867,7 +880,7 @@ fn alloc_shared_counters() !*ServerCounters { const page = try posix.mmap( null, @max(@sizeOf(ServerCounters), 4096), - posix.PROT.READ | posix.PROT.WRITE, + .{ .READ = true, .WRITE = true }, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0, @@ -940,7 +953,7 @@ fn kill_server(pid: *?posix.pid_t) void { _ = posix.waitpid(p, 0); pid.* = null; // Let kernel fully reclaim io_uring ring memory before next server. - std.Thread.sleep(100 * std.time.ns_per_ms); + platform.sleep(100 * std.time.ns_per_ms); } } @@ -1091,7 +1104,11 @@ fn print_scenario_tree( // Leaf data starts at col 25, so pad 14 cols after the type name. // µ is 2 bytes / 1 col → each latency header needs {s:>9} for 8 display cols. std.debug.print(" " ++ D ++ "{s}{s}" ++ Z ++ " {s}" ++ D ++ " {s:>10} {s:>9} {s:>9} {s:>9} {s:>6}" ++ Z ++ "\n", .{ - tr_cont, ty_branch, ty.name, "req/s", "p50 µs", "p99 µs", "p99.9 µs", "errs", + tr_cont, ty_branch, ty.name, "req/s", + "p50 µs", + "p99 µs", + "p99.9 µs", + "errs", }); header_printed = true; } else { @@ -1148,33 +1165,32 @@ fn print_scenario_tree( // ── CLI ──────────────────────────────────────────────────────────────── fn print_usage() void { - std.debug.print( - "Usage: bench [options]\n\n" ++ - "Options:\n" ++ - " --host Server address (default: 127.0.0.1)\n" ++ - " --port Server port (default: 5683)\n" ++ - " --count Override request count per scenario\n" ++ - " --warmup Warmup requests (default: 1000)\n" ++ - " --window Client sliding window size (default: 256)\n" ++ - " --threads Thread count, 0 = nproc (default: 0)\n" ++ - " --no-server Don't fork embedded echo server\n" ++ - " --ipv6 Use IPv6 loopback (::1) instead of IPv4\n" ++ - "\n" ++ - "Filters:\n" ++ - " --plain-only Skip DTLS scenarios\n" ++ - " --dtls-only Skip plain UDP scenarios\n" ++ - " --con-only Skip NON scenarios\n" ++ - " --non-only Skip CON scenarios\n" ++ - " --single-only Skip multi-threaded scenarios\n" ++ - " --multi-only Skip single-threaded scenarios\n" ++ - "\n" ++ - " -h, --help Show this help\n" - , .{}); + std.debug.print("Usage: bench [options]\n\n" ++ + "Options:\n" ++ + " --host Server address (default: 127.0.0.1)\n" ++ + " --port Server port (default: 5683)\n" ++ + " --count Override request count per scenario\n" ++ + " --warmup Warmup requests (default: 1000)\n" ++ + " --window Client sliding window size (default: 256)\n" ++ + " --threads Thread count, 0 = nproc (default: 0)\n" ++ + " --no-server Don't fork embedded echo server\n" ++ + " --ipv6 Use IPv6 loopback (::1) instead of IPv4\n" ++ + "\n" ++ + "Filters:\n" ++ + " --payload-only Only payload size 0, 100, or 1000 bytes\n" ++ + " --plain-only Skip DTLS scenarios\n" ++ + " --dtls-only Skip plain UDP scenarios\n" ++ + " --con-only Skip NON scenarios\n" ++ + " --non-only Skip CON scenarios\n" ++ + " --single-only Skip multi-threaded scenarios\n" ++ + " --multi-only Skip single-threaded scenarios\n" ++ + "\n" ++ + " -h, --help Show this help\n", .{}); } -fn parse_args() SuiteConfig { +fn parse_args(process_args: std.process.Args) SuiteConfig { var config = SuiteConfig{}; - var args = std.process.args(); + var args = process_args.iterate(); _ = args.next(); while (args.next()) |arg| { @@ -1189,6 +1205,17 @@ fn parse_args() SuiteConfig { } else if (std.mem.eql(u8, arg, "--count")) { const val = args.next() orelse "100000"; config.count_override = std.fmt.parseInt(u32, val, 10) catch null; + } else if (std.mem.eql(u8, arg, "--payload-only")) { + const val = args.next() orelse { + std.debug.print("error: --payload-only requires 0, 100, or 1000\n", .{}); + std.process.exit(1); + }; + const size = std.fmt.parseInt(u16, val, 10) catch 65535; + if (size != 0 and size != 100 and size != 1000) { + std.debug.print("error: --payload-only requires 0, 100, or 1000\n", .{}); + std.process.exit(1); + } + config.payload_filter = size; } else if (std.mem.eql(u8, arg, "--warmup")) { const val = args.next() orelse "1000"; config.warmup_count = std.fmt.parseInt(u32, val, 10) catch 1_000; diff --git a/build.zig.zon b/build.zig.zon index 6ee9c5d..40a7e25 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .coap, .version = "0.7.2", - .minimum_zig_version = "0.15.1", + .minimum_zig_version = "0.16.0", .fingerprint = 0x67bf20367bf50f8b, .dependencies = .{ .coapz = .{ diff --git a/docs/ZIG_016_PERFORMANCE.md b/docs/ZIG_016_PERFORMANCE.md new file mode 100644 index 0000000..f551cba --- /dev/null +++ b/docs/ZIG_016_PERFORMANCE.md @@ -0,0 +1,101 @@ +# Zig 0.16 migration: performance checks + +## Method + +Baseline: revision `145100e` built with Zig 0.15.2. Migration: Zig 0.16.0. +Both use `ReleaseFast`, native CPU targeting, Linux 7.1.11, and the same +AMD Ryzen AI MAX+ 395 machine. No CPU governor or system tuning was changed. + +Each comparison uses five sequential, alternating baseline/migration runs, +500,000 requests and 10,000 warmup requests per invocation. Each invocation +runs exactly one scenario; both processes inherit CPU affinity 0–7. The +multi-threaded cases use four clients and four server threads. Windows are +256 (single-threaded) and 64 per client (multi-threaded). + +The baseline benchmark received only the payload-filter harness addition; +its library and crypto implementation were unchanged. Example command: + +```sh +zig build -Doptimize=ReleaseFast +taskset -c 0-7 ./zig-out/bin/bench --threads 4 --count 500000 \ + --warmup 10000 --dtls-only --non-only --single-only --payload-only 1000 +``` + +Change the transport, message type, threading and payload filters to repeat +the matrix below. Baseline and migration must run sequentially, not concurrently. + +The original multi-scenario suite produced large spurious NON differences: +outstanding traffic from a prior scenario can contaminate counters reset +before the next handshake/warmup. The added `--payload-only` filter allows +one scenario per process without changing the measured library paths. + +## Finding and repair + +Unchanged CCM code compiled with Zig 0.16 made 1000-byte single-threaded DTLS +NON throughput **7.5% lower** (278,820 → 258,000 req/s). An isolated encryption +microbenchmark also found a reproducible slowdown with no networking involved: +about 852 → 1007 ns per 1000-byte encryption. + +The plaintext CBC-MAC loop used a variable-sized byte XOR on every block. +Splitting full blocks from the partial tail and expressing the full-block XOR +as a fixed-width vector removed this slowdown. Three post-fix encryption +microbenchmark runs measured approximately 615 ns/op versus the old baseline's +852 ns/op (28% less time). The same fix measured about 90 versus 114 ns/op for +100-byte encryption. These are encryption-only measurements, not end-to-end +latency predictions. + +The optimized code is checked against independent Python cryptography AESCCM +ciphertext/tag digests at lengths 0, 15, 16, 17, 31, 32, 33, 100, 1000 and 1024, +as well as the existing authentication-failure and OpenSSL interoperability tests. + +## End-to-end medians + +Plain rows were collected before the CCM-only optimization (their code paths +are unaffected); DTLS rows were collected afterwards. Throughput is req/s. +Positive throughput changes are improvements. p99 is in microseconds. + +| Transport/type | Threads | Bytes | Baseline | Migration | Change | p99 baseline → migration | +|---|---:|---:|---:|---:|---:|---:| +| Plain NON | 1 | 0 | 914,578 | 911,607 | -0.3% | — | +| Plain NON | 1 | 100 | 865,007 | 861,352 | -0.4% | — | +| Plain NON | 1 | 1000 | 763,076 | 752,559 | -1.4% | — | +| Plain NON | 4 | 0 | 1,874,929 | 1,893,968 | +1.0% | — | +| Plain NON | 4 | 100 | 1,374,825 | 1,905,470 | +38.6% | — | +| Plain NON | 4 | 1000 | 1,439,007 | 1,367,405 | -5.0% | — | +| Plain CON | 1 | 0 | 762,894 | 770,913 | +1.1% | 649.8 → 592.2 | +| Plain CON | 1 | 100 | 755,935 | 772,439 | +2.2% | 638.0 → 590.1 | +| Plain CON | 1 | 1000 | 731,170 | 723,972 | -1.0% | 669.4 → 667.3 | +| Plain CON | 4 | 0 | 1,525,991 | 1,515,705 | -0.7% | 216.3 → 201.9 | +| Plain CON | 4 | 100 | 1,476,967 | 1,053,877 | -28.6% | 227.3 → 277.1 | +| Plain CON | 4 | 1000 | 1,438,915 | 1,377,972 | -4.2% | 221.2 → 272.2 | +| DTLS NON | 1 | 0 | 629,074 | 620,053 | -1.4% | — | +| DTLS NON | 1 | 100 | 538,213 | 546,878 | +1.6% | — | +| DTLS NON | 1 | 1000 | 286,838 | 329,586 | +14.9% | — | +| DTLS NON | 4 | 0 | 1,474,945 | 1,100,510 | -25.4% | — | +| DTLS NON | 4 | 100 | 974,870 | 1,291,080 | +32.4% | — | +| DTLS NON | 4 | 1000 | 679,838 | 774,786 | +14.0% | — | +| DTLS CON | 1 | 0 | 181,116 | 178,627 | -1.4% | 1795.9 → 2172.4 | +| DTLS CON | 1 | 100 | 173,409 | 170,722 | -1.5% | 1861.4 → 2230.0 | +| DTLS CON | 1 | 1000 | 136,089 | 141,732 | +4.1% | 2266.7 → 2504.6 | +| DTLS CON | 4 | 0 | 158,208 | 153,749 | -2.8% | 2531.0 → 2709.7 | +| DTLS CON | 4 | 100 | 165,328 | 154,986 | -6.3% | 2269.8 → 2645.8 | +| DTLS CON | 4 | 1000 | 183,118 | 182,130 | -0.5% | 1880.5 → 2220.0 | + +## Remaining risks and limitations + +This is **not a regression-free result**. Single-threaded DTLS CON p99 remains +about 10–21% higher. The four-thread CON cases above also contain throughput +regression candidates, including Plain 100B (-28.6%) and DTLS 100B (-6.3%). + +Multi-threaded samples are noisy: SO_REUSEPORT hashes the clients' ephemeral +source ports, so four clients do not necessarily use all four server workers +evenly. For example, Plain CON 100B migration throughput ranged from 988,952 +to 1,502,454 req/s; DTLS NON 0B ranged from 969,653 to 1,590,372. These ranges +make a claim of either broad improvement or broad regression unjustified. +A follow-up needs controlled worker placement/client flows and tail-latency +profiling before declaring performance parity. + +All sampled runs reported zero benchmark request errors. NON is +fire-and-forget; zero errors does **not** establish zero UDP packet loss. +The NON metric uses server counters rather than reliable delivery accounting. +Measurements are loopback-specific and do not predict real-network throughput. diff --git a/examples/client_example.zig b/examples/client_example.zig index 5ba51ec..3fef5a9 100644 --- a/examples/client_example.zig +++ b/examples/client_example.zig @@ -2,7 +2,7 @@ const std = @import("std"); const coap = @import("coap"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); diff --git a/examples/echo.zig b/examples/echo.zig index 45225bc..362afc0 100644 --- a/examples/echo.zig +++ b/examples/echo.zig @@ -2,7 +2,7 @@ const std = @import("std"); const coap = @import("coap"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); diff --git a/src/Client.zig b/src/Client.zig index 68a9076..549df7d 100644 --- a/src/Client.zig +++ b/src/Client.zig @@ -24,7 +24,8 @@ /// std.debug.print("{s}\n", .{result.payload}); /// ``` const std = @import("std"); -const posix = std.posix; +const platform = @import("platform.zig"); +const posix = platform; const coapz = @import("coapz"); const constants = @import("constants.zig"); const dtls = @import("dtls/dtls.zig"); @@ -33,7 +34,7 @@ const log = std.log.scoped(.coap_client); const Client = @This(); fn nanoTimestamp() i64 { - return @intCast(std.time.nanoTimestamp()); + return @intCast(platform.nanoTimestamp()); } /// Client configuration. All fields have sensible defaults. @@ -398,11 +399,11 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !Client { effective_config.port = constants.coaps_port_default; } - const dest = try std.net.Address.parseIp(effective_config.host, effective_config.port); + const dest = try platform.parseIp(effective_config.host, effective_config.port); - const fd = try posix.socket(dest.any.family, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const fd = try posix.socket(platform.family(dest), posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); errdefer posix.close(fd); - try posix.connect(fd, &dest.any, dest.getOsSockLen()); + try posix.connect(fd, dest); const buf_size = std.mem.toBytes(@as(c_int, 2 * 1024 * 1024)); posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.SNDBUF, &buf_size) catch {}; @@ -458,7 +459,7 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !Client { @memset(slot_table, empty_sentinel); var seed: u64 = undefined; - std.crypto.random.bytes(std.mem.asBytes(&seed)); + platform.randomBytes(std.mem.asBytes(&seed)); var rng = std.Random.DefaultPrng.init(seed); const initial_msg_id: u16 = rng.random().int(u16); @@ -489,9 +490,9 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !Client { .peer_confirmed = false, .observes = observes, .next_msg_id_val = initial_msg_id, - .next_token_val = std.crypto.random.int(u64), + .next_token_val = platform.randomInt(u64), .rng = rng, - .dtls_session = if (effective_config.psk != null) std.mem.zeroes(dtls.Session.Session) else null, + .dtls_session = if (effective_config.psk != null) std.mem.zeroInit(dtls.Session.Session, .{ .addr = platform.ip4(.{ 0, 0, 0, 0 }, 0) }) else null, .dtls_client_hs_state = .idle, .dtls_cached_session_id = .{0} ** 32, .dtls_cached_session_id_len = 0, @@ -1912,11 +1913,11 @@ const ServerRunner = struct { // Send a dummy packet to wake the server from blocking wait_cqes. const fd = posix.socket(posix.AF.INET, posix.SOCK.DGRAM, 0) catch return; defer posix.close(fd); - const dest = std.net.Address.parseIp( + const dest = std.Io.net.IpAddress.parse( "127.0.0.1", self.server.config.port, ) catch return; - _ = posix.sendto(fd, &[_]u8{0}, 0, &dest.any, dest.getOsSockLen()) catch {}; + _ = posix.sendto(fd, &[_]u8{0}, 0, dest) catch {}; thread.join(); } }; @@ -2282,12 +2283,22 @@ test "checkBlock2Etag captures and compares" { var opt_a = [1]coapz.Option{.{ .kind = .etag, .value = &etag_a }}; var opt_b = [1]coapz.Option{.{ .kind = .etag, .value = &etag_b }}; const pkt_a = coapz.Packet{ - .kind = .acknowledgement, .code = .content, .msg_id = 0, - .token = &.{}, .options = &opt_a, .payload = &.{}, .data_buf = &.{}, + .kind = .acknowledgement, + .code = .content, + .msg_id = 0, + .token = &.{}, + .options = &opt_a, + .payload = &.{}, + .data_buf = &.{}, }; const pkt_b = coapz.Packet{ - .kind = .acknowledgement, .code = .content, .msg_id = 0, - .token = &.{}, .options = &opt_b, .payload = &.{}, .data_buf = &.{}, + .kind = .acknowledgement, + .code = .content, + .msg_id = 0, + .token = &.{}, + .options = &opt_b, + .payload = &.{}, + .data_buf = &.{}, }; // First block — captures ETag. diff --git a/src/Io.zig b/src/Io.zig index dd8762e..925c239 100644 --- a/src/Io.zig +++ b/src/Io.zig @@ -3,8 +3,9 @@ /// Manages the ring, provided buffer pool, multishot recvmsg, and /// sendmsg operations. All buffers are allocated upfront at init. const std = @import("std"); +const platform = @import("platform.zig"); const linux = std.os.linux; -const posix = std.posix; +const posix = platform; const Cqe = linux.io_uring_cqe; const constants = @import("constants.zig"); @@ -73,8 +74,8 @@ pub fn deinit(io: *Io, allocator: std.mem.Allocator) void { /// Bind a UDP socket and provide buffers to the kernel pool. pub fn setup(io: *Io, port: u16, bind_address: []const u8) !void { - const address = try std.net.Address.parseIp(bind_address, port); - const family = address.any.family; + const address = try platform.parseIp(bind_address, port); + const family = platform.family(address); const fd = try posix.socket(family, posix.SOCK.DGRAM, 0); io.fd_socket = fd; @@ -98,7 +99,7 @@ pub fn setup(io: *Io, port: u16, bind_address: []const u8) !void { log.debug("SO_RCVBUF: {}", .{err}); }; - try posix.bind(fd, &address.any, address.getOsSockLen()); + try posix.bind(fd, address); for (io.iovecs, 0..) |*iov, i| { iov.* = .{ @@ -181,7 +182,7 @@ pub fn release_buffer(io: *Io, buffer_id: u16) !void { /// Extract the peer address and payload from a recv CQE. pub const RecvResult = struct { - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, payload: []const u8, buffer_id: u16, }; @@ -205,12 +206,16 @@ pub fn decode_recv(io: *const Io, cqe: *const Cqe) !RecvResult { // Determine address family from the first 2 bytes of the name area. const peer_family = std.mem.readInt(u16, io.buffers[name_offset..][0..2], .little); - const net_address: std.net.Address = if (peer_family == posix.AF.INET) - .{ .in = .{ .sa = @as(*const linux.sockaddr.in, @ptrCast(@alignCast(io.buffers.ptr + name_offset))).* } } - else if (peer_family == posix.AF.INET6) - .{ .in6 = .{ .sa = @as(*const linux.sockaddr.in6, @ptrCast(@alignCast(io.buffers.ptr + name_offset))).* } } - else - return error.UnsupportedAddressFamily; + var peer: platform.SocketAddress = undefined; + const address_len: usize = switch (peer_family) { + posix.AF.INET => @sizeOf(linux.sockaddr.in), + posix.AF.INET6 => @sizeOf(linux.sockaddr.in6), + else => return error.UnsupportedAddressFamily, + }; + if (recvmsg_out.namelen < address_len or name_offset + address_len > buffer_end) + return error.PayloadOutOfBounds; + @memcpy(std.mem.asBytes(&peer)[0..address_len], io.buffers[name_offset..][0..address_len]); + const net_address = try peer.address(); return .{ .peer_address = net_address, diff --git a/src/Server.zig b/src/Server.zig index 9cdb5ea..f30c4df 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -22,8 +22,9 @@ /// try server.run(); /// ``` const std = @import("std"); +const platform = @import("platform.zig"); const linux = std.os.linux; -const posix = std.posix; +const posix = platform; const coapz = @import("coapz"); const Io = @import("Io.zig"); const Exchange = @import("exchange.zig"); @@ -131,7 +132,7 @@ exchange_lifetime_ms: u32, running: std.atomic.Value(bool), // Pre-allocated per-CQE response state. -addrs_response: []std.net.Address, +addrs_response: []platform.SocketAddress, msgs_response: []linux.msghdr_const, iovs_response: []posix.iovec, buffer_response: []u8, @@ -316,7 +317,7 @@ fn init_raw( ); const addrs_response = try allocator.alloc( - std.net.Address, + platform.SocketAddress, batch, ); errdefer allocator.free(addrs_response); @@ -364,8 +365,8 @@ fn init_raw( }); errdefer tbl_ptr.deinit(allocator); dtls_sessions = tbl_ptr; - std.crypto.random.bytes(&dtls_cookie_secret); - std.crypto.random.bytes(&dtls_cookie_secret_prev); + platform.randomBytes(&dtls_cookie_secret); + platform.randomBytes(&dtls_cookie_secret_prev); } return .{ @@ -393,7 +394,7 @@ fn init_raw( .msg_recv = std.mem.zeroes(linux.msghdr), .last_eviction_ns = 0, .tick_count = 0, - .next_msg_id = std.crypto.random.int(u16), + .next_msg_id = platform.randomInt(u16), .force_free_all = false, .load_level = .normal, .buffers_outstanding = 0, @@ -443,8 +444,8 @@ pub fn listen(server: *Server) !void { server.msg_recv.name = @ptrCast(&server.addr_recv); // Set name buffer size based on actual socket family. - const bind_addr = try std.net.Address.parseIp(server.config.bind_address, server.config.port); - server.msg_recv.namelen = bind_addr.getOsSockLen(); + const bind_addr = try platform.parseIp(server.config.bind_address, server.config.port); + server.msg_recv.namelen = platform.addressLen(bind_addr); server.msg_recv.controllen = 0; try server.io.recv_multishot(&server.msg_recv); @@ -632,7 +633,7 @@ fn run_worker( state.restart_count, backoff_ms, }); - std.Thread.sleep(backoff_ms * std.time.ns_per_ms); + platform.sleep(backoff_ms * std.time.ns_per_ms); } if (config.cpu_affinity) |cores| { @@ -704,7 +705,7 @@ pub fn tick(server: *Server) !void { const batch_max = constants.completion_batch_max; var cqes: [batch_max]Cqe = std.mem.zeroes([batch_max]Cqe); - server.tick_now_ns = @intCast(std.time.nanoTimestamp()); + server.tick_now_ns = @intCast(platform.nanoTimestamp()); // Submit a timeout so wait_cqes unblocks periodically, allowing // the run loop to check the shutdown flag even when idle. @@ -770,7 +771,7 @@ pub fn tick(server: *Server) !void { const rotation_ns: i64 = @as(i64, constants.dtls_cookie_secret_rotation_s) * std.time.ns_per_s; if (server.tick_now_ns - server.dtls_cookie_rotation_ns > rotation_ns) { server.dtls_cookie_secret_prev = server.dtls_cookie_secret; - std.crypto.random.bytes(&server.dtls_cookie_secret); + platform.randomBytes(&server.dtls_cookie_secret); server.dtls_cookie_rotation_ns = server.tick_now_ns; } @@ -1083,10 +1084,10 @@ fn handle_recv( }); } } - const before = if (server.config.handler_warn_ns > 0) std.time.nanoTimestamp() else 0; + const before = if (server.config.handler_warn_ns > 0) platform.nanoTimestamp() else 0; const result = server.handler_fn(server.handler_context, request); if (server.config.handler_warn_ns > 0) { - const after = std.time.nanoTimestamp(); + const after = platform.nanoTimestamp(); const elapsed: u64 = @intCast(@max(0, after - before)); if (elapsed > server.config.handler_warn_ns) { log.warn("slow handler: {d}ms", .{elapsed / std.time.ns_per_ms}); @@ -1470,7 +1471,7 @@ fn process_dtls_coap( server: *Server, coap_payload: []const u8, session: *dtls.Session.Session, - peer: std.net.Address, + peer: std.Io.net.IpAddress, index: usize, ) void { const arena = server.arena.allocator(); @@ -1604,10 +1605,10 @@ fn process_dtls_coap( }); } } - const before = if (server.config.handler_warn_ns > 0) std.time.nanoTimestamp() else 0; + const before = if (server.config.handler_warn_ns > 0) platform.nanoTimestamp() else 0; const result = server.handler_fn(server.handler_context, request); if (server.config.handler_warn_ns > 0) { - const after = std.time.nanoTimestamp(); + const after = platform.nanoTimestamp(); const elapsed: u64 = @intCast(@max(0, after - before)); if (elapsed > server.config.handler_warn_ns) { log.warn("slow handler: {d}ms", .{elapsed / std.time.ns_per_ms}); @@ -1690,7 +1691,7 @@ fn sendDtlsResponse( session: *dtls.Session.Session, response: handler.Response, packet: coapz.Packet, - peer: std.net.Address, + peer: std.Io.net.IpAddress, is_con: bool, addr_key: u32, index: usize, @@ -1733,7 +1734,7 @@ fn send_dtls_packet( server: *Server, session: *dtls.Session.Session, pkt: coapz.Packet, - peer: std.net.Address, + peer: std.Io.net.IpAddress, index: usize, ) ![]const u8 { const buf = server.response_buf(index); @@ -1766,7 +1767,7 @@ fn send_dtls_alert( level: dtls.types.AlertLevel, desc: dtls.types.AlertDescription, index: usize, - peer: std.net.Address, + peer: std.Io.net.IpAddress, ) void { var alert_payload: [2]u8 = undefined; dtls.types.encodeAlert(level, desc, &alert_payload); @@ -1783,7 +1784,7 @@ fn send_dtls_alert( } /// Send pre-encoded CoAP data, encrypting if the peer has a DTLS session. -fn sendMaybeEncrypted(server: *Server, data: []const u8, peer: std.net.Address, index: usize) !void { +fn sendMaybeEncrypted(server: *Server, data: []const u8, peer: std.Io.net.IpAddress, index: usize) !void { if (server.dtls_sessions) |tbl| { if (tbl.lookup(peer)) |session| { const buf = server.response_buf(index); @@ -1811,7 +1812,7 @@ fn sendMaybeEncrypted(server: *Server, data: []const u8, peer: std.net.Address, fn send_emergency_ack( server: *Server, raw_payload: []const u8, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, index: usize, ) void { if (coapz.Packet.peekKind(raw_payload) != .confirmable) return; @@ -1828,7 +1829,7 @@ fn send_emergency_ack( fn send_rst( server: *Server, raw_header: []const u8, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, index: usize, ) void { const msg_id = coapz.Packet.peekMsgId(raw_header) orelse return; @@ -1843,7 +1844,6 @@ fn send_rst( /// Drain deferred response queue and retransmit pending separate CONs. /// Handle a Block1 upload fragment. Returns a response to send (2.31 Continue /// or error), or null if the upload is complete and the handler should be called. - const Block1Result = union(enum) { response: handler.Response, complete: u16, @@ -1854,7 +1854,7 @@ fn handleBlock1( bt: *BlockTransfer, bv: coapz.BlockValue, packet: coapz.Packet, - peer: std.net.Address, + peer: std.Io.net.IpAddress, arena: std.mem.Allocator, ) ?Block1Result { // Extract Request-Tag (RFC 9175 §3) for Block1 disambiguation. @@ -1933,7 +1933,7 @@ fn makeContinueResponse(bv: coapz.BlockValue, arena: std.mem.Allocator) ?handler fn handleBlock2Followup( bt: *BlockTransfer, packet: coapz.Packet, - peer: std.net.Address, + peer: std.Io.net.IpAddress, arena: std.mem.Allocator, ) ?handler.Response { const opt = packet.find_option(.block2) orelse return null; @@ -1990,7 +1990,7 @@ fn blockifyResponse( bt: *BlockTransfer, response: handler.Response, packet: coapz.Packet, - peer: std.net.Address, + peer: std.Io.net.IpAddress, arena: std.mem.Allocator, now_ns: i64, ) handler.Response { @@ -2028,7 +2028,7 @@ fn sendResponse( server: *Server, response: handler.Response, packet: coapz.Packet, - peer: std.net.Address, + peer: std.Io.net.IpAddress, is_con: bool, addr_key: u32, raw_header: *const [4]u8, @@ -2310,7 +2310,7 @@ fn response_buf(server: *Server, index: usize) []u8 { fn send_packet( server: *Server, pkt: coapz.Packet, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, index: usize, ) ![]const u8 { const buf = server.response_buf(index); @@ -2323,7 +2323,7 @@ fn send_packet( fn send_data( server: *Server, data: []const u8, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, index: usize, ) !void { const batch: usize = @min(constants.completion_batch_max, server.config.buffer_count); @@ -2354,10 +2354,10 @@ fn send_data( fn send_raw( server: *Server, data: []const u8, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, index: usize, ) !void { - server.addrs_response[index] = peer_address; + server.addrs_response[index] = platform.SocketAddress.init(peer_address); server.iovs_response[index] = .{ .base = @ptrCast(@constCast(data.ptr)), @@ -2366,7 +2366,7 @@ fn send_raw( server.msgs_response[index] = .{ .name = @ptrCast(&server.addrs_response[index]), - .namelen = peer_address.getOsSockLen(), + .namelen = platform.addressLen(peer_address), .iov = @ptrCast(&server.iovs_response[index]), .iovlen = 1, .control = null, @@ -2410,11 +2410,27 @@ fn is_well_known_core(packet: coapz.Packet) bool { fn isRecognizedOption(kind: coapz.OptionKind) bool { return switch (kind) { .unknown => false, - .if_match, .uri_host, .etag, .if_none_match, .observe, - .uri_port, .location_path, .oscore, .uri_path, - .content_format, .max_age, .uri_query, .accept, - .location_query, .block2, .block1, .size2, - .proxy_uri, .proxy_scheme, .size1, .no_response, + .if_match, + .uri_host, + .etag, + .if_none_match, + .observe, + .uri_port, + .location_path, + .oscore, + .uri_path, + .content_format, + .max_age, + .uri_query, + .accept, + .location_query, + .block2, + .block1, + .size2, + .proxy_uri, + .proxy_scheme, + .size1, + .no_response, => true, _ => false, }; @@ -2475,8 +2491,8 @@ fn test_client(port: u16) !posix.socket_t { std.mem.asBytes(&timeout), ); - const dest = try std.net.Address.parseIp("127.0.0.1", port); - try posix.connect(fd, &dest.any, dest.getOsSockLen()); + const dest = try std.Io.net.IpAddress.parse("127.0.0.1", port); + try posix.connect(fd, dest); return fd; } @@ -3120,11 +3136,11 @@ test "recognized_options allows custom critical options" { } fn test_client_ip(host: []const u8, port: u16) !posix.socket_t { - const dest = try std.net.Address.parseIp(host, port); - const fd = try posix.socket(dest.any.family, posix.SOCK.DGRAM, 0); + const dest = try std.Io.net.IpAddress.parse(host, port); + const fd = try posix.socket(platform.family(dest), posix.SOCK.DGRAM, 0); const timeout = posix.timeval{ .sec = 1, .usec = 0 }; try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.RCVTIMEO, std.mem.asBytes(&timeout)); - try posix.connect(fd, &dest.any, dest.getOsSockLen()); + try posix.connect(fd, dest); return fd; } diff --git a/src/block_transfer.zig b/src/block_transfer.zig index 79f597f..d4d5b34 100644 --- a/src/block_transfer.zig +++ b/src/block_transfer.zig @@ -6,7 +6,7 @@ /// complete payload. Block2: handler returns a full (large) payload, /// server serves blocks on demand as the client requests them. const std = @import("std"); -const posix = std.posix; +const platform = @import("platform.zig"); const coapz = @import("coapz"); const BlockTransfer = @This(); @@ -27,7 +27,7 @@ pub const Slot = struct { token_len: u8, request_tag: [8]u8, request_tag_len: u8, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, payload_length: u32, next_num: u32, szx: u3, @@ -69,7 +69,7 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !BlockTransfer { .token_len = 0, .request_tag = .{0} ** 8, .request_tag_len = 0, - .peer_address = std.mem.zeroes(std.net.Address), + .peer_address = platform.ip4(.{ 0, 0, 0, 0 }, 0), .payload_length = 0, .next_num = 0, .szx = 0, @@ -99,7 +99,7 @@ pub fn deinit(self: *BlockTransfer, allocator: std.mem.Allocator) void { pub fn allocate( self: *BlockTransfer, token: []const u8, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, kind: TransferKind, szx: u3, now_ns: i64, @@ -149,7 +149,7 @@ pub fn release(self: *BlockTransfer, idx: u16) void { } /// Find an active transfer by token, peer address, and request tag. -pub fn findByToken(self: *const BlockTransfer, token: []const u8, peer: std.net.Address, request_tag: []const u8) ?u16 { +pub fn findByToken(self: *const BlockTransfer, token: []const u8, peer: std.Io.net.IpAddress, request_tag: []const u8) ?u16 { for (self.slots, 0..) |*slot, i| { if (slot.state != .active) continue; if (slot.token_len != token.len) continue; @@ -165,7 +165,7 @@ pub fn findByToken(self: *const BlockTransfer, token: []const u8, peer: std.net. /// Find an active Block1 transfer by peer address and request-tag. /// Block1 subsequent blocks may use different tokens (RFC 7959 §2.5), /// so token matching is not used here. -pub fn findBlock1ByPeer(self: *const BlockTransfer, peer: std.net.Address, request_tag: []const u8) ?u16 { +pub fn findBlock1ByPeer(self: *const BlockTransfer, peer: std.Io.net.IpAddress, request_tag: []const u8) ?u16 { for (self.slots, 0..) |*slot, i| { if (slot.state != .active) continue; if (slot.kind != .block1_receiving) continue; @@ -244,13 +244,8 @@ fn payloadBuf(self: *const BlockTransfer, idx: u16) []u8 { return self.payload_buffer[offset..][0..self.config.max_payload]; } -fn addrEqual(a: std.net.Address, b: std.net.Address) bool { - if (a.any.family != b.any.family) return false; - return switch (a.any.family) { - posix.AF.INET => std.mem.eql(u8, std.mem.asBytes(&a.in), std.mem.asBytes(&b.in)), - posix.AF.INET6 => std.mem.eql(u8, std.mem.asBytes(&a.in6), std.mem.asBytes(&b.in6)), - else => false, - }; +fn addrEqual(a: std.Io.net.IpAddress, b: std.Io.net.IpAddress) bool { + return platform.addressEqual(a, b); } // ── Tests ── @@ -267,7 +262,7 @@ test "allocate and release" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 2, .max_payload = 1024 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const idx = pool.allocate(&.{0xAA}, addr, .block1_receiving, 6, 0, &.{}) orelse return error.PoolExhausted; try testing.expectEqual(@as(u16, 1), pool.count_active); @@ -281,7 +276,7 @@ test "pool exhaustion" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 2, .max_payload = 256 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); try testing.expect(pool.allocate(&.{0x01}, addr, .block1_receiving, 6, 0, &.{}) != null); try testing.expect(pool.allocate(&.{0x02}, addr, .block1_receiving, 6, 0, &.{}) != null); try testing.expect(pool.allocate(&.{0x03}, addr, .block1_receiving, 6, 0, &.{}) == null); @@ -291,8 +286,8 @@ test "findByToken" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 4, .max_payload = 256 }); defer pool.deinit(testing.allocator); - const addr1 = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); - const addr2 = std.net.Address.initIp4(.{ 10, 0, 0, 1 }, 5683); + const addr1 = platform.ip4(.{ 127, 0, 0, 1 }, 5683); + const addr2 = platform.ip4(.{ 10, 0, 0, 1 }, 5683); const idx = pool.allocate(&.{ 0xAA, 0xBB }, addr1, .block1_receiving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -305,7 +300,7 @@ test "block1: reassemble three fragments" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 4, .max_payload = 4096 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const idx = pool.allocate(&.{0xAA}, addr, .block1_receiving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -324,7 +319,7 @@ test "block1: error on wrong block number" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 4, .max_payload = 4096 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const idx = pool.allocate(&.{0xAA}, addr, .block1_receiving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -336,7 +331,7 @@ test "block1: error on payload too large" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 4, .max_payload = 100 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const idx = pool.allocate(&.{0xAA}, addr, .block1_receiving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -348,7 +343,7 @@ test "block2: store payload and serve blocks" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 4, .max_payload = 4096 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const payload = [_]u8{0x42} ** 2500; const idx = pool.allocate(&.{0xAA}, addr, .block2_serving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -375,7 +370,7 @@ test "block2: smaller SZX negotiation" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 4, .max_payload = 4096 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const payload = [_]u8{0x42} ** 600; const idx = pool.allocate(&.{0xAA}, addr, .block2_serving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -400,7 +395,7 @@ test "block2: out of range block number" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 4, .max_payload = 4096 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const idx = pool.allocate(&.{0xAA}, addr, .block2_serving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -418,7 +413,7 @@ test "block2: payload_length readable after release for Size2" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 1, .max_payload = 4096 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const idx = pool.allocate(&.{0xAA}, addr, .block2_serving, 6, 0, &.{}) orelse return error.PoolExhausted; @@ -449,7 +444,7 @@ test "evictExpired" { var pool = try BlockTransfer.init(testing.allocator, .{ .max_transfers = 4, .max_payload = 256 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); _ = pool.allocate(&.{0x01}, addr, .block1_receiving, 6, 1000, &.{}); _ = pool.allocate(&.{0x02}, addr, .block1_receiving, 6, 5000, &.{}); diff --git a/src/deferred.zig b/src/deferred.zig index 717cff7..75536da 100644 --- a/src/deferred.zig +++ b/src/deferred.zig @@ -7,7 +7,7 @@ /// loop drains the ring, sends the response as a new CON, and handles /// retransmission until the client ACKs. const std = @import("std"); -const posix = std.posix; +const platform = @import("platform.zig"); const coapz = @import("coapz"); const handler = @import("handler.zig"); const constants = @import("constants.zig"); @@ -33,7 +33,7 @@ pub const Slot = struct { state: std.atomic.Value(State), token: [8]u8, token_len: u8, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, /// Message ID for the separate CON response (not the original request). msg_id: u16, response_length: u16, @@ -123,7 +123,7 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !Deferred { .state = std.atomic.Value(State).init(.free), .token = .{0} ** 8, .token_len = 0, - .peer_address = std.mem.zeroes(std.net.Address), + .peer_address = platform.ip4(.{ 0, 0, 0, 0 }, 0), .msg_id = 0, .response_length = 0, .retransmit_count = 0, @@ -159,7 +159,7 @@ pub fn deinit(self: *Deferred, allocator: std.mem.Allocator) void { pub fn allocate( self: *Deferred, token: []const u8, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, msg_id: u16, now_ns: i64, ) ?u16 { @@ -240,7 +240,7 @@ pub fn drainQueue(self: *Deferred, out_buf: []u16) []const u16 { /// Find a pending/sent slot by message ID and peer address (for ACK matching). /// Linear scan — fine for small pool (default 16). -pub fn findByMsgId(self: *Deferred, msg_id: u16, peer_address: std.net.Address) ?u16 { +pub fn findByMsgId(self: *Deferred, msg_id: u16, peer_address: std.Io.net.IpAddress) ?u16 { for (self.slots, 0..) |*slot, i| { const state = slot.state.load(.acquire); if ((state == .sent or state == .ready) and @@ -253,13 +253,8 @@ pub fn findByMsgId(self: *Deferred, msg_id: u16, peer_address: std.net.Address) return null; } -fn addrEqual(a: std.net.Address, b: std.net.Address) bool { - if (a.any.family != b.any.family) return false; - return switch (a.any.family) { - posix.AF.INET => std.mem.eql(u8, std.mem.asBytes(&a.in), std.mem.asBytes(&b.in)), - posix.AF.INET6 => std.mem.eql(u8, std.mem.asBytes(&a.in6), std.mem.asBytes(&b.in6)), - else => false, - }; +fn addrEqual(a: std.Io.net.IpAddress, b: std.Io.net.IpAddress) bool { + return platform.addressEqual(a, b); } // ── Tests ── @@ -278,7 +273,7 @@ test "allocate and release" { const idx = pool.allocate( &.{ 0xAA, 0xBB }, - std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683), + platform.ip4(.{ 127, 0, 0, 1 }, 5683), 0x1234, 1_000_000_000, ) orelse return error.PoolExhausted; @@ -296,7 +291,7 @@ test "pool exhaustion" { var pool = try Deferred.init(testing.allocator, .{ .max_deferred = 2 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const a = pool.allocate(&.{0x01}, addr, 1, 0); const b = pool.allocate(&.{0x02}, addr, 2, 0); const c = pool.allocate(&.{0x03}, addr, 3, 0); @@ -310,7 +305,7 @@ test "enqueue and drain" { var pool = try Deferred.init(testing.allocator, .{}); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const idx = pool.allocate(&.{0xAA}, addr, 0x1234, 0) orelse return error.PoolExhausted; @@ -333,7 +328,7 @@ test "DeferredResponse.respond encodes and enqueues" { const idx = pool.allocate( &.{ 0xAA, 0xBB }, - std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683), + platform.ip4(.{ 127, 0, 0, 1 }, 5683), 0x1234, 0, ) orelse return error.PoolExhausted; @@ -356,7 +351,7 @@ test "DeferredResponse.cancel releases slot" { const idx = pool.allocate( &.{0xAA}, - std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683), + platform.ip4(.{ 127, 0, 0, 1 }, 5683), 0x1234, 0, ) orelse return error.PoolExhausted; @@ -371,7 +366,7 @@ test "findByMsgId" { var pool = try Deferred.init(testing.allocator, .{}); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); const idx = pool.allocate(&.{0xAA}, addr, 0x5678, 0) orelse return error.PoolExhausted; @@ -387,7 +382,7 @@ test "concurrent enqueue from multiple threads" { var pool = try Deferred.init(testing.allocator, .{ .max_deferred = 8 }); defer pool.deinit(testing.allocator); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); var indices: [4]u16 = undefined; for (&indices, 0..) |*idx, i| { idx.* = pool.allocate(&.{0xAA}, addr, @intCast(i), 0) orelse diff --git a/src/dtls/Ccm.zig b/src/dtls/Ccm.zig index 89e439f..f6387fd 100644 --- a/src/dtls/Ccm.zig +++ b/src/dtls/Ccm.zig @@ -91,7 +91,7 @@ pub fn decrypt(ciphertext_and_tag: []const u8, ad: []const u8, nonce: [nonce_len fn makeCtrBlock(nonce: [nonce_len]u8, counter: u24) [block_len]u8 { var block: [block_len]u8 = undefined; block[0] = ctr_flags; - @memcpy(block[1..1 + nonce_len], &nonce); + @memcpy(block[1 .. 1 + nonce_len], &nonce); // 3-byte big-endian counter at bytes 13..15 block[13] = @intCast((counter >> 16) & 0xFF); block[14] = @intCast((counter >> 8) & 0xFF); @@ -145,7 +145,7 @@ fn computeCbcMac(aes: AesEncryptCtx(Aes128), plaintext: []const u8, ad: []const // Build and process B0 var b0: [block_len]u8 = undefined; b0[0] = if (ad.len > 0) b0_flags_ad else b0_flags_no_ad; - @memcpy(b0[1..1 + nonce_len], &nonce); + @memcpy(b0[1 .. 1 + nonce_len], &nonce); // 3-byte big-endian message length at bytes 13..15 const msg_len = plaintext.len; b0[13] = @intCast((msg_len >> 16) & 0xFF); @@ -187,13 +187,21 @@ fn computeCbcMac(aes: AesEncryptCtx(Aes128), plaintext: []const u8, ad: []const } } - // Process plaintext in 16-byte blocks + // Keep the full-block XOR fixed-width. A variable-sized byte loop here + // prevents efficient vectorization with Zig 0.16 for large DTLS records. var pt_offset: usize = 0; - while (pt_offset < plaintext.len) : (pt_offset += block_len) { - const chunk_len = @min(block_len, plaintext.len - pt_offset); + while (plaintext.len - pt_offset >= block_len) : (pt_offset += block_len) { + const block = plaintext[pt_offset..][0..block_len].*; + x = @bitCast(@as(@Vector(block_len, u8), @bitCast(x)) ^ + @as(@Vector(block_len, u8), @bitCast(block))); + var tmp: [block_len]u8 = undefined; + aes.encrypt(&tmp, &x); + x = tmp; + } + // A partial final block is zero-padded (XOR with 0 leaves x unchanged). + if (pt_offset < plaintext.len) { + for (plaintext[pt_offset..], 0..) |b, i| x[i] ^= b; var tmp: [block_len]u8 = undefined; - for (0..chunk_len) |i| x[i] ^= plaintext[pt_offset + i]; - // Remaining bytes: no XOR needed (XOR with 0 = identity), already in x aes.encrypt(&tmp, &x); x = tmp; } @@ -203,6 +211,39 @@ fn computeCbcMac(aes: AesEncryptCtx(Aes128), plaintext: []const u8, ad: []const // ----- Tests ----- +test "CCM block and wide-block boundaries match independent AESCCM vectors" { + // SHA-256 of ciphertext || 8-byte tag generated with Python cryptography + // AESCCM(bytes([7])*16, tag_length=8), nonce=[3]*12, AD=[4]*13, + // plaintext bytes(i % 251 for i in range(length)). + const vectors = .{ + .{ 0, "23e5275fe2c79b4c7ba252a04f56f30c93f4d0edf02517f81a764ca46bd26185" }, + .{ 15, "b23a972fce8ddaf03e8c30ec1b61fefea2fa854d61b0e22b910f75495426ffd8" }, + .{ 16, "0b04ca33593266ffe3c722c1b7516c3da58f82071460918f80eb0b7e47d6c388" }, + .{ 17, "50ef503d90e06d78ea02dcb69c9e640cf61e3a5269d5348039d7e2a940b8333c" }, + .{ 31, "d8f5f2762708f9801214e4b6c53b5a2b7ec99fb23587e00a4fcc0347852854af" }, + .{ 32, "81590d94418e6b4122ec882becb4787b9beb50ecaeaaed3fd2e057ef101ad7f2" }, + .{ 33, "bdfb34b2589bb131ec90d7ab22c3e0cc2abf357e61b59fa6423bffa605441266" }, + .{ 100, "31cc0dc3f1088012bc4d5621fef191d29a3066253fb7b1bb634f02daae9a1281" }, + .{ 1000, "380b19c3ee26288dd91955fec5cb5ff370baa9b95fbf8d97c77f000dbb7da2b0" }, + .{ 1024, "7d181bca0e31a073d7e6cf80572e6df2042a4679e4fd402c7d6ad7ca16b3f9b6" }, + }; + var plaintext: [1024]u8 = undefined; + for (&plaintext, 0..) |*b, i| b.* = @intCast(i % 251); + var ciphertext: [1032]u8 = undefined; + var recovered: [1024]u8 = undefined; + inline for (vectors) |v| { + const len = v[0]; + encrypt(plaintext[0..len], &(.{4} ** 13), .{3} ** 12, .{7} ** 16, ciphertext[0 .. len + tag_len]); + var digest: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(ciphertext[0 .. len + tag_len], &digest, .{}); + var expected: [32]u8 = undefined; + _ = try std.fmt.hexToBytes(&expected, v[1]); + try std.testing.expectEqualSlices(u8, &expected, &digest); + try decrypt(ciphertext[0 .. len + tag_len], &(.{4} ** 13), .{3} ** 12, .{7} ** 16, recovered[0..len]); + try std.testing.expectEqualSlices(u8, plaintext[0..len], recovered[0..len]); + } +} + const testing = std.testing; test "round-trip: basic encrypt/decrypt" { diff --git a/src/dtls/Cookie.zig b/src/dtls/Cookie.zig index 027e0b1..ee34e20 100644 --- a/src/dtls/Cookie.zig +++ b/src/dtls/Cookie.zig @@ -2,41 +2,37 @@ /// /// Cookie = HMAC-SHA256(server_secret, client_addr_bytes || client_random) const std = @import("std"); -const posix = std.posix; +const platform = @import("../platform.zig"); const HmacSha256 = std.crypto.auth.hmac.sha2.HmacSha256; /// Generate a stateless DTLS cookie. -pub fn generate(server_secret: [32]u8, client_addr: std.net.Address, client_random: [32]u8) [32]u8 { +pub fn generate(server_secret: [32]u8, client_addr: std.Io.net.IpAddress, client_random: [32]u8) [32]u8 { var mac: [32]u8 = undefined; var h = HmacSha256.init(&server_secret); - // Hash the full sockaddr for the active family. Use pointer to - // client_addr directly (not a copy) to avoid dangling slice. - switch (client_addr.any.family) { - posix.AF.INET => h.update(std.mem.asBytes(&client_addr.in)), - posix.AF.INET6 => h.update(std.mem.asBytes(&client_addr.in6)), - else => h.update(std.mem.asBytes(&client_addr.in)), - } + // Canonical socket bytes avoid hashing padding in native address types. + const sa = platform.SocketAddress.init(client_addr); + h.update(sa.bytes()); h.update(&client_random); h.final(&mac); return mac; } /// Verify a DTLS cookie against a single server secret. -pub fn verify(server_secret: [32]u8, client_addr: std.net.Address, client_random: [32]u8, cookie: []const u8) bool { +pub fn verify(server_secret: [32]u8, client_addr: std.Io.net.IpAddress, client_random: [32]u8, cookie: []const u8) bool { if (cookie.len != 32) return false; const expected = generate(server_secret, client_addr, client_random); return std.crypto.timing_safe.eql([32]u8, expected, cookie[0..32].*); } /// Verify with rotation: try current secret, then previous. -pub fn verifyWithRotation(current_secret: [32]u8, previous_secret: [32]u8, client_addr: std.net.Address, client_random: [32]u8, cookie: []const u8) bool { +pub fn verifyWithRotation(current_secret: [32]u8, previous_secret: [32]u8, client_addr: std.Io.net.IpAddress, client_random: [32]u8, cookie: []const u8) bool { return verify(current_secret, client_addr, client_random, cookie) or verify(previous_secret, client_addr, client_random, cookie); } test "generate and verify round-trip" { const secret = [_]u8{0x42} ** 32; - const addr = std.net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); const random = [_]u8{0xab} ** 32; const cookie = generate(secret, addr, random); @@ -45,8 +41,8 @@ test "generate and verify round-trip" { test "verify fails with wrong address" { const secret = [_]u8{0x01} ** 32; - const addr1 = std.net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 5684); - const addr2 = std.net.Address.initIp4([4]u8{ 192, 168, 1, 1 }, 5684); + const addr1 = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); + const addr2 = platform.ip4([4]u8{ 192, 168, 1, 1 }, 5684); const random = [_]u8{0x22} ** 32; const cookie = generate(secret, addr1, random); @@ -56,7 +52,7 @@ test "verify fails with wrong address" { test "verify fails with wrong secret" { const secret1 = [_]u8{0x01} ** 32; const secret2 = [_]u8{0x02} ** 32; - const addr = std.net.Address.initIp4([4]u8{ 10, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 10, 0, 0, 1 }, 5684); const random = [_]u8{0x33} ** 32; const cookie = generate(secret1, addr, random); @@ -65,7 +61,7 @@ test "verify fails with wrong secret" { test "verify fails with wrong length (truncated cookie)" { const secret = [_]u8{0x55} ** 32; - const addr = std.net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); const random = [_]u8{0x66} ** 32; const cookie = generate(secret, addr, random); @@ -76,7 +72,7 @@ test "verify fails with wrong length (truncated cookie)" { test "verify fails with wrong client_random" { const secret = [_]u8{0x77} ** 32; - const addr = std.net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); const random1 = [_]u8{0x88} ** 32; const random2 = [_]u8{0x99} ** 32; @@ -87,7 +83,7 @@ test "verify fails with wrong client_random" { test "verifyWithRotation accepts cookie from previous secret" { const current = [_]u8{0xaa} ** 32; const previous = [_]u8{0xbb} ** 32; - const addr = std.net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); const random = [_]u8{0xcc} ** 32; const cookie = generate(previous, addr, random); @@ -101,7 +97,7 @@ test "verifyWithRotation rejects cookie from neither secret" { const current = [_]u8{0xdd} ** 32; const previous = [_]u8{0xee} ** 32; const other = [_]u8{0xff} ** 32; - const addr = std.net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); const random = [_]u8{0x11} ** 32; const cookie = generate(other, addr, random); diff --git a/src/dtls/Handshake.zig b/src/dtls/Handshake.zig index eba3687..83e8092 100644 --- a/src/dtls/Handshake.zig +++ b/src/dtls/Handshake.zig @@ -3,6 +3,7 @@ /// Implements key derivation (RFC 4279), handshake message framing (RFC 6347), /// and both server-side and client-side state machines for PSK-only cipher suites. const std = @import("std"); +const platform = @import("../platform.zig"); const types = @import("types.zig"); const Prf = @import("Prf.zig"); const Record = @import("Record.zig"); @@ -195,7 +196,7 @@ pub fn isClientHelloWithValidCookie( payload: []const u8, cookie_secret: [32]u8, cookie_secret_prev: [32]u8, - peer_addr: std.net.Address, + peer_addr: std.Io.net.IpAddress, ) bool { const msg = decodeHandshakeMessage(payload) orelse return false; if (msg.msg_type != .client_hello) return false; @@ -232,7 +233,7 @@ pub fn isClientHelloWithValidCookie( pub fn buildStatelessHvr( payload: []const u8, cookie_secret: [32]u8, - peer_addr: std.net.Address, + peer_addr: std.Io.net.IpAddress, send_buf: []u8, ) ?[]const u8 { const msg = decodeHandshakeMessage(payload) orelse return null; @@ -337,14 +338,14 @@ fn serverHandleClientHello( session.message_seq = 0; // Generate server_random. - std.crypto.random.bytes(&session.server_random); + platform.randomBytes(&session.server_random); // Generate or reuse session_id. if (resumption) |r| { session.session_id = r.session_id; session.session_id_len = r.session_id_len; } else { - std.crypto.random.bytes(&session.session_id); + platform.randomBytes(&session.session_id); session.session_id_len = 32; } @@ -637,7 +638,7 @@ pub fn clientBuildInitialHello( return .{ .failed = .internal_error }; // Generate client_random. - std.crypto.random.bytes(&session.client_random); + platform.randomBytes(&session.client_random); // Reset handshake hash. session.handshake_hash = Sha256.init(.{}); @@ -1033,7 +1034,7 @@ test "full server handshake" { const psk_key = "test-key-value"; const psk = types.Psk{ .identity = psk_identity, .key = psk_key }; const cookie_secret = [_]u8{0x42} ** 32; - const addr = std.net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 127, 0, 0, 1 }, 5684); // Create a server session. var server_session: Session.Session = undefined; @@ -1041,7 +1042,7 @@ test "full server handshake" { var send_buf: [2048]u8 = undefined; var client_random: [32]u8 = undefined; - std.crypto.random.bytes(&client_random); + platform.randomBytes(&client_random); // Step 1: ClientHello without cookie → expect HelloVerifyRequest { @@ -1140,7 +1141,7 @@ test "full client-server handshake integration" { const psk_key = "my-secret-key"; const psk = types.Psk{ .identity = psk_identity, .key = psk_key }; const cookie_secret = [_]u8{0xAA} ** 32; - const addr = std.net.Address.initIp4([4]u8{ 10, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 10, 0, 0, 1 }, 5684); var server_session: Session.Session = undefined; initTestSession(&server_session, addr); @@ -1315,7 +1316,7 @@ test "full client-server handshake integration" { try testing.expectEqualSlices(u8, &server_session.server_write_key, &client_session.server_write_key); } -fn initTestSession(session: *Session.Session, addr: std.net.Address) void { +fn initTestSession(session: *Session.Session, addr: std.Io.net.IpAddress) void { session.* = .{ .state = .handshaking, .peer_hash = 0, @@ -1352,13 +1353,13 @@ fn initTestSession(session: *Session.Session, addr: std.net.Address) void { test "session resumption: server abbreviated path with ResumptionData" { const psk = types.Psk{ .identity = "test-device", .key = "my-secret-key" }; const cookie_secret = [_]u8{0xAA} ** 32; - const addr = std.net.Address.initIp4([4]u8{ 10, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 10, 0, 0, 1 }, 5684); // Simulate a previously established session with known master_secret. var saved_master_secret: [48]u8 = undefined; - std.crypto.random.bytes(&saved_master_secret); + platform.randomBytes(&saved_master_secret); var saved_session_id: [32]u8 = undefined; - std.crypto.random.bytes(&saved_session_id); + platform.randomBytes(&saved_session_id); // Build a ClientHello with the cached session_id. var client_session: Session.Session = undefined; @@ -1367,7 +1368,10 @@ test "session resumption: server abbreviated path with ResumptionData" { var send_buf: [2048]u8 = undefined; const action1 = clientBuildInitialHello( - &client_session, &client_hs_state, psk, &send_buf, + &client_session, + &client_hs_state, + psk, + &send_buf, &saved_session_id, ); const client_hello_data = switch (action1) { @@ -1390,8 +1394,14 @@ test "session resumption: server abbreviated path with ResumptionData" { { const rec = Record.decodePlaintext(client_hello_data) orelse return error.TestUnexpectedNull; const hvr_action = serverProcessMessage( - &server_session, rec.content_type, rec.payload, - psk, cookie_secret, cookie_secret, &send_buf2, resumption, + &server_session, + rec.content_type, + rec.payload, + psk, + cookie_secret, + cookie_secret, + &send_buf2, + resumption, ); // First ClientHello has no cookie → HelloVerifyRequest switch (hvr_action) { @@ -1417,8 +1427,14 @@ test "session resumption: server abbreviated path with ResumptionData" { initTestSession(&server_session, addr); const action = serverProcessMessage( - &server_session, .handshake, rec[13..], // strip record header, pass payload - psk, cookie_secret, cookie_secret, &send_buf2, resumption, + &server_session, + .handshake, + rec[13..], // strip record header, pass payload + psk, + cookie_secret, + cookie_secret, + &send_buf2, + resumption, ); // With valid cookie + resumption data → abbreviated handshake. @@ -1438,7 +1454,7 @@ test "session resumption: server abbreviated path with ResumptionData" { test "session resumption: client sends cached session_id" { const psk = types.Psk{ .identity = "test-device", .key = "my-secret-key" }; - const addr = std.net.Address.initIp4([4]u8{ 10, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 10, 0, 0, 1 }, 5684); var session: Session.Session = undefined; initTestSession(&session, addr); @@ -1447,7 +1463,7 @@ test "session resumption: client sends cached session_id" { // Build ClientHello with a cached session_id. var cached_sid: [32]u8 = undefined; - std.crypto.random.bytes(&cached_sid); + platform.randomBytes(&cached_sid); const action = clientBuildInitialHello(&session, &hs_state, psk, &send_buf, &cached_sid); const data = switch (action) { @@ -1467,7 +1483,7 @@ test "session resumption: client sends cached session_id" { test "session resumption: empty session_id produces full handshake" { const psk = types.Psk{ .identity = "test-device", .key = "my-secret-key" }; - const addr = std.net.Address.initIp4([4]u8{ 10, 0, 0, 1 }, 5684); + const addr = platform.ip4([4]u8{ 10, 0, 0, 1 }, 5684); var session: Session.Session = undefined; initTestSession(&session, addr); diff --git a/src/dtls/Session.zig b/src/dtls/Session.zig index 576c37a..6753f41 100644 --- a/src/dtls/Session.zig +++ b/src/dtls/Session.zig @@ -3,7 +3,7 @@ /// Pre-allocated open-addressed hash table with intrusive LRU doubly-linked /// list and free list. Provides O(1) lookup, allocation, and eviction. const std = @import("std"); -const posix = std.posix; +const platform = @import("../platform.zig"); const Sha256 = std.crypto.hash.sha2.Sha256; pub const State = enum(u8) { @@ -30,7 +30,7 @@ pub const Config = struct { pub const Session = struct { state: State, peer_hash: u64, - addr: std.net.Address, + addr: std.Io.net.IpAddress, // Crypto state client_write_key: [16]u8, @@ -144,7 +144,7 @@ pub const SessionTable = struct { } /// Look up a session by peer address. - pub fn lookup(self: *SessionTable, addr: std.net.Address) ?*Session { + pub fn lookup(self: *SessionTable, addr: std.Io.net.IpAddress) ?*Session { const hash = addrHash(addr); var idx: u32 = @intCast(hash & self.table_mask); var probes: u32 = 0; @@ -179,7 +179,7 @@ pub const SessionTable = struct { /// Allocate a new session for the given address. /// Evicts LRU if no free slots remain. Returns null if capacity == 0. - pub fn allocate(self: *SessionTable, addr: std.net.Address, now_ns: i64) ?*Session { + pub fn allocate(self: *SessionTable, addr: std.Io.net.IpAddress, now_ns: i64) ?*Session { if (self.config.capacity == 0) return null; // Get a free slot — either from free list or by evicting LRU. @@ -369,12 +369,9 @@ pub const SessionTable = struct { } }; -fn addrHash(addr: std.net.Address) u64 { - const bytes = switch (addr.any.family) { - posix.AF.INET => std.mem.asBytes(&addr.in), - posix.AF.INET6 => std.mem.asBytes(&addr.in6), - else => std.mem.asBytes(&addr.any), - }; +fn addrHash(addr: std.Io.net.IpAddress) u64 { + const sa = platform.SocketAddress.init(addr); + const bytes = sa.bytes(); var hash: u64 = 0xcbf29ce484222325; // FNV-1a 64-bit offset basis for (bytes) |b| { hash ^= b; @@ -383,13 +380,8 @@ fn addrHash(addr: std.net.Address) u64 { return hash; } -fn addrEqual(a: std.net.Address, b: std.net.Address) bool { - if (a.any.family != b.any.family) return false; - return switch (a.any.family) { - posix.AF.INET => std.mem.eql(u8, std.mem.asBytes(&a.in), std.mem.asBytes(&b.in)), - posix.AF.INET6 => std.mem.eql(u8, std.mem.asBytes(&a.in6), std.mem.asBytes(&b.in6)), - else => false, - }; +fn addrEqual(a: std.Io.net.IpAddress, b: std.Io.net.IpAddress) bool { + return platform.addressEqual(a, b); } fn wrappingDistance(from: u32, to: u32, mask: u32) u32 { @@ -411,7 +403,7 @@ test "allocate and lookup" { var tbl = try SessionTable.init(testing.allocator, .{ .capacity = 8 }); defer tbl.deinit(testing.allocator); - const addr = try std.net.Address.parseIp("127.0.0.1", 5684); + const addr = try std.Io.net.IpAddress.parse("127.0.0.1", 5684); const session = tbl.allocate(addr, 0).?; try testing.expectEqual(State.handshaking, session.state); @@ -426,7 +418,7 @@ test "lookup returns null for unknown" { var tbl = try SessionTable.init(testing.allocator, .{ .capacity = 8 }); defer tbl.deinit(testing.allocator); - const addr = try std.net.Address.parseIp("10.0.0.1", 5684); + const addr = try std.Io.net.IpAddress.parse("10.0.0.1", 5684); try testing.expect(tbl.lookup(addr) == null); } @@ -434,7 +426,7 @@ test "release zeros key material" { var tbl = try SessionTable.init(testing.allocator, .{ .capacity = 8 }); defer tbl.deinit(testing.allocator); - const addr = try std.net.Address.parseIp("127.0.0.1", 5684); + const addr = try std.Io.net.IpAddress.parse("127.0.0.1", 5684); const session = tbl.allocate(addr, 0).?; @memset(&session.client_write_key, 0xFF); @@ -457,9 +449,9 @@ test "LRU eviction when table full" { var tbl = try SessionTable.init(testing.allocator, .{ .capacity = 4 }); defer tbl.deinit(testing.allocator); - var addrs: [4]std.net.Address = undefined; + var addrs: [4]std.Io.net.IpAddress = undefined; for (0..4) |i| { - addrs[i] = try std.net.Address.parseIp("10.0.0.1", @intCast(6000 + i)); + addrs[i] = try std.Io.net.IpAddress.parse("10.0.0.1", @intCast(6000 + i)); _ = tbl.allocate(addrs[i], @intCast(i)); } try testing.expectEqual(@as(u32, 4), tbl.count_active); @@ -468,7 +460,7 @@ test "LRU eviction when table full" { const oldest_addr = addrs[0]; // Allocate 5th — should evict oldest (LRU tail). - const new_addr = try std.net.Address.parseIp("10.0.0.2", 7000); + const new_addr = try std.Io.net.IpAddress.parse("10.0.0.2", 7000); _ = tbl.allocate(new_addr, 100).?; try testing.expectEqual(@as(u32, 4), tbl.count_active); @@ -480,10 +472,10 @@ test "promote moves to LRU head" { var tbl = try SessionTable.init(testing.allocator, .{ .capacity = 4 }); defer tbl.deinit(testing.allocator); - var addrs: [4]std.net.Address = undefined; + var addrs: [4]std.Io.net.IpAddress = undefined; var sessions: [4]*Session = undefined; for (0..4) |i| { - addrs[i] = try std.net.Address.parseIp("10.0.0.1", @intCast(6000 + i)); + addrs[i] = try std.Io.net.IpAddress.parse("10.0.0.1", @intCast(6000 + i)); sessions[i] = tbl.allocate(addrs[i], @intCast(i)).?; } // LRU order (tail → head): 0, 1, 2, 3 @@ -493,7 +485,7 @@ test "promote moves to LRU head" { // LRU order (tail → head): 1, 2, 3, 0 // Next eviction candidate is sessions[1] (addr index 1). - const addr_new = try std.net.Address.parseIp("10.0.0.2", 7000); + const addr_new = try std.Io.net.IpAddress.parse("10.0.0.2", 7000); _ = tbl.allocate(addr_new, 200).?; // sessions[0] was promoted — should survive. diff --git a/src/dtls/integration_test.zig b/src/dtls/integration_test.zig index 2b692c7..3467c1a 100644 --- a/src/dtls/integration_test.zig +++ b/src/dtls/integration_test.zig @@ -1,6 +1,7 @@ const std = @import("std"); +const platform = @import("../platform.zig"); const testing = std.testing; -const posix = std.posix; +const posix = platform; const linux = std.os.linux; const dtls = @import("dtls.zig"); @@ -30,11 +31,11 @@ const ServerRunner = struct { // Send a dummy packet to wake the server from blocking wait_cqes. const fd = posix.socket(posix.AF.INET, posix.SOCK.DGRAM, 0) catch return; defer posix.close(fd); - const dest = std.net.Address.parseIp( + const dest = std.Io.net.IpAddress.parse( "127.0.0.1", self.server.config.port, ) catch return; - _ = posix.sendto(fd, &[_]u8{0}, 0, &dest.any, dest.getOsSockLen()) catch {}; + _ = posix.sendto(fd, &[_]u8{0}, 0, dest) catch {}; thread.join(); } }; @@ -348,7 +349,7 @@ test "Observe: notification carries correct client token" { var next_ctx = NextCtx{ .stream = &stream }; const next_thread = try std.Thread.spawn(.{}, NextCtx.run, .{&next_ctx}); - std.Thread.sleep(300 * std.time.ns_per_ms); + platform.sleep(300 * std.time.ns_per_ms); stream.cancel() catch {}; next_thread.join(); @@ -415,7 +416,7 @@ test "Observe: CON notification with ACK" { var next_ctx = NextCtx{ .stream = &stream }; const next_thread = try std.Thread.spawn(.{}, NextCtx.run, .{&next_ctx}); - std.Thread.sleep(300 * std.time.ns_per_ms); + platform.sleep(300 * std.time.ns_per_ms); stream.cancel() catch {}; next_thread.join(); diff --git a/src/dtls/openssl_interop_test.zig b/src/dtls/openssl_interop_test.zig index 9cd3791..dd46eae 100644 --- a/src/dtls/openssl_interop_test.zig +++ b/src/dtls/openssl_interop_test.zig @@ -4,8 +4,9 @@ /// a handshake with OpenSSL's s_server. Skipped when openssl is not /// available or does not support PSK-AES128-CCM8. const std = @import("std"); +const platform = @import("../platform.zig"); const testing = std.testing; -const posix = std.posix; +const posix = platform; const dtls = @import("dtls.zig"); const Record = @import("Record.zig"); @@ -22,49 +23,47 @@ const test_psk_hex = "30313233343536373839616263646566"; /// Check if openssl is available and supports the required cipher. fn opensslAvailable() bool { - var child = std.process.Child.init( - &.{ "openssl", "ciphers", "PSK-AES128-CCM8@SECLEVEL=0" }, - testing.allocator, - ); - child.stdout_behavior = .Ignore; - child.stderr_behavior = .Ignore; - child.spawn() catch return false; - const result = child.wait() catch return false; - return result == .Exited and result.Exited == 0; + var child = std.process.spawn(testing.io, .{ + .argv = &.{ "openssl", "ciphers", "PSK-AES128-CCM8@SECLEVEL=0" }, + .stdout = .ignore, + .stderr = .ignore, + }) catch return false; + const result = child.wait(testing.io) catch return false; + return result == .exited and result.exited == 0; } /// Generate a self-signed cert for openssl s_server (required even for PSK). fn generateTestCert(cert_path: []const u8, key_path: []const u8) bool { - var child = std.process.Child.init(&.{ - "openssl", "req", "-x509", "-newkey", - "rsa:2048", "-keyout", key_path, "-out", - cert_path, "-days", "1", "-nodes", - "-subj", "/CN=test", "-batch", - }, testing.allocator); - child.stdout_behavior = .Ignore; - child.stderr_behavior = .Ignore; - child.spawn() catch return false; - const result = child.wait() catch return false; - return result == .Exited and result.Exited == 0; + var child = std.process.spawn(testing.io, .{ + .argv = &.{ + "openssl", "req", "-x509", "-newkey", "rsa:2048", + "-keyout", key_path, "-out", cert_path, "-days", + "1", "-nodes", "-subj", "/CN=test", "-batch", + }, + .stdout = .ignore, + .stderr = .ignore, + }) catch return false; + const result = child.wait(testing.io) catch return false; + return result == .exited and result.exited == 0; } /// Start OpenSSL DTLS server. Returns the child process. /// Caller must hold stdin_pipe open to keep the server alive. /// Note: do NOT use -listen — it creates a new connected socket after /// cookie verification, so responses go to a different source port. -fn startOpensslServer(port: u16, psk_hex: []const u8) !std.process.Child { +fn startOpensslServer(port: u16, psk_hex: []const u8, cert_path: []const u8, key_path: []const u8) !std.process.Child { var port_buf: [8]u8 = undefined; const port_str = std.fmt.bufPrint(&port_buf, "{d}", .{port}) catch unreachable; - var child = std.process.Child.init(&.{ + return std.process.spawn(testing.io, .{ .argv = &.{ "openssl", "s_server", "-dtls1_2", "-4", "-cert", - "/tmp/coap_test_cert.pem", + cert_path, "-key", - "/tmp/coap_test_key.pem", + key_path, "-psk", psk_hex, "-psk_identity", @@ -74,12 +73,7 @@ fn startOpensslServer(port: u16, psk_hex: []const u8) !std.process.Child { "-port", port_str, "-quiet", - }, testing.allocator); - child.stdin_behavior = .Pipe; // Keep open to prevent server exit. - child.stdout_behavior = .Ignore; - child.stderr_behavior = .Ignore; - try child.spawn(); - return child; + }, .stdin = .pipe, .stdout = .ignore, .stderr = .ignore }); } /// Blocking recv with deadline (ms-granularity poll via sleep). @@ -87,8 +81,8 @@ fn recvWithDeadline(fd: posix.fd_t, buf: []u8, deadline_ns: i128) ?usize { while (true) { const n = posix.recv(fd, buf, 0) catch |err| switch (err) { error.WouldBlock => { - if (std.time.nanoTimestamp() >= deadline_ns) return null; - std.Thread.sleep(500 * std.time.ns_per_us); + if (platform.nanoTimestamp() >= deadline_ns) return null; + platform.sleep(500 * std.time.ns_per_us); continue; }, else => return null, @@ -102,34 +96,41 @@ test "OpenSSL interop: handshake state machine with OpenSSL s_server" { std.log.warn("openssl not available or missing PSK-AES128-CCM8, skipping", .{}); return; } - if (!generateTestCert("/tmp/coap_test_cert.pem", "/tmp/coap_test_key.pem")) { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var dir_buf: [std.fs.max_path_bytes]u8 = undefined; + const dir_len = try tmp.dir.realPath(testing.io, &dir_buf); + const cert_path = try std.fmt.allocPrint(testing.allocator, "{s}/cert.pem", .{dir_buf[0..dir_len]}); + defer testing.allocator.free(cert_path); + const key_path = try std.fmt.allocPrint(testing.allocator, "{s}/key.pem", .{dir_buf[0..dir_len]}); + defer testing.allocator.free(key_path); + if (!generateTestCert(cert_path, key_path)) { std.log.warn("failed to generate test cert, skipping", .{}); return; } const port: u16 = 19790; - var ossl = startOpensslServer(port, test_psk_hex) catch { + var ossl = startOpensslServer(port, test_psk_hex, cert_path, key_path) catch { std.log.warn("failed to start openssl s_server, skipping", .{}); return; }; defer { - if (ossl.stdin) |*stdin| stdin.close(); + if (ossl.stdin) |stdin| stdin.close(testing.io); ossl.stdin = null; - _ = ossl.kill() catch {}; - _ = ossl.wait() catch {}; + ossl.kill(testing.io); } // Wait for server to start listening. - std.Thread.sleep(500 * std.time.ns_per_ms); + platform.sleep(500 * std.time.ns_per_ms); // Create raw UDP socket, bypassing Client abstraction. - const dest = try std.net.Address.parseIp("127.0.0.1", port); + const dest = try std.Io.net.IpAddress.parse("127.0.0.1", port); const fd = try posix.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); defer posix.close(fd); - try posix.connect(fd, &dest.any, dest.getOsSockLen()); + try posix.connect(fd, dest); - var session: Session.Session = std.mem.zeroes(Session.Session); + var session: Session.Session = std.mem.zeroInit(Session.Session, .{ .addr = platform.ip4(.{ 0, 0, 0, 0 }, 0) }); var hs_state: Handshake.ClientHandshakeState = .idle; var send_buf: [512]u8 = undefined; var recv_buf: [1500]u8 = undefined; @@ -143,11 +144,11 @@ test "OpenSSL interop: handshake state machine with OpenSSL s_server" { }; _ = posix.send(fd, initial_flight, 0) catch return error.TestUnexpectedResult; - const deadline_ns: i128 = std.time.nanoTimestamp() + 10 * std.time.ns_per_s; + const deadline_ns: i128 = platform.nanoTimestamp() + 10 * std.time.ns_per_s; // Handshake loop. while (hs_state != .complete) { - if (std.time.nanoTimestamp() >= deadline_ns) { + if (platform.nanoTimestamp() >= deadline_ns) { std.debug.print("interop: timeout in state {}\n", .{hs_state}); return error.TestUnexpectedResult; } diff --git a/src/exchange.zig b/src/exchange.zig index bc7b09f..81ec947 100644 --- a/src/exchange.zig +++ b/src/exchange.zig @@ -5,7 +5,7 @@ /// table keyed on (peer_address, message_id) for O(1) lookups and an /// intrusive free list for O(1) slot allocation. const std = @import("std"); -const posix = std.posix; +const platform = @import("platform.zig"); const constants = @import("constants.zig"); const Exchange = @This(); @@ -112,9 +112,10 @@ pub fn deinit(exchange: *Exchange, allocator: std.mem.Allocator) void { } /// Hash peer address only (no message ID) for peer-based eviction. -pub fn addrHash(address: std.net.Address) u32 { +pub fn addrHash(address: std.Io.net.IpAddress) u32 { var hash: u32 = 0x811c9dc5; // FNV-1a 32-bit offset basis - for (addrBytes(&address)) |b| { + const sa = platform.SocketAddress.init(address); + for (sa.bytes()) |b| { hash ^= b; hash *%= 0x01000193; // FNV-1a 32-bit prime } @@ -122,9 +123,10 @@ pub fn addrHash(address: std.net.Address) u32 { } /// Compute a hash key from peer address and message ID. -pub fn peerKey(address: std.net.Address, message_id: u16) u64 { +pub fn peerKey(address: std.Io.net.IpAddress, message_id: u16) u64 { var hash: u64 = 0xcbf29ce484222325; // FNV-1a offset basis - for (addrBytes(&address)) |b| { + const sa = platform.SocketAddress.init(address); + for (sa.bytes()) |b| { hash ^= b; hash *%= 0x100000001b3; // FNV-1a prime } @@ -133,15 +135,6 @@ pub fn peerKey(address: std.net.Address, message_id: u16) u64 { return hash; } -/// Extract the relevant address bytes for hashing (family-aware). -fn addrBytes(address: *const std.net.Address) []const u8 { - return switch (address.any.family) { - posix.AF.INET => std.mem.asBytes(&address.in), - posix.AF.INET6 => std.mem.asBytes(&address.in6), - else => std.mem.asBytes(&address.any), - }; -} - /// Look up an exchange by peer address and message ID. /// Returns the slot index if found, null otherwise. pub fn find(exchange: *const Exchange, key: u64) ?u16 { @@ -347,7 +340,7 @@ test "insert and find" { }); defer pool.deinit(testing.allocator); - const addr = try std.net.Address.parseIp("127.0.0.1", 5683); + const addr = try std.Io.net.IpAddress.parse("127.0.0.1", 5683); const key = Exchange.peerKey(addr, 0x1234); try testing.expect(pool.find(key) == null); @@ -372,7 +365,7 @@ test "duplicate detection" { }); defer pool.deinit(testing.allocator); - const addr = try std.net.Address.parseIp("127.0.0.1", 5683); + const addr = try std.Io.net.IpAddress.parse("127.0.0.1", 5683); const key = Exchange.peerKey(addr, 0xAAAA); _ = pool.insert(key, 0, 0xAAAA, "first", 0); @@ -395,7 +388,7 @@ test "pool exhaustion" { }); defer pool.deinit(testing.allocator); - const addr = try std.net.Address.parseIp("127.0.0.1", 5683); + const addr = try std.Io.net.IpAddress.parse("127.0.0.1", 5683); const k1 = Exchange.peerKey(addr, 1); const k2 = Exchange.peerKey(addr, 2); const k3 = Exchange.peerKey(addr, 3); @@ -413,7 +406,7 @@ test "evict expired" { }); defer pool.deinit(testing.allocator); - const addr = try std.net.Address.parseIp("127.0.0.1", 5683); + const addr = try std.Io.net.IpAddress.parse("127.0.0.1", 5683); const k1 = Exchange.peerKey(addr, 1); const k2 = Exchange.peerKey(addr, 2); @@ -444,7 +437,7 @@ test "remove is public" { }); defer pool.deinit(testing.allocator); - const addr = try std.net.Address.parseIp("127.0.0.1", 5683); + const addr = try std.Io.net.IpAddress.parse("127.0.0.1", 5683); const key = Exchange.peerKey(addr, 0x0001); const slot = pool.insert(key, 0, 0x0001, "data", 0); @@ -536,8 +529,8 @@ test "evictPeer removes all exchanges for a peer" { }); defer pool.deinit(testing.allocator); - const addr_a = try std.net.Address.parseIp("10.0.0.1", 5683); - const addr_b = try std.net.Address.parseIp("10.0.0.2", 5683); + const addr_a = try std.Io.net.IpAddress.parse("10.0.0.1", 5683); + const addr_b = try std.Io.net.IpAddress.parse("10.0.0.2", 5683); const ak_a = Exchange.addrHash(addr_a); const ak_b = Exchange.addrHash(addr_b); @@ -555,19 +548,19 @@ test "evictPeer removes all exchanges for a peer" { } test "addrHash differentiates IPv4 and IPv6" { - const v4 = try std.net.Address.parseIp("127.0.0.1", 5683); - const v6 = try std.net.Address.parseIp("::1", 5683); + const v4 = try std.Io.net.IpAddress.parse("127.0.0.1", 5683); + const v6 = try std.Io.net.IpAddress.parse("::1", 5683); try testing.expect(Exchange.addrHash(v4) != Exchange.addrHash(v6)); } test "peerKey differentiates IPv4 and IPv6" { - const v4 = try std.net.Address.parseIp("127.0.0.1", 5683); - const v6 = try std.net.Address.parseIp("::1", 5683); + const v4 = try std.Io.net.IpAddress.parse("127.0.0.1", 5683); + const v6 = try std.Io.net.IpAddress.parse("::1", 5683); try testing.expect(Exchange.peerKey(v4, 0x1234) != Exchange.peerKey(v6, 0x1234)); } test "addrHash different IPv6 addresses produce different hashes" { - const a = try std.net.Address.parseIp("::1", 5683); - const b = try std.net.Address.parseIp("fe80::1", 5683); + const a = try std.Io.net.IpAddress.parse("::1", 5683); + const b = try std.Io.net.IpAddress.parse("fe80::1", 5683); try testing.expect(Exchange.addrHash(a) != Exchange.addrHash(b)); } diff --git a/src/handler.zig b/src/handler.zig index 19d2abd..2d911fa 100644 --- a/src/handler.zig +++ b/src/handler.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const platform = @import("platform.zig"); const coapz = @import("coapz"); const Deferred = @import("deferred.zig"); const ObserverRegistry = @import("observe.zig"); @@ -31,7 +32,7 @@ pub const Request = struct { /// kind, raw option iteration). packet: coapz.Packet, /// Source address of the peer. - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, /// Per-request arena allocator. Resets after the handler returns. /// Use for temporary allocations needed during response construction /// (e.g. duping option slices via `Response.content()`). @@ -63,7 +64,7 @@ pub const Request = struct { /// Context for observe registration. Provided by the server. pub const ObserveContext = struct { registry: *ObserverRegistry, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, token: []const u8, }; @@ -88,7 +89,7 @@ pub const Request = struct { self.packet.token, self.peer_address, ctx.next_msg_id, - @truncate(std.time.nanoTimestamp()), + @truncate(platform.nanoTimestamp()), ) orelse return null; return .{ .pool = ctx.pool, .slot_idx = idx }; } @@ -293,7 +294,7 @@ pub const Response = struct { pub fn withEcho(self: Response, arena: std.mem.Allocator) Response { const echo_kind: coapz.OptionKind = @enumFromInt(252); var echo_val: [8]u8 = undefined; - std.crypto.random.bytes(&echo_val); + platform.randomBytes(&echo_val); const echo_opt = coapz.Option{ .kind = echo_kind, .value = arena.dupe(u8, &echo_val) catch return self }; const existing = self.options; const opts = arena.alloc(coapz.Option, existing.len + 1) catch return self; @@ -312,7 +313,7 @@ pub const SimpleHandlerFn = *const fn (Request) ?Response; /// Trampoline for simple handlers: recovers the original function pointer /// from the type-erased context and calls it. pub fn wrapSimple(ctx: ?*anyopaque, request: Request) ?Response { - const func: SimpleHandlerFn = @ptrCast(ctx.?); + const func: SimpleHandlerFn = @ptrCast(@alignCast(ctx.?)); return func(request); } @@ -379,11 +380,17 @@ fn null_fallible_handler(_: Request) anyerror!?Response { } test "safeWrap converts error to 5.00" { + // This test deliberately triggers a handler warning; keep its expected + // stderr out of the build summary without silencing production logging. + const previous_log_level = testing.log_level; + testing.log_level = .err; + defer testing.log_level = previous_log_level; + const wrapped = safeWrap(failing_handler); const dummy_packet = std.mem.zeroes(coapz.Packet); const request = Request{ .packet = dummy_packet, - .peer_address = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683), + .peer_address = platform.ip4(.{ 127, 0, 0, 1 }, 5683), .arena = testing.allocator, }; const resp = wrapped(request); @@ -396,7 +403,7 @@ test "safeWrap passes through success" { const dummy_packet = std.mem.zeroes(coapz.Packet); const request = Request{ .packet = dummy_packet, - .peer_address = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683), + .peer_address = platform.ip4(.{ 127, 0, 0, 1 }, 5683), .arena = testing.allocator, }; const resp = wrapped(request); @@ -410,7 +417,7 @@ test "safeWrap passes through null" { const dummy_packet = std.mem.zeroes(coapz.Packet); const request = Request{ .packet = dummy_packet, - .peer_address = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683), + .peer_address = platform.ip4(.{ 127, 0, 0, 1 }, 5683), .arena = testing.allocator, }; const resp = wrapped(request); @@ -432,7 +439,7 @@ fn testRequest(code: coapz.Code, options: []const coapz.Option, body: []const u8 const parsed = try coapz.Packet.read(testing.allocator, wire); return .{ .packet = parsed, - .peer_address = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683), + .peer_address = platform.ip4(.{ 127, 0, 0, 1 }, 5683), .arena = testing.allocator, }; } diff --git a/src/observe.zig b/src/observe.zig index 3d1a205..c23d8a6 100644 --- a/src/observe.zig +++ b/src/observe.zig @@ -5,7 +5,7 @@ /// notifications. The tick loop drains the MPSC queue and sends NON /// notifications to all registered observers. const std = @import("std"); -const posix = std.posix; +const platform = @import("platform.zig"); const coapz = @import("coapz"); const handler = @import("handler.zig"); @@ -24,7 +24,7 @@ pub const Config = struct { pub const Observer = struct { active: bool = false, - peer_address: std.net.Address, + peer_address: std.Io.net.IpAddress, token: [8]u8, token_len: u8, /// Notifications sent to this observer (for CON interval). @@ -79,7 +79,7 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !ObserverRegistry { errdefer allocator.free(observers); for (observers) |*obs| { obs.* = .{ - .peer_address = std.mem.zeroes(std.net.Address), + .peer_address = platform.ip4(.{ 0, 0, 0, 0 }, 0), .token = .{0} ** 8, .token_len = 0, }; @@ -140,7 +140,7 @@ pub fn allocateResource(self: *ObserverRegistry) ?u16 { /// Register a client as an observer of a resource. Idempotent — if the /// same peer+token is already registered, returns true without duplicating. -pub fn addObserver(self: *ObserverRegistry, resource_id: u16, peer: std.net.Address, token: []const u8) bool { +pub fn addObserver(self: *ObserverRegistry, resource_id: u16, peer: std.Io.net.IpAddress, token: []const u8) bool { if (resource_id >= self.resources.len or !self.resources[resource_id].active) return false; const base = @as(usize, resource_id) * self.observers_per_resource; @@ -176,7 +176,7 @@ pub fn addObserver(self: *ObserverRegistry, resource_id: u16, peer: std.net.Addr } /// Remove a specific observer by peer address and token. -pub fn removeObserver(self: *ObserverRegistry, resource_id: u16, peer: std.net.Address, token: []const u8) void { +pub fn removeObserver(self: *ObserverRegistry, resource_id: u16, peer: std.Io.net.IpAddress, token: []const u8) void { if (resource_id >= self.resources.len or !self.resources[resource_id].active) return; const base = @as(usize, resource_id) * self.observers_per_resource; @@ -195,7 +195,7 @@ pub fn removeObserver(self: *ObserverRegistry, resource_id: u16, peer: std.net.A } /// Remove all observers from a given peer address (e.g. on RST). -pub fn removeByPeer(self: *ObserverRegistry, peer: std.net.Address) void { +pub fn removeByPeer(self: *ObserverRegistry, peer: std.Io.net.IpAddress) void { for (self.resources, 0..) |*res, ri| { if (!res.active) continue; const base = ri * self.observers_per_resource; @@ -309,7 +309,7 @@ pub fn notifyBuf(self: *const ObserverRegistry, slot: u16) []u8 { } /// Clear pending CON state when an ACK is received for a CON notification. -pub fn ackConNotification(self: *ObserverRegistry, msg_id: u16, peer: std.net.Address) void { +pub fn ackConNotification(self: *ObserverRegistry, msg_id: u16, peer: std.Io.net.IpAddress) void { for (self.observers) |*obs| { if (!obs.active) continue; if (obs.pending_con_msg_id != msg_id) continue; @@ -355,13 +355,8 @@ pub fn decodeNotifyMeta(data: []const u8, arena: std.mem.Allocator) ?NotifyMeta }; } -fn addrEqual(a: std.net.Address, b: std.net.Address) bool { - if (a.any.family != b.any.family) return false; - return switch (a.any.family) { - posix.AF.INET => std.mem.eql(u8, std.mem.asBytes(&a.in), std.mem.asBytes(&b.in)), - posix.AF.INET6 => std.mem.eql(u8, std.mem.asBytes(&a.in6), std.mem.asBytes(&b.in6)), - else => false, - }; +fn addrEqual(a: std.Io.net.IpAddress, b: std.Io.net.IpAddress) bool { + return platform.addressEqual(a, b); } // ── Tests ── @@ -401,7 +396,7 @@ test "add and remove observer" { defer reg.deinit(testing.allocator); const rid = reg.allocateResource() orelse return error.Full; - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); try testing.expect(reg.addObserver(rid, addr, &.{ 0xAA, 0xBB })); try testing.expectEqual(@as(u16, 1), reg.resources[rid].observer_count.load(.acquire)); @@ -418,7 +413,7 @@ test "duplicate observer is idempotent" { defer reg.deinit(testing.allocator); const rid = reg.allocateResource() orelse return error.Full; - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); try testing.expect(reg.addObserver(rid, addr, &.{0xAA})); try testing.expect(reg.addObserver(rid, addr, &.{0xAA})); // duplicate @@ -434,8 +429,8 @@ test "removeByPeer removes all observers for that peer" { const r0 = reg.allocateResource() orelse return error.Full; const r1 = reg.allocateResource() orelse return error.Full; - const addr1 = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); - const addr2 = std.net.Address.initIp4(.{ 10, 0, 0, 1 }, 5683); + const addr1 = platform.ip4(.{ 127, 0, 0, 1 }, 5683); + const addr2 = platform.ip4(.{ 10, 0, 0, 1 }, 5683); _ = reg.addObserver(r0, addr1, &.{0x01}); _ = reg.addObserver(r1, addr1, &.{0x02}); @@ -456,7 +451,7 @@ test "notify enqueues and drain processes" { defer reg.deinit(testing.allocator); const rid = reg.allocateResource() orelse return error.Full; - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); _ = reg.addObserver(rid, addr, &.{0xAA}); reg.notify(rid, .{ .code = .content, .payload = "22.5" }); @@ -476,7 +471,7 @@ test "notify increments sequence number" { defer reg.deinit(testing.allocator); const rid = reg.allocateResource() orelse return error.Full; - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683); + const addr = platform.ip4(.{ 127, 0, 0, 1 }, 5683); _ = reg.addObserver(rid, addr, &.{0xAA}); reg.notify(rid, .{ .code = .content, .payload = "v1" }); diff --git a/src/platform.zig b/src/platform.zig new file mode 100644 index 0000000..3bbb1e7 --- /dev/null +++ b/src/platform.zig @@ -0,0 +1,237 @@ +//! Linux syscall boundary for the io_uring backend. Public addresses use std.Io types. +const std = @import("std"); +const linux = std.os.linux; +pub const AF = linux.AF; +pub const SOCK = linux.SOCK; +pub const SO = linux.SO; +pub const SOL = linux.SOL; +pub const POLL = linux.POLL; +pub const SIG = linux.SIG; +pub const fd_t = linux.fd_t; +pub const socket_t = fd_t; +pub const pid_t = linux.pid_t; +pub const timeval = linux.timeval; +pub const iovec = std.posix.iovec; +pub const pollfd = linux.pollfd; +pub const poll = std.posix.poll; +pub const mmap = std.posix.mmap; +pub const munmap = std.posix.munmap; +pub const kill = std.posix.kill; + +pub const SocketAddress = extern union { + any: linux.sockaddr, + ip4: linux.sockaddr.in, + ip6: linux.sockaddr.in6, + + pub fn init(a: std.Io.net.IpAddress) SocketAddress { + return switch (a) { + .ip4 => |v| .{ .ip4 = .{ .port = std.mem.nativeToBig(u16, v.port), .addr = @bitCast(v.bytes) } }, + .ip6 => |v| .{ .ip6 = .{ .port = std.mem.nativeToBig(u16, v.port), .addr = v.bytes, .flowinfo = std.mem.nativeToBig(u32, v.flow), .scope_id = v.interface.index } }, + }; + } + + pub fn len(a: *const SocketAddress) linux.socklen_t { + return if (a.any.family == AF.INET) @sizeOf(linux.sockaddr.in) else @sizeOf(linux.sockaddr.in6); + } + + pub fn bytes(a: *const SocketAddress) []const u8 { + return std.mem.asBytes(a)[0..a.len()]; + } + + pub fn address(a: *const SocketAddress) !std.Io.net.IpAddress { + return switch (a.any.family) { + AF.INET => .{ .ip4 = .{ .port = std.mem.bigToNative(u16, a.ip4.port), .bytes = @bitCast(a.ip4.addr) } }, + AF.INET6 => .{ .ip6 = .{ .port = std.mem.bigToNative(u16, a.ip6.port), .bytes = a.ip6.addr, .flow = std.mem.bigToNative(u32, a.ip6.flowinfo), .interface = .{ .index = a.ip6.scope_id } } }, + else => error.UnsupportedAddressFamily, + }; + } +}; + +/// Preserve numeric IPv6 scope IDs in string-based client/server configs. +/// Native parse requires an I/O context to resolve scopes, even numeric ones. +pub fn parseIp(text: []const u8, port: u16) !std.Io.net.IpAddress { + if (std.mem.indexOfScalar(u8, text, '%')) |separator| { + var address = try std.Io.net.IpAddress.parseIp6(text[0..separator], port); + const scope = std.fmt.parseInt(u32, text[separator + 1 ..], 10) catch return error.InvalidScopeId; + address.ip6.interface = .{ .index = scope }; + return address; + } + return std.Io.net.IpAddress.parse(text, port); +} + +pub fn ip4(bytes: [4]u8, port: u16) std.Io.net.IpAddress { + return .{ .ip4 = .{ .bytes = bytes, .port = port } }; +} +pub fn family(a: std.Io.net.IpAddress) u32 { + return switch (a) { + .ip4 => AF.INET, + .ip6 => AF.INET6, + }; +} +/// Match the complete endpoint, including IPv6 scope and flow information. +/// std.Io.net.IpAddress.eql currently compares only address bytes and port. +pub fn addressEqual(a: std.Io.net.IpAddress, b: std.Io.net.IpAddress) bool { + if (!a.eql(&b)) return false; + return switch (a) { + .ip4 => true, + .ip6 => |v| v.flow == b.ip6.flow and v.interface.index == b.ip6.interface.index, + }; +} + +pub fn addressLen(a: std.Io.net.IpAddress) linux.socklen_t { + return switch (a) { + .ip4 => @sizeOf(linux.sockaddr.in), + .ip6 => @sizeOf(linux.sockaddr.in6), + }; +} + +const SocketError = error{ WouldBlock, ConnectionRefused, ConnectionResetByPeer, NetworkUnreachable, MessageTooBig, AddressInUse, AccessDenied, SystemResources, Unexpected }; +fn result(rc: usize) SocketError!usize { + return switch (linux.errno(rc)) { + .SUCCESS => rc, + .AGAIN => error.WouldBlock, + .CONNREFUSED => error.ConnectionRefused, + .CONNRESET => error.ConnectionResetByPeer, + .NETUNREACH, .HOSTUNREACH => error.NetworkUnreachable, + .MSGSIZE => error.MessageTooBig, + .ADDRINUSE => error.AddressInUse, + .ACCES, .PERM => error.AccessDenied, + .NOMEM, .NOBUFS, .MFILE, .NFILE => error.SystemResources, + else => error.Unexpected, + }; +} +pub fn socket(domain: u32, kind: u32, protocol: u32) SocketError!fd_t { + return @intCast(try result(linux.socket(domain, kind, protocol))); +} +pub fn close(fd: fd_t) void { + _ = linux.close(fd); +} +pub fn connect(fd: fd_t, a: std.Io.net.IpAddress) SocketError!void { + const sa = SocketAddress.init(a); + while (true) { + const rc = linux.connect(fd, &sa.any, sa.len()); + if (linux.errno(rc) == .INTR) continue; + _ = try result(rc); + return; + } +} +pub fn bind(fd: fd_t, a: std.Io.net.IpAddress) SocketError!void { + const sa = SocketAddress.init(a); + _ = try result(linux.bind(fd, &sa.any, sa.len())); +} +pub fn setsockopt(fd: fd_t, level: i32, option: u32, value: []const u8) SocketError!void { + _ = try result(linux.setsockopt(fd, level, option, value.ptr, @intCast(value.len))); +} +pub fn send(fd: fd_t, data: []const u8, flags: u32) SocketError!usize { + return sendto(fd, data, flags, null); +} +pub fn sendto(fd: fd_t, data: []const u8, flags: u32, a: ?std.Io.net.IpAddress) SocketError!usize { + const sa = if (a) |v| SocketAddress.init(v) else undefined; + while (true) { + const rc = linux.sendto(fd, data.ptr, data.len, flags, if (a != null) &sa.any else null, if (a != null) sa.len() else 0); + if (linux.errno(rc) == .INTR) continue; + return result(rc); + } +} +pub fn recv(fd: fd_t, data: []u8, flags: u32) SocketError!usize { + while (true) { + const rc = linux.recvfrom(fd, data.ptr, data.len, flags, null, null); + if (linux.errno(rc) == .INTR) continue; + return result(rc); + } +} + +/// CLOCK_MONOTONIC keeps retransmission deadlines immune to wall-clock changes. +pub fn nanoTimestamp() i128 { + var ts: linux.timespec = undefined; + if (linux.errno(linux.clock_gettime(.MONOTONIC, &ts)) != .SUCCESS) @panic("clock_gettime failed"); + return @as(i128, ts.sec) * std.time.ns_per_s + ts.nsec; +} +pub fn sleep(ns: u64) void { + var ts: linux.timespec = .{ .sec = @intCast(ns / std.time.ns_per_s), .nsec = @intCast(ns % std.time.ns_per_s) }; + while (true) { + const rc = linux.nanosleep(&ts, &ts); + switch (linux.errno(rc)) { + .SUCCESS => return, + .INTR => continue, + else => @panic("nanosleep failed"), + } + } +} +pub fn randomBytes(buffer: []u8) void { + var offset: usize = 0; + while (offset < buffer.len) { + const rc = linux.getrandom(buffer[offset..].ptr, buffer.len - offset, 0); + switch (linux.errno(rc)) { + .SUCCESS => { + if (rc == 0) @panic("getrandom returned zero"); + offset += rc; + }, + .INTR => continue, + else => @panic("getrandom failed"), + } + } +} +pub fn randomInt(comptime T: type) T { + var value: T = undefined; + randomBytes(std.mem.asBytes(&value)); + return value; +} + +// The benchmark intentionally forks so shared counters measure server throughput. +pub fn fork() SocketError!pid_t { + return @intCast(try result(linux.fork())); +} +pub fn open(path: [:0]const u8, flags: linux.O, mode: linux.mode_t) SocketError!fd_t { + return @intCast(try result(linux.open(path.ptr, flags, mode))); +} +pub fn dup2(old: fd_t, new: fd_t) SocketError!void { + _ = try result(linux.dup2(old, new)); +} +pub fn waitpid(pid: pid_t, flags: u32) void { + var status: u32 = undefined; + while (linux.errno(linux.waitpid(pid, &status, flags)) == .INTR) {} +} + +test "UDP syscall boundary reports WouldBlock and transfers datagrams" { + const receiver = try socket(AF.INET, SOCK.DGRAM | SOCK.NONBLOCK, 0); + defer close(receiver); + try bind(receiver, ip4(.{ 127, 0, 0, 1 }, 0)); + var sa: SocketAddress = undefined; + var len: linux.socklen_t = @sizeOf(SocketAddress); + _ = try result(linux.getsockname(receiver, &sa.any, &len)); + const destination = try sa.address(); + var buffer: [32]u8 = undefined; + try std.testing.expectError(error.WouldBlock, recv(receiver, &buffer, 0)); + const sender = try socket(AF.INET, SOCK.DGRAM, 0); + defer close(sender); + try connect(sender, destination); + try std.testing.expectEqual(@as(usize, 4), try send(sender, "ping", 0)); + var fds = [_]pollfd{.{ .fd = receiver, .events = POLL.IN, .revents = 0 }}; + try std.testing.expectEqual(@as(usize, 1), try poll(&fds, 1000)); + const received = try recv(receiver, &buffer, 0); + try std.testing.expectEqualStrings("ping", buffer[0..received]); +} + +test "native addresses round-trip through Linux socket representation" { + for ([_][]const u8{ "127.0.0.1", "::1", "fe80::1" }) |text| { + const a = try std.Io.net.IpAddress.parse(text, 5683); + const sa = SocketAddress.init(a); + const b = try sa.address(); + try std.testing.expect(a.eql(&b)); + } + var scoped = try parseIp("fe80::1%3", 5684); + try std.testing.expectEqual(@as(u32, 3), scoped.ip6.interface.index); + try std.testing.expectError(error.InvalidScopeId, parseIp("fe80::1%", 5684)); + try std.testing.expectError(error.InvalidScopeId, parseIp("fe80::1%4294967296", 5684)); + scoped.ip6.flow = 42; + const sa = SocketAddress.init(scoped); + const roundtrip = try sa.address(); + try std.testing.expect(addressEqual(scoped, roundtrip)); + var other_scope = scoped; + other_scope.ip6.interface.index = 4; + try std.testing.expect(!addressEqual(scoped, other_scope)); + var other_flow = scoped; + other_flow.ip6.flow = 43; + try std.testing.expect(!addressEqual(scoped, other_flow)); +} diff --git a/src/rate_limiter.zig b/src/rate_limiter.zig index c10c476..4b43060 100644 --- a/src/rate_limiter.zig +++ b/src/rate_limiter.zig @@ -3,7 +3,8 @@ /// Pattern follows exchange.zig: linear probing, backward-shift deletion, /// intrusive free list. Clock-hand eviction when the table is full. const std = @import("std"); -const posix = std.posix; +const platform = @import("platform.zig"); +const posix = platform; const RateLimiter = @This(); @@ -24,20 +25,20 @@ pub const AddrKey = struct { pub const zero: AddrKey = .{ .family = 0, .addr = .{0} ** 16 }; - pub fn fromAddress(address: std.net.Address) AddrKey { - return switch (address.any.family) { + pub fn fromAddress(address: std.Io.net.IpAddress) AddrKey { + return switch (platform.family(address)) { posix.AF.INET => .{ .family = posix.AF.INET, .addr = blk: { var a: [16]u8 = .{0} ** 16; - const src: [4]u8 = @bitCast(address.in.sa.addr); + const src = address.ip4.bytes; @memcpy(a[0..4], &src); break :blk a; }, }, posix.AF.INET6 => .{ .family = posix.AF.INET6, - .addr = address.in6.sa.addr, + .addr = address.ip6.bytes, }, else => zero, }; @@ -457,8 +458,8 @@ test "IPv6 addresses independent from IPv4" { }); defer rl.deinit(testing.allocator); - const v4 = AddrKey.fromAddress(try std.net.Address.parseIp("127.0.0.1", 5683)); - const v6 = AddrKey.fromAddress(try std.net.Address.parseIp("::1", 5683)); + const v4 = AddrKey.fromAddress(try std.Io.net.IpAddress.parse("127.0.0.1", 5683)); + const v6 = AddrKey.fromAddress(try std.Io.net.IpAddress.parse("::1", 5683)); const now: i64 = 1_000_000_000; try testing.expect(rl.allow(v4, now)); diff --git a/src/root.zig b/src/root.zig index 361bfc4..0b9a578 100644 --- a/src/root.zig +++ b/src/root.zig @@ -26,7 +26,7 @@ //! } //! //! pub fn main() !void { -//! var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +//! var gpa = std.heap.DebugAllocator(.{}){}; //! var server = try coap.Server.init(gpa.allocator(), .{}, echo); //! defer server.deinit(); //! try server.run(); @@ -39,7 +39,7 @@ //! const coap = @import("coap"); //! //! pub fn main() !void { -//! var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +//! var gpa = std.heap.DebugAllocator(.{}){}; //! const allocator = gpa.allocator(); //! //! var client = try coap.Client.init(allocator, .{ @@ -116,6 +116,8 @@ pub const ContentFormat = coap.ContentFormat; const handler = @import("handler.zig"); const constants = @import("constants.zig"); const Io = @import("Io.zig"); +/// Linux backend support, also used by the benchmark. +pub const platform = @import("platform.zig"); const Exchange = @import("exchange.zig"); const RateLimiter = @import("rate_limiter.zig"); const Deferred = @import("deferred.zig"); @@ -137,6 +139,7 @@ test { _ = handler; _ = constants; _ = Io; + _ = platform; _ = Server; _ = Client; _ = Exchange; diff --git a/src/router.zig b/src/router.zig index ecbf2c8..d27c4d2 100644 --- a/src/router.zig +++ b/src/router.zig @@ -14,6 +14,7 @@ /// var server = try coap.Server.init(allocator, .{}, router.handler()); /// ``` const std = @import("std"); +const platform = @import("platform.zig"); const coapz = @import("coapz"); const handler_mod = @import("handler.zig"); const Request = handler_mod.Request; @@ -175,7 +176,7 @@ fn buildRequest(comptime method: coapz.Code, comptime path: []const u8) Request .payload = "test", .data_buf = &.{}, }, - .peer_address = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 5683), + .peer_address = platform.ip4(.{ 127, 0, 0, 1 }, 5683), .arena = testing.allocator, }; }