Files

75 lines
1.7 KiB
TypeScript

/**
* Outbound STUN Binding probe (RFC 5389) over UDP.
*/
import * as dgram from 'dgram';
import * as crypto from 'crypto';
const STUN_MAGIC = 0x2112a442;
const STUN_HEADER_LEN = 20;
const BINDING_REQUEST = 0x0001;
export function sendStunBindingProbe(
host: string,
port: number,
timeoutMs: number
): Promise<{ ok: boolean; rttMs?: number }> {
return new Promise((resolve) => {
const socket = dgram.createSocket('udp4');
const txId = crypto.randomBytes(12);
const buf = Buffer.alloc(STUN_HEADER_LEN);
buf.writeUInt16BE(BINDING_REQUEST, 0);
buf.writeUInt16BE(0, 2);
buf.writeUInt32BE(STUN_MAGIC, 4);
txId.copy(buf, 8);
const started = Date.now();
const timer = setTimeout(() => {
try {
socket.close();
} catch {
/* ignore */
}
resolve({ ok: false });
}, timeoutMs);
socket.on('message', (msg) => {
if (msg.length < STUN_HEADER_LEN) return;
if (!txId.equals(msg.subarray(8, 20))) return;
const typ = msg.readUInt16BE(0);
if ((typ & 0x3fff) !== 0x0101) return;
clearTimeout(timer);
try {
socket.close();
} catch {
/* ignore */
}
resolve({ ok: true, rttMs: Date.now() - started });
});
socket.on('error', () => {
clearTimeout(timer);
try {
socket.close();
} catch {
/* ignore */
}
resolve({ ok: false });
});
socket.bind(0, () => {
socket.send(buf, port, host, (err) => {
if (err) {
clearTimeout(timer);
try {
socket.close();
} catch {
/* ignore */
}
resolve({ ok: false });
}
});
});
});
}