75 lines
1.7 KiB
TypeScript
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 });
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|