@push.rocks/smartnftables 2.5.1 → 2.6.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 (65) 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 +44 -0
  8. package/native-notices/crates/memchr-2.8.3/unlicense +24 -0
  9. package/native-notices/manifest.json +566 -344
  10. package/native-notices/readme.md +40 -34
  11. package/native-notices/runtime/gcc/gpl-3.0.txt +674 -0
  12. package/native-notices/runtime/gcc/runtime-library-exception-3.1.txt +73 -0
  13. package/native-notices/{rust-standard-library/dependency-notices.html → rust/copyright-library.html} +185 -991
  14. package/native-notices/rust/crates/adler2-2.0.1/license-0bsd +12 -0
  15. package/native-notices/rust/crates/adler2-2.0.1/license-apache +201 -0
  16. package/native-notices/rust/crates/adler2-2.0.1/license-mit +23 -0
  17. package/native-notices/rust/crates/memchr-2.7.6/copying +3 -0
  18. package/native-notices/rust/crates/memchr-2.7.6/license-mit +21 -0
  19. package/native-notices/rust/crates/memchr-2.7.6/unlicense +24 -0
  20. package/native-notices/rust/crates/miniz_oxide-0.8.9/license +25 -0
  21. package/native-notices/{rust-standard-library/llvm-libunwind-license.txt → rust/crates/miniz_oxide-0.8.9/license-apache.md} +11 -145
  22. package/native-notices/rust/crates/miniz_oxide-0.8.9/license-mit.md +24 -0
  23. package/native-notices/rust/crates/miniz_oxide-0.8.9/license-zlib.md +14 -0
  24. package/native-notices/{rust-standard-library → rust/crates/object-0.37.3}/license-apache +25 -0
  25. package/native-notices/{rust-standard-library → rust/crates/object-0.37.3}/license-mit +1 -1
  26. package/native-notices/rust/licenses/apache-2.0.txt +73 -0
  27. package/native-notices/rust/licenses/bsd-2-clause.txt +9 -0
  28. package/native-notices/rust/licenses/cc-by-sa-4.0.txt +427 -0
  29. package/native-notices/rust/licenses/gcc-exception-3.1.txt +30 -0
  30. package/native-notices/rust/licenses/gpl-2.0-only.txt +133 -0
  31. package/native-notices/rust/licenses/gpl-3.0-or-later.txt +202 -0
  32. package/native-notices/rust/licenses/isc.txt +7 -0
  33. package/native-notices/rust/licenses/llvm-exception.txt +15 -0
  34. package/native-notices/rust/licenses/mit.txt +9 -0
  35. package/native-notices/rust/licenses/ncsa.txt +29 -0
  36. package/native-notices/rust/licenses/ofl-1.1.txt +43 -0
  37. package/native-notices/{rust-standard-library/unicode-license.txt → rust/licenses/unicode-3.0.txt} +1 -1
  38. package/package.json +4 -5
  39. package/readme.md +149 -5
  40. package/rust/src/egress.compile.rs +2 -0
  41. package/rust/src/egress.host.rs +88 -1
  42. package/rust/src/egress.hostgrant.rs +13 -13
  43. package/rust/src/egress.poolguard.rs +45 -0
  44. package/rust/src/egress.router.rs +3 -1
  45. package/rust/src/egress.rs +122 -6
  46. package/rust/src/egress.workloadgrant.rs +88 -0
  47. package/rust/src/egress_hostgrant_tests.rs +47 -29
  48. package/rust/src/egress_localplatform_tests.rs +204 -0
  49. package/rust/src/egress_localport_tests.rs +264 -0
  50. package/rust/src/egress_workloadgrant_tests.rs +314 -0
  51. package/rust/src/owner.rs +8 -1
  52. package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
  53. package/rust/src/owner_identity_tests.rs +62 -0
  54. package/rust/src/owner_localplatform_tests.rs +274 -0
  55. package/rust/src/owner_localport_tests.rs +241 -0
  56. package/rust/src/owner_tests.rs +9 -0
  57. package/rust/src/owner_workloadgrant_tests.rs +278 -0
  58. package/ts/00_commitinfo_data.ts +1 -1
  59. package/ts/managed.egress.types.ts +46 -0
  60. package/native-notices/rust-standard-library/cargo.lock +0 -522
  61. package/scripts/check-native-notices.mjs +0 -27
  62. /package/native-notices/{rust-standard-library/musl-1.2.5-copyright.txt → runtime/musl-1.2.5/copyright} +0 -0
  63. /package/native-notices/{rust-standard-library → rust}/compiler-builtins-license.txt +0 -0
  64. /package/native-notices/{rust-standard-library/addr2line-0.25.1-license-apache → rust/crates/addr2line-0.25.1/license-apache} +0 -0
  65. /package/native-notices/{rust-standard-library/addr2line-0.25.1-license-mit → rust/crates/addr2line-0.25.1/license-mit} +0 -0
