@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
@@ -0,0 +1,313 @@
1
+ //! Workload grants: stateful one-way flows between two workload endpoints of
2
+ //! one router scope, with no reverse opening.
3
+ use super::hostgrant_tests::{
4
+ bytes, chains, decide, host_grant, many, normalized, opening, prepared, Flow, ACK, ESTABLISHED,
5
+ };
6
+ use super::tests::{host, pool_guard, router};
7
+ use crate::wire;
8
+ use serde_json::{json, Value};
9
+
10
+ const SOURCE: [u8; 4] = [10, 241, 0, 2];
11
+ const TARGET: [u8; 4] = [10, 241, 1, 2];
12
+ const SOURCE_LINK: Option<(u32, &str)> = Some((2, "work"));
13
+ const TARGET_LINK: Option<(u32, &str)> = Some((5, "target"));
14
+
15
+ /// The router fixture with a second veth workload endpoint, `target`.
16
+ pub(crate) fn two_workloads() -> Value {
17
+ let mut value = router();
18
+ value["scope"]["endpoints"]
19
+ .as_array_mut()
20
+ .unwrap()
21
+ .push(json!(
22
+ {"id":"target","interfaceIndex":5,"interfaceName":"target","interfaceKind":"veth",
23
+ "sourcePrefixes":["10.241.1.2/32"]}));
24
+ value["scope"]["links"].as_array_mut().unwrap().push(json!(
25
+ {"interfaceIndex":5,"interfaceName":"target","interfaceKind":"veth",
26
+ "macAddress":"02:00:00:00:00:05","interfaceLinkIndex":105,
27
+ "requiredIpv4Addresses":["10.241.1.1"]}));
28
+ value
29
+ }
30
+ pub(crate) fn workload_grant(protocol: &str, source: &str, destination: &str, port: u16) -> Value {
31
+ host_grant(protocol, source, destination, port)
32
+ }
33
+ pub(crate) fn granted(mut value: Value, grants: Value) -> Value {
34
+ value["scope"]["workloadGrants"] = grants;
35
+ value
36
+ }
37
+ fn exact() -> Value {
38
+ json!([
39
+ workload_grant("tcp", "10.241.0.2", "10.241.1.2", 8080),
40
+ workload_grant("udp", "10.241.0.2", "10.241.1.2", 5353)
41
+ ])
42
+ }
43
+ /// The source workload's opening toward the target, as FORWARD sees it.
44
+ fn dial(protocol: u8, port: u16) -> Flow {
45
+ Flow {
46
+ input: SOURCE_LINK,
47
+ output: TARGET_LINK,
48
+ protocol,
49
+ source: SOURCE,
50
+ destination: TARGET,
51
+ source_port: 40000,
52
+ destination_port: port,
53
+ original: (SOURCE, TARGET, 40000, port),
54
+ ..opening(SOURCE_LINK, TARGET_LINK)
55
+ }
56
+ }
57
+ fn answer(flow: Flow) -> Flow {
58
+ Flow {
59
+ input: flow.output,
60
+ output: flow.input,
61
+ source: flow.destination,
62
+ destination: flow.source,
63
+ source_port: flow.destination_port,
64
+ destination_port: flow.source_port,
65
+ tcp_flags: ACK,
66
+ state: ESTABLISHED,
67
+ reply: true,
68
+ ..flow
69
+ }
70
+ }
71
+
72
+ #[test]
73
+ fn workload_grants_absent_and_empty_keep_the_router_digest_and_compiled_bytes() {
74
+ let baseline = prepared(two_workloads());
75
+ let empty = prepared(granted(two_workloads(), json!([])));
76
+ assert_eq!(empty, baseline);
77
+ assert_eq!(bytes(&empty), bytes(&baseline));
78
+ let with = prepared(granted(two_workloads(), exact()));
79
+ assert_ne!(with.digest, baseline.digest);
80
+ let mut reversed = exact();
81
+ reversed.as_array_mut().unwrap().reverse();
82
+ assert_eq!(prepared(granted(two_workloads(), reversed)), with);
83
+ // Only the router scope carries workload grants.
84
+ for fixture in [host(), pool_guard()] {
85
+ assert!(normalized(granted(fixture, exact())).is_err());
86
+ }
87
+ }
88
+
89
+ #[test]
90
+ fn workload_grant_opens_one_way_and_admits_only_the_exact_answer() {
91
+ let baseline = prepared(two_workloads());
92
+ let with = prepared(granted(two_workloads(), exact()));
93
+ let decided = |flow: &Flow| {
94
+ (
95
+ decide(&baseline, "forward", flow).verdict,
96
+ decide(&with, "forward", flow).verdict,
97
+ )
98
+ };
99
+ let tcp = dial(6, 8080);
100
+ let udp = dial(17, 5353);
101
+ // The openings, the established original direction and both answers pass;
102
+ // the baseline denies each at the private endpoint denials.
103
+ for flow in [
104
+ tcp,
105
+ Flow {
106
+ state: ESTABLISHED,
107
+ tcp_flags: ACK,
108
+ ..tcp
109
+ },
110
+ udp,
111
+ answer(tcp),
112
+ answer(udp),
113
+ ] {
114
+ assert_eq!(decided(&flow), (0, 1), "{:?}", flow.original);
115
+ }
116
+ // No reverse opening: the target cannot open toward the source, not even
117
+ // from the granted port to the source's port, and an answer that is not
118
+ // ESTABLISHED is refused.
119
+ let reverse = |port: u16, source_port: u16| Flow {
120
+ reply: false,
121
+ state: 8,
122
+ tcp_flags: 0x02,
123
+ original: (TARGET, SOURCE, source_port, port),
124
+ ..answer(Flow {
125
+ source_port: port,
126
+ destination_port: source_port,
127
+ ..tcp
128
+ })
129
+ };
130
+ for flow in [
131
+ reverse(40000, 8080),
132
+ reverse(8080, 40000),
133
+ reverse(5353, 40000),
134
+ Flow {
135
+ state: 8,
136
+ ..answer(tcp)
137
+ },
138
+ Flow {
139
+ state: 8,
140
+ ..answer(udp)
141
+ },
142
+ ] {
143
+ assert_eq!(decided(&flow), (0, 0), "{:?}", flow.original);
144
+ }
145
+ // Everything the grant does not name keeps the baseline decision: another
146
+ // port or protocol, a spoofed or renamed link, another address, a flow
147
+ // translated in transit, another zone and a TCP opening without SYN.
148
+ for flow in [
149
+ dial(6, 8081),
150
+ dial(17, 8080),
151
+ dial(6, 5353),
152
+ Flow {
153
+ input: Some((3, "vpn")),
154
+ ..tcp
155
+ },
156
+ Flow {
157
+ input: Some((2, "renamed")),
158
+ ..tcp
159
+ },
160
+ Flow {
161
+ input: Some((9, "work")),
162
+ ..tcp
163
+ },
164
+ Flow {
165
+ output: Some((5, "renamed")),
166
+ ..tcp
167
+ },
168
+ Flow {
169
+ destination: [10, 241, 1, 3],
170
+ original: (SOURCE, [10, 241, 1, 3], 40000, 8080),
171
+ ..tcp
172
+ },
173
+ Flow {
174
+ original: (SOURCE, TARGET, 40000, 9090),
175
+ ..tcp
176
+ },
177
+ Flow {
178
+ original: ([10, 241, 0, 9], TARGET, 40000, 8080),
179
+ ..tcp
180
+ },
181
+ Flow { zone: 17, ..tcp },
182
+ Flow {
183
+ tcp_flags: ACK,
184
+ ..tcp
185
+ },
186
+ ] {
187
+ let (before, after) = decided(&flow);
188
+ assert_eq!(after, before, "{:?} {:?}", flow.input, flow.original);
189
+ }
190
+ // Everything else in the graph is unchanged.
191
+ let graph = chains(&with);
192
+ let reference = chains(&baseline);
193
+ for (name, rules) in &reference {
194
+ if name != "forward" {
195
+ assert_eq!(&graph[name], rules, "{name}");
196
+ }
197
+ }
198
+ assert_eq!(graph["forward"].len(), reference["forward"].len() + 4);
199
+ }
200
+
201
+ #[test]
202
+ fn workload_grants_reject_unbound_same_link_local_platform_and_tun_ends() {
203
+ assert!(normalized(granted(two_workloads(), exact())).is_ok());
204
+ let grant = |source: &str, destination: &str| {
205
+ granted(
206
+ two_workloads(),
207
+ json!([workload_grant("tcp", source, destination, 8080)]),
208
+ )
209
+ };
210
+ let mut tun = two_workloads();
211
+ tun["scope"]["endpoints"][1]["sourcePrefixes"] = json!(["10.242.0.0/24"]);
212
+ for invalid in [
213
+ // An end outside every workload endpoint, or the same endpoint twice.
214
+ grant("10.241.0.2", "10.241.9.2"),
215
+ grant("10.241.9.2", "10.241.1.2"),
216
+ grant("10.241.0.2", "10.241.0.2"),
217
+ // A router-local address or a TUN endpoint is not a veth workload.
218
+ grant("10.241.0.2", "10.241.1.1"),
219
+ grant("10.242.0.2", "10.241.1.2"),
220
+ grant("10.241.0.2", "10.242.0.2"),
221
+ // Prefixes, wildcards, ports and duplicates, as for host grants.
222
+ grant("10.241.0.2", "10.241.1.0/24"),
223
+ grant("0.0.0.0", "10.241.1.2"),
224
+ granted(
225
+ two_workloads(),
226
+ json!([workload_grant("tcp", "10.241.0.2", "10.241.1.2", 0)]),
227
+ ),
228
+ granted(
229
+ two_workloads(),
230
+ json!([workload_grant("sctp", "10.241.0.2", "10.241.1.2", 80)]),
231
+ ),
232
+ granted(
233
+ two_workloads(),
234
+ json!([
235
+ workload_grant("tcp", "10.241.0.2", "10.241.1.2", 80),
236
+ workload_grant("tcp", "10.241.0.2", "10.241.1.2", 80)
237
+ ]),
238
+ ),
239
+ granted(
240
+ two_workloads(),
241
+ json!([{"protocol":"tcp","sourceAddress":"10.241.0.2","destinationAddress":"10.241.1.2",
242
+ "destinationPort":80,"bidirectional":true}]),
243
+ ),
244
+ granted(
245
+ two_workloads(),
246
+ json!((0..1025_u16)
247
+ .map(|port| workload_grant("tcp", "10.241.0.2", "10.241.1.2", 1 + port))
248
+ .collect::<Vec<_>>()),
249
+ ),
250
+ ] {
251
+ assert!(
252
+ normalized(invalid.clone()).is_err(),
253
+ "{}",
254
+ invalid["scope"]["workloadGrants"]
255
+ );
256
+ }
257
+ // Both directions between two workloads are two separate grants.
258
+ assert!(normalized(granted(
259
+ two_workloads(),
260
+ json!([
261
+ workload_grant("tcp", "10.241.0.2", "10.241.1.2", 8080),
262
+ workload_grant("tcp", "10.241.1.2", "10.241.0.2", 8080)
263
+ ])
264
+ ))
265
+ .is_ok());
266
+ }
267
+
268
+ /// 1024 grants fit the atomic reserve with a constant rule graph; the element
269
+ /// budget is shared with the scope's host grants.
270
+ #[test]
271
+ fn workload_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
272
+ let deletion = 768 * (20 + 72 + 36 + 12);
273
+ let maximum: Vec<_> = (0..1024_u16)
274
+ .map(|index| {
275
+ workload_grant(
276
+ if index % 2 == 0 { "tcp" } else { "udp" },
277
+ "10.241.0.2",
278
+ "10.241.1.2",
279
+ 20000 + index / 2,
280
+ )
281
+ })
282
+ .collect();
283
+ let small = prepared(granted(two_workloads(), exact()));
284
+ let prepared = prepared(granted(two_workloads(), json!(maximum)));
285
+ assert_eq!(
286
+ chains(&prepared)["forward"].len(),
287
+ chains(&small)["forward"].len()
288
+ );
289
+ let program = prepared
290
+ .program(&format!("snft_{}", "x".repeat(59)))
291
+ .unwrap();
292
+ let size = |kinds: &[u16]| -> usize {
293
+ program
294
+ .iter()
295
+ .filter(|(kind, _)| kinds.contains(kind))
296
+ .map(|(_, attributes)| wire::encode_attrs(attributes).len() + 20)
297
+ .sum()
298
+ };
299
+ let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
300
+ assert!(program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES);
301
+ assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
302
+ println!(
303
+ "WORKLOAD_GRANT_MAX grants=1024 operations={} rule_bytes={graph} element_bytes={elements}",
304
+ program.len()
305
+ );
306
+ // Host grants and workload grants share the target budget with the rest of
307
+ // the scope's elements; both maxima together fit it.
308
+ let mut both = granted(two_workloads(), json!(maximum));
309
+ both["scope"]["hostGrants"] = many(1024);
310
+ let both = normalized(both).unwrap().prepare().unwrap();
311
+ let (operations, rule_bytes, element_bytes) = super::tests::cost(&both);
312
+ assert!(operations < 768 && rule_bytes + element_bytes <= super::compile::TARGET_BYTES);
313
+ }
package/rust/src/owner.rs CHANGED
@@ -680,7 +680,8 @@ fn chain_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
680
680
  }
