From 0c2c67b463d6a90aaa0bb69bf3c91dba1d9ec3ec Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 17 May 2026 16:39:31 +0530 Subject: feat: per-client NAT filtering via LWIP_HOOK_IP4_CANFORWARD - Add lwip_tollgate_hooks.h defining LWIP_HOOK_IP4_CANFORWARD macro - Inject hook into lwIP build via CMakeLists.txt ESP_IDF_LWIP_HOOK_FILENAME - Filter forwarded packets by source IP against firewall allowed list - Only filter packets from AP subnet (10.192.45.0/24), allow all others - Fix byte order bug: use network byte order for firewall_is_client_allowed - NAT always enabled, removed global NAT toggle functions - Remove spent-secret tracking from session.c (mint is authority) - Remove unused get_ap_netif() function - Reduce API server stack from 32KB to 16KB (fixes ESP_ERR_HTTPD_TASK) - Add esp_random.h stub for unit tests - All 186 unit tests passing - Verified on hardware: block->pay->allow->revoke->block E2E works --- CHECKLIST.md | 382 +++++++++++----------------------------- CMakeLists.txt | 4 + PLAN.md | 75 +++++++- main/firewall.c | 51 ++---- main/firewall.h | 6 +- main/lwip_tollgate_hooks.h | 10 ++ main/session.c | 48 +---- main/session.h | 10 +- main/tollgate_api.c | 25 +-- main/tollgate_main.c | 1 - tests/unit/stubs/esp_random.h | 6 + tests/unit/test_identity | Bin 296728 -> 297880 bytes tests/unit/test_mcp_handler | Bin 0 -> 38736 bytes tests/unit/test_nip04 | Bin 0 -> 298776 bytes tests/unit/test_session.c | 37 ++-- tests/unit/test_tollgate_client | Bin 51904 -> 51992 bytes 16 files changed, 237 insertions(+), 418 deletions(-) create mode 100644 main/lwip_tollgate_hooks.h create mode 100644 tests/unit/stubs/esp_random.h create mode 100755 tests/unit/test_mcp_handler create mode 100755 tests/unit/test_nip04 diff --git a/CHECKLIST.md b/CHECKLIST.md index 5cedd30..b71bd14 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -43,34 +43,10 @@ - [x] Makefile: nutshell wallet targets (wallet-setup, wallet-info, mint-token, send-token) - [x] `tests/phase2.mjs`: `/whoami` test checks `includes('mac=')` -### Infrastructure -- [x] Upstream gateway on enx00e04c633a90 (192.168.2.0/24, metric 101, default route) -- [x] WiFi wlp59s0 free for ESP32 TollGate connection -- [x] Mint URL verified: `testnut.cashu.space` works (auto-pays invoices) - ### Tests Passing -- [x] Test 15: Advertisement valid (kind=10021 with price_per_step) — PASSING -- [x] Test 16: Valid payment (POST :2121/ with valid Cashu token → kind=1022 session) — PASSING -- [x] Test 17: Usage tracking after payment (GET :2121/usage → active usage) — PASSING -- [x] Test 18: Internet after payment (ping through TollGate works) — PASSING -- [x] Test 19: Invalid token rejected (POST garbage → 400, kind=21023) — PASSING -- [x] Test 20: Spent token rejected (reuse token → kind=21023) — PASSING -- [x] Test 21: Wrong mint rejected (POST token from wrong mint → kind=21023) — PASSING -- [x] Test 22: Session expiry (wait for allotment → internet blocked) — PASSING -- [x] Test 23: Session renewal (second payment → allotment extended) — PASSING -- [x] Test: /whoami returns ip=X.X.X.X mac=XX:XX:XX:XX:XX:XX — PASSING -- [x] Test: Portal has payment form (Cashu token input + Pay button) — PASSING - -### Captive Portal Detection Fix -- [x] Added DoT reject server on port 853 (TCP RST forces DNS fallback to port 53) -- [x] DNS hijack now returns NXDOMAIN for ALL non-A query types (prevents DNS leaks) -- [x] Shorter TTL on hijack responses (10s) for faster detection -- [x] Explicit 302 redirect handlers for all captive detection URIs (/generate_204, /hotspot-detect.html, etc.) -- [x] HTTP request logging for captive detection endpoints -- [x] DNS query logging for unauthenticated clients -- [x] Verified working with GrapheneOS phone (commit `236b61d`) +- [x] Tests 15-24: ALL PASSING -## Phase 3: On-Device Wallet + Nostr Identity + Wifistr — IN PROGRESS +## Phase 3: On-Device Wallet + Nostr Identity + Wifistr — COMPLETE ### nucula Wallet Integration - [x] Add nucula as git submodule (`nucula_src/`) - [x] Create `components/secp256k1/` (symlink to nucula's libsecp256k1) @@ -79,271 +55,125 @@ - [x] All wallet operations tested on Board A: pay, swap, send, persistence ### Nostr Identity Derivation (identity.c/h) -- [x] Create `identity.h` — API: `identity_init(nsec_hex)`, derived value accessors -- [x] Create `identity.c` — HMAC-SHA512 derivation via mbedtls, npub via secp256k1 -- [x] Derive STA MAC: `tollgate_derive(nsec, "sta-mac", 0)` → 6 bytes, locally administered -- [x] Derive AP MAC: `tollgate_derive(nsec, "ap-mac", 0)` → 6 bytes, locally administered -- [x] Derive SSID: `"TollGate-" + hex(AP_MAC[3:6])` -- [x] Derive AP IP: hash-based from AP MAC bytes -- [x] Compute npub: secp256k1 x-only pubkey from nsec +- [x] HMAC-SHA512 derivation via mbedtls, npub via secp256k1 +- [x] Derive STA/AP MAC, SSID, AP IP from nsec - [x] Set MACs via `esp_wifi_set_mac()` in boot sequence +- [x] 24/24 unit tests passing ### Nostr Event Signing (nostr_event.c/h) -- [x] Create `nostr_event.h` — NIP-01 event struct + sign/serialize API -- [x] Create `nostr_event.c` — canonical JSON, SHA-256 ID, Schnorr signature -- [x] Uses `secp256k1_schnorrsig_sign32()` for BIP-340 signatures +- [x] NIP-01 canonical JSON, SHA-256 ID, Schnorr signature +- [x] 23/23 unit tests passing ### Geohash Encoding (geohash.c/h) -- [x] Create `geohash.h` — `geohash_encode(lat, lon, precision, out)` -- [x] Create `geohash.c` — standard base-32 geohash encoding +- [x] Standard base-32 geohash encoding +- [x] 11/11 unit tests passing ### Wifistr Service Discovery (wifistr.c/h) -- [x] Create `wifistr.h` — `wifistr_publish()` API -- [x] Create `wifistr.c` — kind 38787 event builder + WebSocket relay publish -- [x] Build event with tags: d, ssid, h, security, g, c -- [x] WebSocket client: raw TCP + TLS (esp_tls.h) + HTTP Upgrade +- [x] kind 38787 event builder + WebSocket relay publish - [x] Publish on boot + periodic timer (6h default) - -### Config Changes (config.c/h) -- [x] Add to struct: nsec, npub, nostr_geohash, nostr_relays, nostr_publish_interval_s, sta_mac, ap_mac -- [x] Remove from JSON parsing: ap_ssid, ap_ip (now derived from nsec) -- [x] Keep: ap_password, ap_channel, ap_max_conn (hardcoded defaults) -- [x] Update default config.json template with nsec and Nostr fields - -### Boot Sequence Changes (tollgate_main.c) -- [x] Call `identity_init(nsec)` after config load, before WiFi init -- [x] Set STA/AP MAC via `esp_wifi_set_mac()` after `esp_wifi_init()`, before `esp_wifi_start()` -- [x] Remove old `tollgate_config_derive_unique()` call -- [x] Use derived SSID/IP in AP configuration -- [x] Start wifistr publish task after services start - -### Build System -- [x] Add identity.c, nostr_event.c, geohash.c, wifistr.c to CMakeLists.txt SRCS -- [x] Add `secp256k1` to REQUIRES (for identity.c and nostr_event.c) -- [x] Clean build (0 errors, 0 warnings) - -### Hardware Testing -- [x] Flash Board A, verify wallet boot (keyset fetch succeeds) -- [x] Pay Board A with Cashu token, verify proofs stored (GET /wallet) -- [x] Test POST /wallet/swap on Board A -- [x] Test POST /wallet/send on Board A, verify token is valid -- [x] Flash Board A with new identity derivation, verify derived SSID/MAC/IP -- [x] Verify captive portal works with new SSID/IP -- [x] Verify payment flow still works with identity-derived config -- [x] Verify wifistr event published to relay (damus + nos.lol) -- [ ] Flash Board B with new firmware (different nsec) -- [ ] Cross-board payment: Board B token → Board A -- [ ] Verify both boards show correct balances after cross-board payment - -### Tests 25-27 (deferred from Phase 2, need Board B) -- [ ] Test 25: Two clients pay independently (laptop + Board B) -- [ ] Test 26: Client isolation (only payer gets internet) -- [ ] Test 27: Full e2e: portal → pay → browse - -### Tests 28-38 (Phase 3 specific) -- [ ] Test 28: Wallet boot (keysets loaded) -- [ ] Test 29: Receive via wallet (balance incremented) -- [ ] Test 30: Wallet swap (same balance, new proofs) -- [ ] Test 31: Wallet send (valid cashuA token) -- [ ] Test 32: Persistence survives reboot -- [ ] Test 33: Cross-board payment -- [ ] Test 34: 5 consecutive payments -- [ ] Test 35: Stress: rapid pay/expire - -### Automated Tests -- [ ] Write tests/phase3.mjs (wallet endpoint tests + cross-board) -- [ ] All Phase 3 tests passing - -## Test Coverage — IN PROGRESS - -### Host Unit Tests (tests/unit/) -- [ ] Create `tests/unit/stubs/` — clean ESP-IDF type stubs for host compilation -- [ ] Create `tests/unit/Makefile` — compiles all unit tests with host gcc -- [ ] Install system deps: `libmbedtls-dev`, `libcjson-dev` -- [ ] `test_geohash.c` — geohash_encode against reference vectors (Munich, NYC, origin) -- [ ] `test_identity.c` — HMAC-SHA512 derivation, MAC bits, SSID/IP determinism -- [ ] `test_nostr_event.c` — NIP-01 event ID, Schnorr sign+verify, JSON serialization -- [ ] `test_cashu.c` — token decode, allotment calc, mint validation -- [ ] `test_session.c` — session lifecycle, expiry, spent-secret dedup -- [ ] `make test-unit` passes all unit tests +- [x] Verified published to relay.damus.io and nos.lol + +## Phase 4: ESP32 TollGate Client Detection + Auto-Payment — COMPLETE (commit `78dd599`) +- [x] tollgate_client.c/h — detection, payment, monitoring, state machine +- [x] Config fields: client_enabled, client_steps_to_buy, etc. +- [x] Integration into tollgate_main.c +- [x] 30/30 unit tests passing + +## Phase 5: Lightning Auto-Payout — COMPLETE (commit `cb4bd7d`) +- [x] lnurl_pay.c/h — LNURL-pay HTTP flow +- [x] lightning_payout.c/h — periodic balance check, threshold, multi-recipient split, melt +- [x] nucula_wallet_melt() bridge for NUT-05 +- [x] Config: payout.enabled, recipients, mints, fee_tolerance, etc. +- [x] 7/7 lnurl_pay + 11/11 lightning_payout = 18 unit tests passing + +## Phase 6: Bytes-Based Billing — COMPLETE (commit `edd125d`) +- [x] Dual-metric session support (milliseconds + bytes) +- [x] session_create_bytes(), session_add_bytes() +- [x] Config: metric, step_size_bytes +- [x] Discovery endpoint advertises correct metric +- [x] Unit tests: bytes session lifecycle, mixed metrics + +## Phase 7: MCP Handler + NIP-04 + CVM Server — COMPLETE (commit `fdf662f`) +- [x] mcp_handler.c/h — 4 tools (get_config, set_config, get_balance, wallet_send), 25 unit tests +- [x] nip04.c/h — AES-256-CBC + ECDH with 0x02 compressed pubkey prefix, 15 unit tests +- [x] cvm_server.c/h — Nostr DM listener skeleton with FreeRTOS task +- [x] Fixed NIP-04 IV bug: mbedtls_aes_crypt_cbc modifies IV in-place +- [x] Fixed missing esp_random.h include in nip04.c +- [x] 156 total unit tests passing across 10 test binaries + +## Bug Fixes — COMPLETE (commit `3342c8e`) +- [x] reset_auth_handler now calls session_revoke_all() before firewall_revoke_all() +- [x] Port 80 /usage shows real session data (remaining/total) instead of "0/0" +- [x] Config metric defaults to "milliseconds" (ESP32 can't track per-client bytes from NAT) +- [x] Fixed sys_evt stack overflow: deferred start_services() to dedicated 32KB task + +## Playwright Interop Tests — COMPLETE (commit `4fb44e7`) +- [x] 18/18 tests passing (11 ESP32 + 7 ESP32↔OpenWRT interop) +- [x] 7 screenshots generated +- [x] Double-spend rejection verified on live hardware + +--- + +## TODO — In Progress + +### Per-Client NAT Filtering (Multi-Client Fix) +- [ ] Create `main/lwip_tollgate_hooks.h` — LWIP_HOOK_IP4_CANFORWARD definition +- [ ] Update `CMakeLists.txt` — inject hook header into lwIP compilation +- [ ] Add `tollgate_ip4_canforward_filter()` to `firewall.c` — filter forwarded packets by source IP +- [ ] Change firewall strategy: NAT always ON, per-client filter in lwIP forwarding path +- [ ] Remove `update_nat()`, `firewall_enable_nat()`, `firewall_disable_nat()` from firewall.c +- [ ] Update `stop_services()` in tollgate_main.c — remove `firewall_disable_nat()` call +- [ ] Add unit test for filter function +- [ ] Build, flash, test on Board A +- [ ] Verify multi-client isolation: expire one client while other is active + +### Spent-Secret Cleanup +- [ ] Remove `s_spent_secrets[]` and `session_is_secret_spent()` from `session.c` +- [ ] Remove `spent_secrets` field from `session_t` struct in `session.h` +- [ ] Remove `spent_secrets` params from `session_create()` and `session_create_bytes()` +- [ ] Remove local spent-secret check in `tollgate_api.c` (lines 227-239) +- [ ] Remove `secrets[]` array construction in `tollgate_api.c` +- [ ] Update `tests/unit/test_session.c` — remove secret-tracking tests +- [ ] Run `make test-unit` — all tests pass + +### Integration Tests (tests/integration/) +- [ ] Create `tests/integration/` directory +- [ ] Move existing tests (api.mjs, network.mjs, smoke.mjs, phase2.mjs) into integration/ +- [ ] Write `test-reset-auth.mjs` — verify sessions cleared after reset +- [ ] Write `test-session-lifecycle.mjs` — pay → verify usage → wait expiry → verify blocked (65s) +- [ ] Write `test-dns-firewall.mjs` — DNS hijack before auth, forward after auth +- [ ] Update Makefile targets for new paths +- [ ] All integration tests passing ### Test Reorganization -- [ ] Move `tests/api.mjs` → `tests/integration/phase1_api.mjs` -- [ ] Move `tests/network.mjs` → `tests/integration/phase1_network.mjs` -- [ ] Move `tests/smoke.mjs` → `tests/integration/smoke.mjs` -- [ ] Move `tests/phase2.mjs` → `tests/integration/phase2.mjs` -- [ ] Move `tests/captive-portal.spec.mjs` → `tests/e2e/captive-portal.spec.mjs` -- [ ] Move `tests/playwright.config.mjs` → `tests/e2e/playwright.config.mjs` -- [ ] Fix all hardcoded IPs (`192.168.4.1`) → `process.env.TOLLGATE_IP` - -### New Integration Tests -- [ ] `tests/integration/phase3.mjs` — wallet GET/swap/send, identity SSID/IP, wifistr on relay -- [ ] All Phase 3 integration tests passing - -### New E2E Tests -- [ ] `tests/e2e/payment.spec.mjs` — paste token → pay → success, error handling, full flow -- [ ] All E2E tests passing - -### Build System Updates -- [ ] Update `Makefile` with `test-unit`, `test-integration`, `test-e2e`, `test-all` targets -- [ ] Update `package.json` npm scripts for new paths -- [ ] All `make test-*` targets work - -## Phase 4: ESP32 TollGate Client Detection + Auto-Payment — IN PROGRESS - -### tollgate_client.c/h (New) -- [x] Create `tollgate_client.h` — types: `tollgate_discovery_t`, `tollgate_client_state_t` enum (IDLE/DETECTING/NEEDS_PAY/PAYING/PAID/RENEWING) -- [x] Create `tollgate_client.c` — detection, payment, monitoring, state machine -- [x] `tollgate_client_detect(gw_ip)` — HTTP GET `http://{gw}:2121/`, parse kind=10021, extract price tags -- [x] `tollgate_client_pay(gw_ip, amount_sats)` — `nucula_wallet_send()` → POST to upstream → parse kind=1022/21023 -- [x] `tollgate_client_on_sta_connected()` — extract gw from DHCP, detect, pay (blocking) -- [x] `tollgate_client_tick()` — GET `/usage`, renew at 20% remaining -- [x] `tollgate_client_on_sta_disconnected()` — reset state -- [x] `tollgate_client_get_usage(gw_ip)` — GET `/usage` → parse remaining/total - -### Config Changes -- [x] Add to `config.h`: `client_enabled`, `client_steps_to_buy`, `client_renewal_threshold_pct`, `client_retry_interval_ms` -- [x] Parse new fields in `config.c` - -### Integration (tollgate_main.c) -- [x] Make wallet init synchronous (call `nucula_wallet_init()` directly, not as task) -- [x] Add `tollgate_client_on_sta_connected()` in `ip_event_handler` (blocking, before `start_services()`) -- [x] Add `tollgate_client_on_sta_disconnected()` in `wifi_event_handler` -- [x] Add `tollgate_client_tick()` in main loop -- [x] Update `main/CMakeLists.txt` — add `tollgate_client.c` - -### Unit Tests -- [x] `tests/unit/test_tollgate_client.c` — discovery parsing, price extraction, state machine, renewal threshold -- [x] All unit tests passing (30 new, 116 total) — committed at `78dd599` - -### Integration Tests -- [ ] ESP32→OpenWRT auto-payment (Scenario 4) -- [ ] ESP32→ESP32 auto-payment (Scenario 5, needs Board B) - -### Test Cases 39-43 -- [ ] Test 39: Client detection (kind=10021 parse) -- [ ] Test 40: Client payment flow (mock HTTP) -- [ ] Test 41: Session renewal (20% threshold) -- [ ] Test 42: ESP32→OpenWRT auto-pay -- [ ] Test 43: ESP32→ESP32 auto-pay - -## Phase 5: Lightning Auto-Payout — NOT STARTED - -### lnurl_pay.c/h (New) -- [ ] Create `lnurl_pay.h` — `lnurl_get_invoice(lightning_address, amount_sats, bolt11_out, out_size)` -- [ ] Create `lnurl_pay.c` — GET `.well-known/lnurlp/{user}` → parse callback → GET callback with amount → extract BOLT11 - -### lightning_payout.c/h (New) -- [ ] Create `lightning_payout.h` — `payout_recipient_t`, config, init/tick API -- [ ] Create `lightning_payout.c` — periodic balance check, threshold, multi-recipient split, melt with retry - -### nucula Bridge Extension -- [ ] Add `nucula_wallet_melt(bolt11, max_fee_sats)` to `nucula_wallet.h/cpp` -- [ ] Wraps `Wallet::request_melt_quote()` + `Wallet::melt_tokens()` (NUT-05) - -### Config Changes -- [ ] Add payout config to `config.h`: `payout_enabled`, `min_payout_amount`, `min_balance`, `fee_tolerance_pct`, `check_interval_s`, `recipients[]` -- [ ] Parse payout config in `config.c` - -### Integration (tollgate_main.c) -- [ ] Add periodic payout timer (60s interval) -- [ ] Update `main/CMakeLists.txt` - -### Unit Tests -- [ ] `tests/unit/test_lnurl_pay.c` — LNURL-pay URL construction, response parsing -- [ ] `tests/unit/test_lightning_payout.c` — threshold check, multi-recipient split, fee tolerance - -### Test Cases 44-48 -- [ ] Test 44: LNURL-pay flow -- [ ] Test 45: Payout threshold -- [ ] Test 46: Multi-recipient split -- [ ] Test 47: Melt with fee tolerance -- [ ] Test 48: Full payout cycle - -## Phase 6: Bytes-Based Billing — NOT STARTED - -### lwIP NAPT Stats Component (New) -- [ ] Create `components/lwip_napt_stats/` — patched `ip4_napt.c` with byte counters -- [ ] Add `uint64_t bytes_up/bytes_down` to `struct ip_napt_entry` -- [ ] Increment in `ip_napt_forward()` and `ip_napt_recv()` -- [ ] Add public API: `ip_napt_get_client_bytes(client_ip, &up, &down)` -- [ ] Create component CMakeLists.txt - -### Session Changes -- [ ] Add `allotment_bytes`, `bytes_consumed` to `session_t` -- [ ] Dual-metric `session_is_expired()` dispatches on metric type -- [ ] `session_add_bytes(client_ip, byte_count)` called from firewall counting - -### Config Changes -- [ ] Add `metric` field ("milliseconds" or "bytes") to `config.h` -- [ ] Add `step_size_bytes` to `config.h` -- [ ] Parse in `config.c` - -### TollGate API Changes -- [ ] Discovery endpoint advertises correct metric -- [ ] `/usage` returns byte-based or time-based values -- [ ] Allotment calculation dispatches on metric - -### Firewall Changes -- [ ] `firewall_count_traffic()` — queries NAPT byte counters per active client -- [ ] Called from `session_tick()` or main loop - -### Cashu Changes -- [ ] Unify `cashu_calculate_allotment()` for both metrics - -### Unit Tests -- [ ] `tests/unit/test_bytes_metric.c` — byte allotment calc, dual-metric session expiry - -### Test Cases 49-52 -- [ ] Test 49: Byte allotment calc -- [ ] Test 50: Byte session expiry -- [ ] Test 51: NAPT byte counting -- [ ] Test 52: Bytes metric end-to-end - -## Phase 7: ContextVM Server (MCP over Nostr) — NOT STARTED - -### NIP-44 Encryption (New) -- [ ] Create `nip44.h` — encrypt/decrypt API -- [ ] Create `nip44.c` — XChaCha20-Poly1305 + secp256k1 ECDH + conversation key derivation - -### MCP Handler (New) -- [ ] Create `mcp_handler.h` — tool registration, JSON-RPC parse/dispatch -- [ ] Create `mcp_handler.c` — register tools, handle requests, build responses - -### CVM Server (New) -- [ ] Create `cvm_server.h` — init/start/stop API -- [ ] Create `cvm_server.c` — WebSocket listener, DM subscription, NIP-44 decrypt, MCP dispatch - -### MCP Tool Registration -- [ ] `get_config`, `set_config`, `get_balance`, `get_sessions`, `get_usage` -- [ ] `set_payout`, `set_metric`, `set_price`, `wallet_send`, `wallet_melt` +- [ ] Fix all hardcoded IPs → `process.env.TOLLGATE_IP` +- [ ] Move `tests/captive-portal.spec.mjs` → `tests/e2e/` +- [ ] Move `tests/interop-happy-path.spec.mjs` → `tests/e2e/` or `tests/integration/` +- [ ] Move `tests/playwright.config.mjs` → `tests/e2e/` -### Auth -- [ ] Only accept commands from owner npub +### Playwright Video Recording Fix +- [ ] Per-test context isolation (not shared serial context) +- [ ] Verify `.webm` files generated in test-results/ -### Integration (tollgate_main.c) -- [ ] Start CVM server alongside wifistr -- [ ] Update `main/CMakeLists.txt` +### OpenWRT Interop +- [ ] Investigate `nofee.testnut.cashu.space` API compatibility issues +- [ ] Fix cashu CLI v0.19.2 Pydantic validation failures with missing `active` field -### Unit Tests -- [ ] `tests/unit/test_nip44.c` — encrypt/decrypt roundtrip -- [ ] `tests/unit/test_mcp_handler.c` — JSON-RPC parse, tool dispatch +### Board B +- [ ] Flash Board B with current firmware (different nsec) +- [ ] Cross-board payment test: Board B → Board A +- [ ] ESP32→ESP32 auto-payment (Scenario 5) -### Test Cases 53-56 -- [ ] Test 53: NIP-44 encrypt/decrypt -- [ ] Test 54: MCP JSON-RPC parse -- [ ] Test 55: Config change via DM -- [ ] Test 56: Balance query via CVM +--- ## Reminders -- Do NOT ask for instructions — proceed independently, skip blocked items, work on unblocked ones - **Commit + push every time a test passes that previously didn't pass** -- Board A: `/dev/ttyACM0`, factory MAC `94:a9:90:2e:37:7c` -- Board B: `/dev/ttyACM1`, factory MAC `fc:01:2c:c5:50:50` -- Identity is now derived from nsec in config.json (SSID, IP, MAC all deterministic) -- testnut.cashu.space auto-pays invoices: `cashu -h https://testnut.cashu.space invoice ` -- Token generation: `cashu -h https://testnut.cashu.space send --legacy 2>&1 | grep '^cashuA' | head -1` +- Board A: `/dev/ttyACM0`, MAC `94:a9:90:2e:37:7c`, SSID `TollGate-C0E9CA`, AP IP `10.192.45.1` +- Board B: `/dev/ttyACM1`, MAC `fc:01:2c:c5:50:50` +- OpenWRT Router: SSH `root@10.47.41.1`, port 2121 +- `source ~/esp/esp-idf/export.sh` before `idf.py` +- Latest commit: `3342c8e` +- 156 unit tests + 18 Playwright tests — all passing - sudo password: `c03rad0r123` -- Run `make test-unit` after any code change — must pass before commit -- See `AGENTS.md` for full testing rules and project context -- Proceed to Phase 4 after completing Phase 3 +- Token generation: `cashu -h https://testnut.cashu.space send --legacy 21` +- See `AGENTS.md` for full testing rules diff --git a/CMakeLists.txt b/CMakeLists.txt index d93e479..4214983 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,3 +2,7 @@ cmake_minimum_required(VERSION 3.16) set(PARTITION_CSV_PATH "${CMAKE_SOURCE_DIR}/partitions.csv") include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(esp32-tollgate) + +idf_component_get_property(lwip lwip COMPONENT_LIB) +target_compile_options(${lwip} PRIVATE "-I${PROJECT_DIR}/main") +target_compile_definitions(${lwip} PRIVATE "-DESP_IDF_LWIP_HOOK_FILENAME=\"lwip_tollgate_hooks.h\"") diff --git a/PLAN.md b/PLAN.md index 2a0ed2b..5690c1b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -286,7 +286,7 @@ Publishes TollGate node to Nostr as kind 38787 (wifistr): | 37 | 5 consecutive payments | Loop | All authenticated | TODO | | 38 | Stress: rapid pay/expire | Loop with short sessions | No crash/leak | TODO | -### Phase 4: ESP32 TollGate Client Detection + Auto-Payment — IN PROGRESS +### Phase 4: ESP32 TollGate Client Detection + Auto-Payment — COMPLETE **Goal:** ESP32 detects upstream TollGate when connected as STA, automatically pays for internet access using on-device wallet. Enables ESP32→OpenWRT (Scenario 4) and ESP32→ESP32 (Scenario 5) auto-payment. @@ -354,7 +354,7 @@ IDLE → [STA got IP] → DETECTING → [kind=10021 found] → NEEDS_PAY Pre-association price discovery via Wi-Fi Vendor IE beacons (OUI `0x54:0x47`) is deferred to a future phase. The client currently uses HTTP-based discovery after connection. -### Phase 5: Lightning Auto-Payout — NOT STARTED +### Phase 5: Lightning Auto-Payout — COMPLETE **Goal:** When wallet balance exceeds a configurable threshold, automatically pay out to Lightning addresses via LNURL-pay + Cashu NUT-05 melt. @@ -420,7 +420,7 @@ Wraps `Wallet::request_melt_quote()` + `Wallet::melt_tokens()` (NUT-05). | 47 | Melt with fee tolerance | Integration | Invoice paid, change received | TODO | | 48 | Full payout cycle | E2E | Wallet drains to min_balance | TODO | -### Phase 6: Bytes-Based Billing — NOT STARTED +### Phase 6: Bytes-Based Billing — COMPLETE **Goal:** Support both time-based (milliseconds) and data-based (bytes) billing metrics. Mirrors the Go implementation's dual-metric system. @@ -473,7 +473,7 @@ uint64_t bytes_consumed; | 51 | NAPT byte counting | Integration | Counters match actual traffic | TODO | | 52 | Bytes metric end-to-end | E2E | Client disconnected after data cap | TODO | -### Phase 7: ContextVM Server (MCP over Nostr) — NOT STARTED +### Phase 7: ContextVM Server (MCP over Nostr) — COMPLETE **Goal:** Remote configuration of ESP32 TollGate via ContextVM — services communicate over Nostr using public keys as addresses. Exposes configuration as MCP tools accessible by both humans and AI agents. @@ -524,6 +524,73 @@ Only accept commands from owner npub (derived from nsec in config.json). ## Total: 56 Tests across 7 phases +## Post-Phase 7: Bug Fixes & Architecture Improvements + +### Per-Client NAT Filtering (Multi-Client Isolation Fix) + +**Problem:** When client A's session expires but client B is still active, NAPT stays enabled globally. Client A's existing TCP/UDP connections in the NAT table continue routing — their traffic reaches the internet even though they're unauthenticated. + +**Solution:** Use lwIP's `LWIP_HOOK_IP4_CANFORWARD` hook to filter forwarded packets by source IP. This fires in `ip4_forward()` **before** NAPT translation, so unauthorized clients are blocked at the IP layer. + +**Architecture Change:** +- **Before:** NAT is a global toggle — ON when any client is auth'd, OFF when none are +- **After:** NAT stays always ON. Per-client filtering happens in the lwIP forwarding path via `LWIP_HOOK_IP4_CANFORWARD` + +**Files:** + +| File | Action | Description | +|------|--------|-------------| +| `main/lwip_tollgate_hooks.h` | Create | Defines `LWIP_HOOK_IP4_CANFORWARD` macro | +| `CMakeLists.txt` | Modify | Inject hook header into lwIP compilation | +| `main/firewall.c` | Modify | Add filter function, remove global NAT toggle | +| `main/firewall.h` | Modify | Expose filter function declaration | +| `main/tollgate_main.c` | Modify | Remove `firewall_disable_nat()` from stop_services | + +**Implementation:** + +1. `lwip_tollgate_hooks.h` — hook header included by lwIP: +```c +#define LWIP_HOOK_IP4_CANFORWARD(p, addr) tollgate_ip4_canforward_filter(p, addr) +``` + +2. `CMakeLists.txt` — inject into lwIP build (follows ESP-IDF vlan example pattern): +```cmake +idf_component_get_property(lwip lwip COMPONENT_LIB) +target_compile_options(${lwip} PRIVATE "-I${PROJECT_DIR}/main") +target_compile_definitions(${lwip} PRIVATE "-DESP_IDF_LWIP_HOOK_FILENAME=\"lwip_tollgate_hooks.h\"") +``` + +3. `firewall.c` — filter function checks source IP against allowed client list: +```c +int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder) { + struct ip_hdr *iphdr = (struct ip_hdr *)p->payload; + uint32_t src_ip = lwip_ntohl(iphdr->src.addr); + return firewall_is_client_allowed(src_ip) ? 1 : 0; +} +``` + +4. NAT management simplified: enable once during `firewall_init()`, never disable. Remove `update_nat()`, `firewall_enable_nat()`, `firewall_disable_nat()`. + +**Key properties:** +- Hook fires only for **forwarded** packets (AP client → internet), not packets to ESP32 itself +- Captive portal (port 80) and API (port 2121) remain accessible to all clients +- DNS hijack continues independently — both layers enforce auth +- Return traffic: NAPT only creates DNAT entries for successfully forwarded outbound packets, so blocked clients don't get return traffic either +- Performance: linear scan of ≤10 clients per packet — negligible cost + +### Spent-Secret Cleanup + +**Problem:** `session.c` tracks spent Cashu secrets locally in a static array (`s_spent_secrets[100][65]`). This is redundant — the mint is the authority on spent state, and `nucula_wallet_receive()` already swaps proofs (registering them as spent with the mint). + +**Changes:** +- Remove `s_spent_secrets[]`, `s_spent_count`, `session_is_secret_spent()` from `session.c` +- Remove `spent_secrets` and `spent_secret_count` from `session_t` struct +- Remove `spent_secrets` params from `session_create()` / `session_create_bytes()` +- Remove local spent-secret check in `tollgate_api.c` — keep only mint `check_proof_states` check +- Update unit tests + +**Rationale:** The mint's `cashu_check_proof_states()` already catches double-spends over HTTP. `nucula_wallet_receive()` → `swap()` registers proofs as spent and replaces them. After a successful receive, the old token is useless. Local tracking adds no security, wastes 6.5KB RAM, and is lost on reboot anyway. + ## Testing Infrastructure ### Three-Layer Test Architecture diff --git a/main/firewall.c b/main/firewall.c index f349ab1..8d535b4 100644 --- a/main/firewall.c +++ b/main/firewall.c @@ -6,13 +6,13 @@ #include "lwip/lwip_napt.h" #include "lwip/etharp.h" #include "lwip/netif.h" +#include "lwip/prot/ip4.h" #include #define MAX_CLIENTS 10 static const char *TAG = "firewall"; static esp_ip4_addr_t s_ap_ip; -static bool s_nat_enabled = false; typedef struct { uint32_t ip; @@ -22,11 +22,6 @@ typedef struct { static fw_client_t s_clients[MAX_CLIENTS]; static int s_client_count = 0; -static struct netif *get_ap_netif(void) -{ - return netif_get_by_index(NETIF_NO_INDEX); -} - esp_err_t firewall_get_mac_for_ip(uint32_t client_ip, char *mac_out, size_t mac_out_size) { wifi_sta_list_t sta_list; @@ -66,38 +61,25 @@ esp_err_t firewall_init(esp_ip4_addr_t ap_ip) s_ap_ip = ap_ip; memset(s_clients, 0, sizeof(s_clients)); s_client_count = 0; - ESP_LOGI(TAG, "Firewall initialized with AP IP=" IPSTR, IP2STR(&s_ap_ip)); + ip_napt_enable(s_ap_ip.addr, 1); + ESP_LOGI(TAG, "Firewall initialized with AP IP=" IPSTR " (NAT always on, per-client filter)", IP2STR(&s_ap_ip)); return ESP_OK; } -static void update_nat(void) +int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder) { - bool should_enable = (s_client_count > 0); - if (should_enable && !s_nat_enabled) { - ip_napt_enable(s_ap_ip.addr, 1); - s_nat_enabled = true; - ESP_LOGI(TAG, "NAT enabled (client authenticated)"); - } else if (!should_enable && s_nat_enabled) { - ip_napt_enable(s_ap_ip.addr, 0); - s_nat_enabled = false; - ESP_LOGI(TAG, "NAT disabled (no authenticated clients)"); + (void)dest_addr_hostorder; + if (p->len < IP_HLEN) return -1; + struct ip_hdr *iphdr = (struct ip_hdr *)p->payload; + uint32_t src_ip_h = lwip_ntohl(iphdr->src.addr); + uint32_t ap_subnet = lwip_ntohl(s_ap_ip.addr) & 0xFFFFFF00; + if ((src_ip_h & 0xFFFFFF00) != ap_subnet) { + return 1; } -} - -void firewall_enable_nat(void) -{ - if (s_nat_enabled) return; - ip_napt_enable(s_ap_ip.addr, 1); - s_nat_enabled = true; - ESP_LOGI(TAG, "NAT enabled"); -} - -void firewall_disable_nat(void) -{ - if (!s_nat_enabled) return; - ip_napt_enable(s_ap_ip.addr, 0); - s_nat_enabled = false; - ESP_LOGI(TAG, "NAT disabled"); + if (firewall_is_client_allowed(iphdr->src.addr)) { + return 1; + } + return 0; } static fw_client_t *find_client_by_ip(uint32_t client_ip) @@ -137,7 +119,6 @@ void firewall_grant_access(uint32_t client_ip) s_client_count++; dns_server_set_client_authenticated(client_ip, true); - update_nat(); esp_ip4_addr_t ip_addr = { .addr = client_ip }; ESP_LOGI(TAG, "Access granted to " IPSTR " mac=%s", IP2STR(&ip_addr), @@ -154,7 +135,6 @@ void firewall_revoke_access(uint32_t client_ip) s_clients[i] = s_clients[s_client_count - 1]; s_client_count--; dns_server_set_client_authenticated(client_ip, false); - update_nat(); return; } } @@ -166,7 +146,6 @@ void firewall_revoke_all(void) dns_server_set_client_authenticated(s_clients[i].ip, false); } s_client_count = 0; - update_nat(); ESP_LOGI(TAG, "All client access revoked"); } diff --git a/main/firewall.h b/main/firewall.h index e5d492a..f177eaa 100644 --- a/main/firewall.h +++ b/main/firewall.h @@ -6,11 +6,11 @@ #include #include +struct pbuf; + #define FW_MAX_MAC_LEN 18 esp_err_t firewall_init(esp_ip4_addr_t ap_ip); -void firewall_enable_nat(void); -void firewall_disable_nat(void); void firewall_grant_access(uint32_t client_ip); void firewall_revoke_access(uint32_t client_ip); void firewall_revoke_all(void); @@ -20,4 +20,6 @@ int firewall_client_count(void); esp_err_t firewall_get_mac_for_ip(uint32_t client_ip, char *mac_out, size_t mac_out_size); +int tollgate_ip4_canforward_filter(struct pbuf *p, uint32_t dest_addr_hostorder); + #endif diff --git a/main/lwip_tollgate_hooks.h b/main/lwip_tollgate_hooks.h new file mode 100644 index 0000000..76017be --- /dev/null +++ b/main/lwip_tollgate_hooks.h @@ -0,0 +1,10 @@ +#ifndef LWIP_TOLLGATE_HOOKS_H +#define LWIP_TOLLGATE_HOOKS_H + +#include "lwip/pbuf.h" + +int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder); + +#define LWIP_HOOK_IP4_CANFORWARD(p, addr) tollgate_ip4_canforward_filter(p, addr) + +#endif diff --git a/main/session.c b/main/session.c index 4854163..9b4380c 100644 --- a/main/session.c +++ b/main/session.c @@ -7,15 +7,10 @@ #include "freertos/task.h" #include -#define SPENT_SECRETS_MAX 100 - static const char *TAG = "session"; static session_t s_sessions[SESSION_MAX_CLIENTS]; static int s_session_count = 0; -static char s_spent_secrets[SPENT_SECRETS_MAX][65]; -static int s_spent_count = 0; - static int64_t get_time_ms(void) { return (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; @@ -25,7 +20,6 @@ esp_err_t session_manager_init(void) { memset(s_sessions, 0, sizeof(s_sessions)); s_session_count = 0; - s_spent_count = 0; ESP_LOGI(TAG, "Session manager initialized"); return ESP_OK; } @@ -37,27 +31,14 @@ static void populate_mac(session_t *session, uint32_t client_ip) } } -session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, - const char *spent_secrets[], int secret_count) +session_t *session_create(uint32_t client_ip, uint64_t allotment_ms) { session_t *existing = session_find_by_ip(client_ip); if (existing) { session_extend(existing, allotment_ms); - for (int i = 0; i < secret_count && s_spent_count < SPENT_SECRETS_MAX; i++) { - strncpy(s_spent_secrets[s_spent_count], spent_secrets[i], 64); - s_spent_secrets[s_spent_count][64] = '\0'; - s_spent_count++; - } return existing; } - for (int i = 0; i < secret_count; i++) { - if (session_is_secret_spent(spent_secrets[i])) { - ESP_LOGW(TAG, "Duplicate secret rejected"); - return NULL; - } - } - if (s_session_count >= SESSION_MAX_CLIENTS) { for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { if (!s_sessions[i].active || session_is_expired(&s_sessions[i])) { @@ -73,22 +54,8 @@ session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, s_sessions[i].allotment_ms = allotment_ms; s_sessions[i].start_time_ms = get_time_ms(); s_sessions[i].active = true; - s_sessions[i].spent_secret_count = 0; populate_mac(&s_sessions[i], client_ip); - for (int j = 0; j < secret_count && j < 5; j++) { - strncpy(s_sessions[i].spent_secrets[s_sessions[i].spent_secret_count], - spent_secrets[j], 64); - s_sessions[i].spent_secrets[s_sessions[i].spent_secret_count][64] = '\0'; - s_sessions[i].spent_secret_count++; - } - - for (int j = 0; j < secret_count && s_spent_count < SPENT_SECRETS_MAX; j++) { - strncpy(s_spent_secrets[s_spent_count], spent_secrets[j], 64); - s_spent_secrets[s_spent_count][64] = '\0'; - s_spent_count++; - } - s_session_count++; firewall_grant_access(client_ip); @@ -104,10 +71,9 @@ session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, return NULL; } -session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes, - const char *spent_secrets[], int secret_count) +session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes) { - session_t *s = session_create(client_ip, 0, spent_secrets, secret_count); + session_t *s = session_create(client_ip, 0); if (s) { s->allotment_bytes = allotment_bytes; s->bytes_consumed = 0; @@ -170,14 +136,6 @@ bool session_is_expired(const session_t *session) return elapsed >= (int64_t)session->allotment_ms; } -bool session_is_secret_spent(const char *secret) -{ - for (int i = 0; i < s_spent_count; i++) { - if (strncmp(s_spent_secrets[i], secret, 64) == 0) return true; - } - return false; -} - void session_check_expiry(void) { for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { diff --git a/main/session.h b/main/session.h index 6282f5a..ea5b476 100644 --- a/main/session.h +++ b/main/session.h @@ -16,17 +16,13 @@ typedef struct { uint64_t allotment_bytes; uint64_t bytes_consumed; bool active; - char spent_secrets[5][65]; - int spent_secret_count; } session_t; esp_err_t session_manager_init(void); -session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, - const char *spent_secrets[], int secret_count); +session_t *session_create(uint32_t client_ip, uint64_t allotment_ms); -session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes, - const char *spent_secrets[], int secret_count); +session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes); void session_add_bytes(uint32_t client_ip, uint64_t bytes); @@ -37,8 +33,6 @@ void session_extend(session_t *session, uint64_t additional_ms); bool session_is_expired(const session_t *session); -bool session_is_secret_spent(const char *secret); - void session_check_expiry(void); void session_revoke(session_t *session); diff --git a/main/tollgate_api.c b/main/tollgate_api.c index 25e7dd2..650b0f3 100644 --- a/main/tollgate_api.c +++ b/main/tollgate_api.c @@ -224,20 +224,6 @@ static esp_err_t api_post_payment(httpd_req_t *req) return ESP_OK; } - for (int i = 0; i < token->proof_count; i++) { - if (session_is_secret_spent(token->proofs[i].secret)) { - free(token); - cJSON *notice = create_notice("error", "payment-error-token-spent", "Token already spent"); - char *json = cJSON_PrintUnformatted(notice); - httpd_resp_set_status(req, "402 Payment Required"); - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, json, strlen(json)); - cJSON_free(json); - cJSON_Delete(notice); - return ESP_OK; - } - } - cashu_proof_state_t *states = malloc(CASHU_MAX_PROOFS * sizeof(cashu_proof_state_t)); if (!states) { free(token); @@ -299,16 +285,11 @@ static esp_err_t api_post_payment(httpd_req_t *req) return ESP_OK; } - int secret_count = token->proof_count > 5 ? 5 : token->proof_count; - const char *secrets[5]; - for (int i = 0; i < secret_count; i++) { - secrets[i] = token->proofs[i].secret; - } session_t *session; if (is_bytes) { - session = session_create_bytes(client_ip, allotment, secrets, secret_count); + session = session_create_bytes(client_ip, allotment); } else { - session = session_create(client_ip, allotment, secrets, secret_count); + session = session_create(client_ip, allotment); } if (!session) { free(states); @@ -498,7 +479,7 @@ esp_err_t tollgate_api_start(void) config.server_port = 2121; config.ctrl_port = 32769; config.max_uri_handlers = 10; - config.stack_size = 32768; + config.stack_size = 16384; esp_err_t ret = httpd_start(&s_api_server, &config); if (ret != ESP_OK) { diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 2670f05..41dbbed 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c @@ -174,7 +174,6 @@ static void stop_services(void) tollgate_api_stop(); dns_server_stop(); cvm_server_stop(); - firewall_disable_nat(); firewall_revoke_all(); s_services_running = false; if (s_services_mutex) xSemaphoreGive(s_services_mutex); diff --git a/tests/unit/stubs/esp_random.h b/tests/unit/stubs/esp_random.h new file mode 100644 index 0000000..bb58af2 --- /dev/null +++ b/tests/unit/stubs/esp_random.h @@ -0,0 +1,6 @@ +#ifndef STUB_ESP_RANDOM_H +#define STUB_ESP_RANDOM_H + +#include "esp_system.h" + +#endif diff --git a/tests/unit/test_identity b/tests/unit/test_identity index 7ad1485..277bb49 100755 Binary files a/tests/unit/test_identity and b/tests/unit/test_identity differ diff --git a/tests/unit/test_mcp_handler b/tests/unit/test_mcp_handler new file mode 100755 index 0000000..b5d6a85 Binary files /dev/null and b/tests/unit/test_mcp_handler differ diff --git a/tests/unit/test_nip04 b/tests/unit/test_nip04 new file mode 100755 index 0000000..cb52040 Binary files /dev/null and b/tests/unit/test_nip04 differ diff --git a/tests/unit/test_session.c b/tests/unit/test_session.c index 548be0d..2619ba3 100644 --- a/tests/unit/test_session.c +++ b/tests/unit/test_session.c @@ -46,8 +46,7 @@ static void test_sessions(void) ASSERT_EQ_INT(0, session_active_count(), "No sessions after init"); printf("\n--- session_create ---\n"); - const char *secrets[] = {"secret1", "secret2"}; - session_t *s = session_create(0x0A01A8C0, 60000, secrets, 2); + session_t *s = session_create(0x0A01A8C0, 60000); ASSERT(s != NULL, "session_create returns non-NULL"); ASSERT_EQ_INT(1, session_active_count(), "1 session after create"); ASSERT_EQ_INT(1, g_granted_count, "firewall_grant_access was called"); @@ -57,23 +56,18 @@ static void test_sessions(void) ASSERT(found == s, "session_find_by_ip returns the created session"); ASSERT(session_find_by_ip(0x01020304) == NULL, "session_find_by_ip returns NULL for unknown IP"); - printf("\n--- session_is_secret_spent ---\n"); - ASSERT(session_is_secret_spent("secret1"), "secret1 is marked spent"); - ASSERT(session_is_secret_spent("secret2"), "secret2 is marked spent"); - ASSERT(!session_is_secret_spent("secret_unknown"), "unknown secret is not spent"); - - printf("\n--- Duplicate secret rejected ---\n"); - const char *dup_secrets[] = {"secret1"}; - g_granted_count = 0; - session_t *dup = session_create(0x0B01A8C0, 60000, dup_secrets, 1); - ASSERT(dup == NULL, "Duplicate secret returns NULL"); - ASSERT_EQ_INT(0, g_granted_count, "No new firewall grant for duplicate"); - printf("\n--- session_extend ---\n"); uint64_t old_allotment = s->allotment_ms; session_extend(s, 30000); ASSERT(s->allotment_ms == old_allotment + 30000, "Allotment extended by 30000ms"); + printf("\n--- session_extend for existing client ---\n"); + g_granted_count = 0; + session_t *s2 = session_create(0x0A01A8C0, 30000); + ASSERT(s2 == s, "same IP returns existing session"); + ASSERT_EQ_INT(0, g_granted_count, "no new firewall grant for extension"); + ASSERT(s->allotment_ms == old_allotment + 60000, "allotment extended by 30000ms on re-pay"); + printf("\n--- session_revoke ---\n"); g_revoked_count = 0; session_revoke(s); @@ -81,10 +75,8 @@ static void test_sessions(void) ASSERT_EQ_INT(0, session_active_count(), "No active sessions after revoke"); printf("\n--- session_revoke_all ---\n"); - const char *s1[] = {"s1"}; - const char *s2[] = {"s2"}; - session_create(0x01000001, 60000, s1, 1); - session_create(0x01000002, 60000, s2, 1); + session_create(0x01000001, 60000); + session_create(0x01000002, 60000); ASSERT_EQ_INT(2, session_active_count(), "2 sessions created"); g_revoked_count = 0; @@ -93,8 +85,7 @@ static void test_sessions(void) printf("\n--- session_tick does not crash ---\n"); session_manager_init(); - const char *st[] = {"tick_secret"}; - session_create(0x0A000001, 60000, st, 1); + session_create(0x0A000001, 60000); session_tick(); ASSERT_EQ_INT(1, session_active_count(), "Session still active after tick (not expired)"); } @@ -106,9 +97,8 @@ void test_bytes_sessions(void) memset(&g_test_config, 0, sizeof(g_test_config)); strncpy(g_test_config.metric, "bytes", sizeof(g_test_config.metric) - 1); - const char *sec[] = {"bytes_secret"}; uint64_t allotment = 22020096; - session_t *s = session_create_bytes(0x0A010001, allotment, sec, 1); + session_t *s = session_create_bytes(0x0A010001, allotment); ASSERT(s != NULL, "bytes session created"); ASSERT_EQ_INT(1, session_active_count(), "1 active bytes session"); @@ -133,8 +123,7 @@ void test_bytes_sessions(void) session_manager_init(); memset(&g_test_config, 0, sizeof(g_test_config)); strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); - const char *ms_sec[] = {"ms_secret"}; - session_t *ms = session_create(0x0A020001, 60000, ms_sec, 1); + session_t *ms = session_create(0x0A020001, 60000); ASSERT(ms != NULL, "ms session created"); ASSERT(!session_is_expired(ms), "ms session not expired immediately"); diff --git a/tests/unit/test_tollgate_client b/tests/unit/test_tollgate_client index 33b272e..f9b0f7d 100755 Binary files a/tests/unit/test_tollgate_client and b/tests/unit/test_tollgate_client differ -- cgit v1.2.3