@push.rocks/smartnftables 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/changelog.md +19 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/managed.egress.types.d.ts +25 -3
  8. package/package.json +6 -6
  9. package/readme.md +205 -63
  10. package/rust/src/egress.compile.rs +155 -76
  11. package/rust/src/egress.host.rs +24 -51
  12. package/rust/src/egress.hostgrant.rs +1 -1
  13. package/rust/src/egress.keys.rs +305 -0
  14. package/rust/src/egress.leased.rs +399 -0
  15. package/rust/src/egress.private.rs +197 -0
  16. package/rust/src/egress.published.rs +370 -0
  17. package/rust/src/egress.router.rs +61 -294
  18. package/rust/src/egress.rs +126 -11
  19. package/rust/src/egress_hostgrant_tests.rs +149 -18
  20. package/rust/src/egress_localport_tests.rs +3 -1
  21. package/rust/src/egress_publishedrange_tests.rs +812 -0
  22. package/rust/src/egress_routerequivalence_tests.rs +448 -0
  23. package/rust/src/egress_tests.rs +132 -230
  24. package/rust/src/egress_workloadgrant_tests.rs +6 -7
  25. package/rust/src/owner.rs +69 -14
  26. package/rust/src/owner_host_traffic_tests.rs +4 -0
  27. package/rust/src/owner_identity_tests.rs +166 -80
  28. package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
  29. package/rust/src/owner_router_traffic_tests.rs +2 -0
  30. package/rust/src/owner_scale_traffic_tests.rs +383 -0
  31. package/rust/src/owner_tests.rs +6 -0
  32. package/rust/src/policy.rs +29 -25
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/managed.egress.types.ts +25 -3
@@ -6,17 +6,46 @@ use crate::wire::{self, Attr};
6
6
  mod host;
7
7
  #[path = "egress.hostgrant.rs"]
8
8
  mod hostgrant;
9
+ #[path = "egress.keys.rs"]
10
+ mod keys;
11
+ #[path = "egress.leased.rs"]
12
+ mod leased;
9
13
  #[path = "egress.poolguard.rs"]
10
14
  mod poolguard;
15
+ #[path = "egress.private.rs"]
16
+ mod private;
17
+ #[path = "egress.published.rs"]
18
+ mod published;
11
19
  #[path = "egress.router.rs"]
12
20
  mod router;
13
21
  #[path = "egress.workloadgrant.rs"]
14
22
  mod workloadgrant;
15
23
 
16
- /// Set elements of one target. Together with the 100,000-byte rule graph and the
17
- /// handle-only deletion of a previous maximum graph this bounds one replacement
18
- /// batch below `wire::BATCH_BYTES`.
19
- pub(super) const ELEMENT_BYTES: usize = 100_000;
24
+ /// The atomic replacement budget. One replacement is one nfnetlink batch in one
25
+ /// `sendmsg`, and Linux refuses a netlink message above the socket send buffer:
26
+ /// `SO_SNDBUF` clamped to `net.core.wmem_max` and doubled, 425,984 bytes with the
27
+ /// default `wmem_max` of 212,992. That is the hard ceiling; `wire::BATCH_BYTES`
28
+ /// (320,000) is this package's choice, which fits every `wmem_max` from 160,016
29
+ /// up. A batch carries its begin and end messages (at most 64 bytes), the
30
+ /// handle-only deletion of the previous graph — fewer than `OPERATIONS` rules,
31
+ /// chains and sets, each message at most 140 bytes (the longest table name, a
32
+ /// chain name of up to 32 bytes and a handle), 107,520 bytes — and the complete
33
+ /// target: its rules, chains and sets within `RULE_BYTES`, and together with its
34
+ /// set elements within `TARGET_BYTES`. 64 + 107,520 + 212,000 = 319,584 bytes.
35
+ /// The elements take whatever the rules leave: a set replaces a rule per entry,
36
+ /// so the element share grows exactly where the rule share shrinks.
37
+ pub(super) const OPERATIONS: usize = 768;
38
+ pub(super) const RULE_BYTES: usize = 100_000;
39
+ pub(super) const TARGET_BYTES: usize = 212_000;
40
+
41
+ /// One element of an interval set: the first and last key, each field of the
42
+ /// last at or above the same field of the first, and the data of a map.
43
+ #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
44
+ pub(super) struct IntervalElement {
45
+ pub(super) first: Vec<u8>,
46
+ pub(super) last: Vec<u8>,
47
+ pub(super) data: Option<Vec<u8>>,
48
+ }
20
49
 
