@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.
Files changed (44) hide show
  1. package/changelog.md +33 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/classes.manageddockerforwarding.js +3 -4
  8. package/dist_ts/classes.managednftables.d.ts +14 -2
  9. package/dist_ts/classes.managednftables.js +48 -8
  10. package/dist_ts/managed.egress.types.d.ts +25 -3
  11. package/dist_ts/managed.types.d.ts +14 -0
  12. package/package.json +7 -7
  13. package/readme.md +262 -64
  14. package/rust/src/docker.policy.rs +2 -1
  15. package/rust/src/egress.compile.rs +153 -76
  16. package/rust/src/egress.host.rs +24 -51
  17. package/rust/src/egress.hostgrant.rs +1 -1
  18. package/rust/src/egress.keys.rs +305 -0
  19. package/rust/src/egress.leased.rs +399 -0
  20. package/rust/src/egress.private.rs +197 -0
  21. package/rust/src/egress.published.rs +370 -0
  22. package/rust/src/egress.router.rs +61 -294
  23. package/rust/src/egress.rs +140 -19
  24. package/rust/src/egress_hostgrant_tests.rs +158 -20
  25. package/rust/src/egress_localport_tests.rs +11 -2
  26. package/rust/src/egress_publishedrange_tests.rs +812 -0
  27. package/rust/src/egress_routerequivalence_tests.rs +448 -0
  28. package/rust/src/egress_tests.rs +289 -230
  29. package/rust/src/egress_workloadgrant_tests.rs +21 -13
  30. package/rust/src/main.rs +57 -3
  31. package/rust/src/owner.rs +69 -14
  32. package/rust/src/owner_host_traffic_tests.rs +4 -0
  33. package/rust/src/owner_identity_tests.rs +166 -80
  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 +6 -0
  38. package/rust/src/policy.rs +30 -29
  39. package/rust/src/tests.rs +100 -0
  40. package/ts/00_commitinfo_data.ts +1 -1
  41. package/ts/classes.manageddockerforwarding.ts +2 -3
  42. package/ts/classes.managednftables.ts +46 -8
  43. package/ts/managed.egress.types.ts +25 -3
  44. package/ts/managed.types.ts +17 -0
@@ -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
- require(ports.len() <= 64)?;
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].transit_port != port.transit_port),
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.transit_port < range.first
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
- require(grants.len() <= 1024)?;
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
- require(value.links.len() <= 32 && value.generations.len() <= 32)?;
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
- .prepare()?;
642
- value.endpoints = private.policy.endpoints;
643
- value.rules = private.policy.rules;
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
- let mut count = 0;
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
- require(owners.len() <= LOCAL_TCP_PORT_OWNERS)?;
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
- require(ports.len() <= 64)?;
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].host_port != port.host_port),
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.host_port < range.first
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
- /// The compiled table: rules per chain and element keys per set.
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
- assert!(sets
222
- .insert(wire::text(&attributes, 2).unwrap(), Vec::new())
223
- .is_none());
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 keys = sets.get_mut(&wire::text(&attributes, 2).unwrap()).unwrap();
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
- keys.push(data(&key, 1));
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 { chains, sets }
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 => vec![0; 16],
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
- assert_eq!(wire::number(&fields, 5).unwrap(), 0);
371
- let keys = &table.sets[&wire::text(&fields, 1).unwrap()];
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
- matched = keys
374
- .iter()
375
- .any(|key| registers[at..at + key.len()] == key[..]);
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(&table(prepared), chain, flow, &mut zone) {
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
- assert!(table(&prepared(fixture)).sets.is_empty());
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 <= 100_000 && elements <= super::compile::ELEMENT_BYTES
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 the validation bound.
924
- assert!(normalized(granted(fixture, many(1025))).is_err());
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 + 100_000 + super::compile::ELEMENT_BYTES + 64 <= crate::wire::BATCH_BYTES);
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 <= 100_000 && elements <= super::compile::ELEMENT_BYTES);
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
- assert!(normalized(value).is_err());
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
  }