Skip to content

Commit

Permalink
Add udp socket abstraction.
Browse files Browse the repository at this point in the history
  • Loading branch information
reklatsmasters committed Jul 1, 2020
1 parent ce0a87d commit b07f965
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 5 deletions.
5 changes: 3 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* class DNSCryptClient, private interface, implements dns api methods
* receive certificate using something like `_construct`.
* drop state management using `next-state`, recreate client when certificate expired.
* class Session used to store session sensitive data, like certificate.
* implement TCP and UDP transport using different classes, hide impl details there.
* class Session used to store session data, like certificate.
* class UDPSocket and TCPSocket used as low-lever transport, hide impl details here.
* class DNSCryptSocket is independent transport for DNSCryptClient, hide udp and tcp transport details.
* do not use global default resolver due to it's need explicit state management.
* leave encrypt / decrypt / verify in a module, just rename it.
11 changes: 9 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"debug": "^4.1.1",
"dns-packet": "^5.2.1",
"dnsstamp": "^1.1.3",
"nanoresource": "^1.3.0",
"next-state": "^1.0.0",
"random-int": "^2.0.0",
"tweetnacl": "^1.0.3",
Expand All @@ -45,7 +46,7 @@
"prettier": "^1.18.2"
},
"engines": {
"node": ">=10.2"
"node": ">=12"
},
"eslintConfig": {
"extends": "@nodertc",
Expand Down
122 changes: 122 additions & 0 deletions src/transport/udp-socket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
'use strict';

const dgram = require('dgram');
const { isIP, isIPv4 } = require('net');
const nanoresource = require('nanoresource/emitter');

const isLegalPort = port => typeof port === 'number' && port > 0 && port < 0xffff;

/**
* @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.
*/
/**
* UDP socket abstraction.
*/
module.exports = class UDPSocket extends nanoresource {
/**
* @class {UDPSocket}
* @param {UDPSocketOptions} opts
*/
constructor(opts = {}) {
super();

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

if (!isIP(address)) {
throw new Error('Invalid ip address');
}

if (!isLegalPort(port)) {
throw new Error('Invalid port');
}

this.port = port;
this.address = address;

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

this.socket = null;
}

/**
* @private
* @param {Function} callback
*/
_open(callback) {
const type = isIPv4(this.address) ? 'udp4' : 'udp6';
const socket = dgram.createSocket(type);

this.socket = socket;

const connectHandler = () => {
this.socket.removeListener('error', errorHandler);
this.socket.on('error', err => this.emit('error', err));
this.socket.on('message', message => this.emit('data', message));

callback(null);
};

const listeningHandler = () => {
this.socket.connect(this.port, this.address);
};

/**
* @param {Error} error
*/
function errorHandler(error) {
socket.removeListener('connect', connectHandler);
socket.removeListener('listening', listeningHandler);

socket.close(() => callback(error));
}

this.socket.once('error', errorHandler);
this.socket.once('listening', listeningHandler);
this.socket.once('connect', connectHandler);

try {
this.socket.bind(this.bindPort, this.bindAddress);
} catch (error) {
errorHandler(error);
}
}

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

/**
* Send data.
* @param {Buffer} data Data to send.
* @param {Function} callback
*/
write(data, callback) {
this.open(error => {
if (error) {
return callback(error);
}

if (!this.active(callback)) {
return;
}

this.socket.send(data, 0, data.length, (err, bytes) => {
if (err) {
return this.inactive(callback, err);
}

this.inactive(callback, null, bytes);
});
});
}
};
36 changes: 36 additions & 0 deletions test/udp-socket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

const dgram = require('dgram');
const UDPSocket = require('../src/transport/udp-socket');

describe('udp socket', () => {
const target = dgram.createSocket('udp4');

beforeAll(done => {
target.once('error', done);
target.bind(0, 'localhost', done);
});

afterAll(done => {
target.close(done);
});

it('should work', done => {
expect.assertions(3);

const socket = new UDPSocket(target.address());
const data = Buffer.allocUnsafe(10);

socket.write(data, (err, bytes) => {
expect(err).toBeFalsy();
expect(bytes).toEqual(data.length);

socket.close();
});

target.once('message', message => {
expect(message).toEqual(data);
done();
});
});
});

0 comments on commit b07f965

Please sign in to comment.