21
50
  pub(super) struct Program<'a> {
22
51
  table: &'a str,
@@ -32,10 +61,69 @@ impl<'a> Program<'a> {
32
61
  // Same atomic replacement reserve as v1: a previous bounded graph must
33
62
  // fit its handle-only deletions beside the complete target, and the
34
63
  // target's set elements, in one `wire::BATCH_BYTES` batch.
35
- require(self.bytes <= 100_000 && self.operations.len() < 768)?;
64
+ require(
65
+ self.bytes <= RULE_BYTES
66
+ && self.bytes + self.element_bytes <= TARGET_BYTES
67
+ && self.operations.len() < OPERATIONS,
68
+ )?;
36
69
  self.operations.push((operation, attributes));
37
70
  Ok(())
38
71
  }
72
+ /// Element messages count against the target, beside the rules.
73
+ fn push_elements(&mut self, attributes: Vec<Attr>) -> Result<()> {
74
+ self.element_bytes += wire::encode_attrs(&attributes).len() + 20;
75
+ require(
76
+ self.bytes + self.element_bytes <= TARGET_BYTES && self.operations.len() < OPERATIONS,
77
+ )?;
78
+ self.operations.push((12, attributes));
79
+ Ok(())
80
+ }
81
+ /// One named exact-key hash set, or a map to `data_length` bytes. CONSTANT
82
+ /// like every set here.
83
+ fn exact_set(
84
+ &mut self,
85
+ name: &str,
86
+ id: u32,
87
+ key_length: u32,
88
+ data_length: Option<u32>,
89
+ ) -> Result<()> {
90
+ let mut attributes = vec![
91
+ Attr::string(1, self.table),
92
+ Attr::string(2, name),
93
+ Attr::u32(3, 2 | if data_length.is_some() { 8 } else { 0 }),
94
+ Attr::u32(4, 0),
95
+ Attr::u32(5, key_length),
96
+ ];
97
+ if let Some(length) = data_length {
98
+ attributes.extend([Attr::u32(6, 0), Attr::u32(7, length)]);
99
+ }
100
+ attributes.push(Attr::u32(10, id));
101
+ self.push(9, attributes)
102
+ }
103
+ fn exact_elements(
104
+ &mut self,
105
+ name: &str,
106
+ elements: Vec<(Vec<u8>, Option<Vec<u8>>)>,
107
+ ) -> Result<()> {
108
+ for chunk in elements.chunks(256) {
109
+ let list = chunk
110
+ .iter()
111
+ .map(|(key, data)| {
112
+ let mut fields = vec![Attr::nested(1, vec![Attr::bytes(1, key.clone())])];
113
+ if let Some(data) = data {
114
+ fields.push(Attr::nested(2, vec![Attr::bytes(1, data.clone())]));
115
+ }
116
+ Attr::nested(1, fields)
117
+ })
118
+ .collect();
119
+ self.push_elements(vec![
120
+ Attr::string(1, self.table),
121
+ Attr::string(2, name),
122
+ Attr::nested(3, list),
123
+ ])?;
124
+ }
125
+ Ok(())
126
+ }
39
127
  /// One named exact-match set. CONSTANT: its elements cannot change once a
40
128
  /// rule binds it, so replacement always creates a complete new set.