681
681
  serde_json::to_vec(&wire::canonical(&identity, false)?).map_err(|_| Error::Protocol)
682
682
  }
683
- /// A set is its table, name, flags, key type and key length. The handle, the
683
+ /// A set is its table, name, flags, key type and key length, a map's data type
684
+ /// and length, and the field lengths of a concatenation. The handle, the
684
685
  /// informational backend type and count, the transaction ID and an empty
685
686
  /// description are not identity; anything else rejects.
686
687
  fn set_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
@@ -688,30 +689,78 @@ fn set_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
688
689
  for attribute in attributes {
689
690
  match attribute.id() {
690
691
  10 | 16 | 19 | 20 => {}
691
- 9 if wire::attrs(&attribute.value)?.is_empty() => {}
692
- 1..=5 => identity.push(attribute.clone()),
692
+ 9 => {
693
+ if let Some(fields) = concatenation(&attribute.value)? {
694
+ identity.push(Attr::bytes(9, fields));
695
+ }
696
+ }
697
+ 1..=7 => identity.push(attribute.clone()),
693
698
  _ => return Err(Error::Conflict),
694
699
  }
695
700
  }
696
701
  for id in [1, 2, 3, 4, 5] {
697
702
  wire::one(&identity, id)?;
698
703
  }
704
+ // A map carries its data type and length, and nothing else does.
705
+ let map = wire::number(&identity, 3)? & 8 != 0;
706
+ for id in [6, 7] {
707
+ if wire::one(&identity, id).is_ok() != map {
708
+ return Err(Error::Conflict);
709
+ }
710
+ }
699
711
  serde_json::to_vec(&wire::canonical(&identity, false)?).map_err(|_| Error::Protocol)
