@serve.zone/dcrouter 17.9.2 → 17.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +7 -2
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +11 -0
- package/dist_ts/dns/classes.dns-server-runtime.js +81 -1
- package/dist_ts/dns/classes.gateway-route-dns-reconciler.js +29 -4
- package/dist_ts_migrations/index.js +70 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +8 -1
- package/ts/dns/classes.dns-server-runtime.ts +88 -0
- package/ts/dns/classes.gateway-route-dns-reconciler.ts +31 -3
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/dcrouter',
|
|
6
|
-
version: '17.9.
|
|
6
|
+
version: '17.9.4',
|
|
7
7
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfd2ViLzAwX2NvbW1pdGluZm9fZGF0YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUNILE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRztJQUN4QixJQUFJLEVBQUUsc0JBQXNCO0lBQzVCLE9BQU8sRUFBRSxRQUFRO0lBQ2pCLFdBQVcsRUFBRSwwRUFBMEU7Q0FDeEYsQ0FBQSJ9
|
package/package.json
CHANGED
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -743,6 +743,9 @@ export class DcRouter {
|
|
|
743
743
|
} catch (err: unknown) {
|
|
744
744
|
logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
|
|
745
745
|
}
|
|
746
|
+
this.dnsServerRuntime.syncPrivateRouteOverrides(
|
|
747
|
+
routes as IDcRouterRouteConfig[],
|
|
748
|
+
);
|
|
746
749
|
},
|
|
747
750
|
(preparedRoutes) => buildHttpRedirectRuntimeRoutes(preparedRoutes || []),
|
|
748
751
|
(storedRoute: IRoute) => this.emailRouteBuilder.hydrateStoredRouteForRuntime(storedRoute),
|
|
@@ -832,10 +835,14 @@ export class DcRouter {
|
|
|
832
835
|
|
|
833
836
|
// DNS Server: optional, depends on SmartProxy
|
|
834
837
|
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 && this.options.dnsScopes && this.options.dnsScopes.length > 0) {
|
|
838
|
+
const dnsServerDeps = ['SmartProxy'];
|
|
839
|
+
if (this.options.dbConfig?.enabled !== false) {
|
|
840
|
+
dnsServerDeps.push('ConfigManagers', 'EmailServer');
|
|
841
|
+
}
|
|
835
842
|
this.serviceManager.addService(
|
|
836
843
|
new plugins.taskbuffer.Service('DnsServer')
|
|
837
844
|
.optional()
|
|
838
|
-
.dependsOn(
|
|
845
|
+
.dependsOn(...dnsServerDeps)
|
|
839
846
|
.withStart(async () => {
|
|
840
847
|
await this.dnsServerRuntime.setup();
|
|
841
848
|
})
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
3
|
import { buildEmailDnsRecords } from '../email/index.js';
|
|
4
4
|
import type { DcRouter } from '../classes.dcrouter.js';
|
|
5
|
+
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
|
5
6
|
|
|
6
7
|
type TDnsRecordSeed = { name: string; type: string; value: string; ttl?: number; useIngressProxy?: boolean };
|
|
7
8
|
|
|
@@ -18,6 +19,8 @@ export class DnsServerRuntime {
|
|
|
18
19
|
private batchCount = 0;
|
|
19
20
|
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
20
21
|
|
|
22
|
+
private privateRouteHostnames = new Set<string>();
|
|
23
|
+
private privateRouteTargetIp?: string;
|
|
21
24
|
constructor(private dcRouterRef: DcRouter) {}
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -58,6 +61,10 @@ export class DnsServerRuntime {
|
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
this.privateRouteTargetIp = plugins.net.isIP(vmIpAddress)
|
|
65
|
+
&& vmIpAddress !== '0.0.0.0' && vmIpAddress !== '::'
|
|
66
|
+
? vmIpAddress
|
|
67
|
+
: undefined;
|
|
61
68
|
// Create DNS server instance with manual HTTPS mode
|
|
62
69
|
const dnsServer = new plugins.smartdns.dnsServerMod.DnsServer({
|
|
63
70
|
udpPort: 53,
|
|
@@ -72,6 +79,7 @@ export class DnsServerRuntime {
|
|
|
72
79
|
httpsCert: ''
|
|
73
80
|
});
|
|
74
81
|
this.dcRouterRef.dnsServer = dnsServer;
|
|
82
|
+
this.registerPrivateRouteHandler(dnsServer);
|
|
75
83
|
|
|
76
84
|
// Start the DNS server. smartdns owns UDP and DNS-over-TCP; dcrouter
|
|
77
85
|
// keeps DNS-over-HTTPS routed through SmartProxy manual HTTPS mode.
|
|
@@ -172,6 +180,86 @@ export class DnsServerRuntime {
|
|
|
172
180
|
this.dcRouterRef.mailDnsSync?.requestSync('DNS server attached');
|
|
173
181
|
}
|
|
174
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Keep an internal-only A-record overlay for exact route hostnames whose
|
|
185
|
+
* compiled source policy is private or whose only ingress is SmartVPN.
|
|
186
|
+
* Public routes compile without clientIp restrictions and are never added.
|
|
187
|
+
*/
|
|
188
|
+
public syncPrivateRouteOverrides(routesArg: IDcRouterRouteConfig[]): void {
|
|
189
|
+
const nextHostnames = new Set<string>();
|
|
190
|
+
for (const route of routesArg) {
|
|
191
|
+
if (route.action?.type !== 'forward') continue;
|
|
192
|
+
const ingress = route.ingress;
|
|
193
|
+
if (!ingress?.directHub && !ingress?.smartVpn) continue;
|
|
194
|
+
|
|
195
|
+
const clientIp = route.match?.clientIp;
|
|
196
|
+
const clientIpEntries = Array.isArray(clientIp)
|
|
197
|
+
? clientIp
|
|
198
|
+
: [];
|
|
199
|
+
const hasRestrictedSourceProfile = clientIpEntries
|
|
200
|
+
.map((entryArg) => entryArg.trim())
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.some((entryArg) => entryArg !== '*');
|
|
203
|
+
const isSmartVpnOnly = ingress.smartVpn && !ingress.directHub;
|
|
204
|
+
if (!hasRestrictedSourceProfile && !isSmartVpnOnly) continue;
|
|
205
|
+
|
|
206
|
+
const domains = route.match?.domains;
|
|
207
|
+
const domainEntries = Array.isArray(domains)
|
|
208
|
+
? domains
|
|
209
|
+
: typeof domains === 'string'
|
|
210
|
+
? domains.split(',')
|
|
211
|
+
: [];
|
|
212
|
+
for (const domainArg of domainEntries) {
|
|
213
|
+
const hostname = this.normalizePrivateRouteHostname(domainArg);
|
|
214
|
+
if (hostname) nextHostnames.add(hostname);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const changed = nextHostnames.size !== this.privateRouteHostnames.size
|
|
219
|
+
|| [...nextHostnames].some((hostnameArg) => !this.privateRouteHostnames.has(hostnameArg));
|
|
220
|
+
this.privateRouteHostnames = nextHostnames;
|
|
221
|
+
if (changed) {
|
|
222
|
+
logger.log(
|
|
223
|
+
'info',
|
|
224
|
+
`DNS private-route overlay now serves ${nextHostnames.size} exact hostname(s)`,
|
|
225
|
+
{ zone: 'dns' },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private registerPrivateRouteHandler(
|
|
231
|
+
dnsServerArg: plugins.smartdns.dnsServerMod.DnsServer,
|
|
232
|
+
): void {
|
|
233
|
+
dnsServerArg.registerHandler('*', ['A'], (questionArg) => {
|
|
234
|
+
const hostname = this.normalizePrivateRouteHostname(questionArg.name);
|
|
235
|
+
if (
|
|
236
|
+
!hostname
|
|
237
|
+
|| questionArg.type.toUpperCase() !== 'A'
|
|
238
|
+
|| !this.privateRouteTargetIp
|
|
239
|
+
|| !this.privateRouteHostnames.has(hostname)
|
|
240
|
+
) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
name: hostname,
|
|
245
|
+
type: 'A',
|
|
246
|
+
class: 'IN',
|
|
247
|
+
ttl: 60,
|
|
248
|
+
data: this.privateRouteTargetIp,
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private normalizePrivateRouteHostname(hostnameArg: string): string | undefined {
|
|
254
|
+
const hostname = hostnameArg.trim().toLowerCase().replace(/\.$/, '');
|
|
255
|
+
if (!hostname || hostname.includes('*') || hostname.includes(',')) return undefined;
|
|
256
|
+
if (hostname.length > 253) return undefined;
|
|
257
|
+
const labels = hostname.split('.');
|
|
258
|
+
if (labels.some((labelArg) => !labelArg || labelArg.length > 63
|
|
259
|
+
|| !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(labelArg))) return undefined;
|
|
260
|
+
return hostname;
|
|
261
|
+
}
|
|
262
|
+
|
|
175
263
|
/**
|
|
176
264
|
* Create the DoH socket handler SmartProxy routes hand TLS sockets to.
|
|
177
265
|
*/
|
|
@@ -128,7 +128,9 @@ export class GatewayRouteDnsReconciler {
|
|
|
128
128
|
return { success: true, retryable: false, mode, status: 'not-applicable', checkedAt };
|
|
129
129
|
}
|
|
130
130
|
const hostname = normalizeGatewayHostname(optionsArg.hostname);
|
|
131
|
-
const targetIp = optionsArg.delete
|
|
131
|
+
const targetIp = optionsArg.delete
|
|
132
|
+
? undefined
|
|
133
|
+
: this.getDesiredTargetIp(optionsArg.gatewayClientId, hostname);
|
|
132
134
|
const workKey = this.buildWorkKey(optionsArg, hostname, mode, targetIp);
|
|
133
135
|
const latestIntent = this.latestIntentByHostname.get(hostname);
|
|
134
136
|
if (latestIntent?.workKey === workKey) return await latestIntent.promise;
|
|
@@ -217,7 +219,7 @@ export class GatewayRouteDnsReconciler {
|
|
|
217
219
|
status: 'target-invalid',
|
|
218
220
|
hostname,
|
|
219
221
|
checkedAt,
|
|
220
|
-
message: 'Gateway DNS target requires a valid
|
|
222
|
+
message: 'Gateway DNS target requires a valid hub DNS bind address or public ingress IP',
|
|
221
223
|
};
|
|
222
224
|
}
|
|
223
225
|
const recordType = ipFamily === 6 ? 'AAAA' : 'A';
|
|
@@ -543,7 +545,33 @@ export class GatewayRouteDnsReconciler {
|
|
|
543
545
|
return this.successForZone('managed-deleted', modeArg, checkedAtArg, hostnameArg, undefined, undefined, zone, []);
|
|
544
546
|
}
|
|
545
547
|
|
|
546
|
-
private getDesiredTargetIp(
|
|
548
|
+
private getDesiredTargetIp(
|
|
549
|
+
gatewayClientIdArg: string,
|
|
550
|
+
hostnameArg: string,
|
|
551
|
+
): string | undefined {
|
|
552
|
+
const gatewayClientId = gatewayClientIdArg.trim().toLowerCase();
|
|
553
|
+
const claimRoutes = (this.dcRouterRef.routeConfigManager?.getMergedRoutes().routes || [])
|
|
554
|
+
.filter((routeArg) => {
|
|
555
|
+
if (!routeArg.enabled || routeArg.metadata?.ownerType !== 'gatewayClient'
|
|
556
|
+
|| routeArg.metadata.gatewayDnsMode !== 'reconcile'
|
|
557
|
+
|| routeArg.metadata.gatewayClientId?.trim().toLowerCase() !== gatewayClientId) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
return this.getRouteHostname(routeArg.route) === hostnameArg;
|
|
561
|
+
});
|
|
562
|
+
const hasPublicIngress = claimRoutes.some((routeArg) => routeArg.route.remoteIngress?.enabled);
|
|
563
|
+
const hasPrivateHubIngress = claimRoutes.length > 0 && claimRoutes.every((routeArg) => {
|
|
564
|
+
const ingress = routeArg.route.ingress;
|
|
565
|
+
return !routeArg.route.remoteIngress?.enabled
|
|
566
|
+
&& Boolean(ingress?.directHub || ingress?.smartVpn);
|
|
567
|
+
});
|
|
568
|
+
if (!hasPublicIngress && hasPrivateHubIngress) {
|
|
569
|
+
const dnsBindInterface = this.dcRouterRef.options.dnsBindInterface?.trim();
|
|
570
|
+
if (dnsBindInterface && dnsBindInterface !== '0.0.0.0' && dnsBindInterface !== '::') {
|
|
571
|
+
return dnsBindInterface;
|
|
572
|
+
}
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
547
575
|
return this.dcRouterRef.options.proxyIps?.[0]
|
|
548
576
|
|| this.dcRouterRef.options.publicIp
|
|
549
577
|
|| this.dcRouterRef.detectedPublicIp
|