update
This commit is contained in:
parent
eb26a62a87
commit
739eeb63aa
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,3 +20,4 @@ dist_*/
|
||||
# custom
|
||||
**/.claude/settings.local.json
|
||||
data/
|
||||
readme.plan.md
|
||||
|
@ -53,24 +53,41 @@ export interface IDcRouterOptions {
|
||||
caPath?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* DNS domain for automatic DNS server setup with DoH
|
||||
* When set, DNS server will:
|
||||
* - Always bind to UDP port 53 on the VM's IP address
|
||||
* - Use socket-handler approach for DNS-over-HTTPS
|
||||
* - Automatically handle NS delegation validation
|
||||
/**
|
||||
* The nameserver domains (e.g., ['gatewaymain.lossless.directory', 'gatewaymain2.lossless.directory'])
|
||||
* These must have A records pointing to your server's IP
|
||||
* These are what go in the NS records for ALL domains in dnsScopes
|
||||
*/
|
||||
dnsDomain?: string;
|
||||
dnsNsDomains?: string[];
|
||||
|
||||
/**
|
||||
* DNS records to register when using dnsDomain
|
||||
* These are in addition to auto-generated records from email domains with internal-dns mode
|
||||
* Domains this DNS server is authoritative for (e.g., ['bleu.de', 'mail.social.io'])
|
||||
* NS records will be auto-generated for these domains
|
||||
* Any DNS record outside these scopes will trigger a warning
|
||||
* Email domains with `internal-dns` mode must be included here
|
||||
*/
|
||||
dnsScopes?: string[];
|
||||
|
||||
/**
|
||||
* IPs of proxies that forward traffic to your server (optional)
|
||||
* When defined AND useIngressProxy is true, A records with server IP are replaced with proxy IPs
|
||||
* If not defined or empty, all A records use the real server IP
|
||||
* Helps hide real server IP for security/privacy
|
||||
*/
|
||||
proxyIps?: string[];
|
||||
|
||||
/**
|
||||
* DNS records to register
|
||||
* Must be within the defined dnsScopes (or receive warning)
|
||||
* Only need A, CNAME, TXT, MX records (NS and SOA are auto-generated)
|
||||
* Can use `useIngressProxy: false` to expose real server IP (defaults to true)
|
||||
*/
|
||||
dnsRecords?: Array<{
|
||||
name: string;
|
||||
type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA';
|
||||
value: string;
|
||||
ttl?: number;
|
||||
useIngressProxy?: boolean; // Whether to replace server IP with proxy IP (default: true)
|
||||
}>;
|
||||
|
||||
/** DNS challenge configuration for ACME (optional) */
|
||||
@ -139,8 +156,9 @@ export class DcRouter {
|
||||
}
|
||||
}
|
||||
|
||||
// Set up DNS server if configured by dnsDomain
|
||||
if (this.options.dnsDomain) {
|
||||
// Set up DNS server if configured with nameservers and scopes
|
||||
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 &&
|
||||
this.options.dnsScopes && this.options.dnsScopes.length > 0) {
|
||||
await this.setupDnsWithSocketHandler();
|
||||
}
|
||||
|
||||
@ -177,10 +195,10 @@ export class DcRouter {
|
||||
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
|
||||
}
|
||||
|
||||
// If DNS domain is configured, add DNS routes
|
||||
if (this.options.dnsDomain) {
|
||||
// If DNS is configured, add DNS routes
|
||||
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
|
||||
const dnsRoutes = this.generateDnsRoutes();
|
||||
console.log(`DNS Routes for domain ${this.options.dnsDomain}:`, dnsRoutes);
|
||||
console.log(`DNS Routes for nameservers ${this.options.dnsNsDomains.join(', ')}:`, dnsRoutes);
|
||||
routes = [...routes, ...dnsRoutes];
|
||||
}
|
||||
|
||||
@ -395,7 +413,7 @@ export class DcRouter {
|
||||
* Generate SmartProxy routes for DNS configuration
|
||||
*/
|
||||
private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] {
|
||||
if (!this.options.dnsDomain) {
|
||||
if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -404,12 +422,15 @@ export class DcRouter {
|
||||
// Create routes for DNS-over-HTTPS paths
|
||||
const dohPaths = ['/dns-query', '/resolve'];
|
||||
|
||||
// Use the first nameserver domain for DoH routes
|
||||
const primaryNameserver = this.options.dnsNsDomains[0];
|
||||
|
||||
for (const path of dohPaths) {
|
||||
const dohRoute: plugins.smartproxy.IRouteConfig = {
|
||||
name: `dns-over-https-${path.replace('/', '')}`,
|
||||
match: {
|
||||
ports: [443], // HTTPS port for DoH
|
||||
domains: [this.options.dnsDomain],
|
||||
domains: [primaryNameserver],
|
||||
path: path
|
||||
},
|
||||
action: {
|
||||
@ -676,7 +697,8 @@ export class DcRouter {
|
||||
const recordsByDomain = new Map<string, typeof records>();
|
||||
|
||||
for (const record of records) {
|
||||
const pattern = record.name.includes('*') ? record.name : `*.${record.name}`;
|
||||
// Use exact domain name for registration - no automatic wildcard prefix
|
||||
const pattern = record.name;
|
||||
if (!recordsByDomain.has(pattern)) {
|
||||
recordsByDomain.set(pattern, []);
|
||||
}
|
||||
@ -724,6 +746,18 @@ export class DcRouter {
|
||||
return value;
|
||||
case 'NS':
|
||||
return value;
|
||||
case 'SOA':
|
||||
// SOA format: primary-ns admin-email serial refresh retry expire minimum
|
||||
const parts = value.split(' ');
|
||||
return {
|
||||
mname: parts[0],
|
||||
rname: parts[1],
|
||||
serial: parseInt(parts[2]),
|
||||
refresh: parseInt(parts[3]),
|
||||
retry: parseInt(parts[4]),
|
||||
expire: parseInt(parts[5]),
|
||||
minimum: parseInt(parts[6])
|
||||
};
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
@ -733,18 +767,23 @@ export class DcRouter {
|
||||
* Set up DNS server with socket handler for DoH
|
||||
*/
|
||||
private async setupDnsWithSocketHandler(): Promise<void> {
|
||||
if (!this.options.dnsDomain) {
|
||||
throw new Error('dnsDomain is required for DNS socket handler setup');
|
||||
if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
|
||||
throw new Error('dnsNsDomains is required for DNS server setup');
|
||||
}
|
||||
|
||||
logger.log('info', `Setting up DNS server with socket handler for domain: ${this.options.dnsDomain}`);
|
||||
if (!this.options.dnsScopes || this.options.dnsScopes.length === 0) {
|
||||
throw new Error('dnsScopes is required for DNS server setup');
|
||||
}
|
||||
|
||||
const primaryNameserver = this.options.dnsNsDomains[0];
|
||||
logger.log('info', `Setting up DNS server with primary nameserver: ${primaryNameserver}`);
|
||||
|
||||
// Get VM IP address for UDP binding
|
||||
const networkInterfaces = plugins.os.networkInterfaces();
|
||||
let vmIpAddress = '0.0.0.0'; // Default to all interfaces
|
||||
|
||||
// Try to find the VM's internal IP address
|
||||
for (const [name, interfaces] of Object.entries(networkInterfaces)) {
|
||||
for (const [_name, interfaces] of Object.entries(networkInterfaces)) {
|
||||
if (interfaces) {
|
||||
for (const iface of interfaces) {
|
||||
if (!iface.internal && iface.family === 'IPv4') {
|
||||
@ -761,7 +800,7 @@ export class DcRouter {
|
||||
udpBindInterface: vmIpAddress,
|
||||
httpsPort: 443, // Required but won't bind due to manual mode
|
||||
manualHttpsMode: true, // Enable manual HTTPS socket handling
|
||||
dnssecZone: this.options.dnsDomain,
|
||||
dnssecZone: primaryNameserver,
|
||||
// For now, use self-signed cert until we integrate with Let's Encrypt
|
||||
httpsKey: '',
|
||||
httpsCert: ''
|
||||
@ -771,10 +810,28 @@ export class DcRouter {
|
||||
await this.dnsServer.start();
|
||||
logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);
|
||||
|
||||
// Register DNS records if provided
|
||||
// Validate DNS configuration
|
||||
await this.validateDnsConfiguration();
|
||||
|
||||
// Generate and register authoritative records
|
||||
const authoritativeRecords = await this.generateAuthoritativeRecords();
|
||||
|
||||
// Generate email DNS records
|
||||
const emailDnsRecords = await this.generateEmailDnsRecords();
|
||||
|
||||
// Combine all records: authoritative, email, and user-defined
|
||||
const allRecords = [...authoritativeRecords, ...emailDnsRecords];
|
||||
if (this.options.dnsRecords && this.options.dnsRecords.length > 0) {
|
||||
this.registerDnsRecords(this.options.dnsRecords);
|
||||
logger.log('info', `Registered ${this.options.dnsRecords.length} DNS records`);
|
||||
allRecords.push(...this.options.dnsRecords);
|
||||
}
|
||||
|
||||
// Apply proxy IP replacement if configured
|
||||
this.applyProxyIpReplacement(allRecords);
|
||||
|
||||
// Register all DNS records
|
||||
if (allRecords.length > 0) {
|
||||
this.registerDnsRecords(allRecords);
|
||||
logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${this.options.dnsRecords?.length || 0} user-defined)`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -802,6 +859,189 @@ export class DcRouter {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate DNS configuration
|
||||
*/
|
||||
private async validateDnsConfiguration(): Promise<void> {
|
||||
if (!this.options.dnsNsDomains || !this.options.dnsScopes) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('info', 'Validating DNS configuration...');
|
||||
|
||||
// Check if email domains with internal-dns are in dnsScopes
|
||||
if (this.options.emailConfig?.domains) {
|
||||
for (const domainConfig of this.options.emailConfig.domains) {
|
||||
if (domainConfig.dnsMode === 'internal-dns' &&
|
||||
!this.options.dnsScopes.includes(domainConfig.domain)) {
|
||||
logger.log('warn', `Email domain '${domainConfig.domain}' with internal-dns mode is not in dnsScopes. It should be added to dnsScopes.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate user-provided DNS records are within scopes
|
||||
if (this.options.dnsRecords) {
|
||||
for (const record of this.options.dnsRecords) {
|
||||
const recordDomain = this.extractDomain(record.name);
|
||||
const isInScope = this.options.dnsScopes.some(scope =>
|
||||
recordDomain === scope || recordDomain.endsWith(`.${scope}`)
|
||||
);
|
||||
|
||||
if (!isInScope) {
|
||||
logger.log('warn', `DNS record for '${record.name}' is outside defined scopes [${this.options.dnsScopes.join(', ')}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate email DNS records for domains with internal-dns mode
|
||||
*/
|
||||
private async generateEmailDnsRecords(): Promise<Array<{name: string; type: string; value: string; ttl?: number}>> {
|
||||
const records: Array<{name: string; type: string; value: string; ttl?: number}> = [];
|
||||
|
||||
if (!this.options.emailConfig?.domains) {
|
||||
return records;
|
||||
}
|
||||
|
||||
// Filter domains with internal-dns mode
|
||||
const internalDnsDomains = this.options.emailConfig.domains.filter(
|
||||
domain => domain.dnsMode === 'internal-dns'
|
||||
);
|
||||
|
||||
for (const domainConfig of internalDnsDomains) {
|
||||
const domain = domainConfig.domain;
|
||||
const ttl = domainConfig.dns?.internal?.ttl || 3600;
|
||||
const mxPriority = domainConfig.dns?.internal?.mxPriority || 10;
|
||||
|
||||
// MX record - points to the domain itself for email handling
|
||||
records.push({
|
||||
name: domain,
|
||||
type: 'MX',
|
||||
value: `${mxPriority} ${domain}`,
|
||||
ttl
|
||||
});
|
||||
|
||||
// SPF record - using sensible defaults
|
||||
const spfRecord = 'v=spf1 a mx ~all';
|
||||
records.push({
|
||||
name: domain,
|
||||
type: 'TXT',
|
||||
value: spfRecord,
|
||||
ttl
|
||||
});
|
||||
|
||||
// DMARC record - using sensible defaults
|
||||
const dmarcPolicy = 'none'; // Start with 'none' policy for monitoring
|
||||
const dmarcEmail = `dmarc@${domain}`;
|
||||
records.push({
|
||||
name: `_dmarc.${domain}`,
|
||||
type: 'TXT',
|
||||
value: `v=DMARC1; p=${dmarcPolicy}; rua=mailto:${dmarcEmail}`,
|
||||
ttl
|
||||
});
|
||||
|
||||
// Note: DKIM records will be generated later when DKIM keys are available
|
||||
// They require the DKIMCreator which is part of the email server
|
||||
}
|
||||
|
||||
logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`);
|
||||
return records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate authoritative DNS records (NS and SOA) for all domains in dnsScopes
|
||||
*/
|
||||
private async generateAuthoritativeRecords(): Promise<Array<{name: string; type: string; value: string; ttl?: number}>> {
|
||||
const records: Array<{name: string; type: string; value: string; ttl?: number}> = [];
|
||||
|
||||
if (!this.options.dnsNsDomains || !this.options.dnsScopes) {
|
||||
return records;
|
||||
}
|
||||
|
||||
const primaryNameserver = this.options.dnsNsDomains[0];
|
||||
|
||||
// Generate NS and SOA records for each domain in scopes
|
||||
for (const domain of this.options.dnsScopes) {
|
||||
// Add NS records for all nameservers
|
||||
for (const nsDomain of this.options.dnsNsDomains) {
|
||||
records.push({
|
||||
name: domain,
|
||||
type: 'NS',
|
||||
value: nsDomain,
|
||||
ttl: 3600
|
||||
});
|
||||
}
|
||||
|
||||
// Add SOA record with first nameserver as primary
|
||||
const soaValue = `${primaryNameserver} hostmaster.${domain} ${Date.now()} 7200 3600 1209600 3600`;
|
||||
records.push({
|
||||
name: domain,
|
||||
type: 'SOA',
|
||||
value: soaValue,
|
||||
ttl: 3600
|
||||
});
|
||||
}
|
||||
|
||||
logger.log('info', `Generated ${records.length} authoritative records for ${this.options.dnsScopes.length} domains`);
|
||||
return records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the base domain from a DNS record name
|
||||
*/
|
||||
private extractDomain(recordName: string): string {
|
||||
// Handle wildcards
|
||||
if (recordName.startsWith('*.')) {
|
||||
recordName = recordName.substring(2);
|
||||
}
|
||||
return recordName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply proxy IP replacement logic to DNS records
|
||||
*/
|
||||
private applyProxyIpReplacement(records: Array<{name: string; type: string; value: string; ttl?: number; useIngressProxy?: boolean}>): void {
|
||||
if (!this.options.proxyIps || this.options.proxyIps.length === 0) {
|
||||
return; // No proxy IPs configured, skip replacement
|
||||
}
|
||||
|
||||
// Get server's public IP (for now, we'll use a placeholder - in production this would be detected)
|
||||
// This would normally be detected from network interfaces or external service
|
||||
const serverIp = this.detectServerPublicIp();
|
||||
if (!serverIp) {
|
||||
logger.log('warn', 'Could not detect server public IP, skipping proxy IP replacement');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('info', `Applying proxy IP replacement. Server IP: ${serverIp}, Proxy IPs: ${this.options.proxyIps.join(', ')}`);
|
||||
|
||||
let proxyIndex = 0;
|
||||
for (const record of records) {
|
||||
if (record.type === 'A' &&
|
||||
record.value === serverIp &&
|
||||
record.useIngressProxy !== false) {
|
||||
// Round-robin through proxy IPs
|
||||
const proxyIp = this.options.proxyIps[proxyIndex % this.options.proxyIps.length];
|
||||
logger.log('info', `Replacing A record for ${record.name}: ${record.value} → ${proxyIp}`);
|
||||
record.value = proxyIp;
|
||||
proxyIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the server's public IP address
|
||||
*/
|
||||
private detectServerPublicIp(): string | null {
|
||||
// In a real implementation, this would:
|
||||
// 1. Check network interfaces for public IPs
|
||||
// 2. Or make a request to an external service to get public IP
|
||||
// For now, return null to skip proxy replacement
|
||||
// TODO: Implement proper public IP detection
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create mail socket handler for email traffic
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user