@push.rocks/smartnftables 4.3.1 → 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.
@@ -0,0 +1,418 @@
1
+ //! Publications through the allocation-pool guard: a publication translates an
2
+ //! inbound flow from the uplink into a pool (the router's transit address), which
3
+ //! the guard denies by its current destination and, on the reply, by its current
4
+ //! source. A guard publication admits exactly that forwarded flow and its replies;
5
+ //! every other tuple meets the decision of the guard without it.
6
+ use super::hostgrant_tests::{
7
+ bytes, chains, decide, granted, host_grant, normalized, prepared, Flow, ACK, ESTABLISHED,
8
+ };
9
+ use super::tests::pool_guard;
10
+ use super::Prepared;
11
+ use serde_json::{json, Value};
12
+
13
+ const NEW: u32 = 8;
14
+ const SYN: u8 = 0x02;
15
+ const CLIENT: [u8; 4] = [198, 51, 100, 7];
16
+ const UPLINK: [u8; 4] = [192, 0, 2, 2];
17
+ const TRANSIT: [u8; 4] = [10, 240, 0, 1];
18
+ const ENS18: Option<(u32, &str)> = Some((2, "ens18"));
19
+ const HANDOFF: Option<(u32, &str)> = Some((4, "handoff"));
20
+
21
+ fn port(protocol: &str, first: u16, end: Option<u16>, target: u16, address: &str) -> Value {
22
+ let mut value =
23
+ json!({"protocol":protocol,"hostPort":first,"targetPort":target,"targetAddress":address});
24
+ if let Some(end) = end {
25
+ value["hostPortEnd"] = json!(end);
26
+ }
27
+ value
28
+ }
29
+ fn published(mut value: Value, ports: Value) -> Value {
30
+ value["scope"]["publishedPorts"] = ports;
31
+ value
32
+ }
33
+ /// One port translated to another, and a range that keeps every port.
34
+ fn fixture() -> Value {
35
+ published(
36
+ pool_guard(),
37
+ json!([
38
+ port("tcp", 443, None, 8443, "10.240.0.1"),
39
+ port("udp", 20000, Some(20200), 20000, "10.240.0.1")
40
+ ]),
41
+ )
42
+ }
43
+ /// The opening of an inbound publication as the forward hook sees it: after the
44
+ /// destination NAT to the transit address, the original destination still the uplink.
45
+ fn inbound(protocol: u8, outside: u16, inside: u16) -> Flow {
46
+ Flow {
47
+ input: ENS18,
48
+ output: HANDOFF,
49
+ protocol,
50
+ source: CLIENT,
51
+ destination: TRANSIT,
52
+ source_port: 40000,
53
+ destination_port: inside,
54
+ tcp_flags: if protocol == 6 { SYN } else { 0 },
55
+ state: NEW,
56
+ reply: false,
57
+ zone: 0,
58
+ original: (CLIENT, UPLINK, 40000, outside),
59
+ uid: None,
60
+ label: [0; 16],
61
+ }
62
+ }
63
+ /// The workload's reply as the forward hook sees it, before the reverse NAT.
64
+ fn reply(flow: Flow) -> Flow {
65
+ Flow {
66
+ input: flow.output,
67
+ output: flow.input,
68
+ source: flow.destination,
69
+ destination: flow.source,
70
+ source_port: flow.destination_port,
71
+ destination_port: flow.source_port,
72
+ tcp_flags: if flow.protocol == 6 { ACK } else { 0 },
73
+ state: ESTABLISHED,
74
+ reply: true,
75
+ ..flow
76
+ }
77
+ }
78
+ /// Every admitted variant: the opening, the established original direction and
79
+ /// the reply, for the one-port and every boundary of the range.
80
+ fn admitted() -> Vec<Flow> {
81
+ let mut flows = Vec::new();
82
+ for opening in [
83
+ inbound(6, 443, 8443),
84
+ inbound(17, 20000, 20000),
85
+ inbound(17, 20100, 20100),
86
+ inbound(17, 20200, 20200),
87
+ ] {
88
+ flows.push(opening);
89
+ flows.push(Flow {
90
+ state: ESTABLISHED,
91
+ tcp_flags: if opening.protocol == 6 { ACK } else { 0 },
92
+ ..opening
93
+ });
94
+ flows.push(reply(opening));
95
+ }
96
+ flows
97
+ }
98
+ /// Tuples the publications do not name, each of which the guard alone denies.
99
+ fn foreign() -> Vec<Flow> {
100
+ let tcp = inbound(6, 443, 8443);
101
+ let udp = inbound(17, 20000, 20000);
102
+ vec![
103
+ // An original destination in a pool: no translation from outside every pool.
104
+ Flow {
105
+ original: (CLIENT, [10, 240, 0, 9], 40000, 443),
106
+ ..tcp
107
+ },
108
+ Flow {
109
+ original: (CLIENT, TRANSIT, 40000, 8443),
110
+ ..tcp
111
+ },
112
+ reply(Flow {
113
+ original: (CLIENT, [10, 241, 0, 9], 40000, 20000),
114
+ ..udp
115
+ }),
116
+ // Another target address, target port, published port or protocol.
117
+ Flow {
118
+ destination: [10, 240, 0, 3],
119
+ ..tcp
120
+ },
121
+ Flow {
122
+ destination_port: 8444,
123
+ ..tcp
124
+ },
125
+ Flow {
126
+ original: (CLIENT, UPLINK, 40000, 444),
127
+ ..tcp
128
+ },
129
+ Flow {
130
+ original: (CLIENT, UPLINK, 40000, 8443),
131
+ ..tcp
132
+ },
133
+ Flow {
134
+ protocol: 17,
135
+ tcp_flags: 0,
136
+ ..tcp
137
+ },
138
+ Flow {
139
+ protocol: 6,
140
+ tcp_flags: SYN,
141
+ ..udp
142
+ },
143
+ inbound(17, 19999, 19999),
144
+ inbound(17, 20201, 20201),
145
+ reply(inbound(17, 20201, 20201)),
146
+ // Another conntrack zone, a non-SYN opening, a reply that is not ESTABLISHED
147
+ // and the reply direction carrying the original direction's tuple.
148
+ Flow { zone: 7, ..tcp },
149
+ reply(Flow { zone: 7, ..udp }),
150
+ Flow {
151
+ tcp_flags: ACK,
152
+ ..tcp
153
+ },
154
+ Flow {
155
+ state: NEW,
156
+ ..reply(tcp)
157
+ },
158
+ ]
159
+ }
160
+
161
+ #[test]
162
+ fn pool_guard_publications_admit_exactly_the_forwarded_flow_and_its_reply() {
163
+ let baseline = prepared(pool_guard());
164
+ let with = prepared(fixture());
165
+ for flow in admitted() {
166
+ assert_eq!(
167
+ decide(&baseline, "forward", &flow).verdict,
168
+ 0,
169
+ "{:?} baseline",
170
+ flow.destination_port
171
+ );
172
+ let decision = decide(&with, "forward", &flow);
173
+ assert_eq!(
174
+ (decision.verdict, decision.chain.as_str()),
175
+ (1, "pool_guard"),
176
+ "{:?}",
177
+ flow.destination_port
178
+ );
179
+ }
180
+ for (index, flow) in foreign().iter().enumerate() {
181
+ assert_eq!(
182
+ decide(&baseline, "forward", flow).verdict,
183
+ 0,
184
+ "variant {index} baseline"
185
+ );
186
+ assert_eq!(
187
+ decide(&with, "forward", flow),
188
+ decide(&baseline, "forward", flow),
189
+ "variant {index}"
190
+ );
191
+ }
192
+ // The reply direction carrying the original direction's tuple is not from a
193
+ // pool, so the guard passes it with or without publications.
194
+ let turned = Flow {
195
+ reply: true,
196
+ state: ESTABLISHED,
197
+ ..inbound(6, 443, 8443)
198
+ };
199
+ assert_eq!(
200
+ decide(&with, "forward", &turned),
201
+ decide(&baseline, "forward", &turned)
202
+ );
203
+ }
204
+
205
+ #[test]
206
+ fn pool_guard_publications_add_only_their_admissions_to_the_guard_chain() {
207
+ let baseline = prepared(pool_guard());
208
+ let with = prepared(fixture());
209
+ let graph = chains(&with);
210
+ let reference = chains(&baseline);
211
+ assert_eq!(
212
+ graph.keys().collect::<Vec<_>>(),
213
+ reference.keys().collect::<Vec<_>>()
214
+ );
215
+ for name in ["input", "forward", "output", "pool_reply"] {
216
+ assert_eq!(graph[name], reference[name], "{name}");
217
+ }
218
+ // Whatever the number of publications: the ESTABLISHED admission and one
219
+ // opening per protocol, right after the original-destination denial of every pool.
220
+ let pools = 2;
221
+ assert_eq!(
222
+ graph["pool_guard"].len(),
223
+ reference["pool_guard"].len() + 1 + 2
224
+ );
225
+ assert_eq!(
226
+ graph["pool_guard"][..pools],
227
+ reference["pool_guard"][..pools]
228
+ );
229
+ assert_eq!(
230
+ graph["pool_guard"][pools + 3..],
231
+ reference["pool_guard"][pools..]
232
+ );
233
+ // A host-local flow with a publication's tuples, translated from outside every
234
+ // pool, is the same publication on the local hooks.
235
+ for flow in admitted() {
236
+ for (chain, local) in [
237
+ (
238
+ "input",
239
+ Flow {
240
+ output: None,
241
+ ..flow
242
+ },
243
+ ),
244
+ (
245
+ "output",
246
+ Flow {
247
+ input: None,
248
+ ..flow
249
+ },
250
+ ),
251
+ ] {
252
+ assert_eq!(
253
+ decide(&baseline, chain, &local).verdict,
254
+ 0,
255
+ "{chain} baseline"
256
+ );
257
+ assert_eq!(decide(&with, chain, &local).verdict, 1, "{chain}");
258
+ }
259
+ }
260
+ }
261
+
262
+ #[test]
263
+ fn pool_guard_publications_absent_and_empty_keep_the_digest_and_compiled_bytes() {
264
+ let baseline = prepared(pool_guard());
265
+ assert!(!serde_json::to_string(&baseline.policy)
266
+ .unwrap()
267
+ .contains("publishedPorts"));
268
+ let empty = prepared(published(pool_guard(), json!([])));
269
+ assert_eq!(empty, baseline);
270
+ assert_eq!(bytes(&empty), bytes(&baseline));
271
+ let with = prepared(fixture());
272
+ assert_ne!(with.digest, baseline.digest);
273
+ // Canonical order: a reordered list is the same policy.
274
+ let reordered = prepared(published(
275
+ pool_guard(),
276
+ json!([
277
+ port("udp", 20000, Some(20200), 20000, "10.240.0.1"),
278
+ port("tcp", 443, None, 8443, "10.240.0.1")
279
+ ]),
280
+ ));
281
+ assert_eq!(reordered, with);
282
+ // Host grants and publications are independent exceptions of one guard.
283
+ let both = prepared(granted(
284
+ fixture(),
285
+ json!([host_grant("tcp", "10.240.0.1", "10.241.0.2", 8080)]),
286
+ ));
287
+ for flow in admitted() {
288
+ assert_eq!(decide(&both, "forward", &flow).verdict, 1);
289
+ }
290
+ }
291
+
292
+ #[test]
293
+ fn pool_guard_publications_refuse_every_entry_outside_the_contract() {
294
+ let refused = |ports: Value| normalized(published(pool_guard(), ports)).is_err();
295
+ // A target outside every pool needs no exception and is no publication into one.
296
+ assert!(refused(json!([port("tcp", 443, None, 8443, "10.250.0.1")])));
297
+ assert!(refused(json!([port(
298
+ "tcp",
299
+ 443,
300
+ None,
301
+ 8443,
302
+ "10.240.0.0/16"
303
+ )])));
304
+ assert!(refused(json!([port("tcp", 443, None, 8443, "127.0.0.1")])));
305
+ assert!(refused(json!([port(
306
+ "sctp",
307
+ 443,
308
+ None,
309
+ 8443,
310
+ "10.240.0.1"
311
+ )])));
312
+ assert!(refused(json!([port("tcp", 0, None, 8443, "10.240.0.1")])));
313
+ assert!(refused(json!([port("tcp", 443, None, 0, "10.240.0.1")])));
314
+ // A range keeps every port and runs upward; one port has no end.
315
+ assert!(refused(json!([port(
316
+ "udp",
317
+ 20000,
318
+ Some(20200),
319
+ 30000,
320
+ "10.240.0.1"
321
+ )])));
322
+ assert!(refused(json!([port(
323
+ "udp",
324
+ 20000,
325
+ Some(20000),
326
+ 20000,
327
+ "10.240.0.1"
328
+ )])));
329
+ assert!(refused(json!([port(
330
+ "udp",
331
+ 20000,
332
+ Some(19999),
333
+ 20000,
334
+ "10.240.0.1"
335
+ )])));
336
+ // One protocol and port is published once.
337
+ assert!(refused(json!([
338
+ port("tcp", 443, None, 8443, "10.240.0.1"),
339
+ port("tcp", 443, None, 8444, "10.240.0.1")
340
+ ])));
341
+ assert!(refused(json!([
342
+ port("udp", 20000, Some(20200), 20000, "10.240.0.1"),
343
+ port("udp", 20200, None, 5060, "10.240.0.1")
344
+ ])));
345
+ assert!(!refused(json!([
346
+ port("tcp", 443, None, 8443, "10.240.0.1"),
347
+ port("udp", 443, None, 8443, "10.240.0.1")
348
+ ])));
349
+ let mut unknown = port("tcp", 443, None, 8443, "10.240.0.1");
350
+ unknown["hostIp"] = json!("192.0.2.2");
351
+ assert!(refused(json!([unknown])));
352
+ }
353
+
354
+ #[test]
355
+ fn pool_guard_publications_are_bounded_and_fit_the_largest_guard() {
356
+ let many = |count: u16| -> Value {
357
+ json!((0..count)
358
+ .map(|index| port("tcp", 1000 + index, None, 1000 + index, "10.240.0.1"))
359
+ .collect::<Vec<_>>())
360
+ };
361
+ match normalized(published(pool_guard(), many(1025))) {
362
+ Err(crate::Error::Exhausted(bound)) => assert_eq!(bound.name, "publishedPorts"),
363
+ other => panic!("{other:?}"),
364
+ }
365
+ // The largest guard: 64 pools, 1024 host grants, 1024 publications and every
366
+ // loopback port owner compile inside the atomic budget.
367
+ let mut guard = pool_guard();
368
+ guard["scope"]["prefixes"] = json!((0..64)
369
+ .map(|part| format!("10.{part}.0.0/16"))
370
+ .collect::<Vec<_>>());
371
+ let grants: Vec<_> = (0..1024_u16)
372
+ .map(|index| {
373
+ host_grant(
374
+ if index % 2 == 0 { "tcp" } else { "udp" },
375
+ "10.40.0.1",
376
+ "10.41.0.2",
377
+ 20000 + index / 2,
378
+ )
379
+ })
380
+ .collect();
381
+ guard["scope"]["hostGrants"] = json!(grants);
382
+ guard["scope"]["localTcpPortOwners"] = json!((0..8)
383
+ .map(|index| json!({"address":"127.0.0.1","port":10010 + index,"uid":0}))
384
+ .collect::<Vec<_>>());
385
+ let ports: Vec<_> = (0..1024_u16)
386
+ .map(|index| {
387
+ port(
388
+ if index % 2 == 0 { "tcp" } else { "udp" },
389
+ 1000 + index / 2,
390
+ None,
391
+ 1000 + index / 2,
392
+ "10.40.0.1",
393
+ )
394
+ })
395
+ .collect();
396
+ let largest: Prepared = prepared(published(guard, json!(ports)));
397
+ let program = largest
398
+ .program(&format!("snft_{}", "x".repeat(59)))
399
+ .unwrap();
400
+ let size = |kinds: &[u16]| -> usize {
401
+ program
402
+ .iter()
403
+ .filter(|(kind, _)| kinds.contains(kind))
404
+ .map(|(_, attributes)| crate::wire::encode_attrs(attributes).len() + 20)
405
+ .sum()
406
+ };
407
+ let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
408
+ assert!(
409
+ program.len() < 768
410
+ && graph <= super::compile::RULE_BYTES
411
+ && graph + elements <= super::compile::TARGET_BYTES
412
+ );
413
+ assert!(768 * (20 + 72 + 36 + 12) + graph + elements + 64 <= crate::wire::BATCH_BYTES);
414
+ println!(
415
+ "POOL_GUARD_PUBLISHED_MAX prefixes=64 grants=1024 owners=8 published=1024 operations={} rule_bytes={graph} element_bytes={elements}",
416
+ program.len()
417
+ );
418
+ }
@@ -507,8 +507,27 @@ fn prepared(value: Value) -> Prepared {
507
507
  normalized(value).unwrap().prepare().unwrap()
508
508
  }
