@push.rocks/smartnftables 2.5.2 → 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 (41) hide show
  1. package/changelog.md +31 -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 +69 -3
  8. package/package.json +6 -6
  9. package/readme.md +342 -61
  10. package/rust/src/egress.compile.rs +157 -76
  11. package/rust/src/egress.host.rs +104 -44
  12. package/rust/src/egress.hostgrant.rs +14 -14
  13. package/rust/src/egress.keys.rs +305 -0
  14. package/rust/src/egress.leased.rs +399 -0
  15. package/rust/src/egress.poolguard.rs +45 -0
  16. package/rust/src/egress.private.rs +197 -0
  17. package/rust/src/egress.published.rs +370 -0
  18. package/rust/src/egress.router.rs +64 -295
  19. package/rust/src/egress.rs +248 -17
  20. package/rust/src/egress.workloadgrant.rs +88 -0
  21. package/rust/src/egress_hostgrant_tests.rs +196 -47
  22. package/rust/src/egress_localplatform_tests.rs +204 -0
  23. package/rust/src/egress_localport_tests.rs +266 -0
  24. package/rust/src/egress_publishedrange_tests.rs +812 -0
  25. package/rust/src/egress_routerequivalence_tests.rs +448 -0
  26. package/rust/src/egress_tests.rs +132 -230
  27. package/rust/src/egress_workloadgrant_tests.rs +313 -0
  28. package/rust/src/owner.rs +77 -15
  29. package/rust/src/owner_host_traffic_tests.rs +4 -0
  30. package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
  31. package/rust/src/owner_identity_tests.rs +228 -80
  32. package/rust/src/owner_localplatform_tests.rs +274 -0
  33. package/rust/src/owner_localport_tests.rs +241 -0
  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 +15 -0
  38. package/rust/src/owner_workloadgrant_tests.rs +278 -0
  39. package/rust/src/policy.rs +29 -25
  40. package/ts/00_commitinfo_data.ts +1 -1
  41. package/ts/managed.egress.types.ts +71 -3
@@ -6,15 +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;
21
+ #[path = "egress.workloadgrant.rs"]
22
+ mod workloadgrant;
13
23
 
14
- /// Set elements of one target. Together with the 100,000-byte rule graph and the
15
- /// handle-only deletion of a previous maximum graph this bounds one replacement
16
- /// batch below `wire::BATCH_BYTES`.
17
- 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
+ }
18
49
 
19
50
  pub(super) struct Program<'a> {
20
51
  table: &'a str,
@@ -30,10 +61,69 @@ impl<'a> Program<'a> {
30
61
  // Same atomic replacement reserve as v1: a previous bounded graph must
31
62
  // fit its handle-only deletions beside the complete target, and the
32
63
  // target's set elements, in one `wire::BATCH_BYTES` batch.
33
- 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
+ )?;
34
69
  self.operations.push((operation, attributes));
35
70
  Ok(())
36
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
+ }
37
127
  /// One named exact-match set. CONSTANT: its elements cannot change once a
38
128
  /// rule binds it, so replacement always creates a complete new set.
39
129
  fn set(&mut self, name: &str, id: u32, key_length: usize) -> Result<()> {
@@ -54,21 +144,72 @@ impl<'a> Program<'a> {
54
144
  /// own budget: a replacement deletes the previous set, and so its elements,
55
145
  /// by one handle.
56
146
  fn elements(&mut self, name: &str, keys: Vec<Vec<u8>>) -> Result<()> {
57
- for chunk in keys.chunks(256) {
58
- 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
59
193
  .iter()
60
- .map(|key| {
61
- 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)
62
206
  })
63
207
  .collect();
64
- let attributes = vec![
208
+ self.push_elements(vec![
65
209
  Attr::string(1, self.table),
66
210
  Attr::string(2, name),
67
- Attr::nested(3, elements),
68
- ];
69
- self.element_bytes += wire::encode_attrs(&attributes).len() + 20;
70
- require(self.element_bytes <= ELEMENT_BYTES && self.operations.len() < 768)?;
71
- self.operations.push((12, attributes));
211
+ Attr::nested(3, list),
212
+ ])?;
72
213
  }
73
214
  Ok(())
74
215
  }
@@ -241,12 +382,6 @@ fn immediate(register: u32, value: Vec<u8>) -> Attr {
241
382
  ],
242
383
  )
243
384
  }
