@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.
- package/changelog.md +19 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.manageddockerforwarding.js +3 -3
- package/dist_ts/classes.managednftables.d.ts +5 -0
- package/dist_ts/classes.managednftables.js +13 -6
- package/dist_ts/managed.egress.types.d.ts +53 -1
- package/package.json +7 -7
- package/readme.md +153 -4
- package/rust/src/egress.compile.rs +49 -1
- package/rust/src/egress.host.rs +22 -0
- package/rust/src/egress.hostgrant.rs +187 -0
- package/rust/src/egress.poolguard.rs +7 -0
- package/rust/src/egress.router.rs +189 -3
- package/rust/src/egress.rs +244 -1
- package/rust/src/egress_hostgrant_tests.rs +948 -0
- package/rust/src/egress_tests.rs +414 -0
- package/rust/src/main.rs +6 -2
- package/rust/src/owner.rs +116 -4
- package/rust/src/owner_egress_tests.rs +1 -1
- package/rust/src/owner_egress_traffic_tests.rs +3 -3
- package/rust/src/owner_host_traffic_tests.rs +4 -4
- package/rust/src/owner_hostgrant_traffic_tests.rs +280 -0
- package/rust/src/owner_identity_tests.rs +125 -1
- package/rust/src/owner_poolguard_tests.rs +69 -0
- package/rust/src/owner_router_traffic_tests.rs +393 -0
- package/rust/src/owner_tests.rs +6 -0
- package/rust/src/policy.rs +1 -1
- package/rust/src/wire.rs +15 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +2 -2
- package/ts/classes.managednftables.ts +13 -5
- package/ts/managed.egress.types.ts +55 -1
package/rust/src/egress.rs
CHANGED
|
@@ -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;
|