@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
@@ -241,12 +241,6 @@ fn workload_grants_reject_unbound_same_link_local_platform_and_tun_ends() {
241
241
  json!([{"protocol":"tcp","sourceAddress":"10.241.0.2","destinationAddress":"10.241.1.2",
242
242
  "destinationPort":80,"bidirectional":true}]),
243
243
  ),
244
- granted(
245
- two_workloads(),
246
- json!((0..1025_u16)
247
- .map(|port| workload_grant("tcp", "10.241.0.2", "10.241.1.2", 1 + port))
248
- .collect::<Vec<_>>()),
249
- ),
250
244
  ] {
251
245
  assert!(
252
246
  normalized(invalid.clone()).is_err(),
@@ -254,6 +248,21 @@ fn workload_grants_reject_unbound_same_link_local_platform_and_tun_ends() {
254
248
  invalid["scope"]["workloadGrants"]
255
249
  );
256
250
  }
251
+ // The contract maximum is a capacity bound.
252
+ assert_eq!(
253
+ normalized(granted(
254
+ two_workloads(),
255
+ json!((0..1025_u16)
256
+ .map(|port| workload_grant("tcp", "10.241.0.2", "10.241.1.2", 1 + port))
257
+ .collect::<Vec<_>>()),
258
+ ))
259
+ .unwrap_err(),
260
+ crate::Error::Exhausted(crate::Bound {
261
+ name: "workloadGrants",
262
+ limit: 1024,
263
+ actual: 1025
264
+ })
265
+ );
257
266
  // Both directions between two workloads are two separate grants.
258
267
  assert!(normalized(granted(
259
268
  two_workloads(),
@@ -297,18 +306,17 @@ fn workload_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
297
306
  .sum()
298
307
  };
299
308
  let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
300
- assert!(program.len() < 768 && graph <= 100_000 && elements <= super::compile::ELEMENT_BYTES);
309
+ assert!(program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES);
301
310
  assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
302
311
  println!(
303
312
  "WORKLOAD_GRANT_MAX grants=1024 operations={} rule_bytes={graph} element_bytes={elements}",
304
313
  program.len()
305
314
  );
306
- // Host grants and workload grants share one element budget per target:
307
- // half the host grant maximum fits beside the workload maximum, both maxima
308
- // together are refused before any kernel work.
315
+ // Host grants and workload grants share the target budget with the rest of
316
+ // the scope's elements; both maxima together fit it.
309
317
  let mut both = granted(two_workloads(), json!(maximum));
310
- both["scope"]["hostGrants"] = many(512);
311
- normalized(both.clone()).unwrap().prepare().unwrap();
312
318
  both["scope"]["hostGrants"] = many(1024);
313
- assert!(normalized(both).unwrap().prepare().is_err());
319
+ let both = normalized(both).unwrap().prepare().unwrap();
320
+ let (operations, rule_bytes, element_bytes) = super::tests::cost(&both);
321
+ assert!(operations < 768 && rule_bytes + element_bytes <= super::compile::TARGET_BYTES);
314
322
  }
package/rust/src/main.rs CHANGED
@@ -15,9 +15,22 @@ use std::io::{BufRead, Read, Write};
15
15
  /// same lines. A transition carries two complete policies.
16
16
  pub(crate) const IPC_BYTES: usize = 1_048_576;
17
17
 
18
+ /// A capacity bound of the atomic replacement budget that a policy exceeded: the
19
+ /// stable bound name, its limit and the size the policy reached. Input counts and
20
+ /// the schema-v1 budget are exact; the other compiled budgets stop at the first
21
+ /// message or rule past the limit, so the complete policy needs at least `actual`.
22
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
23
+ pub struct Bound {
24
+ pub name: &'static str,
25
+ pub limit: usize,
26
+ pub actual: usize,
27
+ }
28
+
18
29
  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
19
30
  pub enum Error {
20
31
  Invalid,
32
+ /// The policy does not fit a capacity bound; a smaller policy may fit.
33
+ Exhausted(Bound),
21
34
  Unsupported,
22
35
  UnsupportedBackend,
23
36
  Permission,
@@ -28,10 +41,23 @@ pub enum Error {
28
41
  Unavailable,
29
42
  }
30
43
  pub type Result<T> = std::result::Result<T, Error>;
44
+ /// Refuses `actual` above the capacity bound `name`.
45
+ pub fn capacity(name: &'static str, limit: usize, actual: usize) -> Result<()> {
46
+ if actual > limit {
47
+ Err(Error::Exhausted(Bound {
48
+ name,
49
+ limit,
50
+ actual,
51
+ }))
52
+ } else {
53
+ Ok(())
54
+ }
55
+ }
31
56
  impl Error {
32
57
  pub fn code(self) -> &'static str {
33
58
  match self {
34
59
  Self::Invalid => "INVALID",
60
+ Self::Exhausted(_) => "EXHAUSTED",
35
61
  Self::Unsupported => "UNSUPPORTED_KERNEL",
36
62
  Self::UnsupportedBackend => "UNSUPPORTED_BACKEND",
37
63
  Self::Permission => "PERMISSION",
@@ -42,6 +68,26 @@ impl Error {
42
68
  Self::Unavailable => "UNAVAILABLE",
43
69
  }
44
70
  }
71
+ /// The bounded, non-secret refusal text of the response.
72
+ pub fn message(self) -> String {
73
+ match self {
74
+ Self::Invalid => "Managed policy input is invalid.".into(),
75
+ Self::Exhausted(bound) => format!(
76
+ "Managed policy exceeds the {} capacity of {}: it reached {}.",
77
+ bound.name, bound.limit, bound.actual
78
+ ),
79
+ _ => "Managed policy operation failed.".into(),
80
+ }
81
+ }
82
+ /// The structured refusal details of the response, if any.
83
+ pub fn data(self) -> Option<serde_json::Value> {
84
+ match self {
85
+ Self::Exhausted(bound) => Some(
86
+ serde_json::json!({"bound":bound.name,"limit":bound.limit,"actual":bound.actual}),
87
+ ),
88
+ _ => None,
89
+ }
90
+ }
45
91
  pub fn kernel(code: i32) -> Self {
46
92
  match -code {
47
93
  libc::ENOENT => Self::Absent,
@@ -114,6 +160,16 @@ fn dispatch(owner: &mut Option<owner::Owner>, request: Request) -> Result<serde_
114
160
  }
115
161
  }
116
162
 
163
+ /// The failure response line: the refusal text, the stable code and, for a
164
+ /// capacity refusal, its bound.
165
+ fn failure(id: &str, error: Error) -> serde_json::Value {
166
+ let mut value = serde_json::json!({"id":id,"success":false,"error":error.message(),"errorCode":error.code()});
167
+ if let Some(data) = error.data() {
168
+ value["errorData"] = data;
169
+ }
170
+ value
171
+ }
172
+
117
173
  fn main() {
118
174
  let arguments = std::env::args().skip(1).collect::<Vec<_>>();
119
175
  let (docker_mode, descriptor) = match arguments.as_slice() {
@@ -163,9 +219,7 @@ fn main() {
163
219
  let id = request.id.clone();
164
220
  let response = match if docker_mode { docker::dispatch(&mut docker, request) } else { dispatch(&mut owner, request) } {
165
221
  Ok(result) => serde_json::json!({"id":id,"success":true,"result":result}),
166
- Err(error) => {
167
- serde_json::json!({"id":id,"success":false,"error":"Managed policy operation failed.","errorCode":error.code()})
168
- }
222
+ Err(error) => failure(&id, error),
169
223
  };
170
224
  if writeln!(output, "{response}")
171
225
  .and_then(|_| output.flush())
package/rust/src/owner.rs CHANGED
@@ -680,7 +680,8 @@ fn chain_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
680
680
  }
681
681
  serde_json::to_vec(&wire::canonical(&identity, false)?).map_err(|_| Error::Protocol)
682
682
  }
683
- /// A set is its table, name, flags, key type and key length. The handle, the
683
+ /// A set is its table, name, flags, key type and key length, a map's data type
684
+ /// and length, and the field lengths of a concatenation. The handle, the
684
685
  /// informational backend type and count, the transaction ID and an empty
685
686
  /// description are not identity; anything else rejects.
686
687
  fn set_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
@@ -688,30 +689,78 @@ fn set_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
688
689
  for attribute in attributes {
689
690
  match attribute.id() {
690
691
  10 | 16 | 19 | 20 => {}
691
- 9 if wire::attrs(&attribute.value)?.is_empty() => {}
692
- 1..=5 => identity.push(attribute.clone()),
692
+ 9 => {
693
+ if let Some(fields) = concatenation(&attribute.value)? {
694
+ identity.push(Attr::bytes(9, fields));
695
+ }
696
+ }
697
+ 1..=7 => identity.push(attribute.clone()),
693
698
  _ => return Err(Error::Conflict),
694
699
  }
695
700
  }
696
701
  for id in [1, 2, 3, 4, 5] {
697
702
  wire::one(&identity, id)?;
698
703
  }
704
+ // A map carries its data type and length, and nothing else does.
705
+ let map = wire::number(&identity, 3)? & 8 != 0;
706
+ for id in [6, 7] {
707
+ if wire::one(&identity, id).is_ok() != map {
708
+ return Err(Error::Conflict);
709
+ }
710
+ }
699
711
  serde_json::to_vec(&wire::canonical(&identity, false)?).map_err(|_| Error::Protocol)
700
712
  }
701
- /// Only plain keys: an element with data, flags, timeouts, expressions or any
702
- /// other extension is not one the compiler created.
713
+ /// The field lengths of a set description: only a concatenation, never a size.
714
+ /// An empty description is none.
715
+ fn concatenation(value: &[u8]) -> Result<Option<Vec<u8>>> {
716
+ let description = wire::attrs(value)?;
717
+ if description.is_empty() {
718
+ return Ok(None);
719
+ }
720
+ if description.len() != 1 || description[0].id() != 2 {
721
+ return Err(Error::Conflict);
722
+ }
723
+ let mut lengths = Vec::new();
724
+ for field in wire::attrs(&description[0].value)? {
725
+ let length = wire::attrs(&field.value)?;
726
+ if field.id() != 1 || length.len() != 1 {
727
+ return Err(Error::Conflict);
728
+ }
729
+ lengths.extend(wire::number(&length, 1)?.to_be_bytes());
730
+ }
731
+ Ok(Some(lengths))
732
+ }
733
+ /// Only compiler elements: a key, the last key of an interval and a map's data.
734
+ /// Flags, timeouts, expressions or any other extension reject. The identity is
735
+ /// the three values, so any changed bound or datum is a changed element.
703
736
  fn element_keys(list: &[Attr]) -> Result<Vec<Vec<u8>>> {
704
737
  let mut keys = Vec::new();
705
738
  for element in list {
706
- let fields = wire::attrs(&element.value)?;
707
- if element.id() != 1 || fields.len() != 1 {
739
+ if element.id() != 1 {
708
740
  return Err(Error::Conflict);
709
741
  }
710
- let key = wire::attrs(&wire::one(&fields, 1)?.value)?;
711
- if key.len() != 1 {
712
- return Err(Error::Conflict);
742
+ let mut values: [Option<Vec<u8>>; 3] = [None, None, None];
743
+ for field in wire::attrs(&element.value)? {
744
+ let slot = match field.id() {
745
+ 1 => 0,
746
+ 10 => 1,
747
+ 2 => 2,
748
+ _ => return Err(Error::Conflict),
749
+ };
750
+ let value = wire::attrs(&field.value)?;
751
+ if value.len() != 1 || values[slot].is_some() {
752
+ return Err(Error::Conflict);
753
+ }
754
+ values[slot] = Some(wire::one(&value, 1)?.value.clone());
713
755
  }
714
- keys.push(wire::one(&key, 1)?.value.clone());
756
+ let [Some(key), last, data] = values else {
757
+ return Err(Error::Conflict);
758
+ };
759
+ keys.push(if last.is_none() && data.is_none() {
760
+ key
761
+ } else {
762
+ serde_json::to_vec(&(key, last, data)).map_err(|_| Error::Protocol)?
763
+ });
715
764
  }
716
765
  Ok(keys)
717
766
  }
@@ -772,7 +821,7 @@ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
772
821
  /// neither unknown fields nor default repair may hide a changed kernel rule.
773
822
  fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
774
823
  let allowed: &[u16] = match name {
775
- "lookup" => &[1, 2, 5],
824
+ "lookup" => &[1, 2, 3, 5],
776
825
  "ct" | "range" => &[1, 2, 3, 4],
777
826
  "nat" => &[1, 2, 3, 4, 5, 6, 7],
778
827
  "reject" => &[1],
@@ -786,10 +835,16 @@ fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
786
835
  }
787
836
  match name {
788
837
  "lookup" => {
789
- // A plain membership test of a named set: no map, no inversion.
838
+ // A membership or absence test of a named set, or a map lookup into
839
+ // a data register, which is never inverted.
790
840
  wire::text(data, 1)?;
791
841
  wire::number(data, 2)?;
792
- if data.len() != 3 || wire::number(data, 5)? != 0 {
842
+ let map = wire::one(data, 3).is_ok();
843
+ if map {
844
+ wire::number(data, 3)?;
845
+ }
846
+ let flags = wire::number(data, 5)?;
847
+ if data.len() != 3 + usize::from(map) || flags > 1 || map && flags != 0 {
793
848
  return Err(Error::Conflict);
794
849
  }
795
850
  }
@@ -389,6 +389,8 @@ fn v2_host_publishes_exact_uplink_ports_and_removes_them_with_the_generation() {
389
389
  target_port: 8443,
390
390
  target_address: "10.240.0.2".into(),
391
391
  host_ip: "192.0.2.2".into(),
392
+ host_port_end: None,
393
+ symmetric: false,
392
394
  },
393
395
  egress::PublishedPort {
394
396
  protocol: "udp".into(),
@@ -396,6 +398,8 @@ fn v2_host_publishes_exact_uplink_ports_and_removes_them_with_the_generation() {
396
398
  target_port: 8125,
397
399
  target_address: "10.240.0.2".into(),
398
400
  host_ip: "192.0.2.2".into(),
401
+ host_port_end: None,
402
+ symmetric: false,
399
403
  },
400
404
  ];
401
405
  let mut owner = Owner::new(options("host_published")).unwrap();
@@ -168,94 +168,162 @@ fn egress_identity_rejects_unknown_duplicate_or_incomplete_operands() {
168
168
  }
169
169
  }
170
170
 
171
+ /// The compiled chains, rules, and sets with their element identities of one
172
+ /// prepared policy, as the owner's graph comparison sees them.
173
+ type Compiled = (
174
+ Vec<Vec<u8>>,
175
+ Vec<(String, Vec<u8>)>,
176
+ std::collections::BTreeMap<String, (Vec<u8>, Vec<Vec<u8>>)>,
177
+ );
178
+ fn compiled(value: serde_json::Value) -> Compiled {
179
+ let prepared = serde_json::from_value::<crate::egress::Policy>(value)
180
+ .unwrap()
181
+ .prepare()
182
+ .unwrap();
183
+ let (mut chains, mut rules) = (Vec::new(), Vec::new());
184
+ let mut sets = std::collections::BTreeMap::new();
185
+ for (kind, attributes) in prepared.program("snft_identity").unwrap() {
186
+ match kind {
187
+ 3 => chains.push(super::chain_identity(&attributes).unwrap()),
188
+ 6 => {
189
+ // The stamp carries the policy digest; compare the rule itself.
190
+ let unstamped: Vec<Attr> = attributes
191
+ .iter()
192
+ .map(|attribute| match attribute.id() {
193
+ 7 => Attr::bytes(7, b"stamp".to_vec()),
194
+ _ => attribute.clone(),
195
+ })
196
+ .collect();
197
+ rules.push(rule_identity(&unstamped).unwrap());
198
+ }
199
+ 9 => {
200
+ // The kernel's dump of the same set has the same identity.
201
+ let identity = set_identity(&attributes).unwrap();
202
+ assert_eq!(set_identity(&dumped_set(&attributes)).unwrap(), identity);
203
+ sets.insert(wire::text(&attributes, 2).unwrap(), (identity, Vec::new()));
204
+ }
205
+ 12 => {
206
+ let list = wire::attrs(&wire::one(&attributes, 3).unwrap().value).unwrap();
207
+ sets.get_mut(&wire::text(&attributes, 2).unwrap())
208
+ .unwrap()
209
+ .1
210
+ .extend(element_keys(&list).unwrap());
211
+ }
212
+ other => panic!("operation {other}"),
213
+ }
214
+ }
215
+ (chains, rules, sets)
216
+ }
217
+
171
218
  /// A published host-transit generation must survive the same apply-time graph
172
- /// identity as the rest of the table: the kernel dump is compared operand by
173
- /// operand, so destination NAT and its admissions cannot be silently rewritten.
219
+ /// identity as the rest of the table: its rules and every set element are
220
+ /// compared operand by operand, so destination NAT and its admissions cannot be
221
+ /// silently rewritten, while the rules stay the same for any publication.
174
222
  #[test]
175
223
  fn published_host_graph_identity_preserves_destination_nat_and_admission_operands() {
176
- let compiled = |target_port: u16| {
177
- let value = crate::egress::tests::published_host(serde_json::json!([
224
+ let host = |target_port: u16| {
225
+ compiled(crate::egress::tests::published_host(serde_json::json!([
178
226
  crate::egress::tests::published_port("tcp", 443, target_port)
179
- ]));
180
- let prepared = serde_json::from_value::<crate::egress::Policy>(value)
181
- .unwrap()
182
- .prepare()
183
- .unwrap();
184
- let mut chains = Vec::new();
185
- let mut rules = Vec::new();
186
- for (kind, attributes) in prepared.program("snft_identity").unwrap() {
187
- if kind == 3 {
188
- chains.push(super::chain_identity(&attributes).unwrap());
189
- } else {
190
- rules.push(rule_identity(&attributes).unwrap());
191
- }
192
- }
193
- (chains, rules)
227
+ ])))
194
228
  };
195
- let (chains, rules) = compiled(8443);
229
+ let (chains, rules, sets) = host(8443);
196
230
  assert_eq!(rules.iter().filter(|(chain, _)| chain == "pre").count(), 1);
197
231
  assert_eq!(chains.len(), 7);
198
- let translation = rules.iter().find(|(chain, _)| chain == "pre").unwrap();
199
- let (_, changed) = compiled(9443);
200
- let other = changed.iter().find(|(chain, _)| chain == "pre").unwrap();
201
- assert_ne!(translation.1, other.1);
202
- assert_eq!(
203
- rules.iter().filter(|(chain, _)| chain == "forward").count(),
204
- changed.iter().filter(|(chain, _)| chain == "forward").count()
205
- );
232
+ let (_, changed_rules, changed_sets) = host(9443);
233
+ assert_eq!(rules, changed_rules);
234
+ // The translation's port is map data and the admission's port is a key.
235
+ for name in ["published_port", "published_forward"] {
236
+ assert_eq!(sets[name].0, changed_sets[name].0);
237
+ assert_ne!(sets[name].1, changed_sets[name].1, "{name}");
238
+ }
206
239
  }
207
240
 
208
241
  /// A published router generation must survive the same apply-time graph identity
209
- /// as the rest of the table: the kernel dump is compared operand by operand, so
210
- /// the workload translation and its admissions cannot be silently rewritten.
242
+ /// as the rest of the table: its rules and every set element are compared operand
243
+ /// by operand, so the workload translation, both classifications and the
244
+ /// admissions cannot be silently rewritten.
211
245
  #[test]
212
246
  fn published_router_graph_identity_preserves_destination_nat_and_admission_operands() {
213
- let compiled = |endpoint_port: u16| {
214
- let value = crate::egress::tests::published_router(serde_json::json!([
247
+ let router = |endpoint_port: u16| {
248
+ compiled(crate::egress::tests::published_router(serde_json::json!([
215
249
  crate::egress::tests::router_published_port("tcp", 443, endpoint_port)
216
- ]));
217
- let prepared = serde_json::from_value::<crate::egress::Policy>(value)
218
- .unwrap()
219
- .prepare()
220
- .unwrap();
221
- let mut chains = Vec::new();
222
- let mut rules = Vec::new();
223
- for (kind, attributes) in prepared.program("snft_identity").unwrap() {
224
- if kind == 3 {
225
- chains.push(super::chain_identity(&attributes).unwrap());
226
- } else {
227
- rules.push(rule_identity(&attributes).unwrap());
228
- }
229
- }
230
- (chains, rules)
250
+ ])))
231
251
  };
232
- let (chains, rules) = compiled(8443);
252
+ let (chains, rules, sets) = router(8443);
233
253
  assert_eq!(rules.iter().filter(|(chain, _)| chain == "pre").count(), 1);
234
- assert_eq!(chains.len(), 16);
235
- let translation = rules.iter().find(|(chain, _)| chain == "pre").unwrap();
236
- let (_, changed) = compiled(9443);
237
- let other = changed.iter().find(|(chain, _)| chain == "pre").unwrap();
238
- assert_ne!(translation.1, other.1);
239
- assert_eq!(
240
- rules.iter().filter(|(chain, _)| chain == "forward").count(),
241
- changed.iter().filter(|(chain, _)| chain == "forward").count()
242
- );
243
- // The handoff ingress classification is part of the same identity.
254
+ // The router's base chains, the pre chain and its three regular chains.
255
+ assert_eq!(chains.len(), 10);
256
+ let (_, changed_rules, changed_sets) = router(9443);
257
+ assert_eq!(rules, changed_rules);
258
+ for name in ["published_port", "published_forward", "published_endpoint"] {
259
+ assert_ne!(sets[name].1, changed_sets[name].1, "{name}");
260
+ }
261
+ assert_eq!(sets["published_arrival"], changed_sets["published_arrival"]);
262
+ }
263
+
264
+ /// Interval elements carry their last key and a map's data in their identity;
265
+ /// a changed bound or datum is a changed element, and a set description is only
266
+ /// ever a concatenation.
267
+ #[test]
268
+ fn interval_sets_and_elements_have_exact_graph_identity() {
269
+ let (_, _, sets) = compiled(crate::egress::tests::published_router(serde_json::json!([
270
+ crate::egress::tests::router_published_port("tcp", 443, 8443)
271
+ ])));
244
272
  assert_eq!(
245
- rules.iter().filter(|(chain, _)| chain == "raw_return").count(),
246
- changed
247
- .iter()
248
- .filter(|(chain, _)| chain == "raw_return")
249
- .count()
273
+ sets.keys().filter(|name| name.starts_with("published_")).count(),
274
+ 4
250
275
  );
276
+ let element = |last: u8, data: Option<u8>| {
277
+ let value = |kind: u16, byte: u8| Attr::nested(kind, vec![Attr::bytes(1, vec![byte; 8])]);
278
+ let mut fields = vec![value(1, 1), value(10, last)];
279
+ if let Some(data) = data {
280
+ fields.push(value(2, data));
281
+ }
282
+ Attr::nested(1, fields)
283
+ };
284
+ let identity = |attribute: Attr| element_keys(&[attribute]).unwrap();
285
+ let exact = identity(element(1, Some(4)));
286
+ assert_ne!(identity(element(2, Some(4))), exact);
287
+ assert_ne!(identity(element(1, Some(5))), exact);
288
+ assert_ne!(identity(element(1, None)), exact);
289
+ for extension in [
290
+ Attr::u32(3, 1),
291
+ Attr::nested(10, vec![Attr::bytes(1, vec![1; 8])]),
292
+ ] {
293
+ let Attr { value, .. } = element(1, Some(4));
294
+ let mut fields = wire::attrs(&value).unwrap();
295
+ fields.push(extension);
296
+ assert!(element_keys(&[Attr::nested(1, fields)]).is_err());
297
+ }
251
298
  }
252
299
 
253
- /// A host-grant set as Linux dumps it: the compiled creation plus the kernel's
254
- /// handle, an empty description, the backend type and the element count.
300
+ /// A set as Linux dumps it: the compiled creation plus the kernel's handle, its
301
+ /// description (empty for a plain key, the concatenation's field lengths without
302
+ /// nesting flags otherwise), the backend type and the element count.
255
303
  fn dumped_set(created: &[Attr]) -> Vec<Attr> {
256
- let mut dump: Vec<Attr> = created.iter().filter(|attribute| attribute.id() != 10).cloned().collect();
304
+ let mut dump: Vec<Attr> = created
305
+ .iter()
306
+ .filter(|attribute| ![9, 10].contains(&attribute.id()))
307
+ .cloned()
308
+ .collect();
257
309
  dump.push(Attr::u64(16, 7));
258
- dump.push(Attr::nested(9, vec![]));
310
+ let fields = created
311
+ .iter()
312
+ .find(|attribute| attribute.id() == 9)
313
+ .map(|description| {
314
+ let concatenation = wire::attrs(&description.value).unwrap();
315
+ wire::attrs(&wire::one(&concatenation, 2).unwrap().value)
316
+ .unwrap()
317
+ .iter()
318
+ .map(|field| {
319
+ Attr::bytes(1, wire::encode_attrs(&[Attr::u32(1, wire::number(&wire::attrs(&field.value).unwrap(), 1).unwrap())]))
320
+ })
321
+ .collect::<Vec<_>>()
322
+ });
323
+ dump.push(match fields {
324
+ Some(fields) => Attr::bytes(9, wire::encode_attrs(&[Attr::bytes(2, wire::encode_attrs(&fields))])),
325
+ None => Attr::nested(9, vec![]),
326
+ });
259
327
  dump.push(Attr::string(19, "nft_rhash_type"));
260
328
  dump.push(Attr::u32(20, 1024));
261
329
  dump
@@ -273,7 +341,11 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
273
341
  .prepare()
274
342
  .unwrap();
275
343
  let program = prepared.program("snft_identity").unwrap();
276
- let sets: Vec<_> = program.iter().filter(|(kind, _)| *kind == 9).map(|(_, attributes)| attributes.clone()).collect();
344
+ let sets: Vec<_> = program
345
+ .iter()
346
+ .filter(|(kind, attributes)| *kind == 9 && wire::text(attributes, 2).unwrap().starts_with("host_grant"))
347
+ .map(|(_, attributes)| attributes.clone())
348
+ .collect();
277
349
  assert_eq!(sets.len(), 2);
278
350
  for created in &sets {
279
351
  // The kernel's informational attributes are not identity.
@@ -294,21 +366,30 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
294
366
  assert!(set_identity(&changed).is_err());
295
367
  }
296
368
  }
297
- // Elements are plain keys; any extension rejects.
298
- let (_, elements) = program.iter().find(|(kind, _)| *kind == 12).unwrap();
369
+ // Elements are plain keys; flags reject, and data is a different element.
370
+ let (_, elements) = program
371
+ .iter()
372
+ .find(|(kind, attributes)| *kind == 12 && wire::text(attributes, 2).unwrap() == "host_grant")
373
+ .unwrap();
299
374
  let list = wire::attrs(&wire::one(elements, 3).unwrap().value).unwrap();
300
375
  assert_eq!(element_keys(&list).unwrap().len(), 1);
301
376
  let key = wire::attrs(&list[0].value).unwrap();
302
- for extension in [Attr::u32(3, 1), Attr::nested(2, vec![Attr::bytes(1, vec![1; 4])])] {
303
- let mut changed = key.clone();
304
- changed.push(extension);
305
- assert!(element_keys(&[Attr::nested(1, changed)]).is_err());
306
- }
307
- // Every lookup names its set with a plain, uninverted membership test, and
308
- // the key registers are part of identity.
377
+ let mut flagged = key.clone();
378
+ flagged.push(Attr::u32(3, 1));
379
+ assert!(element_keys(&[Attr::nested(1, flagged)]).is_err());
380
+ let mut mapped = key.clone();
381
+ mapped.push(Attr::nested(2, vec![Attr::bytes(1, vec![1; 4])]));
382
+ assert_ne!(
383
+ element_keys(&[Attr::nested(1, mapped)]).unwrap(),
384
+ element_keys(&list[..1]).unwrap()
385
+ );
386
+ // Five rules look up the host grant sets (the router's private and leased
387
+ // sets have their own lookups); the set name and key registers are identity.
309
388
  let lookups: Vec<_> = program
310
389
  .iter()
311
- .filter(|(kind, attributes)| *kind == 6 && rule_identity(attributes).unwrap().1.windows(6).any(|window| window == b"lookup"))
390
+ .filter(|(kind, attributes)| {
391
+ *kind == 6 && wire::encode_attrs(attributes).windows(10).any(|window| window == b"host_grant")
392
+ })
312
393
  .collect();
313
394
  assert_eq!(lookups.len(), 5);
314
395
  let lookup = |data: Vec<Attr>| rule(vec![expression("lookup", data)]);
@@ -317,13 +398,18 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
317
398
  for changed in [
318
399
  vec![Attr::string(1, "host_grant_arrival"), Attr::u32(2, 1), Attr::u32(5, 0)],
319
400
  vec![Attr::string(1, "host_grant"), Attr::u32(2, 9), Attr::u32(5, 0)],
401
+ // A map lookup's data register and an absence test are identity.
402
+ vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2), Attr::u32(5, 0)],
403
+ vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(5, 1)],
320
404
  ] {
321
405
  assert_ne!(rule_identity(&lookup(changed)).unwrap(), identity);
322
406
  }
323
407
  for invalid in [
324
- vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(5, 1)],
408
+ vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(5, 2)],
325
409
  vec![Attr::string(1, "host_grant"), Attr::u32(2, 1)],
326
- vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2), Attr::u32(5, 0)],
410
+ vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2)],
411
+ // A map lookup is never inverted.
412
+ vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2), Attr::u32(5, 1)],
327
413
  ] {
328
414
  assert!(rule_identity(&lookup(invalid)).is_err());
329
415
  }