@serve.zone/interfaces 32.30.0 → 32.32.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.
@@ -61,15 +61,37 @@ export interface IRuntimeNetworkWorkloadLease extends IRuntimeNetworkNodeScope {
61
61
  }
62
62
 
63
63
  /**
64
- * One node-level published port for a workload. Mirrors `IAppStorePublishedPort`
65
- * without range fields: the runtime publishes exactly one host port per entry.
64
+ * One node-level published port, or one contiguous range of them, for a workload.
65
+ *
66
+ * Without `hostPortEnd` the entry publishes exactly `hostPort` to `targetPort`. With it the entry
67
+ * publishes every port from `hostPort` to `hostPortEnd` inclusive, each to the same port inside the
68
+ * workload, so `targetPort` equals `hostPort`. Every port an entry covers is claimed on its node:
69
+ * two entries of one node and protocol never share a port, and the entries of one endpoint never
70
+ * overlap and cover at most `runtimeNetworkPublishedPortContract.maximumPortsPerService` ports.
66
71
  */
67
72
  export interface IRuntimeNetworkPublishedPort {
68
73
  protocol: 'tcp' | 'udp';
69
- /** 1..65535 and unique per node across the projection for its protocol. */
74
+ /**
75
+ * 1..65535; the first port of a range. Cloudly publishes only ports inside
76
+ * `runtimeNetworkPublishedPortContract.publishablePorts` (`published-port-reserved`).
77
+ */
70
78
  hostPort: number;
71
- /** The workload's own listening port inside its network namespace. */
79
+ /**
80
+ * The last port of a range, above `hostPort` and at most 65535. Omitted, the entry is one port;
81
+ * a one-port range is never written with it.
82
+ */
83
+ hostPortEnd?: number;
84
+ /** The workload's own listening port inside its network namespace; `hostPort` for a range. */
72
85
  targetPort: number;
86
+ /**
87
+ * Outbound flows the workload originates from a published target port leave the node from the
88
+ * matching host port, so a peer sees one address and port in both directions (SIP, RTP). Only
89
+ * `true` is ever stated; omitted, outbound flows keep the handoff lease's translation. It grants
90
+ * no egress: an endpoint carrying a symmetric entry has `publicEgress: true`. Its target ports
91
+ * are its own: no other entry of the list publishes to any of them
92
+ * (`findRuntimeNetworkPublishedPortInsideOverlap`).
93
+ */
94
+ symmetric?: true;
73
95
  /**
74
96
  * Never the wildcard or loopback. Omitted means the node's uplink address.
75
97
  * The contract cannot prove an uplink fact, so binding the port to that exact
@@ -84,15 +106,22 @@ export interface IRuntimeNetworkPublishedPort {
84
106
  }
85
107
 
86
108
  /**
87
- * Caller-authored published-port request. It carries no `hostIp` (an omitted
88
- * address means the node uplink) and no ranges: exactly one host port per entry.
89
- * The `authorization` reference of the resulting projection entry is derived by
90
- * Cloudly from the policy revision and digest, never supplied by the caller.
109
+ * Caller-authored published-port request. It carries no `hostIp` (an omitted address means the
110
+ * node uplink). `hostPortEnd` and `symmetric` follow the rules of `IRuntimeNetworkPublishedPort`.
111
+ * The `authorization` reference of the resulting projection entry is derived by Cloudly from the
112
+ * policy revision and digest, never supplied by the caller.
91
113
  */
92
114
  export interface IRuntimeNetworkPublishedPortRequest {
93
115
  protocol: 'tcp' | 'udp';
94
116
  hostPort: number;
117
+ /** The last port of a range; `targetPort` then equals `hostPort`. */
118
+ hostPortEnd?: number;
95
119
  targetPort: number;
120
+ /**
121
+ * Same-port outbound; valid only while the service's policy document grants public egress, and
122
+ * only on target ports no other entry of the list publishes to.
123
+ */
124
+ symmetric?: true;
96
125
  }
97
126
 
98
127
  /**
@@ -105,13 +134,23 @@ export interface IServicePublishedPorts {
105
134
  organizationId: string;
106
135
  serviceId: string;
107
136
  revision: number;
108
- /** Sorted by protocol and host port. Empty means the service publishes nothing. */
137
+ /**
138
+ * Sorted by protocol and host port, never overlapping, at most
139
+ * `runtimeNetworkPublishedPortContract.maximumEntries` entries covering at most its
140
+ * `maximumPortsPerService` ports, and no symmetric entry sharing a target port with another
141
+ * entry of its protocol. Empty means the service publishes nothing.
142
+ */
109
143
  publishedPorts: IRuntimeNetworkPublishedPortRequest[];
110
144
  }
