@push.rocks/smartnftables 4.4.0 → 4.5.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.
@@ -57,7 +57,7 @@ fn host_grant_return(program: &mut Program<'_>, scope: &RouterScope) -> Result<(
57
57
  if !scope.host_grants.is_empty() {
58
58
  let mut expressions = ipv4();
59
59
  expressions.extend(link(&scope.handoff, true));
60
- expressions.extend(hostgrant::arrival_lookup());
60
+ expressions.extend(hostgrant::arrival_lookup()?);
61
61
  program.end("raw_return", expressions, 1)?;
62
62
  }
63
63
  Ok(())
@@ -1344,6 +1344,14 @@ impl Prepared {
1344
1344
  pub fn program(&self, table: &str) -> Result<Vec<(u16, Vec<crate::wire::Attr>)>> {
1345
1345
  compile::compile(self, table)
1346
1346
  }
1347
+ /// Every set the compiled graph declares, with its key and data fields.
1348
+ #[cfg(test)]
1349
+ pub(crate) fn declared(
1350
+ &self,
1351
+ table: &str,
1352
+ ) -> Result<std::collections::BTreeMap<String, compile::SetDeclaration>> {
1353
+ compile::declared(self, table)
1354
+ }
1347
1355
  pub fn validate_interfaces(&self) -> Result<()> {
1348
1356
  let links: Vec<&LocalLink> = match &self.policy.scope {
1349
1357
  Scope::RouterEgress(scope) => scope
@@ -6,7 +6,8 @@
6
6
  //! exact link that holds it (index and name), and both the incoming and the
7
7
  //! outgoing link are looked up on every packet. Only the grant's source opens;
8
8
  //! the destination only answers.
9
- use super::hostgrant::{lookup, meta_to, payload_to, tuple_key, tuple_lookup};
9
+ use super::hostgrant::{tuple_key, tuple_lookup, TUPLE};
10
+ use super::keys::{Field, Loads, IIF, IIFNAME, IP_SADDR};
10
11
  use super::*;
11
12
  use std::collections::BTreeSet;
12
13
 
@@ -14,16 +15,16 @@ use std::collections::BTreeSet;
14
15
  pub(super) const TUPLE_SET: &str = "workload_grant";
15
16
  /// Every grant address with the exact link that holds it.
16
17
  pub(super) const LINK_SET: &str = "workload_link";
18
+ /// A grant address and its link, as the incoming direction loads them.
19
+ const LINK: [Field; 3] = [IIF, IIFNAME, IP_SADDR];
17
20
 
18
21
  /// The link a packet crosses and the address on its side of that link: the
19
22
  /// 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
- ]
23
+ fn link_lookup(incoming: bool) -> Result<Vec<Attr>> {
24
+ Loads::new()
25
+ .link(incoming)
26
+ .address(incoming)
27
+ .lookup(LINK_SET, &LINK)
27
28
  }
28
29
  fn link_key(link: &LocalLink, address: &str) -> Result<Vec<u8>> {
29
30
  let mut key = link.interface_index.to_ne_bytes().to_vec();
@@ -49,9 +50,9 @@ pub(super) fn sets(program: &mut Program<'_>, scope: &RouterScope) -> Result<()>
49
50
  )?);
50
51
  }
51
52
  }
52
- program.set(TUPLE_SET, 3, 32)?;
53
+ program.exact_set(TUPLE_SET, 3, &TUPLE, None)?;
53
54
  program.elements(TUPLE_SET, keys)?;
54
- program.set(LINK_SET, 4, 24)?;
55
+ program.exact_set(LINK_SET, 4, &LINK, None)?;
55
56
  program.elements(LINK_SET, links.into_iter().collect())
56
57
  }
57
58
 
