@push.rocks/smartnftables 1.7.0 → 1.8.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.
package/changelog.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-09-14 - 1.8.0
4
+
5
+ ### Features
6
+
7
+ - Add terminal policy detachment with exact PERSIST verification, retained-intent recovery and failure-safe native process cleanup.
8
+
3
9
  ## 2026-09-14 - 1.7.0
4
10
 
5
11
  ### Features
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "d220f9e5399c394467c212c9c29a01d56395bde551a7a1af7bd233ae313bbf21",
3
+ "binarySha256": "e7f3f755b5f9579969e94d70442710a8b2ebde0aa8dda4c0b312094b8771caf6",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "1.7.0",
7
- "gitCommit": "9f206a95e42e76d6a4a61ecce11cf5d02927fb41",
6
+ "projectVersion": "1.8.0",
7
+ "gitCommit": "63a819de3661273bd4df2cea457f9fb3c4131cc9",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-09-14T02:54:17.256Z",
9
+ "builtAt": "2026-09-14T03:26:32.636Z",
10
10
  "tsrustVersion": "1.13.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_amd64_musl"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "a282a800a9782297af8e7862ef2b8abbbc1aefcc79d50b5d0f453feecc874e36",
3
+ "binarySha256": "f80787be41352d19daf0b3916cb285fe29aa3e88f637180109a07ab8ec609b14",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "1.7.0",
7
- "gitCommit": "9f206a95e42e76d6a4a61ecce11cf5d02927fb41",
6
+ "projectVersion": "1.8.0",
7
+ "gitCommit": "63a819de3661273bd4df2cea457f9fb3c4131cc9",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-09-14T02:54:17.376Z",
9
+ "builtAt": "2026-09-14T03:26:32.863Z",
10
10
  "tsrustVersion": "1.13.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_arm64_musl"
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '1.7.0',
6
+ version: '1.8.0',
7
7
  description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwyQkFBMkI7SUFDakMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLG1IQUFtSDtDQUNqSSxDQUFBIn0=
@@ -18,6 +18,13 @@ export declare class ManagedNftables<TPolicy extends TManagedNftPolicy = IManage
18
18
  release(expected: IAppliedManagedNftPolicy<TPolicy>): Promise<{
19
19
  released: true;
20
20
  }>;
21
- /** Join policy operations and owned-table deletion, then native termination; packet/allocation fencing belongs to the caller. */
21
+ /** Permanently selects retention cleanup, verifies exact PERSIST retention, and
22
+ * joins the native process. Retain the full applied value before this call.
23
+ * After any failure close() only terminates; it never deletes the table. */
24
+ detach(expected: IAppliedManagedNftPolicy<TPolicy>): Promise<{
25
+ detached: true;
26
+ }>;
27
+ /** Join policy operations, then delete and terminate unless detach selected retention.
28
+ * Packet/allocation fencing belongs to the caller; already-admitted close cannot be undone. */
22
29
  close(): Promise<void>;
23
30
  }
