Skip to content

Commit

Permalink
add udp client
Browse files Browse the repository at this point in the history
  • Loading branch information
reklatsmasters committed Jul 1, 2020
1 parent b636f40 commit f900a59
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 5 deletions.
76 changes: 76 additions & 0 deletions src/certificate.js
Expand Up @@ -10,6 +10,7 @@ const debug = require('debug')('dnscrypt');
module.exports = {
validate,
parse,
validateCertificate,
};

const CERT_MAGIC = 0x444e5343;
Expand Down Expand Up @@ -101,3 +102,78 @@ function parse(encodedCertificate) {
return null;
}
}

/**
* Handle remote certificate.
* @param {Object} response DNS response.
* @param {Object} resolver DNSCrypt server config.
* @returns {Certificate}
*/
function validateCertificate(response, resolver) {
if (response.type !== 'response') {
throw new Error('Invalid DNS response');
}

if (response.rcode !== 'NOERROR') {
throw new Error('Invalid DNS response');
}

if (response.answers.length < response.questions.length) {
throw new Error('Invalid DNS response');
}

const record = response.answers.find(
answer => answer.type === 'TXT' && answer.name === resolver.providerName
);

if (!record) {
throw new Error('Invalid DNS response');
}

/** @type {Buffer[]} */
const encodedCertificates = Array.isArray(record.data) ? record.data : [record.data];

if (encodedCertificates.length === 0) {
throw new Error('Invalid DNS response');
}

// Resolver public key.
const publicKey = Buffer.from(resolver.pk, 'hex');

const reducer = (previous, { certificate }) => {
if (!previous) {
return certificate;
}

return certificate.serial > previous.serial ? certificate : previous;
};

const mapper = bytes => {
const certificate = parse(bytes);

if (certificate === null) {
return null;
}

const signed = bytes.slice(4 + 2 + 2 + 64);

return {
certificate,
signed,
};
};

const remoteCertificate = encodedCertificates
.map(bytes => mapper(bytes))
.filter(Boolean)
.filter(({ certificate, signed }) => validate(certificate, signed, publicKey))
.reduce(reducer, null);

if (remoteCertificate === null) {
throw new Error('No valid certificates');
}

debug('got certificate', remoteCertificate);

return remoteCertificate;
}
2 changes: 2 additions & 0 deletions src/session.js
Expand Up @@ -29,6 +29,8 @@ class Session extends Emitter {
this.certificate = null; // Resolver certificate.
this.lookupQueue = []; // Queue to wait for certificate before starting looking up.
this.queue = new TimedQueue(queryTimeout); // Queue of pending queries.
this.certificateTimeout = queryTimeout;
this.queryTimeout = queryTimeout;
}

/**
Expand Down
127 changes: 127 additions & 0 deletions src/transport/udp-client.js
@@ -0,0 +1,127 @@
'use strict';

const nanoresource = require('nanoresource/emitter');
const random = require('random-int');
const dns = require('dns-packet');
const UDPSocket = require('./udp-socket');
const { validateCertificate } = require('../certificate');

/**
* @typedef {Object} UDPClientOptions
* @property {Session} session Instance of dnscrypt session.
*/

/**
* DNSCrypt UDP client abstraction.
*/
module.exports = class UDPClient extends nanoresource {
/**
* @class {UDPClient}
* @param {UDPClientOptions} options
*/
constructor(options) {
super();

this.session = options.session;
this.socket = new UDPSocket({
port: options.session.serverPort,
address: options.session.serverAddress,
});
}

/**
* @private
* @param {Function} callback
*/
_open(callback) {
const packet = {
id: random(1, 0xffff),
type: 'query',
questions: [
{
type: 'TXT',
name: this.session.resolver.providerName,
},
],
};

this._certificate(packet, (err, certificate) => {
if (err) {
this.socket.close(() => callback(err));
return;
}

this.session.certificate = certificate;
callback(null);
});
}

/**
* @private
* @param {Function} callback
*/
_close(callback) {
this.socket.close(() => callback());
}

/**
* Query certificate.
* @param {Object} query DNS query.
* @param {Function} callback
* @private
*/
_certificate(query, callback) {
if (!this.active(callback)) {
return;
}

const packet = dns.encode(query);
const timer = setTimeout(() => {
cleanup(this);
this.inactive(callback, new Error('ETIMEDOUT'));
}, this.session.queryTimeout);

const onmessage = data => {
let response;

try {
response = dns.decode(data);
} catch (_) {
return;
}

if (response.id !== query.id) {
return;
}

let certificate;

try {
certificate = validateCertificate(response, this.session.resolver);
} catch (error) {
cleanup(this);
this.inactive(callback, error);
return;
}

cleanup(this);
this.inactive(callback, null, certificate);
};

this.socket.on('data', onmessage);
this.socket.write(packet, err => {
if (err) {
cleanup(this);
return this.inactive(callback, err);
}
});

/**
* @param {UDPClient} client
*/
function cleanup(client) {
clearTimeout(timer);
client.socket.off('data', onmessage);
}
}
};
7 changes: 2 additions & 5 deletions src/transport/udp-socket.js
Expand Up @@ -10,7 +10,6 @@ const isLegalPort = port => typeof port === 'number' && port > 0 && port < 0xfff
* @typedef {Object} UDPSocketOptions
* @property {number} port Target port.
* @property {string} address Target IP address.
* @property {string} [bindAddress] Source IP address.
* @property {number} [bindPort] Source port.
*/
/**
Expand All @@ -24,7 +23,7 @@ module.exports = class UDPSocket extends nanoresource {
constructor(opts = {}) {
super();

const { port, address, bindAddress, bindPort } = opts;
const { port, address, bindPort } = opts;

if (!isIP(address)) {
throw new Error('Invalid ip address');
Expand All @@ -38,8 +37,6 @@ module.exports = class UDPSocket extends nanoresource {
this.address = address;

this.bindPort = isLegalPort(bindPort) ? bindPort : 0;
this.bindAddress = isIP(bindAddress) ? bindAddress : 'localhost';

this.socket = null;
}

Expand Down Expand Up @@ -80,7 +77,7 @@ module.exports = class UDPSocket extends nanoresource {
this.socket.once('connect', connectHandler);

try {
this.socket.bind(this.bindPort, this.bindAddress);
this.socket.bind(this.bindPort);
} catch (error) {
errorHandler(error);
}
Expand Down
20 changes: 20 additions & 0 deletions test/udp-client.js
@@ -0,0 +1,20 @@
'use strict';

const UDPClient = require('../src/transport/udp-client');
const { Session } = require('../src/session');

describe('udp client', () => {
test('should work', done => {
const session = new Session(4e3);
const client = new UDPClient({
session,
});

client.open(err => {
expect(err).toBeFalsy();
expect(session.certificate).not.toBeNull();

client.close(done);
});
});
});

0 comments on commit f900a59

Please sign in to comment.