@push.rocks/smartnftables 2.5.2 → 3.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 +31 -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/managed.egress.types.d.ts +69 -3
- package/package.json +6 -6
- package/readme.md +342 -61
- package/rust/src/egress.compile.rs +157 -76
- package/rust/src/egress.host.rs +104 -44
- package/rust/src/egress.hostgrant.rs +14 -14
- package/rust/src/egress.keys.rs +305 -0
- package/rust/src/egress.leased.rs +399 -0
- package/rust/src/egress.poolguard.rs +45 -0
- package/rust/src/egress.private.rs +197 -0
- package/rust/src/egress.published.rs +370 -0
- package/rust/src/egress.router.rs +64 -295
- package/rust/src/egress.rs +248 -17
- package/rust/src/egress.workloadgrant.rs +88 -0
- package/rust/src/egress_hostgrant_tests.rs +196 -47
- package/rust/src/egress_localplatform_tests.rs +204 -0
- package/rust/src/egress_localport_tests.rs +266 -0
- package/rust/src/egress_publishedrange_tests.rs +812 -0
- package/rust/src/egress_routerequivalence_tests.rs +448 -0
- package/rust/src/egress_tests.rs +132 -230
- package/rust/src/egress_workloadgrant_tests.rs +313 -0
- package/rust/src/owner.rs +77 -15
- package/rust/src/owner_host_traffic_tests.rs +4 -0
- package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
- package/rust/src/owner_identity_tests.rs +228 -80
- package/rust/src/owner_localplatform_tests.rs +274 -0
- package/rust/src/owner_localport_tests.rs +241 -0
- 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 +15 -0
- package/rust/src/owner_workloadgrant_tests.rs +278 -0
- package/rust/src/policy.rs +29 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/managed.egress.types.ts +71 -3
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
|
|
@@ -115,6 +134,10 @@ pub struct HostGrant {
|
|
|
115
134
|
pub destination_address: String,
|
|
116
135
|
pub destination_port: u16,
|
|
117
136
|
}
|
|
137
|
+
/// One stateful one-way flow between two workloads behind the same router: the
|
|
138
|
+
/// source workload dials one exact port of the destination workload, which may
|
|
139
|
+
/// only answer. It is the same exact tuple as a host grant.
|
|
140
|
+
pub type WorkloadGrant = HostGrant;
|
|
118
141
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
119
142
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
120
143
|
pub struct RouterScope {
|
|
@@ -131,6 +154,9 @@ pub struct RouterScope {
|
|
|
131
154
|
/// Absent and empty are the same canonical policy, like publications.
|
|
132
155
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
133
156
|
pub host_grants: Vec<HostGrant>,
|
|
157
|
+
/// Absent and empty are the same canonical policy, like publications.
|
|
158
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
159
|
+
pub workload_grants: Vec<WorkloadGrant>,
|
|
134
160
|
}
|
|
135
161
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
136
162
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -143,9 +169,28 @@ pub struct HostHandoff {
|
|
|
143
169
|
pub struct PublishedPort {
|
|
144
170
|
pub protocol: String,
|
|
145
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>,
|
|
146
176
|
pub target_port: u16,
|
|
147
177
|
pub target_address: String,
|
|
148
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
|
+
}
|
|
149
194
|
}
|
|
150
195
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
151
196
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -161,6 +206,20 @@ pub struct HostScope {
|
|
|
161
206
|
/// Absent and empty are the same canonical policy, like publications.
|
|
162
207
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
163
208
|
pub host_grants: Vec<HostGrant>,
|
|
209
|
+
/// Ids of platform endpoints this host serves on an address of one of its
|
|
210
|
+
/// bound links. Absent and empty are the same canonical policy.
|
|
211
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
212
|
+
pub local_platform_endpoints: Vec<String>,
|
|
213
|
+
}
|
|
214
|
+
/// One loopback TCP service that only one local user may dial: packets the host
|
|
215
|
+
/// sends to the exact address and port from a socket of any other user reject,
|
|
216
|
+
/// and packets arriving there on any interface but loopback drop.
|
|
217
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
218
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
219
|
+
pub struct LocalTcpPortOwner {
|
|
220
|
+
pub address: String,
|
|
221
|
+
pub port: u16,
|
|
222
|
+
pub uid: u32,
|
|
164
223
|
}
|
|
165
224
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
166
225
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -171,6 +230,10 @@ pub struct AllocationPoolGuardScope {
|
|
|
171
230
|
/// Absent and empty are the same canonical policy.
|
|
172
231
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
173
232
|
pub host_grants: Vec<HostGrant>,
|
|
233
|
+
/// Host-wide loopback port ownership. Absent and empty are the same
|
|
234
|
+
/// canonical policy.
|
|
235
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
236
|
+
pub local_tcp_port_owners: Vec<LocalTcpPortOwner>,
|
|
174
237
|
}
|
|
175
238
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
176
239
|
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -421,16 +484,19 @@ pub fn destination(grant: &Grant, protection: &Protection) -> Result<String> {
|
|
|
421
484
|
/// with the leased outbound source ports translated to that same transit address.
|
|
422
485
|
fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
|
|
423
486
|
let mut ports = std::mem::take(&mut value.published_ports);
|
|
424
|
-
require(ports.len() <=
|
|
487
|
+
require(ports.len() <= PUBLISHED_PORTS)?;
|
|
425
488
|
ports.sort();
|
|
426
489
|
for (index, port) in ports.iter().enumerate() {
|
|
427
490
|
require(
|
|
428
491
|
protocol(&port.protocol)
|
|
429
492
|
&& port.transit_port > 0
|
|
430
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.
|
|
431
497
|
&& (index == 0
|
|
432
498
|
|| ports[index - 1].protocol != port.protocol
|
|
433
|
-
|| ports[index - 1].
|
|
499
|
+
|| ports[index - 1].transit_last() < port.transit_port),
|
|
434
500
|
)?;
|
|
435
501
|
// rtnetlink verifies this exact handoff address at apply, recovery and
|
|
436
502
|
// inspection, and only a current leased transit source address receives a
|
|
@@ -486,15 +552,47 @@ fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
|
|
|
486
552
|
// port inside a leased outbound range is a translation conflict.
|
|
487
553
|
require(
|
|
488
554
|
range.protocol != port.protocol
|
|
489
|
-
|| port.
|
|
555
|
+
|| port.transit_last() < range.first
|
|
490
556
|
|| port.transit_port > range.last,
|
|
491
557
|
)?;
|
|
492
558
|
}
|
|
493
559
|
}
|
|
494
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
|
+
}))?;
|
|
495
570
|
value.published_ports = ports;
|
|
496
571
|
Ok(())
|
|
497
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
|
+
}
|
|
498
596
|
/// Exact tuples only: one protocol, one unicast source and destination address
|
|
499
597
|
/// and one destination port. Addresses never carry a prefix and ports never a
|
|
500
598
|
/// range, so a grant cannot widen past its flow. `bound` checks the authority of
|
|
@@ -565,17 +663,65 @@ fn normalize_router_host_grants(value: &mut RouterScope) -> Result<()> {
|
|
|
565
663
|
value.host_grants = grants;
|
|
566
664
|
Ok(())
|
|
567
665
|
}
|
|
666
|
+
/// Both ends are exact current addresses of two different veth workload endpoints
|
|
667
|
+
/// of this scope, neither router-local nor a platform endpoint. Endpoint prefixes
|
|
668
|
+
/// are protected, so both directions stay in the default conntrack zone ahead of
|
|
669
|
+
/// every leased classifier.
|
|
670
|
+
fn normalize_router_workload_grants(value: &mut RouterScope) -> Result<()> {
|
|
671
|
+
let mut grants = std::mem::take(&mut value.workload_grants);
|
|
672
|
+
let scope = &*value;
|
|
673
|
+
let workload = |ip: &str| -> Result<&policy::Endpoint> {
|
|
674
|
+
let target = format!("{ip}/32");
|
|
675
|
+
let endpoint = scope
|
|
676
|
+
.endpoints
|
|
677
|
+
.iter()
|
|
678
|
+
.find(|endpoint| {
|
|
679
|
+
endpoint
|
|
680
|
+
.source_prefixes
|
|
681
|
+
.iter()
|
|
682
|
+
.any(|source| covers(source, &target) == Ok(true))
|
|
683
|
+
})
|
|
684
|
+
.ok_or(Error::Invalid)?;
|
|
685
|
+
let local = scope
|
|
686
|
+
.links
|
|
687
|
+
.iter()
|
|
688
|
+
.chain(std::iter::once(&scope.handoff))
|
|
689
|
+
.any(|link| link.required_ipv4_addresses.iter().any(|item| item == ip));
|
|
690
|
+
let platform = scope
|
|
691
|
+
.protection
|
|
692
|
+
.platform_endpoints
|
|
693
|
+
.iter()
|
|
694
|
+
.any(|item| item.address == ip);
|
|
695
|
+
require(endpoint.interface_kind == "veth" && !local && !platform)?;
|
|
696
|
+
Ok(endpoint)
|
|
697
|
+
};
|
|
698
|
+
normalize_host_grants(&mut grants, |grant| {
|
|
699
|
+
let source = workload(&grant.source_address)?;
|
|
700
|
+
let destination = workload(&grant.destination_address)?;
|
|
701
|
+
require(source.id != destination.id)
|
|
702
|
+
})?;
|
|
703
|
+
value.workload_grants = grants;
|
|
704
|
+
Ok(())
|
|
705
|
+
}
|
|
706
|
+
/// Router input bounds. Endpoints, rules and grants are set elements, so the
|
|
707
|
+
/// graph budgets bind them, not the rule reserve: the node shape of 128
|
|
708
|
+
/// workloads with local DNS, public egress and four platform endpoints each fits.
|
|
709
|
+
pub const ROUTER_LINKS: usize = 128;
|
|
710
|
+
pub const ROUTER_RULES: usize = 1024;
|
|
711
|
+
pub const ROUTER_GRANTS: usize = 1024;
|
|
712
|
+
/// Publications per scope; they are set elements on both hops.
|
|
713
|
+
pub const PUBLISHED_PORTS: usize = 1024;
|
|
568
714
|
fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
569
|
-
require(value.links.len() <=
|
|
715
|
+
require(value.links.len() <= ROUTER_LINKS && value.generations.len() <= 32)?;
|
|
570
716
|
let private = policy::Policy {
|
|
571
717
|
schema_version: 1,
|
|
572
718
|
revision,
|
|
573
719
|
endpoints: value.endpoints.clone(),
|
|
574
720
|
rules: value.rules.clone(),
|
|
575
721
|
}
|
|
576
|
-
.
|
|
577
|
-
value.endpoints = private.
|
|
578
|
-
value.rules = private.
|
|
722
|
+
.normalize(ROUTER_LINKS, ROUTER_RULES)?;
|
|
723
|
+
value.endpoints = private.endpoints;
|
|
724
|
+
value.rules = private.rules;
|
|
579
725
|
normalize_protection(&mut value.protection)?;
|
|
580
726
|
normalize_link(&mut value.handoff)?;
|
|
581
727
|
require(
|
|
@@ -660,7 +806,7 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
660
806
|
&& labels.insert(generation.conntrack_label.clone()),
|
|
661
807
|
)?;
|
|
662
808
|
count += generation.grants.len();
|
|
663
|
-
require(count <=
|
|
809
|
+
require(count <= ROUTER_GRANTS)?;
|
|
664
810
|
generation.grants.sort();
|
|
665
811
|
for grant in &generation.grants {
|
|
666
812
|
require(
|
|
@@ -737,7 +883,8 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
737
883
|
)
|
|
738
884
|
}))?;
|
|
739
885
|
normalize_router_published(value)?;
|
|
740
|
-
normalize_router_host_grants(value)
|
|
886
|
+
normalize_router_host_grants(value)?;
|
|
887
|
+
normalize_router_workload_grants(value)
|
|
741
888
|
}
|
|
742
889
|
fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
743
890
|
require(
|
|
@@ -760,8 +907,32 @@ fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
|
760
907
|
require(covers_address(&value.prefixes, &grant.destination_address)?)
|
|
761
908
|
})?;
|
|
762
909
|
value.host_grants = grants;
|
|
910
|
+
normalize_local_tcp_port_owners(&mut value.local_tcp_port_owners)
|
|
911
|
+
}
|
|
912
|
+
/// One exact IPv4 loopback host address and port per entry, never a prefix,
|
|
913
|
+
/// wildcard or range, each with exactly one owning uid. `(uid_t)-1` is not a
|
|
914
|
+
/// user; it is the kernel's "unchanged" sentinel.
|
|
915
|
+
fn normalize_local_tcp_port_owners(owners: &mut [LocalTcpPortOwner]) -> Result<()> {
|
|
916
|
+
require(owners.len() <= LOCAL_TCP_PORT_OWNERS)?;
|
|
917
|
+
owners.sort();
|
|
918
|
+
for (index, owner) in owners.iter().enumerate() {
|
|
919
|
+
let ip = address(&owner.address)?;
|
|
920
|
+
require(
|
|
921
|
+
ip >> 24 == 127
|
|
922
|
+
&& ip != 0x7f00_0000
|
|
923
|
+
&& ip != 0x7fff_ffff
|
|
924
|
+
&& owner.port > 0
|
|
925
|
+
&& owner.uid != u32::MAX
|
|
926
|
+
&& owners[..index]
|
|
927
|
+
.iter()
|
|
928
|
+
.all(|other| other.address != owner.address || other.port != owner.port),
|
|
929
|
+
)?;
|
|
930
|
+
}
|
|
763
931
|
Ok(())
|
|
764
932
|
}
|
|
933
|
+
/// Two rules per entry; the maximum fits the atomic graph beside a guard with
|
|
934
|
+
/// 64 pools and 1024 host grants.
|
|
935
|
+
const LOCAL_TCP_PORT_OWNERS: usize = 8;
|
|
765
936
|
/// The host dials from an exact current address of exactly one handoff link, into
|
|
766
937
|
/// protected space that is neither host-local, a platform endpoint nor a leased
|
|
767
938
|
/// router transit address: only a workload behind that handoff remains.
|
|
@@ -813,16 +984,19 @@ fn normalize_host_host_grants(value: &mut HostScope) -> Result<()> {
|
|
|
813
984
|
/// overlap with the leased outbound source ports translated to that same address.
|
|
814
985
|
fn normalize_published(value: &mut HostScope) -> Result<()> {
|
|
815
986
|
let mut ports = std::mem::take(&mut value.published_ports);
|
|
816
|
-
require(ports.len() <=
|
|
987
|
+
require(ports.len() <= PUBLISHED_PORTS)?;
|
|
817
988
|
ports.sort();
|
|
818
989
|
for (index, port) in ports.iter().enumerate() {
|
|
819
990
|
require(
|
|
820
991
|
protocol(&port.protocol)
|
|
821
992
|
&& port.host_port > 0
|
|
822
993
|
&& port.target_port > 0
|
|
994
|
+
&& published_span(port.host_port, port.host_port_end, port.target_port)
|
|
995
|
+
// Sorted by protocol and first port, so disjoint neighbours are
|
|
996
|
+
// disjoint spans: one protocol/port is published exactly once.
|
|
823
997
|
&& (index == 0
|
|
824
998
|
|| ports[index - 1].protocol != port.protocol
|
|
825
|
-
|| ports[index - 1].
|
|
999
|
+
|| ports[index - 1].host_last() < port.host_port),
|
|
826
1000
|
)?;
|
|
827
1001
|
// rtnetlink verifies this exact uplink address at apply, recovery and
|
|
828
1002
|
// inspection. No wildcard, secondary-link or default-route inference.
|
|
@@ -844,14 +1018,39 @@ fn normalize_published(value: &mut HostScope) -> Result<()> {
|
|
|
844
1018
|
// port inside a leased outbound range is a translation conflict.
|
|
845
1019
|
require(
|
|
846
1020
|
range.protocol != port.protocol
|
|
847
|
-
|| port.
|
|
1021
|
+
|| port.host_last() < range.first
|
|
848
1022
|
|| port.host_port > range.last,
|
|
849
1023
|
)?;
|
|
850
1024
|
}
|
|
851
1025
|
}
|
|
852
1026
|
}
|
|
853
1027
|
}
|
|
1028
|
+
if port.symmetric {
|
|
1029
|
+
// The workload's flows leave from its target ports; a leased range of
|
|
1030
|
+
// the same address would claim them for the outer leased translation.
|
|
1031
|
+
for allocation in value
|
|
1032
|
+
.handoffs
|
|
1033
|
+
.iter()
|
|
1034
|
+
.flat_map(|handoff| handoff.allocations.iter())
|
|
1035
|
+
.filter(|allocation| allocation.transit_source_address == port.target_address)
|
|
1036
|
+
{
|
|
1037
|
+
for range in &allocation.source_port_ranges {
|
|
1038
|
+
require(
|
|
1039
|
+
range.protocol != port.protocol
|
|
1040
|
+
|| port.target_last() < range.first
|
|
1041
|
+
|| port.target_port > range.last,
|
|
1042
|
+
)?;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
854
1046
|
}
|
|
1047
|
+
exclusive_symmetric(ports.iter().map(|port| {
|
|
1048
|
+
(
|
|
1049
|
+
port.symmetric,
|
|
1050
|
+
(&port.protocol, &port.target_address),
|
|
1051
|
+
(port.target_port, port.target_last()),
|
|
1052
|
+
)
|
|
1053
|
+
}))?;
|
|
855
1054
|
value.published_ports = ports;
|
|
856
1055
|
Ok(())
|
|
857
1056
|
}
|
|
@@ -878,14 +1077,31 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
|
|
|
878
1077
|
.flat_map(|item| item.link.required_ipv4_addresses.iter().cloned())
|
|
879
1078
|
.chain(value.uplink.required_ipv4_addresses.iter().cloned())
|
|
880
1079
|
.collect();
|
|
1080
|
+
// A platform endpoint on a host address is served here, not forwarded, so
|
|
1081
|
+
// only a declared local endpoint may hold one; it must hold a bound address,
|
|
1082
|
+
// which rtnetlink verifies at apply, recovery and inspection.
|
|
1083
|
+
value.local_platform_endpoints.sort();
|
|
1084
|
+
require(
|
|
1085
|
+
value
|
|
1086
|
+
.local_platform_endpoints
|
|
1087
|
+
.windows(2)
|
|
1088
|
+
.all(|pair| pair[0] != pair[1]),
|
|
1089
|
+
)?;
|
|
1090
|
+
for id in &value.local_platform_endpoints {
|
|
1091
|
+
let endpoint = value
|
|
1092
|
+
.protection
|
|
1093
|
+
.platform_endpoints
|
|
1094
|
+
.iter()
|
|
1095
|
+
.find(|endpoint| endpoint.id == *id)
|
|
1096
|
+
.ok_or(Error::Invalid)?;
|
|
1097
|
+
require(local_addresses.contains(&endpoint.address))?;
|
|
1098
|
+
}
|
|
881
1099
|
for ip in &local_addresses {
|
|
882
1100
|
require(
|
|
883
1101
|
covers_address(&value.protection.prefixes, ip)?
|
|
884
|
-
&& value
|
|
885
|
-
.
|
|
886
|
-
|
|
887
|
-
.iter()
|
|
888
|
-
.all(|endpoint| endpoint.address != *ip),
|
|
1102
|
+
&& value.protection.platform_endpoints.iter().all(|endpoint| {
|
|
1103
|
+
endpoint.address != *ip || value.local_platform_endpoints.contains(&endpoint.id)
|
|
1104
|
+
}),
|
|
889
1105
|
)?;
|
|
890
1106
|
}
|
|
891
1107
|
for handoff in &mut value.handoffs {
|
|
@@ -949,8 +1165,23 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
|
|
|
949
1165
|
#[path = "egress_hostgrant_tests.rs"]
|
|
950
1166
|
pub(crate) mod hostgrant_tests;
|
|
951
1167
|
#[cfg(test)]
|
|
1168
|
+
#[path = "egress_localplatform_tests.rs"]
|
|
1169
|
+
pub(crate) mod localplatform_tests;
|
|
1170
|
+
#[cfg(test)]
|
|
1171
|
+
#[path = "egress_localport_tests.rs"]
|
|
1172
|
+
pub(crate) mod localport_tests;
|
|
1173
|
+
#[cfg(test)]
|
|
1174
|
+
#[path = "egress_publishedrange_tests.rs"]
|
|
1175
|
+
mod publishedrange_tests;
|
|
1176
|
+
#[cfg(test)]
|
|
1177
|
+
#[path = "egress_routerequivalence_tests.rs"]
|
|
1178
|
+
mod routerequivalence_tests;
|
|
1179
|
+
#[cfg(test)]
|
|
952
1180
|
#[path = "egress_tests.rs"]
|
|
953
1181
|
pub(crate) mod tests;
|
|
1182
|
+
#[cfg(test)]
|
|
1183
|
+
#[path = "egress_workloadgrant_tests.rs"]
|
|
1184
|
+
pub(crate) mod workloadgrant_tests;
|
|
954
1185
|
impl Policy {
|
|
955
1186
|
pub fn prepare(self) -> Result<Prepared> {
|
|
956
1187
|
use sha2::{Digest, Sha256};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//! Workload grants: stateful one-way flows from one workload endpoint to one
|
|
2
|
+
//! exact port of another, both behind the same router. Like host grants they
|
|
3
|
+
//! compile to a constant number of rules around exact set lookups. The tuple key
|
|
4
|
+
//! carries both protocols and the live and originally tracked tuple, so a
|
|
5
|
+
//! translated flow never matches. A second set binds every grant address to the
|
|
6
|
+
//! exact link that holds it (index and name), and both the incoming and the
|
|
7
|
+
//! outgoing link are looked up on every packet. Only the grant's source opens;
|
|
8
|
+
//! the destination only answers.
|
|
9
|
+
use super::hostgrant::{lookup, meta_to, payload_to, tuple_key, tuple_lookup};
|
|
10
|
+
use super::*;
|
|
11
|
+
use std::collections::BTreeSet;
|
|
12
|
+
|
|
13
|
+
/// Every exact workload-to-workload flow of the scope.
|
|
14
|
+
pub(super) const TUPLE_SET: &str = "workload_grant";
|
|
15
|
+
/// Every grant address with the exact link that holds it.
|
|
16
|
+
pub(super) const LINK_SET: &str = "workload_link";
|
|
17
|
+
|
|
18
|
+
/// The link a packet crosses and the address on its side of that link: the
|
|
19
|
+
/// incoming link and the source, or the outgoing link and the destination.
|
|
20
|
+
fn link_lookup(incoming: bool) -> Vec<Attr> {
|
|
21
|
+
vec![
|
|
22
|
+
meta_to(if incoming { 4 } else { 5 }, 4),
|
|
23
|
+
meta_to(if incoming { 6 } else { 7 }, 5),
|
|
24
|
+
payload_to(1, if incoming { 12 } else { 16 }, 4, 9),
|
|
25
|
+
lookup(LINK_SET),
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
fn link_key(link: &LocalLink, address: &str) -> Result<Vec<u8>> {
|
|
29
|
+
let mut key = link.interface_index.to_ne_bytes().to_vec();
|
|
30
|
+
let mut name = link.interface_name.as_bytes().to_vec();
|
|
31
|
+
name.resize(16, 0);
|
|
32
|
+
key.extend(name);
|
|
33
|
+
key.extend(super::address(address)?.to_be_bytes());
|
|
34
|
+
Ok(key)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub(super) fn sets(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
38
|
+
if scope.workload_grants.is_empty() {
|
|
39
|
+
return Ok(());
|
|
40
|
+
}
|
|
41
|
+
let mut keys = Vec::new();
|
|
42
|
+
let mut links = BTreeSet::new();
|
|
43
|
+
for grant in &scope.workload_grants {
|
|
44
|
+
keys.push(tuple_key(grant, None)?);
|
|
45
|
+
for address in [&grant.source_address, &grant.destination_address] {
|
|
46
|
+
links.insert(link_key(
|
|
47
|
+
super::router::endpoint_link(scope, address)?,
|
|
48
|
+
address,
|
|
49
|
+
)?);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
program.set(TUPLE_SET, 3, 32)?;
|
|
53
|
+
program.elements(TUPLE_SET, keys)?;
|
|
54
|
+
program.set(LINK_SET, 4, 24)?;
|
|
55
|
+
program.elements(LINK_SET, links.into_iter().collect())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Four forward admissions whatever the number of grants: the ESTABLISHED
|
|
59
|
+
/// original direction, a NEW UDP opening, a NEW TCP opening restricted to SYN
|
|
60
|
+
/// with FIN/RST/ACK clear, and the ESTABLISHED reply, each in the default
|
|
61
|
+
/// conntrack zone. A reply is only ever ESTABLISHED, so the destination
|
|
62
|
+
/// workload can never open a flow toward the source through a grant.
|
|
63
|
+
pub(super) fn admissions(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
64
|
+
if scope.workload_grants.is_empty() {
|
|
65
|
+
return Ok(());
|
|
66
|
+
}
|
|
67
|
+
let admission = |reply: bool, connection_state: u32, transport: Option<&str>| {
|
|
68
|
+
let mut result = ipv4();
|
|
69
|
+
result.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
|
|
70
|
+
result.extend(ct(1, None, vec![u8::from(reply)]));
|
|
71
|
+
result.extend(state(connection_state));
|
|
72
|
+
if let Some(transport) = transport {
|
|
73
|
+
result.extend(meta(16, vec![protocol(transport)]));
|
|
74
|
+
if transport == "tcp" {
|
|
75
|
+
result.extend(opening_tcp());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
result.extend(link_lookup(true));
|
|
79
|
+
result.extend(link_lookup(false));
|
|
80
|
+
result.extend(tuple_lookup(TUPLE_SET, false, reply));
|
|
81
|
+
result
|
|
82
|
+
};
|
|
83
|
+
program.end("forward", admission(false, 2, None), 1)?;
|
|
84
|
+
for transport in ["udp", "tcp"] {
|
|
85
|
+
program.end("forward", admission(false, 8, Some(transport)), 1)?;
|
|
86
|
+
}
|
|
87
|
+
program.end("forward", admission(true, 2, None), 1)
|
|
88
|
+
}
|