Below is a script I made which you can use to test if a proxy supports UDP protocol
It works by trying to resolve a DNS request through the proxy server and if successful returns the response.
Example usage:
node udp_dns_socks5_test.js --proxy-host unmetered.residential.proxyrack.net --proxy-port 9000 --proxy-username YOURUSERNAME --proxy-password YOURAPIKEY
Successful output:
% node udp_dns_socks5_test.js --proxy-host unmetered.residential.proxyrack.net --proxy-port 9000 --proxy-username $PUSER --proxy-password $PPASS
Testing UDP DNS via SOCKS5 proxy unmetered.residential.proxyrack.net:9000 -> DNS 8.8.8.8:53, query A example.com
UDP via SOCKS5 proxy appears to work. Answers:
- 23.220.75.232
- 23.220.75.245
- 23.192.228.80
- 23.192.228.84
- 23.215.0.136
- 23.215.0.138
As you can see it has a list of IPs that example.com resolves to for that particular IP/GEO/request
If unsuccessful it will print an error like this instead:
Testing UDP DNS via SOCKS5 proxy unmetered.residential.proxyrack.net:9000 -> DNS 8.8.8.8:53, query A example.com
UDP via SOCKS5 test failed: TCP timeout connecting to proxy
Script below (nodejs)
Full script below
Save the file as: udp_dns_socks5_test.js
Run with node udp_dns_socks5_test.js
Requirements: Node.js v21.4.0
const net = require('net');
const dgram = require('dgram');
function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith('--')) {
args[key] = next;
i++;
} else {
args[key] = 'true';
}
}
}
return args;
}
function isIPv4(address) {
return /^\d+\.\d+\.\d+\.\d+$/.test(address);
}
function isIPv6(address) {
// Basic detection; Node will validate when sending
return address.includes(':');
}
function buildDnsQuery(hostname, recordType) {
const id = Math.floor(Math.random() * 0xffff);
const flags = 0x0100; // standard query, recursion desired
const qdcount = 1;
const ancount = 0;
const nscount = 0;
const arcount = 0;
const parts = hostname.split('.');
const labels = parts.map((p) => {
const buf = Buffer.alloc(1 + Buffer.byteLength(p));
buf.writeUInt8(Buffer.byteLength(p), 0);
buf.write(p, 1, 'ascii');
return buf;
});
const qname = Buffer.concat([...labels, Buffer.from([0x00])]);
const qtypeMap = { A: 1, AAAA: 28, NS: 2, CNAME: 5, MX: 15, TXT: 16 }; // minimal set
const qtype = qtypeMap[recordType.toUpperCase()] || 1;
const qclass = 1; // IN
const header = Buffer.alloc(12);
header.writeUInt16BE(id, 0);
header.writeUInt16BE(flags, 2);
header.writeUInt16BE(qdcount, 4);
header.writeUInt16BE(ancount, 6);
header.writeUInt16BE(nscount, 8);
header.writeUInt16BE(arcount, 10);
const question = Buffer.alloc(4);
question.writeUInt16BE(qtype, 0);
question.writeUInt16BE(qclass, 2);
return { id, packet: Buffer.concat([header, qname, question]) };
}
function parseDnsName(buffer, offset) {
const start = offset;
let labels = [];
let consumed = 0; // bytes consumed at the original location
let jumped = false;
let safety = 0;
while (true) {
if (offset >= buffer.length) break;
const len = buffer.readUInt8(offset);
// end of name
if (len === 0) {
if (!jumped) consumed += 1; // consume the zero only if we didn't jump
break;
}
// pointer (compression)
if ((len & 0xc0) === 0xc0) {
if (offset + 1 >= buffer.length) break;
const pointer = ((len & 0x3f) << 8) | buffer.readUInt8(offset + 1);
if (!jumped) consumed += 2; // the pointer itself is two bytes in the original stream
offset = pointer; // jump to the pointed name location
jumped = true;
} else {
const labelLen = len;
if (offset + 1 + labelLen > buffer.length) break;
if (!jumped) consumed += 1 + labelLen;
const label = buffer.slice(offset + 1, offset + 1 + labelLen).toString('ascii');
labels.push(label);
offset += 1 + labelLen;
}
if (++safety > 255) break; // guard against malformed loops
}
return { name: labels.filter(Boolean).join('.'), length: consumed };
}
function parseDnsResponseAnswers(buffer) {
if (buffer.length < 12) return [];
const qdcount = buffer.readUInt16BE(4);
const ancount = buffer.readUInt16BE(6);
let offset = 12;
// skip questions
for (let i = 0; i < qdcount; i++) {
const qnameInfo = parseDnsName(buffer, offset);
if (qnameInfo.length <= 0) return [];
offset += qnameInfo.length;
if (offset + 4 > buffer.length) return [];
// QTYPE + QCLASS
offset += 4;
}
const answers = [];
for (let i = 0; i < ancount; i++) {
const nameInfo = parseDnsName(buffer, offset);
if (nameInfo.length <= 0) break;
offset += nameInfo.length;
if (offset + 10 > buffer.length) break; // TYPE(2)+CLASS(2)+TTL(4)+RDLENGTH(2)
const type = buffer.readUInt16BE(offset); offset += 2;
const cls = buffer.readUInt16BE(offset); offset += 2; // eslint-disable-line no-unused-vars
const ttl = buffer.readUInt32BE(offset); offset += 4; // eslint-disable-line no-unused-vars
const rdlength = buffer.readUInt16BE(offset); offset += 2;
if (offset + rdlength > buffer.length) break;
const rdataStart = offset;
const rdata = buffer.slice(offset, offset + rdlength);
offset += rdlength;
if (type === 1 && rdlength === 4) {
answers.push(`${rdata[0]}.${rdata[1]}.${rdata[2]}.${rdata[3]}`);
} else if (type === 28 && rdlength === 16) {
const parts = [];
for (let j = 0; j < 16; j += 2) parts.push(rdata.readUInt16BE(j).toString(16));
// Keep minimal formatting; not full RFC5952 compression
answers.push(parts.join(':'));
} else if (type === 5) {
// CNAME is a domain name possibly compressed; parse from the original buffer at rdataStart
const cnameInfo = parseDnsName(buffer, rdataStart);
answers.push(`CNAME:${cnameInfo.name}`);
}
}
return answers;
}
function buildSocks5UdpHeader(targetHost, targetPort) {
const rsvFrag = Buffer.from([0x00, 0x00, 0x00]);
let atypBuf;
if (isIPv4(targetHost)) {
const parts = targetHost.split('.').map((x) => parseInt(x, 10));
atypBuf = Buffer.from([0x01, ...parts]);
} else if (isIPv6(targetHost)) {
const addr = targetHost;
// Normalize via Node's URL class fallback: require user to provide full IPv6
// Build 16-byte buffer from IPv6 string
const b = Buffer.alloc(16);
// Use Node to parse IPv6 by creating a UDP6 socket and using lookup? Keep simple:
// Accept only full hextet form for simplicity here
const expanded = addr.split(':');
if (expanded.includes('')) {
// Expand :: shorthand
const missing = 8 - (expanded.filter((s) => s !== '').length);
const idx = expanded.indexOf('');
expanded.splice(idx, 1, ...Array(missing + 1).fill('0'));
}
for (let i = 0; i < 8; i++) {
const v = parseInt(expanded[i] || '0', 16) & 0xffff;
b.writeUInt16BE(v, i * 2);
}
atypBuf = Buffer.concat([Buffer.from([0x04]), b]);
} else {
const hostBuf = Buffer.from(targetHost, 'ascii');
atypBuf = Buffer.concat([Buffer.from([0x03, hostBuf.length]), hostBuf]);
}
const portBuf = Buffer.alloc(2);
portBuf.writeUInt16BE(targetPort, 0);
return Buffer.concat([rsvFrag, atypBuf, portBuf]);
}
function parseSocks5Addr(buffer, offset) {
const atyp = buffer.readUInt8(offset);
if (atyp === 0x01) {
const host = `${buffer.readUInt8(offset + 1)}.${buffer.readUInt8(offset + 2)}.${buffer.readUInt8(offset + 3)}.${buffer.readUInt8(offset + 4)}`;
const port = buffer.readUInt16BE(offset + 5);
return { host, port, length: 1 + 4 + 2 };
}
if (atyp === 0x03) {
const len = buffer.readUInt8(offset + 1);
const host = buffer.slice(offset + 2, offset + 2 + len).toString('ascii');
const port = buffer.readUInt16BE(offset + 2 + len);
return { host, port, length: 1 + 1 + len + 2 };
}
if (atyp === 0x04) {
const parts = [];
for (let i = 0; i < 16; i += 2) parts.push(buffer.readUInt16BE(offset + 1 + i).toString(16));
const host = parts.join(':');
const port = buffer.readUInt16BE(offset + 1 + 16);
return { host, port, length: 1 + 16 + 2 };
}
throw new Error('Unknown ATYP in SOCKS5');
}
function socks5UdpAssociate({ proxyHost, proxyPort, username, password }) {
return new Promise((resolve, reject) => {
const socket = new net.Socket();
socket.setTimeout(10000);
socket.once('timeout', () => reject(new Error('TCP timeout connecting to proxy')));
socket.once('error', reject);
socket.connect(proxyPort, proxyHost, () => {
const methods = [];
if (username || password) methods.push(0x02);
methods.push(0x00);
const hello = Buffer.from([0x05, methods.length, ...methods]);
socket.write(hello);
});
let stage = 'greeting';
let buffer = Buffer.alloc(0);
socket.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
try {
if (stage === 'greeting' && buffer.length >= 2) {
const method = buffer.readUInt8(1);
buffer = Buffer.alloc(0);
if (method === 0xff) return reject(new Error('Proxy does not accept provided auth methods'));
if (method === 0x02) {
stage = 'auth';
const user = Buffer.from(username || '', 'utf8');
const pass = Buffer.from(password || '', 'utf8');
const authReq = Buffer.concat([
Buffer.from([0x01, user.length]),
user,
Buffer.from([pass.length]),
pass,
]);
socket.write(authReq);
} else if (method === 0x00) {
stage = 'request';
const req = Buffer.from([0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
socket.write(req);
}
} else if (stage === 'auth' && buffer.length >= 2) {
const status = buffer.readUInt8(1);
buffer = Buffer.alloc(0);
if (status !== 0x00) return reject(new Error('Username/password authentication failed'));
stage = 'request';
const req = Buffer.from([0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
socket.write(req);
} else if (stage === 'request' && buffer.length >= 5) {
if (buffer.readUInt8(1) !== 0x00) {
return reject(new Error(`UDP associate failed with code ${buffer.readUInt8(1)}`));
}
const addrInfo = parseSocks5Addr(buffer, 3);
const udpRelayHost = addrInfo.host;
const udpRelayPort = addrInfo.port;
// Keep TCP socket open while using UDP; caller will close
return resolve({ tcpSocket: socket, udpRelayHost, udpRelayPort });
}
} catch (err) {
reject(err);
}
});
});
}
async function testUdpDnsViaSocks5(opts) {
const {
proxyHost,
proxyPort,
username,
password,
dnsServer,
dnsPort,
queryName,
recordType,
timeoutMs,
} = opts;
const { id, packet } = buildDnsQuery(queryName, recordType);
const { tcpSocket, udpRelayHost, udpRelayPort } = await socks5UdpAssociate({ proxyHost, proxyPort, username, password });
const udpSocket = dgram.createSocket(isIPv6(udpRelayHost) ? 'udp6' : 'udp4');
const header = buildSocks5UdpHeader(dnsServer, dnsPort);
const message = Buffer.concat([header, packet]);
const responsePromise = new Promise((resolve, reject) => {
const timer = setTimeout(() => {
udpSocket.close();
try { tcpSocket.end(); } catch (e) {}
reject(new Error('Timed out waiting for DNS response through proxy'));
}, timeoutMs);
udpSocket.on('error', (err) => {
clearTimeout(timer);
try { udpSocket.close(); } catch (e) {}
try { tcpSocket.end(); } catch (e) {}
reject(err);
});
udpSocket.on('message', (buf) => {
// SOCKS5 UDP header: 2 RSV, 1 FRAG, ATYP + DST.ADDR + DST.PORT
if (buf.length < 10) return; // too short
if (buf.readUInt16BE(0) !== 0x0000) return;
if (buf.readUInt8(2) !== 0x00) return; // fragments not supported
const addrInfo = parseSocks5Addr(buf, 3);
const payloadOffset = 3 + addrInfo.length;
const dns = buf.slice(payloadOffset);
if (dns.length < 12) return;
const respId = dns.readUInt16BE(0);
const flags = dns.readUInt16BE(2);
const rcode = flags & 0x000f;
if (respId !== id) return;
clearTimeout(timer);
udpSocket.close();
try { tcpSocket.end(); } catch (e) {}
if (rcode !== 0) {
return reject(new Error(`DNS error code ${rcode} returned through proxy`));
}
const answers = parseDnsResponseAnswers(dns);
resolve({ answers, raw: dns });
});
udpSocket.send(message, udpRelayPort, udpRelayHost);
});
return responsePromise;
}
async function main() {
const args = parseArgs(process.argv);
if (!args['proxy-host'] || !args['proxy-port']) {
console.error('Usage: node udp_dns_socks5_test.js --proxy-host <host> --proxy-port <port> [--proxy-username <user> --proxy-password <pass>] [--dns <server>] [--dns-port <port>] [--name <domain>] [--type <A|AAAA>] [--timeout <ms>]');
process.exit(1);
}
const proxyHost = args['proxy-host'];
const proxyPort = parseInt(args['proxy-port'], 10);
const username = args['proxy-username'] || '';
const password = args['proxy-password'] || '';
const dnsServer = args['dns'] || '8.8.8.8';
const dnsPort = parseInt(args['dns-port'] || '53', 10);
const queryName = args['name'] || 'example.com';
const recordType = (args['type'] || 'A').toUpperCase();
const timeoutMs = parseInt(args['timeout'] || '5000', 10);
try {
console.log(`Testing UDP DNS via SOCKS5 proxy ${proxyHost}:${proxyPort} -> DNS ${dnsServer}:${dnsPort}, query ${recordType} ${queryName}`);
const result = await testUdpDnsViaSocks5({
proxyHost,
proxyPort,
username,
password,
dnsServer,
dnsPort,
queryName,
recordType,
timeoutMs,
});
if (result.answers.length === 0) {
console.log('Received DNS response but no answers in the answer section.');
} else {
console.log('UDP via SOCKS5 proxy appears to work. Answers:');
for (const a of result.answers) console.log(`- ${a}`);
}
process.exit(0);
} catch (err) {
console.error('UDP via SOCKS5 test failed:', err.message);
process.exit(2);
}
}
if (require.main === module) {
main();
}