update
This commit is contained in:
parent
eb26a62a87
commit
739eeb63aa
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,3 +20,4 @@ dist_*/
|
|||||||
# custom
|
# custom
|
||||||
**/.claude/settings.local.json
|
**/.claude/settings.local.json
|
||||||
data/
|
data/
|
||||||
|
readme.plan.md
|
||||||
|
@ -54,23 +54,40 @@ export interface IDcRouterOptions {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DNS domain for automatic DNS server setup with DoH
|
* The nameserver domains (e.g., ['gatewaymain.lossless.directory', 'gatewaymain2.lossless.directory'])
|
||||||
* When set, DNS server will:
|
* These must have A records pointing to your server's IP
|
||||||
* - Always bind to UDP port 53 on the VM's IP address
|
* These are what go in the NS records for ALL domains in dnsScopes
|
||||||
* - Use socket-handler approach for DNS-over-HTTPS
|
|
||||||
* - Automatically handle NS delegation validation
|
|
||||||
*/
|
*/
|
||||||
dnsDomain?: string;
|
dnsNsDomains?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DNS records to register when using dnsDomain
|
* Domains this DNS server is authoritative for (e.g., ['bleu.de', 'mail.social.io'])
|
||||||
* These are in addition to auto-generated records from email domains with internal-dns mode
|
* 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<{
|
dnsRecords?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA';
|
type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA';
|
||||||
value: string;
|
value: string;
|
||||||
ttl?: number;
|
ttl?: number;
|
||||||
|
useIngressProxy?: boolean; // Whether to replace server IP with proxy IP (default: true)
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/** DNS challenge configuration for ACME (optional) */
|
/** DNS challenge configuration for ACME (optional) */
|
||||||
@ -139,8 +156,9 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up DNS server if configured by dnsDomain
|
// Set up DNS server if configured with nameservers and scopes
|
||||||
if (this.options.dnsDomain) {
|
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 &&
|
||||||
|
this.options.dnsScopes && this.options.dnsScopes.length > 0) {
|
||||||
await this.setupDnsWithSocketHandler();
|
await this.setupDnsWithSocketHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,10 +195,10 @@ export class DcRouter {
|
|||||||
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
|
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
// If DNS domain is configured, add DNS routes
|
// If DNS is configured, add DNS routes
|
||||||
if (this.options.dnsDomain) {
|
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
|
||||||
const dnsRoutes = this.generateDnsRoutes();
|
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];
|
routes = [...routes, ...dnsRoutes];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,7 +413,7 @@ export class DcRouter {
|
|||||||
* Generate SmartProxy routes for DNS configuration
|
* Generate SmartProxy routes for DNS configuration
|
||||||
*/
|
*/
|
||||||
private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] {
|
private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] {
|
||||||
if (!this.options.dnsDomain) {
|
if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,12 +422,15 @@ export class DcRouter {
|
|||||||
// Create routes for DNS-over-HTTPS paths
|
// Create routes for DNS-over-HTTPS paths
|
||||||
const dohPaths = ['/dns-query', '/resolve'];
|
const dohPaths = ['/dns-query', '/resolve'];
|
||||||
|
|
||||||
|
// Use the first nameserver domain for DoH routes
|
||||||
|
const primaryNameserver = this.options.dnsNsDomains[0];
|
||||||
|
|
||||||
for (const path of dohPaths) {
|
for (const path of dohPaths) {
|
||||||
const dohRoute: plugins.smartproxy.IRouteConfig = {
|
const dohRoute: plugins.smartproxy.IRouteConfig = {
|
||||||
name: `dns-over-https-${path.replace('/', '')}`,
|
name: `dns-over-https-${path.replace('/', '')}`,
|
||||||
match: {
|
match: {
|
||||||
ports: [443], // HTTPS port for DoH
|
ports: [443], // HTTPS port for DoH
|
||||||
domains: [this.options.dnsDomain],
|
domains: [primaryNameserver],
|
||||||
path: path
|
path: path
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
@ -676,7 +697,8 @@ export class DcRouter {
|
|||||||
const recordsByDomain = new Map<string, typeof records>();
|
const recordsByDomain = new Map<string, typeof records>();
|
||||||
|
|
||||||
for (const record of 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)) {
|
if (!recordsByDomain.has(pattern)) {
|
||||||
recordsByDomain.set(pattern, []);
|
recordsByDomain.set(pattern, []);
|
||||||
}
|
}
|
||||||
@ -724,6 +746,18 @@ export class DcRouter {
|
|||||||
return value;
|
return value;
|
||||||
case 'NS':
|
case 'NS':
|
||||||
return value;
|
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:
|
default:
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@ -733,18 +767,23 @@ export class DcRouter {
|
|||||||
* Set up DNS server with socket handler for DoH
|
* Set up DNS server with socket handler for DoH
|
||||||
*/
|
*/
|
||||||
private async setupDnsWithSocketHandler(): Promise<void> {
|
private async setupDnsWithSocketHandler(): Promise<void> {
|
||||||
if (!this.options.dnsDomain) {
|
if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
|
||||||
throw new Error('dnsDomain is required for DNS socket handler setup');
|
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
|
// Get VM IP address for UDP binding
|
||||||
const networkInterfaces = plugins.os.networkInterfaces();
|
const networkInterfaces = plugins.os.networkInterfaces();
|
||||||
let vmIpAddress = '0.0.0.0'; // Default to all interfaces
|
let vmIpAddress = '0.0.0.0'; // Default to all interfaces
|
||||||
|
|
||||||
// Try to find the VM's internal IP address
|
// 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) {
|
if (interfaces) {
|
||||||
for (const iface of interfaces) {
|
for (const iface of interfaces) {
|
||||||
if (!iface.internal && iface.family === 'IPv4') {
|
if (!iface.internal && iface.family === 'IPv4') {
|
||||||
@ -761,7 +800,7 @@ export class DcRouter {
|
|||||||
udpBindInterface: vmIpAddress,
|
udpBindInterface: vmIpAddress,
|
||||||
httpsPort: 443, // Required but won't bind due to manual mode
|
httpsPort: 443, // Required but won't bind due to manual mode
|
||||||
manualHttpsMode: true, // Enable manual HTTPS socket handling
|
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
|
// For now, use self-signed cert until we integrate with Let's Encrypt
|
||||||
httpsKey: '',
|
httpsKey: '',
|
||||||
httpsCert: ''
|
httpsCert: ''
|
||||||
@ -771,10 +810,28 @@ export class DcRouter {
|
|||||||
await this.dnsServer.start();
|
await this.dnsServer.start();
|
||||||
logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);
|
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) {
|
if (this.options.dnsRecords && this.options.dnsRecords.length > 0) {
|
||||||
this.registerDnsRecords(this.options.dnsRecords);
|
allRecords.push(...this.options.dnsRecords);
|
||||||
logger.log('info', `Registered ${this.options.dnsRecords.length} DNS records`);
|
}
|
||||||
|
|
||||||
|
// 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
|
* Create mail socket handler for email traffic
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user