111
145
 
112
146
  /**
113
147
  * Exact desired policy. Node-level host-port placement remains Cloudly's decision.
114
148
  *
149
+ * Beyond the shape its snapshot checks, Cloudly refuses a port outside
150
+ * `runtimeNetworkPublishedPortContract.publishablePorts` and a symmetric entry while the document's
151
+ * `publicEgress` is false; `setServiceNetworkEgress` likewise refuses clearing `publicEgress` while a
152
+ * symmetric entry remains.
153
+ *
115
154
  * One document per service holds its published ports and its egress under one `revision`. The
116
155
  * first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
117
156
  * with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
@@ -160,7 +199,8 @@ export interface IServiceNetworkEgress {
160
199
 
161
200
  /**
162
201
  * Exact desired egress. The published ports of the same document are carried forward unchanged.
163
- * Whether each id names a current platform endpoint is Cloudly's decision at acceptance.
202
+ * Whether each id names a current platform endpoint is Cloudly's decision at acceptance, and so is
203
+ * the refusal of `publicEgress: false` while a carried published port is `symmetric`.
164
204
  *
165
205
  * One document per service holds its published ports and its egress under one `revision`. The
166
206
  * first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
@@ -198,7 +238,12 @@ export interface IRuntimeNetworkEndpoint {
198
238
  /** TCP/UDP only; the complete protected union remains denied. */
199
239
  publicEgress: boolean;
200
240
  platformEndpointIds: string[];
201
- /** Sorted by protocol and host port. Absent publishes nothing; a present member is never empty. */
241
+ /**
242
+ * Sorted by protocol and host port and never overlapping, within the bounds of
243
+ * `runtimeNetworkPublishedPortContract`. Absent publishes nothing; a present member is never
244
+ * empty. A `symmetric` entry is admitted only while `publicEgress` is true and only on target
245
+ * ports no other entry of the endpoint publishes to.
246
+ */
202
247
  publishedPorts?: IRuntimeNetworkPublishedPort[];
203
248
  }
204
249
 
@@ -286,6 +331,23 @@ export const runtimeNetworkProjectionContract = Object.freeze({
286
331
  allocationReuse: 'requires-controller-quarantine-and-all-affected-authority-fences',
287
332
  } as const);
288
333
 
334
+ /**
335
+ * The bounds of one service's published ports, the same on its policy document and on each of its
336
+ * projection endpoints.
337
+ *
338
+ * `maximumEntries` bounds the list and `maximumPortsPerService` the ports its entries cover
339
+ * together, a range counting every port in it; the snapshots refuse either excess.
340
+ * `publishablePorts` is Cloudly's placement rule, not a snapshot rule: the ports above it are
341
+ * `runtimeNetworkHandoffSnatPortRange`, which a handoff lease translates outbound flows to, so
342
+ * Cloudly refuses a policy with a port outside it as `published-port-reserved`
343
+ * (`isRuntimeNetworkPublishedPortReserved`). A policy written before this rule stays readable.
344
+ */
345
+ export const runtimeNetworkPublishedPortContract = Object.freeze({
346
+ maximumEntries: 64,
347
+ maximumPortsPerService: 1024,
348
+ publishablePorts: Object.freeze({ first: 1, last: 49_151 } as const),
349
+ } as const);
350
+
289
351
  /** The assignment contract owns the replica rule; a lease names the same slot by the same name. */
