@serve.zone/interfaces 32.55.0 → 32.56.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.
@@ -2,7 +2,11 @@ import { isPlausibleUnicastIpv4 } from '../private/ipv4.js';
2
2
  import { isUtcCalendarDay } from '../private/utccalendarday.js';
3
3
  import { validateProtocolOfferOn } from '../protocol.js';
4
4
  import { hasExactKeys, readDataElements, readExactDataRecord } from '../private/dataproperties.js';
5
- import { parseCanonicalIpPrefix } from '../private/ipaddress.js';
5
+ import {
6
+ formatCanonicalIpPrefix,
7
+ ipv4PrefixOfMapped,
8
+ parseCanonicalIpPrefix,
9
+ } from '../private/ipaddress.js';
6
10
  import { isClusterNodeId } from '../private/nodeidentifier.js';
7
11
  import { validateClusterRelay } from './cluster.js';
8
12
  import type { TServiceRuntimeRefusal } from './serviceruntime.js';
@@ -82,7 +86,11 @@ export interface IClusterHttpRoute {
82
86
  * family (`192.0.2.10/32`, `2001:db8::/64`); a request from any other source is refused.
83
87
  * Absent means every source. When present it lists 1 to
84
88
  * `clusterIngressLimits.maximumSourceAllowListEntries` distinct prefixes, none of length zero:
85
- * an unrestricted route omits the member rather than spelling "everyone" a second way.
89
+ * an unrestricted route omits the member rather than spelling "everyone" a second way. No prefix
90
+ * lies inside the IPv4-mapped range `::ffff:0:0/96`: the ingress compares an IPv4-mapped client
91
+ * as IPv4, so such a prefix could never match and the ingress refuses the whole table carrying
92
+ * it; an IPv4 source is written as IPv4 (`192.0.2.0/24`, not `::ffff:c000:200/120`). A shorter
93
+ * IPv6 prefix that merely contains that range is accepted and admits only IPv6 sources.
86
94
  *
87
95
  * Cloudly sets it on a hostname that is not public, to the addresses the gateway forwards from,
88
96
  * so the gateway that checked the source stays the only way in. An ingress that applies it
@@ -236,7 +244,11 @@ const validateAuthentication = (valueArg: unknown, pathArg: string): string[] =>
236
244
  /**
237
245
  * Validates one source allowlist (`IClusterHttpRoute.sourceAllowList`) and names every reason it is
238
246
  * refused: 1 to `clusterIngressLimits.maximumSourceAllowListEntries` distinct canonical address
239
- * prefixes whose host bits are zero, none of length zero.
247
+ * prefixes whose host bits are zero, none of length zero and none inside the IPv4-mapped range
248
+ * `::ffff:0:0/96` (prefix length 96 or more with the `::ffff` head). The ingress compares an
249
+ * IPv4-mapped client as IPv4, so a mapped entry could never match and the ingress refuses the whole
250
+ * table that carries one; the reason names the IPv4 prefix to write instead. A shorter IPv6 prefix
251
+ * that merely contains the mapped range still matches the IPv6 sources it names and is accepted.
240
252
  */
241
253
  export const validateClusterRouteSourceAllowList = (
242
254
  valueArg: unknown,
@@ -250,11 +262,19 @@ export const validateClusterRouteSourceAllowList = (
250
262
  const errors: string[] = [];
251
263
  prefixes.forEach((prefixArg, indexArg) => {
252
264
  const prefix = parseCanonicalIpPrefix(prefixArg);
265
+ const mappedIpv4 = prefix === null ? null : ipv4PrefixOfMapped(prefix);
253
266
  if (prefix === null) {
254
267
  errors.push(`${pathArg}[${indexArg}] must be a canonical address prefix whose host bits are zero`);
255
268
  } else if (prefix.length === 0) {
256
269
  errors.push(`${pathArg}[${indexArg}] must not admit a whole address family; `
257
270
  + 'an unrestricted route omits sourceAllowList');
271
+ } else if (mappedIpv4 !== null) {
272
+ // Its IPv4 form of `::ffff:0:0/96` would be `0.0.0.0/0`, itself refused above.
273
+ errors.push(`${pathArg}[${indexArg}] must not be IPv4-mapped: an IPv4-mapped prefix never matches, `
274
+ + 'because the ingress compares clients as IPv4; '
275
+ + (mappedIpv4.length === 0
276
+ ? 'its IPv4 form admits a whole address family, and an unrestricted route omits sourceAllowList'
277
+ : `write ${formatCanonicalIpPrefix(mappedIpv4)}`));
258
278
  } else if (prefixes.indexOf(prefixArg) !== indexArg) {
259
279
  errors.push(`${pathArg}[${indexArg}] repeats ${prefixArg}`);
260
280
  }
@@ -117,9 +117,14 @@ export type TDeploymentPreflightWarningCode =
117
117
  * `RECOVERY_REDEPLOY_OVER_FAILED_RUNTIME` documents that an existing-service
118
118
  * reserve preflight suppressed the CURRENT_RUNTIME_DIGEST_MISSING and
119
119
  * RUNTIME_ATTESTATION_MISMATCH blockers because the current runtime provides
120
- * no consistent attested digest and the service's most recent deployment
121
- * operation ended 'failed' or 'cleaned' (a recovery redeploy over a known-bad
122
- * runtime).
120
+ * no consistent attested digest and either
121
+ * - the service's most recent deployment operation ended 'failed' or
122
+ * 'cleaned' (a recovery redeploy over a known-bad runtime), or
123
+ * - the retained deployment plan breaks the plan contract, so it can never be
124
+ * executed again (for example a Swarm-era plan without
125
+ * `containerInvocation`), and no deployment operation is in flight: the
126
+ * most recent one is absent or ended 'succeeded', 'rolled-back', 'failed'
127
+ * or 'cleaned' (a recovery redeploy over an unexecutable plan).
123
128
  * `PLATFORM_BINDING_READINESS_DEFERRED` documents that the promote preflight
124
129
  * of a held greenfield service that was never promoted accepted a Corestore
125
130
  * `database` or `objectstorage` binding that is enabled but not yet `ready`.
@@ -45,7 +45,8 @@ export interface ICloudlySettings {
45
45
  * cluster ingress serves to them (`IClusterHttpRoute.sourceAllowList`), so the gateway that checks
46
46
  * the hostname's source profile stays the only way in. Absent: no non-public hostname is served
47
47
  * by a cluster ingress. A value is accepted only when `validateClusterRouteSourceAllowList`
48
- * answers no error.
48
+ * answers no error; an IPv4 source is written as IPv4 (`192.0.2.0/24`), never as an IPv4-mapped
49
+ * IPv6 prefix (`::ffff:c000:200/120`), which the ingress could never match.
49
50
  */
50
51
  dcrouterForwardSourceAddresses?: string[];
51
52
  dcrouterMailSubmissionHost?: string;
@@ -64,6 +64,10 @@ const parseIpv6 = (valueArg: string): bigint | null => {
64
64
  return groups.reduce((sumArg, groupArg) => (sumArg << 16n) | BigInt(parseInt(groupArg, 16)), 0n);
65
65
  };
66
66
 
67
+ /** The dotted-quad text of one IPv4 address. */
68
+ const formatIpv4 = (valueArg: bigint): string =>
69
+ [24n, 16n, 8n, 0n].map((shiftArg) => ((valueArg >> shiftArg) & 0xffn).toString()).join('.');
70
+
67
71
  /** The RFC 5952 text of one IPv6 address. */
68
72
  const formatIpv6 = (valueArg: bigint): string => {
69
73
  const groups: number[] = [];
@@ -138,3 +142,22 @@ export const ipPrefixContains = (prefixArg: IIpPrefixValue, addressArg: IIpAddre
138
142
  const hostBits = BigInt(familyBits(prefixArg.family) - prefixArg.length);
139
143
  return addressArg.value >> hostBits === prefixArg.network >> hostBits;
140
144
  };
145
+
146
+ /** The canonical text of one prefix, the spelling `parseCanonicalIpPrefix` reads back. */
147
+ export const formatCanonicalIpPrefix = (prefixArg: IIpPrefixValue): string =>
148
+ `${prefixArg.family === 4 ? formatIpv4(prefixArg.network) : formatIpv6(prefixArg.network)}/${prefixArg.length}`;
149
+
150
+ /** Length of the IPv4-mapped IPv6 range `::ffff:0:0/96` (RFC 4291 section 2.5.5.2). */
151
+ const ipv4MappedPrefixLength = 96;
152
+
153
+ /**
154
+ * The IPv4 prefix an IPv6 prefix inside the IPv4-mapped range `::ffff:0:0/96` spells
155
+ * (`::ffff:c000:200/120` is `192.0.2.0/24`), or `null` for any other prefix. A shorter IPv6 prefix
156
+ * that merely contains the range is not mapped: it still names IPv6 addresses outside it.
157
+ */
158
+ export const ipv4PrefixOfMapped = (prefixArg: IIpPrefixValue): IIpPrefixValue | null =>
159
+ prefixArg.family === 6
160
+ && prefixArg.length >= ipv4MappedPrefixLength
161
+ && prefixArg.network >> 32n === 0xffffn
162
+ ? { family: 4, network: prefixArg.network & 0xffffffffn, length: prefixArg.length - ipv4MappedPrefixLength }
163
+ : null;