@push.rocks/smartnftables 2.3.0 → 2.5.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.
Files changed (36) hide show
  1. package/changelog.md +19 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/classes.manageddockerforwarding.js +3 -3
  8. package/dist_ts/classes.managednftables.d.ts +5 -0
  9. package/dist_ts/classes.managednftables.js +13 -6
  10. package/dist_ts/managed.egress.types.d.ts +53 -1
  11. package/package.json +7 -7
  12. package/readme.md +153 -4
  13. package/rust/src/egress.compile.rs +49 -1
  14. package/rust/src/egress.host.rs +22 -0
  15. package/rust/src/egress.hostgrant.rs +187 -0
  16. package/rust/src/egress.poolguard.rs +7 -0
  17. package/rust/src/egress.router.rs +189 -3
  18. package/rust/src/egress.rs +244 -1
  19. package/rust/src/egress_hostgrant_tests.rs +948 -0
  20. package/rust/src/egress_tests.rs +414 -0
  21. package/rust/src/main.rs +6 -2
  22. package/rust/src/owner.rs +116 -4
  23. package/rust/src/owner_egress_tests.rs +1 -1
  24. package/rust/src/owner_egress_traffic_tests.rs +3 -3
  25. package/rust/src/owner_host_traffic_tests.rs +4 -4
  26. package/rust/src/owner_hostgrant_traffic_tests.rs +280 -0
  27. package/rust/src/owner_identity_tests.rs +125 -1
  28. package/rust/src/owner_poolguard_tests.rs +69 -0
  29. package/rust/src/owner_router_traffic_tests.rs +393 -0
  30. package/rust/src/owner_tests.rs +6 -0
  31. package/rust/src/policy.rs +1 -1
  32. package/rust/src/wire.rs +15 -1
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/classes.manageddockerforwarding.ts +2 -2
  35. package/ts/classes.managednftables.ts +13 -5
  36. package/ts/managed.egress.types.ts +55 -1
@@ -94,6 +94,27 @@ pub struct Generation {
94
94
  pub conntrack_label: String,
95
95
  pub grants: Vec<Grant>,
96
96
  }
97
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
98
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
99
+ pub struct RouterPublishedPort {
100
+ pub protocol: String,
101
+ pub transit_port: u16,
102
+ pub endpoint_port: u16,
103
+ pub endpoint_address: String,
104
+ pub transit_source_address: String,
105
+ }
106
+ /// One exact host-origin flow: the node's own host network namespace dials one
107
+ /// port of one local workload at its lease address, from the transit host
108
+ /// address of the current handoff. The same grant is compiled in every scope the
109
+ /// flow crosses; each scope binds it to the authority that scope proves.
110
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
111
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
112
+ pub struct HostGrant {
113
+ pub protocol: String,
114
+ pub source_address: String,
115
+ pub destination_address: String,
116
+ pub destination_port: u16,
117
+ }
97
118
  #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
98
119
  #[serde(rename_all = "camelCase", deny_unknown_fields)]
99
120
  pub struct RouterScope {
@@ -103,6 +124,13 @@ pub struct RouterScope {
103
124
  pub handoff: LocalLink,
104
125
  pub protection: Protection,
105
126
  pub generations: Vec<Generation>,
127
+ /// Absent and empty are the same canonical policy, so an unpublished scope
128
+ /// keeps its exact previous digest and compiled graph.
129
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
130
+ pub published_ports: Vec<RouterPublishedPort>,
131
+ /// Absent and empty are the same canonical policy, like publications.
132
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
133
+ pub host_grants: Vec<HostGrant>,
106
134
  }
107
135
  #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
108
136
  #[serde(rename_all = "camelCase", deny_unknown_fields)]
@@ -130,12 +158,19 @@ pub struct HostScope {
130
158
  /// keeps its exact previous digest and compiled graph.
131
159
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
132
160
  pub published_ports: Vec<PublishedPort>,
161
+ /// Absent and empty are the same canonical policy, like publications.
162
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
163
+ pub host_grants: Vec<HostGrant>,
133
164
  }
134
165
  #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
135
166
  #[serde(rename_all = "camelCase", deny_unknown_fields)]
136
167
  pub struct AllocationPoolGuardScope {
137
168
  pub authority_digest: String,
138
169
  pub prefixes: Vec<String>,
170
+ /// The only exceptions to the guard: exact host-origin flows into a pool.
171
+ /// Absent and empty are the same canonical policy.
172
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
173
+ pub host_grants: Vec<HostGrant>,
139
174
  }
140
175
  #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
141
176
  #[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
@@ -380,6 +415,156 @@ pub fn destination(grant: &Grant, protection: &Protection) -> Result<String> {
380
415
  }
381
416
  }
382
417
  }
