@serve.zone/interfaces 31.7.0 → 31.9.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 +77 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusterruntime.d.ts +39 -0
- package/dist_ts/data/clusterruntime.js +11 -0
- package/dist_ts/data/index.d.ts +5 -0
- package/dist_ts/data/index.js +6 -1
- package/dist_ts/data/networkaddressplan.d.ts +78 -0
- package/dist_ts/data/networkaddressplan.js +114 -0
- package/dist_ts/data/networkauthority.js +5 -17
- package/dist_ts/data/networkcredential.d.ts +10 -4
- package/dist_ts/data/networkcredential.js +14 -8
- package/dist_ts/data/networkdelivery.d.ts +21 -0
- package/dist_ts/data/networkdelivery.js +35 -7
- package/dist_ts/data/networklease.d.ts +110 -0
- package/dist_ts/data/networklease.js +159 -0
- package/dist_ts/data/networknode.d.ts +37 -0
- package/dist_ts/data/networknode.js +6 -0
- package/dist_ts/data/networkprojection.d.ts +10 -5
- package/dist_ts/data/networkprojection.js +15 -22
- package/dist_ts/data/networksigning.js +5 -4
- package/dist_ts/data/runtimeconfig.js +14 -77
- package/dist_ts/data/serviceruntime.d.ts +128 -0
- package/dist_ts/data/serviceruntime.js +92 -0
- package/dist_ts/private/networkcontract.d.ts +20 -0
- package/dist_ts/private/networkcontract.js +45 -1
- package/dist_ts/private/producercontract.d.ts +11 -0
- package/dist_ts/private/producercontract.js +21 -0
- package/dist_ts/private/runtimepolicy.d.ts +31 -0
- package/dist_ts/private/runtimepolicy.js +90 -0
- package/dist_ts/requests/cluster.d.ts +53 -0
- package/dist_ts/requests/cluster.js +1 -1
- package/dist_ts/requests/network.d.ts +57 -1
- package/dist_ts/requests/runtimesession.d.ts +14 -0
- package/dist_ts/requests/service.d.ts +60 -0
- package/dist_ts/runtime.networkprojection.d.ts +14 -0
- package/dist_ts/runtime.networkprojection.js +68 -19
- package/package.json +2 -2
- package/readme.md +251 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusterruntime.ts +45 -0
- package/ts/data/index.ts +5 -0
- package/ts/data/networkaddressplan.ts +175 -0
- package/ts/data/networkauthority.ts +4 -16
- package/ts/data/networkcredential.ts +19 -9
- package/ts/data/networkdelivery.ts +49 -6
- package/ts/data/networklease.ts +229 -0
- package/ts/data/networknode.ts +37 -0
- package/ts/data/networkprojection.ts +28 -26
- package/ts/data/networksigning.ts +5 -4
- package/ts/data/runtimeconfig.ts +17 -76
- package/ts/data/serviceruntime.ts +208 -0
- package/ts/private/networkcontract.ts +48 -0
- package/ts/private/producercontract.ts +19 -0
- package/ts/private/runtimepolicy.ts +105 -0
- package/ts/requests/cluster.ts +65 -0
- package/ts/requests/network.ts +50 -1
- package/ts/requests/runtimesession.ts +17 -0
- package/ts/requests/service.ts +81 -0
- package/ts/runtime.networkprojection.ts +87 -20
package/readme.md
CHANGED
|
@@ -371,7 +371,8 @@ set. An empty array declares NODATA; absent/unauthorized names remain NXDOMAIN,
|
|
|
371
371
|
and IPv4-only service names have AAAA NODATA. The view carries the existing
|
|
372
372
|
forwarding intersection and zero or one search suffix. Pallet must prove the
|
|
373
373
|
actual sandbox/veth/source binding, block private/bare-name forwarding, clip
|
|
374
|
-
TTLs and convert the
|
|
374
|
+
TTLs and convert the effective window from `getRuntimeNetworkEffectiveDnsWindow` (see
|
|
375
|
+
[DNS Lease Renewals](#dns-lease-renewals)) using its qualified boot/time authority.
|
|
375
376
|
These helpers neither cache queries nor renew a lease.
|
|
376
377
|
|
|
377
378
|
The DNS composer accepts an optional second argument of exact local workload
|
|
@@ -454,10 +455,11 @@ capture closed detached JSON shapes and canonical 32-byte key encodings.
|
|
|
454
455
|
`bindGetRuntimeManagedVpnCredentialRequestV1(request, projection, trustedSession)`
|
|
455
456
|
validates the complete projection digest and binds node, controller, namespace,
|
|
456
457
|
physical-session identity and projection reference. The response binder takes
|
|
457
|
-
`(response, request, projection, trustedSession, now)` and additionally requires
|
|
458
|
+
`(response, request, projection, trustedSession, now, renewal = null)` and additionally requires
|
|
458
459
|
the relay's exact UDP tuple to be selected in `router.platformEndpointIds`.
|
|
459
460
|
Expiry must be after the caller-qualified time and no later than the referenced
|
|
460
|
-
projection's DNS
|
|
461
|
+
projection's effective DNS window (see [DNS Lease Renewals](#dns-lease-renewals));
|
|
462
|
+
acceptance before that window starts rejects.
|
|
461
463
|
|
|
462
464
|
These helpers perform content validation. Callers authenticate and fence the
|
|
463
465
|
current physical peer and projection before and after native work, qualify time
|
|
@@ -466,6 +468,188 @@ Noise implementation owns cryptographic validation and authentication. Credentia
|
|
|
466
468
|
delivery does not prove a connected VPN, TUN ownership, packet enforcement or
|
|
467
469
|
workload readiness, and does not replace explicit packet withdrawals.
|
|
468
470
|
|
|
471
|
+
### DNS Lease Renewals
|
|
472
|
+
|
|
473
|
+
A projection's `dns` window lasts at most fifteen minutes and is signed content, so a node's
|
|
474
|
+
private DNS stops when the window ends unless a newer signed statement extends it. Re-issuing the
|
|
475
|
+
projection would extend it too, but a node applies every new projection as new network state,
|
|
476
|
+
which restarts its data plane. `IRuntimeNetworkDnsLeaseRenewal` extends the window and nothing
|
|
477
|
+
else: it carries the node scope (`controller`, `nodeId`, `runtimeNamespace`), the exact
|
|
478
|
+
`projection` reference it renews, a `sequence`, a fresh `dns { issuedAt, notBefore, expiresAt }`
|
|
479
|
+
window and its `digest`. It names the projection by reference, so it never joins the projection
|
|
480
|
+
chain and never changes what the node applied. The new shapes carry no `schemaVersion` and no
|
|
481
|
+
version suffix; the installed package version is the protocol version.
|
|
482
|
+
|
|
483
|
+
`issuedAt` is the start of the window's slot, not the moment of signing: Cloudly signs renewals
|
|
484
|
+
ahead of time on a grid, so the fifteen-minute rule (`maximumDnsLeaseMs`, the same limit a
|
|
485
|
+
projection's window has) is measured from the slot. `computeRuntimeNetworkDnsLeaseRenewalDigest`
|
|
486
|
+
hashes every field except `digest` under its own domain, and `validateRuntimeNetworkDnsLeaseRenewal`
|
|
487
|
+
checks structure and digest only; a digest authenticates nothing.
|
|
488
|
+
|
|
489
|
+
The Node-only `@serve.zone/interfaces/runtime` export signs and verifies renewals with the same
|
|
490
|
+
authority and key that sign the controller's projections, under a separate signature domain.
|
|
491
|
+
`signRuntimeNetworkDnsLeaseRenewal(renewal, authority, privateKey)` produces
|
|
492
|
+
`ISignedRuntimeNetworkDnsLeaseRenewal { authority, renewal, signature }`.
|
|
493
|
+
`verifyRuntimeNetworkDnsLeaseRenewal(signed, trustedAuthority, trustedScope, projectionAuthority)`
|
|
494
|
+
takes the authority and scope from the node's own stored trust, never from the envelope.
|
|
495
|
+
`projectionAuthority` is the reference of the authority revision that signed the projection the
|
|
496
|
+
node admitted, and a renewal verifies only when it was signed under exactly that revision.
|
|
497
|
+
Rotation and revocation end renewals exactly as they end projections: nothing signed under an
|
|
498
|
+
earlier revision verifies after, and a renewal signed under a rotated revision does not renew a
|
|
499
|
+
projection signed under the one before. A renewal never closes a key gap; after rotation, DNS
|
|
500
|
+
continues only with a newly signed projection.
|
|
501
|
+
|
|
502
|
+
`admitRuntimeNetworkDnsLeaseRenewal(renewal, currentProjection, previousRenewal | null, trustedScope, now)`
|
|
503
|
+
is the pure admission decision after verification. The projection and the previous renewal are
|
|
504
|
+
the node's own admitted state, and `now` is its caller-qualified clock, read before the first
|
|
505
|
+
await like every other input. It admits a renewal only when:
|
|
506
|
+
|
|
507
|
+
- the renewal and the projection are in the trusted scope, and the renewal names the current
|
|
508
|
+
projection's exact reference. Once a successor projection is admitted, every renewal of its
|
|
509
|
+
predecessor is refused, and a renewal never introduces a projection the node has not admitted;
|
|
510
|
+
- its `sequence` is higher than the previous renewal's, with gaps allowed, or equal with
|
|
511
|
+
byte-identical content, which is answered as `replay` whatever `now` is, so re-delivery is
|
|
512
|
+
idempotent. The same sequence with any other content is refused;
|
|
513
|
+
- its window is current: `notBefore <= now < expiresAt`. Admitting a window that has not opened
|
|
514
|
+
would displace the coverage the node has now, so a sender delivers a window only once its slot
|
|
515
|
+
has opened; a renewal refused for arriving early is simply delivered again once it has. An
|
|
516
|
+
expired window is refused as well;
|
|
517
|
+
- its window does not move `issuedAt` back and ends later than the previous window. Without a
|
|
518
|
+
previous renewal, the projection's own window is the baseline. Windows need not touch, so a node
|
|
519
|
+
whose lease lapsed can still admit the next renewal it is given.
|
|
520
|
+
|
|
521
|
+
The caller stores the admitted renewal in the same transaction as its trust fence, and deletes it
|
|
522
|
+
in the transaction that admits a successor projection. Admission is not conversion: the node still
|
|
523
|
+
turns the window into a deadline with its qualified clock, and must not use a window before its
|
|
524
|
+
`notBefore`.
|
|
525
|
+
|
|
526
|
+
`requests.runtimesession.IReq_Controller_Pallet_ApplyRuntimeNetworkDnsLeaseRenewal`
|
|
527
|
+
(`applyRuntimeNetworkDnsLeaseRenewal`) delivers one renewal as
|
|
528
|
+
`IApplyRuntimeNetworkDnsLeaseRenewalRequest { session, signed }`.
|
|
529
|
+
`bindApplyRuntimeNetworkDnsLeaseRenewalRequest(request, trustedSession)` requires the node's
|
|
530
|
+
current physical-session binding and a renewal in that session's scope. It does not care who sent
|
|
531
|
+
the request: a cluster relay pushes under the binding the node obtained itself. The answer,
|
|
532
|
+
`IApplyRuntimeNetworkDnsLeaseRenewalResponse { status, renewal }`, repeats
|
|
533
|
+
`IRuntimeNetworkDnsLeaseRenewalReference { projection, sequence, digest }`.
|
|
534
|
+
`bindApplyRuntimeNetworkDnsLeaseRenewalResponse(response, sentRenewal)` compares it with the exact
|
|
535
|
+
renewal that was sent. The ACK asserts durable admission only, not that the node serves DNS.
|
|
536
|
+
|
|
537
|
+
A cluster is outbound-only, so while its relay cannot reach Cloudly nobody can sign a new window.
|
|
538
|
+
Cloudly therefore hands the elected relay a run of renewals it signed ahead of time, with
|
|
539
|
+
`requests.cluster.IReq_Cloudly_Relay_HoldRuntimeNetworkDnsLeaseRenewals`
|
|
540
|
+
(`holdRuntimeNetworkDnsLeaseRenewals`):
|
|
541
|
+
`IHoldRuntimeNetworkDnsLeaseRenewalsRequest { nodeId, projection, renewals }` answered by
|
|
542
|
+
`{ held }`. Each request replaces what the relay held for that node, and an empty run holds
|
|
543
|
+
nothing. The relay keeps the run in memory only, pushes only the renewal whose window is current
|
|
544
|
+
and only while it has no Cloudly session (`escrowRelease`), and holds no key and no trust.
|
|
545
|
+
`snapshotHoldRuntimeNetworkDnsLeaseRenewalsRequest` checks the rules a relay can evaluate without
|
|
546
|
+
the projection itself; the node still measures the first window against its admitted projection,
|
|
547
|
+
and every window against its clock, when it admits it. The run must have:
|
|
548
|
+
|
|
549
|
+
- one node, one projection and one signing authority revision, in one scope;
|
|
550
|
+
- strictly ascending sequences, each window extending the one before;
|
|
551
|
+
- at most `maximumEscrowedRenewals` (32) renewals;
|
|
552
|
+
- at most `maximumEscrowHorizonMs` (four hours) from the first window's `issuedAt` to the last
|
|
553
|
+
window's `expiresAt`. On the intended grid of fifteen-minute windows every ten minutes, the
|
|
554
|
+
horizon allows 23 windows, so it is the limit that binds.
|
|
555
|
+
|
|
556
|
+
`validateHoldRuntimeNetworkDnsLeaseRenewalsRequest` adds every renewal's digest check and
|
|
557
|
+
verifies no signature. `bindHoldRuntimeNetworkDnsLeaseRenewalsResponse(response, sentRequest)`
|
|
558
|
+
requires `held` to equal the number of renewals sent.
|
|
559
|
+
|
|
560
|
+
The horizon is also the revocation latency: a detached cluster keeps DNS authority until its last
|
|
561
|
+
held window ends. So a controller must not release an address or a DNS name before the latest
|
|
562
|
+
`expiresAt` it has signed for that node. Renewals extend DNS only. They grant no packet authority
|
|
563
|
+
and cannot introduce new network state, and they do not extend an already-issued VPN credential;
|
|
564
|
+
a newly issued one may be bound to the renewed window.
|
|
565
|
+
|
|
566
|
+
`getRuntimeNetworkEffectiveDnsWindow(projection, renewal | null)` is the one rule for the window a
|
|
567
|
+
node may use: the admitted renewal's if there is one, else the projection's own. `renewal` is the
|
|
568
|
+
bare `IRuntimeNetworkDnsLeaseRenewal` that admission takes and returns, which is what a node
|
|
569
|
+
stores; Cloudly passes `signed.renewal`. It checks digests and that the renewal names exactly that
|
|
570
|
+
projection, but verifies no signature. Controller and node use it for every limit tied to the
|
|
571
|
+
window, and `bindGetRuntimeManagedVpnCredentialResponseV1` takes the same bare admitted renewal as
|
|
572
|
+
an optional last argument, so a newly issued managed-VPN credential may run to the end of the
|
|
573
|
+
renewed window instead of the projection's original one.
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
import { data } from '@serve.zone/interfaces';
|
|
577
|
+
import { signRuntimeNetworkDnsLeaseRenewal, verifyRuntimeNetworkDnsLeaseRenewal } from '@serve.zone/interfaces/runtime';
|
|
578
|
+
|
|
579
|
+
const renewal: data.IRuntimeNetworkDnsLeaseRenewal = {
|
|
580
|
+
...scope,
|
|
581
|
+
projection: { id: projection.id, generation: projection.generation, digest: projection.digest },
|
|
582
|
+
sequence: 1,
|
|
583
|
+
dns: { issuedAt: slotStart, notBefore: slotStart, expiresAt: slotStart + 900_000 },
|
|
584
|
+
digest: `sha256:${'0'.repeat(64)}` as data.TSha256Digest, // well-formed placeholder, replaced below
|
|
585
|
+
};
|
|
586
|
+
renewal.digest = await data.computeRuntimeNetworkDnsLeaseRenewalDigest(renewal);
|
|
587
|
+
const signed = await signRuntimeNetworkDnsLeaseRenewal(renewal, signingAuthority, privateKey);
|
|
588
|
+
|
|
589
|
+
// On the node: stored trust, then the node's own admitted state and qualified clock.
|
|
590
|
+
const verified = await verifyRuntimeNetworkDnsLeaseRenewal(
|
|
591
|
+
signed, storedAuthority, nodeScope, admittedProjectionAuthority);
|
|
592
|
+
const { renewal: admittedRenewal } = await data.admitRuntimeNetworkDnsLeaseRenewal(
|
|
593
|
+
verified.renewal, admittedProjection, previousRenewal, nodeScope, qualifiedNow);
|
|
594
|
+
const window = await data.getRuntimeNetworkEffectiveDnsWindow(admittedProjection, admittedRenewal);
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Runtime Network Address Plan
|
|
598
|
+
|
|
599
|
+
`data.IRuntimeNetworkAddressPlan` is the address space an administrator gives the runtime network,
|
|
600
|
+
and the single input Cloudly composes the protected authority from:
|
|
601
|
+
`{ id: 'address-plan', revision, prefixes, pools, resolvers, platformEndpoints, vpn: { controlPrefix, relay } }`.
|
|
602
|
+
Pools, resolvers and platform endpoints use the protected authority's own element shapes, and the
|
|
603
|
+
relay uses the managed-VPN credential's `{ id, address, port }`. `platformEndpoints` never lists the
|
|
604
|
+
relay: the composed authority adds it, in id order, as the UDP platform endpoint every node dials.
|
|
605
|
+
|
|
606
|
+
`snapshotRuntimeNetworkAddressPlan` refuses any plan that could describe an authority the authority
|
|
607
|
+
contract refuses. It runs `snapshotRuntimeNetworkProtectedAuthorityV1` twice, once over the plan's
|
|
608
|
+
own sets and once with the relay added, so overlapping pools, more than 16 resolvers, a resolver or
|
|
609
|
+
endpoint outside `prefixes` or inside a pool, and a relay that `prefixes` do not cover are all
|
|
610
|
+
refused by that one validator. On top it requires the VPN `controlPrefix` to be a private prefix no
|
|
611
|
+
longer than /30 (`runtimeNetworkAddressPlanContract.maximumControlPrefixLength`) inside `prefixes`
|
|
612
|
+
that overlaps no pool and holds no resolver or platform endpoint, the relay included. No minimum
|
|
613
|
+
length is stated: the private-range rule already keeps it at /8 or longer.
|
|
614
|
+
`composeRuntimeNetworkProtectedAuthority(plan, frame)` is the composition itself: the producer
|
|
615
|
+
supplies the frame (`TRuntimeNetworkProtectedAuthorityFrame`: authority id, generation, controller,
|
|
616
|
+
previous reference and egress authorities) and receives the complete authority with its digest, so
|
|
617
|
+
the validator and the protection producer can never compose differently.
|
|
618
|
+
|
|
619
|
+
`admitRuntimeNetworkAddressPlanChange(previous | null, next)` is the pure decision for a plan write at
|
|
620
|
+
the following revision; any other revision is a contract violation. Leases and the managed VPN
|
|
621
|
+
outlive a plan revision, so it answers:
|
|
622
|
+
|
|
623
|
+
- `unchanged` when the content equals the stored plan; Cloudly writes nothing;
|
|
624
|
+
- `refused` with `plan-vpn-immutable` when the VPN differs, `plan-pool-removed` when any pool is gone
|
|
625
|
+
or changed its purpose or prefix, and `plan-prefix-shrunk` when the new prefixes, however split,
|
|
626
|
+
no longer cover every address the old ones did;
|
|
627
|
+
- `accepted` with the ids of the resolvers and platform endpoints the change removes or changes. An
|
|
628
|
+
entry that keeps its id but differs in any field counts, so moving it goes through the same
|
|
629
|
+
reference check as removing it.
|
|
630
|
+
|
|
631
|
+
`TRuntimeNetworkAddressPlanRefusal` adds `plan-resolver-referenced` and `plan-endpoint-referenced`,
|
|
632
|
+
which only Cloudly can decide against live references, so every plan refusal has one type.
|
|
633
|
+
`requests.network` carries `setRuntimeNetworkAddressPlan { identity, expectedRevision, plan }`
|
|
634
|
+
(`expectedRevision: null` when no plan exists yet) and `getRuntimeNetworkAddressPlan`, which answers
|
|
635
|
+
`null` until the first plan is set.
|
|
636
|
+
|
|
637
|
+
### Node Network Readiness
|
|
638
|
+
|
|
639
|
+
`requests.network.getRuntimeNetworkNodeReadiness { identity, nodeId }` answers
|
|
640
|
+
`data.IRuntimeNetworkNodeReadiness`: `ready: true` with nothing missing, or `ready: false` with at
|
|
641
|
+
least one `TRuntimeNetworkNodeReadinessGap`. The gaps follow the node producer's order: the node
|
|
642
|
+
joins the protected authority's egress (`egress-pending`), every egress owner reports current
|
|
643
|
+
protection (`protection-receipts-pending`, naming those `owners`, at least one), a handoff is reserved
|
|
644
|
+
(`handoff-pending`), the router selection carries it (`selection-pending`), the node's acknowledged
|
|
645
|
+
projection carries it (`projection-pending`), and the node is a managed VPN member
|
|
646
|
+
(`vpn-member-pending`).
|
|
647
|
+
|
|
648
|
+
`data.IRuntimeRouterIncarnation { id, incarnation }` is a node's router incarnation, from which its
|
|
649
|
+
handoff and router incarnation ids derive; `snapshotRuntimeRouterIncarnation` checks the record.
|
|
650
|
+
`reincarnateRuntimeNetworkRouter { identity, nodeId, expectedIncarnation }` advances it, so the node
|
|
651
|
+
producer reserves the router a fresh handoff, and answers with the new incarnation.
|
|
652
|
+
|
|
469
653
|
### Node-Bound Runtime Assignments
|
|
470
654
|
|
|
471
655
|
The `data` namespace exports `IRuntimeAssignmentV1`, canonical digest helpers,
|
|
@@ -599,6 +783,70 @@ only the named Pallet attempt. They do not authorize storage relocation, prove
|
|
|
599
783
|
whole-host or Docker/Coreflow absence, grant another controller or organization
|
|
600
784
|
authority, or replace independently qualified readiness and routing evidence.
|
|
601
785
|
|
|
786
|
+
### Cluster Runtime Phase
|
|
787
|
+
|
|
788
|
+
`data.IClusterRuntime { id, phase, generation, changedAt, changedBy }` records which runtime owns a
|
|
789
|
+
cluster's workloads: `TClusterRuntimePhase` is `swarm`, `draining`, `switching` or `pallet`. It is
|
|
790
|
+
stored beside the cluster document, because a cluster update merges caller data and no caller may
|
|
791
|
+
move a phase with it; `generation` is its compare-and-swap counter. `snapshotClusterRuntime` checks
|
|
792
|
+
the record. Which phase may follow which belongs to the cutover, whose requests ship with it.
|
|
793
|
+
|
|
794
|
+
`requests.cluster.getClusterRuntime { identity, clusterId }` answers `{ runtime, blockers }`. Each
|
|
795
|
+
`TClusterRuntimeBlocker` names what it is about: cluster-wide blockers (`relay-not-enabled`,
|
|
796
|
+
`relay-not-registered`, `address-plan-missing`, `vpn-unconfigured`) name nothing, node blockers
|
|
797
|
+
(`node-without-pallet-credential`, `node-arch-unsupported`) name the `nodeId`, and service blockers
|
|
798
|
+
(`service-multi-cluster-scope`, `service-authority-unsupported`, `platform-binding-present`) name
|
|
799
|
+
the `serviceId`.
|
|
800
|
+
|
|
801
|
+
### Service Runtime Specs And Status
|
|
802
|
+
|
|
803
|
+
`data.IServiceRuntimeSpec` is what Cloudly runs for one service on a Pallet cluster:
|
|
804
|
+
`{ id, organizationId, clusterId, placement, replicasPerNode, hold, profile, network, revision }`.
|
|
805
|
+
It is stored complete and beside the service document, so a hold written by a cutover never races
|
|
806
|
+
an operator's service edit.
|
|
807
|
+
|
|
808
|
+
- `placement` is `{ kind: 'every-node' }` or `{ kind: 'nodes', nodeIds }`, with `nodeIds` sorted,
|
|
809
|
+
unique, never empty and at most `maximumPinnedNodes`, the protected authority's egress limit.
|
|
810
|
+
- `replicasPerNode` runs from 1 to 64; Cloudly refuses a service desiring more than `maximumReplicas` (64) as `replica-limit`.
|
|
811
|
+
- `profile { cpuMillis, memoryBytes, readiness, logs, readonlyRootfs, runAs }` is the runtime config's
|
|
812
|
+
execution policy ahead of any image: the same readiness and log shapes, and the same CPU, memory
|
|
813
|
+
(at least 16 MiB), identity, log, readiness timing and HTTP request rules, which both validators
|
|
814
|
+
share. `runAs: null` runs the image's own OCI user. Only the service's target ports decide whether
|
|
815
|
+
a readiness probe's target exists: the runtime config requires the service to declare it, and the
|
|
816
|
+
producer refuses a service that does not as `readiness-target-undeclared`.
|
|
817
|
+
- `network` is `{ mode: 'isolated' }` or `{ mode: 'attached', publicEgress, platformEndpointIds }`
|
|
818
|
+
with sorted unique endpoint ids.
|
|
819
|
+
|
|
820
|
+
`snapshotServiceRuntimeSpec` checks all of that; whether the cluster, the nodes and the endpoints
|
|
821
|
+
exist is Cloudly's. `requests.service.setServiceRuntimeSpec { identity, serviceId, expectedRevision, spec }`
|
|
822
|
+
writes `TServiceRuntimeSpecWritableData`, and Cloudly copies the organization from the service.
|
|
823
|
+
`getServiceRuntimeSpec` answers `{ spec, status }`: both `null` before the first spec, and `status`
|
|
824
|
+
`null` until the producer first decides the spec. A status never comes without its spec.
|
|
825
|
+
|
|
826
|
+
`data.IServiceRuntimeStatus { id, specRevision, state, reason, changedAt }` is the producer's verdict
|
|
827
|
+
on one spec revision, and each reason belongs to exactly one state. `converged` and `converging`
|
|
828
|
+
carry `reason: null`. `waiting` carries a `TServiceRuntimeWaitingReason`: `node-network-pending`,
|
|
829
|
+
`conflict` or `failure-backoff`. `refused` carries a `TServiceRuntimeRefusal`:
|
|
830
|
+
`cluster-runtime-missing`, `organization-missing`, `image-plan-missing`, `platform-unsupported`,
|
|
831
|
+
`secrets-unsupported`, `volumes-unsupported`, `port-protocol-unsupported`,
|
|
832
|
+
`readiness-target-undeclared`, `environment-too-large`, `no-eligible-node` or `replica-limit`. A
|
|
833
|
+
refusal never retires running slots. `snapshotServiceRuntimeStatus` enforces the pairing.
|
|
834
|
+
|
|
835
|
+
### Runtime Assignment Views
|
|
836
|
+
|
|
837
|
+
`data.IServiceRuntimeAssignmentView` is one replica slot's current assignment as an administrator
|
|
838
|
+
reads it: `serviceId`, `replicaId`, `nodeId`, the current `assignment` reference, its `disposition`,
|
|
839
|
+
the latest accepted `observation { phase, ready, observedAt, sequence }` (which may cite an earlier
|
|
840
|
+
revision), `terminalReceiptCount` (0 to 2) and `delivery`. `delivered` means the node's own evidence,
|
|
841
|
+
an observation or a terminal receipt, cites the current revision; until then it is `undelivered`.
|
|
842
|
+
|
|
843
|
+
`requests.service.getServiceRuntimeAssignments { identity, serviceId }` answers every slot of one
|
|
844
|
+
service. `requests.cluster.getClusterRuntimeAssignments { identity, clusterId, cursor, limit }` pages
|
|
845
|
+
through every slot on a cluster's nodes; `limit` runs from 1 to
|
|
846
|
+
`serviceRuntimeContract.maximumAssignmentPageSize` (256), and Cloudly refuses any other.
|
|
847
|
+
`requests.service.pushServiceRuntimeChanged { serviceId }` tells admin UIs that a service's spec,
|
|
848
|
+
status or assignments changed; it carries no state, so a late push never overwrites a newer read.
|
|
849
|
+
|
|
602
850
|
### Offline Database Settlement And Packaging
|
|
603
851
|
|
|
604
852
|
The Node-only `@serve.zone/interfaces/runtime` export includes isolated V1
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { captureNetwork, networkCounter, networkId, networkRecord, requireNetwork } from '../private/networkcontract.js';
|
|
2
|
+
import { isOneOf, producerSafe } from '../private/producercontract.js';
|
|
3
|
+
|
|
4
|
+
const clusterRuntimePhases = ['swarm', 'draining', 'switching', 'pallet'] as const;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which runtime owns a cluster's workloads.
|
|
8
|
+
*
|
|
9
|
+
* `swarm` is Coreflow's; `draining` stops every service while its volumes stay; `switching` lets the
|
|
10
|
+
* cluster's nodes join the runtime network while nothing runs; `pallet` is Pallet's alone.
|
|
11
|
+
*/
|
|
12
|
+
export type TClusterRuntimePhase = typeof clusterRuntimePhases[number];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The runtime phase of one cluster, stored beside the cluster document rather than inside it: a
|
|
16
|
+
* cluster update merges caller data, and no caller may move a phase with it.
|
|
17
|
+
*/
|
|
18
|
+
export interface IClusterRuntime {
|
|
19
|
+
/** The cluster id. */
|
|
20
|
+
id: string;
|
|
21
|
+
phase: TClusterRuntimePhase;
|
|
22
|
+
/** Compare-and-swap counter; every phase write advances it by one. */
|
|
23
|
+
generation: number;
|
|
24
|
+
/** Unix milliseconds on Cloudly's clock. */
|
|
25
|
+
changedAt: number;
|
|
26
|
+
/** The administrator or producer that made the change. */
|
|
27
|
+
changedBy: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Why a cluster cannot leave `swarm` yet. Each blocker names the node or service it is about, so an
|
|
32
|
+
* administrator can act on it without a second query.
|
|
33
|
+
*/
|
|
34
|
+
export type TClusterRuntimeBlocker =
|
|
35
|
+
| { code: 'relay-not-enabled' | 'relay-not-registered' | 'address-plan-missing' | 'vpn-unconfigured' }
|
|
36
|
+
| { code: 'node-without-pallet-credential' | 'node-arch-unsupported'; nodeId: string }
|
|
37
|
+
| { code: 'service-multi-cluster-scope' | 'service-authority-unsupported' | 'platform-binding-present'; serviceId: string };
|
|
38
|
+
|
|
39
|
+
/** Structural snapshot for exact persistence; which phase may follow which is the cutover's decision. */
|
|
40
|
+
export const snapshotClusterRuntime = (value: unknown): IClusterRuntime => producerSafe(() => {
|
|
41
|
+
const item = networkRecord(captureNetwork(value), ['id', 'phase', 'generation', 'changedAt', 'changedBy']);
|
|
42
|
+
requireNetwork(isOneOf(clusterRuntimePhases, item.phase));
|
|
43
|
+
return { id: networkId(item.id), phase: item.phase, generation: networkCounter(item.generation),
|
|
44
|
+
changedAt: networkCounter(item.changedAt), changedBy: networkId(item.changedBy) };
|
|
45
|
+
});
|
package/ts/data/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './cloudlyconfig.js';
|
|
2
2
|
export * from './cluster.js';
|
|
3
|
+
export * from './clusterruntime.js';
|
|
3
4
|
export * from './clusteringress.js';
|
|
4
5
|
export * from './config.js';
|
|
5
6
|
export * from './corestore.js';
|
|
@@ -24,9 +25,12 @@ export * from './isolatedrestore.golden.js';
|
|
|
24
25
|
export * from './mail.js';
|
|
25
26
|
export * from './nodecredential.js';
|
|
26
27
|
export * from './nodeenrollment.js';
|
|
28
|
+
export * from './networkaddressplan.js';
|
|
27
29
|
export * from './networkauthority.js';
|
|
28
30
|
export * from './networkcredential.js';
|
|
29
31
|
export * from './networkdelivery.js';
|
|
32
|
+
export * from './networklease.js';
|
|
33
|
+
export * from './networknode.js';
|
|
30
34
|
export * from './networkprojection.js';
|
|
31
35
|
export type { IRuntimeNetworkRouterPacketGrantV1, IRuntimeNetworkRouterProjectionV1 } from './networkrouter.js';
|
|
32
36
|
export * from './networkprotection.js';
|
|
@@ -47,6 +51,7 @@ export * from './sparknodereport.js';
|
|
|
47
51
|
export * from './service.js';
|
|
48
52
|
export * from './servicecredential.js';
|
|
49
53
|
export * from './serviceports.js';
|
|
54
|
+
export * from './serviceruntime.js';
|
|
50
55
|
export * from './status.js';
|
|
51
56
|
export * from './taskexecution.js';
|
|
52
57
|
export * from './traffic.js';
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { TSha256Digest } from './immutableimage.js';
|
|
2
|
+
import type { IRuntimeNetworkProtectedAuthorityV1 } from './networkauthority.js';
|
|
3
|
+
import { computeRuntimeNetworkProtectedAuthorityV1Digest, snapshotRuntimeNetworkProtectedAuthorityV1 } from './networkauthority.js';
|
|
4
|
+
import type { IGetRuntimeManagedVpnCredentialResponseV1 } from './networkcredential.js';
|
|
5
|
+
import { captureNetwork, failNetwork, networkContains, networkCounter, networkEqual, networkId, networkIpv4, networkOverlaps,
|
|
6
|
+
networkPrefixBounds, networkPrivatePrefix, networkRecord, networkSafe, requireNetwork } from '../private/networkcontract.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The address space an administrator gives the runtime network, and the one input the protected
|
|
10
|
+
* authority is composed from. Cloudly keeps a single plan; pools and prefixes only grow, and the
|
|
11
|
+
* managed VPN never changes once set.
|
|
12
|
+
*/
|
|
13
|
+
export interface IRuntimeNetworkAddressPlan {
|
|
14
|
+
id: 'address-plan';
|
|
15
|
+
/** Compare-and-swap counter; every accepted change advances it by one. */
|
|
16
|
+
revision: number;
|
|
17
|
+
/** Every protected prefix: they cover the pools, resolvers, platform endpoints and the VPN. */
|
|
18
|
+
prefixes: string[];
|
|
19
|
+
pools: IRuntimeNetworkProtectedAuthorityV1['pools'];
|
|
20
|
+
resolvers: IRuntimeNetworkProtectedAuthorityV1['resolvers'];
|
|
21
|
+
/** Without the VPN relay, which the composed authority adds as a UDP platform endpoint. */
|
|
22
|
+
platformEndpoints: IRuntimeNetworkProtectedAuthorityV1['platformEndpoints'];
|
|
23
|
+
vpn: {
|
|
24
|
+
/** The private prefix VPN control addresses are allocated from. */
|
|
25
|
+
controlPrefix: string;
|
|
26
|
+
/** The endpoint every node dials for the managed VPN. */
|
|
27
|
+
relay: IGetRuntimeManagedVpnCredentialResponseV1['relay'];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What an administrator writes; Cloudly supplies the id and the revision. */
|
|
32
|
+
export type TRuntimeNetworkAddressPlanWritableData = Omit<IRuntimeNetworkAddressPlan, 'id' | 'revision'>;
|
|
33
|
+
|
|
34
|
+
/** What a protection producer fixes around a plan: the authority's identity, revision chain and egress. */
|
|
35
|
+
export type TRuntimeNetworkProtectedAuthorityFrame = Pick<IRuntimeNetworkProtectedAuthorityV1,
|
|
36
|
+
'id' | 'generation' | 'controller' | 'previous' | 'egressAuthorities'>;
|
|
37
|
+
|
|
38
|
+
/** Refusals the pure change check decides from the two plans alone. */
|
|
39
|
+
export type TRuntimeNetworkAddressPlanChangeRefusal = 'plan-vpn-immutable' | 'plan-pool-removed' | 'plan-prefix-shrunk';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Every refusal of a plan write. Cloudly adds the two that need live references: a resolver or
|
|
43
|
+
* platform endpoint the change removes or changes while a router selection or a service still names it.
|
|
44
|
+
*/
|
|
45
|
+
export type TRuntimeNetworkAddressPlanRefusal =
|
|
46
|
+
| TRuntimeNetworkAddressPlanChangeRefusal | 'plan-resolver-referenced' | 'plan-endpoint-referenced';
|
|
47
|
+
|
|
48
|
+
export type TRuntimeNetworkAddressPlanChange =
|
|
49
|
+
| { status: 'unchanged' }
|
|
50
|
+
/** The ids the change removes or changes, which Cloudly checks against live references before it writes. */
|
|
51
|
+
| { status: 'accepted'; removed: { resolverIds: string[]; platformEndpointIds: string[] } }
|
|
52
|
+
| { status: 'refused'; reason: TRuntimeNetworkAddressPlanChangeRefusal };
|
|
53
|
+
|
|
54
|
+
export const runtimeNetworkAddressPlanContract = Object.freeze({
|
|
55
|
+
id: 'address-plan',
|
|
56
|
+
/**
|
|
57
|
+
* SmartVPN needs its gateway and at least one member inside the control prefix. No minimum length
|
|
58
|
+
* is stated: the private-range rule already keeps the control prefix at /8 or longer.
|
|
59
|
+
*/
|
|
60
|
+
maximumControlPrefixLength: 30,
|
|
61
|
+
relayProtocol: 'udp',
|
|
62
|
+
} as const);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The structural authority validator reads a frame's shape and nothing else, and a plan fixes none of
|
|
66
|
+
* the frame, so every valid frame decides a plan the same way. The digest is a well-formed
|
|
67
|
+
* placeholder: this candidate is never hashed or stored.
|
|
68
|
+
*/
|
|
69
|
+
const validationFrame: TRuntimeNetworkProtectedAuthorityFrame = {
|
|
70
|
+
id: 'address-plan', generation: 1, controller: { kind: 'cloudly', id: 'address-plan', epoch: 'address-plan' },
|
|
71
|
+
previous: null, egressAuthorities: [{ nodeId: 'address-plan', runtimeNamespace: 'address-plan' }],
|
|
72
|
+
};
|
|
73
|
+
const unsealedDigest = `sha256:${'0'.repeat(64)}` as TSha256Digest;
|
|
74
|
+
|
|
75
|
+
type TAuthoritySets = Pick<IRuntimeNetworkProtectedAuthorityV1, 'prefixes' | 'pools' | 'resolvers' | 'platformEndpoints'>;
|
|
76
|
+
|
|
77
|
+
/** Unvalidated sets and frame in, the protected authority validator's verdict out. */
|
|
78
|
+
const authorityOf = (
|
|
79
|
+
sets: Record<keyof TAuthoritySets, unknown>, frame: Record<keyof TRuntimeNetworkProtectedAuthorityFrame, unknown>,
|
|
80
|
+
): IRuntimeNetworkProtectedAuthorityV1 => snapshotRuntimeNetworkProtectedAuthorityV1({ schemaVersion: 1, id: frame.id,
|
|
81
|
+
generation: frame.generation, controller: frame.controller, previous: frame.previous, egressAuthorities: frame.egressAuthorities,
|
|
82
|
+
prefixes: sets.prefixes, pools: sets.pools, resolvers: sets.resolvers, platformEndpoints: sets.platformEndpoints, digest: unsealedDigest });
|
|
83
|
+
|
|
84
|
+
const setsWithRelay = (plan: IRuntimeNetworkAddressPlan): TAuthoritySets => ({
|
|
85
|
+
prefixes: plan.prefixes, pools: plan.pools, resolvers: plan.resolvers,
|
|
86
|
+
platformEndpoints: [...plan.platformEndpoints, { ...plan.vpn.relay, protocol: runtimeNetworkAddressPlanContract.relayProtocol }]
|
|
87
|
+
.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const vpnOf = (value: unknown): IRuntimeNetworkAddressPlan['vpn'] => {
|
|
91
|
+
const item = networkRecord(value, ['controlPrefix', 'relay']);
|
|
92
|
+
const relay = networkRecord(item.relay, ['id', 'address', 'port']);
|
|
93
|
+
const controlPrefix = networkPrivatePrefix(item.controlPrefix);
|
|
94
|
+
requireNetwork(Number(controlPrefix.split('/')[1]) <= runtimeNetworkAddressPlanContract.maximumControlPrefixLength);
|
|
95
|
+
return { controlPrefix, relay: { id: networkId(relay.id), address: networkIpv4(relay.address), port: networkCounter(relay.port, 65535) } };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** The control prefix is protected, and no pool, resolver or platform endpoint, the relay included, sits inside it. */
|
|
99
|
+
const requireVpnProtected = (authority: IRuntimeNetworkProtectedAuthorityV1, controlPrefix: string): void => {
|
|
100
|
+
requireNetwork(authority.prefixes.some(prefix => networkContains(prefix, controlPrefix))
|
|
101
|
+
&& authority.pools.every(pool => !networkOverlaps(pool.prefix, controlPrefix))
|
|
102
|
+
&& [...authority.resolvers, ...authority.platformEndpoints].every(endpoint => !networkContains(controlPrefix, `${endpoint.address}/32`)));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Structural snapshot for exact persistence and for a plan write. A plan passes only when both
|
|
107
|
+
* authorities it can describe pass the protected authority's own validator: its sets alone, and
|
|
108
|
+
* its sets with the relay added, so no plan can compose an authority that contract refuses.
|
|
109
|
+
*/
|
|
110
|
+
export const snapshotRuntimeNetworkAddressPlan = (value: unknown): IRuntimeNetworkAddressPlan => networkSafe(() => {
|
|
111
|
+
const item = networkRecord(captureNetwork(value), ['id', 'revision', 'prefixes', 'pools', 'resolvers', 'platformEndpoints', 'vpn']);
|
|
112
|
+
requireNetwork(item.id === runtimeNetworkAddressPlanContract.id);
|
|
113
|
+
const own = authorityOf({ prefixes: item.prefixes, pools: item.pools, resolvers: item.resolvers,
|
|
114
|
+
platformEndpoints: item.platformEndpoints }, validationFrame);
|
|
115
|
+
const plan: IRuntimeNetworkAddressPlan = { id: runtimeNetworkAddressPlanContract.id, revision: networkCounter(item.revision),
|
|
116
|
+
prefixes: own.prefixes, pools: own.pools, resolvers: own.resolvers, platformEndpoints: own.platformEndpoints, vpn: vpnOf(item.vpn) };
|
|
117
|
+
requireVpnProtected(authorityOf(setsWithRelay(plan), validationFrame), plan.vpn.controlPrefix);
|
|
118
|
+
return plan;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The protected authority a plan describes inside a producer's frame, digest included: the plan's
|
|
123
|
+
* sets with the VPN relay as the UDP platform endpoint every node dials. Both inputs are read before
|
|
124
|
+
* the digest is computed.
|
|
125
|
+
*/
|
|
126
|
+
export const composeRuntimeNetworkProtectedAuthority = async (
|
|
127
|
+
planValue: unknown, frameValue: unknown,
|
|
128
|
+
): Promise<IRuntimeNetworkProtectedAuthorityV1> => {
|
|
129
|
+
try {
|
|
130
|
+
const plan = snapshotRuntimeNetworkAddressPlan(planValue);
|
|
131
|
+
const frame = networkRecord(captureNetwork(frameValue), ['id', 'generation', 'controller', 'previous', 'egressAuthorities']);
|
|
132
|
+
const authority = authorityOf(setsWithRelay(plan), frame);
|
|
133
|
+
return { ...authority, digest: await computeRuntimeNetworkProtectedAuthorityV1Digest(authority) };
|
|
134
|
+
} catch { return failNetwork(); }
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Whether the union of `prefixes`, which never overlap one another, covers every address of `prefix`. */
|
|
138
|
+
const coveredBy = (prefixes: string[], prefix: string): boolean => {
|
|
139
|
+
const [first, last] = networkPrefixBounds(prefix);
|
|
140
|
+
const covered = prefixes.reduce((sum, candidate) => {
|
|
141
|
+
const [low, high] = networkPrefixBounds(candidate);
|
|
142
|
+
return sum + Math.max(0, Math.min(last, high) - Math.max(first, low) + 1);
|
|
143
|
+
}, 0);
|
|
144
|
+
return covered === last - first + 1;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The entries of `previous` that `next` does not hold exactly as they were. An entry changed under its
|
|
149
|
+
* old id counts: whatever still names that id would now reach something else.
|
|
150
|
+
*/
|
|
151
|
+
const removedOrChanged = <T>(previous: T[], next: T[]): T[] =>
|
|
152
|
+
previous.filter(item => !next.some(candidate => networkEqual(candidate, item)));
|
|
153
|
+
|
|
154
|
+
const idsOf = (entries: Array<{ id: string }>): string[] => entries.map(entry => entry.id);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The pure decision for replacing `previousValue`, the stored plan or null, with `nextValue` at the
|
|
158
|
+
* following revision. Leases and the managed VPN outlive a plan revision, so every pool stays exactly
|
|
159
|
+
* as it was, the protected address space only grows, and the VPN never changes. Resolvers and platform
|
|
160
|
+
* endpoints may go or change; `accepted` names every one that did. Equal content is `unchanged`:
|
|
161
|
+
* Cloudly writes nothing and the stored revision stands.
|
|
162
|
+
*/
|
|
163
|
+
export const admitRuntimeNetworkAddressPlanChange = (previousValue: unknown | null, nextValue: unknown): TRuntimeNetworkAddressPlanChange =>
|
|
164
|
+
networkSafe(() => {
|
|
165
|
+
const previous = previousValue === null ? null : snapshotRuntimeNetworkAddressPlan(previousValue);
|
|
166
|
+
const next = snapshotRuntimeNetworkAddressPlan(nextValue);
|
|
167
|
+
requireNetwork(next.revision === (previous?.revision ?? 0) + 1);
|
|
168
|
+
if (previous === null) return { status: 'accepted', removed: { resolverIds: [], platformEndpointIds: [] } };
|
|
169
|
+
if (networkEqual({ ...previous, revision: next.revision }, next)) return { status: 'unchanged' };
|
|
170
|
+
if (!networkEqual(previous.vpn, next.vpn)) return { status: 'refused', reason: 'plan-vpn-immutable' };
|
|
171
|
+
if (removedOrChanged(previous.pools, next.pools).length > 0) return { status: 'refused', reason: 'plan-pool-removed' };
|
|
172
|
+
if (!previous.prefixes.every(prefix => coveredBy(next.prefixes, prefix))) return { status: 'refused', reason: 'plan-prefix-shrunk' };
|
|
173
|
+
return { status: 'accepted', removed: { resolverIds: idsOf(removedOrChanged(previous.resolvers, next.resolvers)),
|
|
174
|
+
platformEndpointIds: idsOf(removedOrChanged(previous.platformEndpoints, next.platformEndpoints)) } };
|
|
175
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TSha256Digest } from './immutableimage.js';
|
|
2
2
|
import type { IRuntimeAuthorityReferenceV1, IRuntimeControllerV1 } from './runtimeassignment.js';
|
|
3
3
|
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
|
|
4
|
+
import { networkPrefix, networkPrivatePrefix } from '../private/networkcontract.js';
|
|
4
5
|
|
|
5
6
|
/** Controller declaration; a digest does not authenticate completeness or activate a pool. */
|
|
6
7
|
export interface IRuntimeNetworkProtectedAuthorityV1 {
|
|
@@ -129,14 +130,6 @@ const endpointAddress = (value: unknown): string => {
|
|
|
129
130
|
requireValue(first > 0 && first < 224 && first !== 127);
|
|
130
131
|
return result;
|
|
131
132
|
};
|
|
132
|
-
const prefix = (value: unknown): string => {
|
|
133
|
-
requireValue(typeof value === 'string' && value.length <= 18);
|
|
134
|
-
const parts = value.split('/');
|
|
135
|
-
requireValue(parts.length === 2 && /^(?:[0-9]|[12][0-9]|3[0-2])$/.test(parts[1]));
|
|
136
|
-
const base = numericAddress(address(parts[0])), width = 2 ** (32 - Number(parts[1]));
|
|
137
|
-
requireValue(base % width === 0);
|
|
138
|
-
return value;
|
|
139
|
-
};
|
|
140
133
|
const bounds = (value: string): [number, number] => {
|
|
141
134
|
const [base, bits] = value.split('/'), first = numericAddress(base);
|
|
142
135
|
return [first, first + 2 ** (32 - Number(bits)) - 1];
|
|
@@ -149,11 +142,6 @@ const overlaps = (left: string, right: string): boolean => {
|
|
|
149
142
|
const [a, b] = bounds(left), [c, d] = bounds(right);
|
|
150
143
|
return a <= d && c <= b;
|
|
151
144
|
};
|
|
152
|
-
const privatePrefix = (value: unknown): string => {
|
|
153
|
-
const result = prefix(value);
|
|
154
|
-
requireValue(['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'].some(range => contains(range, result)));
|
|
155
|
-
return result;
|
|
156
|
-
};
|
|
157
145
|
const nonoverlapping = (values: string[]): void => {
|
|
158
146
|
for (let index = 0; index < values.length; index++) for (let other = 0; other < index; other++) {
|
|
159
147
|
requireValue(!overlaps(values[index], values[other]));
|
|
@@ -170,7 +158,7 @@ export const snapshotRuntimeNetworkProtectedAuthorityV1 = (value: unknown): IRun
|
|
|
170
158
|
const identity = id(item.id), generation = integer(item.generation);
|
|
171
159
|
const previous = item.previous === null ? null : reference(item.previous);
|
|
172
160
|
requireValue(generation === 1 ? previous === null : previous?.id === identity && previous.generation === generation - 1);
|
|
173
|
-
const prefixes = sorted(list(item.prefixes, runtimeNetworkAuthorityV1Contract.maximumPrefixes,
|
|
161
|
+
const prefixes = sorted(list(item.prefixes, runtimeNetworkAuthorityV1Contract.maximumPrefixes, networkPrefix), value => value);
|
|
174
162
|
requireValue(prefixes.length > 0);
|
|
175
163
|
nonoverlapping(prefixes);
|
|
176
164
|
const protectedPrefix = (value: string) => requireValue(prefixes.some(range => contains(range, value)));
|
|
@@ -178,7 +166,7 @@ export const snapshotRuntimeNetworkProtectedAuthorityV1 = (value: unknown): IRun
|
|
|
178
166
|
const pool = record(value, ['id', 'purpose', 'prefix']);
|
|
179
167
|
requireValue(pool.purpose === 'workload' || pool.purpose === 'transit');
|
|
180
168
|
const result: IRuntimeNetworkProtectedAuthorityV1['pools'][number] = {
|
|
181
|
-
id: id(pool.id), purpose: pool.purpose, prefix:
|
|
169
|
+
id: id(pool.id), purpose: pool.purpose, prefix: networkPrivatePrefix(pool.prefix),
|
|
182
170
|
};
|
|
183
171
|
protectedPrefix(result.prefix);
|
|
184
172
|
return result;
|
|
@@ -212,7 +200,7 @@ export const snapshotRuntimeNetworkHandoffLeaseV1 = (value: unknown): IRuntimeNe
|
|
|
212
200
|
'protectedAuthority', 'routerIncarnationId', 'transit', 'sourcePortRanges', 'conntrackZone', 'conntrackLabel', 'digest']);
|
|
213
201
|
requireValue(item.schemaVersion === 1);
|
|
214
202
|
const transit = record(item.transit, ['poolId', 'prefix', 'routerAddress', 'hostAddress']);
|
|
215
|
-
const transitPrefix =
|
|
203
|
+
const transitPrefix = networkPrivatePrefix(transit.prefix), [first, last] = bounds(transitPrefix);
|
|
216
204
|
const routerAddress = endpointAddress(transit.routerAddress), hostAddress = endpointAddress(transit.hostAddress);
|
|
217
205
|
requireValue(first !== last && routerAddress !== hostAddress);
|
|
218
206
|
for (const value of [routerAddress, hostAddress]) {
|