@push.rocks/smartnftables 1.3.1 → 1.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.
- package/changelog.md +24 -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 +7 -7
- package/dist_ts/classes.managednftables.js +9 -3
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +1 -1
- package/dist_ts/managed.egress.types.d.ts +91 -0
- package/dist_ts/managed.egress.types.js +2 -0
- package/dist_ts/managed.types.d.ts +25 -19
- package/package.json +5 -5
- package/readme.md +97 -3
- package/rust/src/egress.compile.rs +288 -0
- package/rust/src/egress.host.rs +103 -0
- package/rust/src/egress.router.rs +231 -0
- package/rust/src/egress.rs +682 -0
- package/rust/src/egress_tests.rs +332 -0
- package/rust/src/main.rs +23 -3
- package/rust/src/managed.rs +105 -0
- package/rust/src/namespace.rs +51 -0
- package/rust/src/owner.rs +96 -6
- package/rust/src/owner_coexistence_tests.rs +77 -0
- package/rust/src/owner_egress_identity_tests.rs +155 -0
- package/rust/src/owner_egress_tests.rs +153 -0
- package/rust/src/owner_egress_traffic_tests.rs +470 -0
- package/rust/src/owner_host_traffic_tests.rs +305 -0
- package/rust/src/owner_identity_tests.rs +169 -0
- package/rust/src/owner_link_tests.rs +119 -0
- package/rust/src/owner_namespace_tests.rs +242 -0
- package/rust/src/owner_packet_fixture.rs +200 -0
- package/rust/src/owner_tests.rs +36 -7
- package/rust/src/policy.rs +120 -81
- package/rust/src/tests.rs +22 -0
- package/rust/src/wire.links.rs +268 -0
- package/rust/src/wire.rs +3 -64
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.managednftables.ts +19 -14
- package/ts/index.ts +1 -0
- package/ts/managed.egress.types.ts +85 -0
- package/ts/managed.types.ts +24 -16
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IAppliedManagedNftPolicy,
|
|
3
|
-
IPreparedManagedNftPolicy, IManagedNftStatus, TManagedNftCommands } from './managed.types.js';
|
|
3
|
+
IPreparedManagedNftPolicy, IManagedNftStatus, 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}.`); }
|
|
@@ -43,9 +43,9 @@ function snapshot<T>(input: T): T {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** One private native/netlink owner. Persistence and workload link lifetimes belong to its caller. */
|
|
46
|
-
export class ManagedNftables {
|
|
46
|
+
export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
|
|
47
47
|
readonly #options: Readonly<IManagedNftOptions>;
|
|
48
|
-
#bridge: plugins.smartrust.RustBridge<TManagedNftCommands
|
|
48
|
+
#bridge: plugins.smartrust.RustBridge<TManagedNftCommands<TPolicy>> | null = null;
|
|
49
49
|
#state: 'new' | 'starting' | 'ready' | 'closing' | 'failed-owned' | 'stopped' = 'new';
|
|
50
50
|
#work: Promise<unknown> | null = null;
|
|
51
51
|
#close: Promise<void> | null = null;
|
|
@@ -53,27 +53,32 @@ export class ManagedNftables {
|
|
|
53
53
|
|
|
54
54
|
constructor(options: IManagedNftOptions) {
|
|
55
55
|
this.#options = snapshot(options);
|
|
56
|
-
if (Object.keys(this.#options).some((key) => !['ownerId', 'instanceId', 'tableName', 'binaryPath'].includes(key))
|
|
56
|
+
if (Object.keys(this.#options).some((key) => !['ownerId', 'instanceId', 'tableName', 'binaryPath', 'networkNamespaceFd'].includes(key))
|
|
57
57
|
|| ![this.#options.ownerId, this.#options.instanceId, this.#options.tableName].every((value) => typeof value === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(value))
|
|
58
58
|
|| this.#options.instanceId.length < 16 || !this.#options.tableName.startsWith('snft_')
|
|
59
59
|
|| this.#options.binaryPath !== undefined && (!plugins.path.isAbsolute(this.#options.binaryPath) || this.#options.binaryPath.includes('\0'))) {
|
|
60
60
|
throw new ManagedNftablesError('INVALID');
|
|
61
61
|
}
|
|
62
|
+
if (this.#options.networkNamespaceFd !== undefined && (process.platform !== 'linux'
|
|
63
|
+
|| !Number.isInteger(this.#options.networkNamespaceFd) || this.#options.networkNamespaceFd < 0
|
|
64
|
+
|| this.#options.networkNamespaceFd > 0x7fff_ffff)) throw new ManagedNftablesError('INVALID');
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
public get state(): string { return this.#state; }
|
|
65
68
|
|
|
66
69
|
/** Starts only the compiler/IPC owner. The returned idle status is not enforcement. */
|
|
67
|
-
public start(): Promise<IManagedNftStatus
|
|
70
|
+
public start(): Promise<IManagedNftStatus<TPolicy>> {
|
|
68
71
|
if (this.#state !== 'new') return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
|
|
69
72
|
this.#state = 'starting';
|
|
70
73
|
const work = (async () => {
|
|
71
74
|
if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch)) throw new ManagedNftablesError('UNSUPPORTED_PLATFORM');
|
|
72
75
|
const root = plugins.url.fileURLToPath(new URL('../', import.meta.url));
|
|
73
|
-
this.#bridge = new plugins.smartrust.RustBridge<TManagedNftCommands
|
|
76
|
+
this.#bridge = new plugins.smartrust.RustBridge<TManagedNftCommands<TPolicy>>({
|
|
74
77
|
binaryName: 'smartnftables', binaryPath: this.#options.binaryPath
|
|
75
78
|
?? plugins.path.join(root, 'dist_rust', `smartnftables_linux_${process.arch === 'x64' ? 'amd64' : 'arm64'}_musl`),
|
|
76
|
-
localPaths: [], searchSystemPath: false,
|
|
79
|
+
localPaths: [], searchSystemPath: false,
|
|
80
|
+
cliArgs: ['--management', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
|
|
81
|
+
inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
|
|
77
82
|
readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
|
|
78
83
|
maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, closeOwner: 1 },
|
|
79
84
|
});
|
|
@@ -89,14 +94,14 @@ export class ManagedNftables {
|
|
|
89
94
|
return this.#track(work);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
public prepare(policy:
|
|
97
|
+
public prepare(policy: TPolicy): Promise<IPreparedManagedNftPolicy<TPolicy>> {
|
|
93
98
|
return this.#invoke('preparePolicy', policy);
|
|
94
99
|
}
|
|
95
|
-
public reconcile(transition: IManagedNftTransition): Promise<IAppliedManagedNftPolicy
|
|
100
|
+
public reconcile(transition: IManagedNftTransition<TPolicy>): Promise<IAppliedManagedNftPolicy<TPolicy>> {
|
|
96
101
|
return this.#invoke('reconcilePolicy', transition);
|
|
97
102
|
}
|
|
98
|
-
public inspect(): Promise<IManagedNftStatus
|
|
99
|
-
public release(expected: IAppliedManagedNftPolicy): Promise<{ released: true }> {
|
|
103
|
+
public inspect(): Promise<IManagedNftStatus<TPolicy>> { return this.#invoke('inspectPolicy', {}); }
|
|
104
|
+
public release(expected: IAppliedManagedNftPolicy<TPolicy>): Promise<{ released: true }> {
|
|
100
105
|
return this.#invoke('releasePolicy', expected);
|
|
101
106
|
}
|
|
102
107
|
|
|
@@ -122,16 +127,16 @@ export class ManagedNftables {
|
|
|
122
127
|
return this.#close;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
#invoke<K extends keyof TManagedNftCommands
|
|
130
|
+
#invoke<K extends keyof TManagedNftCommands<TPolicy>>(method: K, params: TManagedNftCommands<TPolicy>[K]['params']): Promise<TManagedNftCommands<TPolicy>[K]['result']> {
|
|
126
131
|
if (!['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
|
|
127
|
-
let captured: TManagedNftCommands[K]['params'];
|
|
132
|
+
let captured: TManagedNftCommands<TPolicy>[K]['params'];
|
|
128
133
|
try { captured = snapshot(params); } catch { return Promise.reject(new ManagedNftablesError('INVALID')); }
|
|
129
134
|
if (method === 'reconcilePolicy' || method === 'releasePolicy') this.#nativeMayOwn = true;
|
|
130
135
|
return this.#track(this.#bridge.sendCommand(method, captured).then((result) => {
|
|
131
136
|
const capturedResult = snapshot(result);
|
|
132
137
|
if (method === 'releasePolicy') this.#nativeMayOwn = false;
|
|
133
138
|
if (this.#state !== 'closing' && (method === 'reconcilePolicy' || method === 'releasePolicy'
|
|
134
|
-
|| method === 'inspectPolicy' && (capturedResult as IManagedNftStatus).error === null)) this.#state = 'ready';
|
|
139
|
+
|| method === 'inspectPolicy' && (capturedResult as IManagedNftStatus<TPolicy>).error === null)) this.#state = 'ready';
|
|
135
140
|
return capturedResult;
|
|
136
141
|
}));
|
|
137
142
|
}
|
package/ts/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { SmartNftables } from './nft.manager.js';
|
|
3
3
|
export { ManagedNftables, ManagedNftablesError } from './classes.managednftables.js';
|
|
4
4
|
export type * from './managed.types.js';
|
|
5
|
+
export type * from './managed.egress.types.js';
|
|
5
6
|
|
|
6
7
|
// Sub-managers
|
|
7
8
|
export { NatManager } from './nft.manager.nat.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { IManagedNftEndpoint, IManagedNftRule } from './managed.types.js';
|
|
2
|
+
|
|
3
|
+
/** Namespace-local facts, checked through rtnetlink. The caller retains actual link-generation ownership. */
|
|
4
|
+
export interface IManagedNftLocalLinkV2 {
|
|
5
|
+
interfaceIndex: number;
|
|
6
|
+
interfaceName: string;
|
|
7
|
+
interfaceKind: 'veth' | 'tun' | 'ethernet';
|
|
8
|
+
/** Canonical unicast Ethernet address, or null for an L3 TUN. */
|
|
9
|
+
macAddress: string | null;
|
|
10
|
+
interfaceLinkIndex: number;
|
|
11
|
+
requiredIpv4Addresses: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IManagedNftPlatformEndpointV2 {
|
|
15
|
+
id: string;
|
|
16
|
+
address: string;
|
|
17
|
+
protocol: 'tcp' | 'udp';
|
|
18
|
+
port: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Caller-authenticated complete protected authority; hashing does not attest completeness. */
|
|
22
|
+
export interface IManagedNftProtectionV2 {
|
|
23
|
+
authorityDigest: string;
|
|
24
|
+
prefixes: string[];
|
|
25
|
+
platformEndpoints: IManagedNftPlatformEndpointV2[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IManagedNftHandoffPortRangeV2 {
|
|
29
|
+
protocol: 'tcp' | 'udp';
|
|
30
|
+
first: number;
|
|
31
|
+
last: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IManagedNftHandoffAllocationV2 {
|
|
35
|
+
lease: { id: string; generation: number; digest: string };
|
|
36
|
+
transitSourceAddress: string;
|
|
37
|
+
sourcePortRanges: IManagedNftHandoffPortRangeV2[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface IManagedNftEgressGrantV2 {
|
|
41
|
+
/** A workload veth endpoint, or the actual router-local host for DNS/relay sockets. */
|
|
42
|
+
sourceEndpoint: string | null;
|
|
43
|
+
sourcePrefix: string;
|
|
44
|
+
destination: { kind: 'public'; prefix: string } | { kind: 'platform'; endpointId: string };
|
|
45
|
+
protocol: 'tcp' | 'udp';
|
|
46
|
+
sourcePort: number | null;
|
|
47
|
+
destinationPort: number | null;
|
|
48
|
+
/** One exact range from this generation's lease; selects the SNAT range for this grant. */
|
|
49
|
+
sourcePortRange: { first: number; last: number };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface IManagedNftEgressGenerationV2 extends IManagedNftHandoffAllocationV2 {
|
|
53
|
+
conntrackZone: number;
|
|
54
|
+
conntrackLabel: string;
|
|
55
|
+
grants: IManagedNftEgressGrantV2[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One combined private/TUN/local/egress table; never compose a second ACCEPT over v1 terminal drops. */
|
|
59
|
+
export interface IManagedNftRouterEgressScopeV2 {
|
|
60
|
+
kind: 'routerEgress';
|
|
61
|
+
endpoints: IManagedNftEndpoint[];
|
|
62
|
+
rules: IManagedNftRule[];
|
|
63
|
+
/** One exact local binding for every private endpoint. */
|
|
64
|
+
links: IManagedNftLocalLinkV2[];
|
|
65
|
+
handoff: IManagedNftLocalLinkV2;
|
|
66
|
+
protection: IManagedNftProtectionV2;
|
|
67
|
+
/** Empty retains private/local enforcement while denying external egress. */
|
|
68
|
+
generations: IManagedNftEgressGenerationV2[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Shared-host capture barrier and explicit outer SNAT; Docker forwarding remains a separate owner. */
|
|
72
|
+
export interface IManagedNftHostTransitScopeV2 {
|
|
73
|
+
kind: 'hostTransit';
|
|
74
|
+
protection: IManagedNftProtectionV2;
|
|
75
|
+
handoffs: Array<{ link: IManagedNftLocalLinkV2; allocations: IManagedNftHandoffAllocationV2[] }>;
|
|
76
|
+
uplink: IManagedNftLocalLinkV2;
|
|
77
|
+
/** Exact current address present on uplink. No masquerade or default-route inference. */
|
|
78
|
+
snatAddress: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface IManagedNftPolicyV2 {
|
|
82
|
+
schemaVersion: 2;
|
|
83
|
+
revision: number;
|
|
84
|
+
scope: IManagedNftRouterEgressScopeV2 | IManagedNftHostTransitScopeV2;
|
|
85
|
+
}
|
package/ts/managed.types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { IManagedNftPolicyV2 } from './managed.egress.types.js';
|
|
2
|
+
|
|
1
3
|
/** The caller owns these workload links and keeps them disabled until apply is confirmed. */
|
|
2
4
|
export interface IManagedNftEndpoint {
|
|
3
5
|
id: string;
|
|
@@ -26,8 +28,8 @@ export interface IManagedNftPolicy {
|
|
|
26
28
|
rules: IManagedNftRule[];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
export interface IPreparedManagedNftPolicy {
|
|
30
|
-
policy:
|
|
31
|
+
export interface IPreparedManagedNftPolicy<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
|
|
32
|
+
policy: TPolicy;
|
|
31
33
|
digest: string;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -47,23 +49,23 @@ export interface IManagedNftReceipt {
|
|
|
47
49
|
digest: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
export interface IAppliedManagedNftPolicy {
|
|
52
|
+
export interface IAppliedManagedNftPolicy<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
|
|
51
53
|
receipt: IManagedNftReceipt;
|
|
52
|
-
prepared: IPreparedManagedNftPolicy
|
|
54
|
+
prepared: IPreparedManagedNftPolicy<TPolicy>;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** Persist this complete command before reconcile, then retain its complete result. */
|
|
56
|
-
export interface IManagedNftTransition {
|
|
57
|
-
previous: IAppliedManagedNftPolicy | null;
|
|
58
|
-
target: IPreparedManagedNftPolicy
|
|
58
|
+
export interface IManagedNftTransition<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
|
|
59
|
+
previous: IAppliedManagedNftPolicy<TPolicy> | null;
|
|
60
|
+
target: IPreparedManagedNftPolicy<TPolicy>;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
export interface IManagedNftStatus {
|
|
63
|
+
export interface IManagedNftStatus<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
|
|
62
64
|
identity: IManagedNftIdentity;
|
|
63
65
|
state: 'idle' | 'applied' | 'failed-owned' | 'released';
|
|
64
66
|
enforced: boolean;
|
|
65
|
-
applied: IAppliedManagedNftPolicy | null;
|
|
66
|
-
pending: IManagedNftTransition | null;
|
|
67
|
+
applied: IAppliedManagedNftPolicy<TPolicy> | null;
|
|
68
|
+
pending: IManagedNftTransition<TPolicy> | null;
|
|
67
69
|
error: string | null;
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -75,13 +77,19 @@ export interface IManagedNftOptions {
|
|
|
75
77
|
tableName: string;
|
|
76
78
|
/** Explicit executable selection for qualified distributions and isolated tests. */
|
|
77
79
|
binaryPath?: string;
|
|
80
|
+
/** Linux network namespace descriptor, owned by the caller and valid through start().
|
|
81
|
+
* The native process enters before readiness or netlink activity. Keep a namespace
|
|
82
|
+
* owner alive for policy lifetime and recovery; this process option is not durable authority. */
|
|
83
|
+
networkNamespaceFd?: number;
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
export type
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
export type TManagedNftPolicy = IManagedNftPolicy | IManagedNftPolicyV2;
|
|
87
|
+
|
|
88
|
+
export type TManagedNftCommands<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> = {
|
|
89
|
+
preparePolicy: { params: TPolicy; result: IPreparedManagedNftPolicy<TPolicy> };
|
|
90
|
+
openOwner: { params: Pick<IManagedNftOptions, 'ownerId' | 'instanceId' | 'tableName'>; result: IManagedNftStatus<TPolicy> };
|
|
91
|
+
reconcilePolicy: { params: IManagedNftTransition<TPolicy>; result: IAppliedManagedNftPolicy<TPolicy> };
|
|
92
|
+
inspectPolicy: { params: Record<string, never>; result: IManagedNftStatus<TPolicy> };
|
|
93
|
+
releasePolicy: { params: IAppliedManagedNftPolicy<TPolicy>; result: { released: true } };
|
|
86
94
|
closeOwner: { params: Record<string, never>; result: { released: true } };
|
|
87
95
|
};
|