diff options
| author | Your Name <you@example.com> | 2026-05-17 16:39:31 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-17 16:39:31 +0530 |
| commit | 0c2c67b463d6a90aaa0bb69bf3c91dba1d9ec3ec (patch) | |
| tree | afd9d9bca2d184825ebf7413ec31830e14131030 | |
| parent | 3342c8e7b4f645c75470d3d893d09037a672cfd2 (diff) | |
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
| -rw-r--r-- | CHECKLIST.md | 382 | ||||
| -rw-r--r-- | CMakeLists.txt | 4 | ||||
| -rw-r--r-- | PLAN.md | 75 | ||||
| -rw-r--r-- | main/firewall.c | 51 | ||||
| -rw-r--r-- | main/firewall.h | 6 | ||||
| -rw-r--r-- | main/lwip_tollgate_hooks.h | 10 | ||||
| -rw-r--r-- | main/session.c | 48 | ||||
| -rw-r--r-- | main/session.h | 10 | ||||
| -rw-r--r-- | main/tollgate_api.c | 25 | ||||
| -rw-r--r-- | main/tollgate_main.c | 1 | ||||
| -rw-r--r-- | tests/unit/stubs/esp_random.h | 6 | ||||
| -rwxr-xr-x | tests/unit/test_identity | bin | 296728 -> 297880 bytes | |||
| -rwxr-xr-x | tests/unit/test_mcp_handler | bin | 0 -> 38736 bytes | |||
| -rwxr-xr-x | tests/unit/test_nip04 | bin | 0 -> 298776 bytes | |||
| -rw-r--r-- | tests/unit/test_session.c | 37 | ||||
| -rwxr-xr-x | tests/unit/test_tollgate_client | bin | 51904 -> 51992 bytes |
16 files changed, 237 insertions, 418 deletions
diff --git a/CHECKLIST.md b/CHECKLIST.md index 5cedd30..b71bd14 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md | |||
| @@ -43,34 +43,10 @@ | |||
| 43 | - [x] Makefile: nutshell wallet targets (wallet-setup, wallet-info, mint-token, send-token) | 43 | - [x] Makefile: nutshell wallet targets (wallet-setup, wallet-info, mint-token, send-token) |
| 44 | - [x] `tests/phase2.mjs`: `/whoami` test checks `includes('mac=')` | 44 | - [x] `tests/phase2.mjs`: `/whoami` test checks `includes('mac=')` |
| 45 | 45 | ||
| 46 | ### Infrastructure | ||
| 47 | - [x] Upstream gateway on enx00e04c633a90 (192.168.2.0/24, metric 101, default route) | ||
| 48 | - [x] WiFi wlp59s0 free for ESP32 TollGate connection | ||
| 49 | - [x] Mint URL verified: `testnut.cashu.space` works (auto-pays invoices) | ||
| 50 | |||
| 51 | ### Tests Passing | 46 | ### Tests Passing |
| 52 | - [x] Test 15: Advertisement valid (kind=10021 with price_per_step) — PASSING | 47 | - [x] Tests 15-24: ALL PASSING |
| 53 | - [x] Test 16: Valid payment (POST :2121/ with valid Cashu token → kind=1022 session) — PASSING | ||
| 54 | - [x] Test 17: Usage tracking after payment (GET :2121/usage → active usage) — PASSING | ||
| 55 | - [x] Test 18: Internet after payment (ping through TollGate works) — PASSING | ||
| 56 | - [x] Test 19: Invalid token rejected (POST garbage → 400, kind=21023) — PASSING | ||
| 57 | - [x] Test 20: Spent token rejected (reuse token → kind=21023) — PASSING | ||
| 58 | - [x] Test 21: Wrong mint rejected (POST token from wrong mint → kind=21023) — PASSING | ||
| 59 | - [x] Test 22: Session expiry (wait for allotment → internet blocked) — PASSING | ||
| 60 | - [x] Test 23: Session renewal (second payment → allotment extended) — PASSING | ||
| 61 | - [x] Test: /whoami returns ip=X.X.X.X mac=XX:XX:XX:XX:XX:XX — PASSING | ||
| 62 | - [x] Test: Portal has payment form (Cashu token input + Pay button) — PASSING | ||
| 63 | |||
| 64 | ### Captive Portal Detection Fix | ||
| 65 | - [x] Added DoT reject server on port 853 (TCP RST forces DNS fallback to port 53) | ||
| 66 | - [x] DNS hijack now returns NXDOMAIN for ALL non-A query types (prevents DNS leaks) | ||
| 67 | - [x] Shorter TTL on hijack responses (10s) for faster detection | ||
| 68 | - [x] Explicit 302 redirect handlers for all captive detection URIs (/generate_204, /hotspot-detect.html, etc.) | ||
| 69 | - [x] HTTP request logging for captive detection endpoints | ||
| 70 | - [x] DNS query logging for unauthenticated clients | ||
| 71 | - [x] Verified working with GrapheneOS phone (commit `236b61d`) | ||
| 72 | 48 | ||
| 73 | ## Phase 3: On-Device Wallet + Nostr Identity + Wifistr — IN PROGRESS | 49 | ## Phase 3: On-Device Wallet + Nostr Identity + Wifistr — COMPLETE |
| 74 | ### nucula Wallet Integration | 50 | ### nucula Wallet Integration |
| 75 | - [x] Add nucula as git submodule (`nucula_src/`) | 51 | - [x] Add nucula as git submodule (`nucula_src/`) |
| 76 | - [x] Create `components/secp256k1/` (symlink to nucula's libsecp256k1) | 52 | - [x] Create `components/secp256k1/` (symlink to nucula's libsecp256k1) |
| @@ -79,271 +55,125 @@ | |||
| 79 | - [x] All wallet operations tested on Board A: pay, swap, send, persistence | 55 | - [x] All wallet operations tested on Board A: pay, swap, send, persistence |
| 80 | 56 | ||
| 81 | ### Nostr Identity Derivation (identity.c/h) | 57 | ### Nostr Identity Derivation (identity.c/h) |
| 82 | - [x] Create `identity.h` — API: `identity_init(nsec_hex)`, derived value accessors | 58 | - [x] HMAC-SHA512 derivation via mbedtls, npub via secp256k1 |
| 83 | - [x] Create `identity.c` — HMAC-SHA512 derivation via mbedtls, npub via secp256k1 | 59 | - [x] Derive STA/AP MAC, SSID, AP IP from nsec |
| 84 | - [x] Derive STA MAC: `tollgate_derive(nsec, "sta-mac", 0)` → 6 bytes, locally administered | ||
| 85 | - [x] Derive AP MAC: `tollgate_derive(nsec, "ap-mac", 0)` → 6 bytes, locally administered | ||
| 86 | - [x] Derive SSID: `"TollGate-" + hex(AP_MAC[3:6])` | ||
| 87 | - [x] Derive AP IP: hash-based from AP MAC bytes | ||
| 88 | - [x] Compute npub: secp256k1 x-only pubkey from nsec | ||
| 89 | - [x] Set MACs via `esp_wifi_set_mac()` in boot sequence | 60 | - [x] Set MACs via `esp_wifi_set_mac()` in boot sequence |
| 61 | - [x] 24/24 unit tests passing | ||
| 90 | 62 | ||
| 91 | ### Nostr Event Signing (nostr_event.c/h) | 63 | ### Nostr Event Signing (nostr_event.c/h) |
| 92 | - [x] Create `nostr_event.h` — NIP-01 event struct + sign/serialize API | 64 | - [x] NIP-01 canonical JSON, SHA-256 ID, Schnorr signature |
| 93 | - [x] Create `nostr_event.c` — canonical JSON, SHA-256 ID, Schnorr signature | 65 | - [x] 23/23 unit tests passing |
| 94 | - [x] Uses `secp256k1_schnorrsig_sign32()` for BIP-340 signatures | ||
| 95 | 66 | ||
| 96 | ### Geohash Encoding (geohash.c/h) | 67 | ### Geohash Encoding (geohash.c/h) |
| 97 | - [x] Create `geohash.h` — `geohash_encode(lat, lon, precision, out)` | 68 | - [x] Standard base-32 geohash encoding |
| 98 | - [x] Create `geohash.c` — standard base-32 geohash encoding | 69 | - [x] 11/11 unit tests passing |
| 99 | 70 | ||
| 100 | ### Wifistr Service Discovery (wifistr.c/h) | 71 | ### Wifistr Service Discovery (wifistr.c/h) |
| 101 | - [x] Create `wifistr.h` — `wifistr_publish()` API | 72 | - [x] kind 38787 event builder + WebSocket relay publish |
| 102 | - [x] Create `wifistr.c` — kind 38787 event builder + WebSocket relay publish | ||
| 103 | - [x] Build event with tags: d, ssid, h, security, g, c | ||
| 104 | - [x] WebSocket client: raw TCP + TLS (esp_tls.h) + HTTP Upgrade | ||
| 105 | - [x] Publish on boot + periodic timer (6h default) | 73 | - [x] Publish on boot + periodic timer (6h default) |
| 106 | 74 | - [x] Verified published to relay.damus.io and nos.lol | |
| 107 | ### Config Changes (config.c/h) | 75 | |
| 108 | - [x] Add to struct: nsec, npub, nostr_geohash, nostr_relays, nostr_publish_interval_s, sta_mac, ap_mac | 76 | ## Phase 4: ESP32 TollGate Client Detection + Auto-Payment — COMPLETE (commit `78dd599`) |
| 109 | - [x] Remove from JSON parsing: ap_ssid, ap_ip (now derived from nsec) | 77 | - [x] tollgate_client.c/h — detection, payment, monitoring, state machine |
| 110 | - [x] Keep: ap_password, ap_channel, ap_max_conn (hardcoded defaults) | 78 | - [x] Config fields: client_enabled, client_steps_to_buy, etc. |
| 111 | - [x] Update default config.json template with nsec and Nostr fields | 79 | - [x] Integration into tollgate_main.c |
| 112 | 80 | - [x] 30/30 unit tests passing | |
| 113 | ### Boot Sequence Changes (tollgate_main.c) | 81 | |
| 114 | - [x] Call `identity_init(nsec)` after config load, before WiFi init | 82 | ## Phase 5: Lightning Auto-Payout — COMPLETE (commit `cb4bd7d`) |
| 115 | - [x] Set STA/AP MAC via `esp_wifi_set_mac()` after `esp_wifi_init()`, before `esp_wifi_start()` | 83 | - [x] lnurl_pay.c/h — LNURL-pay HTTP flow |
| 116 | - [x] Remove old `tollgate_config_derive_unique()` call | 84 | - [x] lightning_payout.c/h — periodic balance check, threshold, multi-recipient split, melt |
| 117 | - [x] Use derived SSID/IP in AP configuration | 85 | - [x] nucula_wallet_melt() bridge for NUT-05 |
| 118 | - [x] Start wifistr publish task after services start | 86 | - [x] Config: payout.enabled, recipients, mints, fee_tolerance, etc. |
| 119 | 87 | - [x] 7/7 lnurl_pay + 11/11 lightning_payout = 18 unit tests passing | |
| 120 | ### Build System | 88 | |
| 121 | - [x] Add identity.c, nostr_event.c, geohash.c, wifistr.c to CMakeLists.txt SRCS | 89 | ## Phase 6: Bytes-Based Billing — COMPLETE (commit `edd125d`) |
| 122 | - [x] Add `secp256k1` to REQUIRES (for identity.c and nostr_event.c) | 90 | - [x] Dual-metric session support (milliseconds + bytes) |
| 123 | - [x] Clean build (0 errors, 0 warnings) | 91 | - [x] session_create_bytes(), session_add_bytes() |
| 124 | 92 | - [x] Config: metric, step_size_bytes | |
| 125 | ### Hardware Testing | 93 | - [x] Discovery endpoint advertises correct metric |
| 126 | - [x] Flash Board A, verify wallet boot (keyset fetch succeeds) | 94 | - [x] Unit tests: bytes session lifecycle, mixed metrics |
| 127 | - [x] Pay Board A with Cashu token, verify proofs stored (GET /wallet) | 95 | |
| 128 | - [x] Test POST /wallet/swap on Board A | 96 | ## Phase 7: MCP Handler + NIP-04 + CVM Server — COMPLETE (commit `fdf662f`) |
| 129 | - [x] Test POST /wallet/send on Board A, verify token is valid | 97 | - [x] mcp_handler.c/h — 4 tools (get_config, set_config, get_balance, wallet_send), 25 unit tests |
| 130 | - [x] Flash Board A with new identity derivation, verify derived SSID/MAC/IP | 98 | - [x] nip04.c/h — AES-256-CBC + ECDH with 0x02 compressed pubkey prefix, 15 unit tests |
| 131 | - [x] Verify captive portal works with new SSID/IP | 99 | - [x] cvm_server.c/h — Nostr DM listener skeleton with FreeRTOS task |
| 132 | - [x] Verify payment flow still works with identity-derived config | 100 | - [x] Fixed NIP-04 IV bug: mbedtls_aes_crypt_cbc modifies IV in-place |
| 133 | - [x] Verify wifistr event published to relay (damus + nos.lol) | 101 | - [x] Fixed missing esp_random.h include in nip04.c |
| 134 | - [ ] Flash Board B with new firmware (different nsec) | 102 | - [x] 156 total unit tests passing across 10 test binaries |
| 135 | - [ ] Cross-board payment: Board B token → Board A | 103 | |
| 136 | - [ ] Verify both boards show correct balances after cross-board payment | 104 | ## Bug Fixes — COMPLETE (commit `3342c8e`) |
| 137 | 105 | - [x] reset_auth_handler now calls session_revoke_all() before firewall_revoke_all() | |
| 138 | ### Tests 25-27 (deferred from Phase 2, need Board B) | 106 | - [x] Port 80 /usage shows real session data (remaining/total) instead of "0/0" |
| 139 | - [ ] Test 25: Two clients pay independently (laptop + Board B) | 107 | - [x] Config metric defaults to "milliseconds" (ESP32 can't track per-client bytes from NAT) |
| 140 | - [ ] Test 26: Client isolation (only payer gets internet) | 108 | - [x] Fixed sys_evt stack overflow: deferred start_services() to dedicated 32KB task |
| 141 | - [ ] Test 27: Full e2e: portal → pay → browse | 109 | |
| 142 | 110 | ## Playwright Interop Tests — COMPLETE (commit `4fb44e7`) | |
| 143 | ### Tests 28-38 (Phase 3 specific) | 111 | - [x] 18/18 tests passing (11 ESP32 + 7 ESP32↔OpenWRT interop) |
| 144 | - [ ] Test 28: Wallet boot (keysets loaded) | 112 | - [x] 7 screenshots generated |
| 145 | - [ ] Test 29: Receive via wallet (balance incremented) | 113 | - [x] Double-spend rejection verified on live hardware |
| 146 | - [ ] Test 30: Wallet swap (same balance, new proofs) | 114 | |
| 147 | - [ ] Test 31: Wallet send (valid cashuA token) | 115 | --- |
| 148 | - [ ] Test 32: Persistence survives reboot | 116 | |
| 149 | - [ ] Test 33: Cross-board payment | 117 | ## TODO — In Progress |
| 150 | - [ ] Test 34: 5 consecutive payments | 118 | |
| 151 | - [ ] Test 35: Stress: rapid pay/expire | 119 | ### Per-Client NAT Filtering (Multi-Client Fix) |
| 152 | 120 | - [ ] Create `main/lwip_tollgate_hooks.h` — LWIP_HOOK_IP4_CANFORWARD definition | |
| 153 | ### Automated Tests | 121 | - [ ] Update `CMakeLists.txt` — inject hook header into lwIP compilation |
| 154 | - [ ] Write tests/phase3.mjs (wallet endpoint tests + cross-board) | 122 | - [ ] Add `tollgate_ip4_canforward_filter()` to `firewall.c` — filter forwarded packets by source IP |
| 155 | - [ ] All Phase 3 tests passing | 123 | - [ ] Change firewall strategy: NAT always ON, per-client filter in lwIP forwarding path |
| 156 | 124 | - [ ] Remove `update_nat()`, `firewall_enable_nat()`, `firewall_disable_nat()` from firewall.c | |
| 157 | ## Test Coverage — IN PROGRESS | 125 | - [ ] Update `stop_services()` in tollgate_main.c — remove `firewall_disable_nat()` call |
| 158 | 126 | - [ ] Add unit test for filter function | |
| 159 | ### Host Unit Tests (tests/unit/) | 127 | - [ ] Build, flash, test on Board A |
| 160 | - [ ] Create `tests/unit/stubs/` — clean ESP-IDF type stubs for host compilation | 128 | - [ ] Verify multi-client isolation: expire one client while other is active |
| 161 | - [ ] Create `tests/unit/Makefile` — compiles all unit tests with host gcc | 129 | |
| 162 | - [ ] Install system deps: `libmbedtls-dev`, `libcjson-dev` | 130 | ### Spent-Secret Cleanup |
| 163 | - [ ] `test_geohash.c` — geohash_encode against reference vectors (Munich, NYC, origin) | 131 | - [ ] Remove `s_spent_secrets[]` and `session_is_secret_spent()` from `session.c` |
| 164 | - [ ] `test_identity.c` — HMAC-SHA512 derivation, MAC bits, SSID/IP determinism | 132 | - [ ] Remove `spent_secrets` field from `session_t` struct in `session.h` |
| 165 | - [ ] `test_nostr_event.c` — NIP-01 event ID, Schnorr sign+verify, JSON serialization | 133 | - [ ] Remove `spent_secrets` params from `session_create()` and `session_create_bytes()` |
| 166 | - [ ] `test_cashu.c` — token decode, allotment calc, mint validation | 134 | - [ ] Remove local spent-secret check in `tollgate_api.c` (lines 227-239) |
| 167 | - [ ] `test_session.c` — session lifecycle, expiry, spent-secret dedup | 135 | - [ ] Remove `secrets[]` array construction in `tollgate_api.c` |
| 168 | - [ ] `make test-unit` passes all unit tests | 136 | - [ ] Update `tests/unit/test_session.c` — remove secret-tracking tests |
| 137 | - [ ] Run `make test-unit` — all tests pass | ||
| 138 | |||
| 139 | ### Integration Tests (tests/integration/) | ||
| 140 | - [ ] Create `tests/integration/` directory | ||
| 141 | - [ ] Move existing tests (api.mjs, network.mjs, smoke.mjs, phase2.mjs) into integration/ | ||
| 142 | - [ ] Write `test-reset-auth.mjs` — verify sessions cleared after reset | ||
| 143 | - [ ] Write `test-session-lifecycle.mjs` — pay → verify usage → wait expiry → verify blocked (65s) | ||
| 144 | - [ ] Write `test-dns-firewall.mjs` — DNS hijack before auth, forward after auth | ||
| 145 | - [ ] Update Makefile targets for new paths | ||
| 146 | - [ ] All integration tests passing | ||
| 169 | 147 | ||
| 170 | ### Test Reorganization | 148 | ### Test Reorganization |
| 171 | - [ ] Move `tests/api.mjs` → `tests/integration/phase1_api.mjs` | 149 | - [ ] Fix all hardcoded IPs → `process.env.TOLLGATE_IP` |
| 172 | - [ ] Move `tests/network.mjs` → `tests/integration/phase1_network.mjs` | 150 | - [ ] Move `tests/captive-portal.spec.mjs` → `tests/e2e/` |
| 173 | - [ ] Move `tests/smoke.mjs` → `tests/integration/smoke.mjs` | 151 | - [ ] Move `tests/interop-happy-path.spec.mjs` → `tests/e2e/` or `tests/integration/` |
| 174 | - [ ] Move `tests/phase2.mjs` → `tests/integration/phase2.mjs` | 152 | - [ ] Move `tests/playwright.config.mjs` → `tests/e2e/` |
| 175 | - [ ] Move `tests/captive-portal.spec.mjs` → `tests/e2e/captive-portal.spec.mjs` | ||
| 176 | - [ ] Move `tests/playwright.config.mjs` → `tests/e2e/playwright.config.mjs` | ||
| 177 | - [ ] Fix all hardcoded IPs (`192.168.4.1`) → `process.env.TOLLGATE_IP` | ||
| 178 | |||
| 179 | ### New Integration Tests | ||
| 180 | - [ ] `tests/integration/phase3.mjs` — wallet GET/swap/send, identity SSID/IP, wifistr on relay | ||
| 181 | - [ ] All Phase 3 integration tests passing | ||
| 182 | |||
| 183 | ### New E2E Tests | ||
| 184 | - [ ] `tests/e2e/payment.spec.mjs` — paste token → pay → success, error handling, full flow | ||
| 185 | - [ ] All E2E tests passing | ||
| 186 | |||
| 187 | ### Build System Updates | ||
| 188 | - [ ] Update `Makefile` with `test-unit`, `test-integration`, `test-e2e`, `test-all` targets | ||
| 189 | - [ ] Update `package.json` npm scripts for new paths | ||
| 190 | - [ ] All `make test-*` targets work | ||
| 191 | |||
| 192 | ## Phase 4: ESP32 TollGate Client Detection + Auto-Payment — IN PROGRESS | ||
| 193 | |||
| 194 | ### tollgate_client.c/h (New) | ||
| 195 | - [x] Create `tollgate_client.h` — types: `tollgate_discovery_t`, `tollgate_client_state_t` enum (IDLE/DETECTING/NEEDS_PAY/PAYING/PAID/RENEWING) | ||
| 196 | - [x] Create `tollgate_client.c` — detection, payment, monitoring, state machine | ||
| 197 | - [x] `tollgate_client_detect(gw_ip)` — HTTP GET `http://{gw}:2121/`, parse kind=10021, extract price tags | ||
| 198 | - [x] `tollgate_client_pay(gw_ip, amount_sats)` — `nucula_wallet_send()` → POST to upstream → parse kind=1022/21023 | ||
| 199 | - [x] `tollgate_client_on_sta_connected()` — extract gw from DHCP, detect, pay (blocking) | ||
| 200 | - [x] `tollgate_client_tick()` — GET `/usage`, renew at 20% remaining | ||
| 201 | - [x] `tollgate_client_on_sta_disconnected()` — reset state | ||
| 202 | - [x] `tollgate_client_get_usage(gw_ip)` — GET `/usage` → parse remaining/total | ||
| 203 | |||
| 204 | ### Config Changes | ||
| 205 | - [x] Add to `config.h`: `client_enabled`, `client_steps_to_buy`, `client_renewal_threshold_pct`, `client_retry_interval_ms` | ||
| 206 | - [x] Parse new fields in `config.c` | ||
| 207 | |||
| 208 | ### Integration (tollgate_main.c) | ||
| 209 | - [x] Make wallet init synchronous (call `nucula_wallet_init()` directly, not as task) | ||
| 210 | - [x] Add `tollgate_client_on_sta_connected()` in `ip_event_handler` (blocking, before `start_services()`) | ||
| 211 | - [x] Add `tollgate_client_on_sta_disconnected()` in `wifi_event_handler` | ||
| 212 | - [x] Add `tollgate_client_tick()` in main loop | ||
| 213 | - [x] Update `main/CMakeLists.txt` — add `tollgate_client.c` | ||
| 214 | |||
| 215 | ### Unit Tests | ||
| 216 | - [x] `tests/unit/test_tollgate_client.c` — discovery parsing, price extraction, state machine, renewal threshold | ||
| 217 | - [x] All unit tests passing (30 new, 116 total) — committed at `78dd599` | ||
| 218 | |||
| 219 | ### Integration Tests | ||
| 220 | - [ ] ESP32→OpenWRT auto-payment (Scenario 4) | ||
| 221 | - [ ] ESP32→ESP32 auto-payment (Scenario 5, needs Board B) | ||
| 222 | |||
| 223 | ### Test Cases 39-43 | ||
| 224 | - [ ] Test 39: Client detection (kind=10021 parse) | ||
| 225 | - [ ] Test 40: Client payment flow (mock HTTP) | ||
| 226 | - [ ] Test 41: Session renewal (20% threshold) | ||
| 227 | - [ ] Test 42: ESP32→OpenWRT auto-pay | ||
| 228 | - [ ] Test 43: ESP32→ESP32 auto-pay | ||
| 229 | |||
| 230 | ## Phase 5: Lightning Auto-Payout — NOT STARTED | ||
| 231 | |||
| 232 | ### lnurl_pay.c/h (New) | ||
| 233 | - [ ] Create `lnurl_pay.h` — `lnurl_get_invoice(lightning_address, amount_sats, bolt11_out, out_size)` | ||
| 234 | - [ ] Create `lnurl_pay.c` — GET `.well-known/lnurlp/{user}` → parse callback → GET callback with amount → extract BOLT11 | ||
| 235 | |||
| 236 | ### lightning_payout.c/h (New) | ||
| 237 | - [ ] Create `lightning_payout.h` — `payout_recipient_t`, config, init/tick API | ||
| 238 | - [ ] Create `lightning_payout.c` — periodic balance check, threshold, multi-recipient split, melt with retry | ||
| 239 | |||
| 240 | ### nucula Bridge Extension | ||
| 241 | - [ ] Add `nucula_wallet_melt(bolt11, max_fee_sats)` to `nucula_wallet.h/cpp` | ||
| 242 | - [ ] Wraps `Wallet::request_melt_quote()` + `Wallet::melt_tokens()` (NUT-05) | ||
| 243 | |||
| 244 | ### Config Changes | ||
| 245 | - [ ] Add payout config to `config.h`: `payout_enabled`, `min_payout_amount`, `min_balance`, `fee_tolerance_pct`, `check_interval_s`, `recipients[]` | ||
| 246 | - [ ] Parse payout config in `config.c` | ||
| 247 | |||
| 248 | ### Integration (tollgate_main.c) | ||
| 249 | - [ ] Add periodic payout timer (60s interval) | ||
| 250 | - [ ] Update `main/CMakeLists.txt` | ||
| 251 | |||
| 252 | ### Unit Tests | ||
| 253 | - [ ] `tests/unit/test_lnurl_pay.c` — LNURL-pay URL construction, response parsing | ||
| 254 | - [ ] `tests/unit/test_lightning_payout.c` — threshold check, multi-recipient split, fee tolerance | ||
| 255 | |||
| 256 | ### Test Cases 44-48 | ||
| 257 | - [ ] Test 44: LNURL-pay flow | ||
| 258 | - [ ] Test 45: Payout threshold | ||
| 259 | - [ ] Test 46: Multi-recipient split | ||
| 260 | - [ ] Test 47: Melt with fee tolerance | ||
| 261 | - [ ] Test 48: Full payout cycle | ||
| 262 | |||
| 263 | ## Phase 6: Bytes-Based Billing — NOT STARTED | ||
| 264 | |||
| 265 | ### lwIP NAPT Stats Component (New) | ||
| 266 | - [ ] Create `components/lwip_napt_stats/` — patched `ip4_napt.c` with byte counters | ||
| 267 | - [ ] Add `uint64_t bytes_up/bytes_down` to `struct ip_napt_entry` | ||
| 268 | - [ ] Increment in `ip_napt_forward()` and `ip_napt_recv()` | ||
| 269 | - [ ] Add public API: `ip_napt_get_client_bytes(client_ip, &up, &down)` | ||
| 270 | - [ ] Create component CMakeLists.txt | ||
| 271 | |||
| 272 | ### Session Changes | ||
| 273 | - [ ] Add `allotment_bytes`, `bytes_consumed` to `session_t` | ||
| 274 | - [ ] Dual-metric `session_is_expired()` dispatches on metric type | ||
| 275 | - [ ] `session_add_bytes(client_ip, byte_count)` called from firewall counting | ||
| 276 | |||
| 277 | ### Config Changes | ||
| 278 | - [ ] Add `metric` field ("milliseconds" or "bytes") to `config.h` | ||
| 279 | - [ ] Add `step_size_bytes` to `config.h` | ||
| 280 | - [ ] Parse in `config.c` | ||
| 281 | |||
| 282 | ### TollGate API Changes | ||
| 283 | - [ ] Discovery endpoint advertises correct metric | ||
| 284 | - [ ] `/usage` returns byte-based or time-based values | ||
| 285 | - [ ] Allotment calculation dispatches on metric | ||
| 286 | |||
| 287 | ### Firewall Changes | ||
| 288 | - [ ] `firewall_count_traffic()` — queries NAPT byte counters per active client | ||
| 289 | - [ ] Called from `session_tick()` or main loop | ||
| 290 | |||
| 291 | ### Cashu Changes | ||
| 292 | - [ ] Unify `cashu_calculate_allotment()` for both metrics | ||
| 293 | |||
| 294 | ### Unit Tests | ||
| 295 | - [ ] `tests/unit/test_bytes_metric.c` — byte allotment calc, dual-metric session expiry | ||
| 296 | |||
| 297 | ### Test Cases 49-52 | ||
| 298 | - [ ] Test 49: Byte allotment calc | ||
| 299 | - [ ] Test 50: Byte session expiry | ||
| 300 | - [ ] Test 51: NAPT byte counting | ||
| 301 | - [ ] Test 52: Bytes metric end-to-end | ||
| 302 | |||
| 303 | ## Phase 7: ContextVM Server (MCP over Nostr) — NOT STARTED | ||
| 304 | |||
| 305 | ### NIP-44 Encryption (New) | ||
| 306 | - [ ] Create `nip44.h` — encrypt/decrypt API | ||
| 307 | - [ ] Create `nip44.c` — XChaCha20-Poly1305 + secp256k1 ECDH + conversation key derivation | ||
| 308 | |||
| 309 | ### MCP Handler (New) | ||
| 310 | - [ ] Create `mcp_handler.h` — tool registration, JSON-RPC parse/dispatch | ||
| 311 | - [ ] Create `mcp_handler.c` — register tools, handle requests, build responses | ||
| 312 | |||
| 313 | ### CVM Server (New) | ||
| 314 | - [ ] Create `cvm_server.h` — init/start/stop API | ||
| 315 | - [ ] Create `cvm_server.c` — WebSocket listener, DM subscription, NIP-44 decrypt, MCP dispatch | ||
| 316 | |||
| 317 | ### MCP Tool Registration | ||
| 318 | - [ ] `get_config`, `set_config`, `get_balance`, `get_sessions`, `get_usage` | ||
| 319 | - [ ] `set_payout`, `set_metric`, `set_price`, `wallet_send`, `wallet_melt` | ||
| 320 | 153 | ||
| 321 | ### Auth | 154 | ### Playwright Video Recording Fix |
| 322 | - [ ] Only accept commands from owner npub | 155 | - [ ] Per-test context isolation (not shared serial context) |
| 156 | - [ ] Verify `.webm` files generated in test-results/ | ||
| 323 | 157 | ||
| 324 | ### Integration (tollgate_main.c) | 158 | ### OpenWRT Interop |
| 325 | - [ ] Start CVM server alongside wifistr | 159 | - [ ] Investigate `nofee.testnut.cashu.space` API compatibility issues |
| 326 | - [ ] Update `main/CMakeLists.txt` | 160 | - [ ] Fix cashu CLI v0.19.2 Pydantic validation failures with missing `active` field |
| 327 | 161 | ||
| 328 | ### Unit Tests | 162 | ### Board B |
| 329 | - [ ] `tests/unit/test_nip44.c` — encrypt/decrypt roundtrip | 163 | - [ ] Flash Board B with current firmware (different nsec) |
| 330 | - [ ] `tests/unit/test_mcp_handler.c` — JSON-RPC parse, tool dispatch | 164 | - [ ] Cross-board payment test: Board B → Board A |
| 165 | - [ ] ESP32→ESP32 auto-payment (Scenario 5) | ||
| 331 | 166 | ||
| 332 | ### Test Cases 53-56 | 167 | --- |
| 333 | - [ ] Test 53: NIP-44 encrypt/decrypt | ||
| 334 | - [ ] Test 54: MCP JSON-RPC parse | ||
| 335 | - [ ] Test 55: Config change via DM | ||
| 336 | - [ ] Test 56: Balance query via CVM | ||
| 337 | 168 | ||
| 338 | ## Reminders | 169 | ## Reminders |
| 339 | - Do NOT ask for instructions — proceed independently, skip blocked items, work on unblocked ones | ||
| 340 | - **Commit + push every time a test passes that previously didn't pass** | 170 | - **Commit + push every time a test passes that previously didn't pass** |
| 341 | - Board A: `/dev/ttyACM0`, factory MAC `94:a9:90:2e:37:7c` | 171 | - Board A: `/dev/ttyACM0`, MAC `94:a9:90:2e:37:7c`, SSID `TollGate-C0E9CA`, AP IP `10.192.45.1` |
| 342 | - Board B: `/dev/ttyACM1`, factory MAC `fc:01:2c:c5:50:50` | 172 | - Board B: `/dev/ttyACM1`, MAC `fc:01:2c:c5:50:50` |
| 343 | - Identity is now derived from nsec in config.json (SSID, IP, MAC all deterministic) | 173 | - OpenWRT Router: SSH `root@10.47.41.1`, port 2121 |
| 344 | - testnut.cashu.space auto-pays invoices: `cashu -h https://testnut.cashu.space invoice <amount>` | 174 | - `source ~/esp/esp-idf/export.sh` before `idf.py` |
| 345 | - Token generation: `cashu -h https://testnut.cashu.space send --legacy <amount> 2>&1 | grep '^cashuA' | head -1` | 175 | - Latest commit: `3342c8e` |
| 176 | - 156 unit tests + 18 Playwright tests — all passing | ||
| 346 | - sudo password: `c03rad0r123` | 177 | - sudo password: `c03rad0r123` |
| 347 | - Run `make test-unit` after any code change — must pass before commit | 178 | - Token generation: `cashu -h https://testnut.cashu.space send --legacy 21` |
| 348 | - See `AGENTS.md` for full testing rules and project context | 179 | - See `AGENTS.md` for full testing rules |
| 349 | - Proceed to Phase 4 after completing Phase 3 | ||
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) | |||
| 2 | set(PARTITION_CSV_PATH "${CMAKE_SOURCE_DIR}/partitions.csv") | 2 | set(PARTITION_CSV_PATH "${CMAKE_SOURCE_DIR}/partitions.csv") |
| 3 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) | 3 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) |
| 4 | project(esp32-tollgate) | 4 | project(esp32-tollgate) |
| 5 | |||
| 6 | idf_component_get_property(lwip lwip COMPONENT_LIB) | ||
| 7 | target_compile_options(${lwip} PRIVATE "-I${PROJECT_DIR}/main") | ||
| 8 | target_compile_definitions(${lwip} PRIVATE "-DESP_IDF_LWIP_HOOK_FILENAME=\"lwip_tollgate_hooks.h\"") | ||
| @@ -286,7 +286,7 @@ Publishes TollGate node to Nostr as kind 38787 (wifistr): | |||
| 286 | | 37 | 5 consecutive payments | Loop | All authenticated | TODO | | 286 | | 37 | 5 consecutive payments | Loop | All authenticated | TODO | |
| 287 | | 38 | Stress: rapid pay/expire | Loop with short sessions | No crash/leak | TODO | | 287 | | 38 | Stress: rapid pay/expire | Loop with short sessions | No crash/leak | TODO | |
| 288 | 288 | ||
| 289 | ### Phase 4: ESP32 TollGate Client Detection + Auto-Payment — IN PROGRESS | 289 | ### Phase 4: ESP32 TollGate Client Detection + Auto-Payment — COMPLETE |
| 290 | 290 | ||
| 291 | **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. | 291 | **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. |
| 292 | 292 | ||
| @@ -354,7 +354,7 @@ IDLE → [STA got IP] → DETECTING → [kind=10021 found] → NEEDS_PAY | |||
| 354 | 354 | ||
| 355 | 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. | 355 | 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. |
| 356 | 356 | ||
| 357 | ### Phase 5: Lightning Auto-Payout — NOT STARTED | 357 | ### Phase 5: Lightning Auto-Payout — COMPLETE |
| 358 | 358 | ||
| 359 | **Goal:** When wallet balance exceeds a configurable threshold, automatically pay out to Lightning addresses via LNURL-pay + Cashu NUT-05 melt. | 359 | **Goal:** When wallet balance exceeds a configurable threshold, automatically pay out to Lightning addresses via LNURL-pay + Cashu NUT-05 melt. |
| 360 | 360 | ||
| @@ -420,7 +420,7 @@ Wraps `Wallet::request_melt_quote()` + `Wallet::melt_tokens()` (NUT-05). | |||
| 420 | | 47 | Melt with fee tolerance | Integration | Invoice paid, change received | TODO | | 420 | | 47 | Melt with fee tolerance | Integration | Invoice paid, change received | TODO | |
| 421 | | 48 | Full payout cycle | E2E | Wallet drains to min_balance | TODO | | 421 | | 48 | Full payout cycle | E2E | Wallet drains to min_balance | TODO | |
| 422 | 422 | ||
| 423 | ### Phase 6: Bytes-Based Billing — NOT STARTED | 423 | ### Phase 6: Bytes-Based Billing — COMPLETE |
| 424 | 424 | ||
| 425 | **Goal:** Support both time-based (milliseconds) and data-based (bytes) billing metrics. Mirrors the Go implementation's dual-metric system. | 425 | **Goal:** Support both time-based (milliseconds) and data-based (bytes) billing metrics. Mirrors the Go implementation's dual-metric system. |
| 426 | 426 | ||
| @@ -473,7 +473,7 @@ uint64_t bytes_consumed; | |||
| 473 | | 51 | NAPT byte counting | Integration | Counters match actual traffic | TODO | | 473 | | 51 | NAPT byte counting | Integration | Counters match actual traffic | TODO | |
| 474 | | 52 | Bytes metric end-to-end | E2E | Client disconnected after data cap | TODO | | 474 | | 52 | Bytes metric end-to-end | E2E | Client disconnected after data cap | TODO | |
| 475 | 475 | ||
| 476 | ### Phase 7: ContextVM Server (MCP over Nostr) — NOT STARTED | 476 | ### Phase 7: ContextVM Server (MCP over Nostr) — COMPLETE |
| 477 | 477 | ||
| 478 | **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. | 478 | **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. |
| 479 | 479 | ||
| @@ -524,6 +524,73 @@ Only accept commands from owner npub (derived from nsec in config.json). | |||
| 524 | 524 | ||
| 525 | ## Total: 56 Tests across 7 phases | 525 | ## Total: 56 Tests across 7 phases |
| 526 | 526 | ||
| 527 | ## Post-Phase 7: Bug Fixes & Architecture Improvements | ||
| 528 | |||
| 529 | ### Per-Client NAT Filtering (Multi-Client Isolation Fix) | ||
| 530 | |||
| 531 | **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. | ||
| 532 | |||
| 533 | **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. | ||
| 534 | |||
| 535 | **Architecture Change:** | ||
| 536 | - **Before:** NAT is a global toggle — ON when any client is auth'd, OFF when none are | ||
| 537 | - **After:** NAT stays always ON. Per-client filtering happens in the lwIP forwarding path via `LWIP_HOOK_IP4_CANFORWARD` | ||
| 538 | |||
| 539 | **Files:** | ||
| 540 | |||
| 541 | | File | Action | Description | | ||
| 542 | |------|--------|-------------| | ||
| 543 | | `main/lwip_tollgate_hooks.h` | Create | Defines `LWIP_HOOK_IP4_CANFORWARD` macro | | ||
| 544 | | `CMakeLists.txt` | Modify | Inject hook header into lwIP compilation | | ||
| 545 | | `main/firewall.c` | Modify | Add filter function, remove global NAT toggle | | ||
| 546 | | `main/firewall.h` | Modify | Expose filter function declaration | | ||
| 547 | | `main/tollgate_main.c` | Modify | Remove `firewall_disable_nat()` from stop_services | | ||
| 548 | |||
| 549 | **Implementation:** | ||
| 550 | |||
| 551 | 1. `lwip_tollgate_hooks.h` — hook header included by lwIP: | ||
| 552 | ```c | ||
| 553 | #define LWIP_HOOK_IP4_CANFORWARD(p, addr) tollgate_ip4_canforward_filter(p, addr) | ||
| 554 | ``` | ||
| 555 | |||
| 556 | 2. `CMakeLists.txt` — inject into lwIP build (follows ESP-IDF vlan example pattern): | ||
| 557 | ```cmake | ||
| 558 | idf_component_get_property(lwip lwip COMPONENT_LIB) | ||
| 559 | target_compile_options(${lwip} PRIVATE "-I${PROJECT_DIR}/main") | ||
| 560 | target_compile_definitions(${lwip} PRIVATE "-DESP_IDF_LWIP_HOOK_FILENAME=\"lwip_tollgate_hooks.h\"") | ||
| 561 | ``` | ||
| 562 | |||
| 563 | 3. `firewall.c` — filter function checks source IP against allowed client list: | ||
| 564 | ```c | ||
| 565 | int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder) { | ||
| 566 | struct ip_hdr *iphdr = (struct ip_hdr *)p->payload; | ||
| 567 | uint32_t src_ip = lwip_ntohl(iphdr->src.addr); | ||
| 568 | return firewall_is_client_allowed(src_ip) ? 1 : 0; | ||
| 569 | } | ||
| 570 | ``` | ||
| 571 | |||
| 572 | 4. NAT management simplified: enable once during `firewall_init()`, never disable. Remove `update_nat()`, `firewall_enable_nat()`, `firewall_disable_nat()`. | ||
| 573 | |||
| 574 | **Key properties:** | ||
| 575 | - Hook fires only for **forwarded** packets (AP client → internet), not packets to ESP32 itself | ||
| 576 | - Captive portal (port 80) and API (port 2121) remain accessible to all clients | ||
| 577 | - DNS hijack continues independently — both layers enforce auth | ||
| 578 | - Return traffic: NAPT only creates DNAT entries for successfully forwarded outbound packets, so blocked clients don't get return traffic either | ||
| 579 | - Performance: linear scan of ≤10 clients per packet — negligible cost | ||
| 580 | |||
| 581 | ### Spent-Secret Cleanup | ||
| 582 | |||
| 583 | **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). | ||
| 584 | |||
| 585 | **Changes:** | ||
| 586 | - Remove `s_spent_secrets[]`, `s_spent_count`, `session_is_secret_spent()` from `session.c` | ||
| 587 | - Remove `spent_secrets` and `spent_secret_count` from `session_t` struct | ||
| 588 | - Remove `spent_secrets` params from `session_create()` / `session_create_bytes()` | ||
| 589 | - Remove local spent-secret check in `tollgate_api.c` — keep only mint `check_proof_states` check | ||
| 590 | - Update unit tests | ||
| 591 | |||
| 592 | **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. | ||
| 593 | |||
| 527 | ## Testing Infrastructure | 594 | ## Testing Infrastructure |
| 528 | 595 | ||
| 529 | ### Three-Layer Test Architecture | 596 | ### 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 @@ | |||
| 6 | #include "lwip/lwip_napt.h" | 6 | #include "lwip/lwip_napt.h" |
| 7 | #include "lwip/etharp.h" | 7 | #include "lwip/etharp.h" |
| 8 | #include "lwip/netif.h" | 8 | #include "lwip/netif.h" |
| 9 | #include "lwip/prot/ip4.h" | ||
| 9 | #include <string.h> | 10 | #include <string.h> |
| 10 | 11 | ||
| 11 | #define MAX_CLIENTS 10 | 12 | #define MAX_CLIENTS 10 |
| 12 | 13 | ||
| 13 | static const char *TAG = "firewall"; | 14 | static const char *TAG = "firewall"; |
| 14 | static esp_ip4_addr_t s_ap_ip; | 15 | static esp_ip4_addr_t s_ap_ip; |
| 15 | static bool s_nat_enabled = false; | ||
| 16 | 16 | ||
| 17 | typedef struct { | 17 | typedef struct { |
| 18 | uint32_t ip; | 18 | uint32_t ip; |
| @@ -22,11 +22,6 @@ typedef struct { | |||
| 22 | static fw_client_t s_clients[MAX_CLIENTS]; | 22 | static fw_client_t s_clients[MAX_CLIENTS]; |
| 23 | static int s_client_count = 0; | 23 | static int s_client_count = 0; |
| 24 | 24 | ||
| 25 | static struct netif *get_ap_netif(void) | ||
| 26 | { | ||
| 27 | return netif_get_by_index(NETIF_NO_INDEX); | ||
| 28 | } | ||
| 29 | |||
| 30 | esp_err_t firewall_get_mac_for_ip(uint32_t client_ip, char *mac_out, size_t mac_out_size) | 25 | esp_err_t firewall_get_mac_for_ip(uint32_t client_ip, char *mac_out, size_t mac_out_size) |
| 31 | { | 26 | { |
| 32 | wifi_sta_list_t sta_list; | 27 | wifi_sta_list_t sta_list; |
| @@ -66,38 +61,25 @@ esp_err_t firewall_init(esp_ip4_addr_t ap_ip) | |||
| 66 | s_ap_ip = ap_ip; | 61 | s_ap_ip = ap_ip; |
| 67 | memset(s_clients, 0, sizeof(s_clients)); | 62 | memset(s_clients, 0, sizeof(s_clients)); |
| 68 | s_client_count = 0; | 63 | s_client_count = 0; |
| 69 | ESP_LOGI(TAG, "Firewall initialized with AP IP=" IPSTR, IP2STR(&s_ap_ip)); | 64 | ip_napt_enable(s_ap_ip.addr, 1); |
| 65 | ESP_LOGI(TAG, "Firewall initialized with AP IP=" IPSTR " (NAT always on, per-client filter)", IP2STR(&s_ap_ip)); | ||
| 70 | return ESP_OK; | 66 | return ESP_OK; |
| 71 | } | 67 | } |
| 72 | 68 | ||
| 73 | static void update_nat(void) | 69 | int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder) |
| 74 | { | 70 | { |
| 75 | bool should_enable = (s_client_count > 0); | 71 | (void)dest_addr_hostorder; |
| 76 | if (should_enable && !s_nat_enabled) { | 72 | if (p->len < IP_HLEN) return -1; |
| 77 | ip_napt_enable(s_ap_ip.addr, 1); | 73 | struct ip_hdr *iphdr = (struct ip_hdr *)p->payload; |
| 78 | s_nat_enabled = true; | 74 | uint32_t src_ip_h = lwip_ntohl(iphdr->src.addr); |
| 79 | ESP_LOGI(TAG, "NAT enabled (client authenticated)"); | 75 | uint32_t ap_subnet = lwip_ntohl(s_ap_ip.addr) & 0xFFFFFF00; |
| 80 | } else if (!should_enable && s_nat_enabled) { | 76 | if ((src_ip_h & 0xFFFFFF00) != ap_subnet) { |
| 81 | ip_napt_enable(s_ap_ip.addr, 0); | 77 | return 1; |
| 82 | s_nat_enabled = false; | ||
| 83 | ESP_LOGI(TAG, "NAT disabled (no authenticated clients)"); | ||
| 84 | } | 78 | } |
| 85 | } | 79 | if (firewall_is_client_allowed(iphdr->src.addr)) { |
| 86 | 80 | return 1; | |
| 87 | void firewall_enable_nat(void) | 81 | } |
| 88 | { | 82 | return 0; |
| 89 | if (s_nat_enabled) return; | ||
| 90 | ip_napt_enable(s_ap_ip.addr, 1); | ||
| 91 | s_nat_enabled = true; | ||
| 92 | ESP_LOGI(TAG, "NAT enabled"); | ||
| 93 | } | ||
| 94 | |||
| 95 | void firewall_disable_nat(void) | ||
| 96 | { | ||
| 97 | if (!s_nat_enabled) return; | ||
| 98 | ip_napt_enable(s_ap_ip.addr, 0); | ||
| 99 | s_nat_enabled = false; | ||
| 100 | ESP_LOGI(TAG, "NAT disabled"); | ||
| 101 | } | 83 | } |
| 102 | 84 | ||
| 103 | static fw_client_t *find_client_by_ip(uint32_t client_ip) | 85 | static fw_client_t *find_client_by_ip(uint32_t client_ip) |
| @@ -137,7 +119,6 @@ void firewall_grant_access(uint32_t client_ip) | |||
| 137 | s_client_count++; | 119 | s_client_count++; |
| 138 | 120 | ||
| 139 | dns_server_set_client_authenticated(client_ip, true); | 121 | dns_server_set_client_authenticated(client_ip, true); |
| 140 | update_nat(); | ||
| 141 | 122 | ||
| 142 | esp_ip4_addr_t ip_addr = { .addr = client_ip }; | 123 | esp_ip4_addr_t ip_addr = { .addr = client_ip }; |
| 143 | ESP_LOGI(TAG, "Access granted to " IPSTR " mac=%s", IP2STR(&ip_addr), | 124 | ESP_LOGI(TAG, "Access granted to " IPSTR " mac=%s", IP2STR(&ip_addr), |
| @@ -154,7 +135,6 @@ void firewall_revoke_access(uint32_t client_ip) | |||
| 154 | s_clients[i] = s_clients[s_client_count - 1]; | 135 | s_clients[i] = s_clients[s_client_count - 1]; |
| 155 | s_client_count--; | 136 | s_client_count--; |
| 156 | dns_server_set_client_authenticated(client_ip, false); | 137 | dns_server_set_client_authenticated(client_ip, false); |
| 157 | update_nat(); | ||
| 158 | return; | 138 | return; |
| 159 | } | 139 | } |
| 160 | } | 140 | } |
| @@ -166,7 +146,6 @@ void firewall_revoke_all(void) | |||
| 166 | dns_server_set_client_authenticated(s_clients[i].ip, false); | 146 | dns_server_set_client_authenticated(s_clients[i].ip, false); |
| 167 | } | 147 | } |
| 168 | s_client_count = 0; | 148 | s_client_count = 0; |
| 169 | update_nat(); | ||
| 170 | ESP_LOGI(TAG, "All client access revoked"); | 149 | ESP_LOGI(TAG, "All client access revoked"); |
| 171 | } | 150 | } |
| 172 | 151 | ||
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 @@ | |||
| 6 | #include <stdbool.h> | 6 | #include <stdbool.h> |
| 7 | #include <stdint.h> | 7 | #include <stdint.h> |
| 8 | 8 | ||
| 9 | struct pbuf; | ||
| 10 | |||
| 9 | #define FW_MAX_MAC_LEN 18 | 11 | #define FW_MAX_MAC_LEN 18 |
| 10 | 12 | ||
| 11 | esp_err_t firewall_init(esp_ip4_addr_t ap_ip); | 13 | esp_err_t firewall_init(esp_ip4_addr_t ap_ip); |
| 12 | void firewall_enable_nat(void); | ||
| 13 | void firewall_disable_nat(void); | ||
| 14 | void firewall_grant_access(uint32_t client_ip); | 14 | void firewall_grant_access(uint32_t client_ip); |
| 15 | void firewall_revoke_access(uint32_t client_ip); | 15 | void firewall_revoke_access(uint32_t client_ip); |
| 16 | void firewall_revoke_all(void); | 16 | void firewall_revoke_all(void); |
| @@ -20,4 +20,6 @@ int firewall_client_count(void); | |||
| 20 | 20 | ||
| 21 | esp_err_t firewall_get_mac_for_ip(uint32_t client_ip, char *mac_out, size_t mac_out_size); | 21 | esp_err_t firewall_get_mac_for_ip(uint32_t client_ip, char *mac_out, size_t mac_out_size); |
| 22 | 22 | ||
| 23 | int tollgate_ip4_canforward_filter(struct pbuf *p, uint32_t dest_addr_hostorder); | ||
| 24 | |||
| 23 | #endif | 25 | #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 @@ | |||
| 1 | #ifndef LWIP_TOLLGATE_HOOKS_H | ||
| 2 | #define LWIP_TOLLGATE_HOOKS_H | ||
| 3 | |||
| 4 | #include "lwip/pbuf.h" | ||
| 5 | |||
| 6 | int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder); | ||
| 7 | |||
| 8 | #define LWIP_HOOK_IP4_CANFORWARD(p, addr) tollgate_ip4_canforward_filter(p, addr) | ||
| 9 | |||
| 10 | #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 @@ | |||
| 7 | #include "freertos/task.h" | 7 | #include "freertos/task.h" |
| 8 | #include <string.h> | 8 | #include <string.h> |
| 9 | 9 | ||
| 10 | #define SPENT_SECRETS_MAX 100 | ||
| 11 | |||
| 12 | static const char *TAG = "session"; | 10 | static const char *TAG = "session"; |
| 13 | static session_t s_sessions[SESSION_MAX_CLIENTS]; | 11 | static session_t s_sessions[SESSION_MAX_CLIENTS]; |
| 14 | static int s_session_count = 0; | 12 | static int s_session_count = 0; |
| 15 | 13 | ||
| 16 | static char s_spent_secrets[SPENT_SECRETS_MAX][65]; | ||
| 17 | static int s_spent_count = 0; | ||
| 18 | |||
| 19 | static int64_t get_time_ms(void) | 14 | static int64_t get_time_ms(void) |
| 20 | { | 15 | { |
| 21 | return (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | 16 | return (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; |
| @@ -25,7 +20,6 @@ esp_err_t session_manager_init(void) | |||
| 25 | { | 20 | { |
| 26 | memset(s_sessions, 0, sizeof(s_sessions)); | 21 | memset(s_sessions, 0, sizeof(s_sessions)); |
| 27 | s_session_count = 0; | 22 | s_session_count = 0; |
| 28 | s_spent_count = 0; | ||
| 29 | ESP_LOGI(TAG, "Session manager initialized"); | 23 | ESP_LOGI(TAG, "Session manager initialized"); |
| 30 | return ESP_OK; | 24 | return ESP_OK; |
| 31 | } | 25 | } |
| @@ -37,27 +31,14 @@ static void populate_mac(session_t *session, uint32_t client_ip) | |||
| 37 | } | 31 | } |
| 38 | } | 32 | } |
| 39 | 33 | ||
| 40 | session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, | 34 | session_t *session_create(uint32_t client_ip, uint64_t allotment_ms) |
| 41 | const char *spent_secrets[], int secret_count) | ||
| 42 | { | 35 | { |
| 43 | session_t *existing = session_find_by_ip(client_ip); | 36 | session_t *existing = session_find_by_ip(client_ip); |
| 44 | if (existing) { | 37 | if (existing) { |
| 45 | session_extend(existing, allotment_ms); | 38 | session_extend(existing, allotment_ms); |
| 46 | for (int i = 0; i < secret_count && s_spent_count < SPENT_SECRETS_MAX; i++) { | ||
| 47 | strncpy(s_spent_secrets[s_spent_count], spent_secrets[i], 64); | ||
| 48 | s_spent_secrets[s_spent_count][64] = '\0'; | ||
| 49 | s_spent_count++; | ||
| 50 | } | ||
| 51 | return existing; | 39 | return existing; |
| 52 | } | 40 | } |
| 53 | 41 | ||
| 54 | for (int i = 0; i < secret_count; i++) { | ||
| 55 | if (session_is_secret_spent(spent_secrets[i])) { | ||
| 56 | ESP_LOGW(TAG, "Duplicate secret rejected"); | ||
| 57 | return NULL; | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | if (s_session_count >= SESSION_MAX_CLIENTS) { | 42 | if (s_session_count >= SESSION_MAX_CLIENTS) { |
| 62 | for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { | 43 | for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { |
| 63 | if (!s_sessions[i].active || session_is_expired(&s_sessions[i])) { | 44 | 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, | |||
| 73 | s_sessions[i].allotment_ms = allotment_ms; | 54 | s_sessions[i].allotment_ms = allotment_ms; |
| 74 | s_sessions[i].start_time_ms = get_time_ms(); | 55 | s_sessions[i].start_time_ms = get_time_ms(); |
| 75 | s_sessions[i].active = true; | 56 | s_sessions[i].active = true; |
| 76 | s_sessions[i].spent_secret_count = 0; | ||
| 77 | populate_mac(&s_sessions[i], client_ip); | 57 | populate_mac(&s_sessions[i], client_ip); |
| 78 | 58 | ||
| 79 | for (int j = 0; j < secret_count && j < 5; j++) { | ||
| 80 | strncpy(s_sessions[i].spent_secrets[s_sessions[i].spent_secret_count], | ||
| 81 | spent_secrets[j], 64); | ||
| 82 | s_sessions[i].spent_secrets[s_sessions[i].spent_secret_count][64] = '\0'; | ||
| 83 | s_sessions[i].spent_secret_count++; | ||
| 84 | } | ||
| 85 | |||
| 86 | for (int j = 0; j < secret_count && s_spent_count < SPENT_SECRETS_MAX; j++) { | ||
| 87 | strncpy(s_spent_secrets[s_spent_count], spent_secrets[j], 64); | ||
| 88 | s_spent_secrets[s_spent_count][64] = '\0'; | ||
| 89 | s_spent_count++; | ||
| 90 | } | ||
| 91 | |||
| 92 | s_session_count++; | 59 | s_session_count++; |
| 93 | firewall_grant_access(client_ip); | 60 | firewall_grant_access(client_ip); |
| 94 | 61 | ||
| @@ -104,10 +71,9 @@ session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, | |||
| 104 | return NULL; | 71 | return NULL; |
| 105 | } | 72 | } |
| 106 | 73 | ||
| 107 | session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes, | 74 | session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes) |
| 108 | const char *spent_secrets[], int secret_count) | ||
| 109 | { | 75 | { |
| 110 | session_t *s = session_create(client_ip, 0, spent_secrets, secret_count); | 76 | session_t *s = session_create(client_ip, 0); |
| 111 | if (s) { | 77 | if (s) { |
| 112 | s->allotment_bytes = allotment_bytes; | 78 | s->allotment_bytes = allotment_bytes; |
| 113 | s->bytes_consumed = 0; | 79 | s->bytes_consumed = 0; |
| @@ -170,14 +136,6 @@ bool session_is_expired(const session_t *session) | |||
| 170 | return elapsed >= (int64_t)session->allotment_ms; | 136 | return elapsed >= (int64_t)session->allotment_ms; |
| 171 | } | 137 | } |
| 172 | 138 | ||
| 173 | bool session_is_secret_spent(const char *secret) | ||
| 174 | { | ||
| 175 | for (int i = 0; i < s_spent_count; i++) { | ||
| 176 | if (strncmp(s_spent_secrets[i], secret, 64) == 0) return true; | ||
| 177 | } | ||
| 178 | return false; | ||
| 179 | } | ||
| 180 | |||
| 181 | void session_check_expiry(void) | 139 | void session_check_expiry(void) |
| 182 | { | 140 | { |
| 183 | for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { | 141 | 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 { | |||
| 16 | uint64_t allotment_bytes; | 16 | uint64_t allotment_bytes; |
| 17 | uint64_t bytes_consumed; | 17 | uint64_t bytes_consumed; |
| 18 | bool active; | 18 | bool active; |
| 19 | char spent_secrets[5][65]; | ||
| 20 | int spent_secret_count; | ||
| 21 | } session_t; | 19 | } session_t; |
| 22 | 20 | ||
| 23 | esp_err_t session_manager_init(void); | 21 | esp_err_t session_manager_init(void); |
| 24 | 22 | ||
| 25 | session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, | 23 | session_t *session_create(uint32_t client_ip, uint64_t allotment_ms); |
| 26 | const char *spent_secrets[], int secret_count); | ||
| 27 | 24 | ||
| 28 | session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes, | 25 | session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes); |
| 29 | const char *spent_secrets[], int secret_count); | ||
| 30 | 26 | ||
| 31 | void session_add_bytes(uint32_t client_ip, uint64_t bytes); | 27 | void session_add_bytes(uint32_t client_ip, uint64_t bytes); |
| 32 | 28 | ||
| @@ -37,8 +33,6 @@ void session_extend(session_t *session, uint64_t additional_ms); | |||
| 37 | 33 | ||
| 38 | bool session_is_expired(const session_t *session); | 34 | bool session_is_expired(const session_t *session); |
| 39 | 35 | ||
| 40 | bool session_is_secret_spent(const char *secret); | ||
| 41 | |||
| 42 | void session_check_expiry(void); | 36 | void session_check_expiry(void); |
| 43 | 37 | ||
| 44 | void session_revoke(session_t *session); | 38 | 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) | |||
| 224 | return ESP_OK; | 224 | return ESP_OK; |
| 225 | } | 225 | } |
| 226 | 226 | ||
| 227 | for (int i = 0; i < token->proof_count; i++) { | ||
| 228 | if (session_is_secret_spent(token->proofs[i].secret)) { | ||
| 229 | free(token); | ||
| 230 | cJSON *notice = create_notice("error", "payment-error-token-spent", "Token already spent"); | ||
| 231 | char *json = cJSON_PrintUnformatted(notice); | ||
| 232 | httpd_resp_set_status(req, "402 Payment Required"); | ||
| 233 | httpd_resp_set_type(req, "application/json"); | ||
| 234 | httpd_resp_send(req, json, strlen(json)); | ||
| 235 | cJSON_free(json); | ||
| 236 | cJSON_Delete(notice); | ||
| 237 | return ESP_OK; | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 241 | cashu_proof_state_t *states = malloc(CASHU_MAX_PROOFS * sizeof(cashu_proof_state_t)); | 227 | cashu_proof_state_t *states = malloc(CASHU_MAX_PROOFS * sizeof(cashu_proof_state_t)); |
| 242 | if (!states) { | 228 | if (!states) { |
| 243 | free(token); | 229 | free(token); |
| @@ -299,16 +285,11 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 299 | return ESP_OK; | 285 | return ESP_OK; |
| 300 | } | 286 | } |
| 301 | 287 | ||
| 302 | int secret_count = token->proof_count > 5 ? 5 : token->proof_count; | ||
| 303 | const char *secrets[5]; | ||
| 304 | for (int i = 0; i < secret_count; i++) { | ||
| 305 | secrets[i] = token->proofs[i].secret; | ||
| 306 | } | ||
| 307 | session_t *session; | 288 | session_t *session; |
| 308 | if (is_bytes) { | 289 | if (is_bytes) { |
| 309 | session = session_create_bytes(client_ip, allotment, secrets, secret_count); | 290 | session = session_create_bytes(client_ip, allotment); |
| 310 | } else { | 291 | } else { |
| 311 | session = session_create(client_ip, allotment, secrets, secret_count); | 292 | session = session_create(client_ip, allotment); |
| 312 | } | 293 | } |
| 313 | if (!session) { | 294 | if (!session) { |
| 314 | free(states); | 295 | free(states); |
| @@ -498,7 +479,7 @@ esp_err_t tollgate_api_start(void) | |||
| 498 | config.server_port = 2121; | 479 | config.server_port = 2121; |
| 499 | config.ctrl_port = 32769; | 480 | config.ctrl_port = 32769; |
| 500 | config.max_uri_handlers = 10; | 481 | config.max_uri_handlers = 10; |
| 501 | config.stack_size = 32768; | 482 | config.stack_size = 16384; |
| 502 | 483 | ||
| 503 | esp_err_t ret = httpd_start(&s_api_server, &config); | 484 | esp_err_t ret = httpd_start(&s_api_server, &config); |
| 504 | if (ret != ESP_OK) { | 485 | 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) | |||
| 174 | tollgate_api_stop(); | 174 | tollgate_api_stop(); |
| 175 | dns_server_stop(); | 175 | dns_server_stop(); |
| 176 | cvm_server_stop(); | 176 | cvm_server_stop(); |
| 177 | firewall_disable_nat(); | ||
| 178 | firewall_revoke_all(); | 177 | firewall_revoke_all(); |
| 179 | s_services_running = false; | 178 | s_services_running = false; |
| 180 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); | 179 | 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 @@ | |||
| 1 | #ifndef STUB_ESP_RANDOM_H | ||
| 2 | #define STUB_ESP_RANDOM_H | ||
| 3 | |||
| 4 | #include "esp_system.h" | ||
| 5 | |||
| 6 | #endif | ||
diff --git a/tests/unit/test_identity b/tests/unit/test_identity index 7ad1485..277bb49 100755 --- a/tests/unit/test_identity +++ b/tests/unit/test_identity | |||
| Binary files differ | |||
diff --git a/tests/unit/test_mcp_handler b/tests/unit/test_mcp_handler new file mode 100755 index 0000000..b5d6a85 --- /dev/null +++ b/tests/unit/test_mcp_handler | |||
| Binary files differ | |||
diff --git a/tests/unit/test_nip04 b/tests/unit/test_nip04 new file mode 100755 index 0000000..cb52040 --- /dev/null +++ b/tests/unit/test_nip04 | |||
| Binary files 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) | |||
| 46 | ASSERT_EQ_INT(0, session_active_count(), "No sessions after init"); | 46 | ASSERT_EQ_INT(0, session_active_count(), "No sessions after init"); |
| 47 | 47 | ||
| 48 | printf("\n--- session_create ---\n"); | 48 | printf("\n--- session_create ---\n"); |
| 49 | const char *secrets[] = {"secret1", "secret2"}; | 49 | session_t *s = session_create(0x0A01A8C0, 60000); |
| 50 | session_t *s = session_create(0x0A01A8C0, 60000, secrets, 2); | ||
| 51 | ASSERT(s != NULL, "session_create returns non-NULL"); | 50 | ASSERT(s != NULL, "session_create returns non-NULL"); |
| 52 | ASSERT_EQ_INT(1, session_active_count(), "1 session after create"); | 51 | ASSERT_EQ_INT(1, session_active_count(), "1 session after create"); |
| 53 | ASSERT_EQ_INT(1, g_granted_count, "firewall_grant_access was called"); | 52 | ASSERT_EQ_INT(1, g_granted_count, "firewall_grant_access was called"); |
| @@ -57,23 +56,18 @@ static void test_sessions(void) | |||
| 57 | ASSERT(found == s, "session_find_by_ip returns the created session"); | 56 | ASSERT(found == s, "session_find_by_ip returns the created session"); |
| 58 | ASSERT(session_find_by_ip(0x01020304) == NULL, "session_find_by_ip returns NULL for unknown IP"); | 57 | ASSERT(session_find_by_ip(0x01020304) == NULL, "session_find_by_ip returns NULL for unknown IP"); |
| 59 | 58 | ||
| 60 | printf("\n--- session_is_secret_spent ---\n"); | ||
| 61 | ASSERT(session_is_secret_spent("secret1"), "secret1 is marked spent"); | ||
| 62 | ASSERT(session_is_secret_spent("secret2"), "secret2 is marked spent"); | ||
| 63 | ASSERT(!session_is_secret_spent("secret_unknown"), "unknown secret is not spent"); | ||
| 64 | |||
| 65 | printf("\n--- Duplicate secret rejected ---\n"); | ||
| 66 | const char *dup_secrets[] = {"secret1"}; | ||
| 67 | g_granted_count = 0; | ||
| 68 | session_t *dup = session_create(0x0B01A8C0, 60000, dup_secrets, 1); | ||
| 69 | ASSERT(dup == NULL, "Duplicate secret returns NULL"); | ||
| 70 | ASSERT_EQ_INT(0, g_granted_count, "No new firewall grant for duplicate"); | ||
| 71 | |||
| 72 | printf("\n--- session_extend ---\n"); | 59 | printf("\n--- session_extend ---\n"); |
| 73 | uint64_t old_allotment = s->allotment_ms; | 60 | uint64_t old_allotment = s->allotment_ms; |
| 74 | session_extend(s, 30000); | 61 | session_extend(s, 30000); |
| 75 | ASSERT(s->allotment_ms == old_allotment + 30000, "Allotment extended by 30000ms"); | 62 | ASSERT(s->allotment_ms == old_allotment + 30000, "Allotment extended by 30000ms"); |
| 76 | 63 | ||
| 64 | printf("\n--- session_extend for existing client ---\n"); | ||
| 65 | g_granted_count = 0; | ||
| 66 | session_t *s2 = session_create(0x0A01A8C0, 30000); | ||
| 67 | ASSERT(s2 == s, "same IP returns existing session"); | ||
| 68 | ASSERT_EQ_INT(0, g_granted_count, "no new firewall grant for extension"); | ||
| 69 | ASSERT(s->allotment_ms == old_allotment + 60000, "allotment extended by 30000ms on re-pay"); | ||
| 70 | |||
| 77 | printf("\n--- session_revoke ---\n"); | 71 | printf("\n--- session_revoke ---\n"); |
| 78 | g_revoked_count = 0; | 72 | g_revoked_count = 0; |
| 79 | session_revoke(s); | 73 | session_revoke(s); |
| @@ -81,10 +75,8 @@ static void test_sessions(void) | |||
| 81 | ASSERT_EQ_INT(0, session_active_count(), "No active sessions after revoke"); | 75 | ASSERT_EQ_INT(0, session_active_count(), "No active sessions after revoke"); |
| 82 | 76 | ||
| 83 | printf("\n--- session_revoke_all ---\n"); | 77 | printf("\n--- session_revoke_all ---\n"); |
| 84 | const char *s1[] = {"s1"}; | 78 | session_create(0x01000001, 60000); |
| 85 | const char *s2[] = {"s2"}; | 79 | session_create(0x01000002, 60000); |
| 86 | session_create(0x01000001, 60000, s1, 1); | ||
| 87 | session_create(0x01000002, 60000, s2, 1); | ||
| 88 | ASSERT_EQ_INT(2, session_active_count(), "2 sessions created"); | 80 | ASSERT_EQ_INT(2, session_active_count(), "2 sessions created"); |
| 89 | 81 | ||
| 90 | g_revoked_count = 0; | 82 | g_revoked_count = 0; |
| @@ -93,8 +85,7 @@ static void test_sessions(void) | |||
| 93 | 85 | ||
| 94 | printf("\n--- session_tick does not crash ---\n"); | 86 | printf("\n--- session_tick does not crash ---\n"); |
| 95 | session_manager_init(); | 87 | session_manager_init(); |
| 96 | const char *st[] = {"tick_secret"}; | 88 | session_create(0x0A000001, 60000); |
| 97 | session_create(0x0A000001, 60000, st, 1); | ||
| 98 | session_tick(); | 89 | session_tick(); |
| 99 | ASSERT_EQ_INT(1, session_active_count(), "Session still active after tick (not expired)"); | 90 | ASSERT_EQ_INT(1, session_active_count(), "Session still active after tick (not expired)"); |
| 100 | } | 91 | } |
| @@ -106,9 +97,8 @@ void test_bytes_sessions(void) | |||
| 106 | memset(&g_test_config, 0, sizeof(g_test_config)); | 97 | memset(&g_test_config, 0, sizeof(g_test_config)); |
| 107 | strncpy(g_test_config.metric, "bytes", sizeof(g_test_config.metric) - 1); | 98 | strncpy(g_test_config.metric, "bytes", sizeof(g_test_config.metric) - 1); |
| 108 | 99 | ||
| 109 | const char *sec[] = {"bytes_secret"}; | ||
| 110 | uint64_t allotment = 22020096; | 100 | uint64_t allotment = 22020096; |
| 111 | session_t *s = session_create_bytes(0x0A010001, allotment, sec, 1); | 101 | session_t *s = session_create_bytes(0x0A010001, allotment); |
| 112 | ASSERT(s != NULL, "bytes session created"); | 102 | ASSERT(s != NULL, "bytes session created"); |
| 113 | ASSERT_EQ_INT(1, session_active_count(), "1 active bytes session"); | 103 | ASSERT_EQ_INT(1, session_active_count(), "1 active bytes session"); |
| 114 | 104 | ||
| @@ -133,8 +123,7 @@ void test_bytes_sessions(void) | |||
| 133 | session_manager_init(); | 123 | session_manager_init(); |
| 134 | memset(&g_test_config, 0, sizeof(g_test_config)); | 124 | memset(&g_test_config, 0, sizeof(g_test_config)); |
| 135 | strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); | 125 | strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); |
| 136 | const char *ms_sec[] = {"ms_secret"}; | 126 | session_t *ms = session_create(0x0A020001, 60000); |
| 137 | session_t *ms = session_create(0x0A020001, 60000, ms_sec, 1); | ||
| 138 | ASSERT(ms != NULL, "ms session created"); | 127 | ASSERT(ms != NULL, "ms session created"); |
| 139 | ASSERT(!session_is_expired(ms), "ms session not expired immediately"); | 128 | ASSERT(!session_is_expired(ms), "ms session not expired immediately"); |
| 140 | 129 | ||
diff --git a/tests/unit/test_tollgate_client b/tests/unit/test_tollgate_client index 33b272e..f9b0f7d 100755 --- a/tests/unit/test_tollgate_client +++ b/tests/unit/test_tollgate_client | |||
| Binary files differ | |||