244
- fn set_ct(key: u32, value: Vec<u8>) -> Vec<Attr> {
245
- vec![
246
- immediate(1, value),
247
- expr("ct", vec![Attr::u32(2, key), Attr::u32(4, 1)]),
248
- ]
249
- }
250
385
  fn label(generation: &Generation) -> Vec<u8> {
251
386
  // A label is an opaque canonical 16-byte value, never a host-endian integer.
252
387
  generation
@@ -256,14 +391,6 @@ fn label(generation: &Generation) -> Vec<u8> {
256
391
  .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())
257
392
  .collect()
258
393
  }
259
- fn generation(generation: &Generation, direction: u8, labelled: bool) -> Vec<Attr> {
260
- let mut result = ct(17, None, generation.conntrack_zone.to_ne_bytes().to_vec());
261
- result.extend(ct(1, None, vec![direction]));
262
- if labelled {
263
- result.extend(ct(13, None, label(generation)));
264
- }
265
- result
266
- }
267
394
  fn state(value: u32) -> Vec<Attr> {
268
395
  ct(0, None, value.to_ne_bytes().to_vec())
269
396
  }
@@ -295,50 +422,6 @@ fn snat(address: &str, range: Option<(u16, u16)>) -> Result<Vec<Attr>> {
295
422
  result.push(expr("nat", data));
296
423
  Ok(result)
297
424
  }
