@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.
- package/changelog.md +12 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.managednftables.d.ts +9 -1
- package/dist_ts/classes.managednftables.js +33 -9
- package/dist_ts/managed.egress.types.d.ts +16 -0
- package/dist_ts/managed.types.d.ts +13 -0
- package/package.json +3 -3
- package/readme.md +72 -1
- package/rust/src/egress.compile.rs +20 -0
- package/rust/src/egress.host.rs +73 -0
- package/rust/src/egress.rs +61 -0
- package/rust/src/egress_tests.rs +194 -1
- package/rust/src/main.rs +4 -0
- package/rust/src/owner.detach.rs +1 -1
- package/rust/src/owner.rs +18 -2
- package/rust/src/owner.transitionrelease.rs +107 -0
- package/rust/src/owner_host_traffic_tests.rs +224 -1
- package/rust/src/owner_identity_tests.rs +37 -0
- package/rust/src/owner_tests.rs +3 -0
- package/rust/src/owner_transitionrelease_tests.rs +327 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.managednftables.ts +33 -9
- package/ts/managed.egress.types.ts +17 -0
- package/ts/managed.types.ts +9 -0
|
@@ -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
|
-
|
|
177
|
-
|
|
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
|
|
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'
|
|
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.
|
package/ts/managed.types.ts
CHANGED
|
@@ -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
|
};
|