418
+ /// Inbound publication is bounded by the authority this scope already proves: an
419
+ /// exact leased transit source address present on the handoff, an exact current
420
+ /// workload endpoint address behind one of this scope's veth links, and no overlap
421
+ /// with the leased outbound source ports translated to that same transit address.
422
+ fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
423
+ let mut ports = std::mem::take(&mut value.published_ports);
424
+ require(ports.len() <= 64)?;
425
+ ports.sort();
426
+ for (index, port) in ports.iter().enumerate() {
427
+ require(
428
+ protocol(&port.protocol)
429
+ && port.transit_port > 0
430
+ && port.endpoint_port > 0
431
+ && (index == 0
432
+ || ports[index - 1].protocol != port.protocol
433
+ || ports[index - 1].transit_port != port.transit_port),
434
+ )?;
435
+ // rtnetlink verifies this exact handoff address at apply, recovery and
436
+ // inspection, and only a current leased transit source address receives a
437
+ // publication; that binding is what the host publication already targets.
438
+ require(
439
+ value
440
+ .handoff
441
+ .required_ipv4_addresses
442
+ .contains(&port.transit_source_address)
443
+ && value.generations.iter().any(|generation| {
444
+ generation.transit_source_address == port.transit_source_address
445
+ }),
446
+ )?;
447
+ unicast(&port.endpoint_address)?;
448
+ // Endpoint source prefixes are canonical and pairwise disjoint, so at most
449
+ // one endpoint can hold this address. It must be an actual workload veth
450
+ // behind this scope, never a router-local address, a gateway, the transit
451
+ // address or a protected platform endpoint.
452
+ let target = format!("{}/32", port.endpoint_address);
453
+ let endpoint = value
454
+ .endpoints
455
+ .iter()
456
+ .find(|endpoint| {
457
+ endpoint
458
+ .source_prefixes
459
+ .iter()
460
+ .any(|source| covers(source, &target) == Ok(true))
461
+ })
462
+ .ok_or(Error::Invalid)?;
463
+ require(
464
+ endpoint.interface_kind == "veth"
465
+ && value
466
+ .links
467
+ .iter()
468
+ .chain(std::iter::once(&value.handoff))
469
+ .all(|link| {
470
+ !link
471
+ .required_ipv4_addresses
472
+ .contains(&port.endpoint_address)
473
+ })
474
+ && value
475
+ .protection
476
+ .platform_endpoints
477
+ .iter()
478
+ .all(|item| item.address != port.endpoint_address),
479
+ )?;
480
+ for generation in &value.generations {
481
+ if generation.transit_source_address != port.transit_source_address {
482
+ continue;
483
+ }
484
+ for range in &generation.source_port_ranges {
485
+ // Source NAT translates to this same transit address, so a published
486
+ // port inside a leased outbound range is a translation conflict.
487
+ require(
488
+ range.protocol != port.protocol
489
+ || port.transit_port < range.first
490
+ || port.transit_port > range.last,
491
+ )?;
492
+ }
493
+ }
494
+ }
495
+ value.published_ports = ports;
496
+ Ok(())
497
+ }
498
+ /// Exact tuples only: one protocol, one unicast source and destination address
499
+ /// and one destination port. Addresses never carry a prefix and ports never a
500
+ /// range, so a grant cannot widen past its flow. `bound` checks the authority of
501
+ /// the enclosing scope.
502
+ fn normalize_host_grants(
503
+ grants: &mut [HostGrant],
504
+ bound: impl Fn(&HostGrant) -> Result<()>,
505
+ ) -> Result<()> {
506
+ // The contract maximum per projection; set lookups keep the rule graph
507
+ // constant, and the element budget holds this many in every scope.
508
+ require(grants.len() <= 1024)?;
509
+ grants.sort();
510
+ for (index, grant) in grants.iter().enumerate() {
511
+ require(
512
+ protocol(&grant.protocol)
513
+ && grant.destination_port > 0
514
+ && (index == 0 || grants[index - 1] != *grant),
515
+ )?;
516
+ unicast(&grant.source_address)?;
517
+ unicast(&grant.destination_address)?;
518
+ require(grant.source_address != grant.destination_address)?;
519
+ bound(grant)?;
520
+ }
521
+ Ok(())
522
+ }
523
+ /// The router forwards a host grant from its handoff to one workload: the
524
+ /// destination is an exact current veth endpoint address of this scope, and the
525
+ /// source is protected handoff-side space that is neither router-local, a
526
+ /// platform endpoint nor any workload's address.
527
+ fn normalize_router_host_grants(value: &mut RouterScope) -> Result<()> {
528
+ let mut grants = std::mem::take(&mut value.host_grants);
529
+ let scope = &*value;
530
+ let local = |ip: &str| {
531
+ scope
532
+ .links
533
+ .iter()
534
+ .chain(std::iter::once(&scope.handoff))
535
+ .any(|link| link.required_ipv4_addresses.iter().any(|item| item == ip))
536
+ };
537
+ let platform = |ip: &str| {
538
+ scope
539
+ .protection
540
+ .platform_endpoints
541
+ .iter()
542
+ .any(|endpoint| endpoint.address == ip)
543
+ };
544
+ let workload = |ip: &str| {
545
+ let target = format!("{ip}/32");
546
+ scope.endpoints.iter().find(|endpoint| {
547
+ endpoint
548
+ .source_prefixes
549
+ .iter()
550
+ .any(|source| covers(source, &target) == Ok(true))
551
+ })
552
+ };
553
+ normalize_host_grants(&mut grants, |grant| {
554
+ let endpoint = workload(&grant.destination_address).ok_or(Error::Invalid)?;
555
+ require(
556
+ endpoint.interface_kind == "veth"
557
+ && !local(&grant.destination_address)
558
+ && !platform(&grant.destination_address)
559
+ && covers_address(&scope.protection.prefixes, &grant.source_address)?
560
+ && !local(&grant.source_address)
561
+ && !platform(&grant.source_address)
562
+ && workload(&grant.source_address).is_none(),
563
+ )
564
+ })?;
565
+ value.host_grants = grants;
566
+ Ok(())
567
+ }
383
568
  fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
