@serve.zone/dcrouter 15.11.0 → 16.0.0
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_serve/bundle.js +34 -34
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.dcrouter.d.ts +10 -3
- package/dist_ts/classes.dcrouter.js +39 -43
- package/dist_ts/db/documents/classes.cached.email.d.ts +32 -0
- package/dist_ts/db/documents/classes.cached.email.js +67 -3
- package/dist_ts/db/documents/classes.email-domain.doc.d.ts +2 -1
- package/dist_ts/db/documents/classes.email-domain.doc.js +8 -2
- package/dist_ts/db/documents/classes.email-server-settings.doc.js +2 -2
- package/dist_ts/db/documents/classes.remote-ingress-edge.doc.d.ts +3 -0
- package/dist_ts/db/documents/classes.remote-ingress-edge.doc.js +20 -2
- package/dist_ts/dns/classes.dns-server-runtime.js +3 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +5 -1
- package/dist_ts/email/classes.accepted-email-spool.js +9 -1
- package/dist_ts/email/classes.email-domain.manager.d.ts +17 -1
- package/dist_ts/email/classes.email-domain.manager.js +100 -17
- package/dist_ts/email/classes.email-route-builder.js +5 -2
- package/dist_ts/email/classes.email-settings.manager.js +3 -3
- package/dist_ts/email/classes.mail-dns-sync.d.ts +27 -0
- package/dist_ts/email/classes.mail-dns-sync.js +196 -0
- package/dist_ts/email/classes.mail-egress-coordinator.d.ts +37 -0
- package/dist_ts/email/classes.mail-egress-coordinator.js +113 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/config.handler.js +4 -2
- package/dist_ts/opsserver/handlers/email-domain.handler.js +3 -1
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +20 -3
- package/dist_ts/opsserver/handlers/email-ops.handler.js +141 -19
- package/dist_ts/opsserver/handlers/email-settings.handler.js +11 -6
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +16 -7
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +33 -1
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +94 -2
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +18 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +114 -12
- package/dist_ts/remoteingress/edge-derivation-routes.d.ts +10 -0
- package/dist_ts/remoteingress/edge-derivation-routes.js +28 -0
- package/dist_ts/remoteingress/index.d.ts +1 -0
- package/dist_ts/remoteingress/index.js +2 -1
- package/dist_ts_interfaces/data/email-domain.d.ts +11 -1
- package/dist_ts_interfaces/data/email-settings.d.ts +11 -0
- package/dist_ts_interfaces/data/remoteingress.d.ts +10 -0
- package/dist_ts_interfaces/data/remoteingress.js +5 -2
- package/dist_ts_interfaces/requests/email-domains.d.ts +5 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +4 -0
- package/dist_ts_interfaces/requests/remoteingress.d.ts +11 -0
- package/dist_ts_migrations/index.js +91 -9
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/package.json +4 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +41 -48
- package/ts/db/documents/classes.cached.email.ts +72 -1
- package/ts/db/documents/classes.email-domain.doc.ts +4 -0
- package/ts/db/documents/classes.email-server-settings.doc.ts +1 -1
- package/ts/db/documents/classes.remote-ingress-edge.doc.ts +9 -0
- package/ts/dns/classes.dns-server-runtime.ts +2 -0
- package/ts/email/classes.accepted-email-spool.ts +12 -1
- package/ts/email/classes.email-domain.manager.ts +118 -17
- package/ts/email/classes.email-route-builder.ts +4 -1
- package/ts/email/classes.email-settings.manager.ts +2 -2
- package/ts/email/classes.mail-dns-sync.ts +218 -0
- package/ts/email/classes.mail-egress-coordinator.ts +125 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/config.handler.ts +3 -1
- package/ts/opsserver/handlers/email-domain.handler.ts +2 -0
- package/ts/opsserver/handlers/email-ops.handler.ts +156 -18
- package/ts/opsserver/handlers/email-settings.handler.ts +10 -5
- package/ts/opsserver/handlers/remoteingress.handler.ts +15 -5
- package/ts/remoteingress/classes.remoteingress-manager.ts +111 -0
- package/ts/remoteingress/classes.tunnel-manager.ts +125 -10
- package/ts/remoteingress/edge-derivation-routes.ts +29 -0
- package/ts/remoteingress/index.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -73,6 +73,7 @@ export class EmailDomainHandler {
|
|
|
73
73
|
dkimKeySize: dataArg.dkimKeySize,
|
|
74
74
|
rotateKeys: dataArg.rotateKeys,
|
|
75
75
|
rotationIntervalDays: dataArg.rotationIntervalDays,
|
|
76
|
+
remoteIngress: dataArg.remoteIngress,
|
|
76
77
|
});
|
|
77
78
|
return { success: true, domain };
|
|
78
79
|
} catch (err: unknown) {
|
|
@@ -96,6 +97,7 @@ export class EmailDomainHandler {
|
|
|
96
97
|
rotateKeys: dataArg.rotateKeys,
|
|
97
98
|
rotationIntervalDays: dataArg.rotationIntervalDays,
|
|
98
99
|
rateLimits: dataArg.rateLimits,
|
|
100
|
+
remoteIngress: dataArg.remoteIngress,
|
|
99
101
|
});
|
|
100
102
|
return { success: true };
|
|
101
103
|
} catch (err: unknown) {
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|
|
2
2
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
4
|
import { requireOpsAuth } from '../helpers/auth.js';
|
|
5
|
+
import { CachedEmail } from '../../db/index.js';
|
|
5
6
|
|
|
6
7
|
export class EmailOpsHandler {
|
|
7
8
|
constructor(private opsServerRef: OpsServer) {
|
|
@@ -20,7 +21,7 @@ export class EmailOpsHandler {
|
|
|
20
21
|
'getAllEmails',
|
|
21
22
|
async (dataArg) => {
|
|
22
23
|
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'emails:read' });
|
|
23
|
-
const emails = this.
|
|
24
|
+
const emails = await this.getAllEmails(dataArg.direction, dataArg.limit);
|
|
24
25
|
return { emails };
|
|
25
26
|
}
|
|
26
27
|
)
|
|
@@ -32,7 +33,7 @@ export class EmailOpsHandler {
|
|
|
32
33
|
'getEmailDetail',
|
|
33
34
|
async (dataArg) => {
|
|
34
35
|
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'emails:read' });
|
|
35
|
-
const email = this.getEmailDetail(dataArg.emailId);
|
|
36
|
+
const email = await this.getEmailDetail(dataArg.emailId);
|
|
36
37
|
return { email };
|
|
37
38
|
}
|
|
38
39
|
)
|
|
@@ -85,36 +86,172 @@ export class EmailOpsHandler {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
/**
|
|
88
|
-
*
|
|
89
|
+
* List accepted emails (inbound + outbound) from the durable CachedEmail
|
|
90
|
+
* store, plus any live outbound queue items not yet represented there.
|
|
89
91
|
*/
|
|
90
|
-
private
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
private async getAllEmails(
|
|
93
|
+
direction?: interfaces.requests.TEmailDirection,
|
|
94
|
+
limitArg?: number,
|
|
95
|
+
): Promise<interfaces.requests.IEmail[]> {
|
|
96
|
+
const limit = Math.min(Math.max(Math.floor(limitArg ?? 200), 1), 1000);
|
|
97
|
+
const cachedDocs = this.isCachedEmailStoreAvailable()
|
|
98
|
+
? await CachedEmail.findRecent(limit, { direction })
|
|
99
|
+
: [];
|
|
100
|
+
const emails = cachedDocs.map((doc) => this.mapCachedEmailToEmail(doc));
|
|
101
|
+
const representedIds = new Set(cachedDocs.map((doc) => doc.id));
|
|
102
|
+
|
|
103
|
+
// Live delivery-queue items are always outbound; every spooled message
|
|
104
|
+
// carries the cache-id header, so dedupe against the cached listing.
|
|
105
|
+
if (direction !== 'inbound') {
|
|
106
|
+
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
107
|
+
const spool = this.opsServerRef.dcRouterRef.acceptedEmailSpool;
|
|
108
|
+
for (const item of emailServer?.getQueueItems() ?? []) {
|
|
109
|
+
const cachedId = spool.getCachedEmailIdFromQueueItem(item);
|
|
110
|
+
if (cachedId && representedIds.has(cachedId)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
emails.push(this.mapQueueItemToEmail(item));
|
|
114
|
+
}
|
|
94
115
|
}
|
|
95
|
-
const emails = emailServer.getQueueItems().map((item) => this.mapQueueItemToEmail(item));
|
|
96
116
|
|
|
97
|
-
// Sort by
|
|
117
|
+
// Sort by timestamp descending (newest first)
|
|
98
118
|
emails.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
99
119
|
|
|
100
|
-
return emails;
|
|
120
|
+
return emails.slice(0, limit);
|
|
101
121
|
}
|
|
102
122
|
|
|
103
123
|
/**
|
|
104
|
-
* Get a single email detail by ID
|
|
124
|
+
* Get a single email detail by ID — live queue item first (freshest state),
|
|
125
|
+
* else the durable CachedEmail row (covers inbound + historical mail).
|
|
105
126
|
*/
|
|
106
|
-
private getEmailDetail(emailId: string): interfaces.requests.IEmailDetail | null {
|
|
127
|
+
private async getEmailDetail(emailId: string): Promise<interfaces.requests.IEmailDetail | null> {
|
|
107
128
|
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
const item = emailServer?.getQueueItem(emailId);
|
|
130
|
+
if (item) {
|
|
131
|
+
return this.mapQueueItemToEmailDetail(item);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.isCachedEmailStoreAvailable()) {
|
|
135
|
+
const cachedEmail = await CachedEmail.findById(emailId);
|
|
136
|
+
if (cachedEmail) {
|
|
137
|
+
return this.mapCachedEmailToEmailDetail(cachedEmail);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Durable listing needs the DB; without it only the live queue is visible. */
|
|
144
|
+
private isCachedEmailStoreAvailable(): boolean {
|
|
145
|
+
return this.opsServerRef.dcRouterRef.dcRouterDb?.isReady() === true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Map a CachedEmail row to catalog IEmail format
|
|
150
|
+
*/
|
|
151
|
+
private mapCachedEmailToEmail(doc: CachedEmail): interfaces.requests.IEmail {
|
|
152
|
+
return {
|
|
153
|
+
id: doc.id,
|
|
154
|
+
direction: doc.direction,
|
|
155
|
+
status: this.mapStatus(doc.status),
|
|
156
|
+
from: doc.from || '',
|
|
157
|
+
to: doc.to?.[0] || '',
|
|
158
|
+
subject: doc.subject || '',
|
|
159
|
+
timestamp: new Date(doc.acceptedAt || 0).toISOString(),
|
|
160
|
+
messageId: doc.messageId || '',
|
|
161
|
+
size: this.formatSize(doc.rawContent?.length || 0),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Map a CachedEmail row to catalog IEmailDetail format
|
|
167
|
+
*/
|
|
168
|
+
private mapCachedEmailToEmailDetail(doc: CachedEmail): interfaces.requests.IEmailDetail {
|
|
169
|
+
const base = this.mapCachedEmailToEmail(doc);
|
|
170
|
+
const { headers, body } = this.parseRawEmail(doc.rawContent || '');
|
|
171
|
+
const session = this.parseCachedEmailSession(doc);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...base,
|
|
175
|
+
toList: doc.to || [],
|
|
176
|
+
cc: doc.cc || [],
|
|
177
|
+
smtpLog: [],
|
|
178
|
+
connectionInfo: {
|
|
179
|
+
sourceIp: session.remoteAddress || '',
|
|
180
|
+
sourceHostname: session.clientHostname || '',
|
|
181
|
+
destinationIp: '',
|
|
182
|
+
destinationPort: 0,
|
|
183
|
+
tlsVersion: session.secure ? 'TLS' : '',
|
|
184
|
+
tlsCipher: '',
|
|
185
|
+
authenticated: !!session.authenticated,
|
|
186
|
+
authMethod: '',
|
|
187
|
+
authUser: this.extractSessionUser(session),
|
|
188
|
+
},
|
|
189
|
+
authenticationResults: {
|
|
190
|
+
spf: 'none',
|
|
191
|
+
spfDomain: '',
|
|
192
|
+
dkim: 'none',
|
|
193
|
+
dkimDomain: '',
|
|
194
|
+
dmarc: 'none',
|
|
195
|
+
dmarcPolicy: '',
|
|
196
|
+
},
|
|
197
|
+
rejectionReason: doc.status === 'failed' ? doc.lastError : undefined,
|
|
198
|
+
bounceMessage: doc.status === 'failed' ? doc.lastError : undefined,
|
|
199
|
+
headers,
|
|
200
|
+
body,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Session snapshot persisted in routeData at acceptance time. */
|
|
205
|
+
private parseCachedEmailSession(doc: CachedEmail): {
|
|
206
|
+
remoteAddress?: string;
|
|
207
|
+
clientHostname?: string;
|
|
208
|
+
secure?: boolean;
|
|
209
|
+
authenticated?: boolean;
|
|
210
|
+
user?: unknown;
|
|
211
|
+
} {
|
|
212
|
+
try {
|
|
213
|
+
const routeData = doc.routeData ? JSON.parse(doc.routeData) : {};
|
|
214
|
+
return routeData.session || {};
|
|
215
|
+
} catch {
|
|
216
|
+
return {};
|
|
110
217
|
}
|
|
111
|
-
|
|
218
|
+
}
|
|
112
219
|
|
|
113
|
-
|
|
114
|
-
|
|
220
|
+
private extractSessionUser(session: { user?: unknown }): string {
|
|
221
|
+
if (!session.user) {
|
|
222
|
+
return '';
|
|
115
223
|
}
|
|
224
|
+
if (typeof session.user === 'string') {
|
|
225
|
+
return session.user;
|
|
226
|
+
}
|
|
227
|
+
const user = session.user as { username?: string; id?: string };
|
|
228
|
+
return user.username || user.id || '';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Split raw RFC822 content into an unfolded header map and the body. */
|
|
232
|
+
private parseRawEmail(rawContent: string): { headers: Record<string, string>; body: string } {
|
|
233
|
+
const separatorMatch = rawContent.match(/\r?\n\r?\n/);
|
|
234
|
+
const headerBlock = separatorMatch
|
|
235
|
+
? rawContent.slice(0, separatorMatch.index)
|
|
236
|
+
: rawContent;
|
|
237
|
+
const body = separatorMatch
|
|
238
|
+
? rawContent.slice((separatorMatch.index || 0) + separatorMatch[0].length)
|
|
239
|
+
: '';
|
|
116
240
|
|
|
117
|
-
|
|
241
|
+
const headers: Record<string, string> = {};
|
|
242
|
+
let currentKey: string | undefined;
|
|
243
|
+
for (const line of headerBlock.split(/\r?\n/)) {
|
|
244
|
+
if (/^[\t ]/.test(line) && currentKey) {
|
|
245
|
+
headers[currentKey] += ` ${line.trim()}`;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const idx = line.indexOf(':');
|
|
249
|
+
if (idx > 0) {
|
|
250
|
+
currentKey = line.slice(0, idx).trim();
|
|
251
|
+
headers[currentKey] = line.slice(idx + 1).trim();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { headers, body };
|
|
118
255
|
}
|
|
119
256
|
|
|
120
257
|
/**
|
|
@@ -243,6 +380,7 @@ export class EmailOpsHandler {
|
|
|
243
380
|
switch (queueStatus) {
|
|
244
381
|
case 'pending':
|
|
245
382
|
case 'processing':
|
|
383
|
+
case 'queued':
|
|
246
384
|
return 'pending';
|
|
247
385
|
case 'delivered':
|
|
248
386
|
return 'delivered';
|
|
@@ -49,16 +49,21 @@ export class EmailSettingsHandler {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
private getSettings(): interfaces.data.IEmailServerSettings {
|
|
52
|
-
const
|
|
52
|
+
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
53
|
+
const outboundEgress = dcRouter.getOutboundEgressStatus();
|
|
54
|
+
const manager = dcRouter.emailSettingsManager;
|
|
53
55
|
if (manager) {
|
|
54
|
-
return manager.getPublicSettings();
|
|
56
|
+
return { ...manager.getPublicSettings(), outboundEgress };
|
|
55
57
|
}
|
|
56
|
-
const emailConfig =
|
|
57
|
-
const emailPortConfig =
|
|
58
|
+
const emailConfig = dcRouter.options.emailConfig;
|
|
59
|
+
const emailPortConfig = dcRouter.options.emailPortConfig;
|
|
58
60
|
return {
|
|
59
61
|
enabled: Boolean(emailConfig),
|
|
60
62
|
hostname: emailConfig?.hostname || null,
|
|
61
|
-
|
|
63
|
+
// No settings manager yet: report the config seed; absent means the
|
|
64
|
+
// 'remoteIngress' default applies — never silently claim 'direct'.
|
|
65
|
+
outboundMode: dcRouter.options.emailOutboundMode || 'remoteIngress',
|
|
66
|
+
outboundEgress,
|
|
62
67
|
ports: [...(emailConfig?.ports || [])],
|
|
63
68
|
portMapping: emailPortConfig?.portMapping ? { ...emailPortConfig.portMapping } : null,
|
|
64
69
|
receivedEmailsPath: emailPortConfig?.receivedEmailsPath || null,
|
|
@@ -64,12 +64,18 @@ export class RemoteIngressHandler {
|
|
|
64
64
|
dataArg.autoDerivePorts ?? true,
|
|
65
65
|
dataArg.performance,
|
|
66
66
|
dataArg.egress,
|
|
67
|
+
{
|
|
68
|
+
publicIp: dataArg.publicIp,
|
|
69
|
+
publicIpV6: dataArg.publicIpV6,
|
|
70
|
+
mailHostname: dataArg.mailHostname,
|
|
71
|
+
},
|
|
67
72
|
));
|
|
68
73
|
return { success: true, edge };
|
|
69
74
|
} catch (err: unknown) {
|
|
70
75
|
return {
|
|
71
76
|
success: false,
|
|
72
77
|
edge: null as any,
|
|
78
|
+
message: (err as Error).message,
|
|
73
79
|
};
|
|
74
80
|
}
|
|
75
81
|
},
|
|
@@ -111,6 +117,7 @@ export class RemoteIngressHandler {
|
|
|
111
117
|
scope: 'remote-ingress:write',
|
|
112
118
|
requireAdminIdentity: true,
|
|
113
119
|
});
|
|
120
|
+
let failureMessage: string | undefined;
|
|
114
121
|
const result = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(async (manager) => {
|
|
115
122
|
const edge = await manager.updateEdge(dataArg.id, {
|
|
116
123
|
name: dataArg.name,
|
|
@@ -120,6 +127,9 @@ export class RemoteIngressHandler {
|
|
|
120
127
|
performance: dataArg.performance,
|
|
121
128
|
egress: dataArg.egress,
|
|
122
129
|
tags: dataArg.tags,
|
|
130
|
+
publicIp: dataArg.publicIp,
|
|
131
|
+
publicIpV6: dataArg.publicIpV6,
|
|
132
|
+
mailHostname: dataArg.mailHostname,
|
|
123
133
|
});
|
|
124
134
|
|
|
125
135
|
if (!edge) {
|
|
@@ -136,14 +146,14 @@ export class RemoteIngressHandler {
|
|
|
136
146
|
derivedPorts: breakdown.derived,
|
|
137
147
|
};
|
|
138
148
|
}).catch((err: unknown) => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
149
|
+
// Validation rejections (bad IP, hub-IP guard, bad FQDN) and a
|
|
150
|
+
// missing manager both surface as success:false with the reason.
|
|
151
|
+
failureMessage = (err as Error).message;
|
|
152
|
+
return null;
|
|
143
153
|
});
|
|
144
154
|
|
|
145
155
|
if (!result) {
|
|
146
|
-
return { success: false, edge: null as any };
|
|
156
|
+
return { success: false, edge: null as any, message: failureMessage };
|
|
147
157
|
}
|
|
148
158
|
return {
|
|
149
159
|
success: true,
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressEgressPolicyConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
|
|
3
|
+
import { REMOTE_INGRESS_MAIL_TAG } from '../../ts_interfaces/data/remoteingress.js';
|
|
3
4
|
import { RemoteIngressEdgeDoc, RemoteIngressHubSettingsDoc } from '../db/index.js';
|
|
4
5
|
|
|
6
|
+
/** Operator-declared host identity of an edge (public IPs + PTR-aligned mail hostname). */
|
|
7
|
+
export interface IRemoteIngressEdgeHostInfo {
|
|
8
|
+
publicIp?: string;
|
|
9
|
+
publicIpV6?: string;
|
|
10
|
+
mailHostname?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const fqdnRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i;
|
|
14
|
+
|
|
5
15
|
export interface IRemoteIngressFirewallConfig {
|
|
6
16
|
blockedIps?: string[];
|
|
7
17
|
}
|
|
@@ -51,6 +61,8 @@ export class RemoteIngressManager {
|
|
|
51
61
|
private edges: Map<string, IRemoteIngress> = new Map();
|
|
52
62
|
private routes: IDcRouterRouteConfig[] = [];
|
|
53
63
|
private firewallConfig?: IRemoteIngressFirewallConfig;
|
|
64
|
+
private hubPublicIps: string[] = [];
|
|
65
|
+
private onEdgesChanged?: (reason: string) => void;
|
|
54
66
|
private hubSettings: IRemoteIngressHubSettings = {
|
|
55
67
|
enabled: false,
|
|
56
68
|
tunnelPort: defaultTunnelPort,
|
|
@@ -60,6 +72,26 @@ export class RemoteIngressManager {
|
|
|
60
72
|
|
|
61
73
|
constructor() {}
|
|
62
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Set the hub's own public IPs. Edge publicIp values equal to any of these
|
|
77
|
+
* are rejected — the hub IP must never become an edge-fronted DNS target.
|
|
78
|
+
*/
|
|
79
|
+
public setHubPublicIps(ips: Array<string | undefined>): void {
|
|
80
|
+
this.hubPublicIps = ips.filter((ip): ip is string => Boolean(ip));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register a callback fired after every edge mutation (create/update/delete)
|
|
85
|
+
* so dependent state — mail DNS records — can reconcile.
|
|
86
|
+
*/
|
|
87
|
+
public setOnEdgesChanged(callback: (reason: string) => void): void {
|
|
88
|
+
this.onEdgesChanged = callback;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private notifyEdgesChanged(reason: string): void {
|
|
92
|
+
this.onEdgesChanged?.(reason);
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
/**
|
|
64
96
|
* Load all edge registrations from the database into memory.
|
|
65
97
|
*/
|
|
@@ -82,6 +114,9 @@ export class RemoteIngressManager {
|
|
|
82
114
|
performance: doc.performance,
|
|
83
115
|
egress: this.normalizeEgressPolicy(doc.egress),
|
|
84
116
|
tags: doc.tags,
|
|
117
|
+
publicIp: doc.publicIp,
|
|
118
|
+
publicIpV6: doc.publicIpV6,
|
|
119
|
+
mailHostname: doc.mailHostname,
|
|
85
120
|
createdAt: doc.createdAt,
|
|
86
121
|
updatedAt: doc.updatedAt,
|
|
87
122
|
};
|
|
@@ -277,6 +312,7 @@ export class RemoteIngressManager {
|
|
|
277
312
|
autoDerivePorts: boolean = true,
|
|
278
313
|
performance?: IRemoteIngressPerformanceConfig,
|
|
279
314
|
egress?: IRemoteIngressEgressPolicyConfig,
|
|
315
|
+
hostInfo?: IRemoteIngressEdgeHostInfo,
|
|
280
316
|
): Promise<IRemoteIngress> {
|
|
281
317
|
const id = plugins.uuid.v4();
|
|
282
318
|
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
|
@@ -292,6 +328,9 @@ export class RemoteIngressManager {
|
|
|
292
328
|
performance,
|
|
293
329
|
egress: this.normalizeEgressPolicy(egress),
|
|
294
330
|
tags: tags || [],
|
|
331
|
+
publicIp: this.normalizeEdgePublicIp(hostInfo?.publicIp, 4),
|
|
332
|
+
publicIpV6: this.normalizeEdgePublicIp(hostInfo?.publicIpV6, 6),
|
|
333
|
+
mailHostname: this.normalizeMailHostname(hostInfo?.mailHostname),
|
|
295
334
|
createdAt: now,
|
|
296
335
|
updatedAt: now,
|
|
297
336
|
};
|
|
@@ -300,6 +339,7 @@ export class RemoteIngressManager {
|
|
|
300
339
|
Object.assign(doc, edge);
|
|
301
340
|
await doc.save();
|
|
302
341
|
this.edges.set(id, edge);
|
|
342
|
+
this.notifyEdgesChanged(`edge created: ${name}`);
|
|
303
343
|
return edge;
|
|
304
344
|
}
|
|
305
345
|
|
|
@@ -330,6 +370,10 @@ export class RemoteIngressManager {
|
|
|
330
370
|
performance?: IRemoteIngressPerformanceConfig;
|
|
331
371
|
egress?: IRemoteIngressEgressPolicyConfig;
|
|
332
372
|
tags?: string[];
|
|
373
|
+
/** null clears the stored value. */
|
|
374
|
+
publicIp?: string | null;
|
|
375
|
+
publicIpV6?: string | null;
|
|
376
|
+
mailHostname?: string | null;
|
|
333
377
|
},
|
|
334
378
|
): Promise<IRemoteIngress | null> {
|
|
335
379
|
const edge = this.edges.get(id);
|
|
@@ -344,6 +388,15 @@ export class RemoteIngressManager {
|
|
|
344
388
|
if (updates.performance !== undefined) edge.performance = updates.performance;
|
|
345
389
|
if (updates.egress !== undefined) edge.egress = this.normalizeEgressPolicy(updates.egress);
|
|
346
390
|
if (updates.tags !== undefined) edge.tags = updates.tags;
|
|
391
|
+
if (updates.publicIp !== undefined) {
|
|
392
|
+
edge.publicIp = updates.publicIp === null ? undefined : this.normalizeEdgePublicIp(updates.publicIp, 4);
|
|
393
|
+
}
|
|
394
|
+
if (updates.publicIpV6 !== undefined) {
|
|
395
|
+
edge.publicIpV6 = updates.publicIpV6 === null ? undefined : this.normalizeEdgePublicIp(updates.publicIpV6, 6);
|
|
396
|
+
}
|
|
397
|
+
if (updates.mailHostname !== undefined) {
|
|
398
|
+
edge.mailHostname = updates.mailHostname === null ? undefined : this.normalizeMailHostname(updates.mailHostname);
|
|
399
|
+
}
|
|
347
400
|
edge.updatedAt = Date.now();
|
|
348
401
|
|
|
349
402
|
const doc = await RemoteIngressEdgeDoc.findById(id);
|
|
@@ -352,6 +405,7 @@ export class RemoteIngressManager {
|
|
|
352
405
|
await doc.save();
|
|
353
406
|
}
|
|
354
407
|
this.edges.set(id, edge);
|
|
408
|
+
this.notifyEdgesChanged(`edge updated: ${edge.name}`);
|
|
355
409
|
return edge;
|
|
356
410
|
}
|
|
357
411
|
|
|
@@ -367,6 +421,7 @@ export class RemoteIngressManager {
|
|
|
367
421
|
await doc.delete();
|
|
368
422
|
}
|
|
369
423
|
this.edges.delete(id);
|
|
424
|
+
this.notifyEdgesChanged(`edge deleted: ${id}`);
|
|
370
425
|
return true;
|
|
371
426
|
}
|
|
372
427
|
|
|
@@ -432,6 +487,62 @@ export class RemoteIngressManager {
|
|
|
432
487
|
return result;
|
|
433
488
|
}
|
|
434
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Resolve enabled edges whose id or tags match any entry in the filter
|
|
492
|
+
* (same semantics as route-level edgeFilter in derivePortsForEdge).
|
|
493
|
+
*/
|
|
494
|
+
public resolveEdgesByFilter(filter: string[]): IRemoteIngress[] {
|
|
495
|
+
if (!Array.isArray(filter) || filter.length === 0) {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
const result: IRemoteIngress[] = [];
|
|
499
|
+
for (const edge of this.edges.values()) {
|
|
500
|
+
if (!edge.enabled) continue;
|
|
501
|
+
const idMatch = filter.includes(edge.id);
|
|
502
|
+
const tagMatch = edge.tags?.some((tag) => filter.includes(tag)) ?? false;
|
|
503
|
+
if (idMatch || tagMatch) {
|
|
504
|
+
result.push(edge);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Enabled edges carrying the well-known 'mail' tag. */
|
|
511
|
+
public getMailEdges(): IRemoteIngress[] {
|
|
512
|
+
return this.resolveEdgesByFilter([REMOTE_INGRESS_MAIL_TAG]);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private normalizeEdgePublicIp(value: string | undefined, family: 4 | 6): string | undefined {
|
|
516
|
+
if (value === undefined) {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
const ip = `${value}`.trim();
|
|
520
|
+
if (!ip) {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
if (plugins.smartnetwork.getIpVersion(ip) !== family) {
|
|
524
|
+
throw new Error(`Edge publicIp${family === 6 ? 'V6' : ''} must be a valid IPv${family} address`);
|
|
525
|
+
}
|
|
526
|
+
if (this.hubPublicIps.includes(ip)) {
|
|
527
|
+
throw new Error(`Edge public IP ${ip} equals the hub public IP — the hub IP must never front edge traffic`);
|
|
528
|
+
}
|
|
529
|
+
return ip;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private normalizeMailHostname(value: string | undefined): string | undefined {
|
|
533
|
+
if (value === undefined) {
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
const hostname = `${value}`.trim().toLowerCase().replace(/\.$/, '');
|
|
537
|
+
if (!hostname) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
if (hostname.length > 253 || !fqdnRegex.test(hostname)) {
|
|
541
|
+
throw new Error('Edge mailHostname must be a fully qualified domain name');
|
|
542
|
+
}
|
|
543
|
+
return hostname;
|
|
544
|
+
}
|
|
545
|
+
|
|
435
546
|
private normalizeEgressPolicy(
|
|
436
547
|
egress?: IRemoteIngressEgressPolicyConfig,
|
|
437
548
|
): IRemoteIngressEgressPolicyConfig | undefined {
|