298
- /// Published inbound translation. Address and port are always both specified, so
299
- /// the kernel never picks a port and the graph identity carries explicit registers.
300
- fn dnat(address: &str, port: u16) -> Result<Vec<Attr>> {
301
- Ok(vec![
302
- immediate(1, super::address(address)?.to_be_bytes().to_vec()),
303
- immediate(2, port.to_be_bytes().to_vec()),
304
- expr(
305
- "nat",
306
- vec![
307
- Attr::u32(1, 1),
308
- Attr::u32(2, 2),
309
- Attr::u32(3, 1),
310
- Attr::u32(4, 1),
311
- Attr::u32(5, 2),
312
- Attr::u32(6, 2),
313
- Attr::u32(7, 3),
314
- ],
315
- ),
316
- ])
317
- }
318
- fn tuple(grant: &Grant, protection: &Protection, reply: bool, original: bool) -> Result<Vec<Attr>> {
319
- let destination = super::destination(grant, protection)?;
320
- let mut result = ipv4();
321
- result.extend(meta(16, vec![protocol(&grant.protocol)]));
322
- result.extend(current_address(&grant.source_prefix, !reply)?);
323
- result.extend(current_address(&destination, reply)?);
324
- for (port, source) in [(grant.source_port, !reply), (grant.destination_port, reply)] {
325
- if let Some(port) = port {
326
- result.extend(current_port(port, source));
327
- }
328
- }
329
- if original {
330
- result.extend(original_address(&grant.source_prefix, true)?);
331
- result.extend(original_address(&destination, false)?);
332
- result.extend(ct(10, None, vec![protocol(&grant.protocol)]));
333
- for (port, source) in [(grant.source_port, true), (grant.destination_port, false)] {
334
- if let Some(port) = port {
335
- result.extend(original_port(port, source));
336
- }
337
- }
338
- }
339
- Ok(result)
340
- }
341
-
342
425
  pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<Attr>)>> {
343
426
  let mut program = Program {
344
427
  table,
@@ -349,9 +432,7 @@ pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<
349
432
  operations: Vec::new(),
350
433
  };
351
434
  match &prepared.policy.scope {
352
- Scope::RouterEgress(scope) => {
353
- router::compile(&mut program, scope, prepared.policy.revision)?
354
- }
435
+ Scope::RouterEgress(scope) => router::compile(&mut program, scope)?,
355
436
  Scope::HostTransit(scope) => host::compile(&mut program, scope)?,
356
437
  Scope::AllocationPoolGuard(scope) => poolguard::compile(&mut program, scope)?,
357
438
  }
@@ -36,10 +36,25 @@ fn envelope(
36
36
  allocation: &Allocation,
37
37
  range: &PortRange,
38
38
  reply: bool,
39
+ ) -> Result<Vec<Attr>> {
40
+ leased(handoff, Some(&scope.uplink), allocation, range, reply)
41
+ }
42
+ /// One leased range of one allocation on its exact handoff: the default zone, the
43
+ /// direction, the protocols, and the transit source address and port range in
44
+ /// both the live and the originally tracked tuple. A forwarded envelope adds the
45
+ /// uplink; a host-local one has none.
46
+ fn leased(
47
+ handoff: &HostHandoff,
48
+ uplink: Option<&LocalLink>,
49
+ allocation: &Allocation,
50
+ range: &PortRange,
51
+ reply: bool,
39
52
  ) -> Result<Vec<Attr>> {
40
53
  let mut expressions = ipv4();
41
54
  expressions.extend(link(&handoff.link, !reply));
42
- expressions.extend(link(&scope.uplink, reply));
55
+ if let Some(uplink) = uplink {
56
+ expressions.extend(link(uplink, reply));
57
+ }
43
58
  expressions.extend(meta(16, vec![protocol(&range.protocol)]));
44
59
  expressions.extend(ct(10, None, vec![protocol(&range.protocol)]));
45
60
  expressions.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
@@ -51,34 +66,12 @@ fn envelope(
51
66
  expressions.extend(original_range(range.first, range.last));
52
67
  Ok(expressions)
53
68
  }
54
- /// Inbound publication envelope. Both directions check the exact links, the
55
- /// translated current tuple and the original conntrack destination, so foreign
56
- /// NAT cannot borrow this admission for another flow.
57
- fn published_envelope(
58
- scope: &HostScope,
59
- handoff: &HostHandoff,
60
- port: &PublishedPort,
61
- reply: bool,
62
- ) -> Result<Vec<Attr>> {
63
- let mut expressions = ipv4();
64
- expressions.extend(link(&scope.uplink, !reply));
65
- expressions.extend(link(&handoff.link, reply));
66
- expressions.extend(meta(16, vec![protocol(&port.protocol)]));
67
- expressions.extend(ct(10, None, vec![protocol(&port.protocol)]));
68
- expressions.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
69
- expressions.extend(ct(1, None, vec![u8::from(reply)]));
70
- let target = format!("{}/32", port.target_address);
71
- expressions.extend(current_address(&target, reply)?);
72
- expressions.extend(current_port(port.target_port, reply));
73
- let published = format!("{}/32", port.host_ip);
74
- expressions.extend(original_address(&published, false)?);
75
- expressions.extend(original_port(port.host_port, false));
76
- Ok(expressions)
77
- }
78
- 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();
79
74
  for port in &scope.published_ports {
80
- // Validation admits only leased transit source addresses, and one address
81
- // never spans two active handoff links.
82
75
  let handoff = scope
83
76
  .handoffs
84
77
  .iter()
@@ -89,28 +82,80 @@ fn published(program: &mut Program<'_>, scope: &HostScope) -> Result<()> {
89
82
  .any(|allocation| allocation.transit_source_address == port.target_address)
90
83
  })
91
84
  .ok_or(crate::Error::Invalid)?;
92
- let original = published_envelope(scope, handoff, port, false)?;
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
+ });
92
+ }
93
+ Ok(published::Hop {
94
+ publications,
95
+ fixed: &scope.uplink,
96
+ })
97
+ }
98
+ /// The exact platform endpoints served on host addresses. Every packet is
99
+ /// checked against the live and the originally tracked destination tuple, so a
100
+ /// flow rewritten in transit never matches.
101
+ fn local_endpoints(scope: &HostScope) -> Vec<&PlatformEndpoint> {
102
+ scope
103
+ .protection
104
+ .platform_endpoints
105
+ .iter()
106
+ .filter(|endpoint| scope.local_platform_endpoints.contains(&endpoint.id))
107
+ .collect()
108
+ }
109
+ fn local_endpoint(endpoint: &PlatformEndpoint, reply: bool) -> Result<Vec<Attr>> {
110
+ let mut expressions = meta(16, vec![protocol(&endpoint.protocol)]);
111
+ expressions.extend(ct(10, None, vec![protocol(&endpoint.protocol)]));
112
+ let address = format!("{}/32", endpoint.address);
113
+ expressions.extend(current_address(&address, reply)?);
114
+ expressions.extend(current_port(endpoint.port, reply));
115
+ expressions.extend(original_address(&address, false)?);
116
+ expressions.extend(original_port(endpoint.port, false));
117
+ Ok(expressions)
118
+ }
119
+ /// Leased flows into host-local platform endpoints, the reverse of a host grant:
120
+ /// INPUT admits the original direction only from an exact handoff with a leased
121
+ /// source address and port range of that handoff, NEW (a TCP opening only with
122
+ /// SYN) or ESTABLISHED; OUTPUT admits only the ESTABLISHED reply back to it. Each
123
+ /// leased range jumps once per direction, and the endpoint chains hold one tuple
124
+ /// check per endpoint, so the graph grows with ranges plus endpoints.
125
+ fn local_platform(program: &mut Program<'_>, scope: &HostScope) -> Result<()> {
126
+ let endpoints = local_endpoints(scope);
127
+ for endpoint in &endpoints {
128
+ let original = local_endpoint(endpoint, false)?;
93
129
  for connection_state in [2, 8] {
94
130
  let mut expressions = original.clone();
95
131
  expressions.extend(state(connection_state));
96
- if connection_state == 8 && port.protocol == "tcp" {
132
+ if connection_state == 8 && endpoint.protocol == "tcp" {
97
133
  expressions.extend(opening_tcp());
98
134
  }
99
- program.end("forward", expressions, 1)?;
135
+ program.end("local_platform_in", expressions, 1)?;
100
136
  }
101
- let mut reply = published_envelope(scope, handoff, port, true)?;
137
+ let mut reply = local_endpoint(endpoint, true)?;
102
138
  reply.extend(state(2));
103
- program.end("forward", reply, 1)?;
104
- let mut translation = ipv4();
105
- translation.extend(link(&scope.uplink, true));
106
- translation.extend(meta(16, vec![protocol(&port.protocol)]));
107
- translation.extend(ct(10, None, vec![protocol(&port.protocol)]));
108
- translation.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
109
- translation.extend(ct(1, None, vec![0]));
110
- translation.extend(current_address(&format!("{}/32", port.host_ip), false)?);
111
- translation.extend(current_port(port.host_port, false));
112
- translation.extend(dnat(&port.target_address, port.target_port)?);
113
- program.rule("pre", translation)?;
139
+ program.end("local_platform_back", reply, 1)?;
140
+ }
141
+ for handoff in &scope.handoffs {
142
+ for allocation in &handoff.allocations {
143
+ for range in &allocation.source_port_ranges {
144
+ if endpoints
145
+ .iter()
146
+ .all(|endpoint| endpoint.protocol != range.protocol)
147
+ {
148
+ continue;
149
+ }
150
+ for (chain, target, reply) in [
151
+ ("input", "local_platform_in", false),
152
+ ("output", "local_platform_back", true),
153
+ ] {
154
+ let expressions = leased(handoff, None, allocation, range, reply)?;
155
+ program.jump(chain, expressions, target)?;
156
+ }
157
+ }
158
+ }
114
159
  }
115
160
  Ok(())
116
161
  }
@@ -135,6 +180,11 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &HostScope) -> Result<()
135
180
  for name in ["protected_out", "protected_back"] {
136
181
  program.chain(name, None)?;
137
182
  }
