@push.rocks/smartnftables 4.1.0 → 4.3.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 (38) hide show
  1. package/changelog.md +23 -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/classes.manageddockerforwarding.d.ts +4 -1
  8. package/dist_ts/classes.manageddockerforwarding.js +8 -5
  9. package/dist_ts/classes.managednftables.d.ts +8 -2
  10. package/dist_ts/classes.managednftables.js +11 -6
  11. package/dist_ts/forwardhooks.d.ts +8 -0
  12. package/dist_ts/forwardhooks.js +42 -0
  13. package/dist_ts/index.d.ts +2 -0
  14. package/dist_ts/index.js +2 -1
  15. package/dist_ts/managed.docker.types.d.ts +2 -1
  16. package/dist_ts/managed.forwardhooks.types.d.ts +35 -0
  17. package/dist_ts/managed.forwardhooks.types.js +2 -0
  18. package/package.json +4 -4
  19. package/readme.md +80 -17
  20. package/rust/src/docker.frontend.rs +47 -9
  21. package/rust/src/docker.graph.rs +53 -14
  22. package/rust/src/docker.policy.rs +90 -31
  23. package/rust/src/docker.rs +48 -20
  24. package/rust/src/docker_tests.rs +206 -2
  25. package/rust/src/egress.host.rs +44 -45
  26. package/rust/src/egress.rs +56 -0
  27. package/rust/src/forwardhooks.rs +176 -0
  28. package/rust/src/forwardhooks_tests.rs +136 -0
  29. package/rust/src/main.rs +12 -8
  30. package/rust/src/wire.links.rs +28 -6
  31. package/rust/src/wire.rs +22 -2
  32. package/ts/00_commitinfo_data.ts +1 -1
  33. package/ts/classes.manageddockerforwarding.ts +7 -4
  34. package/ts/classes.managednftables.ts +13 -5
  35. package/ts/forwardhooks.ts +39 -0
  36. package/ts/index.ts +2 -0
  37. package/ts/managed.docker.types.ts +2 -1
  38. package/ts/managed.forwardhooks.types.ts +26 -0