@@ -0,0 +1,88 @@
1
+ //! Workload grants: stateful one-way flows from one workload endpoint to one
2
+ //! exact port of another, both behind the same router. Like host grants they
3
+ //! compile to a constant number of rules around exact set lookups. The tuple key
4
+ //! carries both protocols and the live and originally tracked tuple, so a
5
+ //! translated flow never matches. A second set binds every grant address to the
6
+ //! exact link that holds it (index and name), and both the incoming and the
7
+ //! outgoing link are looked up on every packet. Only the grant's source opens;
8
+ //! the destination only answers.
9
+ use super::hostgrant::{lookup, meta_to, payload_to, tuple_key, tuple_lookup};
10
+ use super::*;
11
+ use std::collections::BTreeSet;
12
+
13
+ /// Every exact workload-to-workload flow of the scope.
14
+ pub(super) const TUPLE_SET: &str = "workload_grant";
15
+ /// Every grant address with the exact link that holds it.
16
+ pub(super) const LINK_SET: &str = "workload_link";
17
+
18
+ /// The link a packet crosses and the address on its side of that link: the
19
+ /// incoming link and the source, or the outgoing link and the destination.
20
+ fn link_lookup(incoming: bool) -> Vec<Attr> {
21
+ vec![
22
+ meta_to(if incoming { 4 } else { 5 }, 4),
23
+ meta_to(if incoming { 6 } else { 7 }, 5),
24
+ payload_to(1, if incoming { 12 } else { 16 }, 4, 9),
25
+ lookup(LINK_SET),
26
+ ]
27
+ }
28
+ fn link_key(link: &LocalLink, address: &str) -> Result<Vec<u8>> {
29
+ let mut key = link.interface_index.to_ne_bytes().to_vec();
30
+ let mut name = link.interface_name.as_bytes().to_vec();
31
+ name.resize(16, 0);
32
+ key.extend(name);
33
+ key.extend(super::address(address)?.to_be_bytes());
34
+ Ok(key)
35
+ }
36
+
37
+ pub(super) fn sets(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
38
+ if scope.workload_grants.is_empty() {
39
+ return Ok(());
40
+ }
41
+ let mut keys = Vec::new();
42
+ let mut links = BTreeSet::new();
43
+ for grant in &scope.workload_grants {
44
+ keys.push(tuple_key(grant, None)?);
45
+ for address in [&grant.source_address, &grant.destination_address] {
46
+ links.insert(link_key(
47
+ super::router::endpoint_link(scope, address)?,
48
+ address,
49
+ )?);
50
+ }
51
+ }
52
+ program.set(TUPLE_SET, 3, 32)?;
53
+ program.elements(TUPLE_SET, keys)?;
54
+ program.set(LINK_SET, 4, 24)?;
55
+ program.elements(LINK_SET, links.into_iter().collect())
56
+ }
57
+
58
+ /// Four forward admissions whatever the number of grants: the ESTABLISHED
59
+ /// original direction, a NEW UDP opening, a NEW TCP opening restricted to SYN
60
+ /// with FIN/RST/ACK clear, and the ESTABLISHED reply, each in the default
61
+ /// conntrack zone. A reply is only ever ESTABLISHED, so the destination
62
+ /// workload can never open a flow toward the source through a grant.
63
+ pub(super) fn admissions(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
64
+ if scope.workload_grants.is_empty() {
65
+ return Ok(());
66
+ }
67
+ let admission = |reply: bool, connection_state: u32, transport: Option<&str>| {
68
+ let mut result = ipv4();
69
+ result.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
70
+ result.extend(ct(1, None, vec![u8::from(reply)]));
71
+ result.extend(state(connection_state));
72
+ if let Some(transport) = transport {
73
+ result.extend(meta(16, vec![protocol(transport)]));
74
+ if transport == "tcp" {
75
+ result.extend(opening_tcp());
76
+ }
77
+ }
78
+ result.extend(link_lookup(true));
79
+ result.extend(link_lookup(false));
80
+ result.extend(tuple_lookup(TUPLE_SET, false, reply));
81
+ result
82
+ };
83
+ program.end("forward", admission(false, 2, None), 1)?;
84
+ for transport in ["udp", "tcp"] {
85
+ program.end("forward", admission(false, 8, Some(transport)), 1)?;
86
+ }
87
+ program.end("forward", admission(true, 2, None), 1)
88
+ }
@@ -15,15 +15,15 @@ pub(crate) fn granted(mut value: Value, grants: Value) -> Value {
15
15
  value["scope"]["hostGrants"] = grants;
16
16
  value
17
17
  }
