@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.
Files changed (41) hide show
  1. package/changelog.md +31 -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/managed.egress.types.d.ts +69 -3
  8. package/package.json +6 -6
  9. package/readme.md +342 -61
  10. package/rust/src/egress.compile.rs +157 -76
  11. package/rust/src/egress.host.rs +104 -44
  12. package/rust/src/egress.hostgrant.rs +14 -14
  13. package/rust/src/egress.keys.rs +305 -0
  14. package/rust/src/egress.leased.rs +399 -0
  15. package/rust/src/egress.poolguard.rs +45 -0
  16. package/rust/src/egress.private.rs +197 -0
  17. package/rust/src/egress.published.rs +370 -0
  18. package/rust/src/egress.router.rs +64 -295
  19. package/rust/src/egress.rs +248 -17
  20. package/rust/src/egress.workloadgrant.rs +88 -0
  21. package/rust/src/egress_hostgrant_tests.rs +196 -47
  22. package/rust/src/egress_localplatform_tests.rs +204 -0
  23. package/rust/src/egress_localport_tests.rs +266 -0
  24. package/rust/src/egress_publishedrange_tests.rs +812 -0
  25. package/rust/src/egress_routerequivalence_tests.rs +448 -0
  26. package/rust/src/egress_tests.rs +132 -230
  27. package/rust/src/egress_workloadgrant_tests.rs +313 -0
  28. package/rust/src/owner.rs +77 -15
  29. package/rust/src/owner_host_traffic_tests.rs +4 -0
  30. package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
  31. package/rust/src/owner_identity_tests.rs +228 -80
  32. package/rust/src/owner_localplatform_tests.rs +274 -0
  33. package/rust/src/owner_localport_tests.rs +241 -0
  34. package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
  35. package/rust/src/owner_router_traffic_tests.rs +2 -0
  36. package/rust/src/owner_scale_traffic_tests.rs +383 -0
  37. package/rust/src/owner_tests.rs +15 -0
  38. package/rust/src/owner_workloadgrant_tests.rs +278 -0
  39. package/rust/src/policy.rs +29 -25
  40. package/ts/00_commitinfo_data.ts +1 -1
  41. package/ts/managed.egress.types.ts +71 -3
@@ -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() <= 64)?;
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].transit_port != port.transit_port),
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.transit_port < range.first
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() <= 32 && value.generations.len() <= 32)?;
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
- .prepare()?;
577
- value.endpoints = private.policy.endpoints;
578
- value.rules = private.policy.rules;
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 <= 128)?;
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() <= 64)?;
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].host_port != port.host_port),
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.host_port < range.first
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
- .protection
886
- .platform_endpoints
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
+ }