@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
@@ -0,0 +1,197 @@
1
+ //! The router's private endpoints and directed rules compile to a constant number
2
+ //! of rules around named sets, so the rule graph does not grow with workloads.
3
+ //! Every endpoint link (index and name) is one element of `private_link`; the
4
+ //! anti-spoof guard, the directed rules and the terminal denials look the packet
5
+ //! up there and in sets keyed on the link index, so a renamed or reused interface
6
+ //! never matches an element of another link.
7
+ use super::keys::{self, Key, Loads, ADDRESS, INDEX, NAME, PORT, PROTOCOL};
8
+ use super::*;
9
+
10
+ /// Every endpoint link, index and name, and the absent interface (0, "") of
11
+ /// router-local traffic. Also the proof that a key's link index names its link:
12
+ /// indices are unique, so an index in a key and its pair in this set fix the name.
13
+ pub(super) const LINK: &str = "private_link";
14
+ /// Every endpoint link index, and every endpoint link name: either one alone is
15
+ /// enough for the terminal denial, so a rename or index reuse stays denied.
16
+ const INDEX_SET: &str = "private_index";
17
+ const NAME_SET: &str = "private_name";
18
+ /// Each endpoint link index with each of its source prefixes.
19
+ const SOURCE: &str = "private_source";
20
+ /// Directed rules of any protocol, and of one transport protocol.
21
+ const ANY: &str = "private_any";
22
+ const PORTED: &str = "private_port";
23
+
24
+ fn name(value: &str) -> Vec<u8> {
25
+ let mut bytes = value.as_bytes().to_vec();
26
+ bytes.resize(16, 0);
27
+ bytes
28
+ }
29
+ fn link_key(index: u32, interface: &str) -> Vec<u8> {
30
+ let mut key = index.to_ne_bytes().to_vec();
31
+ key.extend(name(interface));
32
+ key
33
+ }
34
+ fn index_of(scope: &RouterScope, id: &Option<String>) -> Result<u32> {
35
+ match id {
36
+ None => Ok(0),
37
+ Some(id) => scope
38
+ .endpoints
39
+ .iter()
40
+ .find(|endpoint| endpoint.id == *id)
41
+ .map(|endpoint| endpoint.interface_index)
42
+ .ok_or(Error::Invalid),
43
+ }
44
+ }
45
+
46
+ pub(super) fn sets(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
47
+ let mut links = vec![(link_key(0, ""), None)];
48
+ let mut indices = Vec::new();
49
+ let mut names = Vec::new();
50
+ let mut sources = Vec::new();
51
+ for endpoint in &scope.endpoints {
52
+ links.push((
53
+ link_key(endpoint.interface_index, &endpoint.interface_name),
54
+ None,
55
+ ));
56
+ indices.push((endpoint.interface_index.to_ne_bytes().to_vec(), None));
57
+ names.push((name(&endpoint.interface_name), None));
58
+ for prefix in &endpoint.source_prefixes {
59
+ sources.push(
60
+ Key::default()
61
+ .index(endpoint.interface_index)
62
+ .prefix(prefix)?
63
+ .element(None),
64
+ );
65
+ }
66
+ }
67
+ keys::exact(program, LINK, 32, INDEX + NAME, None, links)?;
68
+ keys::exact(program, INDEX_SET, 33, INDEX, None, indices)?;
69
+ keys::exact(program, NAME_SET, 34, NAME, None, names)?;
70
+ keys::interval(program, SOURCE, 35, &[INDEX, ADDRESS], None, sources)?;
71
+ let (mut any, mut ported) = (Vec::new(), Vec::new());
72
+ for rule in &scope.rules {
73
+ let key = Key::default()
74
+ .index(index_of(scope, &rule.source_endpoint)?)
75
+ .index(index_of(scope, &rule.destination_endpoint)?)
76
+ .prefix(&rule.source_prefix)?
77
+ .prefix(&rule.destination_prefix)?;
78
+ if rule.protocol == "any" {
79
+ any.push(key.element(None));
80
+ } else {
81
+ ported.push(
82
+ key.protocol(&rule.protocol)
83
+ .port(rule.source_port)
84
+ .port(rule.destination_port)
85
+ .element(None),
86
+ );
87
+ }
88
+ }
89
+ keys::interval(
90
+ program,
91
+ ANY,
92
+ 36,
93
+ &[INDEX, INDEX, ADDRESS, ADDRESS],
94
+ None,
95
+ any,
96
+ )?;
97
+ keys::interval(
98
+ program,
99
+ PORTED,
100
+ 37,
101
+ &[INDEX, INDEX, ADDRESS, ADDRESS, PROTOCOL, PORT, PORT],
102
+ None,
103
+ ported,
104
+ )?;
105
+ Ok(())
106
+ }
107
+ /// The directions a filter chain sees an endpoint link in.
108
+ fn directions(chain: &str) -> Vec<bool> {
109
+ match chain {
110
+ "input" => vec![true],
111
+ "output" => vec![false],
112
+ _ => vec![true, false],
113
+ }
114
+ }
115
+ /// The interface of one direction is an endpoint link or absent, by index and
116
+ /// name.
117
+ pub(super) fn known(incoming: bool) -> Vec<Attr> {
118
+ Loads::new().link(incoming).lookup(LINK)
119
+ }
120
+ /// Anti-spoof guards: a packet arriving from an endpoint link must carry one of
121
+ /// its source prefixes, and one leaving to it one of its prefixes as destination;
122
+ /// anything else on the link, IPv6 included, drops. The absent interface never
123
+ /// occurs in the directions a chain guards.
124
+ pub(super) fn guards(program: &mut Program<'_>, scope: &RouterScope, chain: &str) -> Result<()> {
125
+ if scope.endpoints.is_empty() {
126
+ return Ok(());
127
+ }
128
+ for incoming in directions(chain) {
129
+ let mut foreign = known(incoming);
130
+ foreign.push(expr("meta", vec![Attr::u32(1, 1), Attr::u32(2, 15)]));
131
+ foreign.push(expr(
132
+ "cmp",
133
+ vec![
134
+ Attr::u32(1, 1),
135
+ Attr::u32(2, 1),
136
+ Attr::nested(3, vec![Attr::bytes(1, vec![libc::NFPROTO_IPV4 as u8])]),
137
+ ],
138
+ ));
139
+ program.end(chain, foreign, 0)?;
140
+ let mut spoofed = ipv4();
141
+ spoofed.extend(known(incoming));
142
+ spoofed.extend(
143
+ Loads::new()
144
+ .index(incoming)
145
+ .address(incoming)
146
+ .absent(SOURCE),
147
+ );
148
+ program.end(chain, spoofed, 0)?;
149
+ }
150
+ Ok(())
151
+ }
152
+ /// Directed rules: the incoming and outgoing interface (an endpoint link, or
153
+ /// absent for the router itself), the addresses and, for a transport rule, the
154
+ /// protocol and ports. Input sees no outgoing and output no incoming interface,
155
+ /// so each chain matches exactly the rules that were compiled into it.
156
+ pub(super) fn grants(program: &mut Program<'_>, scope: &RouterScope, chain: &str) -> Result<()> {
157
+ for (set, ported) in [(ANY, false), (PORTED, true)] {
158
+ if !scope
159
+ .rules
160
+ .iter()
161
+ .any(|rule| (rule.protocol != "any") == ported)
162
+ {
163
+ continue;
164
+ }
165
+ let mut expressions = ipv4();
166
+ expressions.extend(known(true));
167
+ expressions.extend(known(false));
168
+ let mut loads = Loads::new();
169
+ loads.index(true).index(false).address(true).address(false);
170
+ if ported {
171
+ loads.protocol().port(true).port(false);
172
+ }
173
+ expressions.extend(loads.lookup(set));
174
+ program.end(chain, expressions, 1)?;
175
+ }
176
+ Ok(())
177
+ }
178
+ /// Terminal denial of every endpoint link, by index or by name alone.
179
+ pub(super) fn denials(program: &mut Program<'_>, scope: &RouterScope, chain: &str) -> Result<()> {
180
+ if scope.endpoints.is_empty() {
181
+ return Ok(());
182
+ }
183
+ for incoming in directions(chain) {
184
+ program.end(chain, Loads::new().index(incoming).lookup(INDEX_SET), 0)?;
185
+ program.end(
186
+ chain,
187
+ Loads::new()
188
+ .push(
189
+ |at| super::hostgrant::meta_to(if incoming { 6 } else { 7 }, at),
190
+ NAME,
191
+ )
192
+ .lookup(NAME_SET),
193
+ 0,
194
+ )?;
195
+ }
196
+ Ok(())
197
+ }
@@ -0,0 +1,370 @@
1
+ //! Published ports compile to a constant number of rules around concatenated
2
+ //! interval sets and maps, on both hops, so the rule graph does not grow with the
3
+ //! number of publications; publications cost set elements, which have their own
4
+ //! budget. Every key carries the operands the per-publication rules compared one
5
+ //! by one: the variable interface (index and name), the packet and conntrack
6
+ //! protocols, the live address and port, and the originally tracked address and
7
+ //! port. A port field is an inclusive interval, so one element holds one port or
8
+ //! a whole range, and every other field is exact.
9
+ use super::hostgrant::{meta_to, register};
10
+ use super::keys::{self, word, Key, Loads, ADDRESS, INDEX, NAME, PORT, PROTOCOL};
11
+ use super::*;
12
+
13
+ /// Router handoff arrivals of a publication, classified before conntrack.
14
+ pub(super) const ARRIVAL: &str = "published_arrival";
15
+ /// Router workload packets from a published port, classified before conntrack.
16
+ pub(super) const ENDPOINT: &str = "published_endpoint";
17
+ /// Destination NAT of one-port publications to their inside address and port.
18
+ pub(super) const PORT_MAP: &str = "published_port";
19
+ /// Destination NAT of ranges to their inside address, keeping every port.
20
+ pub(super) const ADDRESS_MAP: &str = "published_address";
21
+ /// Inbound flows of every publication, both directions.
22
+ pub(super) const FORWARD: &str = "published_forward";
23
+ /// Flows symmetric publications open from their inside ports, both directions;
24
+ /// the data is the source NAT to the outside address and port span.
25
+ pub(super) const SYMMETRIC: &str = "published_symmetric";
26
+
27
+ const LINK: [u32; 2] = [INDEX, NAME];
28
+
29
+ /// One publication on one hop. The outside is what clients reach (the uplink
30
+ /// address and port on the host, the transit address and port in the router);
31
+ /// the inside is its translation (the leased transit address on the host, the
32
+ /// workload address in the router), behind `link`, the interface toward it.
33
+ pub(super) struct Publication<'a> {
34
+ pub(super) protocol: &'a str,
35
+ pub(super) outside: (&'a str, u16, u16),
36
+ pub(super) inside: (&'a str, u16, u16),
37
+ pub(super) link: &'a LocalLink,
38
+ pub(super) symmetric: bool,
39
+ }
40
+ /// One hop's publications and its fixed interface: the uplink on the host, the
41
+ /// handoff in the router.
42
+ pub(super) struct Hop<'a> {
43
+ pub(super) publications: Vec<Publication<'a>>,
44
+ pub(super) fixed: &'a LocalLink,
45
+ }
46
+
47
+ /// `[link, protocols, address, port, original address, original port]`, the key
48
+ /// of both admission sets.
49
+ fn flow_fields() -> Vec<u32> {
50
+ let mut fields = LINK.to_vec();
51
+ fields.extend([PROTOCOL, PROTOCOL, ADDRESS, PORT, ADDRESS, PORT]);
52
+ fields
53
+ }
54
+ fn inbound_key(publication: &Publication<'_>) -> Result<IntervalElement> {
55
+ let (outside, outside_first, outside_last) = publication.outside;
56
+ let (inside, inside_first, inside_last) = publication.inside;
57
+ Ok(Key::default()
58
+ .link(publication.link)
59
+ .protocols(publication.protocol)
60
+ .address(inside)?
61
+ .span(inside_first, inside_last)
62
+ .address(outside)?
63
+ .span(outside_first, outside_last)
64
+ .element(None))
65
+ }
66
+ fn symmetric_key(publication: &Publication<'_>) -> Result<IntervalElement> {
67
+ let (outside, outside_first, outside_last) = publication.outside;
68
+ let (inside, inside_first, inside_last) = publication.inside;
69
+ let mut data = super::super::address(outside)?.to_be_bytes().to_vec();
70
+ data.extend(word(&outside_first.to_be_bytes()));
71
+ data.extend(word(&outside_last.to_be_bytes()));
72
+ Ok(Key::default()
73
+ .link(publication.link)
74
+ .protocols(publication.protocol)
75
+ .address(inside)?
76
+ .span(inside_first, inside_last)
77
+ .address(inside)?
78
+ .span(inside_first, inside_last)
79
+ .element(Some(data)))
80
+ }
81
+ /// Inbound flows: the inside link is the outgoing interface of the original
82
+ /// direction and the incoming one of the reply; the live tuple is the translated
83
+ /// inside destination, the original tuple the published outside destination.
84
+ fn inbound_loads(reply: bool) -> Loads {
85
+ let mut loads = Loads::new();
86
+ loads
87
+ .link(reply)
88
+ .protocols()
89
+ .address(reply)
90
+ .port(reply)
91
+ .original(false);
92
+ loads
93
+ }
94
+ /// Symmetric flows: opened from the inside link, address and port, so the live
95
+ /// and the original tuple both carry the inside source.
96
+ fn symmetric_loads(reply: bool) -> Loads {
97
+ let mut loads = Loads::new();
98
+ loads
99
+ .link(!reply)
100
+ .protocols()
101
+ .address(!reply)
102
+ .port(!reply)
103
+ .original(true);
104
+ loads
105
+ }
106
+ fn translation_loads() -> Loads {
107
+ let mut loads = Loads::new();
108
+ loads.protocols().address(false).port(false);
109
+ loads
110
+ }
111
+ fn translation_fields() -> [u32; 4] {
112
+ [PROTOCOL, PROTOCOL, ADDRESS, PORT]
113
+ }
114
+ fn ranged(publication: &Publication<'_>) -> bool {
115
+ publication.outside.1 != publication.outside.2
116
+ }
117
+
118
+ /// Every set of one hop with its elements, before any rule binds it. `router`
119
+ /// adds the two raw classification sets.
120
+ pub(super) fn sets(program: &mut Program<'_>, hop: &Hop<'_>, router: bool) -> Result<()> {
121
+ if hop.publications.is_empty() {
122
+ return Ok(());
123
+ }
124
+ if router {
125
+ let mut arrivals = Vec::new();
126
+ for publication in &hop.publications {
127
+ let (outside, first, last) = publication.outside;
128
+ arrivals.push(
129
+ Key::default()
130
+ .protocol(publication.protocol)
131
+ .address(outside)?
132
+ .span(first, last)
133
+ .element(None),
134
+ );
135
+ }
136
+ declare(
137
+ program,
138
+ ARRIVAL,
139
+ 16,
140
+ &[PROTOCOL, ADDRESS, PORT],
141
+ None,
142
+ arrivals,
143
+ )?;
144
+ // Several publications may share inside ports that are not symmetric;
145
+ // the classification is their union, as disjoint intervals.
146
+ let mut endpoints = Vec::new();
147
+ for publication in &hop.publications {
148
+ let (inside, first, last) = publication.inside;
149
+ endpoints.push(
150
+ Key::default()
151
+ .link(publication.link)
152
+ .protocol(publication.protocol)
153
+ .address(inside)?
154
+ .span(first, last)
155
+ .element(None),
156
+ );
157
+ }
158
+ let mut fields = LINK.to_vec();
159
+ fields.extend([PROTOCOL, ADDRESS, PORT]);
160
+ declare(program, ENDPOINT, 17, &fields, None, endpoints)?;
161
+ }
162
+ let (mut ports, mut addresses) = (Vec::new(), Vec::new());
163
+ for publication in &hop.publications {
164
+ let (outside, first, last) = publication.outside;
165
+ let (inside, inside_first, _) = publication.inside;
166
+ let key = Key::default()
167
+ .protocols(publication.protocol)
168
+ .address(outside)?
169
+ .span(first, last);
170
+ let mut data = super::super::address(inside)?.to_be_bytes().to_vec();
171
+ if ranged(publication) {
172
+ addresses.push(key.element(Some(data)));
173
+ } else {
174
+ data.extend(word(&inside_first.to_be_bytes()));
175
+ ports.push(key.element(Some(data)));
176
+ }
177
+ }
178
+ declare(program, PORT_MAP, 18, &translation_fields(), Some(8), ports)?;
179
+ declare(
180
+ program,
181
+ ADDRESS_MAP,
182
+ 19,
183
+ &translation_fields(),
184
+ Some(4),
185
+ addresses,
186
+ )?;
187
+ let inbound = hop
188
+ .publications
189
+ .iter()
190
+ .map(inbound_key)
191
+ .collect::<Result<Vec<_>>>()?;
192
+ declare(program, FORWARD, 20, &flow_fields(), None, inbound)?;
193
+ let symmetric = hop
194
+ .publications
195
+ .iter()
196
+ .filter(|publication| publication.symmetric)
197
+ .map(symmetric_key)
198
+ .collect::<Result<Vec<_>>>()?;
199
+ declare(program, SYMMETRIC, 21, &flow_fields(), Some(12), symmetric)
200
+ }
201
+ fn declare(
202
+ program: &mut Program<'_>,
203
+ name: &str,
204
+ id: u32,
205
+ fields: &[u32],
206
+ data: Option<u32>,
207
+ elements: Vec<IntervalElement>,
208
+ ) -> Result<()> {
209
+ keys::interval(program, name, id, fields, data, elements).map(|_| ())
210
+ }
211
+
212
+ /// The admissions of one set whatever its number of elements: the ESTABLISHED
213
+ /// original direction, a NEW opening per protocol (TCP only with SYN and
214
+ /// FIN/RST/ACK clear), and the ESTABLISHED reply. Each checks the fixed
215
+ /// interface, the default conntrack zone and the direction before the lookup.
216
+ fn admissions(
217
+ program: &mut Program<'_>,
218
+ hop: &Hop<'_>,
219
+ set: &str,
220
+ protocols: &BTreeSet<&str>,
221
+ fixed_incoming: bool,
222
+ loads: fn(bool) -> Loads,
223
+ ) -> Result<()> {
224
+ if protocols.is_empty() {
225
+ return Ok(());
226
+ }
227
+ let envelope = |reply: bool, connection_state: u32| {
228
+ let mut result = ipv4();
229
+ result.extend(link(hop.fixed, fixed_incoming != reply));
230
+ result.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
231
+ result.extend(ct(1, None, vec![u8::from(reply)]));
232
+ result.extend(state(connection_state));
233
+ result
234
+ };
235
+ let mut established = envelope(false, 2);
236
+ established.extend(loads(false).lookup(set));
237
+ program.end("forward", established, 1)?;
238
+ for transport in protocols {
239
+ let mut opening = envelope(false, 8);
240
+ opening.extend(meta(16, vec![protocol(transport)]));
241
+ if *transport == "tcp" {
242
+ opening.extend(opening_tcp());
243
+ }
244
+ opening.extend(loads(false).lookup(set));
245
+ program.end("forward", opening, 1)?;
246
+ }
247
+ let mut reply = envelope(true, 2);
248
+ reply.extend(loads(true).lookup(set));
249
+ program.end("forward", reply, 1)
250
+ }
251
+ fn protocols<'a>(hop: &Hop<'a>, symmetric: bool) -> BTreeSet<&'a str> {
252
+ hop.publications
253
+ .iter()
254
+ .filter(|publication| publication.symmetric || !symmetric)
255
+ .map(|publication| publication.protocol)
256
+ .collect()
257
+ }
258
+ /// Inbound admissions from the fixed interface to the inside link. They precede
259
+ /// the capture barrier, which denies the protected published address.
260
+ pub(super) fn inbound(program: &mut Program<'_>, hop: &Hop<'_>) -> Result<()> {
261
+ admissions(
262
+ program,
263
+ hop,
264
+ FORWARD,
265
+ &protocols(hop, false),
266
+ true,
267
+ inbound_loads,
268
+ )
269
+ }
270
+ /// Symmetric admissions from the inside link to the fixed interface. They follow
271
+ /// the capture barrier, so every protected current or original destination stays
272
+ /// denied.
273
+ pub(super) fn symmetric(program: &mut Program<'_>, hop: &Hop<'_>) -> Result<()> {
274
+ admissions(
275
+ program,
276
+ hop,
277
+ SYMMETRIC,
278
+ &protocols(hop, true),
279
+ false,
280
+ symmetric_loads,
281
+ )
282
+ }
283
+ /// NAT expressions reading the map data the lookup stored: the address from the
284
+ /// first register, and the port bounds from the next when `ports` is 1 or 2.
285
+ pub(super) fn nat(kind: u32, ports: u32) -> Attr {
286
+ let address = register(4);
287
+ let mut data = vec![
288
+ Attr::u32(1, kind),
289
+ Attr::u32(2, 2),
290
+ Attr::u32(3, address),
291
+ Attr::u32(4, address),
292
+ ];
293
+ if ports > 0 {
294
+ data.extend([Attr::u32(5, register(5)), Attr::u32(6, register(4 + ports))]);
295
+ }
296
+ data.push(Attr::u32(7, if ports > 0 { 3 } else { 1 }));
297
+ expr("nat", data)
298
+ }
299
+ fn tracked(fixed: &LocalLink, incoming: bool) -> Vec<Attr> {
300
+ let mut result = ipv4();
301
+ result.extend(link(fixed, incoming));
302
+ result.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
303
+ result.extend(ct(1, None, vec![0]));
304
+ result
305
+ }
306
+ /// Destination NAT from the fixed interface: one-port publications to their
307
+ /// inside address and port, ranges to their inside address alone, which never
308
+ /// changes a port.
309
+ pub(super) fn translations(program: &mut Program<'_>, hop: &Hop<'_>) -> Result<()> {
310
+ for (set, ports, present) in [
311
+ (
312
+ PORT_MAP,
313
+ 1,
314
+ hop.publications.iter().any(|item| !ranged(item)),
315
+ ),
316
+ (ADDRESS_MAP, 0, hop.publications.iter().any(ranged)),
317
+ ] {
318
+ if present {
319
+ let mut expressions = tracked(hop.fixed, true);
320
+ expressions.extend(translation_loads().map(set));
321
+ expressions.push(nat(1, ports));
322
+ program.rule("pre", expressions)?;
323
+ }
324
+ }
325
+ Ok(())
326
+ }
327
+ /// Source NAT of symmetric flows leaving through the fixed interface, to the
328
+ /// outside address and port span of their publication.
329
+ pub(super) fn symmetric_translation(program: &mut Program<'_>, hop: &Hop<'_>) -> Result<()> {
330
+ if hop.publications.iter().any(|item| item.symmetric) {
331
+ let mut expressions = tracked(hop.fixed, false);
332
+ expressions.extend(symmetric_loads(false).map(SYMMETRIC));
333
+ expressions.push(nat(0, 2));
334
+ program.rule("post", expressions)?;
335
+ }
336
+ Ok(())
337
+ }
338
+ /// Router handoff ingress classification of every publication's arrivals. Raw
339
+ /// runs before conntrack, so it matches the arriving tuple only and never sets a
340
+ /// generation zone.
341
+ pub(super) fn arrival(program: &mut Program<'_>, hop: &Hop<'_>) -> Result<()> {
342
+ if !hop.publications.is_empty() {
343
+ let mut expressions = ipv4();
344
+ expressions.extend(link(hop.fixed, true));
345
+ let mut loads = Loads::new();
346
+ loads
347
+ .push(|index| meta_to(16, index), PROTOCOL)
348
+ .address(false)
349
+ .port(false);
350
+ expressions.extend(loads.lookup(ARRIVAL));
351
+ program.end("raw_return", expressions, 1)?;
352
+ }
353
+ Ok(())
354
+ }
355
+ /// Router workload ingress classification of packets from a published port on
356
+ /// its exact link and address, ahead of every leased classifier.
357
+ pub(super) fn endpoint(program: &mut Program<'_>, hop: &Hop<'_>) -> Result<()> {
358
+ if !hop.publications.is_empty() {
359
+ let mut expressions = ipv4();
360
+ let mut loads = Loads::new();
361
+ loads
362
+ .link(true)
363
+ .push(|index| meta_to(16, index), PROTOCOL)
364
+ .address(true)
365
+ .port(true);
366
+ expressions.extend(loads.lookup(ENDPOINT));
367
+ program.end("raw_in", expressions, 1)?;
368
+ }
369
+ Ok(())
370
+ }