Skip to content

Instantly share code, notes, and snippets.

@lithdew
Last active May 11, 2023 06:31
Show Gist options
  • Save lithdew/b64f978fb5ec38fbe36d73c2a5434b36 to your computer and use it in GitHub Desktop.
Save lithdew/b64f978fb5ec38fbe36d73c2a5434b36 to your computer and use it in GitHub Desktop.
deno (udp holepunching): basic stun client for fetching public-facing ip:port
// deno run -A --unstable main.ts
export const classToValue = {
request: 0x0000,
indication: 0x0010,
success: 0x0100,
failure: 0x0110,
};
export const methodToValue = {
binding: 0x0001,
};
export const typeToValue = {
mappedAddress: 0x0001,
username: 0x0006,
messageIntegrity: 0x0008,
errorCode: 0x0009,
unknownAttributes: 0x000a,
realm: 0x0014,
nonce: 0x0015,
xorMappedAddress: 0x0020,
software: 0x8022,
alternateServer: 0x8023,
fingerprint: 0x8028,
};
export const addressFamilyToValue = {
ipv4: 0x01,
ipv6: 0x02,
};
export const valueToClass: Record<number, string> = Object.fromEntries(
Object.entries(classToValue).map(([k, v]) => [v, k])
);
export const valueToMethod: Record<number, string> = Object.fromEntries(
Object.entries(methodToValue).map(([k, v]) => [v, k])
);
export const valueToType: Record<number, string> = Object.fromEntries(
Object.entries(typeToValue).map(([k, v]) => [v, k])
);
export const valueToAddressFamily: Record<number, string> = Object.fromEntries(
Object.entries(addressFamilyToValue).map(([k, v]) => [v, k])
);
const attributeDecoders: { [key in Type]?: (b: Uint8Array) => unknown } = {
xorMappedAddress: decodeXorMappedAddress,
};
export type Class = keyof typeof classToValue;
export type Method = keyof typeof methodToValue;
export type Type = keyof typeof typeToValue;
export type AddressFamily = keyof typeof addressFamilyToValue;
const MAGIC_COOKIE = 0x2112a442;
const MAX_RESPONSE_LENGTH = 548;
const HEADER_LENGTH = 20;
const ATTRIBUTE_HEADER_LENGTH = 4;
type Header = {
class: Class | number;
method: Method | number;
length: number;
magicCookie: number;
transactionId: [number, number, number];
};
type Attribute = {
type: Type | number;
length: number;
value: unknown;
};
export type Message = {
header: Header;
attributes: Attribute[];
};
export function decodeAttribute(b: Uint8Array) {
if (b.byteLength < ATTRIBUTE_HEADER_LENGTH) {
throw new Error("invalid attribute length");
}
const v = new DataView(b.buffer);
const typeValue = v.getUint16(0);
const type = (valueToType[typeValue] as Type) || typeValue;
const length = v.getUint16(2);
const bytes = b.slice(4, 4 + length);
const value = attributeDecoders[type]?.(bytes) ?? bytes;
const attribute: Attribute = {
type,
length,
value,
};
return attribute;
}
export function encodeHeader(header: Header) {
const classValue =
typeof header.class === "string"
? classToValue[header.class]
: header.class;
const methodValue =
typeof header.method === "string"
? methodToValue[header.method]
: header.method;
const b = new Uint8Array(HEADER_LENGTH);
const v = new DataView(b.buffer);
v.setUint16(0, classValue | methodValue);
v.setUint16(2, header.length);
v.setUint32(4, header.magicCookie);
v.setUint32(8, header.transactionId[0]);
v.setUint32(12, header.transactionId[1]);
v.setUint32(16, header.transactionId[2]);
return b;
}
export function decodeHeader(b: Uint8Array) {
if (b.byteLength < HEADER_LENGTH) {
throw new Error("invalid header length");
}
const v = new DataView(b.buffer);
const classAndMethod = v.getUint16(0);
if ((classAndMethod & 0xc000) !== 0x0000) {
throw new Error("invalid protocol");
}
const length = v.getUint16(2);
const magicCookie = v.getUint32(4);
const transactionId = [v.getUint32(8), v.getUint32(12), v.getUint32(16)] as [
number,
number,
number
];
if (magicCookie !== MAGIC_COOKIE) {
throw new Error("invalid magic cookie");
}
const header: Header = {
class:
(valueToClass[classAndMethod & 0x0110] as Class) ||
classAndMethod & 0x0110,
method:
(valueToMethod[classAndMethod & 0x0001] as Method) ||
classAndMethod & 0x0001,
length,
magicCookie,
transactionId,
};
return header;
}
export function decodeMessage(b: Uint8Array) {
const header = decodeHeader(b);
const rest = b.slice(HEADER_LENGTH, HEADER_LENGTH + header.length);
const attributes: Attribute[] = [];
for (
let i = 0;
i < rest.byteLength;
i += ATTRIBUTE_HEADER_LENGTH + attributes[i].length
) {
attributes.push(decodeAttribute(rest.slice(i)));
}
const message: Message = {
header,
attributes,
};
return message;
}
export function decodeXorMappedAddress(b: Uint8Array) {
if (b.byteLength < 8) {
throw new Error("invalid xor mapped address length");
}
const v = new DataView(b.buffer);
const familyValue = v.getUint8(1);
if (!valueToAddressFamily[familyValue]) {
throw new Error("invalid address family");
}
const family = valueToAddressFamily[familyValue] as AddressFamily;
const xorPort = v.getUint16(2);
const port = xorPort ^ (MAGIC_COOKIE >> 16);
const magicCookieBytes = new Uint8Array(4);
const magicCookieView = new DataView(magicCookieBytes.buffer);
magicCookieView.setUint32(0, MAGIC_COOKIE);
switch (family) {
case "ipv4": {
const xorAddress = new Uint8Array(4);
for (let i = 0; i < 4; i++) {
xorAddress[i] = b[4 + i] ^ magicCookieBytes[i];
}
const address = Array.from(xorAddress).join(".");
return { family, address, port };
}
case "ipv6": {
const xorAddress = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
xorAddress[i] = b[4 + i] ^ magicCookieBytes[i % 4];
}
const address = Array.from(xorAddress)
.map((v) => v.toString(16))
.join(":");
return { family, address, port };
}
}
}
if (import.meta.main) {
const socket = Deno.listenDatagram({
hostname: "0.0.0.0",
port: 0,
transport: "udp",
});
socket.send(
encodeHeader({
class: "request",
method: "binding",
length: 0,
magicCookie: MAGIC_COOKIE,
transactionId: [Math.random(), Math.random(), Math.random()],
}),
{ transport: "udp", port: 19302, hostname: "stun.l.google.com" }
);
const buffer = new Uint8Array(MAX_RESPONSE_LENGTH);
const [bytes] = await socket.receive(buffer);
console.log(decodeMessage(bytes));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment