Initial STUN NAT mapping console
This commit is contained in:
48
test/engine.test.js
Normal file
48
test/engine.test.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import dgram from 'node:dgram';
|
||||
import net from 'node:net';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { after, test } from 'node:test';
|
||||
|
||||
const engine = process.env.NATMAP_BIN;
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'stunmap-engine-'));
|
||||
const children = [];
|
||||
const sockets = [];
|
||||
after(async () => { for (const child of children) child.kill('SIGTERM'); for (const socket of sockets) socket.close(); await fs.rm(directory, { recursive: true, force: true }); });
|
||||
|
||||
function bind(socket) { return new Promise((resolve) => socket.bind(0, '127.0.0.1', () => resolve(socket.address().port))); }
|
||||
function listen(server) { return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server.address().port))); }
|
||||
function waitFor(predicate, timeout = 5000) { return new Promise((resolve, reject) => { const deadline = Date.now() + timeout; const timer = setInterval(async () => { try { const value = await predicate(); if (value) { clearInterval(timer); resolve(value); } else if (Date.now() >= deadline) { clearInterval(timer); reject(new Error('timed out waiting for mapping')); } } catch (error) { clearInterval(timer); reject(error); } }, 40); }); }
|
||||
|
||||
test('NATMap UDP forward mode maps a STUN channel port to the local target', { skip: !engine }, async () => {
|
||||
const stun = dgram.createSocket('udp4'); sockets.push(stun); const stunPort = await bind(stun);
|
||||
stun.on('message', (message, remote) => {
|
||||
if (message.length < 20 || message.readUInt16BE(0) !== 1) return;
|
||||
const response = Buffer.alloc(32); response.writeUInt16BE(0x0101, 0); response.writeUInt16BE(12, 2); message.copy(response, 4, 4, 20); response.writeUInt16BE(0x0020, 20); response.writeUInt16BE(8, 22); response[25] = 1; response.writeUInt16BE(remote.port ^ 0x2112, 26); Buffer.from([0x7f ^ 0x21, 0x00 ^ 0x12, 0x00 ^ 0xa4, 0x01 ^ 0x42]).copy(response, 28); stun.send(response, remote.port, remote.address);
|
||||
});
|
||||
const echo = dgram.createSocket('udp4'); sockets.push(echo); const echoPort = await bind(echo); echo.on('message', (message, remote) => echo.send(message, remote.port, remote.address));
|
||||
const stateDir = path.join(directory, 'state'); await fs.mkdir(stateDir); const child = spawn(engine, ['-u', '-s', `127.0.0.1:${stunPort}`, '-b', '0', '-k', '1', '-c', '1', '-t', '127.0.0.1', '-p', String(echoPort), '-e', path.resolve('scripts/notify.js')], { env: { ...process.env, STUNMAP_RULE_ID: 'udp', STUNMAP_STATE_DIR: stateDir } }); children.push(child);
|
||||
let stderr = ''; child.stderr.on('data', (chunk) => { stderr += chunk; });
|
||||
const state = await waitFor(async () => { try { return JSON.parse(await fs.readFile(path.join(stateDir, 'udp.json'), 'utf8')); } catch { return null; } });
|
||||
const client = dgram.createSocket('udp4'); sockets.push(client); const response = await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error(`UDP forwarding timed out: ${stderr}`)), 3000); client.on('message', (message) => { clearTimeout(timer); resolve(message.toString()); }); client.send(Buffer.from('stunmap-ok'), state.privatePort, '127.0.0.1'); });
|
||||
assert.equal(response, 'stunmap-ok');
|
||||
const initialHeartbeat = state.lastVerifiedAt; await new Promise((resolve) => setTimeout(resolve, 1200)); const refreshed = JSON.parse(await fs.readFile(path.join(stateDir, 'udp.json'), 'utf8'));
|
||||
assert.notEqual(refreshed.lastVerifiedAt, initialHeartbeat, 'each successful STUN check must refresh mapping health');
|
||||
echo.close(); sockets.splice(sockets.indexOf(echo), 1);
|
||||
const probe = dgram.createSocket('udp4'); sockets.push(probe); const probeResponse = await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('UDP channel health response timed out')), 3000); probe.on('message', (message) => { clearTimeout(timer); resolve(message.toString()); }); probe.send(Buffer.from('SMHP:target-is-irrelevant'), refreshed.privatePort, '127.0.0.1'); });
|
||||
assert.equal(probeResponse, 'SMHP:target-is-irrelevant');
|
||||
});
|
||||
|
||||
test('NATMap TCP forward mode keeps a channel and proxies TCP traffic', { skip: !engine }, async () => {
|
||||
const keepalive = net.createServer((socket) => socket.on('data', () => socket.write('HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n'))); const keepalivePort = await listen(keepalive);
|
||||
const stun = net.createServer((socket) => socket.once('data', (message) => { if (message.length < 20 || message.readUInt16BE(0) !== 1) return socket.destroy(); const remote = socket.address(); const response = Buffer.alloc(32); response.writeUInt16BE(0x0101, 0); response.writeUInt16BE(12, 2); message.copy(response, 4, 4, 20); response.writeUInt16BE(0x0020, 20); response.writeUInt16BE(8, 22); response[25] = 1; response.writeUInt16BE(remote.port ^ 0x2112, 26); Buffer.from([0x7f ^ 0x21, 0x00 ^ 0x12, 0x00 ^ 0xa4, 0x01 ^ 0x42]).copy(response, 28); socket.end(response); })); const stunPort = await listen(stun);
|
||||
const echo = net.createServer((socket) => socket.pipe(socket)); const echoPort = await listen(echo); after(() => { keepalive.close(); stun.close(); echo.close(); });
|
||||
const stateDir = path.join(directory, 'tcp-state'); await fs.mkdir(stateDir); const child = spawn(engine, ['-s', `127.0.0.1:${stunPort}`, '-h', `127.0.0.1:${keepalivePort}`, '-b', '0', '-k', '1', '-t', '127.0.0.1', '-p', String(echoPort), '-e', path.resolve('scripts/notify.js')], { env: { ...process.env, STUNMAP_RULE_ID: 'tcp', STUNMAP_STATE_DIR: stateDir } }); children.push(child);
|
||||
let stderr = ''; child.stderr.on('data', (chunk) => { stderr += chunk; });
|
||||
const state = await waitFor(async () => { try { return JSON.parse(await fs.readFile(path.join(stateDir, 'tcp.json'), 'utf8')); } catch { return null; } });
|
||||
const reply = await new Promise((resolve, reject) => { const socket = net.connect(state.privatePort, '127.0.0.1'); const timer = setTimeout(() => { socket.destroy(); reject(new Error(`TCP forwarding timed out: ${stderr}`)); }, 3000); socket.on('connect', () => socket.write('stunmap-tcp')); socket.on('data', (chunk) => { clearTimeout(timer); socket.end(); resolve(chunk.toString()); }); socket.on('error', reject); });
|
||||
assert.equal(reply, 'stunmap-tcp');
|
||||
});
|
||||
Reference in New Issue
Block a user