@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.
package/changelog.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-10-06 - 4.5.0
4
+
5
+ ### Features
6
+
7
+ - Typed sets: `nft` decodes every managed set and lists the rules that look them up, up to nftables' own limit below (Pallet lab RUN 9, O9-1). Every set was declared with key type 0, data type 0 and no userdata, so nftables 1.0.9 aborted listing any rule that looked one up (`netlink_delinearize.c`: `dtype != &invalid_type`) and segfaulted listing the sets themselves. Each key field now has one descriptor (length, nft type, byte order, `typeof` expression) from which both the rule's loads and the set declaration derive, and a lookup whose loads do not fit its set's key refuses to compile (`INVALID`). Sets carry their key and data types and nft's userdata (byte orders, `typeof` key and data, data interval) byte for byte as nftables 1.0.9 writes them for the same `typeof` declaration; keys beyond four fields carry their type and byte order alone, as nft cannot describe them by `typeof`. Policy digests are unchanged; compiled bytes change for every graph with sets, and stripping the types again reproduces the 4.4.0 goldens byte for byte. nftables itself still crashes listing a rule that tests membership of seven or more concatenated fields (host grants, workload grants, published flows, ported private rules), also on nftables master; readme "Listing the managed tables with `nft`" states what lists today. Tests: nft-captured userdata bytes (`egress_keys_tests`), and in the isolated guest, now carrying the host's nftables 1.0.9, every declared set against the set nft creates from the same `typeof`, every set, chain and ruleset listed, the nft crash pinned, and the untyped control aborting.
8
+ - Upgrading over a table applied by 4.4.x or earlier: its graph with untyped sets is adopted, inspected, verified (`verify_external`) and released as the applied graph, and `reconcile()` never treats it as already applied but replaces the whole graph with typed sets in its single batch, including a retry of the transition that applied it. This acceptance is legacy for kernel state left by 4.4.x and goes with the next major. The step is one-way per boot: 4.4.x refuses typed sets (`CONFLICT`). Tests: graph agreement (exact, untyped, mixed, retyped, changed element) and, in the isolated guest, adoption and replacement of a 4.4.x graph by a new and by the same transition (claim plus one batch), external verification and release.
9
+
3
10
  ## 2026-10-06 - 4.4.0
4
11
 
5
12
  ### Features
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "92ddb3d386ae7d83ce27c0df8b54bb08ce3bb54cba71e80df8e0c3e808e4da7b",
3
+ "binarySha256": "f30fb315a0059adfc4afe0728a8304fe14a5a45e2437cb4e76d5c79c7dbf8917",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "4.4.0",
7
- "gitCommit": "77c4a57ed74dce10ee91ef180972ebd9cc821211",
6
+ "projectVersion": "4.5.0",
7
+ "gitCommit": "f648edff675c7693b2ec8bca6138bd7d1e2da65b",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-10-06T03:48:00.001Z",
9
+ "builtAt": "2026-10-06T05:09:47.306Z",
10
10
  "tsrustVersion": "4.0.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_amd64_musl"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "889d263bb28613ed54717f261f3fb75f5e394e88d9cf9632d611a7fbe621c582",
3
+ "binarySha256": "3347480d9c39a2ee4891c13f7c6a12d50d9175e15776f1c6999e1574a491dfd8",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "4.4.0",
7
- "gitCommit": "77c4a57ed74dce10ee91ef180972ebd9cc821211",
6
+ "projectVersion": "4.5.0",
7
+ "gitCommit": "f648edff675c7693b2ec8bca6138bd7d1e2da65b",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-10-06T03:48:09.553Z",
9
+ "builtAt": "2026-10-06T05:09:57.015Z",
10
10
  "tsrustVersion": "4.0.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_arm64_musl"
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '4.4.0',
6
+ version: '4.5.0',
7
7
  description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwyQkFBMkI7SUFDakMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLG1IQUFtSDtDQUNqSSxDQUFBIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartnftables",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "private": false,
5
5
  "description": "A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.",
6
6
  "main": "dist_ts/index.js",
package/readme.md CHANGED
@@ -1041,6 +1041,56 @@ and netdev hooks do not see routed packets and are not reported. Guard
1041
1041
  either Docker chain or any legacy table is present. The read is a point in time;
1042
1042
  the caller still owns exclusive authority over who may add a forward owner later.