@@ -64,25 +65,26 @@ pub(super) fn admissions(program: &mut Program<'_>, scope: &RouterScope) -> Resu
64
65
  if scope.workload_grants.is_empty() {
65
66
  return Ok(());
66
67
  }
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());
68
+ let admission =
69
+ |reply: bool, connection_state: u32, transport: Option<&str>| -> Result<Vec<Attr>> {
70
+ let mut result = ipv4();
71
+ result.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
72
+ result.extend(ct(1, None, vec![u8::from(reply)]));
73
+ result.extend(state(connection_state));
74
+ if let Some(transport) = transport {
75
+ result.extend(meta(16, vec![protocol(transport)]));
76
+ if transport == "tcp" {
77
+ result.extend(opening_tcp());
78
+ }
76
79
  }
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)?;
80
+ result.extend(link_lookup(true)?);
81
+ result.extend(link_lookup(false)?);
82
+ result.extend(tuple_lookup(TUPLE_SET, false, reply)?);
83
+ Ok(result)
84
+ };
85
+ program.end("forward", admission(false, 2, None)?, 1)?;
84
86
  for transport in ["udp", "tcp"] {
85
- program.end("forward", admission(false, 8, Some(transport)), 1)?;
87
+ program.end("forward", admission(false, 8, Some(transport))?, 1)?;
86
88
  }
87
- program.end("forward", admission(true, 2, None), 1)
89
+ program.end("forward", admission(true, 2, None)?, 1)
88
90
  }