183
+ if !scope.local_platform_endpoints.is_empty() {
184
+ for name in ["local_platform_in", "local_platform_back"] {
185
+ program.chain(name, None)?;
186
+ }
187
+ }
138
188
  if !scope.host_grants.is_empty() {
139
189
  // Validation admits only an address of exactly one handoff link.
140
190
  hostgrant::tuple_set(program, &scope.host_grants, |grant| {
@@ -151,18 +201,24 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &HostScope) -> Result<()
151
201
  .ok_or(crate::Error::Invalid)
152
202
  })?;
153
203
  }
204
+ let hop = hop(scope)?;
205
+ published::sets(program, &hop, false)?;
154
206
  protection(program, scope, false)?;
155
207
  protection(program, scope, true)?;
208
+ published::translations(program, &hop)?;
156
209
  // Published admission precedes the capture barrier: the barrier denies every
157
210
  // protected original destination, which includes this uplink address, and an
158
211
  // exact inbound publication is the explicit authority for that exception.
159
- published(program, scope)?;
212
+ published::inbound(program, &hop)?;
160
213
  // Host-origin grants leave through the exact handoff that holds their source
161
214
  // address, which is part of every set key, and precede the terminal output
162
215
  // and input denials. Nothing else of the host's own traffic crosses a handoff.
163
216
  if !scope.host_grants.is_empty() {
164
217
  hostgrant::admissions(program, ["output", "input"], true, |_| vec![])?;
165
218
  }
219
+ // Leased flows into host-local platform endpoints precede the same terminal
220
+ // handoff denials, which remain for everything else.
221
+ local_platform(program, scope)?;
166
222
  for handoff in &scope.handoffs {
167
223
  program.jump("forward", link(&handoff.link, true), "protected_out")?;
168
224
  program.jump("forward", link(&handoff.link, false), "protected_back")?;
@@ -188,6 +244,10 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &HostScope) -> Result<()
188
244
  }
189
245
  }
