@push.rocks/smartnftables 2.1.0 → 2.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.
@@ -1,6 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IAppliedManagedNftPolicy,
3
- IPreparedManagedNftPolicy, IManagedNftStatus, TManagedNftCommands, TManagedNftPolicy } from './managed.types.js';
3
+ IPreparedManagedNftPolicy, IManagedNftStatus, IManagedNftTransitionRelease, TManagedNftCommands, TManagedNftPolicy } from './managed.types.js';
4
4
 
5
5
  export class ManagedNftablesError extends Error {
6
6
  constructor(public readonly code: string) { super(`Managed nftables ${code}.`); }
@@ -55,6 +55,7 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
55
55
  #retaining = false;
56
56
  #detachExpected: IAppliedManagedNftPolicy<TPolicy> | null = null;
57
57
  #detach: Promise<{ detached: true }> | null = null;
58
+ #transitionRelease: { request: IManagedNftTransitionRelease<TPolicy> } | null = null;
58
59
  readonly #failure = new AbortController();
59
60
  #processLost = false;
60
61
 
@@ -97,7 +98,7 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
97
98
  cliArgs: ['--management', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
98
99
  inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
99
100
  readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
100
- maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, detachPolicy: 1, closeOwner: 1 },
101
+ maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, releasePolicyTransition: 1, detachPolicy: 1, closeOwner: 1 },
101
102
  });
102
103
  this.#bridge.on('exit', () => {
103
104
  this.#processLost = true;
@@ -123,11 +124,20 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
123
124
  return this.#invoke('releasePolicy', expected);
124
125
  }
125
126
 
127
+ /** Terminal release when an apply acknowledgement was lost. Retain the full
128
+ * original identity and transition first. The native owner accepts only an
129
+ * absent table or the exact previous/target graph; it never recreates policy
130
+ * or requires surviving interfaces. Exact retries and inspection remain
131
+ * available; close() retries admitted cleanup, closeRetaining() only joins. */
132
+ public releaseTransition(request: IManagedNftTransitionRelease<TPolicy>): Promise<{ released: true }> {
133
+ return this.#invoke('releasePolicyTransition', request);
134
+ }
135
+
126
136
  /** Permanently selects retention cleanup, verifies exact PERSIST retention, and
127
137
  * joins the native process. Retain the full applied value before this call.
128
138
  * After any failure close() only terminates; it never deletes the table. */