290
352
  const replicaId = (value: unknown): string => {
291
353
  requireNetwork(isRuntimeReplicaId(value)); return value as string;
@@ -346,28 +408,95 @@ const packetGrant = (value: unknown): IRuntimeNetworkPacketGrant => {
346
408
  return { source: networkReference(item.source), destination: { kind, id: networkId(destination.id), address: networkIpv4(destination.address),
347
409
  protocol: destination.protocol, port: networkCounter(destination.port, 65535) } };
348
410
  };
349
- const publishedPortKey = (value: { protocol: 'tcp' | 'udp'; hostPort: number }): string =>
411
+ type TPublishedPortSpan = Pick<IRuntimeNetworkPublishedPortRequest, 'protocol' | 'hostPort' | 'hostPortEnd'>;
412
+ const publishedPortKey = (value: TPublishedPortSpan): string =>
350
413
  `${value.protocol}/${String(value.hostPort).padStart(5, '0')}`;
351
- const publishedPort = (value: unknown): IRuntimeNetworkPublishedPort => {
352
- const hasHostIp = value !== null && typeof value === 'object' && Object.hasOwn(value, 'hostIp');
353
- const item = networkRecord(value, ['protocol', 'hostPort', 'targetPort', 'authorization', ...(hasHostIp ? ['hostIp'] : [])]);
414
+ const publishedPortLast = (value: TPublishedPortSpan): number => value.hostPortEnd ?? value.hostPort;
415
+
416
+ /**
417
+ * Whether any port an entry covers lies outside `runtimeNetworkPublishedPortContract.publishablePorts`:
418
+ * Cloudly's `published-port-reserved`. It judges an entry its snapshot already accepted.
419
+ */
420
+ export const isRuntimeNetworkPublishedPortReserved = (value: TPublishedPortSpan): boolean =>
421
+ value.hostPort < runtimeNetworkPublishedPortContract.publishablePorts.first
422
+ || publishedPortLast(value) > runtimeNetworkPublishedPortContract.publishablePorts.last;
423
+
424
+ type TPublishedPortInside = Pick<IRuntimeNetworkPublishedPortRequest, 'protocol' | 'hostPort' | 'hostPortEnd' | 'targetPort' | 'symmetric'>;
425
+ /** The last workload port an entry publishes to; its inside span starts at `targetPort`. */
426
+ const publishedPortInsideLast = (value: TPublishedPortInside): number =>
427
+ value.targetPort + publishedPortLast(value) - value.hostPort;
428
+
429
+ /**
430
+ * The first pair of one list in which a `symmetric` entry's inside ports (`targetPort` onwards, a
431
+ * range's every port) share a port with another entry of the same protocol, or `null`: Cloudly's
432
+ * `published-port-symmetric-inside-overlap`.
433
+ *
434
+ * A symmetric entry translates the flows its workload opens from those ports back to its own host
435
+ * ports, so no other entry of the workload may publish to any of them, symmetric or not: the flows
436
+ * would have two published sources. The lists of one service's policy document and of each of its
437
+ * projection endpoints are judged alike, and their snapshots refuse any pair. Distinct protocols
438
+ * never conflict. It judges entries their snapshot already accepted.
439
+ */
440
+ export const findRuntimeNetworkPublishedPortInsideOverlap = <T extends TPublishedPortInside>(
441
+ ports: readonly T[],
442
+ ): { symmetric: T; other: T } | null => {
443
+ for (let index = 0; index < ports.length; index++) {
444
+ const symmetric = ports[index];
445
+ if (symmetric.symmetric !== true) continue;
446
+ const first = symmetric.targetPort, last = publishedPortInsideLast(symmetric);
447
+ for (let other = 0; other < ports.length; other++) {
448
+ const candidate = ports[other];
449
+ if (other !== index && candidate.protocol === symmetric.protocol
450
+ && candidate.targetPort <= last && first <= publishedPortInsideLast(candidate)) return { symmetric, other: candidate };
451
+ }
452
+ }
453
+ return null;
454
+ };
455
+
456
+ /** The members every published-port entry shares; an optional member is stated only when present. */
457
+ const publishedPortSpan = (item: Record<string, unknown>): IRuntimeNetworkPublishedPortRequest => {
354
458
  requireNetwork(item.protocol === 'tcp' || item.protocol === 'udp');
355
- const hostIp = hasHostIp ? networkIpv4(item.hostIp) : null;
459
+ const hostPort = networkCounter(item.hostPort, 65535), targetPort = networkCounter(item.targetPort, 65535);
460
+ const hasEnd = Object.hasOwn(item, 'hostPortEnd'), hasSymmetric = Object.hasOwn(item, 'symmetric');
461
+ const hostPortEnd = hasEnd ? networkCounter(item.hostPortEnd, 65535) : hostPort;
462
+ // A range maps each host port to the same workload port; a one-port range has one encoding, none.
463
+ requireNetwork(!hasEnd || (hostPortEnd > hostPort && targetPort === hostPort));
464
+ requireNetwork(!hasSymmetric || item.symmetric === true);
465
+ return { protocol: item.protocol, hostPort, ...(hasEnd ? { hostPortEnd } : {}), targetPort,
466
+ ...(hasSymmetric ? { symmetric: true as const } : {}) };
467
+ };
468
+ /**
469
+ * Sorted, never overlapping within a protocol, bounded in entries and in covered ports, and no
470
+ * symmetric entry sharing an inside port with another entry.
471
+ */
472
+ const publishedPortList = <T extends TPublishedPortInside>(value: unknown, read: (item: unknown) => T): T[] => {
473
+ const ports = networkSorted(networkList(value, runtimeNetworkPublishedPortContract.maximumEntries, read), publishedPortKey);
474
+ let covered = 0;
475
+ ports.forEach((port, index) => {
476
+ const previous = index === 0 ? null : ports[index - 1];
477
+ requireNetwork(previous === null || previous.protocol !== port.protocol || publishedPortLast(previous) < port.hostPort);
478
+ covered += publishedPortLast(port) - port.hostPort + 1;
479
+ });
480
+ requireNetwork(covered <= runtimeNetworkPublishedPortContract.maximumPortsPerService);
481
+ requireNetwork(findRuntimeNetworkPublishedPortInsideOverlap(ports) === null);
482
+ return ports;
483
+ };
484
+ const publishedPort = (value: unknown): IRuntimeNetworkPublishedPort => {
485
+ const item = networkRecord(value, ['protocol', 'hostPort', 'targetPort', 'authorization'], ['hostIp', 'hostPortEnd', 'symmetric']);
486
+ const { symmetric, ...span } = publishedPortSpan(item);
487
+ const hostIp = Object.hasOwn(item, 'hostIp') ? networkIpv4(item.hostIp) : null;
356
488
  requireNetwork(hostIp === null || (hostIp !== '0.0.0.0' && !networkContains('127.0.0.0/8', `${hostIp}/32`)));
357
- return { protocol: item.protocol, hostPort: networkCounter(item.hostPort, 65535), targetPort: networkCounter(item.targetPort, 65535),
358
- ...(hostIp === null ? {} : { hostIp }), authorization: networkReference(item.authorization) };
489
+ return { ...span, ...(hostIp === null ? {} : { hostIp }), ...(symmetric ? { symmetric } : {}),
490
+ authorization: networkReference(item.authorization) };
359
491
  };
360
492
  const publishedPorts = (value: unknown): IRuntimeNetworkPublishedPort[] => {
361
- const ports = networkSorted(networkList(value, 64, publishedPort), publishedPortKey);
493
+ const ports = publishedPortList(value, publishedPort);
362
494
  requireNetwork(ports.length > 0); return ports;
363
495
  };
364
- const publishedPortRequest = (value: unknown): IRuntimeNetworkPublishedPortRequest => {
365
- const item = networkRecord(value, ['protocol', 'hostPort', 'targetPort']);
366
- requireNetwork(item.protocol === 'tcp' || item.protocol === 'udp');
367
- return { protocol: item.protocol, hostPort: networkCounter(item.hostPort, 65535), targetPort: networkCounter(item.targetPort, 65535) };
368
- };
496
+ const publishedPortRequest = (value: unknown): IRuntimeNetworkPublishedPortRequest =>
497
+ publishedPortSpan(networkRecord(value, ['protocol', 'hostPort', 'targetPort'], ['hostPortEnd', 'symmetric']));
369
498
  const publishedPortRequests = (value: unknown): IRuntimeNetworkPublishedPortRequest[] =>
370
- networkSorted(networkList(value, 64, publishedPortRequest), publishedPortKey);
499
+ publishedPortList(value, publishedPortRequest);
371
500
 
372
501
  /** Structural snapshot only; node placement, quota and authorization remain Cloudly's decision. */
373
502
  export const snapshotServicePublishedPorts = (value: unknown): IServicePublishedPorts => networkSafe(() => {
@@ -420,9 +549,12 @@ const endpoint = (value: unknown): IRuntimeNetworkEndpoint => {
420
549
  reporterSessionId: networkId(observed.reporterSessionId), sequence: networkCounter(observed.sequence), digest: networkDigest(observed.digest) };
421
550
  }
422
551
  requireNetwork(!item.ready || (observation !== null && observation.assignment.generation === 1));
552
+ const ports = hasPublishedPorts ? publishedPorts(item.publishedPorts) : null;
553
+ // Same-port outbound rides the public egress grant; it never grants egress of its own.
554
+ requireNetwork(item.publicEgress || !(ports ?? []).some(port => port.symmetric));
423
555
  return { lease, ready: item.ready, observation, publicEgress: item.publicEgress,
424
556
  platformEndpointIds: networkSorted(networkList(item.platformEndpointIds, 64, networkId), value => value),
425
- ...(hasPublishedPorts ? { publishedPorts: publishedPorts(item.publishedPorts) } : {}) };
557
+ ...(ports === null ? {} : { publishedPorts: ports }) };
426
558
  };
427
559
 
428
560
  /** Structural snapshot only; use validate/signature/admission helpers before accepting authority. */
@@ -527,7 +659,8 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
527
659
  requireNetwork(!aliases.has(name)); aliases.add(name);
528
660
  }