509
509
  fn program_bytes(prepared: &Prepared, table: &str) -> Vec<u8> {
510
+ encoded(prepared.program(table).unwrap())
511
+ }
512
+ /// LEGACY(<=4.4.x): the program as releases before typed sets compiled it,
513
+ /// whose goldens it must still reproduce byte for byte, so their kernel graphs
514
+ /// stay adoptable.
515
+ fn untyped_program_bytes(prepared: &Prepared, table: &str) -> Vec<u8> {
516
+ encoded(
517
+ prepared
518
+ .program(table)
519
+ .unwrap()
520
+ .into_iter()
521
+ .map(|(kind, attributes)| match kind {
522
+ 9 => (kind, crate::owner::untyped_set(&attributes)),
523
+ _ => (kind, attributes),
524
+ })
525
+ .collect(),
526
+ )
527
+ }
528
+ fn encoded(program: Vec<(u16, Vec<crate::wire::Attr>)>) -> Vec<u8> {
510
529
  let mut bytes = Vec::new();
511
- for (kind, attributes) in prepared.program(table).unwrap() {
530
+ for (kind, attributes) in program {
512
531
  let encoded = crate::wire::encode_attrs(&attributes);
513
532
  bytes.extend(kind.to_be_bytes());
514
533
  bytes.extend((encoded.len() as u32).to_be_bytes());
@@ -606,12 +625,20 @@ fn host_v2_published_ports_compile_uplink_dnat_and_exact_forward_admission() {
606
625
  for (_, expressions) in &forward[..4] {
607
626
  assert_eq!(expressions[expressions.len() - 2], "lookup");
608
627
  }
609
- let bytes = program_bytes(&published, "snft_v2_host_golden");
628
+ // The graph of 4.4.x, byte for byte, once its sets are untyped again.
629
+ let bytes = untyped_program_bytes(&published, "snft_v2_host_golden");
610
630
  assert_eq!(bytes.len(), 30988);
611
631
  assert_eq!(
612
632
  format!("{:x}", Sha256::digest(&bytes)),
613
633
  "b36f359c6439e9fcd77c291c7dac7854014225d3de1f8a4e49df6fec6ae570c4"
614
634
  );
635
+ // Re-frozen with typed sets in 4.5.0.
636
+ let bytes = program_bytes(&published, "snft_v2_host_golden");
637
+ assert_eq!(bytes.len(), 31168);
638
+ assert_eq!(
639
+ format!("{:x}", Sha256::digest(&bytes)),
640
+ "609f2388e3c5cf45b921182c17252c072c4db2f07e3cc605e282faddd937d458"
641
+ );
615
642
  }
616
643
 
617
644
  #[test]
@@ -724,12 +751,20 @@ fn router_v2_without_published_ports_keeps_its_exact_digest_and_compiled_bytes()
724
751
  );
725
752
  // Re-frozen with the set-backed router graph of 3.0.0: the digest is the
726
753
  // policy's and did not change, the compiled graph did.
727
- let bytes = program_bytes(&baseline, "snft_v2_router_golden");
754
+ // The graph of 4.4.x, byte for byte, once its sets are untyped again.
755
+ let bytes = untyped_program_bytes(&baseline, "snft_v2_router_golden");
728
756
  assert_eq!(bytes.len(), 47674);
729
757
  assert_eq!(
730
758
  format!("{:x}", Sha256::digest(&bytes)),
731
759
  "ea609865f32031f6835ef4fc7f67f5754a15f466580308bdccc9db87d4ccd2c5"
732
760
  );
761
+ // Re-frozen with typed sets in 4.5.0.
762
+ let bytes = program_bytes(&baseline, "snft_v2_router_golden");
763
+ assert_eq!(bytes.len(), 48362);
764
+ assert_eq!(
765
+ format!("{:x}", Sha256::digest(&bytes)),
766
+ "47280ecb596125d6421b1ab42e2d768e53844f55d7d0f130fe31a8cd5b9a5668"
767
+ );
733
768
  let serialized = serde_json::to_string(&baseline.policy).unwrap();
734
769
  assert!(!serialized.contains("publishedPorts"));
735
770
  // An explicit empty publication is the same canonical policy, digest and graph.
@@ -796,12 +831,20 @@ fn router_v2_published_ports_compile_handoff_dnat_classification_and_admission()
796
831
  assert!(incoming[4..]
797
832
  .iter()
798
833
  .any(|expressions| expressions.contains(&"ct".to_string())));
799
- let bytes = program_bytes(&published, "snft_v2_router_golden");
834
+ // The graph of 4.4.x, byte for byte, once its sets are untyped again.
835
+ let bytes = untyped_program_bytes(&published, "snft_v2_router_golden");
800
836
  assert_eq!(bytes.len(), 55494);
801
837
  assert_eq!(
802
838
  format!("{:x}", Sha256::digest(&bytes)),
803
839
  "60a7ba1c15d49e5ff54cfbf9fbe2d72f03973acd1a897eef2d975547ef4abf5b"
804
840
  );
841
+ // Re-frozen with typed sets in 4.5.0.
842
+ let bytes = program_bytes(&published, "snft_v2_router_golden");
843
+ assert_eq!(bytes.len(), 56454);
844
+ assert_eq!(
845
+ format!("{:x}", Sha256::digest(&bytes)),
846
+ "1d2a72707bc10ed689e3f9b4c1cb3e798e89c01fc97115078dcd32e891b22306"
847
+ );
805
848
  }
806
849
 
807
850
  #[test]