@@ -62,6 +62,9 @@ export class ManagedNftables {
62
62
  #work = null;
63
63
  #close = null;
64
64
  #nativeMayOwn = false;
65
+ #retaining = false;
66
+ #detachExpected = null;
67
+ #detach = null;
65
68
  constructor(options) {
66
69
  this.#options = snapshot(options);
67
70
  if (Object.keys(this.#options).some((key) => !['ownerId', 'instanceId', 'tableName', 'binaryPath', 'networkNamespaceFd'].includes(key))
@@ -78,7 +81,7 @@ export class ManagedNftables {
78
81
  get state() { return this.#state; }
79
82
  /** Starts only the compiler/IPC owner. The returned idle status is not enforcement. */
80
83
  start() {
81
- if (this.#state !== 'new')
84
+ if (this.#state !== 'new' || this.#retaining)
82
85
  return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
83
86
  this.#state = 'starting';
84
87
  const work = (async () => {
@@ -92,7 +95,7 @@ export class ManagedNftables {
92
95
  cliArgs: ['--management', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
93
96
  inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
94
97
  readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
95
- maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, closeOwner: 1 },
98
+ maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, detachPolicy: 1, closeOwner: 1 },
96
99
  });
97
100
  this.#bridge.on('exit', () => {
98
101
  if (!['closing', 'stopped'].includes(this.#state))
@@ -119,17 +122,50 @@ export class ManagedNftables {
119
122
  release(expected) {
120
123
  return this.#invoke('releasePolicy', expected);
121
124
  }
122
- /** Join policy operations and owned-table deletion, then native termination; packet/allocation fencing belongs to the caller. */
125
+ /** Permanently selects retention cleanup, verifies exact PERSIST retention, and
126
+ * joins the native process. Retain the full applied value before this call.
127
+ * After any failure close() only terminates; it never deletes the table. */
128
+ detach(expected) {
129
+ this.#retaining = true;
130
+ let captured;
131
+ try {
132
+ captured = snapshot(expected);
133
+ if (this.#detachExpected && !plugins.util.isDeepStrictEqual(this.#detachExpected, captured))
134
+ throw new ManagedNftablesError('CONFLICT');
135
+ if (this.#detach)
136
+ return this.#detach;
137
+ if (!['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work || this.#close)
138
+ throw new ManagedNftablesError('UNAVAILABLE');
139
+ this.#detachExpected = captured;
140
+ }
141
+ catch (error) {
142
+ return Promise.reject(error instanceof ManagedNftablesError ? error : new ManagedNftablesError('INVALID'));
143
+ }
144
+ const work = this.#track(this.#bridge.sendCommand('detachPolicy', captured).then((value) => {
145
+ const result = snapshot(value);
146
+ if (result?.detached !== true || Object.keys(result).length !== 1)
147
+ throw new ManagedNftablesError('PROTOCOL');
148
+ this.#nativeMayOwn = false;
149
+ return result;
150
+ }));
151
+ const detached = (async () => { const result = await work; await this.close(); return result; })();
152
+ this.#detach = detached;
153
+ void detached.catch(() => { this.#detach = null; });
154
+ return detached;
155
+ }
156
+ /** Join policy operations, then delete and terminate unless detach selected retention.
157
+ * Packet/allocation fencing belongs to the caller; already-admitted close cannot be undone. */
123
158
  close() {
124
159
  if (this.#close)
125
160
  return this.#close;
126
161
  if (this.#state === 'stopped')
127
162
  return Promise.resolve();
163
+ const retaining = this.#retaining;
128
164
  this.#state = 'closing';
129
165
  this.#close = (async () => {
130
166
  await this.#work?.catch(() => undefined);
131
167
  if (this.#bridge) {
132
- if (this.#nativeMayOwn) {
168
+ if (this.#nativeMayOwn && !retaining) {
133
169
  const result = await this.#bridge.sendCommand('closeOwner', {});
134
170
  if (result.released !== true)
135
171
  throw new ManagedNftablesError('PROTOCOL');
@@ -146,7 +182,7 @@ export class ManagedNftables {
146
182
  return this.#close;
147
183
  }
148
184
  #invoke(method, params) {
149
- if (!['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work)
185
+ if (this.#retaining || !['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work)
150
186
  return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
151
187
  let captured;
152
188
  try {
@@ -179,4 +215,4 @@ export class ManagedNftables {
179
215
  return owned;
180
216
  }
181
217
  }
182
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5tYW5hZ2VkbmZ0YWJsZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9jbGFzc2VzLm1hbmFnZWRuZnRhYmxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUl4QyxNQUFNLE9BQU8sb0JBQXFCLFNBQVEsS0FBSztJQUM3QyxZQUE0QixJQUFZO1FBQUksS0FBSyxDQUFDLG9CQUFvQixJQUFJLEdBQUcsQ0FBQyxDQUFDO1FBQW5ELFNBQUksR0FBSixJQUFJLENBQVE7SUFBd0MsQ0FBQztDQUNsRjtBQUVELG9GQUFvRjtBQUNwRixNQUFNLFVBQVUsUUFBUSxDQUFJLEtBQVE7SUFDbEMsSUFBSSxLQUFLLEdBQUcsQ0FBQyxDQUFDO0lBQ2QsSUFBSSxXQUFXLEdBQUcsQ0FBQyxDQUFDO0lBQ3BCLE1BQU0sTUFBTSxHQUFHLENBQUMsS0FBYSxFQUFVLEVBQUU7UUFDdkMsSUFBSSxLQUFLLENBQUMsTUFBTSxHQUFHLE9BQU8sSUFBSSxDQUFDLFdBQVcsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsT0FBTztZQUFFLE1BQU0sSUFBSSxvQkFBb0IsQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUM3SCxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUMsQ0FBQztJQUNGLE1BQU0sSUFBSSxHQUFHLENBQUMsS0FBYyxFQUFFLEtBQWEsRUFBVyxFQUFFO1FBQ3RELElBQUksRUFBRSxLQUFLLEdBQUcsTUFBTSxJQUFJLEtBQUssR0FBRyxFQUFFO1lBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQzlFLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtZQUFFLE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ3BELElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxTQUFTO1lBQUUsT0FBTyxL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
218
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5tYW5hZ2VkbmZ0YWJsZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9jbGFzc2VzLm1hbmFnZWRuZnRhYmxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUl4QyxNQUFNLE9BQU8sb0JBQXFCLFNBQVEsS0FBSztJQUM3QyxZQUE0QixJQUFZO1FBQUksS0FBSyxDQUFDLG9CQUFvQixJQUFJLEdBQUcsQ0FBQyxDQUFDO1FBQW5ELFNBQUksR0FBSixJQUFJLENBQVE7SUFBd0MsQ0FBQztDQUNsRjtBQUVELG9GQUFvRjtBQUNwRixNQUFNLFVBQVUsUUFBUSxDQUFJLEtBQVE7SUFDbEMsSUFBSSxLQUFLLEdBQUcsQ0FBQyxDQUFDO0lBQ2QsSUFBSSxXQUFXLEdBQUcsQ0FBQyxDQUFDO0lBQ3BCLE1BQU0sTUFBTSxHQUFHLENBQUMsS0FBYSxFQUFVLEVBQUU7UUFDdkMsSUFBSSxLQUFLLENBQUMsTUFBTSxHQUFHLE9BQU8sSUFBSSxDQUFDLFdBQVcsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsT0FBTztZQUFFLE1BQU0sSUFBSSxvQkFBb0IsQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUM3SCxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUMsQ0FBQztJQUNGLE1BQU0sSUFBSSxHQUFHLENBQUMsS0FBYyxFQUFFLEtBQWEsRUFBVyxFQUFFO1FBQ3RELElBQUksRUFBRSxLQUFLLEdBQUcsTUFBTSxJQUFJLEtBQUssR0FBRyxFQUFFO1lBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQzlFLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtZQUFFLE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ3BELElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxTQUFTO1lBQUUsT0FBTyxL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
@@ -53,7 +53,7 @@ export interface IManagedNftTransition<TPolicy extends TManagedNftPolicy = IMana
53
53
  }
54
54
  export interface IManagedNftStatus<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
55
55
  identity: IManagedNftIdentity;
56
- state: 'idle' | 'applied' | 'failed-owned' | 'released';
56
+ state: 'idle' | 'applied' | 'failed-owned' | 'released' | 'retained';
57
57
  enforced: boolean;
58
58
  applied: IAppliedManagedNftPolicy<TPolicy> | null;
59
59
  pending: IManagedNftTransition<TPolicy> | null;
@@ -96,6 +96,12 @@ export type TManagedNftCommands<TPolicy extends TManagedNftPolicy = IManagedNftP
96
96
  released: true;
97
97
  };
98
98
  };
99
+ detachPolicy: {
100
+ params: IAppliedManagedNftPolicy<TPolicy>;
101
+ result: {
102
+ detached: true;
103
+ };
104
+ };
99
105
  closeOwner: {
100
106
  params: Record<string, never>;
101
107
  result: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartnftables",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "private": false,
5
5
  "description": "A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.",
6
6
  "main": "dist_ts/index.js",
package/readme.md CHANGED
@@ -113,7 +113,8 @@ grants remain outstanding.
113
113
  | `reconcile({ previous, target })` | Creates or replaces the entire policy in one kernel transaction. `previous` is null initially, otherwise the full prior applied result. |
114
114
  | `inspect()` | Reads the current owned graph and reports `enforced`, pending intent and a bounded error code. |
115
115
  | `release(applied)` | Verifies the exact retained graph and confirms deletion; repeats are idempotent. |
116
- | `close()` | Stops command admission, joins admitted policy operations, confirms owned-table deletion, then joins the native process. Failure retains the owner for explicit recovery. |
116
+ | `detach(applied)` | Selects terminal retention, verifies the same graph with PERSIST-only flags after dropping socket ownership, then joins the native process. Returns `{ detached: true }`; retain the full applied result. |
117
+ | `close()` | Stops command admission, joins admitted policy operations, confirms owned-table deletion unless `detach()` selected retention, then joins the native process. Failure retains the owner for explicit recovery. |
117
118
 
118
119
  Schema-v1 policies are directed. Return traffic requires its own grant; there is no broad
119
120
  connection-tracking bypass. A null endpoint denotes the actual local host, with an
@@ -154,6 +155,33 @@ invalidates the old boot receipt; the caller must fence its old link/workload
154
155
  lifecycle before starting a new owner instance. Cleanup failures and uncertain
155
156
  operations must not be reported as successful release or used to transfer authority.
156
157
 
158
+ For an orderly restart that must preserve policy, persist the complete applied
159
+ result and call `await owner.detach(applied)` before calling `close()`. The native
160
+ operation validates the exact receipt, graph, interfaces, boot and namespace. It
161
+ accepts this socket's OWNER/PERSIST table or an exact orphan; it never adopts a
162
+ foreign owner. After closing the owner socket it reads the graph again and requires
163
+ the same handle and compiled body with PERSIST-only flags. Native inspection then
164
+ reports `state: 'retained'`. The facade returns success only after child termination.
165
+
166
+ Calling `detach()` irreversibly selects retention cleanup, including invalid input,
167
+ transport failure, timeout, native rejection or malformed acknowledgement. Subsequent
168
+ `close()` only joins and terminates; it never sends a table-deletion command. It
169
+ cannot undo a `close()` already admitted earlier. Other policy operations reject.
170
+ Resolve any pending policy transition by exact replay before requesting detachment.
171
+ If a detach ACK is lost while the process remains available, only the same captured
172
+ body may be retried. After process loss, start a fresh owner with identical options
173
+ and retry `detach(applied)` against the exact orphan. Absent graphs, foreign active
174
+ owners, changed interfaces, handles, bodies or boot/namespace identities reject.
175
+ Native release/reconcile/close commands reject after a valid detach intent, even
176
+ when post-drop verification fails; explicit recovery requires the retained body.
177
+
178
+ Detachment confirms retention at observation time. A completed facade retry returns
179
+ its existing acknowledgement; it is not a fresh kernel inspection. Retention does
180
+ not guarantee future enforcement, namespace survival or reboot ordering, and does
181
+ not withdraw external authority, drain packets, clear conntrack or release leases.
182
+ A separately created owner can later recover the full policy and perform an
183
+ explicit release after the caller has fenced that authority and packet lifecycle.
184
+
157
185
  Apply and release receipts describe this owner's table and policy operations.
158
186
  They do not certify that packets admitted under an earlier graph have drained:
159
187
  foreign NFQUEUE or other deferred packet owners may still hold such packets.
@@ -201,7 +229,8 @@ kernel reboot. It does not fence foreign packet queues, later packet rewrites,
201
229
  offloads or proxies, and supplies no quarantine release, conntrack drainage or
202
230
  allocation-reuse evidence. Historical leases remain the caller's durable ownership
203
231
  responsibility. PERSIST retains this table only while its network namespace exists;
204
- `close()` still deletes it, so revoke any external protection claim before release.
232
+ `release()` and ordinary `close()` delete it, so revoke any external protection
233
+ claim before release, or use the verified retention operation described above.
205
234
 
206
235
  Local bindings include name, index, kind, MAC (null for L3 TUN), interface-link
207
236
  index, and required IPv4 addresses. RTM_GETLINK/RTM_GETADDR verify those local
package/rust/src/main.rs CHANGED
@@ -91,6 +91,10 @@ fn dispatch(owner: &mut Option<owner::Owner>, request: Request) -> Result<serde_
91
91
  .release(decode(request.params)?)?;
92
92
  Ok(serde_json::json!({"released":true}))
93
93
  }
94
+ "detachPolicy" => {
95
+ owner.as_mut().ok_or(Error::Conflict)?.detach(decode(request.params)?)?;
96
+ Ok(serde_json::json!({"detached":true}))
97
+ }
94
98
  "closeOwner" => {
95
99
  if decode::<serde_json::Value>(request.params)? != serde_json::json!({}) {
96
100
  return Err(Error::Invalid);
@@ -0,0 +1,73 @@
1
+ use super::*;
2
+
3
+ impl Owner {
4
+ /// Terminal retention, never adoption/deletion. Exact retry may observe an
5
+ /// orphan after an ACK was lost, including through a fresh native process.
6
+ pub fn detach(&mut self, expected: Applied) -> Result<()> {
7
+ self.validate_receipt(&expected)?;
8
+ if self.released || self.pending.is_some() {
9
+ return Err(Error::Conflict);
10
+ }
11
+ if let Some(retention) = &self.retention {
12
+ if retention.expected != expected {
13
+ return Err(Error::Conflict);
14
+ }
15
+ } else {
16
+ self.retention = Some(Retention {
17
+ expected: expected.clone(),
18
+ confirmed: false,
19
+ });
20
+ }
21
+ let result = (|| {
22
+ self.verify_retention(&expected, false)?;
23
+ // OWNER disappears with this socket; PERSIST must retain exactly
24
+ // the same table. The fresh verification socket never adopts it.
25
+ self.socket = None;
26
+ self.verify_retention(&expected, true)?;
27
+ self.socket = None;
28
+ Ok(())
29
+ })();
30
+ self.retention.as_mut().unwrap().confirmed = result.is_ok();
31
+ match result {
32
+ Ok(()) => {
33
+ self.applied = Some(expected);
34
+ self.error = None;
35
+ Ok(())
36
+ }
37
+ Err(error) => {
38
+ self.error = Some(error);
39
+ Err(error)
40
+ }
41
+ }
42
+ }
43
+
44
+ fn verify_retention(&mut self, expected: &Applied, orphan_only: bool) -> Result<()> {
45
+ let graph = self.graph()?.ok_or(Error::Conflict)?;
46
+ self.binding(&graph, Some(expected))?;
47
+ let flags = wire::number(&graph.table, 2)?;
48
+ if (orphan_only && flags != 4)
49
+ || (flags == 6 && wire::number(&graph.table, 7)? != self.socket()?.port)
50
+ || !self.matches(&graph, &expected.prepared)?
51
+ {
52
+ return Err(Error::Conflict);
53
+ }
54
+ expected.prepared.validate_interfaces()
55
+ }
56
+
57
+ pub(super) fn inspect_retained(&mut self) -> Status {
58
+ let expected = self.retention.as_ref().unwrap().expected.clone();
59
+ if let Err(error) = self.verify_retention(&expected, true) {
60
+ self.retention.as_mut().unwrap().confirmed = false;
61
+ self.error = Some(error);
62
+ }
63
+ let enforced = self.retention.as_ref().unwrap().confirmed && self.error.is_none();
64
+ Status {
65
+ identity: self.identity.clone(),
66
+ state: if enforced { "retained" } else { "failed-owned" }.into(),
67
+ enforced,
68
+ applied: self.applied.clone(),
69
+ pending: self.pending.clone(),
70
+ error: self.error.map(|error| error.code().into()),
71
+ }
72
+ }
73
+ }
package/rust/src/owner.rs CHANGED
@@ -68,8 +68,17 @@ pub struct Owner {
68
68
  pending: Option<Transition>,
69
69
  error: Option<Error>,
70
70
  released: bool,
71
+ retention: Option<Retention>,
71
72
  }
72
73
 
74
+ struct Retention {
75
+ expected: Applied,
76
+ confirmed: bool,
77
+ }
78
+
79
+ #[path = "owner.detach.rs"]
80
+ mod detach;
81
+
73
82
  pub(crate) fn context() -> Result<(String, String, String)> {
74
83
  let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
75
84
  .map_err(|_| Error::Unavailable)?
@@ -154,6 +163,7 @@ impl Owner {
154
163
  pending: None,
155
164
  error: None,
156
165
  released: false,
166
+ retention: None,
157
167
  })
158
168
  }
159
169
  fn socket(&mut self) -> Result<&mut Socket> {
@@ -350,7 +360,7 @@ impl Owner {
350
360
  })
351
361
  }
352
362
  pub fn reconcile(&mut self, transition: Transition) -> Result<Applied> {
353
- if self.released {
363
+ if self.released || self.retention.is_some() {
354
364
  return Err(Error::Conflict);
355
365
  }
356
366
  transition.target.validate()?;
@@ -453,6 +463,9 @@ impl Owner {
453
463
  self.applied(&graph, transition.target.clone())
454
464
  }
455
465
  pub fn inspect(&mut self) -> Status {
466
+ if self.retention.is_some() {
467
+ return self.inspect_retained();
468
+ }
456
469
  let mut enforced = false;
457
470
  if let Some(applied) = self.applied.clone().filter(|_| !self.released) {
458
471
  match self.graph().and_then(|graph| {
@@ -490,6 +503,9 @@ impl Owner {
490
503
  }
491
504
  }
492
505
  pub fn release(&mut self, expected: Applied) -> Result<()> {
506
+ if self.retention.is_some() {
507
+ return Err(Error::Conflict);
508
+ }
493
509
  self.validate_receipt(&expected)?;
494
510
  if self.pending.is_some() {
495
511
  return Err(Error::Conflict);
@@ -545,6 +561,9 @@ impl Owner {
545
561
  }
546
562
  }
547
563
  pub fn close(&mut self) -> Result<()> {
564
+ if self.retention.is_some() {
565
+ return Err(Error::Conflict);
566
+ }
548
567
  if self.released {
549
568
  return Ok(());
550
569
  }
@@ -0,0 +1,252 @@
1
+ use super::namespace_tests::NativeOwner;
2
+ use super::*;
3
+ use crate::egress;
4
+ use serde_json::json;
5
+ use std::io::Write;
6
+ use std::time::{Duration, Instant};
7
+
8
+ fn pool_target(revision: u64) -> Prepared {
9
+ let mut policy = egress::tests::pool_guard();
10
+ policy["revision"] = json!(revision);
11
+ serde_json::from_value::<egress::Policy>(policy)
12
+ .unwrap()
13
+ .prepare()
14
+ .unwrap()
15
+ .into()
16
+ }
17
+ fn apply(name: &str) -> (Owner, Applied) {
18
+ let mut owner = Owner::new(options(name)).unwrap();
19
+ let applied = owner
20
+ .reconcile(Transition {
21
+ previous: None,
22
+ target: pool_target(1),
23
+ })
24
+ .unwrap();
25
+ (owner, applied)
26
+ }
27
+ fn retained(owner: &mut Owner, applied: &Applied) {
28
+ let status = owner.inspect();
29
+ assert_eq!(status.state, "retained");
30
+ assert!(status.enforced);
31
+ assert_eq!(status.applied, Some(applied.clone()));
32
+ let graph = owner.graph().unwrap().unwrap();
33
+ assert_eq!(wire::number(&graph.table, 2).unwrap(), 4);
34
+ assert_eq!(
35
+ wire::handle(&graph.table, 4).unwrap().to_string(),
36
+ applied.receipt.table_handle
37
+ );
38
+ assert!(owner.matches(&graph, &applied.prepared).unwrap());
39
+ }
40
+
41
+ #[test]
42
+ #[ignore = "requires disposable isolated native qualification guest"]
43
+ fn detach_retains_exact_graph_is_terminal_and_replays_without_adoption() {
44
+ isolated();
45
+ let (mut owner, applied) = apply("detach_replay");
46
+ owner.detach(applied.clone()).unwrap();
47
+ retained(&mut owner, &applied);
48
+ owner.detach(applied.clone()).unwrap();
49
+ assert_eq!(owner.close(), Err(Error::Conflict));
50
+ assert_eq!(owner.release(applied.clone()), Err(Error::Conflict));
51
+ assert_eq!(
52
+ owner.reconcile(Transition {
53
+ previous: Some(applied.clone()),
54
+ target: pool_target(2)
55
+ }),
56
+ Err(Error::Conflict)
57
+ );
58
+ let mut different = applied.clone();
59
+ different.prepared = pool_target(2);
60
+ different.receipt.revision = 2;
61
+ different.receipt.digest = different.prepared.digest.clone();
62
+ assert_eq!(owner.detach(different), Err(Error::Conflict));
63
+ retained(&mut owner, &applied);
64
+ drop(owner);
65
+ let mut fresh = Owner::new(options("detach_replay")).unwrap();
66
+ fresh.detach(applied.clone()).unwrap();
67
+ retained(&mut fresh, &applied);
68
+ // A separate active owner invalidates retention inspection. It is never
69
+ // detached by this reader, even with the same full logical identity.
70
+ let mut active = Owner::new(options("detach_replay")).unwrap();
71
+ active
72
+ .reconcile(Transition {
73
+ previous: None,
74
+ target: applied.prepared.clone(),
75
+ })
76
+ .unwrap();
77
+ assert_eq!(fresh.detach(applied.clone()), Err(Error::Conflict));
78
+ assert!(!fresh.inspect().enforced);
79
+ assert!(active.inspect().enforced);
80
+ assert_eq!(fresh.close(), Err(Error::Conflict));
81
+ drop(active);
82
+ fresh.detach(applied.clone()).unwrap();
83
+ retained(&mut fresh, &applied);
84
+ drop(fresh);
85
+ Owner::new(options("detach_replay"))
86
+ .unwrap()
87
+ .release(applied)
88
+ .unwrap();
89
+ }
90
+
91
+ #[test]
92
+ #[ignore = "requires disposable isolated native qualification guest"]
93
+ fn detach_rejects_pending_wrong_receipts_foreign_mutation_and_absence() {
94
+ isolated();
95
+ let (mut owner, applied) = apply("detach_faults");
96
+ let pending = Transition {
97
+ previous: Some(applied.clone()),
98
+ target: pool_target(2),
99
+ };
100
+ owner.pending = Some(pending.clone());
101
+ assert_eq!(owner.detach(applied.clone()), Err(Error::Conflict));
102
+ assert!(owner.retention.is_none());
103
+ let applied = owner.reconcile(pending).unwrap();
104
+ for field in ["boot", "namespace", "handle", "digest"] {
105
+ let mut invalid = applied.clone();
106
+ match field {
107
+ "boot" => {
108
+ invalid.receipt.identity.boot_id = "00000000-0000-0000-0000-000000000000".into()
109
+ }
110
+ "namespace" => invalid.receipt.identity.namespace_inode = "0".into(),
111
+ "handle" => invalid.receipt.table_handle = "18446744073709551615".into(),
112
+ _ => invalid.receipt.digest = format!("sha256:{}", "0".repeat(64)),
113
+ }
114
+ let mut reader = Owner::new(options("detach_faults")).unwrap();
115
+ assert_eq!(reader.detach(invalid), Err(Error::Conflict));
116
+ assert!(owner.inspect().enforced);
117
+ }
118
+ owner.detach(applied.clone()).unwrap();
119
+ // Mutation after detachment is observed on exact retry and permanently
120
+ // retains the failed owner. No failure cleanup adopts or deletes the graph.
121
+ let mut foreign = Socket::open().unwrap();
122
+ let generation = foreign.generation().unwrap();
123
+ foreign
124
+ .batch(
125
+ generation,
126
+ vec![(
127
+ 3,
128
+ 0x600,
129
+ vec![
130
+ Attr::string(1, "snft_detach_faults"),
131
+ Attr::string(3, "foreign"),
132
+ ],
133
+ )],
134
+ )
135
+ .unwrap();
136
+ assert_eq!(owner.detach(applied.clone()), Err(Error::Conflict));
137
+ assert!(!owner.inspect().enforced);
138
+ assert_eq!(owner.close(), Err(Error::Conflict));
139
+ assert_eq!(owner.release(applied.clone()), Err(Error::Conflict));
140
+ assert!(owner
141
+ .graph()
142
+ .unwrap()
143
+ .unwrap()
144
+ .chains
145
+ .iter()
146
+ .any(|chain| wire::text(chain, 3).unwrap() == "foreign"));
147
+ drop(owner);
148
+ // This disposable fixture's foreign owner deletes only its exact orphan.
149
+ let generation = foreign.generation().unwrap();
150
+ foreign
151
+ .batch(
152
+ generation,
153
+ vec![(2, 0, vec![Attr::string(1, "snft_detach_faults")])],
154
+ )
155
+ .unwrap();
156
+ let mut absent = Owner::new(options("detach_faults")).unwrap();
157
+ assert_eq!(absent.detach(applied), Err(Error::Conflict));
158
+ assert!(!absent.inspect().enforced);
159
+ assert_eq!(absent.close(), Err(Error::Conflict));
160
+ assert!(absent.graph().unwrap().is_none());
161
+ }
162
+
163
+ #[test]
164
+ #[ignore = "requires disposable isolated native qualification guest"]
165
+ fn detach_rechecks_interface_bindings_without_releasing_on_drift() {
166
+ isolated();
167
+ let endpoint = endpoint("a", 71);
168
+ let policy = crate::policy::Policy {
169
+ schema_version: 1,
170
+ revision: 1,
171
+ endpoints: vec![endpoint],
172
+ rules: vec![],
173
+ };
174
+ let mut owner = Owner::new(options("detach_links")).unwrap();
175
+ let applied = owner
176
+ .reconcile(Transition {
177
+ previous: None,
178
+ target: policy.prepare().unwrap().into(),
179
+ })
180
+ .unwrap();
181
+ ip(&["link", "set", "worka", "name", "renameda"]);
182
+ assert_eq!(owner.detach(applied.clone()), Err(Error::Conflict));
183
+ assert_eq!(owner.close(), Err(Error::Conflict));
184
+ let graph = owner.graph().unwrap().unwrap();
185
+ assert_eq!(
186
+ wire::handle(&graph.table, 4).unwrap().to_string(),
187
+ applied.receipt.table_handle
188
+ );
189
+ ip(&["link", "set", "renameda", "name", "worka"]);
190
+ owner.detach(applied.clone()).unwrap();
191
+ retained(&mut owner, &applied);
192
+ drop(owner);
193
+ Owner::new(options("detach_links"))
194
+ .unwrap()
195
+ .release(applied)
196
+ .unwrap();
197
+ }
198
+
199
+ #[test]
200
+ #[ignore = "requires disposable isolated native qualification guest"]
201
+ fn detach_recovers_lost_response_and_undelivered_request_across_real_native_processes() {
202
+ isolated();
203
+ let namespace = std::fs::File::open("/proc/thread-self/ns/net").unwrap();
204
+ for delivered in [true, false] {
205
+ let name = if delivered {
206
+ "detach_ack"
207
+ } else {
208
+ "detach_unsent"
209
+ };
210
+ let mut native = NativeOwner::start(&namespace);
211
+ native.call("openOwner", serde_json::to_value(options(name)).unwrap());
212
+ let applied: Applied = serde_json::from_value(native.call(
213
+ "reconcilePolicy",
214
+ json!({"previous":null,"target":pool_target(1)}),
215
+ ))
216
+ .unwrap();
217
+ let mut reader = Owner::new(options(name)).unwrap();
218
+ reader.validate_receipt(&applied).unwrap();
219
+ if delivered {
220
+ writeln!(
221
+ native.child.stdin.as_mut().unwrap(),
222
+ "{}",
223
+ json!({"id":"lost-detach","method":"detachPolicy","params":applied})
224
+ )
225
+ .unwrap();
226
+ let until = Instant::now() + Duration::from_secs(3);
227
+ loop {
228
+ let graph = reader.graph().unwrap().unwrap();
229
+ if wire::number(&graph.table, 2).unwrap() == 4 {
230
+ break;
231
+ }
232
+ assert!(Instant::now() < until);
233
+ std::thread::sleep(Duration::from_millis(5));
234
+ }
235
+ // Do not read the detach response: terminate the actual process
236
+ // after observing its kernel effect but before recording an ACK.
237
+ }
238
+ drop(native);
239
+ let mut fresh = NativeOwner::start(&namespace);
240
+ fresh.call("openOwner", serde_json::to_value(options(name)).unwrap());
241
+ assert_eq!(
242
+ fresh.call("detachPolicy", serde_json::to_value(&applied).unwrap()),
243
+ json!({"detached":true})
244
+ );
245
+ assert_eq!(fresh.call("inspectPolicy", json!({}))["state"], "retained");
246
+ drop(fresh);
247
+ let graph = reader.graph().unwrap().unwrap();
248
+ assert_eq!(wire::number(&graph.table, 2).unwrap(), 4);
249
+ assert!(reader.matches(&graph, &applied.prepared).unwrap());
250
+ reader.release(applied).unwrap();
251
+ }
252
+ }
@@ -30,13 +30,13 @@ fn command(namespace: &File) -> Command {
30
30
  command
31
31
  }
32
32
 
33
- struct NativeOwner {
34
- child: Child,
33
+ pub(super) struct NativeOwner {
34
+ pub(super) child: Child,
35
35
  messages: Receiver<Value>,
36
36
  reader: Option<JoinHandle<()>>,
37
37
  }
38
38
  impl NativeOwner {
39
- fn start(namespace: &File) -> Self {
39
+ pub(super) fn start(namespace: &File) -> Self {
40
40
  let mut child = command(namespace).spawn().unwrap();
41
41
  let output = child.stdout.take().unwrap();
42
42
  let (send, messages) = mpsc::channel();
@@ -76,7 +76,7 @@ impl NativeOwner {
76
76
  assert_eq!(response["id"], method);
77
77
  response
78
78
  }
79
- fn call(&mut self, method: &str, params: Value) -> Value {
79
+ pub(super) fn call(&mut self, method: &str, params: Value) -> Value {
80
80
  let response = self.response(method, params);
81
81
  assert_eq!(response["success"], true, "{response}");
82
82
  response["result"].clone()
@@ -234,6 +234,8 @@ fn allocation_pool_guard_covers_all_host_hooks_dnat_untracked_and_reply_directio
234
234
  })
235
235
  .unwrap();
236
236
  assert!(owner.inspect().enforced);
237
+ owner.detach(applied.clone()).unwrap();
238
+ assert_eq!(owner.inspect().state, "retained");
237
239
  (owner, applied)
238
240
  });
239
241
  for ((destination, _, _), target) in cases.iter().zip(&targets) {
@@ -254,7 +256,8 @@ fn allocation_pool_guard_covers_all_host_hooks_dnat_untracked_and_reply_directio
254
256
  in_namespace(HOST, move || {
255
257
  let mut owner = owner;
256
258
  assert!(owner.inspect().enforced);
257
- owner.release(applied).unwrap();
259
+ drop(owner);
260
+ Owner::new(options("pool_traffic")).unwrap().release(applied).unwrap();
258
261
  });
259
262
  // Positive control after release proves the denials were this guard.
260
263
  for ((destination, _, _), target) in cases.iter().zip(&targets) {
@@ -24,6 +24,9 @@ mod host_traffic_tests;
24
24
  #[path = "owner_poolguard_tests.rs"]
25
25
  mod poolguard_tests;
26
26
 
27
+ #[path = "owner_detach_tests.rs"]
28
+ mod detach_tests;
29
+
27
30
  #[path = "owner_coexistence_tests.rs"]
28
31
  mod coexistence_tests;
29
32
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '1.7.0',
6
+ version: '1.8.0',
7
7
  description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
8
8
  }
@@ -50,6 +50,9 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
50
50
  #work: Promise<unknown> | null = null;
51
51
  #close: Promise<void> | null = null;
52
52
  #nativeMayOwn = false;
53
+ #retaining = false;
54
+ #detachExpected: IAppliedManagedNftPolicy<TPolicy> | null = null;
55
+ #detach: Promise<{ detached: true }> | null = null;
53
56
 
54
57
  constructor(options: IManagedNftOptions) {
55
58
  this.#options = snapshot(options);
@@ -68,7 +71,7 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
68
71
 
69
72
  /** Starts only the compiler/IPC owner. The returned idle status is not enforcement. */
70
73
  public start(): Promise<IManagedNftStatus<TPolicy>> {
71
- if (this.#state !== 'new') return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
74
+ if (this.#state !== 'new' || this.#retaining) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
72
75
  this.#state = 'starting';
73
76
  const work = (async () => {
74
77
  if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch)) throw new ManagedNftablesError('UNSUPPORTED_PLATFORM');
@@ -80,7 +83,7 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
80
83
  cliArgs: ['--management', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
81
84
  inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
82
85
  readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
83
- maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, closeOwner: 1 },
86
+ maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, detachPolicy: 1, closeOwner: 1 },
84
87
  });
85
88
  this.#bridge.on('exit', () => {
86
89
  if (!['closing', 'stopped'].includes(this.#state)) this.#state = 'failed-owned';
@@ -105,15 +108,42 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
105
108
  return this.#invoke('releasePolicy', expected);
106
109
  }
107
110
 
108
- /** Join policy operations and owned-table deletion, then native termination; packet/allocation fencing belongs to the caller. */
111
+ /** Permanently selects retention cleanup, verifies exact PERSIST retention, and
112
+ * joins the native process. Retain the full applied value before this call.
113
+ * After any failure close() only terminates; it never deletes the table. */
114
+ public detach(expected: IAppliedManagedNftPolicy<TPolicy>): Promise<{ detached: true }> {
115
+ this.#retaining = true;
116
+ let captured: IAppliedManagedNftPolicy<TPolicy>;
117
+ try {
118
+ captured = snapshot(expected);
119
+ if (this.#detachExpected && !plugins.util.isDeepStrictEqual(this.#detachExpected, captured)) throw new ManagedNftablesError('CONFLICT');
120
+ if (this.#detach) return this.#detach;
121
+ if (!['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work || this.#close) throw new ManagedNftablesError('UNAVAILABLE');
122
+ this.#detachExpected = captured;
123
+ } catch (error) { return Promise.reject(error instanceof ManagedNftablesError ? error : new ManagedNftablesError('INVALID')); }
124
+ const work = this.#track(this.#bridge.sendCommand('detachPolicy', captured).then((value) => {
125
+ const result = snapshot(value);
126
+ if (result?.detached !== true || Object.keys(result).length !== 1) throw new ManagedNftablesError('PROTOCOL');
127
+ this.#nativeMayOwn = false;
128
+ return result;
129
+ }));
130
+ const detached = (async () => { const result = await work; await this.close(); return result; })();
131
+ this.#detach = detached;
132
+ void detached.catch(() => { this.#detach = null; });
133
+ return detached;
134
+ }
135
+
136
+ /** Join policy operations, then delete and terminate unless detach selected retention.
137
+ * Packet/allocation fencing belongs to the caller; already-admitted close cannot be undone. */
109
138
  public close(): Promise<void> {
110
139
  if (this.#close) return this.#close;
111
140
  if (this.#state === 'stopped') return Promise.resolve();
141
+ const retaining = this.#retaining;
112
142
  this.#state = 'closing';
113
143
  this.#close = (async () => {
114
144
  await this.#work?.catch(() => undefined);
115
145
  if (this.#bridge) {
116
- if (this.#nativeMayOwn) {
146
+ if (this.#nativeMayOwn && !retaining) {
117
147
  const result = await this.#bridge.sendCommand('closeOwner', {});
118
148
  if (result.released !== true) throw new ManagedNftablesError('PROTOCOL');
119
149
  }
@@ -128,7 +158,7 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
128
158
  }
129
159
 
130
160
  #invoke<K extends keyof TManagedNftCommands<TPolicy>>(method: K, params: TManagedNftCommands<TPolicy>[K]['params']): Promise<TManagedNftCommands<TPolicy>[K]['result']> {
131
- if (!['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
161
+ if (this.#retaining || !['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
132
162
  let captured: TManagedNftCommands<TPolicy>[K]['params'];
133
163
  try { captured = snapshot(params); } catch { return Promise.reject(new ManagedNftablesError('INVALID')); }
134
164
  if (method === 'reconcilePolicy' || method === 'releasePolicy') this.#nativeMayOwn = true;
@@ -62,7 +62,7 @@ export interface IManagedNftTransition<TPolicy extends TManagedNftPolicy = IMana
62
62
 
63
63
  export interface IManagedNftStatus<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
64
64
  identity: IManagedNftIdentity;
65
- state: 'idle' | 'applied' | 'failed-owned' | 'released';
65
+ state: 'idle' | 'applied' | 'failed-owned' | 'released' | 'retained';
66
66
  enforced: boolean;
67
67
  applied: IAppliedManagedNftPolicy<TPolicy> | null;
68
68
  pending: IManagedNftTransition<TPolicy> | null;
@@ -91,5 +91,6 @@ export type TManagedNftCommands<TPolicy extends TManagedNftPolicy = IManagedNftP
91
91
  reconcilePolicy: { params: IManagedNftTransition<TPolicy>; result: IAppliedManagedNftPolicy<TPolicy> };
92
92
  inspectPolicy: { params: Record<string, never>; result: IManagedNftStatus<TPolicy> };
93
93
  releasePolicy: { params: IAppliedManagedNftPolicy<TPolicy>; result: { released: true } };
94
+ detachPolicy: { params: IAppliedManagedNftPolicy<TPolicy>; result: { detached: true } };
94
95
  closeOwner: { params: Record<string, never>; result: { released: true } };
95
96
  };