@push.rocks/smartnftables 2.6.0 → 4.0.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 +33 -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 -4
- package/dist_ts/classes.managednftables.d.ts +14 -2
- package/dist_ts/classes.managednftables.js +48 -8
- package/dist_ts/managed.egress.types.d.ts +25 -3
- package/dist_ts/managed.types.d.ts +14 -0
- package/package.json +7 -7
- package/readme.md +262 -64
- package/rust/src/docker.policy.rs +2 -1
- package/rust/src/egress.compile.rs +153 -76
- package/rust/src/egress.host.rs +24 -51
- package/rust/src/egress.hostgrant.rs +1 -1
- package/rust/src/egress.keys.rs +305 -0
- package/rust/src/egress.leased.rs +399 -0
- package/rust/src/egress.private.rs +197 -0
- package/rust/src/egress.published.rs +370 -0
- package/rust/src/egress.router.rs +61 -294
- package/rust/src/egress.rs +140 -19
- package/rust/src/egress_hostgrant_tests.rs +158 -20
- package/rust/src/egress_localport_tests.rs +11 -2
- package/rust/src/egress_publishedrange_tests.rs +812 -0
- package/rust/src/egress_routerequivalence_tests.rs +448 -0
- package/rust/src/egress_tests.rs +289 -230
- package/rust/src/egress_workloadgrant_tests.rs +21 -13
- package/rust/src/main.rs +57 -3
- package/rust/src/owner.rs +69 -14
- package/rust/src/owner_host_traffic_tests.rs +4 -0
- package/rust/src/owner_identity_tests.rs +166 -80
- package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
- package/rust/src/owner_router_traffic_tests.rs +2 -0
- package/rust/src/owner_scale_traffic_tests.rs +383 -0
- package/rust/src/owner_tests.rs +6 -0
- package/rust/src/policy.rs +30 -29
- package/rust/src/tests.rs +100 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +2 -3
- package/ts/classes.managednftables.ts +46 -8
- package/ts/managed.egress.types.ts +25 -3
- package/ts/managed.types.ts +17 -0
package/rust/src/egress.rs
CHANGED
|
@@ -99,9 +99,28 @@ pub struct Generation {
|
|
|
99
99
|
pub struct RouterPublishedPort {
|
|
100
100
|
pub protocol: String,
|
|
101
101
|
pub transit_port: u16,
|
|
102
|
+
/// Last port of a published range starting at `transit_port`. A range keeps
|
|
103
|
+
/// every port: `endpoint_port` equals `transit_port`. Absent for one port.
|
|
104
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
105
|
+
pub transit_port_end: Option<u16>,
|
|
102
106
|
pub endpoint_port: u16,
|
|
103
107
|
pub endpoint_address: String,
|
|
104
108
|
pub transit_source_address: String,
|
|
109
|
+
/// The workload may open flows from its published port(s), translated to
|
|
110
|
+
/// `transit_source_address` and the published port. Absent and false are the
|
|
111
|
+
/// same canonical policy.
|
|
112
|
+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
113
|
+
pub symmetric: bool,
|
|
114
|
+
}
|
|
115
|
+
impl RouterPublishedPort {
|
|
116
|
+
/// Last published transit port; equal to `transit_port` for one port.
|
|
117
|
+
pub fn transit_last(&self) -> u16 {
|
|
118
|
+
self.transit_port_end.unwrap_or(self.transit_port)
|
|
119
|
+
}
|
|
120
|
+
/// Last workload port the publication translates to.
|
|
121
|
+
pub fn endpoint_last(&self) -> u16 {
|
|
122
|
+
self.endpoint_port + (self.transit_last() - self.transit_port)
|
|
123
|
+
}
|
|
105
124
|
}
|
|
106
125
|
/// One exact host-origin flow: the node's own host network namespace dials one
|
|
107
126
|
/// port of one local workload at its lease address, from the transit host
|
|
@@ -150,9 +169,28 @@ pub struct HostHandoff {
|
|
|
150
169
|
pub struct PublishedPort {
|
|
151
170
|
pub protocol: String,
|
|
152
171
|
pub host_port: u16,
|
|
172
|
+
/// Last port of a published range starting at `host_port`. A range keeps
|
|
173
|
+
/// every port: `target_port` equals `host_port`. Absent for one port.
|
|
174
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
175
|
+
pub host_port_end: Option<u16>,
|
|
153
176
|
pub target_port: u16,
|
|
154
177
|
pub target_address: String,
|
|
155
178
|
pub host_ip: String,
|
|
179
|
+
/// Flows from `target_address` and the published target port(s) may leave
|
|
180
|
+
/// through the uplink, translated to `host_ip` and the published port. Absent
|
|
181
|
+
/// and false are the same canonical policy.
|
|
182
|
+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
183
|
+
pub symmetric: bool,
|
|
184
|
+
}
|
|
185
|
+
impl PublishedPort {
|
|
186
|
+
/// Last published uplink port; equal to `host_port` for one port.
|
|
187
|
+
pub fn host_last(&self) -> u16 {
|
|
188
|
+
self.host_port_end.unwrap_or(self.host_port)
|
|
189
|
+
}
|
|
190
|
+
/// Last transit port the publication translates to.
|
|
191
|
+
pub fn target_last(&self) -> u16 {
|
|
192
|
+
self.target_port + (self.host_last() - self.host_port)
|
|
193
|
+
}
|
|
156
194
|
}
|
|
157
195
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
158
196
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -446,16 +484,19 @@ pub fn destination(grant: &Grant, protection: &Protection) -> Result<String> {
|
|
|
446
484
|
/// with the leased outbound source ports translated to that same transit address.
|
|
447
485
|
fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
|
|
448
486
|
let mut ports = std::mem::take(&mut value.published_ports);
|
|
449
|
-
|
|
487
|
+
crate::capacity("publishedPorts", PUBLISHED_PORTS, ports.len())?;
|
|
450
488
|
ports.sort();
|
|
451
489
|
for (index, port) in ports.iter().enumerate() {
|
|
452
490
|
require(
|
|
453
491
|
protocol(&port.protocol)
|
|
454
492
|
&& port.transit_port > 0
|
|
455
493
|
&& port.endpoint_port > 0
|
|
494
|
+
&& published_span(port.transit_port, port.transit_port_end, port.endpoint_port)
|
|
495
|
+
// Sorted by protocol and first port, so disjoint neighbours are
|
|
496
|
+
// disjoint spans: one protocol/port is published exactly once.
|
|
456
497
|
&& (index == 0
|
|
457
498
|
|| ports[index - 1].protocol != port.protocol
|
|
458
|
-
|| ports[index - 1].
|
|
499
|
+
|| ports[index - 1].transit_last() < port.transit_port),
|
|
459
500
|
)?;
|
|
460
501
|
// rtnetlink verifies this exact handoff address at apply, recovery and
|
|
461
502
|
// inspection, and only a current leased transit source address receives a
|
|
@@ -511,26 +552,59 @@ fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
|
|
|
511
552
|
// port inside a leased outbound range is a translation conflict.
|
|
512
553
|
require(
|
|
513
554
|
range.protocol != port.protocol
|
|
514
|
-
|| port.
|
|
555
|
+
|| port.transit_last() < range.first
|
|
515
556
|
|| port.transit_port > range.last,
|
|
516
557
|
)?;
|
|
517
558
|
}
|
|
518
559
|
}
|
|
519
560
|
}
|
|
561
|
+
// A symmetric publication translates its workload's own flows back to its
|
|
562
|
+
// published ports, so its workload ports belong to it alone.
|
|
563
|
+
exclusive_symmetric(ports.iter().map(|port| {
|
|
564
|
+
(
|
|
565
|
+
port.symmetric,
|
|
566
|
+
(&port.protocol, &port.endpoint_address),
|
|
567
|
+
(port.endpoint_port, port.endpoint_last()),
|
|
568
|
+
)
|
|
569
|
+
}))?;
|
|
520
570
|
value.published_ports = ports;
|
|
521
571
|
Ok(())
|
|
522
572
|
}
|
|
573
|
+
/// One published port, or a range `first..=end` that keeps every port. The range
|
|
574
|
+
/// is strictly ascending: one port has exactly one canonical form, without an end.
|
|
575
|
+
fn published_span(first: u16, end: Option<u16>, inside: u16) -> bool {
|
|
576
|
+
end.is_none_or(|end| end > first && inside == first)
|
|
577
|
+
}
|
|
578
|
+
/// A symmetric publication's inside span may not overlap the inside span of any
|
|
579
|
+
/// other publication of the same protocol and address, or its workload's flows
|
|
580
|
+
/// would have two published sources.
|
|
581
|
+
fn exclusive_symmetric<'a>(
|
|
582
|
+
spans: impl Iterator<Item = (bool, (&'a String, &'a String), (u16, u16))> + Clone,
|
|
583
|
+
) -> Result<()> {
|
|
584
|
+
for (index, (symmetric, key, (first, last))) in spans.clone().enumerate() {
|
|
585
|
+
if !symmetric {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
for (other, (_, other_key, (other_first, other_last))) in spans.clone().enumerate() {
|
|
589
|
+
require(
|
|
590
|
+
other == index || other_key != key || last < other_first || other_last < first,
|
|
591
|
+
)?;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
Ok(())
|
|
595
|
+
}
|
|
523
596
|
/// Exact tuples only: one protocol, one unicast source and destination address
|
|
524
597
|
/// and one destination port. Addresses never carry a prefix and ports never a
|
|
525
598
|
/// range, so a grant cannot widen past its flow. `bound` checks the authority of
|
|
526
599
|
/// the enclosing scope.
|
|
527
600
|
fn normalize_host_grants(
|
|
528
601
|
grants: &mut [HostGrant],
|
|
602
|
+
name: &'static str,
|
|
529
603
|
bound: impl Fn(&HostGrant) -> Result<()>,
|
|
530
604
|
) -> Result<()> {
|
|
531
605
|
// The contract maximum per projection; set lookups keep the rule graph
|
|
532
606
|
// constant, and the element budget holds this many in every scope.
|
|
533
|
-
|
|
607
|
+
crate::capacity(name, HOST_GRANTS, grants.len())?;
|
|
534
608
|
grants.sort();
|
|
535
609
|
for (index, grant) in grants.iter().enumerate() {
|
|
536
610
|
require(
|
|
@@ -575,7 +649,7 @@ fn normalize_router_host_grants(value: &mut RouterScope) -> Result<()> {
|
|
|
575
649
|
.any(|source| covers(source, &target) == Ok(true))
|
|
576
650
|
})
|
|
577
651
|
};
|
|
578
|
-
normalize_host_grants(&mut grants, |grant| {
|
|
652
|
+
normalize_host_grants(&mut grants, "hostGrants", |grant| {
|
|
579
653
|
let endpoint = workload(&grant.destination_address).ok_or(Error::Invalid)?;
|
|
580
654
|
require(
|
|
581
655
|
endpoint.interface_kind == "veth"
|
|
@@ -622,7 +696,7 @@ fn normalize_router_workload_grants(value: &mut RouterScope) -> Result<()> {
|
|
|
622
696
|
require(endpoint.interface_kind == "veth" && !local && !platform)?;
|
|
623
697
|
Ok(endpoint)
|
|
624
698
|
};
|
|
625
|
-
normalize_host_grants(&mut grants, |grant| {
|
|
699
|
+
normalize_host_grants(&mut grants, "workloadGrants", |grant| {
|
|
626
700
|
let source = workload(&grant.source_address)?;
|
|
627
701
|
let destination = workload(&grant.destination_address)?;
|
|
628
702
|
require(source.id != destination.id)
|
|
@@ -630,17 +704,28 @@ fn normalize_router_workload_grants(value: &mut RouterScope) -> Result<()> {
|
|
|
630
704
|
value.workload_grants = grants;
|
|
631
705
|
Ok(())
|
|
632
706
|
}
|
|
707
|
+
/// Router input bounds. Endpoints, rules and grants are set elements, so the
|
|
708
|
+
/// graph budgets bind them, not the rule reserve: the node shape of 128
|
|
709
|
+
/// workloads with local DNS, public egress and four platform endpoints each fits.
|
|
710
|
+
pub const ROUTER_LINKS: usize = 128;
|
|
711
|
+
pub const ROUTER_RULES: usize = 1024;
|
|
712
|
+
pub const ROUTER_GRANTS: usize = 1024;
|
|
713
|
+
/// Publications per scope; they are set elements on both hops.
|
|
714
|
+
pub const PUBLISHED_PORTS: usize = 1024;
|
|
715
|
+
/// Host grants per scope and workload grants per router, each a set element.
|
|
716
|
+
pub const HOST_GRANTS: usize = 1024;
|
|
633
717
|
fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
634
|
-
|
|
718
|
+
crate::capacity("links", ROUTER_LINKS, value.links.len())?;
|
|
719
|
+
require(value.generations.len() <= 32)?;
|
|
635
720
|
let private = policy::Policy {
|
|
636
721
|
schema_version: 1,
|
|
637
722
|
revision,
|
|
638
723
|
endpoints: value.endpoints.clone(),
|
|
639
724
|
rules: value.rules.clone(),
|
|
640
725
|
}
|
|
641
|
-
.
|
|
642
|
-
value.endpoints = private.
|
|
643
|
-
value.rules = private.
|
|
726
|
+
.normalize(ROUTER_LINKS, ROUTER_RULES)?;
|
|
727
|
+
value.endpoints = private.endpoints;
|
|
728
|
+
value.rules = private.rules;
|
|
644
729
|
normalize_protection(&mut value.protection)?;
|
|
645
730
|
normalize_link(&mut value.handoff)?;
|
|
646
731
|
require(
|
|
@@ -698,7 +783,11 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
698
783
|
let mut zones = BTreeSet::new();
|
|
699
784
|
let mut labels = BTreeSet::new();
|
|
700
785
|
let mut classifiers: Vec<(&Grant, String)> = Vec::new();
|
|
701
|
-
|
|
786
|
+
crate::capacity(
|
|
787
|
+
"grants",
|
|
788
|
+
ROUTER_GRANTS,
|
|
789
|
+
value.generations.iter().map(|item| item.grants.len()).sum(),
|
|
790
|
+
)?;
|
|
702
791
|
value
|
|
703
792
|
.generations
|
|
704
793
|
.sort_by(|a, b| (&a.lease.id, a.lease.generation).cmp(&(&b.lease.id, b.lease.generation)));
|
|
@@ -724,8 +813,6 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
724
813
|
&& generation.conntrack_label.bytes().any(|item| item != b'0')
|
|
725
814
|
&& labels.insert(generation.conntrack_label.clone()),
|
|
726
815
|
)?;
|
|
727
|
-
count += generation.grants.len();
|
|
728
|
-
require(count <= 128)?;
|
|
729
816
|
generation.grants.sort();
|
|
730
817
|
for grant in &generation.grants {
|
|
731
818
|
require(
|
|
@@ -822,7 +909,7 @@ fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
|
822
909
|
}
|
|
823
910
|
// An exception is only ever an exact flow into a guarded pool.
|
|
824
911
|
let mut grants = std::mem::take(&mut value.host_grants);
|
|
825
|
-
normalize_host_grants(&mut grants, |grant| {
|
|
912
|
+
normalize_host_grants(&mut grants, "hostGrants", |grant| {
|
|
826
913
|
require(covers_address(&value.prefixes, &grant.destination_address)?)
|
|
827
914
|
})?;
|
|
828
915
|
value.host_grants = grants;
|
|
@@ -832,7 +919,7 @@ fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
|
832
919
|
/// wildcard or range, each with exactly one owning uid. `(uid_t)-1` is not a
|
|
833
920
|
/// user; it is the kernel's "unchanged" sentinel.
|
|
834
921
|
fn normalize_local_tcp_port_owners(owners: &mut [LocalTcpPortOwner]) -> Result<()> {
|
|
835
|
-
|
|
922
|
+
crate::capacity("localTcpPortOwners", LOCAL_TCP_PORT_OWNERS, owners.len())?;
|
|
836
923
|
owners.sort();
|
|
837
924
|
for (index, owner) in owners.iter().enumerate() {
|
|
838
925
|
let ip = address(&owner.address)?;
|
|
@@ -858,7 +945,7 @@ const LOCAL_TCP_PORT_OWNERS: usize = 8;
|
|
|
858
945
|
fn normalize_host_host_grants(value: &mut HostScope) -> Result<()> {
|
|
859
946
|
let mut grants = std::mem::take(&mut value.host_grants);
|
|
860
947
|
let scope = &*value;
|
|
861
|
-
normalize_host_grants(&mut grants, |grant| {
|
|
948
|
+
normalize_host_grants(&mut grants, "hostGrants", |grant| {
|
|
862
949
|
let destination = &grant.destination_address;
|
|
863
950
|
require(
|
|
864
951
|
scope
|
|
@@ -903,16 +990,19 @@ fn normalize_host_host_grants(value: &mut HostScope) -> Result<()> {
|
|
|
903
990
|
/// overlap with the leased outbound source ports translated to that same address.
|
|
904
991
|
fn normalize_published(value: &mut HostScope) -> Result<()> {
|
|
905
992
|
let mut ports = std::mem::take(&mut value.published_ports);
|
|
906
|
-
|
|
993
|
+
crate::capacity("publishedPorts", PUBLISHED_PORTS, ports.len())?;
|
|
907
994
|
ports.sort();
|
|
908
995
|
for (index, port) in ports.iter().enumerate() {
|
|
909
996
|
require(
|
|
910
997
|
protocol(&port.protocol)
|
|
911
998
|
&& port.host_port > 0
|
|
912
999
|
&& port.target_port > 0
|
|
1000
|
+
&& published_span(port.host_port, port.host_port_end, port.target_port)
|
|
1001
|
+
// Sorted by protocol and first port, so disjoint neighbours are
|
|
1002
|
+
// disjoint spans: one protocol/port is published exactly once.
|
|
913
1003
|
&& (index == 0
|
|
914
1004
|
|| ports[index - 1].protocol != port.protocol
|
|
915
|
-
|| ports[index - 1].
|
|
1005
|
+
|| ports[index - 1].host_last() < port.host_port),
|
|
916
1006
|
)?;
|
|
917
1007
|
// rtnetlink verifies this exact uplink address at apply, recovery and
|
|
918
1008
|
// inspection. No wildcard, secondary-link or default-route inference.
|
|
@@ -934,14 +1024,39 @@ fn normalize_published(value: &mut HostScope) -> Result<()> {
|
|
|
934
1024
|
// port inside a leased outbound range is a translation conflict.
|
|
935
1025
|
require(
|
|
936
1026
|
range.protocol != port.protocol
|
|
937
|
-
|| port.
|
|
1027
|
+
|| port.host_last() < range.first
|
|
938
1028
|
|| port.host_port > range.last,
|
|
939
1029
|
)?;
|
|
940
1030
|
}
|
|
941
1031
|
}
|
|
942
1032
|
}
|
|
943
1033
|
}
|
|
1034
|
+
if port.symmetric {
|
|
1035
|
+
// The workload's flows leave from its target ports; a leased range of
|
|
1036
|
+
// the same address would claim them for the outer leased translation.
|
|
1037
|
+
for allocation in value
|
|
1038
|
+
.handoffs
|
|
1039
|
+
.iter()
|
|
1040
|
+
.flat_map(|handoff| handoff.allocations.iter())
|
|
1041
|
+
.filter(|allocation| allocation.transit_source_address == port.target_address)
|
|
1042
|
+
{
|
|
1043
|
+
for range in &allocation.source_port_ranges {
|
|
1044
|
+
require(
|
|
1045
|
+
range.protocol != port.protocol
|
|
1046
|
+
|| port.target_last() < range.first
|
|
1047
|
+
|| port.target_port > range.last,
|
|
1048
|
+
)?;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
944
1052
|
}
|
|
1053
|
+
exclusive_symmetric(ports.iter().map(|port| {
|
|
1054
|
+
(
|
|
1055
|
+
port.symmetric,
|
|
1056
|
+
(&port.protocol, &port.target_address),
|
|
1057
|
+
(port.target_port, port.target_last()),
|
|
1058
|
+
)
|
|
1059
|
+
}))?;
|
|
945
1060
|
value.published_ports = ports;
|
|
946
1061
|
Ok(())
|
|
947
1062
|
}
|
|
@@ -1062,6 +1177,12 @@ pub(crate) mod localplatform_tests;
|
|
|
1062
1177
|
#[path = "egress_localport_tests.rs"]
|
|
1063
1178
|
pub(crate) mod localport_tests;
|
|
1064
1179
|
#[cfg(test)]
|
|
1180
|
+
#[path = "egress_publishedrange_tests.rs"]
|
|
1181
|
+
mod publishedrange_tests;
|
|
1182
|
+
#[cfg(test)]
|
|
1183
|
+
#[path = "egress_routerequivalence_tests.rs"]
|
|
1184
|
+
mod routerequivalence_tests;
|
|
1185
|
+
#[cfg(test)]
|
|
1065
1186
|
#[path = "egress_tests.rs"]
|
|
1066
1187
|
pub(crate) mod tests;
|
|
1067
1188
|
#[cfg(test)]
|
|
@@ -55,6 +55,8 @@ pub(super) struct Flow {
|
|
|
55
55
|
/// The file system uid of the packet's user socket; None without one, when
|
|
56
56
|
/// `meta skuid` breaks the rule like the kernel.
|
|
57
57
|
pub(super) uid: Option<u32>,
|
|
58
|
+
/// The conntrack label of the flow's entry.
|
|
59
|
+
pub(super) label: [u8; 16],
|
|
58
60
|
}
|
|
59
61
|
const HOST: [u8; 4] = [10, 240, 0, 1];
|
|
60
62
|
const WORK: [u8; 4] = [10, 241, 0, 2];
|
|
@@ -82,6 +84,7 @@ pub(super) fn opening(
|
|
|
82
84
|
zone: 0,
|
|
83
85
|
original: (HOST, WORK, 443, 8080),
|
|
84
86
|
uid: None,
|
|
87
|
+
label: [0; 16],
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
/// The workload's answer to `flow`, arriving on the reverse links.
|
|
@@ -194,11 +197,47 @@ pub(super) struct Decision {
|
|
|
194
197
|
pub(super) verdict: i32,
|
|
195
198
|
pub(super) chain: String,
|
|
196
199
|
pub(super) zone: u16,
|
|
200
|
+
/// The NAT a nat-chain rule performed, which also ends that rule with an
|
|
201
|
+
/// accept exactly as the kernel's nat expression does.
|
|
202
|
+
pub(super) translation: Option<Translation>,
|
|
197
203
|
}
|
|
198
|
-
///
|
|
204
|
+
/// Source (0) or destination (1) NAT to an address and, when the rule names
|
|
205
|
+
/// them, an inclusive port range.
|
|
206
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
207
|
+
pub(super) struct Translation {
|
|
208
|
+
pub(super) kind: u32,
|
|
209
|
+
pub(super) address: [u8; 4],
|
|
210
|
+
pub(super) ports: Option<(u16, u16)>,
|
|
211
|
+
}
|
|
212
|
+
/// The compiled table: rules per chain, element keys per exact set, and the
|
|
213
|
+
/// fields and elements of every interval set or map.
|
|
199
214
|
struct Table {
|
|
200
215
|
chains: BTreeMap<String, Vec<Vec<Attr>>>,
|
|
201
216
|
sets: BTreeMap<String, Vec<Vec<u8>>>,
|
|
217
|
+
intervals: BTreeMap<String, Interval>,
|
|
218
|
+
}
|
|
219
|
+
/// Field byte lengths, then `(first, last, data)` per element.
|
|
220
|
+
#[derive(Default)]
|
|
221
|
+
struct Interval {
|
|
222
|
+
fields: Vec<usize>,
|
|
223
|
+
elements: Vec<(Vec<u8>, Vec<u8>, Option<Vec<u8>>)>,
|
|
224
|
+
}
|
|
225
|
+
impl Interval {
|
|
226
|
+
/// Every field of the key within its bounds, compared as the kernel's
|
|
227
|
+
/// pipapo backend does: byte strings of the field's own length.
|
|
228
|
+
fn find(&self, key: &[u8]) -> Option<&(Vec<u8>, Vec<u8>, Option<Vec<u8>>)> {
|
|
229
|
+
self.elements.iter().find(|(first, last, _)| {
|
|
230
|
+
let mut at = 0;
|
|
231
|
+
self.fields.iter().all(|length| {
|
|
232
|
+
let range = at..at + length;
|
|
233
|
+
at += length.div_ceil(4) * 4;
|
|
234
|
+
first[range.clone()] <= key[range.clone()] && key[range.clone()] <= last[range]
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
fn width(&self) -> usize {
|
|
239
|
+
self.fields.iter().map(|length| length.div_ceil(4) * 4).sum()
|
|
240
|
+
}
|
|
202
241
|
}
|
|
203
242
|
pub(super) fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
|
|
204
243
|
table(prepared).chains
|
|
@@ -206,6 +245,7 @@ pub(super) fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
|
|
|
206
245
|
fn table(prepared: &Prepared) -> Table {
|
|
207
246
|
let mut chains: BTreeMap<String, Vec<Vec<Attr>>> = BTreeMap::new();
|
|
208
247
|
let mut sets: BTreeMap<String, Vec<Vec<u8>>> = BTreeMap::new();
|
|
248
|
+
let mut intervals: BTreeMap<String, Interval> = BTreeMap::new();
|
|
209
249
|
for (kind, attributes) in prepared.program("snft_host_grant").unwrap() {
|
|
210
250
|
match kind {
|
|
211
251
|
3 => {
|
|
@@ -218,21 +258,59 @@ fn table(prepared: &Prepared) -> Table {
|
|
|
218
258
|
.or_default()
|
|
219
259
|
.push(wire::attrs(&wire::one(&attributes, 4).unwrap().value).unwrap()),
|
|
220
260
|
9 => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.
|
|
261
|
+
let name = wire::text(&attributes, 2).unwrap();
|
|
262
|
+
if let Ok(description) = wire::one(&attributes, 9) {
|
|
263
|
+
let concatenation = wire::attrs(&description.value).unwrap();
|
|
264
|
+
let fields = wire::attrs(&wire::one(&concatenation, 2).unwrap().value)
|
|
265
|
+
.unwrap()
|
|
266
|
+
.iter()
|
|
267
|
+
.map(|field| {
|
|
268
|
+
wire::number(&wire::attrs(&field.value).unwrap(), 1).unwrap() as usize
|
|
269
|
+
})
|
|
270
|
+
.collect();
|
|
271
|
+
assert!(intervals
|
|
272
|
+
.insert(name, Interval { fields, elements: Vec::new() })
|
|
273
|
+
.is_none());
|
|
274
|
+
} else if wire::number(&attributes, 3).unwrap() & 8 != 0 {
|
|
275
|
+
// An exact map is one field of the whole key.
|
|
276
|
+
let fields = vec![wire::number(&attributes, 5).unwrap() as usize];
|
|
277
|
+
assert!(intervals
|
|
278
|
+
.insert(name, Interval { fields, elements: Vec::new() })
|
|
279
|
+
.is_none());
|
|
280
|
+
} else {
|
|
281
|
+
assert!(sets.insert(name, Vec::new()).is_none());
|
|
282
|
+
}
|
|
224
283
|
}
|
|
225
284
|
12 => {
|
|
226
|
-
let
|
|
285
|
+
let name = wire::text(&attributes, 2).unwrap();
|
|
227
286
|
for element in wire::attrs(&wire::one(&attributes, 3).unwrap().value).unwrap() {
|
|
228
287
|
let key = wire::attrs(&element.value).unwrap();
|
|
229
|
-
|
|
288
|
+
if let Some(interval) = intervals.get_mut(&name) {
|
|
289
|
+
// An element without a last key is one exact key.
|
|
290
|
+
let first = data(&key, 1);
|
|
291
|
+
let last = if wire::one(&key, 10).is_ok() {
|
|
292
|
+
data(&key, 10)
|
|
293
|
+
} else {
|
|
294
|
+
first.clone()
|
|
295
|
+
};
|
|
296
|
+
interval.elements.push((
|
|
297
|
+
first,
|
|
298
|
+
last,
|
|
299
|
+
wire::one(&key, 2).ok().map(|_| data(&key, 2)),
|
|
300
|
+
));
|
|
301
|
+
} else {
|
|
302
|
+
sets.get_mut(&name).unwrap().push(data(&key, 1));
|
|
303
|
+
}
|
|
230
304
|
}
|
|
231
305
|
}
|
|
232
306
|
other => panic!("operation {other}"),
|
|
233
307
|
}
|
|
234
308
|
}
|
|
235
|
-
Table {
|
|
309
|
+
Table {
|
|
310
|
+
chains,
|
|
311
|
+
sets,
|
|
312
|
+
intervals,
|
|
313
|
+
}
|
|
236
314
|
}
|
|
237
315
|
fn data(attributes: &[Attr], id: u16) -> Vec<u8> {
|
|
238
316
|
wire::attrs(&wire::one(attributes, id).unwrap().value).unwrap()[0]
|
|
@@ -290,7 +368,7 @@ fn load(flow: &Flow, name: &str, fields: &[Attr]) -> Option<Vec<u8>> {
|
|
|
290
368
|
10 => vec![flow.protocol],
|
|
291
369
|
11 => source_port.to_be_bytes().to_vec(),
|
|
292
370
|
12 => destination_port.to_be_bytes().to_vec(),
|
|
293
|
-
13 =>
|
|
371
|
+
13 => flow.label.to_vec(),
|
|
294
372
|
17 => flow.zone.to_ne_bytes().to_vec(),
|
|
295
373
|
19 => source.to_vec(),
|
|
296
374
|
20 => destination.to_vec(),
|
|
@@ -321,6 +399,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
321
399
|
let mut registers = [0_u8; 80];
|
|
322
400
|
let mut matched = true;
|
|
323
401
|
let mut verdict: Option<(i32, Option<String>)> = None;
|
|
402
|
+
let mut translation = None;
|
|
324
403
|
for expression in expressions {
|
|
325
404
|
let parts = wire::attrs(&expression.value).unwrap();
|
|
326
405
|
let name = wire::text(&parts, 1).unwrap();
|
|
@@ -367,12 +446,29 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
367
446
|
matched = loaded >= &first[..] && loaded <= &last[..];
|
|
368
447
|
}
|
|
369
448
|
"lookup" => {
|
|
370
|
-
|
|
371
|
-
let
|
|
449
|
+
let inverted = wire::number(&fields, 5).unwrap() == 1;
|
|
450
|
+
let name = wire::text(&fields, 1).unwrap();
|
|
372
451
|
let at = offset(&fields, 2);
|
|
373
|
-
|
|
374
|
-
.
|
|
375
|
-
|
|
452
|
+
if let Some(interval) = table.intervals.get(&name) {
|
|
453
|
+
let key = registers[at..at + interval.width()].to_vec();
|
|
454
|
+
let found = interval.find(&key);
|
|
455
|
+
matched = found.is_some();
|
|
456
|
+
if let (Some((_, _, data)), Ok(_)) = (found, wire::one(&fields, 3)) {
|
|
457
|
+
// A map lookup stores the element's data.
|
|
458
|
+
let data = data.as_ref().unwrap();
|
|
459
|
+
let to = offset(&fields, 3);
|
|
460
|
+
registers[to..to + data.len()].copy_from_slice(data);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
let keys = &table.sets[&name];
|
|
464
|
+
matched = keys
|
|
465
|
+
.iter()
|
|
466
|
+
.any(|key| registers[at..at + key.len()] == key[..]);
|
|
467
|
+
}
|
|
468
|
+
if inverted {
|
|
469
|
+
assert!(wire::one(&fields, 3).is_err());
|
|
470
|
+
matched = !matched;
|
|
471
|
+
}
|
|
376
472
|
}
|
|
377
473
|
"immediate" if wire::number(&fields, 1).unwrap() == 0 => {
|
|
378
474
|
let code = wire::attrs(
|
|
@@ -394,7 +490,19 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
394
490
|
let at = offset(&fields, 1);
|
|
395
491
|
registers[at..at + value.len()].copy_from_slice(&value);
|
|
396
492
|
}
|
|
397
|
-
"nat" => {
|
|
493
|
+
"nat" => {
|
|
494
|
+
let at = offset(&fields, 3);
|
|
495
|
+
let port = |id: u16| {
|
|
496
|
+
let at = offset(&fields, id);
|
|
497
|
+
u16::from_be_bytes(registers[at..at + 2].try_into().unwrap())
|
|
498
|
+
};
|
|
499
|
+
translation = Some(Translation {
|
|
500
|
+
kind: wire::number(&fields, 1).unwrap(),
|
|
501
|
+
address: registers[at..at + 4].try_into().unwrap(),
|
|
502
|
+
ports: wire::one(&fields, 5).ok().map(|_| (port(5), port(6))),
|
|
503
|
+
});
|
|
504
|
+
verdict = Some((1, None));
|
|
505
|
+
}
|
|
398
506
|
"reject" => {
|
|
399
507
|
assert_eq!(fields, vec![Attr::u32(1, 1)]);
|
|
400
508
|
verdict = Some((REJECT, None));
|
|
@@ -414,6 +522,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
414
522
|
verdict: code,
|
|
415
523
|
chain: chain.into(),
|
|
416
524
|
zone: *zone,
|
|
525
|
+
translation,
|
|
417
526
|
})
|
|
418
527
|
}
|
|
419
528
|
Some((-5, _)) => return Step::Next,
|
|
@@ -431,13 +540,34 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
431
540
|
}
|
|
432
541
|
/// A base chain that ends without a terminal verdict accepts by policy.
|
|
433
542
|
pub(super) fn decide(prepared: &Prepared, chain: &str, flow: &Flow) -> Decision {
|
|
543
|
+
Simulator::new(prepared).decide(chain, flow)
|
|
544
|
+
}
|
|
545
|
+
/// One compiled table, parsed once for many decisions.
|
|
546
|
+
pub(super) struct Simulator {
|
|
547
|
+
table: Table,
|
|
548
|
+
}
|
|
549
|
+
impl Simulator {
|
|
550
|
+
pub(super) fn new(prepared: &Prepared) -> Self {
|
|
551
|
+
Self {
|
|
552
|
+
table: table(prepared),
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
pub(super) fn has(&self, chain: &str) -> bool {
|
|
556
|
+
self.table.chains.contains_key(chain)
|
|
557
|
+
}
|
|
558
|
+
pub(super) fn decide(&self, chain: &str, flow: &Flow) -> Decision {
|
|
559
|
+
decide_in(&self.table, chain, flow)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
fn decide_in(table: &Table, chain: &str, flow: &Flow) -> Decision {
|
|
434
563
|
let mut zone = 0;
|
|
435
|
-
match run(
|
|
564
|
+
match run(table, chain, flow, &mut zone) {
|
|
436
565
|
Step::Final(decision) => decision,
|
|
437
566
|
Step::Next => Decision {
|
|
438
567
|
verdict: 1,
|
|
439
568
|
chain: "policy".into(),
|
|
440
569
|
zone,
|
|
570
|
+
translation: None,
|
|
441
571
|
},
|
|
442
572
|
}
|
|
443
573
|
}
|
|
@@ -835,7 +965,8 @@ fn host_grants_compile_a_constant_rule_graph_and_one_element_per_grant() {
|
|
|
835
965
|
if let Some(arrival) = all.sets.get("host_grant_arrival") {
|
|
836
966
|
assert_eq!(arrival.len(), 1024);
|
|
837
967
|
}
|
|
838
|
-
|
|
968
|
+
let without = table(&prepared(fixture));
|
|
969
|
+
assert!(without.sets.keys().all(|name| !name.starts_with("host_grant")));
|
|
839
970
|
}
|
|
840
971
|
}
|
|
841
972
|
|
|
@@ -917,11 +1048,18 @@ fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
917
1048
|
};
|
|
918
1049
|
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
919
1050
|
assert!(
|
|
920
|
-
program.len() < 768 && graph <=
|
|
1051
|
+
program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES
|
|
921
1052
|
);
|
|
922
1053
|
assert!(deletion + graph + elements + 64 <= crate::wire::BATCH_BYTES);
|
|
923
|
-
// The contract maximum is
|
|
924
|
-
|
|
1054
|
+
// The contract maximum is a capacity bound.
|
|
1055
|
+
assert_eq!(
|
|
1056
|
+
normalized(granted(fixture, many(1025))).unwrap_err(),
|
|
1057
|
+
crate::Error::Exhausted(crate::Bound {
|
|
1058
|
+
name: "hostGrants",
|
|
1059
|
+
limit: 1024,
|
|
1060
|
+
actual: 1025
|
|
1061
|
+
})
|
|
1062
|
+
);
|
|
925
1063
|
println!(
|
|
926
1064
|
"HOST_GRANT_MAX scope={name} grants=1024 operations={} rule_bytes={graph} element_bytes={elements} batch_bound={}",
|
|
927
1065
|
program.len(),
|
|
@@ -929,7 +1067,7 @@ fn host_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
929
1067
|
);
|
|
930
1068
|
}
|
|
931
1069
|
// The reserve itself: a maximum previous graph beside a maximum target.
|
|
932
|
-
assert!(deletion +
|
|
1070
|
+
assert!(deletion + super::compile::TARGET_BYTES + 64 <= crate::wire::BATCH_BYTES);
|
|
933
1071
|
}
|
|
934
1072
|
|
|
935
1073
|
/// A status carries up to three complete policies: the applied one and the
|
|
@@ -118,6 +118,7 @@ fn local_tcp_port_owner_rejects_every_other_uid_at_its_exact_loopback_destinatio
|
|
|
118
118
|
input,
|
|
119
119
|
output: None,
|
|
120
120
|
uid: None,
|
|
121
|
+
label: [0; 16],
|
|
121
122
|
..dial(10010, None)
|
|
122
123
|
};
|
|
123
124
|
assert_eq!(decide(&with, "input", &arriving(LO)).verdict, 1);
|
|
@@ -132,6 +133,7 @@ fn local_tcp_port_owner_rejects_every_other_uid_at_its_exact_loopback_destinatio
|
|
|
132
133
|
// Guarded pool traffic from any user keeps the guard's decision.
|
|
133
134
|
let pool = Flow {
|
|
134
135
|
uid: Some(1000),
|
|
136
|
+
label: [0; 16],
|
|
135
137
|
..opening(None, Some((4, "handoff")))
|
|
136
138
|
};
|
|
137
139
|
assert_eq!(
|
|
@@ -250,7 +252,7 @@ fn local_tcp_port_owners_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
250
252
|
.sum()
|
|
251
253
|
};
|
|
252
254
|
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
253
|
-
assert!(program.len() < 768 && graph <=
|
|
255
|
+
assert!(program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES);
|
|
254
256
|
assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
|
|
255
257
|
println!(
|
|
256
258
|
"LOCAL_TCP_PORT_OWNER_MAX owners=8 prefixes=64 grants=1024 operations={} rule_bytes={graph}",
|
|
@@ -260,5 +262,12 @@ fn local_tcp_port_owners_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
|
260
262
|
.as_array_mut()
|
|
261
263
|
.unwrap()
|
|
262
264
|
.push(owner("127.0.0.1", 10008, 0));
|
|
263
|
-
|
|
265
|
+
assert_eq!(
|
|
266
|
+
normalized(value).unwrap_err(),
|
|
267
|
+
crate::Error::Exhausted(crate::Bound {
|
|
268
|
+
name: "localTcpPortOwners",
|
|
269
|
+
limit: 8,
|
|
270
|
+
actual: 9
|
|
271
|
+
})
|
|
272
|
+
);
|
|
264
273
|
}
|