1043
1043
 
1044
+ ### Listing the managed tables with `nft`
1045
+
1046
+ Every set the compiler declares carries the types `nft` needs to decode it: the
1047
+ key and data types, their byte orders and, for keys of up to four fields, the
1048
+ `typeof` expressions, byte for byte as nftables 1.0.9 writes them for the same
1049
+ `typeof` declaration (`nft add set ... '{ typeof meta iif . meta iifname; ... }'`).
1050
+ A lookup's loads must fit its set's declared key field by field, or compiling
1051
+ refuses with `INVALID`. Before 4.5.0 every set was declared with key type 0 and
1052
+ no userdata, so `nft list` aborted (`dtype != &invalid_type`) or crashed on any
1053
+ table that held one.
1054
+
1055
+ What an operator can list today with nftables 1.0.9:
1056
+
1057
+ - `nft list set|map inet <table> <set>` for every set. Keys of up to four fields
1058
+ list as `typeof ...`; longer keys as `type ...`. nftables 1.0.9 cannot
1059
+ describe a longer key by `typeof` (it stops at four fields, `NFT_REG32_SIZE`),
1060
+ and its 32-bit type holds at most five fields, so the elements of a key of six
1061
+ or more fields, or of five with a `ct zone`, print wrongly. The generation map
1062
+ `leased_generation` lists as `type 0 : ct_label`: its key is the zone padded to
1063
+ a register, as every release before typed sets declared it.
1064
+ - `nft list chain` for every chain whose rules test membership of keys of at most
1065
+ six fields. A map lookup of a key of more than four fields lists, but nft may
1066
+ render its data wrongly or drop the statement (seen for the leased flow maps
1067
+ and `published_symmetric`).
1068
+ - `nft list ruleset`, `nft list table` and `nft list chain` crash (SIGSEGV) on any
1069
+ rule that tests membership of a concatenation of seven or more fields. This is
1070
+ an nftables defect, also on nftables master: `expr_postprocess_concat` walks
1071
+ past the five fields the 32-bit type holds, for sets nft created itself just
1072
+ as for these. Host grants (8 or 10 fields), workload grants (8), published
1073
+ flows (8) and ported private rules (7) use such lookups, so on nodes that use
1074
+ them `nft list ruleset` crashes until a fixed nftables is installed. List the
1075
+ sets and the other chains individually meanwhile.
1076
+
1077
+ The isolated qualification below proves each of these against the host's
1078
+ nftables 1.0.9 and pins the crash, so it fails once a fixed `nft` lands.
1079
+
1080
+ #### Tables applied before 4.5.0
1081
+
1082
+ A table applied by 4.4.x or earlier holds the same graph with untyped sets, and
1083
+ Linux enforces it identically: the kernel stores and dumps the key type and
1084
+ userdata but never interprets them. This release adopts such a graph, inspects,
1085
+ verifies and releases it as the applied graph, and never treats it as already
1086
+ applied: the next `reconcile()`, including a retry of the transition that
1087
+ applied it, replaces the whole graph with typed sets in the same single batch
1088
+ as any replacement, so enforcement never lapses. No migration step is needed.
1089
+ The transition is one-way per boot: 4.4.x refuses a table holding typed sets
1090
+ (`CONFLICT`), so a downgrade on the same boot cannot adopt it, while a reboot
1091
+ drops the table for either version. The acceptance of untyped sets is legacy
1092
+ support for kernel state left by 4.4.x and goes with the next major release.
1093
+
1044
1094
  ### Native qualification
1045
1095
 
1046
1096
  The separate [Docker fixture](test/native/readme.docker.md) exercises the native
@@ -1052,7 +1102,8 @@ Its checked-in input manifest and per-run receipts identify the tested artifacts
1052
1102
  snapshot and bounded-subprocess tests. The ignored native cases require the explicitly marked disposable guest
1053
1103
  from `test/native/qualify.py`; requesting them on an ordinary host fails its scope
1054
1104
  check. Qualification uses an offline Linux 6.18.35 x86_64 guest with no host disks,
1055
- mounts or external network backend. One VirtIO device connects only to a singleton
1105
+ mounts or external network backend; it carries the host's nftables 1.0.9 binary
1106
+ and libraries for the listing proof. One VirtIO device connects only to a singleton
1056
1107
  QEMU-internal hub; packet-path tests use guest-owned namespaces, veth pairs and TUN.
