@push.rocks/smartnftables 2.4.0 → 2.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.
Files changed (33) hide show
  1. package/changelog.md +12 -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.js +3 -3
  8. package/dist_ts/classes.managednftables.d.ts +5 -0
  9. package/dist_ts/classes.managednftables.js +13 -6
  10. package/dist_ts/managed.egress.types.d.ts +31 -1
  11. package/package.json +7 -7
  12. package/readme.md +91 -3
  13. package/rust/src/egress.compile.rs +49 -1
  14. package/rust/src/egress.host.rs +22 -0
  15. package/rust/src/egress.hostgrant.rs +187 -0
  16. package/rust/src/egress.poolguard.rs +7 -0
  17. package/rust/src/egress.router.rs +45 -4
  18. package/rust/src/egress.rs +150 -1
  19. package/rust/src/egress_hostgrant_tests.rs +948 -0
  20. package/rust/src/main.rs +6 -2
  21. package/rust/src/owner.rs +116 -4
  22. package/rust/src/owner_egress_tests.rs +1 -1
  23. package/rust/src/owner_hostgrant_traffic_tests.rs +280 -0
  24. package/rust/src/owner_identity_tests.rs +80 -1
  25. package/rust/src/owner_poolguard_tests.rs +69 -0
  26. package/rust/src/owner_router_traffic_tests.rs +1 -1
  27. package/rust/src/owner_tests.rs +3 -0
  28. package/rust/src/policy.rs +1 -1
  29. package/rust/src/wire.rs +15 -1
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/classes.manageddockerforwarding.ts +2 -2
  32. package/ts/classes.managednftables.ts +13 -5
  33. package/ts/managed.egress.types.ts +32 -1
@@ -27,6 +27,9 @@ mod router_traffic_tests;
27
27
  #[path = "owner_poolguard_tests.rs"]
28
28
  mod poolguard_tests;
29
29
 
30
+ #[path = "owner_hostgrant_traffic_tests.rs"]
31
+ mod hostgrant_traffic_tests;
32
+
30
33
  #[path = "owner_detach_tests.rs"]
31
34
  mod detach_tests;
32
35
 
@@ -195,7 +195,7 @@ impl Policy {
195
195
  };
196
196
  // Compile during preparation; an oversized native batch never reaches the kernel.
197
197
  let program = prepared.program(&format!("snft_{}", "x".repeat(59)))?;
198
- // Reserve enough of the 240 KiB batch for deleting a previous maximum
198
+ // Reserve enough of the batch for deleting a previous maximum
199
199
  // graph, then installing this complete graph in the same transaction.
200
200
  if program.len() > 768
201
201
  || program
