311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
const { exec } = require('child_process');
|
|
const dgram = require('dgram');
|
|
const dnsPacket = require('dns-packet');
|
|
const HolesailClient = require('holesail-client');
|
|
const Corestore = require('corestore');
|
|
const Hyperswarm = require('hyperswarm');
|
|
const http = require('http');
|
|
const b4a = require('b4a');
|
|
const { createHash } = require('crypto');
|
|
const net = require('net');
|
|
const os = require('os');
|
|
|
|
// Corestore and Hyperswarm setup for P2P sync
|
|
const store = new Corestore('./my-storage');
|
|
const swarm = new Hyperswarm();
|
|
const dnsCore = store.get({ name: 'dns-core' });
|
|
|
|
let holesailClient = null;
|
|
const dnsServer = dgram.createSocket('udp4');
|
|
const domainToIPMap = {};
|
|
let currentIP = 2; // Start assigning IP addresses from 192.168.100.2
|
|
|
|
// Helper function to remove existing virtual interface if it already exists (for macOS)
|
|
function removeExistingInterface(subnetName) {
|
|
return new Promise((resolve) => {
|
|
exec(`sudo ifconfig ${subnetName} down`, (err) => {
|
|
if (err) resolve();
|
|
else {
|
|
console.log(`Removed existing virtual interface: ${subnetName}`);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Helper function to create virtual interfaces (for macOS)
|
|
async function createVirtualInterface(subnetName, subnetCIDR) {
|
|
await removeExistingInterface(subnetName);
|
|
return new Promise((resolve, reject) => {
|
|
exec(`sudo ifconfig ${subnetName} alias ${subnetCIDR}`, (err, stdout, stderr) => {
|
|
if (err) {
|
|
console.error(`Error creating virtual interface ${subnetName}:`, stderr);
|
|
reject(`Error creating virtual interface ${subnetName}: ${stderr}`);
|
|
} else {
|
|
console.log(`Virtual interface ${subnetName} created with CIDR ${subnetCIDR}.`);
|
|
resolve(subnetCIDR);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Updated function to create virtual interfaces dynamically, starting from 192.168.100.2
|
|
async function createInterfaceForDomain(domain) {
|
|
const subnetID = currentIP;
|
|
const subnetName = `lo0`;
|
|
const subnetCIDR = `192.168.100.${subnetID}/24`;
|
|
|
|
try {
|
|
await createVirtualInterface(subnetName, subnetCIDR);
|
|
domainToIPMap[domain] = `192.168.100.${subnetID}`;
|
|
logDebug(`Assigned virtual interface IP: 192.168.100.${subnetID} for domain: ${domain}`);
|
|
currentIP++;
|
|
return domainToIPMap[domain];
|
|
} catch (error) {
|
|
console.error(error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Debug log wrapper for standardized output
|
|
function logDebug(info) {
|
|
const timestamp = new Date().toISOString();
|
|
console.log(`[DEBUG ${timestamp}] ${info}`);
|
|
}
|
|
|
|
// Function to check if a port is responsive
|
|
function checkPortResponsive(ip, port) {
|
|
return new Promise((resolve) => {
|
|
const client = new net.Socket();
|
|
client.setTimeout(2000);
|
|
|
|
client.connect(port, ip, () => {
|
|
client.destroy();
|
|
resolve(true);
|
|
});
|
|
|
|
client.on('error', () => resolve(false));
|
|
client.on('timeout', () => {
|
|
client.destroy();
|
|
resolve(false);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Function to check public DNS using Cloudflare
|
|
function checkPublicDNS(domain) {
|
|
return new Promise((resolve) => {
|
|
const resolver = dgram.createSocket('udp4');
|
|
const query = dnsPacket.encode({
|
|
type: 'query',
|
|
id: Math.floor(Math.random() * 65535),
|
|
questions: [{ type: 'A', name: domain }]
|
|
});
|
|
|
|
resolver.send(query, 53, '192.168.0.16', (err) => {
|
|
if (err) {
|
|
logDebug(`Error forwarding DNS query to 1.1.1.1: ${err}`);
|
|
return resolve(null);
|
|
}
|
|
});
|
|
|
|
resolver.on('message', (res) => {
|
|
const response = dnsPacket.decode(res);
|
|
if (response.answers.length > 0) {
|
|
logDebug(`Public DNS returned records for ${domain}`);
|
|
resolve(response.answers);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
resolver.close();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Function to start Holesail client for tunneling and use port 80 with SO_REUSEADDR
|
|
function startHolesailClient(domain, hash, ip, port) {
|
|
logDebug(`Starting Holesail client for domain: ${domain}, hash: ${hash}, IP: ${ip}, Port: ${port}`);
|
|
|
|
const connector = setupConnector(hash);
|
|
holesailClient = new HolesailClient(connector);
|
|
|
|
holesailClient.connect({ port: port, address: ip, reuseAddr: true }, () => {
|
|
logDebug(`Holesail client for ${domain} connected on ${ip}:${port}`);
|
|
});
|
|
|
|
setTimeout(() => {
|
|
logDebug(`Destroying Holesail client for ${domain}`);
|
|
holesailClient.destroy();
|
|
}, 300000);
|
|
}
|
|
|
|
// Function to restart Holesail client if the port is unresponsive
|
|
async function restartHolesailClient(domain, hash, ip, port) {
|
|
const isResponsive = await checkPortResponsive(ip, port);
|
|
|
|
if (!isResponsive) {
|
|
logDebug(`Port ${port} on ${ip} is unresponsive, destroying and recreating the Holesail client`);
|
|
if (holesailClient) {
|
|
holesailClient.destroy();
|
|
}
|
|
|
|
await createInterfaceForDomain(domain);
|
|
startHolesailClient(domain, hash, ip, port);
|
|
} else {
|
|
logDebug(`Port ${port} on ${ip} is responsive, using the existing connection.`);
|
|
}
|
|
}
|
|
|
|
// Ensure the DNS Core is ready before joining the swarm
|
|
(async () => {
|
|
await dnsCore.ready();
|
|
const topic = dnsCore.discoveryKey;
|
|
|
|
logDebug(`DNS Core ready, joining Hyperswarm with topic: ${topic.toString('hex')}`);
|
|
|
|
swarm.join(topic, { server: true, client: true });
|
|
|
|
swarm.on('connection', (conn) => {
|
|
logDebug('Peer connected, starting replication...');
|
|
dnsCore.replicate(conn);
|
|
});
|
|
})();
|
|
|
|
// Function to add a domain and hash to the DNS core (P2P network)
|
|
async function addDomain(domain, hash) {
|
|
await dnsCore.ready();
|
|
const record = JSON.stringify({ domain, hash });
|
|
|
|
logDebug(`Adding domain ${domain} with hash ${hash} to DNS core`);
|
|
await dnsCore.append(Buffer.from(record));
|
|
logDebug(`Domain ${domain} added to DNS core`);
|
|
}
|
|
|
|
// Function to handle long connectors (128-byte) or standard connectors (64-byte)
|
|
function setupConnector(keyInput) {
|
|
if (keyInput.length === 64) {
|
|
logDebug(`Using 64-byte connector: ${keyInput}`);
|
|
return keyInput;
|
|
} else {
|
|
logDebug(`Hashing 128-byte connector: ${keyInput}`);
|
|
const connector = createHash('sha256').update(keyInput.toString()).digest('hex');
|
|
const seed = Buffer.from(connector, 'hex');
|
|
return b4a.toString(seed, 'hex');
|
|
}
|
|
}
|
|
|
|
// DNS Server Setup for listening on port 53
|
|
dnsServer.on('message', async (msg, rinfo) => {
|
|
const query = dnsPacket.decode(msg);
|
|
const domain = query.questions[0].name;
|
|
const type = query.questions[0].type;
|
|
|
|
logDebug(`DNS query received: Domain = ${domain}, Type = ${type}`);
|
|
|
|
await dnsCore.ready();
|
|
let p2pRecord = null;
|
|
|
|
for await (const data of dnsCore.createReadStream()) {
|
|
const record = JSON.parse(data.toString());
|
|
if (record.domain === domain) {
|
|
p2pRecord = record;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const publicDNSRecords = await checkPublicDNS(domain);
|
|
|
|
if (p2pRecord) {
|
|
const localIP = domainToIPMap[domain] || await createInterfaceForDomain(domain);
|
|
startHolesailClient(domain, p2pRecord.hash, localIP, 80);
|
|
|
|
const response = dnsPacket.encode({
|
|
type: 'response',
|
|
id: query.id,
|
|
questions: query.questions,
|
|
answers: [{
|
|
type: 'A',
|
|
name: domain,
|
|
ttl: 300,
|
|
data: localIP
|
|
}]
|
|
});
|
|
dnsServer.send(response, rinfo.port, rinfo.address);
|
|
} else if (publicDNSRecords) {
|
|
const response = dnsPacket.encode({
|
|
type: 'response',
|
|
id: query.id,
|
|
questions: query.questions,
|
|
answers: publicDNSRecords
|
|
});
|
|
dnsServer.send(response, rinfo.port, rinfo.address);
|
|
} else {
|
|
logDebug(`No P2P or public DNS records found for ${domain}`);
|
|
const response = dnsPacket.encode({
|
|
type: 'response',
|
|
id: query.id,
|
|
questions: query.questions,
|
|
answers: []
|
|
});
|
|
dnsServer.send(response, rinfo.port, rinfo.address);
|
|
}
|
|
});
|
|
|
|
// Ensure DNS server binds to port 53 with elevated permissions (may require sudo)
|
|
dnsServer.bind(53, '0.0.0.0', (err) => {
|
|
if (err) {
|
|
console.error(`Failed to bind DNS server to port 53: ${err.message}`);
|
|
process.exit(1);
|
|
} else {
|
|
logDebug('DNS Server running on port 53, bound to 0.0.0.0');
|
|
}
|
|
});
|
|
|
|
// HTTP Server with DNS query before proxying
|
|
http.createServer(async (req, res) => {
|
|
const domain = req.url.replace("/", "");
|
|
logDebug(`HTTP request for domain: ${domain}`);
|
|
|
|
try {
|
|
const localIP = domainToIPMap[domain] || await createInterfaceForDomain(domain);
|
|
|
|
if (!localIP) {
|
|
logDebug(`No DNS records for ${domain}`);
|
|
res.writeHead(404);
|
|
return res.end('Domain not found');
|
|
}
|
|
|
|
await restartHolesailClient(domain, '78b94094b5ee1e352fec1f37e9cf3780561bcaa6ef9e0678100c9283888f', localIP, 80);
|
|
|
|
const options = {
|
|
hostname: localIP,
|
|
port: 80,
|
|
path: req.url,
|
|
method: req.method,
|
|
headers: req.headers,
|
|
};
|
|
|
|
const proxyRequest = http.request(options, (proxyRes) => {
|
|
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
proxyRes.pipe(res, { end: true });
|
|
});
|
|
|
|
proxyRequest.on('error', (err) => {
|
|
logDebug(`Error proxying request for ${domain}: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end('Internal Server Error');
|
|
});
|
|
|
|
req.pipe(proxyRequest, { end: true });
|
|
} catch (err) {
|
|
logDebug(`Failed to handle request for ${domain}: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end('Internal Server Error');
|
|
}
|
|
}).listen(80, '127.0.0.1', () => {
|
|
logDebug('HTTP server running on port 80');
|
|
});
|
|
|
|
// Example: Add a domain to the P2P DNS system
|
|
addDomain('hello.geek', '78b94094b5ee1e352fec1f37e9cf3780561bcaa6ef9e0678100c9283888f');
|