@push.rocks/smartnftables 3.0.0 → 4.0.1
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 +20 -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.manageddockerforwarding.js +3 -4
- package/dist_ts/classes.managednftables.d.ts +14 -2
- package/dist_ts/classes.managednftables.js +48 -8
- package/dist_ts/managed.egress.types.d.ts +2 -0
- package/dist_ts/managed.types.d.ts +14 -0
- package/package.json +2 -2
- package/readme.md +69 -3
- package/rust/src/docker.policy.rs +2 -1
- package/rust/src/egress.compile.rs +8 -10
- package/rust/src/egress.host.rs +19 -1
- package/rust/src/egress.rs +28 -18
- package/rust/src/egress_hostgrant_tests.rs +9 -2
- package/rust/src/egress_localport_tests.rs +8 -1
- package/rust/src/egress_tests.rs +157 -0
- package/rust/src/egress_uplink_tests.rs +159 -0
- package/rust/src/egress_workloadgrant_tests.rs +15 -6
- package/rust/src/main.rs +57 -3
- package/rust/src/policy.rs +10 -13
- package/rust/src/tests.rs +100 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +2 -3
- package/ts/classes.managednftables.ts +46 -8
- package/ts/managed.egress.types.ts +2 -0
- package/ts/managed.types.ts +17 -0
package/rust/src/tests.rs
CHANGED
|
@@ -117,3 +117,103 @@ fn compiler_bounds_full_policy_and_keeps_directed_port_grants() {
|
|
|
117
117
|
value.revision = 9_007_199_254_740_992;
|
|
118
118
|
assert_eq!(value.prepare(), Err(Error::Invalid));
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
fn exhausted<T>(name: &'static str, limit: usize, actual: usize) -> crate::Result<T> {
|
|
122
|
+
Err(Error::Exhausted(crate::Bound {
|
|
123
|
+
name,
|
|
124
|
+
limit,
|
|
125
|
+
actual,
|
|
126
|
+
}))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Every endpoint with 16 prefixes and `rules` distinct directed rules between
|
|
130
|
+
/// them: well-formed input whose only fault can be its size.
|
|
131
|
+
fn sized(endpoints: u32, rules: u16) -> Policy {
|
|
132
|
+
let mut value = policy();
|
|
133
|
+
let template = value.endpoints[0].clone();
|
|
134
|
+
value.endpoints = (0..endpoints)
|
|
135
|
+
.map(|index| {
|
|
136
|
+
let mut endpoint = template.clone();
|
|
137
|
+
endpoint.id = format!("e{index}");
|
|
138
|
+
endpoint.interface_index = 10 + index;
|
|
139
|
+
endpoint.interface_name = format!("w{index}");
|
|
140
|
+
endpoint.source_prefixes = (0..16)
|
|
141
|
+
.map(|part| format!("10.{}.{part}.0/24", 100 + index))
|
|
142
|
+
.collect();
|
|
143
|
+
endpoint
|
|
144
|
+
})
|
|
145
|
+
.collect();
|
|
146
|
+
let template = value.rules[0].clone();
|
|
147
|
+
value.rules = (0..rules)
|
|
148
|
+
.map(|index| {
|
|
149
|
+
let (from, to) = (
|
|
150
|
+
u32::from(index) % endpoints,
|
|
151
|
+
(u32::from(index) + 1) % endpoints,
|
|
152
|
+
);
|
|
153
|
+
let mut rule = template.clone();
|
|
154
|
+
rule.source_endpoint = Some(format!("e{from}"));
|
|
155
|
+
rule.destination_endpoint = Some(format!("e{to}"));
|
|
156
|
+
rule.source_prefix = format!("10.{}.0.0/24", 100 + from);
|
|
157
|
+
rule.destination_prefix = format!("10.{}.0.0/24", 100 + to);
|
|
158
|
+
rule.destination_port = Some(1000 + index);
|
|
159
|
+
rule
|
|
160
|
+
})
|
|
161
|
+
.collect();
|
|
162
|
+
value
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn v1_capacity_refusals_name_their_bound_and_invalid_input_stays_invalid() {
|
|
167
|
+
assert_eq!(sized(2, 2).prepare().map(|_| ()), Ok(()));
|
|
168
|
+
// Within the schema's counts, the complete graph exceeds the byte and the
|
|
169
|
+
// operation budget; the complete program is measured, so both are exact.
|
|
170
|
+
assert_eq!(
|
|
171
|
+
sized(8, 16).prepare(),
|
|
172
|
+
exhausted("ruleBytes", 100_000, 186_952)
|
|
173
|
+
);
|
|
174
|
+
assert_eq!(sized(32, 128).prepare(), exhausted("operations", 768, 1667));
|
|
175
|
+
assert_eq!(sized(33, 16).prepare(), exhausted("endpoints", 32, 33));
|
|
176
|
+
assert_eq!(sized(8, 129).prepare(), exhausted("rules", 128, 129));
|
|
177
|
+
// Invalid input is never reported as capacity, whatever its size.
|
|
178
|
+
let mut value = sized(33, 129);
|
|
179
|
+
value.schema_version = 2;
|
|
180
|
+
assert_eq!(value.prepare(), Err(Error::Invalid));
|
|
181
|
+
let mut value = sized(2, 2);
|
|
182
|
+
value.endpoints[0].interface_name = "w".repeat(16);
|
|
183
|
+
assert_eq!(value.prepare(), Err(Error::Invalid));
|
|
184
|
+
let mut value = sized(2, 2);
|
|
185
|
+
value.endpoints[0]
|
|
186
|
+
.source_prefixes
|
|
187
|
+
.push("10.200.0.0/24".into());
|
|
188
|
+
assert_eq!(value.prepare(), Err(Error::Invalid));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn failure_responses_carry_the_refusal_reason_and_capacity_bound() {
|
|
193
|
+
let bound = crate::Bound {
|
|
194
|
+
name: "targetBytes",
|
|
195
|
+
limit: 212_000,
|
|
196
|
+
actual: 212_345,
|
|
197
|
+
};
|
|
198
|
+
assert_eq!(
|
|
199
|
+
crate::failure("7", Error::Exhausted(bound)),
|
|
200
|
+
serde_json::json!({"id":"7","success":false,
|
|
201
|
+
"error":"Managed policy exceeds the targetBytes capacity of 212000: it reached 212345.",
|
|
202
|
+
"errorCode":"EXHAUSTED","errorData":{"bound":"targetBytes","limit":212_000,"actual":212_345}})
|
|
203
|
+
);
|
|
204
|
+
assert_eq!(
|
|
205
|
+
crate::failure("8", Error::Invalid),
|
|
206
|
+
serde_json::json!({"id":"8","success":false,"error":"Managed policy input is invalid.","errorCode":"INVALID"})
|
|
207
|
+
);
|
|
208
|
+
for error in [
|
|
209
|
+
Error::Conflict,
|
|
210
|
+
Error::Permission,
|
|
211
|
+
Error::Protocol,
|
|
212
|
+
Error::Unavailable,
|
|
213
|
+
] {
|
|
214
|
+
assert_eq!(
|
|
215
|
+
crate::failure("9", error),
|
|
216
|
+
serde_json::json!({"id":"9","success":false,"error":"Managed policy operation failed.","errorCode":error.code()})
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import { snapshot, managedIpcBytes, ManagedNftablesError } from './classes.managednftables.js';
|
|
2
|
+
import { snapshot, managedIpcBytes, ManagedNftablesError, nativeError } 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. */
|
|
@@ -103,8 +103,7 @@ export class ManagedDockerForwarding {
|
|
|
103
103
|
#track<T>(work: Promise<T>): Promise<T> {
|
|
104
104
|
const owned = work.catch((error) => {
|
|
105
105
|
if (this.#state !== 'closing') this.#state = 'failed-owned';
|
|
106
|
-
|
|
107
|
-
throw new ManagedNftablesError(typeof code === 'string' && /^[A-Z_]{1,64}$/.test(code) ? code : 'UNAVAILABLE');
|
|
106
|
+
throw nativeError(error);
|
|
108
107
|
});
|
|
109
108
|
this.#work = owned;
|
|
110
109
|
void owned.then(() => { this.#work = null; }, () => { this.#work = null; });
|
|
@@ -1,9 +1,46 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IAppliedManagedNftPolicy,
|
|
3
|
-
|
|
2
|
+
import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IAppliedManagedNftPolicy, IPreparedManagedNftPolicy,
|
|
3
|
+
IManagedNftStatus, IManagedNftTransitionRelease, TManagedNftCommands, TManagedNftPolicy, IManagedNftCapacity,
|
|
4
|
+
TManagedNftCapacityBound } from './managed.types.js';
|
|
4
5
|
|
|
6
|
+
/** `INVALID`: the input is outside the contract. `EXHAUSTED`: the policy exceeds a
|
|
7
|
+
* capacity bound of the atomic replacement budget, and a smaller policy may fit.
|
|
8
|
+
* Both carry the native refusal text in `reason`; `EXHAUSTED` also names its
|
|
9
|
+
* bound in `details`. Every other code, and the facade's own rejections, carry
|
|
10
|
+
* neither. */
|
|
5
11
|
export class ManagedNftablesError extends Error {
|
|
6
|
-
constructor(public readonly code: string
|
|
12
|
+
constructor(public readonly code: string, public readonly reason: string | null = null,
|
|
13
|
+
public readonly details: Readonly<IManagedNftCapacity> | null = null) {
|
|
14
|
+
super(reason === null ? `Managed nftables ${code}.` : `Managed nftables ${code}: ${reason}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const capacityBounds: ReadonlySet<string> = new Set<TManagedNftCapacityBound>(['ruleBytes', 'targetBytes', 'operations',
|
|
19
|
+
'endpoints', 'links', 'rules', 'grants', 'hostGrants', 'workloadGrants', 'publishedPorts', 'localTcpPortOwners',
|
|
20
|
+
'restoreRules', 'restoreBytes']);
|
|
21
|
+
|
|
22
|
+
function capacity(value: unknown): Readonly<IManagedNftCapacity> | null {
|
|
23
|
+
if (typeof value !== 'object' || value === null || Object.getPrototypeOf(value) !== Object.prototype
|
|
24
|
+
|| Object.keys(value).sort().join(',') !== 'actual,bound,limit') return null;
|
|
25
|
+
const { bound, limit, actual } = value as Record<string, unknown>;
|
|
26
|
+
if (typeof bound !== 'string' || !capacityBounds.has(bound) || typeof limit !== 'number' || typeof actual !== 'number'
|
|
27
|
+
|| !Number.isSafeInteger(limit) || !Number.isSafeInteger(actual) || limit < 0 || actual <= limit) return null;
|
|
28
|
+
return Object.freeze({ bound: bound as TManagedNftCapacityBound, limit, actual });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Maps a failed native request. An `INVALID` or `EXHAUSTED` response keeps the
|
|
32
|
+
* bounded native refusal text and, for `EXHAUSTED`, its bound; a refusal outside
|
|
33
|
+
* that shape is `PROTOCOL`. Other responses keep only their code, and a failure
|
|
34
|
+
* without a native code is `UNAVAILABLE`. */
|
|
35
|
+
export function nativeError(error: unknown): ManagedNftablesError {
|
|
36
|
+
if (!(error instanceof plugins.smartrust.RustBridgeRequestError)) return new ManagedNftablesError('UNAVAILABLE');
|
|
37
|
+
const { responseErrorCode: code, message: reason, responseErrorData: data } = error;
|
|
38
|
+
if (typeof code !== 'string' || !/^[A-Z_]{1,64}$/.test(code)) return new ManagedNftablesError('UNAVAILABLE');
|
|
39
|
+
if (code !== 'INVALID' && code !== 'EXHAUSTED') return new ManagedNftablesError(code);
|
|
40
|
+
if (!/^[\x20-\x7e]{1,256}$/.test(reason)) return new ManagedNftablesError('PROTOCOL');
|
|
41
|
+
if (code === 'INVALID') return data === undefined ? new ManagedNftablesError(code, reason) : new ManagedNftablesError('PROTOCOL');
|
|
42
|
+
const details = capacity(data);
|
|
43
|
+
return details ? new ManagedNftablesError(code, reason, details) : new ManagedNftablesError('PROTOCOL');
|
|
7
44
|
}
|
|
8
45
|
|
|
9
46
|
/** Bounds of the private native IPC. A status carries up to three complete policies (applied,
|
|
@@ -240,16 +277,17 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
|
|
|
240
277
|
#track<T>(work: Promise<T>, method: keyof TManagedNftCommands<TPolicy>): Promise<T> {
|
|
241
278
|
const owned = work.catch((error) => {
|
|
242
279
|
if (this.#state !== 'closing') this.#state = 'failed-owned';
|
|
243
|
-
const
|
|
244
|
-
// Only a native compiler rejection or explicit INVALID
|
|
245
|
-
// effect-free validation
|
|
280
|
+
const failure = nativeError(error);
|
|
281
|
+
// Only a native compiler rejection or explicit INVALID or EXHAUSTED
|
|
282
|
+
// response proves effect-free validation: the native owner refuses both
|
|
283
|
+
// before any kernel work. Other native errors can follow lost ownership
|
|
246
284
|
// or admitted kernel work. Recovery stays available; confidence is lost.
|
|
247
285
|
const validation = error instanceof plugins.smartrust.RustBridgeRequestError
|
|
248
286
|
&& error.code === 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE'
|
|
249
|
-
&& (method === 'preparePolicy' || code === 'INVALID');
|
|
287
|
+
&& (method === 'preparePolicy' || failure.code === 'INVALID' || failure.code === 'EXHAUSTED');
|
|
250
288
|
if (!validation && (this.#state !== 'closing'
|
|
251
289
|
|| ['reconcilePolicy', 'releasePolicy', 'releasePolicyTransition', 'detachPolicy'].includes(method))) this.#fail();
|
|
252
|
-
throw
|
|
290
|
+
throw failure;
|
|
253
291
|
});
|
|
254
292
|
this.#work = owned;
|
|
255
293
|
void owned.then(() => { this.#work = null; }, () => { this.#work = null; });
|
|
@@ -172,6 +172,8 @@ export interface IManagedNftHostTransitScopeV2 {
|
|
|
172
172
|
kind: 'hostTransit';
|
|
173
173
|
protection: IManagedNftProtectionV2;
|
|
174
174
|
handoffs: Array<{ link: IManagedNftLocalLinkV2; allocations: IManagedNftHandoffAllocationV2[] }>;
|
|
175
|
+
/** Its addresses need not lie in `protection.prefixes`; the host barrier protects each
|
|
176
|
+
* uncovered one as a `/32` destination. Handoff addresses must be protected. */
|
|
175
177
|
uplink: IManagedNftLocalLinkV2;
|
|
176
178
|
/** Exact current address present on uplink. No masquerade or default-route inference. */
|
|
177
179
|
snatAddress: string;
|
package/ts/managed.types.ts
CHANGED
|
@@ -93,6 +93,23 @@ export interface IManagedNftOptions {
|
|
|
93
93
|
|
|
94
94
|
export type TManagedNftPolicy = IManagedNftPolicy | IManagedNftPolicyV2;
|
|
95
95
|
|
|
96
|
+
/** Capacity bounds of the atomic replacement budget. The compiled target's
|
|
97
|
+
* `ruleBytes` (100,000), `targetBytes` (212,000) and `operations` (768), the input
|
|
98
|
+
* counts that exist to hold it (`endpoints`, `links`, `rules`, `grants`,
|
|
99
|
+
* `hostGrants`, `workloadGrants`, `publishedPorts`, `localTcpPortOwners`) and the
|
|
100
|
+
* Docker forwarding contribution's `restoreRules` (192) and `restoreBytes` (100,000). */
|
|
101
|
+
export type TManagedNftCapacityBound = 'ruleBytes' | 'targetBytes' | 'operations' | 'endpoints' | 'links' | 'rules'
|
|
102
|
+
| 'grants' | 'hostGrants' | 'workloadGrants' | 'publishedPorts' | 'localTcpPortOwners' | 'restoreRules' | 'restoreBytes';
|
|
103
|
+
|
|
104
|
+
/** The bound an `EXHAUSTED` refusal names. `actual` is always above `limit`. It is
|
|
105
|
+
* exact for input counts and the schema-v1 budget; the other compiled budgets stop
|
|
106
|
+
* at the first message or rule past the limit, so the policy needs at least `actual`. */
|
|
107
|
+
export interface IManagedNftCapacity {
|
|
108
|
+
bound: TManagedNftCapacityBound;
|
|
109
|
+
limit: number;
|
|
110
|
+
actual: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
export type TManagedNftCommands<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> = {
|
|
97
114
|
preparePolicy: { params: TPolicy; result: IPreparedManagedNftPolicy<TPolicy> };
|
|
98
115
|
openOwner: { params: Pick<IManagedNftOptions, 'ownerId' | 'instanceId' | 'tableName'>; result: IManagedNftStatus<TPolicy> };
|