700
712
  }
701
- /// Only plain keys: an element with data, flags, timeouts, expressions or any
702
- /// other extension is not one the compiler created.
713
+ /// The field lengths of a set description: only a concatenation, never a size.
714
+ /// An empty description is none.
715
+ fn concatenation(value: &[u8]) -> Result<Option<Vec<u8>>> {
716
+ let description = wire::attrs(value)?;
717
+ if description.is_empty() {
718
+ return Ok(None);
719
+ }
720
+ if description.len() != 1 || description[0].id() != 2 {
721
+ return Err(Error::Conflict);
722
+ }
723
+ let mut lengths = Vec::new();
724
+ for field in wire::attrs(&description[0].value)? {
725
+ let length = wire::attrs(&field.value)?;
726
+ if field.id() != 1 || length.len() != 1 {
727
+ return Err(Error::Conflict);
728
+ }
729
+ lengths.extend(wire::number(&length, 1)?.to_be_bytes());
730
+ }
731
+ Ok(Some(lengths))
732
+ }
733
+ /// Only compiler elements: a key, the last key of an interval and a map's data.
734
+ /// Flags, timeouts, expressions or any other extension reject. The identity is
735
+ /// the three values, so any changed bound or datum is a changed element.
703
736
  fn element_keys(list: &[Attr]) -> Result<Vec<Vec<u8>>> {
704
737
  let mut keys = Vec::new();
705
738
  for element in list {
706
- let fields = wire::attrs(&element.value)?;
707
- if element.id() != 1 || fields.len() != 1 {
739
+ if element.id() != 1 {
708
740
  return Err(Error::Conflict);
709
741
  }
710
- let key = wire::attrs(&wire::one(&fields, 1)?.value)?;
711
- if key.len() != 1 {
712
- return Err(Error::Conflict);
742
+ let mut values: [Option<Vec<u8>>; 3] = [None, None, None];
743
+ for field in wire::attrs(&element.value)? {
744
+ let slot = match field.id() {
745
+ 1 => 0,
746
+ 10 => 1,
747
+ 2 => 2,
748
+ _ => return Err(Error::Conflict),
749
+ };
750
+ let value = wire::attrs(&field.value)?;
751
+ if value.len() != 1 || values[slot].is_some() {
752
+ return Err(Error::Conflict);
753
+ }
754
+ values[slot] = Some(wire::one(&value, 1)?.value.clone());
713
755
  }
714
- keys.push(wire::one(&key, 1)?.value.clone());
756
+ let [Some(key), last, data] = values else {
757
+ return Err(Error::Conflict);
758
+ };
759
+ keys.push(if last.is_none() && data.is_none() {
760
+ key
761
+ } else {
762
+ serde_json::to_vec(&(key, last, data)).map_err(|_| Error::Protocol)?
763
+ });
715
764
  }
716
765
  Ok(keys)
717
766
  }
@@ -741,7 +790,7 @@ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
741
790
  "cmp" => &[3],
742
791
  "bitwise" => &[4, 5],
743
792
  "range" => &[3, 4],
744
- "meta" | "payload" | "ct" | "nat" | "lookup" => &[],
793
+ "meta" | "payload" | "ct" | "nat" | "lookup" | "reject" => &[],
745
794
  _ => return Err(Error::Conflict),
746
795
  };
747
796
  for attribute in &mut data {
@@ -772,9 +821,10 @@ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
772
821
  /// neither unknown fields nor default repair may hide a changed kernel rule.
773
822
  fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
774
823
  let allowed: &[u16] = match name {
775
- "lookup" => &[1, 2, 5],
824
+ "lookup" => &[1, 2, 3, 5],
776
825
  "ct" | "range" => &[1, 2, 3, 4],
777
826
  "nat" => &[1, 2, 3, 4, 5, 6, 7],
827
+ "reject" => &[1],
778
828
  _ => return Ok(()),
779
829
  };
780
830
  for attribute in data {
@@ -785,10 +835,16 @@ fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
785
835
  }
786
836
  match name {
787
837
  "lookup" => {
788
- // A plain membership test of a named set: no map, no inversion.
838
+ // A membership or absence test of a named set, or a map lookup into
839
+ // a data register, which is never inverted.
789
840
  wire::text(data, 1)?;
790
841
  wire::number(data, 2)?;
791
- if data.len() != 3 || wire::number(data, 5)? != 0 {
842
+ let map = wire::one(data, 3).is_ok();
843
+ if map {
844
+ wire::number(data, 3)?;
845
+ }
846
+ let flags = wire::number(data, 5)?;
847
+ if data.len() != 3 + usize::from(map) || flags > 1 || map && flags != 0 {
792
848
  return Err(Error::Conflict);
793
849
  }
794
850
  }
@@ -825,6 +881,12 @@ fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
825
881
  wire::number(data, 6)?;
826
882
  }
827
883
  }
884
+ "reject" => {
885
+ // Only a TCP reset, which carries no ICMP code.
886
+ if data.len() != 1 || wire::number(data, 1)? != 1 {
887
+ return Err(Error::Conflict);
888
+ }
889
+ }
828
890
  "range" => {
829
891
  wire::number(data, 1)?;
830
892
  if wire::number(data, 2)? > 1 || data.len() != 4 {
@@ -389,6 +389,8 @@ fn v2_host_publishes_exact_uplink_ports_and_removes_them_with_the_generation() {
389
389
  target_port: 8443,
390
390
  target_address: "10.240.0.2".into(),
391
391
  host_ip: "192.0.2.2".into(),
392
+ host_port_end: None,
393
+ symmetric: false,
392
394
  },
393
395
  egress::PublishedPort {
394
396
  protocol: "udp".into(),
@@ -396,6 +398,8 @@ fn v2_host_publishes_exact_uplink_ports_and_removes_them_with_the_generation() {
396
398
  target_port: 8125,
397
399
  target_address: "10.240.0.2".into(),
398
400
  host_ip: "192.0.2.2".into(),
401
+ host_port_end: None,
402
+ symmetric: false,
399
403
  },
400
404
  ];
401
405
  let mut owner = Owner::new(options("host_published")).unwrap();
@@ -79,13 +79,13 @@ fn guard_policy(revision: u64, granted: bool) -> egress::Policy {
79
79
  policy
80
80
  }
81
81
  /// One policy owner in its own namespace; every transition runs there.
82
- struct Hop {
82
+ pub(super) struct Hop {
83
83
  namespace: &'static str,
84
84
  owner: Option<Owner>,
85
85
  applied: Option<Applied>,
86
86
  }
87
87
  impl Hop {
88
- fn new(namespace: &'static str, table: &'static str) -> Self {
88
+ pub(super) fn new(namespace: &'static str, table: &'static str) -> Self {
89
89
  let owner = in_namespace(namespace, move || Owner::new(options(table)).unwrap());
90
90
  Self {
91
91
  namespace,
@@ -93,7 +93,12 @@ impl Hop {
93
93
  applied: None,
94
94
  }
95
95
  }
96
- fn apply(&mut self, build: fn(u64, bool) -> egress::Policy, revision: u64, granted: bool) {
96
+ pub(super) fn apply(
97
+ &mut self,
98
+ build: fn(u64, bool) -> egress::Policy,
99
+ revision: u64,
100
+ granted: bool,
101
+ ) {
97
102
  let mut owner = self.owner.take().unwrap();
98
103
  let previous = self.applied.take();
99
104
  let (owner, applied) = in_namespace(self.namespace, move || {
@@ -108,7 +113,7 @@ impl Hop {
108
113
  self.owner = Some(owner);
109
114
  self.applied = Some(applied);
110
115
  }
111
- fn release(mut self) {
116
+ pub(super) fn release(mut self) {
112
117
  let mut owner = self.owner.take().unwrap();
113
118
  let applied = self.applied.take().unwrap();
114
119
  in_namespace(self.namespace, move || {