41
129
  fn set(&mut self, name: &str, id: u32, key_length: usize) -> Result<()> {
@@ -56,21 +144,72 @@ impl<'a> Program<'a> {
56
144
  /// own budget: a replacement deletes the previous set, and so its elements,
57
145
  /// by one handle.
58
146
  fn elements(&mut self, name: &str, keys: Vec<Vec<u8>>) -> Result<()> {
59
- for chunk in keys.chunks(256) {
60
- let elements = chunk
147
+ self.exact_elements(name, keys.into_iter().map(|key| (key, None)).collect())
148
+ }
149
+ /// One named CONSTANT set of concatenated fields in which every field is an
150
+ /// inclusive interval (the kernel's pipapo backend), optionally a map to
151
+ /// `data_length` bytes. `fields` are byte lengths; each occupies whole
152
+ /// 32-bit registers of the key.
153
+ fn interval_set(
154
+ &mut self,
155
+ name: &str,
156
+ id: u32,
157
+ fields: &[u32],
158
+ data_length: Option<u32>,
159
+ ) -> Result<()> {
160
+ let key_length: u32 = fields.iter().map(|length| length.div_ceil(4) * 4).sum();
161
+ let mut attributes = vec![
162
+ Attr::string(1, self.table),
163
+ Attr::string(2, name),
164
+ // CONSTANT | INTERVAL | CONCAT, and MAP for a map.
165
+ Attr::u32(3, 2 | 4 | 0x80 | if data_length.is_some() { 8 } else { 0 }),
166
+ Attr::u32(4, 0),
167
+ Attr::u32(5, key_length),
168
+ ];
169
+ if let Some(length) = data_length {
170
+ attributes.extend([Attr::u32(6, 0), Attr::u32(7, length)]);
171
+ }
172
+ attributes.extend([
173
+ Attr::nested(
174
+ 9,
175
+ vec![Attr::nested(
176
+ 2,
177
+ fields
178
+ .iter()
179
+ .map(|length| Attr::nested(1, vec![Attr::u32(1, *length)]))
180
+ .collect(),
181
+ )],
182
+ ),
183
+ Attr::u32(10, id),
184
+ ]);
185
+ self.push(9, attributes)
186
+ }
187
+ /// Interval elements `(first, last, data)`, 256 to a message like `elements`.
188
+ /// An element whose bounds are equal carries no last key; Linux takes the
189
+ /// first as the last and dumps it the same way.
190
+ fn interval_elements(&mut self, name: &str, elements: Vec<IntervalElement>) -> Result<()> {
191
+ for chunk in elements.chunks(256) {
192
+ let list = chunk
61
193
  .iter()
62
- .map(|key| {
63
- Attr::nested(1, vec![Attr::nested(1, vec![Attr::bytes(1, key.clone())])])
194
+ .map(|element| {
195
+ let value = |kind: u16, bytes: &[u8]| {
196
+ Attr::nested(kind, vec![Attr::bytes(1, bytes.to_vec())])
197
+ };
198
+ let mut fields = vec![value(1, &element.first)];
199
+ if element.last != element.first {
200
+ fields.push(value(10, &element.last));
201
+ }
202
+ if let Some(data) = &element.data {
203
+ fields.push(value(2, data));
204
+ }
205
+ Attr::nested(1, fields)
64
206
  })
65
207
  .collect();
66
- let attributes = vec![
208
+ self.push_elements(vec![
67
209
  Attr::string(1, self.table),
68
210
  Attr::string(2, name),
69
- Attr::nested(3, elements),
70
- ];
71
- self.element_bytes += wire::encode_attrs(&attributes).len() + 20;
72
- require(self.element_bytes <= ELEMENT_BYTES && self.operations.len() < 768)?;
73
- self.operations.push((12, attributes));
211
+ Attr::nested(3, list),
212
+ ])?;
74
213
  }
75
214
  Ok(())
76
215
  }
@@ -243,12 +382,6 @@ fn immediate(register: u32, value: Vec<u8>) -> Attr {
243
382
  ],
244
383
  )
245
384
  }
246
- fn set_ct(key: u32, value: Vec<u8>) -> Vec<Attr> {
247
- vec![
248
- immediate(1, value),
249
- expr("ct", vec![Attr::u32(2, key), Attr::u32(4, 1)]),
250
- ]
251
- }
252
385
  fn label(generation: &Generation) -> Vec<u8> {
253
386
  // A label is an opaque canonical 16-byte value, never a host-endian integer.
254
387
  generation
@@ -258,14 +391,6 @@ fn label(generation: &Generation) -> Vec<u8> {
258
391
  .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())
259
392
  .collect()
260
393
  }
261
- fn generation(generation: &Generation, direction: u8, labelled: bool) -> Vec<Attr> {
262
- let mut result = ct(17, None, generation.conntrack_zone.to_ne_bytes().to_vec());
263
- result.extend(ct(1, None, vec![direction]));
264
- if labelled {
265
- result.extend(ct(13, None, label(generation)));
266
- }
267
- result
268
- }
269
394
  fn state(value: u32) -> Vec<Attr> {
270
395
  ct(0, None, value.to_ne_bytes().to_vec())
271
396
  }
@@ -297,50 +422,6 @@ fn snat(address: &str, range: Option<(u16, u16)>) -> Result<Vec<Attr>> {
297
422
  result.push(expr("nat", data));
298
423
  Ok(result)
299
424
  }
300
- /// Published inbound translation. Address and port are always both specified, so
301
- /// the kernel never picks a port and the graph identity carries explicit registers.
302
- fn dnat(address: &str, port: u16) -> Result<Vec<Attr>> {
303
- Ok(vec![
304
- immediate(1, super::address(address)?.to_be_bytes().to_vec()),
305
- immediate(2, port.to_be_bytes().to_vec()),
306
- expr(
307
- "nat",
308
- vec![
309
- Attr::u32(1, 1),
310
- Attr::u32(2, 2),
311
- Attr::u32(3, 1),
312
- Attr::u32(4, 1),
313
- Attr::u32(5, 2),
314
- Attr::u32(6, 2),
315
- Attr::u32(7, 3),
316
- ],
317
- ),
318
- ])
319
- }
320
- fn tuple(grant: &Grant, protection: &Protection, reply: bool, original: bool) -> Result<Vec<Attr>> {
321
- let destination = super::destination(grant, protection)?;
322
- let mut result = ipv4();
323
- result.extend(meta(16, vec![protocol(&grant.protocol)]));
324
- result.extend(current_address(&grant.source_prefix, !reply)?);
325
- result.extend(current_address(&destination, reply)?);
326
- for (port, source) in [(grant.source_port, !reply), (grant.destination_port, reply)] {
327
- if let Some(port) = port {
328
- result.extend(current_port(port, source));
329
- }
330
- }
331
- if original {
332
- result.extend(original_address(&grant.source_prefix, true)?);
333
- result.extend(original_address(&destination, false)?);
334
- result.extend(ct(10, None, vec![protocol(&grant.protocol)]));
335
- for (port, source) in [(grant.source_port, true), (grant.destination_port, false)] {
336
- if let Some(port) = port {
337
- result.extend(original_port(port, source));
338
- }
339
- }
340
- }
341
- Ok(result)
342
- }
343
-
344
425
  pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<Attr>)>> {
345
426
  let mut program = Program {
346
427
  table,
@@ -351,9 +432,7 @@ pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<
351
432
  operations: Vec::new(),
352
433
  };
353
434
  match &prepared.policy.scope {
354
- Scope::RouterEgress(scope) => {
355
- router::compile(&mut program, scope, prepared.policy.revision)?
356
- }
435
+ Scope::RouterEgress(scope) => router::compile(&mut program, scope)?,
357
436
  Scope::HostTransit(scope) => host::compile(&mut program, scope)?,
358
437
  Scope::AllocationPoolGuard(scope) => poolguard::compile(&mut program, scope)?,
359
438
  }
@@ -66,34 +66,12 @@ fn leased(
66
66
  expressions.extend(original_range(range.first, range.last));
67
67
  Ok(expressions)
68
68
  }
69
- /// Inbound publication envelope. Both directions check the exact links, the
70
- /// translated current tuple and the original conntrack destination, so foreign
71
- /// NAT cannot borrow this admission for another flow.
72
- fn published_envelope(
73
- scope: &HostScope,
74
- handoff: &HostHandoff,
75
- port: &PublishedPort,
76
- reply: bool,
77
- ) -> Result<Vec<Attr>> {
78
- let mut expressions = ipv4();
79
- expressions.extend(link(&scope.uplink, !reply));
80
- expressions.extend(link(&handoff.link, reply));
81
- expressions.extend(meta(16, vec![protocol(&port.protocol)]));
82
- expressions.extend(ct(10, None, vec![protocol(&port.protocol)]));
83
- expressions.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
84
- expressions.extend(ct(1, None, vec![u8::from(reply)]));
85
- let target = format!("{}/32", port.target_address);
86
- expressions.extend(current_address(&target, reply)?);
87
- expressions.extend(current_port(port.target_port, reply));
88
- let published = format!("{}/32", port.host_ip);
89
- expressions.extend(original_address(&published, false)?);
90
- expressions.extend(original_port(port.host_port, false));
91
- Ok(expressions)
92
- }
93
- fn published(program: &mut Program<'_>, scope: &HostScope) -> Result<()> {
69
+ /// The publications of this hop. Validation admits only leased transit source
70
+ /// addresses as targets, and one address never spans two active handoff links,
71
+ /// so each publication has exactly one handoff.
72
+ fn hop(scope: &HostScope) -> Result<published::Hop<'_>> {
73
+ let mut publications = Vec::new();
94
74
  for port in &scope.published_ports {
95
- // Validation admits only leased transit source addresses, and one address
96
- // never spans two active handoff links.
97
75
  let handoff = scope
98
76
  .handoffs
99
77
  .iter()
@@ -104,30 +82,18 @@ fn published(program: &mut Program<'_>, scope: &HostScope) -> Result<()> {
104
82
  .any(|allocation| allocation.transit_source_address == port.target_address)
105
83
  })
106
84
  .ok_or(crate::Error::Invalid)?;
107
- let original = published_envelope(scope, handoff, port, false)?;
108
- for connection_state in [2, 8] {
109
- let mut expressions = original.clone();
110
- expressions.extend(state(connection_state));
111
- if connection_state == 8 && port.protocol == "tcp" {
112
- expressions.extend(opening_tcp());
113
- }
114
- program.end("forward", expressions, 1)?;
115
- }
116
- let mut reply = published_envelope(scope, handoff, port, true)?;
117
- reply.extend(state(2));
118
- program.end("forward", reply, 1)?;
119
- let mut translation = ipv4();
120
- translation.extend(link(&scope.uplink, true));
121
- translation.extend(meta(16, vec![protocol(&port.protocol)]));
122
- translation.extend(ct(10, None, vec![protocol(&port.protocol)]));
123
- translation.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
124
- translation.extend(ct(1, None, vec![0]));
125
- translation.extend(current_address(&format!("{}/32", port.host_ip), false)?);
126
- translation.extend(current_port(port.host_port, false));
127
- translation.extend(dnat(&port.target_address, port.target_port)?);
128
- program.rule("pre", translation)?;
85
+ publications.push(published::Publication {
86
+ protocol: &port.protocol,
87
+ outside: (&port.host_ip, port.host_port, port.host_last()),
88
+ inside: (&port.target_address, port.target_port, port.target_last()),
89
+ link: &handoff.link,
90
+ symmetric: port.symmetric,
91
+ });
129
92
  }
130
- Ok(())
93
+ Ok(published::Hop {
94
+ publications,
95
+ fixed: &scope.uplink,
96
+ })
131
97
  }
132
98
  /// The exact platform endpoints served on host addresses. Every packet is
133
99
  /// checked against the live and the originally tracked destination tuple, so a
@@ -235,12 +201,15 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &HostScope) -> Result<()
235
201
  .ok_or(crate::Error::Invalid)
236
202
  })?;
