@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.
- package/changelog.md +40 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/catalog.d.ts +1 -1
- package/dist_ts/appstore/catalog.js +2 -1
- package/dist_ts/appstore/types.d.ts +36 -0
- package/dist_ts/appstore/types.js +11 -1
- package/dist_ts/appstore/versionconfig.d.ts +11 -3
- package/dist_ts/appstore/versionconfig.js +146 -7
- package/dist_ts/data/clusteringress.d.ts +10 -2
- package/dist_ts/data/clusteringress.js +16 -3
- package/dist_ts/data/deploymentpreflight.d.ts +8 -3
- package/dist_ts/data/deploymentpreflight.js +1 -1
- package/dist_ts/data/settings.d.ts +2 -1
- package/dist_ts/data/settings.js +1 -1
- package/dist_ts/private/ipaddress.d.ts +8 -0
- package/dist_ts/private/ipaddress.js +17 -1
- package/package.json +1 -1
- package/readme.md +22 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/catalog.ts +1 -0
- package/ts/appstore/types.ts +38 -0
- package/ts/appstore/versionconfig.ts +163 -5
- package/ts/data/clusteringress.ts +23 -3
- package/ts/data/deploymentpreflight.ts +8 -3
- package/ts/data/settings.ts +2 -1
- package/ts/private/ipaddress.ts +23 -0
|
@@ -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 {
|
|
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
|
|
121
|
-
*
|
|
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`.
|
package/ts/data/settings.ts
CHANGED
|
@@ -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;
|
package/ts/private/ipaddress.ts
CHANGED
|
@@ -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;
|