529
661
  }
530
- const assignments = new Set<string>(), attempts = new Set<string>(), readyReplicas = new Set<string>(), hostPorts = new Set<string>();
662
+ const assignments = new Set<string>(), attempts = new Set<string>(), readyReplicas = new Set<string>();
663
+ const hostPortClaims: Array<{ node: string; first: number; last: number }> = [];
531
664
  for (const endpoint of projection.endpoints) {
532
665
  const lease = endpoint.lease, authority = allocationAuthority(lease.protectedAuthority);
533
666
  // A controller on the node itself has no other node: every endpoint it projects is local.
@@ -546,12 +679,16 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
546
679
  pool.id === lease.ipv4.poolId && pool.purpose === 'workload' && networkContains(pool.prefix, lease.ipv4.linkSubnet)));
547
680
  if (networkSameScope(lease, projection)) requireNetwork(projection.handoff !== null && lease.routerIncarnationId === projection.handoff.routerIncarnationId);
548
681
  for (const id of endpoint.platformEndpointIds) requireNetwork(current.platformEndpoints.some(platform => platform.id === id));
549
- for (const port of endpoint.publishedPorts ?? []) {
550
- const claim = networkCanonical([lease.nodeId, port.protocol, port.hostPort]);
551
- requireNetwork(!hostPorts.has(claim)); hostPorts.add(claim);
552
- }
682
+ for (const port of endpoint.publishedPorts ?? []) hostPortClaims.push({ node: networkCanonical([lease.nodeId, port.protocol]),
683
+ first: port.hostPort, last: publishedPortLast(port) });
553
684
  requireNetwork(!projection.retiredLeases.some(retired => networkEqual(retired, networkReferenceOf(lease))));