package/rust/src/wire.rs CHANGED
@@ -24,6 +24,10 @@ impl Attr {
24
24
  pub fn nested(kind: u16, value: Vec<Attr>) -> Self {
25
25
  Self::bytes(kind | 0x8000, encode_attrs(&value))
26
26
  }
27
+ /// Netlink attribute lengths are 16 bits; a longer value cannot be encoded.
28
+ pub fn encodable(&self) -> bool {
29
+ self.value.len() <= usize::from(u16::MAX) - 4
30
+ }
27
31
  pub fn id(&self) -> u16 {
28
32
  self.kind & 0x3fff
29
33
  }
@@ -31,6 +35,9 @@ impl Attr {
31
35
  pub fn encode_attrs(attrs: &[Attr]) -> Vec<u8> {
32
36
  let mut result = Vec::new();
33
37
  for attr in attrs {
38
+ // Every caller bounds its attributes far below this; a truncated length
39
+ // would make the kernel parse a different message.
40
+ assert!(attr.encodable(), "netlink attribute exceeds 65,531 bytes");
34
41
  let len = 4 + attr.value.len();
35
42
  result.extend((len as u16).to_ne_bytes());
36
43
  result.extend(attr.kind.to_ne_bytes());
@@ -117,6 +124,13 @@ struct Message {
117
124
  sequence: u32,
118
125
  payload: Vec<u8>,
119
126
  }
127
+ /// One replacement batch: handle-only deletion of a previous maximum graph
128
+ /// (768 operations, about 96 KB), a 100,000-byte target rule graph and 100,000
129
+ /// bytes of target set elements, with framing. Netlink refuses a message larger
130
+ /// than the socket send buffer; Linux's default `net.core.wmem_max` of 212,992
131
+ /// gives a 425,984-byte buffer, and this limit needs at least 160,016.
132
+ pub const BATCH_BYTES: usize = 320_000;
133
+
120
134
  pub struct Socket {
121
135
  fd: OwnedFd,
122
136
  pub port: u32,
@@ -213,7 +227,7 @@ impl Socket {
213
227
  Ok((sequence, bytes))
214
228
  }
215
229
  fn send(&self, bytes: &[u8]) -> Result<()> {
216
- if bytes.len() > 240_000 {
230
+ if bytes.len() > BATCH_BYTES {
217
231
  return Err(Error::Invalid);
218
232
  }
219
233
  let mut address: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '2.4.0',
6
+ version: '2.5.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, ManagedNftablesError } from './classes.managednftables.js';
2
+ import { snapshot, managedIpcBytes, ManagedNftablesError } 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. */
@@ -39,7 +39,7 @@ export class ManagedDockerForwarding {
39
39
  localPaths: [], searchSystemPath: false,
40
40
  cliArgs: ['--docker-forwarding', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
41
41
  inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
42
- readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
42
+ readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: managedIpcBytes,
43
43
  maxPendingRequestsByMethod: { prepareDockerForwarding: 1, openDockerForwarding: 1, reconcileDockerForwarding: 1, inspectDockerForwarding: 1, releaseDockerForwarding: 1, closeDockerForwarding: 1 },
44
44
  });
45
45
  this.#bridge.on('exit', () => {
@@ -6,16 +6,24 @@ export class ManagedNftablesError extends Error {
6
6
  constructor(public readonly code: string) { super(`Managed nftables ${code}.`); }
7
7
  }
8
8
 
9
+ /** Bounds of the private native IPC. A status carries up to three complete policies (applied,
10
+ * pending previous and pending target); a policy with the maximum 1024 host grants serializes to
11
+ * about 111 KB and 5,300 values, so these bounds hold three of them with room for the rest of a
12
+ * large scope. */
13
+ export const managedIpcBytes = 1_048_576;
14
+ const snapshotBytes = 1_000_000;
15
+ const snapshotValues = 65_536;
16
+
9
17
  /** Capture inert input without evaluating getters, proxies, coercions or toJSON. */
10
18
  export function snapshot<T>(input: T): T {
11
19
  let count = 0;
12
20
  let stringBytes = 0;
13
21
  const string = (value: string): string => {
14
- if (value.length > 240_000 || (stringBytes += Buffer.byteLength(value)) > 240_000) throw new ManagedNftablesError('INVALID');
22
+ if (value.length > snapshotBytes || (stringBytes += Buffer.byteLength(value)) > snapshotBytes) throw new ManagedNftablesError('INVALID');
15
23
  return value;
16
24
  };
17
25
  const copy = (value: unknown, depth: number): unknown => {
18
- if (++count > 16_384 || depth > 20) throw new ManagedNftablesError('INVALID');
26
+ if (++count > snapshotValues || depth > 20) throw new ManagedNftablesError('INVALID');
19
27
  if (typeof value === 'string') return string(value);
20
28
  if (value === null || typeof value === 'boolean') return value;
21
29
  if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
@@ -23,7 +31,7 @@ export function snapshot<T>(input: T): T {
23
31
  const array = Array.isArray(value);
24
32
  if (Object.getPrototypeOf(value) !== (array ? Array.prototype : Object.prototype)) throw new ManagedNftablesError('INVALID');
25
33
  const keys = Reflect.ownKeys(value);
26
- if (keys.length > 16_384 - count) throw new ManagedNftablesError('INVALID');
34
+ if (keys.length > snapshotValues - count) throw new ManagedNftablesError('INVALID');
27
35
  for (const key of keys) if (typeof key === 'string') string(key);
28
36
  const descriptors = Object.getOwnPropertyDescriptors(value);
29
37
  const result: Record<string, unknown> | unknown[] = array ? [] : {};
@@ -38,7 +46,7 @@ export function snapshot<T>(input: T): T {
38
46
  return result;
39
47
  };
40
48
  const result = copy(input, 0) as T;
41
- if (Buffer.byteLength(JSON.stringify(result)) > 240_000) throw new ManagedNftablesError('INVALID');
49
+ if (Buffer.byteLength(JSON.stringify(result)) > snapshotBytes) throw new ManagedNftablesError('INVALID');
42
50
  return result;
43
51
  }
44
52
 
@@ -97,7 +105,7 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
97
105
  localPaths: [], searchSystemPath: false,
98
106
  cliArgs: ['--management', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
99
107
  inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
100
- readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
108
+ readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: managedIpcBytes,
101
109
  maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, releasePolicyTransition: 1, detachPolicy: 1, closeOwner: 1 },
102
110
  });
103
111
  this.#bridge.on('exit', () => {
@@ -72,6 +72,22 @@ export interface IManagedNftRouterPublishedPortV2 {
72
72
  transitSourceAddress: string;
73
73
  }
74
74
 
75
+ /** One exact host-origin flow: the node's own host network namespace dials one port of one local
76
+ * workload at its lease address, from the transit host address of the current handoff. Compile the
77
+ * same grant into every scope the flow crosses (`allocationPoolGuard` and `hostTransit` on the host,
78
+ * `routerEgress` in the router namespace); each scope binds it to the authority it proves. It admits
79
+ * that tuple and its replies in the default conntrack zone and nothing else: no loopback, no
80
+ * translation, no source-port selection and no workload-origin flow. */
81
+ export interface IManagedNftHostGrant {
82
+ protocol: 'tcp' | 'udp';
83
+ /** Exact unicast address, never a prefix: the host's own address on the handoff. */
84
+ sourceAddress: string;
85
+ /** Exact unicast workload lease address, never a prefix. */
86
+ destinationAddress: string;
87
+ /** The workload's exact listening port, 1–65535; never a range. */
88
+ destinationPort: number;
89
+ }
90
+
75
91
  /** One combined private/TUN/local/egress table; never compose a second ACCEPT over v1 terminal drops. */
76
92
  export interface IManagedNftRouterEgressScopeV2 {
77
93
  kind: 'routerEgress';
@@ -89,6 +105,12 @@ export interface IManagedNftRouterEgressScopeV2 {
89
105
  * forward and reply admission in this generation, so a failed apply, a target
90
106
  * without it, or release removes it with the generation. */
91
107
  publishedPorts?: IManagedNftRouterPublishedPortV2[];
108
+ /** Optional host-origin grants forwarded from the handoff to a workload endpoint. Each
109
+ * `destinationAddress` is an exact current veth endpoint address of this scope; each
110
+ * `sourceAddress` is protected space that is neither router-local, a platform endpoint nor a
111
+ * workload. Absent and empty are the same canonical policy, digest and compiled graph; at most 1024,
112
+ * held in a named set behind a constant number of rules. */
113
+ hostGrants?: IManagedNftHostGrant[];
92
114
  }
93
115
 
94
116
  /** One inbound uplink publication, compiled inside the same host-transit generation. */
@@ -116,15 +138,24 @@ export interface IManagedNftHostTransitScopeV2 {
116
138
  * exact forward and reply admission in this generation, so a failed apply, a target
117
139
  * without it, or release removes it with the generation. */
118
140
  publishedPorts?: IManagedNftPublishedPortV2[];
141
+ /** Optional host-origin grants leaving through a handoff. Each `sourceAddress` is an exact current
142
+ * address of exactly one handoff link; each `destinationAddress` is protected space that is neither
143
+ * host-local, a platform endpoint nor a leased transit source address. Absent and empty are the
144
+ * same canonical policy; at most 1024, held in a named set behind a constant number of rules. */
145
+ hostGrants?: IManagedNftHostGrant[];
119
146
  }
120
147
 
121
148
  /** Host-wide IPv4 destination denial for caller-authenticated private allocation pools.
122
- * This has no link dependency or allow grants. It is not allocation-release or boot-order proof. */
149
+ * It has no link dependency; exact host grants are its only exceptions. It is not allocation-release or boot-order proof. */
123
150
  export interface IManagedNftAllocationPoolGuardScopeV2 {
124
151
  kind: 'allocationPoolGuard';
125
152
  authorityDigest: string;
126
153
  /** Complete current pool list: 1–64 canonical, disjoint RFC1918 prefixes. */
127
154
  prefixes: string[];
155
+ /** The only exceptions to the guard: exact host-origin flows whose `destinationAddress` lies in a
156
+ * listed pool, and their replies. Absent and empty are the same canonical policy; at most 1024,
157
+ * held in a named set behind a constant number of rules. */
158
+ hostGrants?: IManagedNftHostGrant[];
128
159
  }
129
160
 
130
161
  export interface IManagedNftPolicyV2 {