@@ -0,0 +1,136 @@
1
+ use super::*;
2
+
3
+ fn table(family: u8, name: &str, handle: u64) -> (u8, Vec<Attr>) {
4
+ (family, vec![Attr::string(1, name), Attr::u64(4, handle)])
5
+ }
6
+ fn base(
7
+ family: u8,
8
+ table: &str,
9
+ name: &str,
10
+ hook: u32,
11
+ priority: i32,
12
+ policy: u32,
13
+ ) -> (u8, Vec<Attr>) {
14
+ let hook = Attr::nested(
15
+ 4,
16
+ vec![
17
+ Attr::bytes(1, hook.to_be_bytes().to_vec()),
18
+ Attr::bytes(2, priority.to_be_bytes().to_vec()),
19
+ ],
20
+ );
21
+ (
22
+ family,
23
+ vec![
24
+ Attr::string(1, table),
25
+ Attr::string(3, name),
26
+ hook,
27
+ Attr::bytes(5, policy.to_be_bytes().to_vec()),
28
+ Attr::string(7, "filter"),
29
+ ],
30
+ )
31
+ }
32
+ fn regular(family: u8, table: &str, name: &str) -> (u8, Vec<Attr>) {
33
+ (family, vec![Attr::string(1, table), Attr::string(3, name)])
34
+ }
35
+
36
+ #[test]
37
+ fn forward_hooks_report_every_routed_forward_base_chain_with_its_table() {
38
+ let tables = [
39
+ table(1, "snft_host", 7),
40
+ table(2, "filter", 3),
41
+ table(10, "filter", 4),
42
+ table(7, "filter", 5),
43
+ table(5, "edge", 6),
44
+ table(1, "other", 8),
45
+ ];
46
+ let chains = [
47
+ base(1, "snft_host", "forward", 2, 0, 1),
48
+ base(1, "snft_host", "input", 1, 0, 1),
49
+ base(2, "filter", "FORWARD", 2, 0, 0),
50
+ regular(2, "filter", "DOCKER-USER"),
51
+ regular(2, "filter", "DOCKER-FORWARD"),
52
+ base(10, "filter", "FORWARD", 2, 0, 1),
53
+ // Bridge and netdev hooks do not see routed packets.
54
+ base(7, "filter", "FORWARD", 2, -200, 0),
55
+ base(5, "edge", "ingress", 0, -500, 1),
56
+ base(1, "other", "early", 2, -150, 0),
57
+ ];
58
+ let (chains, docker) = classify(&tables, &chains).unwrap();
59
+ let summary: Vec<_> = chains
60
+ .iter()
61
+ .map(|c| {
62
+ (
63
+ c.family,
64
+ c.table.as_str(),
65
+ c.table_handle.as_str(),
66
+ c.chain.as_str(),
67
+ c.priority,
68
+ c.policy,
69
+ )
70
+ })
71
+ .collect();
72
+ assert_eq!(
73
+ summary,
74
+ vec![
75
+ ("inet", "other", "8", "early", -150, "drop"),
76
+ ("inet", "snft_host", "7", "forward", 0, "accept"),
77
+ ("ip", "filter", "3", "FORWARD", 0, "drop"),
78
+ ("ip6", "filter", "4", "FORWARD", 0, "accept"),
79
+ ]
80
+ );
81
+ assert_eq!(
82
+ docker,
83
+ Docker {
84
+ docker_user: true,
85
+ docker_forward: true
86
+ }
87
+ );
88
+ let json = serde_json::to_value(&chains[0]).unwrap();
89
+ assert_eq!(
90
+ json,
91
+ serde_json::json!({"family":"inet","table":"other","tableHandle":"8","chain":"early",
92
+ "type":"filter","priority":-150,"policy":"drop"})
93
+ );
94
+ }
95
+
96
+ #[test]
97
+ fn forward_hooks_report_absent_docker_chains_and_reject_inconsistent_dumps() {
98
+ let (chains, docker) = classify(
99
+ &[],
100
+ &[
101
+ regular(10, "filter", "DOCKER-USER"),
102
+ regular(2, "nat", "DOCKER-FORWARD"),
103
+ ],
104
+ )
105
+ .unwrap();
106
+ assert!(chains.is_empty());
107
+ assert_eq!(
108
+ docker,
109
+ Docker {
110
+ docker_user: false,
111
+ docker_forward: false
112
+ }
113
+ );
114
+ // A base chain whose table the table dump does not hold exactly once.
115
+ let chain = [base(2, "filter", "FORWARD", 2, 0, 0)];
116
+ assert_eq!(classify(&[], &chain), Err(Error::Conflict));
117
+ assert_eq!(
118
+ classify(&[table(2, "filter", 1), table(2, "filter", 2)], &chain),
119
+ Err(Error::Conflict)
120
+ );
121
+ assert_eq!(
122
+ classify(
123
+ &[table(2, "filter", 1)],
124
+ &[base(2, "filter", "FORWARD", 2, 0, 3)]
125
+ ),
126
+ Err(Error::Protocol)
127
+ );
128
+ }
129
+
130
+ #[test]
131
+ fn forward_hooks_read_absent_legacy_tables_as_none() {
132
+ assert_eq!(
133
+ legacy("/proc/self/net/smartnftables-absent-fixture").unwrap(),
134
+ Vec::<String>::new()
135
+ );
136
+ }
package/rust/src/main.rs CHANGED
@@ -1,5 +1,6 @@
1
1
  mod egress;
2
2
  mod docker;
3
+ mod forwardhooks;
3
4
  mod managed;
4
5
  mod namespace;
5
6
  mod owner;
