hyper-nat
Version:
Securely tunnel UDP and TCP connections over peer-to-peer networks using hyperswarm for NAT traversal
553 lines (471 loc) • 18.8 kB
JavaScript
const DHT = require("@hyperswarm/dht");
const pump = require("pump");
const net = require("net");
const udp = require('dgram');
/**
* DHT Relay functionality for hyper-nat
* Handles TCP, UDP, and TCP-over-UDP protocols
*/
class DHTRelay {
constructor() {
this.node = null;
}
async initialize() {
if (!this.node) {
this.node = new DHT();
await this.node.ready();
}
return this;
}
async createRelay() {
await this.initialize();
return {
tcp: {
server: this.createTcpServer.bind(this),
client: this.createTcpClient.bind(this)
},
udp: {
server: this.createUdpServer.bind(this),
client: this.createUdpClient.bind(this)
},
tcpudp: {
server: this.createTcpUdpServer.bind(this),
client: this.createTcpUdpClient.bind(this)
}
};
}
async createTcpServer(keyPair, port, host) {
const server = this.node.createServer({ reusableSocket: true });
server.on("connection", (servsock) => {
console.log('new connection, relaying to ' + port);
const socket = net.connect({
port,
host,
allowHalfOpen: false,
timeout: 15000
});
let destroyed = false;
const cleanup = () => {
if (!destroyed) {
destroyed = true;
socket.end();
servsock.end();
}
};
socket.on('timeout', () => {
console.log('Local connection timeout');
cleanup();
});
socket.on('error', (err) => {
console.log('Local connection error:', err.message);
cleanup();
});
servsock.on('error', (err) => {
console.log('Remote connection error:', err.message);
cleanup();
});
socket.on('end', () => {
console.log('Local connection ended naturally');
servsock.end();
});
servsock.on('end', () => {
console.log('Remote connection ended naturally');
socket.end();
});
pump(servsock, socket, servsock, (err) => {
if (err) {
console.log('Pump error:', err.message);
}
cleanup();
});
});
console.log('listening for remote connections for tcp', port);
server.listen(keyPair);
}
async createTcpClient(publicKey, port, config) {
console.log('connecting to tcp', port);
// Test connection with retries
let attempts = 3;
let lastError;
while (attempts > 0) {
try {
const testSocket = this.node.connect(publicKey, { reusableSocket: true });
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
testSocket.destroy();
reject(new Error('Connection timeout after 15 seconds'));
}, 15000);
testSocket.on('open', () => {
clearTimeout(timeout);
testSocket.destroy();
resolve();
});
testSocket.on('error', (err) => {
clearTimeout(timeout);
reject(err);
});
});
console.log('Connection established');
break;
} catch (err) {
lastError = err;
attempts--;
if (attempts > 0) {
console.log(`Connection failed, retrying... (${attempts} attempts remaining)`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
if (attempts === 0) {
throw lastError;
}
console.log('connection ready');
const server = net.createServer({allowHalfOpen: false}, (local) => {
console.log('new local connection on port ' + config.localPort + ', relaying to remote tcp ' + port);
const socket = this.node.connect(publicKey, { reusableSocket: true });
let destroyed = false;
const cleanup = () => {
if (!destroyed) {
destroyed = true;
socket.end();
local.end();
}
};
const connectTimeout = setTimeout(() => {
console.log('Connection timeout');
cleanup();
}, 15000);
socket.on('open', () => {
clearTimeout(connectTimeout);
console.log('Remote connection established');
});
socket.on('error', (err) => {
console.log('Remote connection error:', err.message);
cleanup();
});
local.on('error', (err) => {
console.log('Local connection error:', err.message);
cleanup();
});
socket.on('end', () => {
console.log('Remote connection ended naturally');
local.end();
});
local.on('end', () => {
console.log('Local connection ended naturally');
socket.end();
});
pump(local, socket, local, (err) => {
if (err) {
console.log('Pump error:', err.message);
}
cleanup();
});
});
server.listen(config.localPort, "127.0.0.1");
console.log('TCP stream ready, listening for connections on', port);
}
async createUdpServer(keyPair, port, host) {
const server = this.node.createServer();
server.on("connection", (conn) => {
console.log('new connection, relaying to ' + port);
const client = udp.createSocket('udp4');
let destroyed = false;
const cleanup = () => {
if (!destroyed) {
destroyed = true;
client.close();
conn.end();
}
};
client.on('error', (err) => {
console.log('UDP client error:', err.message);
cleanup();
});
conn.on('error', (err) => {
console.log('UDP connection error:', err.message);
cleanup();
});
conn.on('end', () => {
console.log('UDP connection ended naturally');
cleanup();
});
client.connect(port, host);
client.on('message', (buf) => {
if (!destroyed) {
try {
conn.rawStream.send(buf);
} catch (err) {
console.log('Error sending to remote:', err.message);
cleanup();
}
}
});
conn.rawStream.on('message', (buf) => {
if (!destroyed) {
try {
client.send(buf);
} catch (err) {
console.log('Error sending to local:', err.message);
cleanup();
}
}
});
});
console.log('listening for remote connections for udp', port);
await server.listen(keyPair);
}
async createUdpClient(publicKey, port, config) {
console.log('connecting to udp', port);
const conn = await this.node.connect(publicKey);
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
conn.destroy();
reject(new Error('Connection timeout after 15 seconds'));
}, 15000);
conn.on('open', () => {
clearTimeout(timeout);
resolve();
});
conn.on('error', (err) => {
clearTimeout(timeout);
reject(err);
});
});
console.log('connection open');
const server = udp.createSocket('udp4');
let inport;
server.on('message', async (buf, rinfo) => {
if (!inport) {
console.log('setting port', rinfo);
inport = rinfo.port;
}
conn.rawStream.send(buf);
});
conn.rawStream.on('message', (buf) => {
server.send(buf, inport);
});
server.bind(config.localPort);
console.log('UDP stream ready, listening for packets on ', port);
}
async createTcpUdpServer(keyPair, port, host) {
const server = this.node.createServer();
server.on("connection", (conn) => {
console.log('new connection, relaying to tcp-over-udp ' + port);
const socket = net.connect({port, host, allowHalfOpen: true });
socket.setNoDelay(true);
socket.setTimeout(60000); // 60 second timeout for browser compatibility
let tcpEnded = false;
let udpEnded = false;
const gracefulClose = () => {
if (tcpEnded && udpEnded) return;
if (!tcpEnded && socket && !socket.destroyed) {
tcpEnded = true;
socket.end();
}
if (!udpEnded && conn && !conn.destroyed) {
udpEnded = true;
conn.end();
}
};
socket.on('connect', () => {
console.log('connected to local service');
socket.on('data', (data) => {
try {
if (conn && !conn.destroyed && conn.rawStream) {
conn.rawStream.send(data);
}
} catch (err) {
console.log('error sending data to remote:', err.message);
gracefulClose();
}
});
conn.rawStream.on('message', (data) => {
try {
if (socket && !socket.destroyed && socket.writable) {
socket.write(data);
}
} catch (err) {
console.log('error writing data to local socket:', err.message);
gracefulClose();
}
});
});
// Handle socket events
this.setupTcpUdpSocketHandlers(socket, conn, gracefulClose);
});
console.log('listening for remote connections for tcp-over-udp', port);
await server.listen(keyPair);
}
async createTcpUdpClient(publicKey, port, config) {
console.log('connecting to tcp-over-udp', port);
const testConn = await this.node.connect(publicKey);
await new Promise(res => testConn.on('open', res));
testConn.destroy();
console.log('connection ready');
const server = net.createServer({allowHalfOpen: true}, async (localSocket) => {
console.log('new local tcp connection, relaying to remote tcp-over-udp', port);
localSocket.setNoDelay(true);
localSocket.setTimeout(60000); // 60 second timeout for browser compatibility
let tcpEnded = false;
let udpEnded = false;
const gracefulClose = () => {
if (tcpEnded && udpEnded) return;
if (!tcpEnded && localSocket && !localSocket.destroyed) {
tcpEnded = true;
localSocket.end();
}
};
try {
const conn = this.node.connect(publicKey);
await new Promise((resolve, reject) => {
conn.on('open', resolve);
conn.on('error', reject);
setTimeout(() => reject(new Error('Connection timeout')), 10000);
});
localSocket.on('data', (data) => {
try {
if (conn && !conn.destroyed && conn.rawStream) {
conn.rawStream.send(data);
}
} catch (err) {
console.log('error sending data to remote:', err.message);
gracefulClose();
}
});
conn.rawStream.on('message', (data) => {
try {
if (localSocket && !localSocket.destroyed && localSocket.writable) {
localSocket.write(data);
}
} catch (err) {
console.log('error writing data to local socket:', err.message);
if (conn && !conn.destroyed) {
conn.end();
}
}
});
// Setup handlers for both sockets
this.setupTcpUdpClientHandlers(localSocket, conn, gracefulClose, tcpEnded, udpEnded);
} catch (err) {
console.log('failed to connect to remote:', err.message);
tcpEnded = true;
localSocket.destroy();
}
});
server.listen(config.localPort, "127.0.0.1");
console.log('TCP-over-UDP stream ready, listening for connections on', config.localPort);
}
setupTcpUdpSocketHandlers(socket, conn, gracefulClose) {
let tcpEnded = false;
let udpEnded = false;
socket.on('close', () => {
console.log('local tcp connection closed');
tcpEnded = true;
if (conn && !conn.destroyed) {
conn.end();
}
});
socket.on('end', () => {
console.log('local tcp connection ended');
tcpEnded = true;
if (conn && !conn.destroyed) {
conn.end();
}
});
socket.on('timeout', () => {
console.log('local tcp socket timeout');
gracefulClose();
});
socket.on('error', (err) => {
console.log('local tcp socket error:', err.message);
tcpEnded = true;
if (conn && !conn.destroyed) {
conn.destroy();
}
});
conn.on('close', () => {
console.log('remote udp connection closed');
udpEnded = true;
if (socket && !socket.destroyed) {
if (socket.readyState === 'open') {
socket.end();
} else {
socket.destroy();
}
}
});
conn.on('end', () => {
console.log('remote udp connection ended');
udpEnded = true;
if (socket && !socket.destroyed) {
socket.end();
}
});
conn.on('error', (err) => {
console.log('remote udp connection error:', err.message);
udpEnded = true;
if (socket && !socket.destroyed) {
socket.destroy();
}
});
}
setupTcpUdpClientHandlers(localSocket, conn, gracefulClose, tcpEnded, udpEnded) {
localSocket.on('close', () => {
console.log('local tcp connection closed');
tcpEnded = true;
if (conn && !conn.destroyed) {
conn.end();
}
});
localSocket.on('end', () => {
console.log('local tcp connection ended');
tcpEnded = true;
if (conn && !conn.destroyed) {
conn.end();
}
});
localSocket.on('timeout', () => {
console.log('local tcp socket timeout');
gracefulClose();
});
localSocket.on('error', (err) => {
console.log('local tcp socket error:', err.message);
tcpEnded = true;
if (conn && !conn.destroyed) {
conn.destroy();
}
});
conn.on('close', () => {
console.log('remote udp connection closed');
udpEnded = true;
if (localSocket && !localSocket.destroyed) {
if (localSocket.readyState === 'open') {
localSocket.end();
} else {
localSocket.destroy();
}
}
});
conn.on('end', () => {
console.log('remote udp connection ended');
udpEnded = true;
if (localSocket && !localSocket.destroyed) {
localSocket.end();
}
});
conn.on('error', (err) => {
console.log('remote udp connection error:', err.message);
udpEnded = true;
if (localSocket && !localSocket.destroyed) {
localSocket.destroy();
}
});
}
}
// Factory function to create relay instance
const createRelay = async () => {
const relay = new DHTRelay();
return await relay.createRelay();
};
module.exports = { DHTRelay, createRelay };