384
569
  require(value.links.len() <= 32 && value.generations.len() <= 32)?;
385
570
  let private = policy::Policy {
@@ -550,7 +735,9 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
550
735
  item.transit_source_address.as_str(),
551
736
  item.source_port_ranges.as_slice(),
552
737
  )
553
- }))
738
+ }))?;
739
+ normalize_router_published(value)?;
740
+ normalize_router_host_grants(value)
554
741
  }
555
742
  fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
556
743
  require(
@@ -567,6 +754,58 @@ fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
567
754
  require(!overlaps(item, previous)?)?;
568
755
  }
569
756
  }
757
+ // An exception is only ever an exact flow into a guarded pool.
758
+ let mut grants = std::mem::take(&mut value.host_grants);
759
+ normalize_host_grants(&mut grants, |grant| {
760
+ require(covers_address(&value.prefixes, &grant.destination_address)?)
761
+ })?;
762
+ value.host_grants = grants;
763
+ Ok(())
764
+ }
765
+ /// The host dials from an exact current address of exactly one handoff link, into
766
+ /// protected space that is neither host-local, a platform endpoint nor a leased
767
+ /// router transit address: only a workload behind that handoff remains.
768
+ fn normalize_host_host_grants(value: &mut HostScope) -> Result<()> {
769
+ let mut grants = std::mem::take(&mut value.host_grants);
770
+ let scope = &*value;
771
+ normalize_host_grants(&mut grants, |grant| {
772
+ let destination = &grant.destination_address;
773
+ require(
774
+ scope
775
+ .handoffs
776
+ .iter()
777
+ .filter(|handoff| {
778
+ handoff
779
+ .link
780
+ .required_ipv4_addresses
781
+ .contains(&grant.source_address)
782
+ })
783
+ .count()
784
+ == 1
785
+ && !scope
786
+ .uplink
787
+ .required_ipv4_addresses
788
+ .contains(&grant.source_address)
789
+ && covers_address(&scope.protection.prefixes, destination)?
790
+ && scope
791
+ .handoffs
792
+ .iter()
793
+ .map(|handoff| &handoff.link)
794
+ .chain(std::iter::once(&scope.uplink))
795
+ .all(|link| !link.required_ipv4_addresses.contains(destination))
796
+ && scope
797
+ .protection
798
+ .platform_endpoints
799
+ .iter()
800
+ .all(|endpoint| endpoint.address != *destination)
801
+ && scope
802
+ .handoffs
803
+ .iter()
804
+ .flat_map(|handoff| handoff.allocations.iter())
805
+ .all(|allocation| allocation.transit_source_address != *destination),
806
+ )
807
+ })?;
808
+ value.host_grants = grants;
570
809
  Ok(())
571
810
  }
572
811
  /// Inbound publication is bounded by the same authority the scope already proves:
@@ -683,6 +922,7 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
683
922
  }
684
923
  }
685
924
  normalize_published(value)?;
925
+ normalize_host_host_grants(value)?;
686
926
  unique_links(
687
927
  value
688
928
  .handoffs
@@ -705,6 +945,9 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
705
945
  )
706
946
  }
707
947
 
948
+ #[cfg(test)]
949
+ #[path = "egress_hostgrant_tests.rs"]
950
+ pub(crate) mod hostgrant_tests;
708
951
  #[cfg(test)]
709
952
  #[path = "egress_tests.rs"]
710
953
  pub(crate) mod tests;