@@ -172,14 +173,12 @@ fn failure(id: &str, error: Error) -> serde_json::Value {
172
173
 
173
174
  fn main() {
174
175
  let arguments = std::env::args().skip(1).collect::<Vec<_>>();
175
- let (docker_mode, descriptor) = match arguments.as_slice() {
176
- [management] if management == "--management" => (false, None),
177
- [management] if management == "--docker-forwarding" => (true, None),
178
- [management, option, fd]
179
- if (management == "--management" || management == "--docker-forwarding") && option == "--network-namespace-fd" =>
180
- {
176
+ const MODES: [&str; 3] = ["--management", "--docker-forwarding", "--forward-hooks"];
177
+ let (mode, descriptor) = match arguments.as_slice() {
178
+ [mode] if MODES.contains(&mode.as_str()) => (mode.clone(), None),
179
+ [mode, option, fd] if MODES.contains(&mode.as_str()) && option == "--network-namespace-fd" => {
181
180
  match fd.parse::<i32>() {
182
- Ok(value) => (management == "--docker-forwarding", Some(value)),
181
+ Ok(value) => (mode.clone(), Some(value)),
183
182
  Err(_) => std::process::exit(2),
184
183
  }
185
184
  }
@@ -217,7 +216,12 @@ fn main() {
217
216
  std::process::exit(2);
218
217
  }
219
218
  let id = request.id.clone();
220
- let response = match if docker_mode { docker::dispatch(&mut docker, request) } else { dispatch(&mut owner, request) } {
219
+ let result = match mode.as_str() {
220
+ "--docker-forwarding" => docker::dispatch(&mut docker, request),
221
+ "--forward-hooks" => forwardhooks::dispatch(request),
222
+ _ => dispatch(&mut owner, request),
223
+ };
224
+ let response = match result {
221
225
  Ok(result) => serde_json::json!({"id":id,"success":true,"result":result}),
222
226
  Err(error) => failure(&id, error),
223
227
  };
@@ -22,6 +22,11 @@ pub struct BoundInterface<'a> {
22
22
  }
23
23
 
24
24
  fn read_link(socket: &mut Socket, index: u32) -> Result<Link> {
25
+ query_link(socket, index)?.map_err(Error::kernel)
26
+ }
27
+ /// The link at `index`, or the kernel's refusal code, such as `-ENODEV` for an
28
+ /// index no link holds.
29
+ fn query_link(socket: &mut Socket, index: u32) -> Result<std::result::Result<Link, i32>> {
25
30
  let sequence = socket.sequence()?;
26
31
  let mut request = Vec::new();
27
32
  request.extend(32_u32.to_ne_bytes());
@@ -49,17 +54,17 @@ fn read_link(socket: &mut Socket, index: u32) -> Result<Link> {
49
54
  .unwrap(),
50
55
  );
51
56
  if error != 0 {
52
- return Err(Error::kernel(error));
57
+ return Ok(Err(error));
53
58
  }
54
59
  } else if message.kind == 16 && message.payload.len() >= 16 {
55
60
  if u32::from_ne_bytes(message.payload[4..8].try_into().unwrap()) != index {
56
61
  return Err(Error::Conflict);
57
62
  }
58
- return Ok(Link {
63
+ return Ok(Ok(Link {
59
64
  hardware: u16::from_ne_bytes(message.payload[2..4].try_into().unwrap()),
60
65
  flags: u32::from_ne_bytes(message.payload[8..12].try_into().unwrap()),
61
66
  attributes: attrs(&message.payload[16..])?,
62
- });
67
+ }));
63
68
  } else {
64
69
  return Err(Error::Protocol);
65
70
  }
@@ -271,9 +276,26 @@ pub fn verify_bound_interfaces<'a>(
271
276
  pub fn verify_bound_interfaces_down<'a>(interfaces: impl Iterator<Item = BoundInterface<'a>>) -> Result<()> {
272
277
  let mut socket = Socket::open_protocol(libc::NETLINK_ROUTE)?;
273
278
  for binding in interfaces {
274
- let link = read_link(&mut socket, binding.index)?;
275
- matches(&link, &binding)?;
276
- if link.flags & libc::IFF_UP as u32 != 0 {return Err(Error::Conflict);}
279
+ down(&read_link(&mut socket, binding.index)?, &binding)?;
277
280
  }
278
281
  Ok(())
279
282
  }
283
+ /// Cleanup fence: a bound link whose index no link holds any more carries no
284
+ /// packets and counts as down. A link at that index must still be the exact
285
+ /// bound link, and down.
286
+ pub fn verify_bound_interfaces_down_or_absent<'a>(interfaces: impl Iterator<Item = BoundInterface<'a>>) -> Result<()> {
287
+ let mut socket = Socket::open_protocol(libc::NETLINK_ROUTE)?;
288
+ for binding in interfaces {
289
+ match query_link(&mut socket, binding.index)? {
290
+ Ok(link) => down(&link, &binding)?,
291
+ Err(code) if code == -libc::ENODEV => {}
292
+ Err(code) => return Err(Error::kernel(code)),
293
+ }
294
+ }
295
+ Ok(())
296
+ }
297
+ fn down(link: &Link, binding: &BoundInterface<'_>) -> Result<()> {
298
+ matches(link, binding)?;
299
+ if link.flags & libc::IFF_UP as u32 != 0 {return Err(Error::Conflict);}
300
+ Ok(())
301
+ }
package/rust/src/wire.rs CHANGED
@@ -139,7 +139,10 @@ pub struct Socket {
139
139
 
140
140
  #[path = "wire.links.rs"]
141
141
  mod links;
142
- pub use links::{verify_bound_interfaces, verify_bound_interfaces_down, verify_interfaces, BoundInterface};
142
+ pub use links::{
143
+ verify_bound_interfaces, verify_bound_interfaces_down, verify_bound_interfaces_down_or_absent,
144
+ verify_interfaces, BoundInterface,
145
+ };
143
146
  impl Socket {
144
147
  pub fn open() -> Result<Self> {
145
148
  Self::open_protocol(libc::NETLINK_NETFILTER)
@@ -311,6 +314,23 @@ impl Socket {
311
314
  self.query_family(operation, input, dump, 2)
312
315
  }
313
316
  fn query_family(&mut self, operation: u16, input: Vec<Attr>, dump: bool, family: u8) -> Result<Vec<Vec<Attr>>> {
317
+ Ok(self
318
+ .query_families(operation, input, dump, family)?
319
+ .into_iter()
320
+ .map(|(_, attributes)| attributes)
321
+ .collect())
322
+ }
323
+ /// Dump every object of one kind across all families, each with its family.
324
+ pub fn dump_all_families(&mut self, operation: u16) -> Result<Vec<(u8, Vec<Attr>)>> {
325
+ self.query_families(operation, vec![], true, 0)
326
+ }
327
+ fn query_families(
328
+ &mut self,
329
+ operation: u16,
330
+ input: Vec<Attr>,
331
+ dump: bool,
332
+ family: u8,
333
+ ) -> Result<Vec<(u8, Vec<Attr>)>> {
314
334
  let (sequence, bytes) =
315
335
  self.message_family(0x0a00 | operation, 1 | if dump { 0x300 } else { 0 }, &input, family)?;
316
336
  self.send(&bytes)?;
@@ -350,7 +370,7 @@ impl Socket {
350
370
  return Err(Error::Protocol);
351
371
  }
352
372
  if family == 2 && message.payload[0] != family {return Err(Error::Protocol);}
353
- result.push(attrs(&message.payload[4..])?);
373
+ result.push((message.payload[0], attrs(&message.payload[4..])?));
354
374
  if !dump {
355
375
  return Ok(result);
356
376
  }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '4.1.0',
6
+ version: '4.3.0',
7
7
  description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
8
8
  }
@@ -1,5 +1,5 @@
1
1
  import * as plugins from './plugins.js';
2
- import { snapshot, managedIpcBytes, ManagedNftablesError, nativeError } from './classes.managednftables.js';
2
+ import { snapshot, managedIpcBytes, ManagedNftablesError, nativeError, cleanupUnconfirmed } from './classes.managednftables.js';
3
3
  import type { IManagedDockerForwardingOptions, IManagedDockerForwardingStatus, IManagedDockerForwardingPolicy, IPreparedManagedDockerForwardingPolicy, IManagedDockerForwardingTransition, IAppliedManagedDockerForwardingPolicy, TManagedDockerForwardingCommands } from './managed.docker.types.js';
4
4
 
5
5
  /** One node-level Docker DOCKER-USER contribution. Caller owns durable intents and handoff lifetimes. */
@@ -65,7 +65,10 @@ export class ManagedDockerForwarding {
65
65
  return this.#invoke('releaseDockerForwarding', expected);
66
66
  }
67
67
 
68
- /** Join operations and exact contributed-rule deletion, then native termination. Keep affected handoff links down through cleanup. */
68
+ /** Join operations and exact contributed-rule deletion, then native termination. Keep affected handoff links down
69
+ * through cleanup; a handoff link that no longer exists counts as down. An owner that holds no rule, such as one
70
+ * whose first apply was refused, closes once the filter table reads without one, whatever Docker's forwarding
71
+ * path is. A failed read rejects `CLEANUP_UNCONFIRMED` with its native code as `cause`. */
69
72
  public close(): Promise<void> {
70
73
  if (this.#close) return this.#close;
71
74
  if (this.#state === 'stopped') return Promise.resolve();
@@ -80,9 +83,9 @@ export class ManagedDockerForwarding {
80
83
  await this.#bridge.terminate({ gracePeriodMs: 1000 });
81
84
  }
82
85
  this.#bridge = null; this.#state = 'stopped';
83
- })().catch(() => {
86
+ })().catch((error: unknown) => {
84
87
  this.#state = 'failed-owned'; this.#close = null;
85
- throw new ManagedNftablesError('CLEANUP_UNCONFIRMED');
88
+ throw cleanupUnconfirmed(error);
86
89
  });
87
90
  return this.#close;
88
91
  }
@@ -7,14 +7,22 @@ import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IApp
7
7
  * capacity bound of the atomic replacement budget, and a smaller policy may fit.
8
8
  * Both carry the native refusal text in `reason`; `EXHAUSTED` also names its
9
9
  * bound in `details`. Every other code, and the facade's own rejections, carry
10
- * neither. */
10
+ * neither. `CLEANUP_UNCONFIRMED` names the failure that left cleanup unconfirmed
11
+ * in `cause`. */
11
12
  export class ManagedNftablesError extends Error {
13
+ declare public readonly cause?: ManagedNftablesError;
12
14
  constructor(public readonly code: string, public readonly reason: string | null = null,
13
- public readonly details: Readonly<IManagedNftCapacity> | null = null) {
14
- super(reason === null ? `Managed nftables ${code}.` : `Managed nftables ${code}: ${reason}`);
15
+ public readonly details: Readonly<IManagedNftCapacity> | null = null, options: { cause?: ManagedNftablesError } = {}) {
16
+ super(reason === null ? `Managed nftables ${code}.` : `Managed nftables ${code}: ${reason}`, options);
15
17
  }
16
18
  }
17
19
 
20
+ /** The close failure, naming its cause: a facade error as itself, a native one by its code. */
21
+ export function cleanupUnconfirmed(error: unknown): ManagedNftablesError {
22
+ return new ManagedNftablesError('CLEANUP_UNCONFIRMED', null, null,
23
+ { cause: error instanceof ManagedNftablesError ? error : nativeError(error) });
24
+ }
25
+
18
26
  const capacityBounds: ReadonlySet<string> = new Set<TManagedNftCapacityBound>(['ruleBytes', 'targetBytes', 'operations',
19
27
  'endpoints', 'links', 'rules', 'grants', 'hostGrants', 'workloadGrants', 'publishedPorts', 'localTcpPortOwners',
20
28
  'restoreRules', 'restoreBytes']);
@@ -239,10 +247,10 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
239
247
  await this.#bridge.terminate({ gracePeriodMs: 1000 });
240
248
  }
241
249
  this.#bridge = null; this.#state = 'stopped';
242
- })().catch(() => {
250
+ })().catch((error: unknown) => {
243
251
  this.#state = 'failed-owned'; this.#close = null;
244
252
  this.#fail();
245
- throw new ManagedNftablesError('CLEANUP_UNCONFIRMED');
253
+ throw cleanupUnconfirmed(error);
246
254
  });
247
255
  return this.#close;
248
256
  }
@@ -0,0 +1,39 @@
1
+ import * as plugins from './plugins.js';
2
+ import { snapshot, managedIpcBytes, ManagedNftablesError, nativeError } from './classes.managednftables.js';
3
+ import type { IForwardHookInspection, IForwardHookInspectionOptions, TForwardHookCommands } from './managed.forwardhooks.types.js';
4
+
5
+ /** Reads every packet-filter owner on the IPv4/IPv6 forward hook of one network
6
+ * namespace: nf_tables forward base chains of the `ip`, `ip6` and `inet`
7
+ * families, Docker's DOCKER-USER/DOCKER-FORWARD chains and registered legacy
8
+ * xtables tables. One native process takes one generation-consistent read and
9
+ * exits; it claims, adopts and changes nothing. The result is a point-in-time
10
+ * view: another owner can appear after it. */
11
+ export async function inspectForwardHooks(options: IForwardHookInspectionOptions = {}): Promise<IForwardHookInspection> {
12
+ const captured = snapshot(options);
13
+ if (Object.keys(captured).some((key) => !['binaryPath', 'networkNamespaceFd'].includes(key))
14
+ || captured.binaryPath !== undefined && (typeof captured.binaryPath !== 'string'
15
+ || !plugins.path.isAbsolute(captured.binaryPath) || captured.binaryPath.includes('\0'))
16
+ || captured.networkNamespaceFd !== undefined && (!Number.isInteger(captured.networkNamespaceFd)
17
+ || captured.networkNamespaceFd < 0 || captured.networkNamespaceFd > 0x7fff_ffff)) {
18
+ throw new ManagedNftablesError('INVALID');
19
+ }
20
+ if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch)) throw new ManagedNftablesError('UNSUPPORTED_PLATFORM');
21
+ const root = plugins.url.fileURLToPath(new URL('../', import.meta.url));
22
+ const bridge = new plugins.smartrust.RustBridge<TForwardHookCommands>({
23
+ binaryName: 'smartnftables', binaryPath: captured.binaryPath
24
+ ?? plugins.path.join(root, 'dist_rust', `smartnftables_linux_${process.arch === 'x64' ? 'amd64' : 'arm64'}_musl`),
25
+ localPaths: [], searchSystemPath: false,
26
+ cliArgs: ['--forward-hooks', ...(captured.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
27
+ inheritedFileDescriptors: captured.networkNamespaceFd === undefined ? undefined : [captured.networkNamespaceFd],
28
+ readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: managedIpcBytes,
29
+ maxPendingRequestsByMethod: { inspectForwardHooks: 1 },
30
+ });
31
+ try {
32
+ if (!await bridge.spawn()) throw new ManagedNftablesError('UNAVAILABLE');
33
+ return snapshot(await bridge.sendCommand('inspectForwardHooks', {}));
34
+ } catch (error) {
35
+ throw error instanceof ManagedNftablesError ? error : nativeError(error);
36
+ } finally {
37
+ await bridge.terminate({ gracePeriodMs: 1000 });
38
+ }
39
+ }
package/ts/index.ts CHANGED
@@ -64,3 +64,5 @@ export type {
64
64
  } from './nft.types.js';
65
65
  export * from './managed.docker.types.js';
66
66
  export * from './classes.manageddockerforwarding.js';
67
+ export type * from './managed.forwardhooks.types.js';
68
+ export { inspectForwardHooks } from './forwardhooks.js';
@@ -25,7 +25,8 @@ export interface IAppliedManagedDockerForwardingPolicy {
25
25
  receipt: { identity: IManagedDockerForwardingIdentity; backend: string; revision: number; digest: string };
26
26
  prepared: IPreparedManagedDockerForwardingPolicy;
27
27
  }
28
- /** Persist before mutation. Keep both generations' exact handoff links down until reconciliation is confirmed. */
28
+ /** Persist before mutation. A handoff link both generations bind exactly may stay up: one atomic transaction swaps
29
+ * the contribution. Keep every link the transition adds or drops down until reconciliation is confirmed. */
29
30
  export interface IManagedDockerForwardingTransition {
30
31
  previous: IAppliedManagedDockerForwardingPolicy | null;
31
32
  target: IPreparedManagedDockerForwardingPolicy;
@@ -0,0 +1,26 @@
1
+ import type { IManagedNftOptions } from './managed.types.js';
2
+
3
+ export interface IForwardHookInspectionOptions extends Pick<IManagedNftOptions, 'binaryPath' | 'networkNamespaceFd'> {}
4
+ /** One nf_tables base chain on the forward hook of the `ip`, `ip6` or `inet` family. */
5
+ export interface IForwardHookChain {
6
+ family: 'ip' | 'ip6' | 'inet';
7
+ table: string;
8
+ /** Kernel table handle, as a managed receipt's `tableHandle`. */
9
+ tableHandle: string;
10
+ chain: string;
11
+ type: string;
12
+ priority: number;
13
+ policy: 'accept' | 'drop';
14
+ }
15
+ /** One consistent read of every packet-filter owner on the routed forward path of a network namespace. */
16
+ export interface IForwardHookInspection {
17
+ /** Every forward base chain, sorted by family, table and chain; managed tables included. */
18
+ chains: IForwardHookChain[];
19
+ /** Docker's iptables-nft dispatch chains in table `ip filter`. */
20
+ docker: { dockerUser: boolean; dockerForward: boolean };
21
+ /** Registered legacy xtables tables, whose hooks are not nf_tables chains. */
22
+ legacyTables: { ipv4: string[]; ipv6: string[] };
23
+ }
24
+ export type TForwardHookCommands = {
25
+ inspectForwardHooks: { params: Record<string, never>; result: IForwardHookInspection };
26
+ };