18
- fn normalized(value: Value) -> crate::Result<Policy> {
18
+ pub(super) fn normalized(value: Value) -> crate::Result<Policy> {
19
19
  serde_json::from_value::<Policy>(value)
20
20
  .map_err(|_| crate::Error::Invalid)?
21
21
  .normalize()
22
22
  }
23
- fn prepared(value: Value) -> Prepared {
23
+ pub(super) fn prepared(value: Value) -> Prepared {
24
24
  normalized(value).unwrap().prepare().unwrap()
25
25
  }
26
- fn bytes(prepared: &Prepared) -> Vec<u8> {
26
+ pub(super) fn bytes(prepared: &Prepared) -> Vec<u8> {
27
27
  let mut result = Vec::new();
28
28
  for (kind, attributes) in prepared.program("snft_host_grant").unwrap() {
29
29
  result.extend(kind.to_be_bytes());
@@ -38,28 +38,36 @@ fn exact() -> Value {
38
38
 
39
39
  /// One packet with the conntrack facts a filter hook observes.
40
40
  #[derive(Clone, Copy)]
41
- struct Flow {
42
- input: Option<(u32, &'static str)>,
43
- output: Option<(u32, &'static str)>,
44
- protocol: u8,
45
- source: [u8; 4],
46
- destination: [u8; 4],
47
- source_port: u16,
48
- destination_port: u16,
49
- tcp_flags: u8,
50
- state: u32,
51
- reply: bool,
52
- zone: u16,
41
+ pub(super) struct Flow {
42
+ pub(super) input: Option<(u32, &'static str)>,
43
+ pub(super) output: Option<(u32, &'static str)>,
44
+ pub(super) protocol: u8,
45
+ pub(super) source: [u8; 4],
46
+ pub(super) destination: [u8; 4],
47
+ pub(super) source_port: u16,
48
+ pub(super) destination_port: u16,
49
+ pub(super) tcp_flags: u8,
50
+ pub(super) state: u32,
51
+ pub(super) reply: bool,
52
+ pub(super) zone: u16,
53
53
  /// Original-direction conntrack tuple: source, destination, ports.
54
- original: ([u8; 4], [u8; 4], u16, u16),
54
+ pub(super) original: ([u8; 4], [u8; 4], u16, u16),
55
+ /// The file system uid of the packet's user socket; None without one, when
56
+ /// `meta skuid` breaks the rule like the kernel.
57
+ pub(super) uid: Option<u32>,
55
58
  }
56
59
  const HOST: [u8; 4] = [10, 240, 0, 1];
57
60
  const WORK: [u8; 4] = [10, 241, 0, 2];
58
61
  const NEW: u32 = 8;
59
- const ESTABLISHED: u32 = 2;
62
+ pub(super) const ESTABLISHED: u32 = 2;
60
63
  const SYN: u8 = 0x02;
61
- const ACK: u8 = 0x10;
62
- fn opening(input: Option<(u32, &'static str)>, output: Option<(u32, &'static str)>) -> Flow {
64
+ pub(super) const ACK: u8 = 0x10;
65
+ /// The `reject with tcp reset` decision, distinct from a plain drop.
66
+ pub(super) const REJECT: i32 = i32::MIN;
67
+ pub(super) fn opening(
68
+ input: Option<(u32, &'static str)>,
69
+ output: Option<(u32, &'static str)>,
70
+ ) -> Flow {
63
71
  Flow {
64
72
  input,
65
73
  output,
@@ -73,6 +81,7 @@ fn opening(input: Option<(u32, &'static str)>, output: Option<(u32, &'static str
73
81
  reply: false,
74
82
  zone: 0,
75
83
  original: (HOST, WORK, 443, 8080),
84
+ uid: None,
76
85
  }
77
86
  }
78
87
  /// The workload's answer to `flow`, arriving on the reverse links.
@@ -181,17 +190,17 @@ fn foreign(flow: Flow) -> Vec<Flow> {
181
190
  /// kernel does: the terminal verdict, the chain that reached it (`policy` for the
182
191
  /// base chain's accept policy) and the conntrack zone a raw rule stored.
183
192
  #[derive(Debug, PartialEq, Eq)]
184
- struct Decision {
185
- verdict: i32,
186
- chain: String,
187
- zone: u16,
193
+ pub(super) struct Decision {
194
+ pub(super) verdict: i32,
195
+ pub(super) chain: String,
196
+ pub(super) zone: u16,
188
197
  }
189
198
  /// The compiled table: rules per chain and element keys per set.
190
199
  struct Table {
191
200
  chains: BTreeMap<String, Vec<Vec<Attr>>>,
192
201
  sets: BTreeMap<String, Vec<Vec<u8>>>,
193
202
  }
194
- fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
203
+ pub(super) fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
195
204
  table(prepared).chains
196
205
  }
197
206
  fn table(prepared: &Prepared) -> Table {
@@ -246,6 +255,7 @@ fn load(flow: &Flow, name: &str, fields: &[Attr]) -> Option<Vec<u8>> {
246
255
  Some(match name {
247
256
  "meta" => match wire::number(fields, 2).unwrap() {
248
257
  4 => interface(flow.input, false),
258
+ 10 => flow.uid?.to_ne_bytes().to_vec(),
249
259
  5 => interface(flow.output, false),
250
260
  6 => interface(flow.input, true),
251
261
  7 => interface(flow.output, true),
@@ -340,10 +350,14 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
340
350
  }
341
351
  }
342
352
  "cmp" => {
343
- assert_eq!(wire::number(&fields, 2).unwrap(), 0);
344
353
  let expected = data(&fields, 3);
345
354
  let at = offset(&fields, 1);
346
- matched = registers[at..at + expected.len()] == expected[..];
355
+ let equal = registers[at..at + expected.len()] == expected[..];
356
+ matched = match wire::number(&fields, 2).unwrap() {
357
+ 0 => equal,
358
+ 1 => !equal,
359
+ operation => panic!("cmp {operation}"),
360
+ };
347
361
  }
348
362
  "range" => {
349
363
  assert_eq!(wire::number(&fields, 2).unwrap(), 0);
@@ -381,6 +395,10 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
381
395
  registers[at..at + value.len()].copy_from_slice(&value);
382
396
  }
383
397
  "nat" => {}
398
+ "reject" => {
399
+ assert_eq!(fields, vec![Attr::u32(1, 1)]);
400
+ verdict = Some((REJECT, None));
401
+ }
384
402
  other => panic!("{other}"),
385
403
  }
386
404
  if !matched {
@@ -391,7 +409,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
391
409
  continue;
392
410
  }
393
411
  match verdict {
394
- Some((code @ (0 | 1), _)) => {
412
+ Some((code @ (0 | 1 | REJECT), _)) => {
395
413
  return Step::Final(Decision {
396
414
  verdict: code,
397
415
  chain: chain.into(),
@@ -412,7 +430,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
412
430
  Step::Next
413
431
  }
414
432
  /// A base chain that ends without a terminal verdict accepts by policy.
415
- fn decide(prepared: &Prepared, chain: &str, flow: &Flow) -> Decision {
433
+ pub(super) fn decide(prepared: &Prepared, chain: &str, flow: &Flow) -> Decision {
416
434
  let mut zone = 0;
417
435
  match run(&table(prepared), chain, flow, &mut zone) {
418
436
  Step::Final(decision) => decision,
@@ -793,7 +811,7 @@ fn host_grants_reject_ranges_wildcards_and_unbound_addresses() {
793
811
  }
794
812
 
795
813
  /// `count` distinct exact grants to the fixture workload, both protocols.
796
- fn many(count: usize) -> Value {
814
+ pub(super) fn many(count: usize) -> Value {
797
815
  json!((0..count)
798
816
  .map(|index| host_grant(
799
817
  if index % 2 == 0 { "tcp" } else { "udp" },
@@ -0,0 +1,204 @@
1
+ //! Host-local platform endpoints in host transit: leased flows from a handoff
2
+ //! into a platform endpoint served on a host address, and nothing else.
3
+ use super::hostgrant_tests::{
4
+ bytes, chains, decide, normalized, opening, prepared, Flow, ACK, ESTABLISHED,
5
+ };
6
+ use super::tests::host;
7
+ use serde_json::{json, Value};
8
+
9
+ const TRANSIT: [u8; 4] = [10, 240, 0, 2];
10
+ const UPLINK: [u8; 4] = [192, 0, 2, 2];
11
+ const HANDOFF: [u8; 4] = [10, 240, 0, 1];
12
+ const LINK: Option<(u32, &str)> = Some((4, "handoff"));
13
+
14
+ /// The host fixture with two platform endpoints on host addresses: a TCP hub
15
+ /// on the uplink address and a UDP store on the handoff address.
16
+ pub(crate) fn served(mut value: Value) -> Value {
17
+ value["scope"]["protection"]["platformEndpoints"]
18
+ .as_array_mut()
19
+ .unwrap()
20
+ .extend([
21
+ json!({"id":"hub","address":"192.0.2.2","protocol":"tcp","port":8443}),
22
+ json!({"id":"store","address":"10.240.0.1","protocol":"udp","port":7000}),
23
+ ]);
24
+ value
25
+ }
26
+ pub(crate) fn local(mut value: Value, ids: Value) -> Value {
27
+ value["scope"]["localPlatformEndpoints"] = ids;
28
+ value
29
+ }
30
+ fn exact() -> Value {
31
+ local(served(host()), json!(["store", "hub"]))
32
+ }
33
+ /// A leased opening from the transit source address arriving on the handoff.
34
+ fn arrival(protocol: u8, source_port: u16, destination: [u8; 4], port: u16) -> Flow {
35
+ Flow {
36
+ input: LINK,
37
+ output: None,
38
+ protocol,
39
+ source: TRANSIT,
40
+ destination,
41
+ source_port,
42
+ destination_port: port,
43
+ original: (TRANSIT, destination, source_port, port),
44
+ ..opening(LINK, None)
45
+ }
46
+ }
47
+ /// The host's answer to `flow`, leaving on the handoff.
48
+ fn answer(flow: Flow) -> Flow {
49
+ Flow {
50
+ input: None,
51
+ output: LINK,
52
+ source: flow.destination,
53
+ destination: flow.source,
54
+ source_port: flow.destination_port,
55
+ destination_port: flow.source_port,
56
+ tcp_flags: ACK,
57
+ state: ESTABLISHED,
58
+ reply: true,
59
+ ..flow
60
+ }
61
+ }
62
+
63
+ #[test]
64
+ fn local_platform_endpoints_absent_and_empty_keep_the_host_digest_and_compiled_bytes() {
65
+ let baseline = prepared(host());
66
+ let empty = prepared(local(host(), json!([])));
67
+ assert_eq!(empty, baseline);
68
+ assert_eq!(bytes(&empty), bytes(&baseline));
69
+ let with = prepared(exact());
70
+ assert!(serde_json::to_string(&with.policy)
71
+ .unwrap()
72
+ .contains("\"localPlatformEndpoints\":[\"hub\",\"store\"]"));
73
+ // Canonical order.
74
+ assert_eq!(
75
+ prepared(local(served(host()), json!(["hub", "store"]))),
76
+ with
77
+ );
78
+ // The endpoints and the chains exist only when declared.
79
+ let graph = chains(&with);
80
+ assert!(
81
+ graph.contains_key("local_platform_in")
82
+ && !chains(&baseline).contains_key("local_platform_in")
83
+ );
84
+ }
85
+
86
+ #[test]
87
+ fn local_platform_endpoint_on_a_host_address_is_refused_unless_declared() {
88
+ // The refusal stays for every undeclared endpoint on a host address.
89
+ assert!(normalized(served(host())).is_err());
90
+ assert!(normalized(local(served(host()), json!(["hub"]))).is_err());
91
+ assert!(normalized(local(served(host()), json!(["store"]))).is_err());
92
+ assert!(normalized(exact()).is_ok());
93
+ for invalid in [
94
+ // Unknown, duplicate, or an endpoint the host does not hold.
95
+ json!(["hub", "store", "missing"]),
96
+ json!(["hub", "store", "hub"]),
97
+ json!(["hub", "store", "dns"]),
98
+ json!(["hub", "store", "relay"]),
99
+ json!(["hub", "store", ""]),
100
+ json!(["hub", "store", 1]),
101
+ json!("hub"),
102
+ json!({"hub": true}),
103
+ ] {
104
+ assert!(
105
+ normalized(local(served(host()), invalid.clone())).is_err(),
106
+ "{invalid}"
107
+ );
108
+ }
109
+ }
110
+
111
+ #[test]
112
+ fn local_platform_endpoint_admits_only_leased_exact_untranslated_flows_and_their_replies() {
113
+ let baseline = prepared(host());
114
+ let with = prepared(exact());
115
+ let tcp = arrival(6, 10500, UPLINK, 8443);
116
+ let udp = arrival(17, 10500, HANDOFF, 7000);
117
+ let decided = |flow: &Flow| {
118
+ let chain = if flow.input.is_some() {
119
+ "input"
120
+ } else {
121
+ "output"
122
+ };
123
+ (
124
+ decide(&baseline, chain, flow).verdict,
125
+ decide(&with, chain, flow).verdict,
126
+ )
127
+ };
128
+ // Every admitted packet: the TCP opening, established TCP, the UDP opening,
129
+ // and both replies. The baseline denies each at the handoff.
130
+ for flow in [
131
+ tcp,
132
+ Flow {
133
+ state: ESTABLISHED,
134
+ tcp_flags: ACK,
135
+ ..tcp
136
+ },
137
+ udp,
138
+ answer(tcp),
139
+ answer(udp),
140
+ ] {
141
+ assert_eq!(decided(&flow), (0, 1));
142
+ }
143
+ // Everything else keeps the baseline decision: a source port outside the
144
+ // leased range of the protocol, another source address, another port or
145
+ // protocol, a flow translated in transit, another zone, a TCP opening
146
+ // without SYN, and a host-origin opening toward the transit address.
147
+ for flow in [
148
+ arrival(6, 20000, UPLINK, 8443),
149
+ arrival(6, 9999, UPLINK, 8443),
150
+ arrival(6, 10500, UPLINK, 8444),
151
+ arrival(17, 10500, UPLINK, 8443),
152
+ arrival(6, 10500, HANDOFF, 7000),
153
+ arrival(17, 10500, HANDOFF, 7001),
154
+ Flow {
155
+ source: [10, 240, 0, 3],
156
+ original: ([10, 240, 0, 3], UPLINK, 10500, 8443),
157
+ ..tcp
158
+ },
159
+ Flow {
160
+ original: (TRANSIT, HANDOFF, 10500, 9000),
161
+ ..tcp
162
+ },
163
+ Flow { zone: 7, ..tcp },
164
+ Flow {
165
+ tcp_flags: ACK,
166
+ ..tcp
167
+ },
168
+ Flow {
169
+ reply: false,
170
+ state: 8,
171
+ tcp_flags: 0x02,
172
+ original: (UPLINK, TRANSIT, 8443, 10500),
173
+ ..answer(tcp)
174
+ },
175
+ Flow {
176
+ state: 8,
177
+ ..answer(tcp)
178
+ },
179
+ ] {
180
+ let (before, after) = decided(&flow);
181
+ assert_eq!(after, before, "{:?}", flow.original);
182
+ }
183
+ // A translated arrival that the source range admits is still refused: the
184
+ // original tuple is part of every check.
185
+ assert_eq!(
186
+ decided(&Flow {
187
+ original: (TRANSIT, HANDOFF, 10500, 9000),
188
+ ..tcp
189
+ }),
190
+ (0, 0)
191
+ );
192
+ // Forwarded traffic and the uplink keep their exact previous rules. (The
193
+ // protected chains list every platform endpoint, served here or not.)
194
+ let graph = chains(&with);
195
+ let reference = chains(&baseline);
196
+ for name in ["forward", "post"] {
197
+ assert_eq!(graph[name], reference[name], "{name}");
198
+ }
199
+ // One jump per leased range per direction, ahead of the handoff denials.
200
+ assert_eq!(graph["input"].len(), reference["input"].len() + 2);
201
+ assert_eq!(graph["output"].len(), reference["output"].len() + 2);
202
+ assert_eq!(graph["local_platform_in"].len(), 4);
203
+ assert_eq!(graph["local_platform_back"].len(), 2);
204
+ }