Implement comprehensive P2P domain conflict resolution allowing users to choose between local claim hashes and consensus-resolved hashes for domains where they have local claims but another claimant won consensus. Key features: - P2P Domain Conflicts UI tab in Local DNS section - Hash preference toggle (local vs resolved) with automatic client restart - Extended selector_cache.json to store hashPreferences alongside versionPreferences - DNS cache invalidation for immediate preference application - REST API endpoints for conflict detection and preference management - Automatic Holesail client restart when hash preferences change - Complete documentation updates across README, API docs, and glossary Resolves conflicts between local claims and consensus resolution by giving users control over which hash their domain resolves to, with seamless client management ensuring immediate effect.
689 lines
28 KiB
JavaScript
689 lines
28 KiB
JavaScript
const fs = require('fs').promises;
|
|
const dgram = require('dgram');
|
|
const crypto = require('crypto');
|
|
const state = require('../../infrastructure/state');
|
|
const { addDomain } = require('../../core/domains');
|
|
const { validateHolesailClient } = require('../../infrastructure/validation');
|
|
const { createInterfaceForDomain } = require('../../networking/virtual_interfaces');
|
|
const { logDebug, logError, logInfo, logWarn } = require('../../infrastructure/logger');
|
|
const { startHolesailServer, saveHolesailServers } = require('../holesail-servers');
|
|
const { startForkedHolesailClient, saveHolesailClients } = require('../holesail-clients');
|
|
const { ensurePortFree } = require('../port-management');
|
|
const { broadcast } = require('../websocket');
|
|
|
|
const domainsFile = process.env.DOMAINS_FILE || './cache/domains.json';
|
|
|
|
async function handleHolesailRoutes(req, res) {
|
|
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
|
|
const method = req.method;
|
|
|
|
if (method === 'GET' && urlPath === '/api/holesail-servers') {
|
|
try {
|
|
const servers = Array.from(state.holesailOpts.entries()).map(([id, opts]) => {
|
|
const child = state.holesailChildren.get(id);
|
|
const info = state.holesailInfos.get(id) || {};
|
|
const status = child && !child.killed ? 'running' : 'stopped';
|
|
return { id, opts, info: { ...info, state: status } };
|
|
});
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(servers));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to fetch Holesail servers: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ error: 'Failed to fetch Holesail servers' }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (method === 'GET' && urlPath === '/api/holesail-clients') {
|
|
try {
|
|
const clients = Array.from(state.holesailClientOpts.entries()).map(([id, opts]) => {
|
|
const child = state.holesailClientChildren.get(id);
|
|
const info = state.holesailClientInfos.get(id) || {};
|
|
const key = `${opts.domain}:${opts.port}`;
|
|
const isHolesailActive = state.holesails.has(key);
|
|
const isChildRunning = child && !child.killed;
|
|
let status = 'stopped';
|
|
if (isChildRunning && isHolesailActive && info.state !== 'error') {
|
|
status = 'running';
|
|
} else if (isChildRunning || isHolesailActive) {
|
|
status = 'starting';
|
|
} else if (info.state === 'error') {
|
|
status = 'error';
|
|
}
|
|
return { id, opts, info: { ...info, state: status } };
|
|
});
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(clients));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to fetch Holesail clients: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ error: 'Failed to fetch Holesail clients' }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/holesail-create') {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const data = JSON.parse(body);
|
|
const opts = { ...data };
|
|
const domain = opts.domain;
|
|
delete opts.domain;
|
|
const id = crypto.randomBytes(16).toString('hex');
|
|
logDebug('Admin', `Creating Holesail server ${id} on ${opts.host || '0.0.0.0'}:${opts.port} without port check`);
|
|
const { id: createdId, info } = await startHolesailServer(id, opts);
|
|
if (domain) {
|
|
const hash = info.url;
|
|
await addDomain(domain, hash);
|
|
if (!state.domainToIPMap.has(domain)) {
|
|
await createInterfaceForDomain(domain);
|
|
logDebug('Admin', `Assigned IP to ${domain}: ${state.domainToIPMap.get(domain)}`);
|
|
}
|
|
let domains = [];
|
|
if (await fs.access(domainsFile).then(() => true).catch(() => false)) {
|
|
const parsed = JSON.parse(await fs.readFile(domainsFile, 'utf8'));
|
|
if (!Array.isArray(parsed)) {
|
|
logError('Admin', `Domains file does not contain an array, resetting to empty array`);
|
|
domains = [];
|
|
} else {
|
|
domains = parsed;
|
|
}
|
|
}
|
|
const existingIndex = domains.findIndex(d => d.domain === domain);
|
|
if (existingIndex !== -1) {
|
|
domains[existingIndex].hash = hash;
|
|
} else {
|
|
domains.push({ domain, hash });
|
|
}
|
|
await fs.writeFile(domainsFile, JSON.stringify(domains, null, 2));
|
|
logInfo('Admin', `Automatically added domain ${domain} with hash ${hash} to P2P network and domains.json`);
|
|
}
|
|
await saveHolesailServers();
|
|
broadcast({ type: 'update-holesail' });
|
|
broadcast({ type: 'update-database' });
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ id: createdId }));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to create Holesail server: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(err.message);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/holesail-delete') {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const { id } = JSON.parse(body);
|
|
const child = state.holesailChildren.get(id);
|
|
const opts = state.holesailOpts.get(id);
|
|
if (child) {
|
|
child.kill('SIGTERM');
|
|
await new Promise(resolve => {
|
|
child.on('exit', () => resolve());
|
|
setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
logWarn('Admin', `Forced SIGKILL for Holesail server child ${id}`);
|
|
resolve();
|
|
}, 3000);
|
|
});
|
|
state.holesailChildren.delete(id);
|
|
state.holesailChildStartTimes.delete(id);
|
|
logInfo('Admin', `Closed Holesail server child process ${id}`);
|
|
}
|
|
state.holesailOpts.delete(id);
|
|
state.holesailInfos.delete(id);
|
|
await saveHolesailServers();
|
|
broadcast({ type: 'update-holesail' });
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('OK');
|
|
} catch (err) {
|
|
logError('Admin', `Failed to delete Holesail server: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(err.message);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/holesail-restart') {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const { id } = JSON.parse(body);
|
|
const child = state.holesailChildren.get(id);
|
|
const opts = state.holesailOpts.get(id);
|
|
if (!opts) {
|
|
throw new Error('Server not found');
|
|
}
|
|
let exitPromise;
|
|
if (child) {
|
|
logDebug('Admin', `Terminating existing Holesail server child process ${id}`);
|
|
exitPromise = new Promise((resolve) => {
|
|
child.once('exit', resolve);
|
|
setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
logWarn('Admin', `Forced SIGKILL for Holesail server child ${id}`);
|
|
resolve();
|
|
}, 3000);
|
|
});
|
|
child.kill('SIGTERM');
|
|
await exitPromise;
|
|
logInfo('Admin', `Closed Holesail server child process ${id}`);
|
|
}
|
|
state.holesailInfos.delete(id);
|
|
broadcast({ type: 'update-holesail' });
|
|
logDebug('Admin', `Restarting Holesail server ${id} on ${opts.host || '0.0.0.0'}:${opts.port} without port check`);
|
|
const { id: createdId, info } = await startHolesailServer(id, opts);
|
|
if (opts.domain) {
|
|
const hash = info.url.replace('hs://', '');
|
|
await addDomain(opts.domain, hash);
|
|
if (!state.domainToIPMap.has(opts.domain)) {
|
|
await createInterfaceForDomain(opts.domain);
|
|
logDebug('Admin', `Assigned IP to ${opts.domain}: ${state.domainToIPMap.get(opts.domain)}`);
|
|
}
|
|
let domains = [];
|
|
if (await fs.access(domainsFile).then(() => true).catch(() => false)) {
|
|
const parsed = JSON.parse(await fs.readFile(domainsFile, 'utf8'));
|
|
if (!Array.isArray(parsed)) {
|
|
logError('Admin', `Domains file does not contain an array, resetting to empty array`);
|
|
domains = [];
|
|
} else {
|
|
domains = parsed;
|
|
}
|
|
}
|
|
const existingIndex = domains.findIndex(d => d.domain === opts.domain);
|
|
if (existingIndex !== -1) {
|
|
domains[existingIndex].hash = hash;
|
|
} else {
|
|
domains.push({ domain: opts.domain, hash });
|
|
}
|
|
await fs.writeFile(domainsFile, JSON.stringify(domains, null, 2));
|
|
logInfo('Admin', `Automatically added domain ${opts.domain} with hash ${hash} to P2P network and domains.json`);
|
|
}
|
|
await saveHolesailServers();
|
|
broadcast({ type: 'update-holesail' });
|
|
broadcast({ type: 'update-database' });
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('OK');
|
|
} catch (err) {
|
|
logError('Admin', `Failed to restart Holesail server: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(err.message);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/holesail-client-create') {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const data = JSON.parse(body);
|
|
const validation = validateHolesailClient(data);
|
|
if (!validation.valid) {
|
|
res.writeHead(400);
|
|
res.end(validation.error || 'Invalid input');
|
|
return;
|
|
}
|
|
const { domain, key, port, protocol } = validation;
|
|
if (!state.domainToIPMap.has(domain)) {
|
|
await createInterfaceForDomain(domain);
|
|
logDebug('Admin', `Assigned IP to ${domain}: ${state.domainToIPMap.get(domain)}`);
|
|
}
|
|
const ip = state.domainToIPMap.get(domain);
|
|
const portFree = await ensurePortFree(ip, port);
|
|
if (!portFree) {
|
|
throw new Error(`Unable to ensure port ${port} free on ${ip}`);
|
|
}
|
|
const id = crypto.randomBytes(16).toString('hex');
|
|
state.holesailClientInfos.set(id, { state: 'starting' });
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
await startForkedHolesailClient(id, { domain, key, port, protocol: protocol || 'tcp' });
|
|
state.holesailClientInfos.set(id, { ...state.holesailClientInfos.get(id), state: 'running' });
|
|
await saveHolesailClients();
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ id }));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to create Holesail client: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(err.message);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/holesail-client-delete') {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const { id } = JSON.parse(body);
|
|
const child = state.holesailClientChildren.get(id);
|
|
const opts = state.holesailClientOpts.get(id);
|
|
if (child) {
|
|
child.kill('SIGTERM');
|
|
await new Promise(resolve => {
|
|
child.on('exit', () => resolve());
|
|
setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
logWarn('Admin', `Forced SIGKILL for Holesail client child ${id}`);
|
|
resolve();
|
|
}, 3000);
|
|
});
|
|
state.holesailClientChildren.delete(id);
|
|
state.holesailChildStartTimes.delete(id);
|
|
logInfo('Admin', `Closed Holesail client ${id} for ${opts.domain}:${opts.port}`);
|
|
}
|
|
if (opts) {
|
|
const key = `${opts.domain}:${opts.port}`;
|
|
const holesail = state.holesails.get(key);
|
|
if (holesail) {
|
|
if (holesail instanceof dgram.Socket) {
|
|
await new Promise(resolve => {
|
|
holesail.close(() => {
|
|
logInfo('Admin', `Closed UDP Holesail connection for ${key}`);
|
|
resolve();
|
|
});
|
|
setTimeout(() => {
|
|
logWarn('Admin', `Timeout closing UDP Holesail for ${key}, forcing closure`);
|
|
holesail.close();
|
|
resolve();
|
|
}, 5000);
|
|
});
|
|
} else {
|
|
await holesail.close();
|
|
logInfo('Admin', `Closed TCP Holesail connection for ${key}`);
|
|
}
|
|
state.holesails.delete(key);
|
|
if (state.holesailStartTimes) {
|
|
state.holesailStartTimes.delete(key);
|
|
}
|
|
}
|
|
const tlsServer = state.tlsServers.get(key);
|
|
if (tlsServer) {
|
|
await new Promise(resolve => {
|
|
tlsServer.close(resolve);
|
|
setTimeout(() => {
|
|
logWarn('Admin', `Timeout closing TLS server for ${key}, forcing closure`);
|
|
tlsServer.destroy ? tlsServer.destroy() : tlsServer.close();
|
|
resolve();
|
|
}, 5000);
|
|
});
|
|
state.tlsServers.delete(key);
|
|
logInfo('Admin', `Closed TLS server for ${key}`);
|
|
}
|
|
const httpServer = state.httpServers.get(key);
|
|
if (httpServer) {
|
|
await new Promise(resolve => {
|
|
httpServer.close(resolve);
|
|
setTimeout(() => {
|
|
logWarn('Admin', `Timeout closing HTTP server for ${key}, forcing closure`);
|
|
httpServer.destroy ? httpServer.destroy() : httpServer.close();
|
|
resolve();
|
|
}, 5000);
|
|
});
|
|
state.httpServers.delete(key);
|
|
logInfo('Admin', `Closed HTTP server for ${key}`);
|
|
}
|
|
const ip = state.domainToIPMap.get(opts.domain);
|
|
if (ip && opts.port) {
|
|
const freed = await ensurePortFree(ip, opts.port);
|
|
if (!freed) {
|
|
logError('Admin', `Failed to ensure port ${opts.port} free on ${ip} for ${key}`);
|
|
} else {
|
|
logInfo('Admin', `Successfully ensured port ${opts.port} free on ${ip} for ${key}`);
|
|
}
|
|
}
|
|
}
|
|
state.holesailClientOpts.delete(id);
|
|
state.holesailClientInfos.delete(id);
|
|
await saveHolesailClients();
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('OK');
|
|
} catch (err) {
|
|
logError('Admin', `Failed to delete Holesail client: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(err.message);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/holesail-client-restart') {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const { id } = JSON.parse(body);
|
|
logDebug('Admin', `Initiating restart for Holesail client ${id}`);
|
|
const child = state.holesailClientChildren.get(id);
|
|
const opts = state.holesailClientOpts.get(id);
|
|
if (!opts) {
|
|
throw new Error(`Client ${id} not found`);
|
|
}
|
|
const key = `${opts.domain}:${opts.port}`;
|
|
let exitPromise;
|
|
if (child) {
|
|
logDebug('Admin', `Terminating existing child process for client ${id}`);
|
|
exitPromise = new Promise((resolve) => {
|
|
child.once('exit', resolve);
|
|
setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
logWarn('Admin', `Forced SIGKILL for Holesail client child ${id}`);
|
|
resolve();
|
|
}, 3000);
|
|
});
|
|
child.kill('SIGTERM');
|
|
await exitPromise;
|
|
state.holesailClientChildren.delete(id);
|
|
state.holesailChildStartTimes.delete(id);
|
|
logInfo('Admin', `Closed Holesail client child process ${id}`);
|
|
}
|
|
const holesail = state.holesails.get(key);
|
|
if (holesail) {
|
|
logDebug('Admin', `Closing Holesail connection for ${key}`);
|
|
if (holesail instanceof dgram.Socket) {
|
|
await new Promise((resolve, reject) => {
|
|
holesail.close((err) => {
|
|
if (err) {
|
|
logWarn('Admin', `Error closing UDP Holesail for ${key}: ${err.message}`);
|
|
reject(err);
|
|
} else {
|
|
logInfo('Admin', `Closed UDP Holesail connection for ${key}`);
|
|
resolve();
|
|
}
|
|
});
|
|
setTimeout(() => {
|
|
logWarn('Admin', `Timeout closing UDP Holesail for ${key}, forcing closure`);
|
|
try {
|
|
holesail.close();
|
|
resolve();
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
}, 5000);
|
|
});
|
|
} else {
|
|
await holesail.close();
|
|
logInfo('Admin', `Closed TCP Holesail connection for ${key}`);
|
|
}
|
|
state.holesails.delete(key);
|
|
if (state.holesailStartTimes) {
|
|
state.holesailStartTimes.delete(key);
|
|
}
|
|
}
|
|
const tlsServer = state.tlsServers.get(key);
|
|
if (tlsServer) {
|
|
logDebug('Admin', `Closing TLS server for ${key}`);
|
|
await new Promise(resolve => {
|
|
tlsServer.close(resolve);
|
|
setTimeout(() => {
|
|
logWarn('Admin', `Timeout closing TLS server for ${key}, forcing closure`);
|
|
tlsServer.destroy ? tlsServer.destroy() : tlsServer.close();
|
|
resolve();
|
|
}, 5000);
|
|
});
|
|
state.tlsServers.delete(key);
|
|
logInfo('Admin', `Closed TLS server for ${key}`);
|
|
}
|
|
const httpServer = state.httpServers.get(key);
|
|
if (httpServer) {
|
|
logDebug('Admin', `Closing HTTP server for ${key}`);
|
|
await new Promise(resolve => {
|
|
httpServer.close(resolve);
|
|
setTimeout(() => {
|
|
logWarn('Admin', `Timeout closing HTTP server for ${key}, forcing closure`);
|
|
httpServer.destroy ? httpServer.destroy() : httpServer.close();
|
|
resolve();
|
|
}, 5000);
|
|
});
|
|
state.httpServers.delete(key);
|
|
logInfo('Admin', `Closed HTTP server for ${key}`);
|
|
}
|
|
state.holesailClientInfos.set(id, { state: 'starting' });
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
logInfo('Admin', `Holesail client ${id} stopped, preparing to restart`);
|
|
if (!state.domainToIPMap.has(opts.domain)) {
|
|
await createInterfaceForDomain(opts.domain);
|
|
logDebug('Admin', `Assigned IP to ${opts.domain}: ${state.domainToIPMap.get(opts.domain)}`);
|
|
}
|
|
const ip = state.domainToIPMap.get(opts.domain);
|
|
const portFree = await ensurePortFree(ip, opts.port);
|
|
if (!portFree) {
|
|
state.holesailClientInfos.set(id, { state: 'error', error: `Unable to free port ${opts.port} on ${ip}` });
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
throw new Error(`Unable to ensure port ${opts.port} free on ${ip}`);
|
|
}
|
|
await startForkedHolesailClient(id, opts);
|
|
logInfo('Admin', `Successfully restarted Holesail client ${id} for ${opts.domain}:${opts.port}`);
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
const isHolesailActive = state.holesails.has(key);
|
|
if (!isHolesailActive) {
|
|
logWarn('Admin', `Holesail client ${id} for ${key} started but not active in state.holesails. Attempting final restart.`);
|
|
state.holesailClientInfos.set(id, { state: 'starting' });
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
await startForkedHolesailClient(id, opts);
|
|
}
|
|
const finalCheck = state.holesails.has(key);
|
|
if (!finalCheck) {
|
|
state.holesailClientInfos.set(id, { state: 'error', error: `Failed to start after final attempt` });
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
throw new Error(`Holesail client ${id} for ${key} failed to start after final attempt`);
|
|
}
|
|
state.holesailClientInfos.set(id, { ...state.holesailClientInfos.get(id), state: 'running' });
|
|
logDebug('Admin', `Verified Holesail client ${id} is active for ${key}`);
|
|
await saveHolesailClients();
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('OK');
|
|
} catch (err) {
|
|
state.holesailClientInfos.set(id, { state: 'error', error: err.message });
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
logError('Admin', `Failed to restart Holesail client: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(err.message);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/restart-holesail-clients-for-domain') {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const { domain } = JSON.parse(body);
|
|
logInfo('Admin', `Restarting all Holesail connections for domain ${domain} due to hash preference change`);
|
|
|
|
// Find all active Holesail connections for this domain
|
|
const keysToClose = [];
|
|
for (const key of state.holesails.keys()) {
|
|
const [connectionDomain, port] = key.split(':');
|
|
if (connectionDomain === domain) {
|
|
keysToClose.push(key);
|
|
}
|
|
}
|
|
|
|
// Also find admin-managed clients for this domain
|
|
const clientIdsToRestart = [];
|
|
for (const [id, opts] of state.holesailClientOpts) {
|
|
if (opts.domain === domain) {
|
|
clientIdsToRestart.push(id);
|
|
}
|
|
}
|
|
|
|
logInfo('Admin', `Found ${keysToClose.length} active connections and ${clientIdsToRestart.length} admin clients for domain ${domain}`);
|
|
|
|
// Close all active DNS-triggered connections for this domain
|
|
const closePromises = keysToClose.map(async (key) => {
|
|
try {
|
|
logDebug('Admin', `Closing Holesail connection for ${key}`);
|
|
const holesail = state.holesails.get(key);
|
|
if (holesail) {
|
|
if (holesail instanceof dgram.Socket) {
|
|
await new Promise((resolve, reject) => {
|
|
holesail.close((err) => {
|
|
if (err) {
|
|
logWarn('Admin', `Error closing UDP Holesail for ${key}: ${err.message}`);
|
|
reject(err);
|
|
} else {
|
|
logInfo('Admin', `Closed UDP Holesail connection for ${key}`);
|
|
resolve();
|
|
}
|
|
});
|
|
setTimeout(() => {
|
|
logWarn('Admin', `Timeout closing UDP Holesail for ${key}, forcing closure`);
|
|
try {
|
|
holesail.close();
|
|
resolve();
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
}, 2000);
|
|
});
|
|
} else {
|
|
holesail.close();
|
|
logInfo('Admin', `Closed TCP Holesail connection for ${key}`);
|
|
}
|
|
}
|
|
state.holesails.delete(key);
|
|
if (state.holesailStartTimes) {
|
|
state.holesailStartTimes.delete(key);
|
|
}
|
|
} catch (err) {
|
|
logError('Admin', `Failed to close Holesail connection ${key}: ${err.message}`);
|
|
}
|
|
});
|
|
|
|
// Restart admin-managed clients
|
|
const restartPromises = clientIdsToRestart.map(async (id) => {
|
|
try {
|
|
logDebug('Admin', `Restarting admin-managed Holesail client ${id} for domain ${domain}`);
|
|
const child = state.holesailClientChildren.get(id);
|
|
const opts = state.holesailClientOpts.get(id);
|
|
if (!opts) {
|
|
logWarn('Admin', `Client options not found for ${id}`);
|
|
return;
|
|
}
|
|
|
|
// Get the new hash for the domain
|
|
const { getHashForDomain } = require('../../core/core');
|
|
const newHash = await getHashForDomain(domain);
|
|
|
|
const key = `${opts.domain}:${opts.port}`;
|
|
|
|
// Terminate existing child process
|
|
let exitPromise;
|
|
if (child) {
|
|
logDebug('Admin', `Terminating existing child process for client ${id}`);
|
|
exitPromise = new Promise((resolve) => {
|
|
child.on('exit', () => {
|
|
logDebug('Admin', `Child process exited for client ${id}`);
|
|
resolve();
|
|
});
|
|
child.on('error', (err) => {
|
|
logWarn('Admin', `Error during child process termination for ${id}: ${err.message}`);
|
|
resolve();
|
|
});
|
|
});
|
|
child.kill('SIGTERM');
|
|
// Force kill after 5 seconds
|
|
setTimeout(() => {
|
|
if (!child.killed) {
|
|
logWarn('Admin', `Force killing child process for client ${id}`);
|
|
child.kill('SIGKILL');
|
|
}
|
|
}, 5000);
|
|
await exitPromise;
|
|
}
|
|
|
|
// Clean up old state
|
|
state.holesailClientChildren.delete(id);
|
|
state.holesailClientInfos.delete(id);
|
|
state.holesailChildStartTimes.delete(id);
|
|
|
|
// Start new client with updated hash
|
|
logInfo('Admin', `Starting new admin-managed Holesail client for ${domain} with hash ${newHash}`);
|
|
const { startHolesailClient } = require('../../admin/admin-backend/admin-holesail');
|
|
await startHolesailClient(domain, newHash, opts.ip, opts.port);
|
|
|
|
// Wait a moment for the connection to establish
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
// Verify the new connection is active
|
|
const newKey = `${domain}:${opts.port}`;
|
|
if (state.holesails.has(newKey)) {
|
|
logInfo('Admin', `Successfully restarted admin-managed Holesail client for ${domain} - new connection active`);
|
|
} else {
|
|
logWarn('Admin', `Admin-managed Holesail client restart for ${domain} completed but connection not yet active`);
|
|
}
|
|
|
|
} catch (err) {
|
|
logError('Admin', `Failed to restart admin-managed Holesail client ${id}: ${err.message}`);
|
|
}
|
|
});
|
|
|
|
await Promise.all([...closePromises, ...restartPromises]);
|
|
|
|
// Create new DNS-triggered connections if needed
|
|
const { getHashForDomain } = require('../../core/core');
|
|
const { startHolesailClient: startNetworkingHolesailClient } = require('../../networking/holesail');
|
|
const { createInterfaceForDomain } = require('../../networking/virtual_interfaces');
|
|
const newHash = await getHashForDomain(domain);
|
|
|
|
// Ensure domain has an IP assigned
|
|
let localIP = state.domainToIPMap[domain];
|
|
if (!localIP) {
|
|
logInfo('Admin', `Assigning IP for domain ${domain} during restart`);
|
|
localIP = await createInterfaceForDomain(domain);
|
|
}
|
|
|
|
if (localIP && newHash) {
|
|
logInfo('Admin', `Creating new DNS-triggered Holesail client for ${domain} with hash ${newHash} on IP ${localIP}`);
|
|
try {
|
|
await startNetworkingHolesailClient(domain, newHash, localIP, state.internalPort);
|
|
logInfo('Admin', `Successfully created new DNS-triggered Holesail client for ${domain}`);
|
|
} catch (err) {
|
|
logError('Admin', `Failed to create new DNS-triggered Holesail client for ${domain}: ${err.message}`);
|
|
}
|
|
} else {
|
|
logWarn('Admin', `Cannot create DNS-triggered client for ${domain}: localIP=${localIP}, newHash=${newHash}`);
|
|
}
|
|
|
|
// Final verification - ensure new connections are established
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
// Check that we have active connections for this domain
|
|
const finalConnections = Array.from(state.holesails.keys()).filter(key => key.startsWith(`${domain}:`));
|
|
logInfo('Admin', `Final verification: ${finalConnections.length} active connections for domain ${domain}`);
|
|
|
|
broadcast({ type: 'update-holesail-clients' });
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('OK');
|
|
|
|
} catch (err) {
|
|
logError('Admin', `Failed to restart Holesail clients for domain: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(err.message);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
module.exports = { handleHolesailRoutes };
|
|
|