190
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)?;
191
251
  for handoff in &scope.handoffs {
192
252
  program.deny_link("input", &handoff.link, true)?;
193
253
  program.deny_link("output", &handoff.link, false)?;
@@ -13,7 +13,7 @@ pub(super) const ARRIVAL_SET: &str = "host_grant_arrival";
13
13
 
14
14
  /// Register numbers as Linux dumps them: 16-byte aligned registers use the
15
15
  /// legacy NFT_REG_1..4 names, every other 4-byte register NFT_REG32_xx.
16
- fn register(index: u32) -> u32 {
16
+ pub(super) fn register(index: u32) -> u32 {
17
17
  if index % 4 == 0 {
18
18
  index / 4
19
19
  } else {
@@ -24,10 +24,10 @@ fn store(name: &str, mut data: Vec<Attr>, index: u32) -> Attr {
24
24
  data.insert(0, Attr::u32(1, register(index)));
25
25
  expr(name, data)
26
26
  }
27
- fn meta_to(key: u32, index: u32) -> Attr {
27
+ pub(super) fn meta_to(key: u32, index: u32) -> Attr {
28
28
  store("meta", vec![Attr::u32(2, key)], index)
29
29
  }
30
- fn payload_to(base: u32, offset: u32, length: u32, index: u32) -> Attr {
30
+ pub(super) fn payload_to(base: u32, offset: u32, length: u32, index: u32) -> Attr {
31
31
  store(
32
32
  "payload",
33
33
  vec![
@@ -38,14 +38,14 @@ fn payload_to(base: u32, offset: u32, length: u32, index: u32) -> Attr {
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]));
45
45
  }
46
46
  store("ct", data, index)
47
47
  }
48
- fn lookup(set: &str) -> Attr {
48
+ pub(super) fn lookup(set: &str) -> Attr {
49
49
  expr(
50
50
  "lookup",
51
51
  vec![
@@ -61,10 +61,10 @@ fn word(bytes: &[u8]) -> Vec<u8> {
61
61
  result
62
62
  }
63
63
 
64
- /// Key loads for one direction, then the lookup. `linked` adds the variable
65
- /// interface: the outgoing interface of the original direction, the incoming
66
- /// interface of the reply.
67
- fn tuple_lookup(linked: bool, reply: bool) -> Vec<Attr> {
64
+ /// Key loads for one direction, then the lookup in `set`. `linked` adds the
65
+ /// variable interface: the outgoing interface of the original direction, the
66
+ /// incoming interface of the reply.
67
+ pub(super) fn tuple_lookup(set: &str, linked: bool, reply: bool) -> Vec<Attr> {
68
68
  let mut result = Vec::new();
69
69
  let mut index = 4;
70
70
  if linked {
@@ -82,10 +82,10 @@ fn tuple_lookup(linked: bool, reply: bool) -> Vec<Attr> {
82
82
  result.push(ct_to(19, true, index + 5));
83
83
  result.push(ct_to(20, true, index + 6));
84
84
  result.push(ct_to(12, true, index + 7));
85
- result.push(lookup(TUPLE_SET));
85
+ result.push(lookup(set));
86
86
  result
87
87
  }
88
- fn tuple_key(grant: &HostGrant, link: Option<&LocalLink>) -> Result<Vec<u8>> {
88
+ pub(super) fn tuple_key(grant: &HostGrant, link: Option<&LocalLink>) -> Result<Vec<u8>> {
89
89
  let protocol = word(&[protocol(&grant.protocol)]);
90
90
  let source = address(&grant.source_address)?.to_be_bytes();
91
91
  let destination = address(&grant.destination_address)?.to_be_bytes();
@@ -170,7 +170,7 @@ pub(super) fn admissions(
170
170
  result
171
171
  };
172
172
  let mut established = envelope(false, 2);
173
- established.extend(tuple_lookup(linked, false));
173
+ established.extend(tuple_lookup(TUPLE_SET, linked, false));
174
174
  program.end(chains[0], established, 1)?;
175
175
  for transport in ["udp", "tcp"] {
176
176
  let mut opening = envelope(false, 8);
@@ -178,10 +178,10 @@ pub(super) fn admissions(
178
178
  if transport == "tcp" {
179
179
  opening.extend(opening_tcp());
180
180
  }
181
- opening.extend(tuple_lookup(linked, false));
181
+ opening.extend(tuple_lookup(TUPLE_SET, linked, false));
182
182
  program.end(chains[0], opening, 1)?;
183
183
  }
184
184
  let mut reply = envelope(true, 2);
185
- reply.extend(tuple_lookup(linked, true));
185
+ reply.extend(tuple_lookup(TUPLE_SET, linked, true));
186
186
  program.end(chains[1], reply, 1)
187
187
  }