I have a situation where two "clients" connect to each other by performing simultaneously (TCP hole punching):
// The socket already exist, and was connected to a rendez-vous server
// I re-use it with the same local port and address as it was using with the
// server, except now it's trying to contact its peer.
socket.connect({
localAddress,
localPort,
port: peerAddresses.remoteAddress.port,
host: peerAddresses.remoteAddress.host,
});
This works well. The socket is a TLSSocket that originally connected to a rendez-vous server.
After performing the hole-punching and connecting to the peer though, the encryption is lost (I think, based on what I saw with Wireshark).
Is there a way I can upgrade the connection between the peers to be encrypted?
I tried various flavors of:
tls.connect({ socket: socket, secureContext: tls.createSecureContext(...) });
// or
new tls.TSLSocket(socket, { secureContext: ..., isServer: true /* just on one end */ })
on only one side of the connection (because my understanding is that TLS is asymmetrical, and you need a server and a client), and on both (because, why not...), but to no avail.
I'm starting to think I'm going to need to do the Diffie-Hellman key exchange manually, but I would really like to avoid that.
EDIT: Here is one concrete example, with two client trying to connect to each other, and then upgrading:
// clientA.js
const net = require('net');
const tls = require('tls');
const fs = require('fs');
const localPort = 3018;
const peerPort = 3019;
const socket = net.createConnection({
localPort: localPort,
port: peerPort
});
socket.on('connect', () => {
console.log('connected!');
const secureContext = tls.createSecureContext({
key: fs.readFileSync('./ca-key.pem'),
cert: fs.readFileSync('./ca-cert.pem'),
});
const secureSocket = new tls.TLSSocket(socket, { isServer: true, secureContext });
secureSocket.on('session', session => console.log('session', session));
secureSocket.on('secureConnect', () => {
console.log('securely connected!');
});
secureSocket.on('close', () => console.log('closing secure socket A'));
});
socket.on('error', (e) => { console.log(e); });
socket.on('close', () => {
socket.connect({
localPort: localPort,
port: peerPort
});
});
and
// clientB.js
const net = require('net');
const tls = require('tls');
const fs = require('fs');
const localPort = 3019;
const peerPort = 3018;
const socket = net.createConnection({
localPort: localPort,
port: peerPort
});
socket.on('connect', () => {
console.log('connected!');
const secureSocket = new tls.TLSSocket(socket, { isServer: false });
secureSocket.on('session', () => {
console.log('session');
});
secureSocket.on('secureConnect', () => {
console.log('securely connected!');
});
secureSocket.on('close', () => console.log('closing secure socket B'));
});
socket.on('error', (e) => { console.log(e); });
socket.on('close', () => {
socket.connect({
localPort: localPort,
port: peerPort
});
});
The "securely connected!" message never shows up, nor do the closing / session messages on the secure sockets.
I must say it does seem off to me to just instantiate a new TLS socket around the existing one - and indeed, it seemingly does nothing. I do get the connected!
message, but no upgrade seem to happen.
If I were to call connect({ localPort: localPort, port: peerPort }) on the secureSocket, I would get EADDRINUSE, which makes sense to me.
I've also tried a scenario where I'd call tls.connect({ socket: socket, secureContext: tls.createSecureContext(...) })
on both sides, but this also seems off because I cannot indicate which socket should behave as server.
Any tip welcome, I'm pretty much in the dark with what happens under the hood, I may need to dig into Node's internals at some point.
Thanks to user President James K. Polk!
Working code, to upgrade from regular TCP socket to TLS in Node.js:
// clientA.js
const net = require('net');
const tls = require('tls');
const fs = require('fs');
const localPort = 20001;
const peerPort = 20000;
const socket = net.createConnection({
localPort: localPort,
port: peerPort
});
socket.on('connect', () => {
console.log('connected!');
// Do not call socket.write here! A "clean pipe" is needed to proceed to the upgrade
const secureContext = tls.createSecureContext({
key: fs.readFileSync('./ca-key.pem'),
cert: fs.readFileSync('./ca-cert.pem'),
});
// This constitutes the upgrade. One side needs to be a server, the other not.
const secureSocket = new tls.TLSSocket(socket, { isServer: true, secureContext });
// You can write here, but the client should initiate data sending, or the ping-pong won't start.
// secureSocket.write('ping');
secureSocket.on('keylog', (keylog) => {
console.log('keylog', keylog.toString());
});
secureSocket.on('data', data => {
console.log('received', data.toString());
setTimeout(() => {
console.log('sending ping');
secureSocket.write('ping');
}, 1000);
});
});
socket.on('error', (e) => { console.log(e); });
socket.on('close', () => {
socket.connect({
localPort: localPort,
port: peerPort
});
});
and
// clientB.js
const net = require('net');
const tls = require('tls');
const localPort = 20000;
const peerPort = 20001;
const socket = net.createConnection({
localPort: localPort,
port: peerPort
});
socket.on('connect', () => {
console.log('connected!');
// Do not call socket.write here! A "clean pipe" is needed to proceed to the upgrade
// This constitutes the upgrade.
const secureSocket = new tls.TLSSocket(socket, { isServer: false });
// This will start the ping-pong
secureSocket.write('pong');
secureSocket.on('keylog', (keylog) => {
console.log('keylog', keylog.toString());
});
secureSocket.on('data', data => {
console.log('received', data.toString());
setTimeout(() => {
console.log('sending pong');
secureSocket.write('pong');
}, 1000);
});
});
socket.on('error', (e) => { console.log(e); });
socket.on('close', () => {
socket.connect({
localPort: localPort,
port: peerPort
});
});
new tls.TLSSocket(socket, option)
does indeed perform an upgrade when one side is declared as server and provides a secureContext, and the other one is declared as a client.
The client should write some data first.
There is no need to call connect on the secureSocket - the socket is connected, doing so would result in an EADDRINUSE.
Thanks again!