@@ -0,0 +1,150 @@
1
+ //! Set declarations against what nftables 1.0.9 itself writes. Each expected
2
+ //! value was dumped from `nft --debug=mnl add set|map ...` with the host nft
3
+ //! 1.0.9 (libnftnl 1.2.6) on x86_64; userdata numbers are host byte order.
4
+ use super::*;
5
+
6
+ fn hex(text: &str) -> Vec<u8> {
7
+ let digits: String = text.split_whitespace().collect();
8
+ (0..digits.len())
9
+ .step_by(2)
10
+ .map(|at| u8::from_str_radix(&digits[at..at + 2], 16).unwrap())
11
+ .collect()
12
+ }
13
+
14
+ /// `typeof ip saddr . th dport . ct original ip saddr . ct zone`
15
+ #[test]
16
+ #[cfg(target_endian = "little")]
17
+ fn concatenated_key_types_and_userdata_are_nfts() {
18
+ let key = [IP_SADDR, TH_DPORT, CT_ORIGINAL_SADDR, CT_ZONE];
19
+ assert_eq!(datatype(&key), 0x001c_d1c4);
20
+ assert_eq!(key_length(&key), 16);
21
+ assert_eq!(
22
+ userdata(&key, None).unwrap(),
23
+ hex("00 04 00000000 03 60 00 04 0d000000 01 58
24
+ 00 14 00 04 07000000 01 0c 00 04 0c000000 01 04 0b000000
25
+ 01 14 00 04 07000000 01 0c 00 04 0b000000 01 04 02000000
26
+ 02 14 00 04 0c000000 01 0c 00 04 13000000 01 04 00000000
27
+ 03 14 00 04 0c000000 01 0c 00 04 11000000 01 04 ffffffff")
28
+ );
29
+ // `typeof meta oif . meta oifname . ct reply ip saddr . ct reply proto-src`
30
+ let key = [OIF, OIFNAME, CT_REPLY_SADDR, CT_REPLY_SPORT];
31
+ assert_eq!(datatype(&key), 0x0052_91cd);
32
+ assert_eq!(key_length(&key), 28);
33
+ assert_eq!(
34
+ userdata(&key, None).unwrap(),
35
+ hex("00 04 00000000 03 54 00 04 0d000000 01 4c
36
+ 00 0e 00 04 09000000 01 06 00 04 05000000
37
+ 01 0e 00 04 09000000 01 06 00 04 07000000
38
+ 02 14 00 04 0c000000 01 0c 00 04 13000000 01 04 01000000
39
+ 03 14 00 04 0c000000 01 0c 00 04 0b000000 01 04 01000000")
40
+ );
41
+ }
42
+
43
+ /// `typeof meta l4proto . ct protocol . ip daddr : ip saddr . th sport . th dport`
44
+ /// and `typeof ct zone : ct label`.
45
+ #[test]
46
+ #[cfg(target_endian = "little")]
47
+ fn map_data_types_and_userdata_are_nfts() {
48
+ let key = [L4PROTO, CT_PROTOCOL, IP_DADDR];
49
+ let data = [IP_SADDR, TH_SPORT, TH_DPORT];
50
+ assert_eq!(datatype(&key), 0xc307);
51
+ assert_eq!(datatype(&data), 0x734d);
52
+ assert_eq!((key_length(&key), data_length(&data)), (12, 12));
53
+ assert_eq!(
54
+ userdata(&key, Some(&data)).unwrap(),
55
+ hex("00 04 00000000 01 04 00000000
56
+ 03 44 00 04 0d000000 01 3c
57
+ 00 0e 00 04 09000000 01 06 00 04 10000000
58
+ 01 14 00 04 0c000000 01 0c 00 04 0a000000 01 04 ffffffff
59
+ 02 14 00 04 07000000 01 0c 00 04 0c000000 01 04 0c000000
60
+ 04 4a 00 04 0d000000 01 42
61
+ 00 14 00 04 07000000 01 0c 00 04 0c000000 01 04 0b000000
62
+ 01 14 00 04 07000000 01 0c 00 04 0b000000 01 04 01000000
63
+ 02 14 00 04 07000000 01 0c 00 04 0b000000 01 04 02000000
64
+ 06 04 00000000")
65
+ );
66
+ let (key, data) = ([CT_ZONE], [CT_LABEL]);
67
+ assert_eq!((datatype(&key), datatype(&data)), (4, 30));
68
+ assert_eq!(data_length(&data), 16);
69
+ assert_eq!(
70
+ userdata(&key, Some(&data)).unwrap(),
71
+ hex("00 04 01000000 01 04 01000000
72
+ 03 14 00 04 0c000000 01 0c 00 04 11000000 01 04 ffffffff
73
+ 04 14 00 04 0c000000 01 0c 00 04 0d000000 01 04 ffffffff
74
+ 06 04 00000000")
75
+ );
76
+ // nft declares the zone alone as its 2 bytes; this map keeps the zone
77
+ // padded to its register, as every release before typed sets did.
78
+ assert_eq!(key_length(&key), 4);
79
+ }
80
+
81
+ /// Beyond four fields nft 1.0.9 cannot describe a key by `typeof`: its
82
+ /// concatenation userdata stops at `NFT_REG32_SIZE` (4) entries, and the type
83
+ /// keeps 32 bits, six per field, dropping what a longer key shifts out.
84
+ #[test]
85
+ fn longer_keys_carry_their_type_and_byte_order_alone() {
86
+ let key = [IIF, L4PROTO, IP_SADDR, IP_DADDR, TH_DPORT];
87
+ assert_eq!(
88
+ datatype(&key),
89
+ (20 << 24) | (12 << 18) | (7 << 12) | (7 << 6) | 13
90
+ );
91
+ assert_eq!(userdata(&key, None).unwrap(), hex("00 04 00000000"));
92
+ let flow = [
93
+ IIF,
94
+ L4PROTO,
95
+ CT_PROTOCOL,
96
+ IP_SADDR,
97
+ IP_DADDR,
98
+ TH_DPORT,
99
+ CT_ORIGINAL_SADDR,
100
+ CT_ORIGINAL_DADDR,
101
+ CT_ORIGINAL_DPORT,
102
+ CT_ZONE,
103
+ ];
104
+ let truncated = flow.iter().fold(0_u64, |result, field| {
105
+ (result << 6) | u64::from(field.datatype)
106
+ });
107
+ assert_eq!(u64::from(datatype(&flow)), truncated & u64::from(u32::MAX));
108
+ let data = [IP_SADDR, TH_SPORT, TH_SPORT];
109
+ let mut expected = hex("00 04 00000000 01 04 00000000");
110
+ expected.extend(hex("04 4a 00 04 0d000000 01 42
111
+ 00 14 00 04 07000000 01 0c 00 04 0c000000 01 04 0b000000
112
+ 01 14 00 04 07000000 01 0c 00 04 0b000000 01 04 01000000
113
+ 02 14 00 04 07000000 01 0c 00 04 0b000000 01 04 01000000
114
+ 06 04 00000000"));
115
+ #[cfg(target_endian = "little")]
116
+ assert_eq!(userdata(&flow, Some(&data)).unwrap(), expected);
117
+ }
118
+
119
+ /// A lookup loads every field of the set's key, by length and type; the other
120
+ /// direction's field of the same kind fits, anything else refuses.
121
+ #[test]
122
+ fn lookups_must_fit_the_declared_key() {
123
+ let declared = [IIF, IIFNAME, IP_SADDR];
124
+ assert!(Loads::new()
125
+ .link(true)
126
+ .address(true)
127
+ .lookup("s", &declared)
128
+ .is_ok());
129
+ assert!(Loads::new()
130
+ .link(false)
131
+ .address(false)
132
+ .lookup("s", &declared)
133
+ .is_ok());
134
+ assert_eq!(
135
+ Loads::new().link(true).port(true).lookup("s", &declared),
136
+ Err(Error::Invalid)
137
+ );
138
+ assert_eq!(
139
+ Loads::new().link(true).lookup("s", &declared),
140
+ Err(Error::Invalid)
141
+ );
142
+ assert_eq!(
143
+ Loads::new()
144
+ .index(true)
145
+ .address(true)
146
+ .address(true)
147
+ .lookup("s", &declared),
148
+ Err(Error::Invalid)
149
+ );
150
+ }
@@ -507,8 +507,27 @@ fn prepared(value: Value) -> Prepared {
507
507
  normalized(value).unwrap().prepare().unwrap()
508
508
  }
509
509
  fn program_bytes(prepared: &Prepared, table: &str) -> Vec<u8> {
510
+ encoded(prepared.program(table).unwrap())
511
+ }
512
+ /// LEGACY(<=4.4.x): the program as releases before typed sets compiled it,
513
+ /// whose goldens it must still reproduce byte for byte, so their kernel graphs
514
+ /// stay adoptable.
515
+ fn untyped_program_bytes(prepared: &Prepared, table: &str) -> Vec<u8> {
516
+ encoded(
517
+ prepared
518
+ .program(table)
519
+ .unwrap()
520
+ .into_iter()
521
+ .map(|(kind, attributes)| match kind {
522
+ 9 => (kind, crate::owner::untyped_set(&attributes)),
523
+ _ => (kind, attributes),
524
+ })
525
+ .collect(),
526
+ )
527
+ }
528
+ fn encoded(program: Vec<(u16, Vec<crate::wire::Attr>)>) -> Vec<u8> {
510
529
  let mut bytes = Vec::new();
511
- for (kind, attributes) in prepared.program(table).unwrap() {
530
+ for (kind, attributes) in program {
512
531
  let encoded = crate::wire::encode_attrs(&attributes);
513
532
  bytes.extend(kind.to_be_bytes());
514
533
  bytes.extend((encoded.len() as u32).to_be_bytes());
@@ -606,12 +625,20 @@ fn host_v2_published_ports_compile_uplink_dnat_and_exact_forward_admission() {
606
625
  for (_, expressions) in &forward[..4] {
607
626
  assert_eq!(expressions[expressions.len() - 2], "lookup");
608
627
  }
609
- let bytes = program_bytes(&published, "snft_v2_host_golden");
628
+ // The graph of 4.4.x, byte for byte, once its sets are untyped again.
629
+ let bytes = untyped_program_bytes(&published, "snft_v2_host_golden");
610
630
  assert_eq!(bytes.len(), 30988);
611
631
  assert_eq!(
612
632
  format!("{:x}", Sha256::digest(&bytes)),
613
633
  "b36f359c6439e9fcd77c291c7dac7854014225d3de1f8a4e49df6fec6ae570c4"
614
634
  );
635
+ // Re-frozen with typed sets in 4.5.0.
636
+ let bytes = program_bytes(&published, "snft_v2_host_golden");
637
+ assert_eq!(bytes.len(), 31168);
638
+ assert_eq!(
639
+ format!("{:x}", Sha256::digest(&bytes)),
640
+ "609f2388e3c5cf45b921182c17252c072c4db2f07e3cc605e282faddd937d458"
641
+ );
615
642
  }
616
643
 
617
644
  #[test]
@@ -724,12 +751,20 @@ fn router_v2_without_published_ports_keeps_its_exact_digest_and_compiled_bytes()
724
751
  );
725
752
  // Re-frozen with the set-backed router graph of 3.0.0: the digest is the
726
753
  // policy's and did not change, the compiled graph did.
727
- let bytes = program_bytes(&baseline, "snft_v2_router_golden");
754
+ // The graph of 4.4.x, byte for byte, once its sets are untyped again.
755
+ let bytes = untyped_program_bytes(&baseline, "snft_v2_router_golden");
728
756
  assert_eq!(bytes.len(), 47674);
729
757
  assert_eq!(
730
758
  format!("{:x}", Sha256::digest(&bytes)),
731
759
  "ea609865f32031f6835ef4fc7f67f5754a15f466580308bdccc9db87d4ccd2c5"
732
760
  );
761
+ // Re-frozen with typed sets in 4.5.0.
762
+ let bytes = program_bytes(&baseline, "snft_v2_router_golden");
763
+ assert_eq!(bytes.len(), 48362);
764
+ assert_eq!(
765
+ format!("{:x}", Sha256::digest(&bytes)),
766
+ "47280ecb596125d6421b1ab42e2d768e53844f55d7d0f130fe31a8cd5b9a5668"
767
+ );
733
768
  let serialized = serde_json::to_string(&baseline.policy).unwrap();
734
769
  assert!(!serialized.contains("publishedPorts"));
735
770
  // An explicit empty publication is the same canonical policy, digest and graph.
@@ -796,12 +831,20 @@ fn router_v2_published_ports_compile_handoff_dnat_classification_and_admission()
796
831
  assert!(incoming[4..]
797
832
  .iter()
798
833
  .any(|expressions| expressions.contains(&"ct".to_string())));
799
- let bytes = program_bytes(&published, "snft_v2_router_golden");
834
+ // The graph of 4.4.x, byte for byte, once its sets are untyped again.
835
+ let bytes = untyped_program_bytes(&published, "snft_v2_router_golden");
800
836
  assert_eq!(bytes.len(), 55494);
801
837
  assert_eq!(
802
838
  format!("{:x}", Sha256::digest(&bytes)),
803
839
  "60a7ba1c15d49e5ff54cfbf9fbe2d72f03973acd1a897eef2d975547ef4abf5b"
804
840
  );
841
+ // Re-frozen with typed sets in 4.5.0.
842
+ let bytes = program_bytes(&published, "snft_v2_router_golden");
843
+ assert_eq!(bytes.len(), 56454);
844
+ assert_eq!(
845
+ format!("{:x}", Sha256::digest(&bytes)),
846
+ "1d2a72707bc10ed689e3f9b4c1cb3e798e89c01fc97115078dcd32e891b22306"
847
+ );
805
848
  }
806
849
 
807
850
  #[test]
package/rust/src/owner.rs CHANGED
@@ -302,7 +302,13 @@ impl Owner {
302
302
  }
303
303
  Ok(())
304
304
  }
305
+ /// Whether `graph` is `prepared`'s compiled graph. LEGACY(<=4.4.x): this
306
+ /// includes the untyped sets of a release before typed sets; with the next
307
+ /// major it becomes exact agreement alone.
305
308
  fn matches(&self, graph: &Graph, prepared: &Prepared) -> Result<bool> {
309
+ Ok(self.agreement(graph, prepared)? != Agreement::Different)
310
+ }
311
+ fn agreement(&self, graph: &Graph, prepared: &Prepared) -> Result<Agreement> {
306
312
  let program = prepared.program(&self.identity.table_name)?;
307
313
  let expected_chains: Vec<_> = program
308
314
  .iter()
@@ -336,21 +342,8 @@ impl Owner {
336
342
  }
337
343
  result
338
344
  };
339
- // Element order is the set backend's; element keys are the identity.
340
- let mut expected_sets = std::collections::BTreeMap::<String, (Vec<u8>, Vec<Vec<u8>>)>::new();
341
- for (kind, attrs) in &program {
342
- match kind {
343
- 9 => {
344
- expected_sets.insert(wire::text(attrs, 2)?, (set_identity(attrs)?, Vec::new()));
345
- }
346
- 12 => {
347
- let entry = expected_sets
348
- .get_mut(&wire::text(attrs, 2)?)
349
- .ok_or(Error::Invalid)?;
350
- entry.1.extend(element_keys(&wire::attrs(&wire::one(attrs, 3)?.value)?)?);
351
- }
352
- _ => {}
353
- }
345
+ if expected_chains != actual_chains || regroup(expected_rules) != regroup(actual_rules) {
346
+ return Ok(Agreement::Different);
354
347
  }
355
348
  let mut actual_sets = std::collections::BTreeMap::new();
356
349
  for (attrs, keys) in &graph.sets {
@@ -361,12 +354,22 @@ impl Owner {
361
354
  return Err(Error::Conflict);
362
355
  }
363
356
  }
364
- for entry in expected_sets.values_mut().chain(actual_sets.values_mut()) {
357
+ for entry in actual_sets.values_mut() {
365
358
  entry.1.sort();
366
359
  }
367
- Ok(expected_chains == actual_chains
368
- && regroup(expected_rules) == regroup(actual_rules)
369
- && expected_sets == actual_sets)
360
+ if expected_sets(&program, false)? == actual_sets {
361
+ return Ok(Agreement::Exact);
362
+ }
363
+ // LEGACY(<=4.4.x): kernel state applied before typed sets carries every
364
+ // set with key and data type 0 and no userdata, which Linux stores and
365
+ // dumps but never interprets, so it enforces exactly the same graph.
366
+ // Such a graph is adopted, inspected and released as this graph, and
367
+ // reconciliation always replaces it. Remove with the next major.
368
+ Ok(if expected_sets(&program, true)? == actual_sets {
369
+ Agreement::Untyped
370
+ } else {
371
+ Agreement::Different
372
+ })
370
373
  }
371
374
  fn adopt(&mut self, graph: Graph, transition: &Transition) -> Result<Graph> {
372
375
  self.binding(&graph, transition.previous.as_ref())?;
@@ -464,13 +467,18 @@ impl Owner {
464
467
  let mut operations = Vec::new();
465
468
  let generation = if let Some(graph) = graph {
466
469
  let graph = self.adopt(graph, transition)?;
467
- if self.matches(&graph, &transition.target)? {
470
+ let target = self.agreement(&graph, &transition.target)?;
471
+ if target == Agreement::Exact {
468
472
  transition.target.validate_interfaces()?;
469
473
  return self.applied(&graph, transition.target.clone());
470
474
  }
471
- let previous = transition.previous.as_ref().ok_or(Error::Conflict)?;
472
- if !self.matches(&graph, &previous.prepared)? {
473
- return Err(Error::Conflict);
475
+ // LEGACY(<=4.4.x): the target's own untyped graph is never already
476
+ // applied; it is replaced like a previous graph, in the same batch.
477
+ if target == Agreement::Different {
478
+ let previous = transition.previous.as_ref().ok_or(Error::Conflict)?;
479
+ if !self.matches(&graph, &previous.prepared)? {
480
+ return Err(Error::Conflict);
481
+ }
474
482
  }
475
483
  for rule in &graph.rules {
476
484
  operations.push((
@@ -533,7 +541,7 @@ impl Owner {
533
541
  self.binding(&graph, transition.previous.as_ref())?;
534
542
  if wire::number(&graph.table, 2)? != 6
535
543
  || wire::number(&graph.table, 7)? != self.socket()?.port
536
- || !self.matches(&graph, &transition.target)?
544
+ || self.agreement(&graph, &transition.target)? != Agreement::Exact
537
545
  {
538
546
  return Err(Error::Conflict);
539
547
  }
@@ -668,6 +676,61 @@ impl Owner {
668
676
  }
669
677
  }
670
678
 
679
+ /// How a kernel graph agrees with a compiled one.
680
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
681
+ enum Agreement {
682
+ /// Every chain, rule, set and element, with every set's types.
683
+ Exact,
684
+ /// LEGACY(<=4.4.x): the same graph with untyped sets. Remove with the next
685
+ /// major.
686
+ Untyped,
687
+ Different,
688
+ }
689
+
690
+ /// Sets by name, each with its identity and sorted element keys.
691
+ type Sets = std::collections::BTreeMap<String, (Vec<u8>, Vec<Vec<u8>>)>;
692
+ /// The compiled graph's sets. LEGACY(<=4.4.x): `untyped` declares them as a
693
+ /// release before typed sets did; remove the parameter with the next major.
694
+ fn expected_sets(program: &[(u16, Vec<Attr>)], untyped: bool) -> Result<Sets> {
695
+ // Element order is the set backend's; element keys are the identity.
696
+ let mut sets = Sets::new();
697
+ for (kind, attrs) in program {
698
+ match kind {
699
+ 9 => {
700
+ let identity = if untyped {
701
+ set_identity(&untyped_set(attrs))?
702
+ } else {
703
+ set_identity(attrs)?
704
+ };
705
+ sets.insert(wire::text(attrs, 2)?, (identity, Vec::new()));
706
+ }
707
+ 12 => {
708
+ let entry = sets
709
+ .get_mut(&wire::text(attrs, 2)?)
710
+ .ok_or(Error::Invalid)?;
711
+ entry.1.extend(element_keys(&wire::attrs(&wire::one(attrs, 3)?.value)?)?);
712
+ }
713
+ _ => {}
714
+ }
715
+ }
716
+ for entry in sets.values_mut() {
717
+ entry.1.sort();
718
+ }
719
+ Ok(sets)
720
+ }
721
+ /// LEGACY(<=4.4.x): a set declaration as releases before typed sets wrote it:
722
+ /// key and data type 0 and no userdata. Remove with the next major.
723
+ pub(super) fn untyped_set(attributes: &[Attr]) -> Vec<Attr> {
724
+ attributes
725
+ .iter()
726
+ .filter(|attribute| attribute.id() != 13)
727
+ .map(|attribute| match attribute.id() {
728
+ 4 | 6 => Attr::u32(attribute.id(), 0),
729
+ _ => attribute.clone(),
730
+ })
731
+ .collect()
732
+ }
733
+
671
734
  fn chain_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
672
735
  let mut identity = Vec::new();
673
736
  for attribute in attributes {
@@ -681,9 +744,10 @@ fn chain_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
681
744
  serde_json::to_vec(&wire::canonical(&identity, false)?).map_err(|_| Error::Protocol)
682
745
  }
683
746
  /// 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
685
- /// informational backend type and count, the transaction ID and an empty
686
- /// description are not identity; anything else rejects.
747
+ /// and length, the field lengths of a concatenation and its userdata (byte
748
+ /// orders and `typeof` descriptions). The handle, the informational backend
749
+ /// type and count, the transaction ID and an empty description are not
750
+ /// identity; anything else rejects.
687
751
  fn set_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
688
752
  let mut identity = Vec::new();
689
753
  for attribute in attributes {
@@ -694,13 +758,17 @@ fn set_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
694
758
  identity.push(Attr::bytes(9, fields));
695
759
  }
696
760
  }
697
- 1..=7 => identity.push(attribute.clone()),
761
+ 1..=7 | 13 => identity.push(attribute.clone()),
698
762
  _ => return Err(Error::Conflict),
699
763
  }
700
764
  }
701
765
  for id in [1, 2, 3, 4, 5] {
702
766
  wire::one(&identity, id)?;
703
767
  }
768
+ // At most one userdata: a typed set carries it, a legacy untyped one not.
769
+ if identity.iter().filter(|attribute| attribute.id() == 13).count() > 1 {
770
+ return Err(Error::Conflict);
771
+ }
704
772
  // A map carries its data type and length, and nothing else does.
705
773
  let map = wire::number(&identity, 3)? & 8 != 0;
706
774
  for id in [6, 7] {