237
203
  }
204
+ let hop = hop(scope)?;
205
+ published::sets(program, &hop, false)?;
238
206
  protection(program, scope, false)?;
239
207
  protection(program, scope, true)?;
208
+ published::translations(program, &hop)?;
240
209
  // Published admission precedes the capture barrier: the barrier denies every
241
210
  // protected original destination, which includes this uplink address, and an
242
211
  // exact inbound publication is the explicit authority for that exception.
243
- published(program, scope)?;
212
+ published::inbound(program, &hop)?;
244
213
  // Host-origin grants leave through the exact handoff that holds their source
245
214
  // address, which is part of every set key, and precede the terminal output
246
215
  // and input denials. Nothing else of the host's own traffic crosses a handoff.
@@ -275,6 +244,10 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &HostScope) -> Result<()
275
244
  }
276
245
  }
277
246
  }
247
+ // Symmetric flows leave from ports outside every leased range of their
248
+ // address, after the capture barrier and before the terminal handoff denials.
249
+ published::symmetric(program, &hop)?;
250
+ published::symmetric_translation(program, &hop)?;
278
251
  for handoff in &scope.handoffs {
279
252
  program.deny_link("input", &handoff.link, true)?;
280
253
  program.deny_link("output", &handoff.link, false)?;
@@ -38,7 +38,7 @@ pub(super) fn payload_to(base: u32, offset: u32, length: u32, index: u32) -> Att
38
38
  index,
39
39
  )
40
40
  }
41
- fn ct_to(key: u32, original: bool, index: u32) -> Attr {
41
+ pub(super) fn ct_to(key: u32, original: bool, index: u32) -> Attr {
42
42
  let mut data = vec![Attr::u32(2, key)];
43
43
  if original {
44
44
  data.push(Attr::bytes(3, vec![0]));