554
685
  }
686
+ // Every port of every entry is claimed on its node: ordered by start, no claim reaches the next one.
687
+ hostPortClaims.sort((left, right) => left.node < right.node ? -1 : left.node > right.node ? 1 : left.first - right.first);
688
+ for (let index = 1; index < hostPortClaims.length; index++) {
689
+ const previous = hostPortClaims[index - 1], current = hostPortClaims[index];
690
+ requireNetwork(previous.node !== current.node || previous.last < current.first);
691
+ }
555
692
  for (const id of projection.hostPlatformEndpointIds ?? []) requireNetwork(current.platformEndpoints.some(platform => platform.id === id));
556
693
  for (let index = 0; index < projection.endpoints.length; index++) for (let previous = 0; previous < index; previous++) {
557
694
  requireNetwork(!networkOverlaps(projection.endpoints[index].lease.ipv4.linkSubnet, projection.endpoints[previous].lease.ipv4.linkSubnet));
@@ -91,6 +91,7 @@ const serviceRuntimeRefusals = [
91
91
  'volume-placement-unsupported', 'volume-initialization-unknown', 'volume-ownership-unknown',
92
92
  'objectstorage-endpoint-missing', 'objectstorage-binding-invalid', 'objectstorage-environment-conflict',
93
93
  'objectstorage-endpoint-unselected', 'database-endpoint-missing', 'database-endpoint-unselected',
94
+ 'published-port-reserved', 'published-port-symmetric-without-egress',
94
95
  ] as const;
95
96
 
96
97
  /**
@@ -149,6 +150,14 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
149
150
  * or an IPv6 literal is never selected. Each is judged only for an endpoint that is valid: a node
150
151
  * stating none, or an invalid one, stays `database-endpoint-missing` or `objectstorage-endpoint-missing`.
151
152
  * `platform.findUnselectedCorestoreEndpoints` is this rule; a producer answers what it returns.
153
+ *
154
+ * The two `published-port-` refusals name a service policy document whose published ports no
155
+ * projection may carry, so its endpoints are not compiled rather than published on some ports only.
156
+ * `published-port-reserved` is an entry covering a port outside
157
+ * `runtimeNetworkPublishedPortContract.publishablePorts` (`isRuntimeNetworkPublishedPortReserved`),
158
+ * which the handoff leases' `runtimeNetworkHandoffSnatPortRange` owns; a document written before
159
+ * that rule can hold one. `published-port-symmetric-without-egress` is a `symmetric` entry in a
160
+ * document whose `publicEgress` is false, which a projection endpoint refuses.
152
161
  */
153
162
  export type TServiceRuntimeRefusal = typeof serviceRuntimeRefusals[number];
154
163