129
139
  public detach(expected: IAppliedManagedNftPolicy<TPolicy>): Promise<{ detached: true }> {
130
- if (this.#closeRetainingSelected || this.#closeMode === 'delete') return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
140
+ if (this.#closeRetainingSelected || this.#closeMode === 'delete' || this.#transitionRelease) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
131
141
  this.#retaining = true;
132
142
  let captured: IAppliedManagedNftPolicy<TPolicy>;
133
143
  try {
@@ -173,8 +183,13 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
173
183
  await this.#work?.catch(() => undefined);
174
184
  if (this.#bridge) {
175
185
  if (this.#nativeMayOwn && !retaining) {
176
- const result = await this.#bridge.sendCommand('closeOwner', {});
177
- if (result.released !== true) throw new ManagedNftablesError('PROTOCOL');
186
+ // Even native validation may have rejected before selecting its own
187
+ // terminal intent. Retry this exact request; generic close must not
188
+ // delete an earlier applied graph outside the requested transition.
189
+ const result = this.#transitionRelease
190
+ ? await this.#bridge.sendCommand('releasePolicyTransition', this.#transitionRelease.request)
191
+ : await this.#bridge.sendCommand('closeOwner', {});
192
+ if (result.released !== true || this.#transitionRelease && Object.keys(result).length !== 1) throw new ManagedNftablesError('PROTOCOL');
178
193
  }
179
194
  await this.#bridge.terminate({ gracePeriodMs: 1000 });
180
195
  }
@@ -191,15 +206,24 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
191
206
  if (this.#retaining || !['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
192
207
  let captured: TManagedNftCommands<TPolicy>[K]['params'];
193
208
  try { captured = snapshot(params); } catch { return Promise.reject(new ManagedNftablesError('INVALID')); }
194
- if (method === 'reconcilePolicy' || method === 'releasePolicy') this.#nativeMayOwn = true;
209
+ if (this.#transitionRelease && (method !== 'inspectPolicy' && method !== 'releasePolicyTransition'
210
+ || method === 'releasePolicyTransition' && !plugins.util.isDeepStrictEqual(this.#transitionRelease.request, captured))) {
211
+ return Promise.reject(new ManagedNftablesError('CONFLICT'));
212
+ }
213
+ if (method === 'releasePolicyTransition') this.#transitionRelease = { request: captured as IManagedNftTransitionRelease<TPolicy> };
214
+ if (method === 'reconcilePolicy' || method === 'releasePolicy' || method === 'releasePolicyTransition') this.#nativeMayOwn = true;
195
215
  return this.#track(this.#bridge.sendCommand(method, captured).then((result) => {
196
216
  const capturedResult = snapshot(result);
197
- if (method === 'releasePolicy') this.#nativeMayOwn = false;
217
+ if (method === 'releasePolicy' || method === 'releasePolicyTransition') {
218
+ if (method === 'releasePolicyTransition' && ((capturedResult as { released: true })?.released !== true
219
+ || Object.keys(capturedResult).length !== 1)) throw new ManagedNftablesError('PROTOCOL');
220
+ this.#nativeMayOwn = false;
221
+ }
198
222
  if (method === 'inspectPolicy') {
199
223
  const status = capturedResult as IManagedNftStatus<TPolicy>;
200
224
  if (status.state === 'failed-owned' || status.error !== null) this.#fail();
201
225
  }
202
- if (!this.#processLost && this.#state !== 'closing' && (method === 'reconcilePolicy' || method === 'releasePolicy'
226
+ if (!this.#processLost && this.#state !== 'closing' && (method === 'reconcilePolicy' || method === 'releasePolicy' || method === 'releasePolicyTransition'
203
227
  || method === 'inspectPolicy' && (capturedResult as IManagedNftStatus<TPolicy>).error === null
204
228
  && (capturedResult as IManagedNftStatus<TPolicy>).state !== 'failed-owned')) this.#state = 'ready';
205
229
  return capturedResult;
@@ -216,7 +240,7 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
216
240
  && error.code === 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE'
217
241
  && (method === 'preparePolicy' || code === 'INVALID');
218
242
  if (!validation && (this.#state !== 'closing'
219
- || ['reconcilePolicy', 'releasePolicy', 'detachPolicy'].includes(method))) this.#fail();
243
+ || ['reconcilePolicy', 'releasePolicy', 'releasePolicyTransition', 'detachPolicy'].includes(method))) this.#fail();
220
244
  throw new ManagedNftablesError(typeof code === 'string' && /^[A-Z_]{1,64}$/.test(code) ? code : 'UNAVAILABLE');
221
245
  });
222
246
  this.#work = owned;
@@ -68,6 +68,18 @@ export interface IManagedNftRouterEgressScopeV2 {
68
68
  generations: IManagedNftEgressGenerationV2[];
69
69
  }
70
70
 
71
+ /** One inbound uplink publication, compiled inside the same host-transit generation. */
72
+ export interface IManagedNftPublishedPortV2 {
73
+ protocol: 'tcp' | 'udp';
74
+ /** Published uplink port, 1–65535 and unique per protocol across the scope. */
75
+ hostPort: number;
76
+ targetPort: number;
77
+ /** Exact leased transit source address of one current allocation; it selects the handoff. */
78
+ targetAddress: string;
79
+ /** Exact current uplink address that publishes this port. No wildcard or route inference. */
80
+ hostIp: string;
81
+ }
82
+
71
83
  /** Shared-host capture barrier and explicit outer SNAT; Docker forwarding remains a separate owner. */
72
84
  export interface IManagedNftHostTransitScopeV2 {
73
85
  kind: 'hostTransit';
@@ -76,6 +88,11 @@ export interface IManagedNftHostTransitScopeV2 {
76
88
  uplink: IManagedNftLocalLinkV2;
77
89
  /** Exact current address present on uplink. No masquerade or default-route inference. */
78
90
  snatAddress: string;
91
+ /** Optional inbound destination NAT. Absent and empty are the same canonical policy and
92
+ * keep the compiled graph unchanged. Every entry compiles its own uplink DNAT plus the
93
+ * exact forward and reply admission in this generation, so a failed apply, a target
94
+ * without it, or release removes it with the generation. */
95
+ publishedPorts?: IManagedNftPublishedPortV2[];
79
96
  }
80
97
 
81
98
  /** Host-wide IPv4 destination denial for caller-authenticated private allocation pools.
@@ -60,6 +60,14 @@ export interface IManagedNftTransition<TPolicy extends TManagedNftPolicy = IMana
60
60
  target: IPreparedManagedNftPolicy<TPolicy>;
61
61
  }
62
62
 
63
+ /** Persist the original boot/namespace identity and full transition before cleanup.
64
+ * This releases only its exact owned table; it does not prove packet drain or
65
+ * authorize address reuse. Surviving policy interfaces are not required. */
66
+ export interface IManagedNftTransitionRelease<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
67
+ identity: IManagedNftIdentity;
68
+ transition: IManagedNftTransition<TPolicy>;
69
+ }
70
+
63
71
  export interface IManagedNftStatus<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
64
72
  identity: IManagedNftIdentity;
65
73
  state: 'idle' | 'applied' | 'failed-owned' | 'released' | 'retained';
@@ -91,6 +99,7 @@ export type TManagedNftCommands<TPolicy extends TManagedNftPolicy = IManagedNftP
91
99
  reconcilePolicy: { params: IManagedNftTransition<TPolicy>; result: IAppliedManagedNftPolicy<TPolicy> };
92
100
  inspectPolicy: { params: Record<string, never>; result: IManagedNftStatus<TPolicy> };
93
101
  releasePolicy: { params: IAppliedManagedNftPolicy<TPolicy>; result: { released: true } };
102
+ releasePolicyTransition: { params: IManagedNftTransitionRelease<TPolicy>; result: { released: true } };
94
103
  detachPolicy: { params: IAppliedManagedNftPolicy<TPolicy>; result: { detached: true } };
95
104
  closeOwner: { params: Record<string, never>; result: { released: true } };
96
105
  };