From 8071741815f0b0938701e80a63e80b0ec94b2778 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 17 May 2026 17:18:43 +0530 Subject: refactor: reorganize test suite, add integration tests for NAT filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move integration tests (api, network, phase2, smoke) to tests/integration/ - Move Playwright specs (captive-portal, interop-happy-path) to tests/e2e/ - Move playwright.config.mjs to tests/e2e/ - Fix hardcoded IP fallbacks: 192.168.4.1 → 10.192.45.1 - Add test-reset-auth.mjs: reset→pay→allow→revoke→block cycle - Add test-session-expiry.mjs: pay→wait 65s→verify blocked (slow test) - Add test-dns-firewall.mjs: DNS hijack/forward + per-client NAT filter - Update Makefile with test-unit, test-integration, test-e2e, test-all targets - Update package.json scripts for new paths - Fix Playwright video: retain-on-failure instead of always-on - Update AGENTS.md: per-client NAT filter description - Update CHECKLIST.md: mark completed items, add Board B identity - Board B nsec: 9af47906... → SSID TollGate-b96d80, AP IP 10.185.47.1 - 186 unit tests passing --- tests/integration/test-dns-firewall.mjs | 123 ++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/integration/test-dns-firewall.mjs (limited to 'tests/integration/test-dns-firewall.mjs') diff --git a/tests/integration/test-dns-firewall.mjs b/tests/integration/test-dns-firewall.mjs new file mode 100644 index 0000000..b69b524 --- /dev/null +++ b/tests/integration/test-dns-firewall.mjs @@ -0,0 +1,123 @@ +import { execSync } from 'child_process'; + +const IP = process.env.TOLLGATE_IP || '10.192.45.1'; +const API = `http://${IP}:2121`; +let passed = 0, failed = 0; + +function assert(cond, msg) { + if (cond) { console.log(` ✓ ${msg}`); passed++; } + else { console.log(` ✗ ${msg}`); failed++; } +} + +function run(cmd) { + try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } + catch { return null; } +} + +function runJson(cmd) { + const out = run(cmd); + try { return out ? JSON.parse(out) : null; } + catch { return null; } +} + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +function mintToken(amount = 21) { + run('cashu -h https://testnut.cashu.space invoice ' + amount + ' 2>&1'); + const out = run('cashu -h https://testnut.cashu.space send --legacy ' + amount + ' 2>&1'); + const match = out && out.match(/cashuA[a-zA-Z0-9_-]+/); + return match ? match[0] : null; +} + +function dnsResolves(domain, server) { + const result = run(`nslookup -timeout=3 ${domain} ${server} 2>&1`); + return result && result.includes('Address') && !result.includes('NXDOMAIN'); +} + +function dnsResolvesToSelf(domain) { + try { + const result = run(`nslookup ${domain} ${IP} 2>&1`); + return result && result.includes(IP); + } catch { + return false; + } +} + +function canPing(host = '8.8.8.8') { + const result = run(`ping -c 1 -W 2 -I wlp59s0 ${host} 2>/dev/null`); + return result && !result.includes('100% packet loss'); +} + +console.log(`\n=== DNS + Firewall Integration Test (target: ${IP}) ===\n`); + +console.log('--- Part 1: Before Authentication ---\n'); + +console.log('1. DNS hijack: resolves to ESP32 AP IP'); +assert(dnsResolvesToSelf('google.com'), 'google.com resolves to AP IP'); +assert(dnsResolvesToSelf('random-test.example.com'), 'random domain resolves to AP IP'); + +console.log('\n2. DNS hijack: upstream DNS not reachable'); +const upstreamResolve = run(`nslookup -timeout=3 google.com 8.8.8.8 2>&1`); +assert(!upstreamResolve || upstreamResolve.includes('connection timed out') || upstreamResolve.includes('no servers'), 'Upstream DNS unreachable before auth'); + +console.log('\n3. Per-client NAT filter: ping blocked'); +assert(!canPing(), 'Ping to 8.8.8.8 blocked by NAT filter'); + +console.log('\n4. Per-client NAT filter: HTTP blocked'); +const httpBefore = run(`curl -s --connect-timeout 5 -m 5 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`); +assert(!httpBefore || httpBefore.length === 0, 'HTTP blocked before auth'); + +console.log('\n5. Captive portal and API still accessible'); +const portal = run(`curl -s --connect-timeout 5 http://${IP}/`); +assert(portal && portal.includes('TollGate'), 'Portal HTML accessible'); +const apiDisc = runJson(`curl -s --connect-timeout 5 ${API}/`); +assert(apiDisc && apiDisc.kind === 10021, 'API discovery accessible'); + +console.log('\n--- Part 2: After Authentication ---\n'); + +console.log('6. Reset + Pay'); +run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`); +await sleep(1000); + +const token = mintToken(21); +assert(token !== null, 'Token generated'); +if (token) { + const payResult = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token}' -H "Content-Type: application/cashu" ${API}/`); + assert(payResult && payResult.kind === 1022, 'Payment accepted'); +} + +await sleep(1000); + +console.log('\n7. DNS now forwards to upstream'); +assert(dnsResolveWorks('google.com'), 'DNS resolves to real IPs after auth'); + +console.log('\n8. Per-client NAT filter: ping allowed'); +assert(canPing(), 'Ping to 8.8.8.8 allowed after auth'); + +console.log('\n9. Per-client NAT filter: HTTP allowed'); +const httpAfter = run(`curl -s --connect-timeout 10 -m 10 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`); +assert(httpAfter && httpAfter.length > 0, 'HTTP allowed after auth'); + +console.log('\n--- Part 3: After Revocation ---\n'); + +console.log('10. Reset auth'); +run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`); +await sleep(1000); + +console.log('\n11. DNS goes back to hijack'); +assert(dnsResolvesToSelf('google.com'), 'DNS hijack restored after revoke'); + +console.log('\n12. Per-client NAT filter: ping blocked again'); +assert(!canPing(), 'Ping blocked after revoke'); + +console.log('\n13. Per-client NAT filter: HTTP blocked again'); +const httpRevoke = run(`curl -s --connect-timeout 5 -m 5 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`); +assert(!httpRevoke || httpRevoke.length === 0, 'HTTP blocked after revoke'); + +function dnsResolveWorks(domain) { + const result = run(`nslookup -timeout=3 ${domain} 2>&1`); + return result && result.includes('Address') && !result.includes(IP) && !result.includes('NXDOMAIN'); +} + +console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`); +process.exit(failed > 0 ? 1 : 0); -- cgit v1.2.3