first commit
This commit is contained in:
commit
3b43c0ded0
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
my-storage
|
||||
.env
|
109
README.md
Normal file
109
README.md
Normal file
@ -0,0 +1,109 @@
|
||||
# P2P Decentralized DNS System with Holesail Integration
|
||||
|
||||
This project implements a firewall-resistant, peer-to-peer (P2P) DNS system that leverages UDP hole-punching to bypass common network restrictions, including firewalls, NAT, and CGNAT. Built in Node.js, it integrates with [Holesail](https://holesail.network) for tunneling, provides seamless DNS and HTTP proxying, and supports both public and private P2P networks.
|
||||
|
||||
## Features
|
||||
|
||||
- **Global Decentralized DNS**: Independent of traditional DNS infrastructure; no reliance on central DNS servers.
|
||||
- **Firewall & NAT Bypass**: Achieved using UDP hole-punching, allowing access across restricted networks like 4G, 5G, and satellite (e.g., Starlink).
|
||||
- **Local IP Assignment**: Dynamically assigns local IP addresses (192.168.100.x) for each domain, isolated to virtual network interfaces.
|
||||
- **Hybrid DNS Mode**: Resolves both P2P and public DNS records seamlessly.
|
||||
- **Integrated HTTP Proxy**: Proxies HTTP traffic directly to P2P tunnels, eliminating the need for third-party proxy servers.
|
||||
- **Domain-Driven Hash Proxying**: Routes connections via domain-based unique hashes, no need for traditional IP address exposure.
|
||||
- **Customizable P2P Network**: Initialize with a custom master key to create a private, isolated DNS network.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js**: Version 18+ recommended
|
||||
- **Holesail**: For tunneling and establishing P2P connections ([Holesail CLI setup](https://holesail.io/))
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Clone the Repository**
|
||||
|
||||
```bash
|
||||
git clone https://git.ssh.surf/snxraven/p2ns.git
|
||||
cd p2ns
|
||||
```
|
||||
|
||||
2. **Install Dependencies**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start the DNS Server**
|
||||
|
||||
Run the DNS server with elevated permissions (required for binding to port 53).
|
||||
|
||||
```bash
|
||||
sudo node index.js
|
||||
```
|
||||
|
||||
4. **Start the HTTP Server**
|
||||
|
||||
```bash
|
||||
node index.js
|
||||
```
|
||||
|
||||
### How to Add a Domain
|
||||
|
||||
To add a domain, use the `addDomain` function in the script, or add it programmatically. You’ll need to provide a unique connection hash for each domain, generated by Holesail.
|
||||
|
||||
#### Generating a Connection Hash
|
||||
|
||||
Use the following command to generate a live, publicly accessible connection hash from Holesail:
|
||||
|
||||
```bash
|
||||
holesail --live 80 --public
|
||||
```
|
||||
|
||||
This command will create a P2P tunnel on port 80 and output a connection hash. Example output:
|
||||
|
||||
```bash
|
||||
Connection hash: 8a5b90945f8fbd5d1b620be3c888a47aaae20706a7f140be4bfa0df9e0dbcf38
|
||||
```
|
||||
|
||||
### Example Domain Addition
|
||||
|
||||
Once you have a connection hash, add the domain to the DNS core with the following example code in `index.js`:
|
||||
|
||||
```javascript
|
||||
addDomain('example.tld', '8a5b90945f8fbd5d1b620be3c888a47aaae20706a7f140be4bfa0df9e0dbcf38');
|
||||
```
|
||||
|
||||
This command assigns a virtual IP and establishes a tunnel for `example.tld`.
|
||||
|
||||
### Starting Holesail Clients for Each Domain
|
||||
|
||||
The system will automatically start or reuse a Holesail client for each domain as requests come in, ensuring the connection stays alive and accessible over P2P.
|
||||
|
||||
### Optional: Running on a Private Network
|
||||
|
||||
To create a private DNS network, initialize the Holesail server and clients with a custom master key. Change the key in `holesail-client` to partition your DNS namespace from the public P2P DNS network.
|
||||
|
||||
## Usage
|
||||
|
||||
The P2P DNS server listens on port 53 for DNS requests and automatically proxies HTTP requests on port 80. This means you can access domains in your network without needing direct IPs or proxy servers.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
curl http://example.tld
|
||||
```
|
||||
|
||||
The system will route the request through the P2P network to the correct local IP, based on the domain's connection hash and DNS record.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Binding Issues on Port 53**: Run the DNS server with elevated permissions (`sudo`).
|
||||
- **DNS Lookup Errors**: Check that the domain and hash are correctly added to the DNS core.
|
||||
- **Firewall or NAT Issues**: Ensure that Holesail is set to `--public` for external access.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Automatic Key Rotation**: Enable the system to rotate keys for increased security.
|
||||
- **Dynamic Public/Private DNS Switching**: Allow users to toggle between public and private network modes.
|
||||
- **Additional Domain Support**: Add support for more complex DNS records (e.g., CNAME, MX).
|
329
p2ns.js
Normal file
329
p2ns.js
Normal file
@ -0,0 +1,329 @@
|
||||
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');
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const activeConnections = {}; // Store active connections by domain
|
||||
|
||||
// Function to start Holesail client for tunneling and use port 80 with SO_REUSEADDR
|
||||
function startHolesailClient(domain, hash, ip, port) {
|
||||
logDebug(`Attempting to start/reuse Holesail client for domain: ${domain}`);
|
||||
|
||||
if (activeConnections[domain]) {
|
||||
logDebug(`Reusing existing Holesail client for domain: ${domain} on ${ip}:${port}`);
|
||||
return activeConnections[domain]; // Reuse the existing connection
|
||||
}
|
||||
|
||||
logDebug(`Starting new Holesail client for domain: ${domain}, hash: ${hash}, IP: ${ip}, Port: ${port}`);
|
||||
|
||||
const connector = setupConnector(hash);
|
||||
const holesailClient = new HolesailClient(connector);
|
||||
|
||||
holesailClient.connect({ port: port, address: ip, reuseAddr: true }, () => {
|
||||
logDebug(`Holesail client for ${domain} connected on ${ip}:${port}`);
|
||||
});
|
||||
|
||||
// Store the client in activeConnections
|
||||
activeConnections[domain] = holesailClient;
|
||||
|
||||
// Set a timeout to destroy and remove from activeConnections after 5 minutes (300,000 ms)
|
||||
setTimeout(() => {
|
||||
logDebug(`Destroying Holesail client for domain ${domain}`);
|
||||
holesailClient.destroy();
|
||||
delete activeConnections[domain]; // Remove the client from activeConnections
|
||||
}, 300000);
|
||||
|
||||
return holesailClient;
|
||||
}
|
||||
|
||||
// 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`);
|
||||
|
||||
// Destroy the existing client if available and remove it from activeConnections
|
||||
if (activeConnections[domain]) {
|
||||
activeConnections[domain].destroy();
|
||||
delete activeConnections[domain];
|
||||
}
|
||||
|
||||
// Create a new connection and add it to activeConnections
|
||||
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, '07b8b52fbbad7a89ce26ad2d8375e6a82b2e3c02f18596bddff18e9c31164b04', 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', '07b8b52fbbad7a89ce26ad2d8375e6a82b2e3c02f18596bddff18e9c31164b04');
|
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "p2ns",
|
||||
"version": "1.0.0",
|
||||
"description": "Peer-to-Peer DNS system with Holesail tunneling",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"author": "Raven Scott",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"child_process": "^1.0.2",
|
||||
"dgram": "^1.0.1",
|
||||
"dns-packet": "^5.5.2",
|
||||
"holesail-client": "^1.0.0",
|
||||
"corestore": "^3.2.0",
|
||||
"hyperswarm": "^2.11.0",
|
||||
"http": "^0.0.1-security",
|
||||
"b4a": "^2.1.1",
|
||||
"crypto": "^1.0.1",
|
||||
"net": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user