1057
1108
  It covers
1058
1109
  UDP/TCP grants, direct-IP denial, source spoofing, IPv6 denial, renamed interfaces,
@@ -21,6 +21,8 @@ mod router;
21
21
  #[path = "egress.workloadgrant.rs"]
22
22
  mod workloadgrant;
23
23
 
24
+ use keys::Field;
25
+
24
26
  /// The atomic replacement budget. One replacement is one nfnetlink batch in one
25
27
  /// `sendmsg`, and Linux refuses a netlink message above the socket send buffer:
26
28
  /// `SO_SNDBUF` clamped to `net.core.wmem_max` and doubled, 425,984 bytes with the
@@ -54,6 +56,22 @@ pub(super) struct Program<'a> {
54
56
  bytes: usize,
55
57
  element_bytes: usize,
56
58
  operations: Vec<(u16, Vec<Attr>)>,
59
+ /// Each declared set's key and a map's data.
60
+ declared: std::collections::BTreeMap<String, Declared>,
61
+ }
62
+ /// One declared set: its key fields and a map's data fields.
63
+ struct Declared {
64
+ key: Vec<Field>,
65
+ data: Option<Vec<Field>>,
66
+ }
67
+ /// A declared set as nft names it: its key's and a map's data's `typeof`
68
+ /// expressions, and whether it is an interval set.
69
+ #[cfg(test)]
70
+ #[derive(Clone, Debug)]
71
+ pub(crate) struct SetDeclaration {
72
+ pub(crate) key: Vec<&'static str>,
73
+ pub(crate) data: Option<Vec<&'static str>>,
74
+ pub(crate) interval: bool,
57
75
  }
58
76
  impl<'a> Program<'a> {
59
77
  fn push(&mut self, operation: u16, attributes: Vec<Attr>) -> Result<()> {
@@ -76,26 +94,59 @@ impl<'a> Program<'a> {
76
94
  self.element_bytes += wire::encode_attrs(&attributes).len() + 20;
77
95
  self.admit(12, attributes)
78
96
  }
79
- /// One named exact-key hash set, or a map to `data_length` bytes. CONSTANT
80
- /// like every set here.
97
+ /// The key's type and length and a map's data type and length, as nft
98
+ /// declares them, after which the userdata carries their byte orders and
99
+ /// `typeof` descriptions. Remembers the lengths every element must have.
100
+ fn typed(&mut self, name: &str, key: &[Field], data: Option<&[Field]>) -> Vec<Attr> {
101
+ let mut attributes = vec![
102
+ Attr::u32(4, keys::datatype(key)),
103
+ Attr::u32(5, keys::key_length(key)),
104
+ ];
105
+ if let Some(data) = data {
106
+ attributes.extend([
107
+ Attr::u32(6, keys::datatype(data)),
108
+ Attr::u32(7, keys::data_length(data)),
109
+ ]);
110
+ }
111
+ self.declared.insert(
112
+ name.to_string(),
113
+ Declared {
114
+ key: key.to_vec(),
115
+ data: data.map(<[Field]>::to_vec),
116
+ },
117
+ );
118
+ attributes
119
+ }
120
+ /// Whether an element's key and data have the lengths its set declares.
121
+ fn fits(&self, name: &str, key: &[u8], data: Option<&Vec<u8>>) -> Result<()> {
122
+ let declared = self.declared.get(name).ok_or(crate::Error::Invalid)?;
123
+ let length = |bytes: &[u8]| u32::try_from(bytes.len()).ok();
124
+ require(
125
+ length(key) == Some(keys::key_length(&declared.key))
126
+ && data.map(|data| length(data))
127
+ == declared
128
+ .data
129
+ .as_deref()
130
+ .map(|data| Some(keys::data_length(data))),
131
+ )
132
+ }
133
+ /// One named exact-key hash set, or a map to `data`. CONSTANT like every
134
+ /// set here.
81
135
  fn exact_set(
82
136
  &mut self,
83
137
  name: &str,
84
138
  id: u32,
85
- key_length: u32,
86
- data_length: Option<u32>,
139
+ key: &[Field],
140
+ data: Option<&[Field]>,
87
141
  ) -> Result<()> {
88
142
  let mut attributes = vec![
89
143
  Attr::string(1, self.table),
90
144
  Attr::string(2, name),
91
- Attr::u32(3, 2 | if data_length.is_some() { 8 } else { 0 }),
92
- Attr::u32(4, 0),
93
- Attr::u32(5, key_length),
145
+ Attr::u32(3, 2 | if data.is_some() { 8 } else { 0 }),
94
146
  ];
95
- if let Some(length) = data_length {
96
- attributes.extend([Attr::u32(6, 0), Attr::u32(7, length)]);
97
- }
147
+ attributes.extend(self.typed(name, key, data));
98
148
  attributes.push(Attr::u32(10, id));
149
+ attributes.push(Attr::bytes(13, keys::userdata(key, data)?));
99
150
  self.push(9, attributes)
100
151
  }
101
152
  fn exact_elements(
@@ -103,6 +154,9 @@ impl<'a> Program<'a> {
103
154
  name: &str,
104
155
  elements: Vec<(Vec<u8>, Option<Vec<u8>>)>,
105
156
  ) -> Result<()> {
157
+ for (key, data) in &elements {
158
+ self.fits(name, key, data.as_ref())?;
159
+ }
106
160
  for chunk in elements.chunks(256) {
107
161
  let list = chunk
108
162
  .iter()
@@ -122,21 +176,6 @@ impl<'a> Program<'a> {
122
176
  }
123
177
  Ok(())
124
178
  }
125
- /// One named exact-match set. CONSTANT: its elements cannot change once a
126
- /// rule binds it, so replacement always creates a complete new set.
127
- fn set(&mut self, name: &str, id: u32, key_length: usize) -> Result<()> {
128
- self.push(
129
- 9,
130
- vec![
131
- Attr::string(1, self.table),
132
- Attr::string(2, name),
133
- Attr::u32(3, 2),
134
- Attr::u32(4, 0),
135
- Attr::u32(5, key_length as u32),
136
- Attr::u32(10, id),
137
- ],
138
- )
139
- }
140
179
  /// The elements of one set, 256 to a message so every nested attribute
141
180
  /// stays far below netlink's 16-bit attribute length. Elements have their
142
181
  /// own budget: a replacement deletes the previous set, and so its elements,
@@ -146,27 +185,21 @@ impl<'a> Program<'a> {
146
185
  }
147
186
  /// One named CONSTANT set of concatenated fields in which every field is an
148
187
  /// inclusive interval (the kernel's pipapo backend), optionally a map to
149
- /// `data_length` bytes. `fields` are byte lengths; each occupies whole
150
- /// 32-bit registers of the key.
188
+ /// `data`. Each field occupies whole 32-bit registers of the key.
151
189
  fn interval_set(
152
190
  &mut self,
153
191
  name: &str,
154
192
  id: u32,
155
- fields: &[u32],
156
- data_length: Option<u32>,
193
+ fields: &[Field],
194
+ data: Option<&[Field]>,
157
195
  ) -> Result<()> {
158
- let key_length: u32 = fields.iter().map(|length| length.div_ceil(4) * 4).sum();
159
196
  let mut attributes = vec![
160
197
  Attr::string(1, self.table),
161
198
  Attr::string(2, name),
162
199
  // CONSTANT | INTERVAL | CONCAT, and MAP for a map.
163
- Attr::u32(3, 2 | 4 | 0x80 | if data_length.is_some() { 8 } else { 0 }),
164
- Attr::u32(4, 0),
165
- Attr::u32(5, key_length),
200
+ Attr::u32(3, 2 | 4 | 0x80 | if data.is_some() { 8 } else { 0 }),
166
201
  ];
167
- if let Some(length) = data_length {
168
- attributes.extend([Attr::u32(6, 0), Attr::u32(7, length)]);
169
- }
202
+ attributes.extend(self.typed(name, fields, data));
170
203
  attributes.extend([
171
204
  Attr::nested(
172
205
  9,
@@ -174,11 +207,12 @@ impl<'a> Program<'a> {
174
207
  2,
175
208
  fields
176
209
  .iter()
177
- .map(|length| Attr::nested(1, vec![Attr::u32(1, *length)]))
210
+ .map(|field| Attr::nested(1, vec![Attr::u32(1, field.length)]))
178
211
  .collect(),
179
212
  )],
180
213
  ),
181
214
  Attr::u32(10, id),
215
+ Attr::bytes(13, keys::userdata(fields, data)?),
182
216
  ]);
183
217
  self.push(9, attributes)
184
218
  }
@@ -186,6 +220,10 @@ impl<'a> Program<'a> {
186
220
  /// An element whose bounds are equal carries no last key; Linux takes the
187
221
  /// first as the last and dumps it the same way.
188
222
  fn interval_elements(&mut self, name: &str, elements: Vec<IntervalElement>) -> Result<()> {
223
+ for element in &elements {
224
+ self.fits(name, &element.first, element.data.as_ref())?;
225
+ self.fits(name, &element.last, element.data.as_ref())?;
226
+ }
189
227
  for chunk in elements.chunks(256) {
190
228
  let list = chunk
191
229
  .iter()
@@ -421,6 +459,34 @@ fn snat(address: &str, range: Option<(u16, u16)>) -> Result<Vec<Attr>> {
421
459
  Ok(result)
422
460
  }
423
461
  pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<Attr>)>> {
462
+ Ok(compiled(prepared, table)?.operations)
463
+ }
464
+ /// Every set the compiled graph declares, by name.
465
+ #[cfg(test)]
466
+ pub(super) fn declared(
467
+ prepared: &Prepared,
468
+ table: &str,
469
+ ) -> Result<std::collections::BTreeMap<String, SetDeclaration>> {
470
+ let program = compiled(prepared, table)?;
471
+ let names = |fields: &[Field]| fields.iter().map(|field| field.name).collect();
472
+ let mut result = std::collections::BTreeMap::new();
473
+ for (kind, attributes) in &program.operations {
474
+ if *kind == 9 {
475
+ let name = wire::text(attributes, 2)?;
476
+ let declared = program.declared.get(&name).ok_or(crate::Error::Invalid)?;
477
+ result.insert(
478
+ name,
479
+ SetDeclaration {
480
+ key: names(&declared.key),
481
+ data: declared.data.as_deref().map(names),
482
+ interval: wire::number(attributes, 3)? & 4 != 0,
483
+ },
484
+ );
485
+ }
486
+ }
487
+ Ok(result)
488
+ }
489
+ fn compiled<'a>(prepared: &Prepared, table: &'a str) -> Result<Program<'a>> {
424
490
  let mut program = Program {
425
491
  table,
426
492
  stamp: format!("snft2:{}:{}", prepared.policy.revision, prepared.digest),
@@ -428,11 +494,12 @@ pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<
428
494
  bytes: 0,
429
495
  element_bytes: 0,
430
496
  operations: Vec::new(),
497
+ declared: std::collections::BTreeMap::new(),
431
498
  };
432
499
  match &prepared.policy.scope {
433
500
  Scope::RouterEgress(scope) => router::compile(&mut program, scope)?,
434
501
  Scope::HostTransit(scope) => host::compile(&mut program, scope)?,
435
502
  Scope::AllocationPoolGuard(scope) => poolguard::compile(&mut program, scope)?,
436
503
  }
437
- Ok(program.operations)
504
+ Ok(program)
438
505
  }
@@ -4,6 +4,10 @@
4
4
  //! variable interface (index and name), the packet and conntrack protocols, the
5
5
  //! live tuple and the originally tracked tuple. An element is one grant with its
6
6
  //! live tuple equal to its original tuple, so translated flows never match.
7
+ use super::keys::{
8
+ Field, Loads, CT_ORIGINAL_DADDR, CT_ORIGINAL_DPORT, CT_ORIGINAL_SADDR, CT_PROTOCOL, IP_DADDR,
9
+ IP_SADDR, L4PROTO, OIF, OIFNAME, TH_DPORT,
10
+ };
7
11
  use super::*;
8
12
 
9
13
  /// Every exact host-origin flow of the scope, keyed on its full tuple.
@@ -11,6 +15,42 @@ pub(super) const TUPLE_SET: &str = "host_grant";
11
15
  /// Router raw ingress runs before conntrack: exact arriving tuples only.
12
16
  pub(super) const ARRIVAL_SET: &str = "host_grant_arrival";
13
17
 
18
+ /// The tuple key of a link-independent scope: both protocols, the live tuple
19
+ /// and the originally tracked tuple, as the original direction loads them.
20
+ pub(super) const TUPLE: [Field; 8] = [
21
+ L4PROTO,
22
+ CT_PROTOCOL,
23
+ IP_SADDR,
24
+ IP_DADDR,
25
+ TH_DPORT,
26
+ CT_ORIGINAL_SADDR,
27
+ CT_ORIGINAL_DADDR,
28
+ CT_ORIGINAL_DPORT,
29
+ ];
30
+ /// The tuple key behind the variable interface: its index and name first.
31
+ const LINKED_TUPLE: [Field; 10] = [
32
+ OIF,
33
+ OIFNAME,
34
+ L4PROTO,
35
+ CT_PROTOCOL,
36
+ IP_SADDR,
37
+ IP_DADDR,
38
+ TH_DPORT,
39
+ CT_ORIGINAL_SADDR,
40
+ CT_ORIGINAL_DADDR,
41
+ CT_ORIGINAL_DPORT,
42
+ ];
43
+ /// The exact arriving tuple: protocol, addresses and port.
44
+ const ARRIVAL: [Field; 4] = [L4PROTO, IP_SADDR, IP_DADDR, TH_DPORT];
45
+
46
+ fn tuple(linked: bool) -> &'static [Field] {
47
+ if linked {
48
+ &LINKED_TUPLE
49
+ } else {
50
+ &TUPLE
51
+ }
52
+ }
53
+
14
54
  /// Register numbers as Linux dumps them: 16-byte aligned registers use the
15
55
  /// legacy NFT_REG_1..4 names, every other 4-byte register NFT_REG32_xx.
16
56
  pub(super) fn register(index: u32) -> u32 {
@@ -38,21 +78,14 @@ pub(super) fn payload_to(base: u32, offset: u32, length: u32, index: u32) -> Att
38
78
  index,
39
79
  )
40
80
  }
41
- pub(super) fn ct_to(key: u32, original: bool, index: u32) -> Attr {
81
+ /// A conntrack key of the original (0) or reply (1) tuple, or of neither.
82
+ pub(super) fn ct_to(key: u32, direction: Option<u8>, index: u32) -> Attr {
42
83
  let mut data = vec![Attr::u32(2, key)];
43
- if original {
44
- data.push(Attr::bytes(3, vec![0]));
84
+ if let Some(direction) = direction {
85
+ data.push(Attr::bytes(3, vec![direction]));
45
86
  }
46
87
  store("ct", data, index)
47
88
  }
48
- /// A conntrack key of the reply direction's tuple.
49
- pub(super) fn ct_reply_to(key: u32, index: u32) -> Attr {
50
- store(
51
- "ct",
52
- vec![Attr::u32(2, key), Attr::bytes(3, vec![1])],
53
- index,
54
- )
55
- }
56
89
  pub(super) fn lookup(set: &str) -> Attr {
57
90
  expr(
58
91
  "lookup",
@@ -72,26 +105,22 @@ fn word(bytes: &[u8]) -> Vec<u8> {
72
105
  /// Key loads for one direction, then the lookup in `set`. `linked` adds the
73
106
  /// variable interface: the outgoing interface of the original direction, the
74
107
  /// incoming interface of the reply.
75
- pub(super) fn tuple_lookup(set: &str, linked: bool, reply: bool) -> Vec<Attr> {
76
- let mut result = Vec::new();
77
- let mut index = 4;
108
+ pub(super) fn tuple_lookup(set: &str, linked: bool, reply: bool) -> Result<Vec<Attr>> {
109
+ let mut loads = Loads::new();
78
110
  if linked {
79
- result.push(meta_to(if reply { 4 } else { 5 }, index));
80
- result.push(meta_to(if reply { 6 } else { 7 }, index + 1));
81
- index += 5;
111
+ loads.link(reply);
82
112
  }
83
- result.push(meta_to(16, index));
84
- result.push(ct_to(10, false, index + 1));
85
113
  // The grant's source and destination are the packet's own addresses in the
86
114
  // original direction and swapped on the reply; the port is the workload's.
87
- result.push(payload_to(1, if reply { 16 } else { 12 }, 4, index + 2));
88
- result.push(payload_to(1, if reply { 12 } else { 16 }, 4, index + 3));
89
- result.push(payload_to(2, if reply { 0 } else { 2 }, 2, index + 4));
90
- result.push(ct_to(19, true, index + 5));
91
- result.push(ct_to(20, true, index + 6));
92
- result.push(ct_to(12, true, index + 7));
93
- result.push(lookup(set));
94
- result
115
+ loads
116
+ .protocols()
117
+ .address(!reply)
118
+ .address(reply)
119
+ .port(reply)
120
+ .original_address(true)
121
+ .original_address(false)
122
+ .original_port(false);
123
+ loads.lookup(set, tuple(linked))
95
124
  }
96
125
  pub(super) fn tuple_key(grant: &HostGrant, link: Option<&LocalLink>) -> Result<Vec<u8>> {
97
126
  let protocol = word(&[protocol(&grant.protocol)]);
@@ -128,11 +157,15 @@ pub(super) fn tuple_set<'s>(
128
157
  link: impl Fn(&HostGrant) -> Result<Option<&'s LocalLink>>,
129
158
  ) -> Result<()> {
130
159
  let mut keys = Vec::new();
160
+ let mut linked = false;
131
161
  for grant in grants {
132
- keys.push(tuple_key(grant, link(grant)?)?);
162
+ let link = link(grant)?;
163
+ linked = link.is_some();
164
+ keys.push(tuple_key(grant, link)?);
133
165
  }
134
- let length = keys.first().map(Vec::len).ok_or(crate::Error::Invalid)?;
135
- program.set(TUPLE_SET, 1, length)?;
166
+ // Every element has the declared key's length, so a scope never mixes them.
167
+ require(!keys.is_empty())?;
168
+ program.exact_set(TUPLE_SET, 1, tuple(linked), None)?;
136
169
  program.elements(TUPLE_SET, keys)
137
170
  }
138
171
  pub(super) fn arrival_set(program: &mut Program<'_>, grants: &[HostGrant]) -> Result<()> {
@@ -144,18 +177,17 @@ pub(super) fn arrival_set(program: &mut Program<'_>, grants: &[HostGrant]) -> Re
144
177
  key.extend(word(&grant.destination_port.to_be_bytes()));
145
178
  keys.push(key);
146
179
  }
147
- program.set(ARRIVAL_SET, 2, 16)?;
180
+ program.exact_set(ARRIVAL_SET, 2, &ARRIVAL, None)?;
148
181
  program.elements(ARRIVAL_SET, keys)
149
182
  }
150
183
  /// Exact arriving tuple, before conntrack: protocol, addresses and port.
151
- pub(super) fn arrival_lookup() -> Vec<Attr> {
152
- vec![
153
- meta_to(16, 4),
154
- payload_to(1, 12, 4, 5),
155
- payload_to(1, 16, 4, 6),
156
- payload_to(2, 2, 2, 7),
157
- lookup(ARRIVAL_SET),
158
- ]
184
+ pub(super) fn arrival_lookup() -> Result<Vec<Attr>> {
185
+ Loads::new()
186
+ .protocol()
187
+ .address(true)
188
+ .address(false)
189
+ .port(false)
190
+ .lookup(ARRIVAL_SET, &ARRIVAL)
159
191
  }
160
192
 
161
193
  /// Four admissions whatever the number of grants: the ESTABLISHED original
@@ -178,7 +210,7 @@ pub(super) fn admissions(
178
210
  result
179
211
  };
180
212
  let mut established = envelope(false, 2);
181
- established.extend(tuple_lookup(TUPLE_SET, linked, false));
213
+ established.extend(tuple_lookup(TUPLE_SET, linked, false)?);
182
214
  program.end(chains[0], established, 1)?;
183
215
  for transport in ["udp", "tcp"] {
184
216
  let mut opening = envelope(false, 8);
@@ -186,10 +218,10 @@ pub(super) fn admissions(
186
218
  if transport == "tcp" {
187
219
  opening.extend(opening_tcp());
188
220
  }
189
- opening.extend(tuple_lookup(TUPLE_SET, linked, false));
221
+ opening.extend(tuple_lookup(TUPLE_SET, linked, false)?);
190
222
  program.end(chains[0], opening, 1)?;
191
223
  }
192
224
  let mut reply = envelope(true, 2);
193
- reply.extend(tuple_lookup(TUPLE_SET, linked, true));
225
+ reply.extend(tuple_lookup(TUPLE_SET, linked, true)?);
194
226
  program.end(chains[1], reply, 1)
195
227
  }