@push.rocks/smartnftables 2.6.0 → 3.0.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 +19 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/managed.egress.types.d.ts +25 -3
- package/package.json +6 -6
- package/readme.md +205 -63
- package/rust/src/egress.compile.rs +155 -76
- package/rust/src/egress.host.rs +24 -51
- package/rust/src/egress.hostgrant.rs +1 -1
- package/rust/src/egress.keys.rs +305 -0
- package/rust/src/egress.leased.rs +399 -0
- package/rust/src/egress.private.rs +197 -0
- package/rust/src/egress.published.rs +370 -0
- package/rust/src/egress.router.rs +61 -294
- package/rust/src/egress.rs +126 -11
- package/rust/src/egress_hostgrant_tests.rs +149 -18
- package/rust/src/egress_localport_tests.rs +3 -1
- package/rust/src/egress_publishedrange_tests.rs +812 -0
- package/rust/src/egress_routerequivalence_tests.rs +448 -0
- package/rust/src/egress_tests.rs +132 -230
- package/rust/src/egress_workloadgrant_tests.rs +6 -7
- package/rust/src/owner.rs +69 -14
- package/rust/src/owner_host_traffic_tests.rs +4 -0
- package/rust/src/owner_identity_tests.rs +166 -80
- package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
- package/rust/src/owner_router_traffic_tests.rs +2 -0
- package/rust/src/owner_scale_traffic_tests.rs +383 -0
- package/rust/src/owner_tests.rs +6 -0
- package/rust/src/policy.rs +29 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/managed.egress.types.ts +25 -3
|
@@ -1,52 +1,6 @@
|
|
|
1
|
+
use super::leased::Kind;
|
|
1
2
|
use super::*;
|
|
2
3
|
|
|
3
|
-
fn endpoint<'a>(scope: &'a RouterScope, grant: &Grant) -> Option<&'a LocalLink> {
|
|
4
|
-
grant.source_endpoint.as_ref().and_then(|id| {
|
|
5
|
-
let endpoint = scope.endpoints.iter().find(|item| &item.id == id)?;
|
|
6
|
-
scope
|
|
7
|
-
.links
|
|
8
|
-
.iter()
|
|
9
|
-
.find(|item| item.interface_index == endpoint.interface_index)
|
|
10
|
-
})
|
|
11
|
-
}
|
|
12
|
-
fn raw_original(scope: &RouterScope, grant: &Grant) -> Result<Vec<Attr>> {
|
|
13
|
-
let mut result = tuple(grant, &scope.protection, false, false)?;
|
|
14
|
-
result.extend(match endpoint(scope, grant) {
|
|
15
|
-
Some(endpoint) => link(endpoint, true),
|
|
16
|
-
None => link(&scope.handoff, false),
|
|
17
|
-
});
|
|
18
|
-
Ok(result)
|
|
19
|
-
}
|
|
20
|
-
fn filter(scope: &RouterScope, grant: &Grant, reply: bool) -> Result<Vec<Attr>> {
|
|
21
|
-
let mut result = tuple(grant, &scope.protection, reply, true)?;
|
|
22
|
-
result.extend(link(&scope.handoff, reply));
|
|
23
|
-
if let Some(endpoint) = endpoint(scope, grant) {
|
|
24
|
-
result.extend(link(endpoint, !reply));
|
|
25
|
-
}
|
|
26
|
-
Ok(result)
|
|
27
|
-
}
|
|
28
|
-
fn raw_return(scope: &RouterScope, grant: &Grant, generation: &Generation) -> Result<Vec<Attr>> {
|
|
29
|
-
let mut result = ipv4();
|
|
30
|
-
result.extend(link(&scope.handoff, true));
|
|
31
|
-
result.extend(meta(16, vec![protocol(&grant.protocol)]));
|
|
32
|
-
result.extend(current_address(
|
|
33
|
-
&super::super::destination(grant, &scope.protection)?,
|
|
34
|
-
true,
|
|
35
|
-
)?);
|
|
36
|
-
result.extend(current_address(
|
|
37
|
-
&format!("{}/32", generation.transit_source_address),
|
|
38
|
-
false,
|
|
39
|
-
)?);
|
|
40
|
-
if let Some(port) = grant.destination_port {
|
|
41
|
-
result.extend(current_port(port, true));
|
|
42
|
-
}
|
|
43
|
-
result.extend(current_range(
|
|
44
|
-
grant.source_port_range.first,
|
|
45
|
-
grant.source_port_range.last,
|
|
46
|
-
false,
|
|
47
|
-
));
|
|
48
|
-
Ok(result)
|
|
49
|
-
}
|
|
50
4
|
/// The exact veth link of the workload endpoint that holds an endpoint address.
|
|
51
5
|
/// Validation admits only a current endpoint address, and endpoint prefixes are
|
|
52
6
|
/// pairwise disjoint, so one publication or host grant never spans two links.
|
|
@@ -68,70 +22,30 @@ pub(super) fn endpoint_link<'a>(scope: &'a RouterScope, address: &str) -> Result
|
|
|
68
22
|
.find(|link| link.interface_index == endpoint.interface_index)
|
|
69
23
|
.ok_or(crate::Error::Invalid)
|
|
70
24
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/// Inbound publication envelope. Both directions check the exact links, the
|
|
75
|
-
/// translated current tuple and the original conntrack destination, so foreign
|
|
76
|
-
/// NAT cannot borrow this admission for another flow.
|
|
77
|
-
fn published_envelope(
|
|
78
|
-
scope: &RouterScope,
|
|
79
|
-
endpoint: &LocalLink,
|
|
80
|
-
port: &RouterPublishedPort,
|
|
81
|
-
reply: bool,
|
|
82
|
-
) -> Result<Vec<Attr>> {
|
|
83
|
-
let mut expressions = ipv4();
|
|
84
|
-
expressions.extend(link(&scope.handoff, !reply));
|
|
85
|
-
expressions.extend(link(endpoint, reply));
|
|
86
|
-
expressions.extend(meta(16, vec![protocol(&port.protocol)]));
|
|
87
|
-
expressions.extend(ct(10, None, vec![protocol(&port.protocol)]));
|
|
88
|
-
expressions.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
|
|
89
|
-
expressions.extend(ct(1, None, vec![u8::from(reply)]));
|
|
90
|
-
let target = format!("{}/32", port.endpoint_address);
|
|
91
|
-
expressions.extend(current_address(&target, reply)?);
|
|
92
|
-
expressions.extend(current_port(port.endpoint_port, reply));
|
|
93
|
-
let published = format!("{}/32", port.transit_source_address);
|
|
94
|
-
expressions.extend(original_address(&published, false)?);
|
|
95
|
-
expressions.extend(original_port(port.transit_port, false));
|
|
96
|
-
Ok(expressions)
|
|
97
|
-
}
|
|
98
|
-
/// Handoff ingress classification for an exact current publication. Raw runs
|
|
99
|
-
/// before conntrack, so it matches the arriving tuple only and never sets a
|
|
100
|
-
/// generation zone: a publication is an ordinary default-zone flow.
|
|
101
|
-
fn published_return(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
102
|
-
for port in &scope.published_ports {
|
|
103
|
-
let mut expressions = ipv4();
|
|
104
|
-
expressions.extend(link(&scope.handoff, true));
|
|
105
|
-
expressions.extend(meta(16, vec![protocol(&port.protocol)]));
|
|
106
|
-
expressions.extend(current_address(
|
|
107
|
-
&format!("{}/32", port.transit_source_address),
|
|
108
|
-
false,
|
|
109
|
-
)?);
|
|
110
|
-
expressions.extend(current_port(port.transit_port, false));
|
|
111
|
-
program.end("raw_return", expressions, 1)?;
|
|
112
|
-
}
|
|
113
|
-
Ok(())
|
|
114
|
-
}
|
|
115
|
-
/// Endpoint ingress classification for an exact current publication. The workload
|
|
116
|
-
/// side of a publication is an ordinary default-zone flow too, so it is matched on
|
|
117
|
-
/// its exact link, address and dedicated published port before the leased
|
|
118
|
-
/// classifiers below can zone it: a client whose source port equals a leased
|
|
119
|
-
/// grant's destination port would otherwise move the workload's published packets
|
|
120
|
-
/// into that generation, where they are not its flow. This only classifies; both
|
|
121
|
-
/// the anti-spoof guards and the publication's admissions still decide admission.
|
|
122
|
-
fn published_endpoint(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
25
|
+
/// The publications of this hop, each behind the veth link of its workload.
|
|
26
|
+
fn hop(scope: &RouterScope) -> Result<published::Hop<'_>> {
|
|
27
|
+
let mut publications = Vec::new();
|
|
123
28
|
for port in &scope.published_ports {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
29
|
+
publications.push(published::Publication {
|
|
30
|
+
protocol: &port.protocol,
|
|
31
|
+
outside: (
|
|
32
|
+
&port.transit_source_address,
|
|
33
|
+
port.transit_port,
|
|
34
|
+
port.transit_last(),
|
|
35
|
+
),
|
|
36
|
+
inside: (
|
|
37
|
+
&port.endpoint_address,
|
|
38
|
+
port.endpoint_port,
|
|
39
|
+
port.endpoint_last(),
|
|
40
|
+
),
|
|
41
|
+
link: endpoint_link(scope, &port.endpoint_address)?,
|
|
42
|
+
symmetric: port.symmetric,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
Ok(published::Hop {
|
|
46
|
+
publications,
|
|
47
|
+
fixed: &scope.handoff,
|
|
48
|
+
})
|
|
135
49
|
}
|
|
136
50
|
/// Handoff ingress classification for an exact host grant. Like a publication it
|
|
137
51
|
/// is the explicit authority for its tuple, so it leads the protected-source
|
|
@@ -166,42 +80,6 @@ fn host_grant_sets(program: &mut Program<'_>, scope: &RouterScope) -> Result<()>
|
|
|
166
80
|
}
|
|
167
81
|
Ok(())
|
|
168
82
|
}
|
|
169
|
-
fn published_translation(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
170
|
-
for port in &scope.published_ports {
|
|
171
|
-
let mut expressions = ipv4();
|
|
172
|
-
expressions.extend(link(&scope.handoff, true));
|
|
173
|
-
expressions.extend(meta(16, vec![protocol(&port.protocol)]));
|
|
174
|
-
expressions.extend(ct(10, None, vec![protocol(&port.protocol)]));
|
|
175
|
-
expressions.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
|
|
176
|
-
expressions.extend(ct(1, None, vec![0]));
|
|
177
|
-
expressions.extend(current_address(
|
|
178
|
-
&format!("{}/32", port.transit_source_address),
|
|
179
|
-
false,
|
|
180
|
-
)?);
|
|
181
|
-
expressions.extend(current_port(port.transit_port, false));
|
|
182
|
-
expressions.extend(dnat(&port.endpoint_address, port.endpoint_port)?);
|
|
183
|
-
program.rule("pre", expressions)?;
|
|
184
|
-
}
|
|
185
|
-
Ok(())
|
|
186
|
-
}
|
|
187
|
-
fn published_admissions(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
188
|
-
for port in &scope.published_ports {
|
|
189
|
-
let endpoint = published_link(scope, port)?;
|
|
190
|
-
let original = published_envelope(scope, endpoint, port, false)?;
|
|
191
|
-
for connection_state in [2, 8] {
|
|
192
|
-
let mut expressions = original.clone();
|
|
193
|
-
expressions.extend(state(connection_state));
|
|
194
|
-
if connection_state == 8 && port.protocol == "tcp" {
|
|
195
|
-
expressions.extend(opening_tcp());
|
|
196
|
-
}
|
|
197
|
-
program.end("forward", expressions, 1)?;
|
|
198
|
-
}
|
|
199
|
-
let mut reply = published_envelope(scope, endpoint, port, true)?;
|
|
200
|
-
reply.extend(state(2));
|
|
201
|
-
program.end("forward", reply, 1)?;
|
|
202
|
-
}
|
|
203
|
-
Ok(())
|
|
204
|
-
}
|
|
205
83
|
fn protected(
|
|
206
84
|
program: &mut Program<'_>,
|
|
207
85
|
chain: &str,
|
|
@@ -221,7 +99,7 @@ fn protected(
|
|
|
221
99
|
}
|
|
222
100
|
program.end(chain, vec![], -5)
|
|
223
101
|
}
|
|
224
|
-
fn raw(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
102
|
+
fn raw(program: &mut Program<'_>, scope: &RouterScope, hop: &published::Hop<'_>) -> Result<()> {
|
|
225
103
|
// Handoff ingress must classify only exact leased return envelopes and exact
|
|
226
104
|
// inbound publications. Unknown ingress never reaches private/default-zone
|
|
227
105
|
// rules, even after a rename.
|
|
@@ -231,120 +109,28 @@ fn raw(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
|
231
109
|
// classified ahead of the protected-source denial below, exactly as a
|
|
232
110
|
// platform return envelope is. The host publishes into this address, so its
|
|
233
111
|
// clients are not confined to unprotected space.
|
|
234
|
-
|
|
112
|
+
published::arrival(program, hop)?;
|
|
235
113
|
host_grant_return(program, scope)?;
|
|
236
114
|
// Both sides of a publication lead the leased classifiers of every generation,
|
|
237
115
|
// so neither direction depends on the client's choice of source port.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
let mut expressions = ipv4();
|
|
248
|
-
expressions.extend(current_address(prefix, true)?);
|
|
249
|
-
program.end("raw_return", expressions, 0)?;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
for generation in &scope.generations {
|
|
253
|
-
for grant in &generation.grants {
|
|
254
|
-
if matches!(grant.destination, Destination::Platform { .. }) != platform {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
let mut original = raw_original(scope, grant)?;
|
|
258
|
-
original.extend(set_ct(17, generation.conntrack_zone.to_ne_bytes().to_vec()));
|
|
259
|
-
program.end(
|
|
260
|
-
if grant.source_endpoint.is_some() {
|
|
261
|
-
"raw_in"
|
|
262
|
-
} else {
|
|
263
|
-
"raw_out"
|
|
264
|
-
},
|
|
265
|
-
original,
|
|
266
|
-
1,
|
|
267
|
-
)?;
|
|
268
|
-
let mut reply = raw_return(scope, grant, generation)?;
|
|
269
|
-
reply.extend(set_ct(17, generation.conntrack_zone.to_ne_bytes().to_vec()));
|
|
270
|
-
program.end("raw_return", reply, 1)?;
|
|
271
|
-
}
|
|
116
|
+
published::endpoint(program, hop)?;
|
|
117
|
+
// Platform grants are classified ahead of the protected barrier, public
|
|
118
|
+
// grants behind it, whose protected destinations stay in the default zone.
|
|
119
|
+
leased::classification(program, scope, Kind::Platform)?;
|
|
120
|
+
for prefix in &scope.protection.prefixes {
|
|
121
|
+
for chain in ["raw_in", "raw_out"] {
|
|
122
|
+
let mut expressions = ipv4();
|
|
123
|
+
expressions.extend(current_address(prefix, false)?);
|
|
124
|
+
program.end(chain, expressions, -5)?;
|
|
272
125
|
}
|
|
126
|
+
let mut expressions = ipv4();
|
|
127
|
+
expressions.extend(current_address(prefix, true)?);
|
|
128
|
+
program.end("raw_return", expressions, 0)?;
|
|
273
129
|
}
|
|
130
|
+
leased::classification(program, scope, Kind::Public)?;
|
|
274
131
|
program.end("raw_return", vec![], 0)
|
|
275
132
|
}
|
|
276
|
-
fn
|
|
277
|
-
for (index, allocation) in scope.generations.iter().enumerate() {
|
|
278
|
-
for protocol in ["tcp", "udp"] {
|
|
279
|
-
if !allocation
|
|
280
|
-
.grants
|
|
281
|
-
.iter()
|
|
282
|
-
.any(|grant| grant.protocol == protocol)
|
|
283
|
-
{
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
let chain = format!("original_{index}_{protocol}");
|
|
287
|
-
program.chain(&chain, None)?;
|
|
288
|
-
// Only exact tuple/link/zone/direction classifiers jump here. Share
|
|
289
|
-
// state checks instead of repeating those classifiers three times.
|
|
290
|
-
for connection_state in [2, 8] {
|
|
291
|
-
let mut labelled = ct(13, None, label(allocation));
|
|
292
|
-
labelled.extend(state(connection_state));
|
|
293
|
-
if connection_state == 8 && protocol == "tcp" {
|
|
294
|
-
labelled.extend(opening_tcp());
|
|
295
|
-
}
|
|
296
|
-
program.end(&chain, labelled, 1)?;
|
|
297
|
-
}
|
|
298
|
-
let mut fresh = state(8);
|
|
299
|
-
fresh.extend(ct(13, None, vec![0; 16]));
|
|
300
|
-
if protocol == "tcp" {
|
|
301
|
-
fresh.extend(opening_tcp());
|
|
302
|
-
}
|
|
303
|
-
fresh.extend(set_ct(13, label(allocation)));
|
|
304
|
-
program.end(&chain, fresh, 1)?;
|
|
305
|
-
// A failed state/label match returns to the caller's remaining
|
|
306
|
-
// classifiers and terminal denials, exactly as the inlined rules.
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
Ok(())
|
|
310
|
-
}
|
|
311
|
-
fn grants(
|
|
312
|
-
program: &mut Program<'_>,
|
|
313
|
-
scope: &RouterScope,
|
|
314
|
-
chain: &str,
|
|
315
|
-
platform: bool,
|
|
316
|
-
) -> Result<()> {
|
|
317
|
-
for (index, allocation) in scope.generations.iter().enumerate() {
|
|
318
|
-
for grant in &allocation.grants {
|
|
319
|
-
if matches!(grant.destination, Destination::Platform { .. }) != platform {
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
let local = grant.source_endpoint.is_none();
|
|
323
|
-
if chain == "forward" && local || chain != "forward" && !local {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
if chain != "input" {
|
|
327
|
-
let mut original = filter(scope, grant, false)?;
|
|
328
|
-
original.extend(generation(allocation, 0, false));
|
|
329
|
-
// Current and original tuples remain checked on every packet;
|
|
330
|
-
// a label alone never authorizes a changed grant or endpoint.
|
|
331
|
-
program.jump(
|
|
332
|
-
chain,
|
|
333
|
-
original,
|
|
334
|
-
&format!("original_{index}_{}", grant.protocol),
|
|
335
|
-
)?;
|
|
336
|
-
}
|
|
337
|
-
if chain != "output" {
|
|
338
|
-
let mut reply = filter(scope, grant, true)?;
|
|
339
|
-
reply.extend(generation(allocation, 1, true));
|
|
340
|
-
reply.extend(state(2));
|
|
341
|
-
program.end(chain, reply, 1)?;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
Ok(())
|
|
346
|
-
}
|
|
347
|
-
pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision: u64) -> Result<()> {
|
|
133
|
+
pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
348
134
|
for (name, hook, priority, kind) in [
|
|
349
135
|
("raw_in", 0, -300, "filter"),
|
|
350
136
|
("raw_out", 3, -300, "filter"),
|
|
@@ -358,37 +144,24 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision:
|
|
|
358
144
|
if !scope.published_ports.is_empty() {
|
|
359
145
|
// Published destination NAT lives in this table's own prerouting chain, so
|
|
360
146
|
// it is created, replaced and deleted with the exact generation. An empty
|
|
361
|
-
// publication list keeps the previous chain set
|
|
147
|
+
// publication list keeps the previous chain set.
|
|
362
148
|
program.chain("pre", Some((0, -100, "nat")))?;
|
|
363
149
|
}
|
|
364
150
|
for name in ["raw_return", "protected_out", "protected_back"] {
|
|
365
151
|
program.chain(name, None)?;
|
|
366
152
|
}
|
|
153
|
+
let hop = hop(scope)?;
|
|
154
|
+
published::sets(program, &hop, true)?;
|
|
367
155
|
host_grant_sets(program, scope)?;
|
|
368
156
|
workloadgrant::sets(program, scope)?;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
endpoints: scope.endpoints.clone(),
|
|
373
|
-
rules: scope.rules.clone(),
|
|
374
|
-
}
|
|
375
|
-
.prepare()?;
|
|
376
|
-
for (name, rules) in private.guard_chains()? {
|
|
377
|
-
program.chain(&name, None)?;
|
|
378
|
-
for expressions in rules {
|
|
379
|
-
program.rule(&name, expressions)?;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
original_states(program, scope)?;
|
|
383
|
-
raw(program, scope)?;
|
|
157
|
+
private::sets(program, scope)?;
|
|
158
|
+
leased::sets(program, scope)?;
|
|
159
|
+
raw(program, scope, &hop)?;
|
|
384
160
|
protected(program, "protected_out", scope, false)?;
|
|
385
161
|
protected(program, "protected_back", scope, true)?;
|
|
386
|
-
|
|
162
|
+
published::translations(program, &hop)?;
|
|
387
163
|
for chain in ["input", "forward", "output"] {
|
|
388
|
-
|
|
389
|
-
for expressions in parts.guards {
|
|
390
|
-
program.rule(chain, expressions)?;
|
|
391
|
-
}
|
|
164
|
+
private::guards(program, scope, chain)?;
|
|
392
165
|
// Published admission keeps the private anti-spoof guards ahead of it and
|
|
393
166
|
// precedes the capture barrier: the barrier denies every protected original
|
|
394
167
|
// destination, which includes this transit address, and an exact inbound
|
|
@@ -396,14 +169,12 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision:
|
|
|
396
169
|
// Host grants take the same position for the same reason: the barrier
|
|
397
170
|
// denies the host's protected transit address in both directions.
|
|
398
171
|
if chain == "forward" {
|
|
399
|
-
|
|
172
|
+
published::inbound(program, &hop)?;
|
|
400
173
|
host_grant_admissions(program, scope)?;
|
|
401
174
|
workloadgrant::admissions(program, scope)?;
|
|
402
175
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
grants(program, scope, chain, true)?;
|
|
176
|
+
private::grants(program, scope, chain)?;
|
|
177
|
+
leased::admissions(program, scope, chain, Kind::Platform)?;
|
|
407
178
|
for incoming in [true, false] {
|
|
408
179
|
if chain == "input" && !incoming || chain == "output" && incoming {
|
|
409
180
|
continue;
|
|
@@ -418,10 +189,14 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision:
|
|
|
418
189
|
},
|
|
419
190
|
)?;
|
|
420
191
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
192
|
+
// Symmetric flows follow the capture barrier, so protected destinations
|
|
193
|
+
// stay denied; the endpoint classification already keeps them, and
|
|
194
|
+
// their replies, in the default zone.
|
|
195
|
+
if chain == "forward" {
|
|
196
|
+
published::symmetric(program, &hop)?;
|
|
424
197
|
}
|
|
198
|
+
leased::admissions(program, scope, chain, Kind::Public)?;
|
|
199
|
+
private::denials(program, scope, chain)?;
|
|
425
200
|
for incoming in [true, false] {
|
|
426
201
|
if chain == "input" && !incoming || chain == "output" && incoming {
|
|
427
202
|
continue;
|
|
@@ -429,16 +204,8 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision:
|
|
|
429
204
|
program.deny_link(chain, &scope.handoff, incoming)?;
|
|
430
205
|
}
|
|
431
206
|
}
|
|
432
|
-
for
|
|
433
|
-
|
|
434
|
-
let mut expressions = filter(scope, grant, false)?;
|
|
435
|
-
expressions.extend(generation(allocation, 0, true));
|
|
436
|
-
expressions.extend(snat(
|
|
437
|
-
&allocation.transit_source_address,
|
|
438
|
-
Some((grant.source_port_range.first, grant.source_port_range.last)),
|
|
439
|
-
)?);
|
|
440
|
-
program.rule("post", expressions)?;
|
|
441
|
-
}
|
|
207
|
+
for kind in [Kind::Platform, Kind::Public] {
|
|
208
|
+
leased::translation(program, scope, kind)?;
|
|
442
209
|
}
|
|
443
|
-
|
|
210
|
+
published::symmetric_translation(program, &hop)
|
|
444
211
|
}
|
package/rust/src/egress.rs
CHANGED
|
@@ -99,9 +99,28 @@ pub struct Generation {
|
|
|
99
99
|
pub struct RouterPublishedPort {
|
|
100
100
|
pub protocol: String,
|
|
101
101
|
pub transit_port: u16,
|
|
102
|
+
/// Last port of a published range starting at `transit_port`. A range keeps
|
|
103
|
+
/// every port: `endpoint_port` equals `transit_port`. Absent for one port.
|
|
104
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
105
|
+
pub transit_port_end: Option<u16>,
|
|
102
106
|
pub endpoint_port: u16,
|
|
103
107
|
pub endpoint_address: String,
|
|
104
108
|
pub transit_source_address: String,
|
|
109
|
+
/// The workload may open flows from its published port(s), translated to
|
|
110
|
+
/// `transit_source_address` and the published port. Absent and false are the
|
|
111
|
+
/// same canonical policy.
|
|
112
|
+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
113
|
+
pub symmetric: bool,
|
|
114
|
+
}
|
|
115
|
+
impl RouterPublishedPort {
|
|
116
|
+
/// Last published transit port; equal to `transit_port` for one port.
|
|
117
|
+
pub fn transit_last(&self) -> u16 {
|
|
118
|
+
self.transit_port_end.unwrap_or(self.transit_port)
|
|
119
|
+
}
|
|
120
|
+
/// Last workload port the publication translates to.
|
|
121
|
+
pub fn endpoint_last(&self) -> u16 {
|
|
122
|
+
self.endpoint_port + (self.transit_last() - self.transit_port)
|
|
123
|
+
}
|
|
105
124
|
}
|
|
106
125
|
/// One exact host-origin flow: the node's own host network namespace dials one
|
|
107
126
|
/// port of one local workload at its lease address, from the transit host
|
|
@@ -150,9 +169,28 @@ pub struct HostHandoff {
|
|
|
150
169
|
pub struct PublishedPort {
|
|
151
170
|
pub protocol: String,
|
|
152
171
|
pub host_port: u16,
|
|
172
|
+
/// Last port of a published range starting at `host_port`. A range keeps
|
|
173
|
+
/// every port: `target_port` equals `host_port`. Absent for one port.
|
|
174
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
175
|
+
pub host_port_end: Option<u16>,
|
|
153
176
|
pub target_port: u16,
|
|
154
177
|
pub target_address: String,
|
|
155
178
|
pub host_ip: String,
|
|
179
|
+
/// Flows from `target_address` and the published target port(s) may leave
|
|
180
|
+
/// through the uplink, translated to `host_ip` and the published port. Absent
|
|
181
|
+
/// and false are the same canonical policy.
|
|
182
|
+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
183
|
+
pub symmetric: bool,
|
|
184
|
+
}
|
|
185
|
+
impl PublishedPort {
|
|
186
|
+
/// Last published uplink port; equal to `host_port` for one port.
|
|
187
|
+
pub fn host_last(&self) -> u16 {
|
|
188
|
+
self.host_port_end.unwrap_or(self.host_port)
|
|
189
|
+
}
|
|
190
|
+
/// Last transit port the publication translates to.
|
|
191
|
+
pub fn target_last(&self) -> u16 {
|
|
192
|
+
self.target_port + (self.host_last() - self.host_port)
|
|
193
|
+
}
|
|
156
194
|
}
|
|
157
195
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
158
196
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -446,16 +484,19 @@ pub fn destination(grant: &Grant, protection: &Protection) -> Result<String> {
|
|
|
446
484
|
/// with the leased outbound source ports translated to that same transit address.
|
|
447
485
|
fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
|
|
448
486
|
let mut ports = std::mem::take(&mut value.published_ports);
|
|
449
|
-
require(ports.len() <=
|
|
487
|
+
require(ports.len() <= PUBLISHED_PORTS)?;
|
|
450
488
|
ports.sort();
|
|
451
489
|
for (index, port) in ports.iter().enumerate() {
|
|
452
490
|
require(
|
|
453
491
|
protocol(&port.protocol)
|
|
454
492
|
&& port.transit_port > 0
|
|
455
493
|
&& port.endpoint_port > 0
|
|
494
|
+
&& published_span(port.transit_port, port.transit_port_end, port.endpoint_port)
|
|
495
|
+
// Sorted by protocol and first port, so disjoint neighbours are
|
|
496
|
+
// disjoint spans: one protocol/port is published exactly once.
|
|
456
497
|
&& (index == 0
|
|
457
498
|
|| ports[index - 1].protocol != port.protocol
|
|
458
|
-
|| ports[index - 1].
|
|
499
|
+
|| ports[index - 1].transit_last() < port.transit_port),
|
|
459
500
|
)?;
|
|
460
501
|
// rtnetlink verifies this exact handoff address at apply, recovery and
|
|
461
502
|
// inspection, and only a current leased transit source address receives a
|
|
@@ -511,15 +552,47 @@ fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
|
|
|
511
552
|
// port inside a leased outbound range is a translation conflict.
|
|
512
553
|
require(
|
|
513
554
|
range.protocol != port.protocol
|
|
514
|
-
|| port.
|
|
555
|
+
|| port.transit_last() < range.first
|
|
515
556
|
|| port.transit_port > range.last,
|
|
516
557
|
)?;
|
|
517
558
|
}
|
|
518
559
|
}
|
|
519
560
|
}
|
|
561
|
+
// A symmetric publication translates its workload's own flows back to its
|
|
562
|
+
// published ports, so its workload ports belong to it alone.
|
|
563
|
+
exclusive_symmetric(ports.iter().map(|port| {
|
|
564
|
+
(
|
|
565
|
+
port.symmetric,
|
|
566
|
+
(&port.protocol, &port.endpoint_address),
|
|
567
|
+
(port.endpoint_port, port.endpoint_last()),
|
|
568
|
+
)
|
|
569
|
+
}))?;
|
|
520
570
|
value.published_ports = ports;
|
|
521
571
|
Ok(())
|
|
522
572
|
}
|
|
573
|
+
/// One published port, or a range `first..=end` that keeps every port. The range
|
|
574
|
+
/// is strictly ascending: one port has exactly one canonical form, without an end.
|
|
575
|
+
fn published_span(first: u16, end: Option<u16>, inside: u16) -> bool {
|
|
576
|
+
end.is_none_or(|end| end > first && inside == first)
|
|
577
|
+
}
|
|
578
|
+
/// A symmetric publication's inside span may not overlap the inside span of any
|
|
579
|
+
/// other publication of the same protocol and address, or its workload's flows
|
|
580
|
+
/// would have two published sources.
|
|
581
|
+
fn exclusive_symmetric<'a>(
|
|
582
|
+
spans: impl Iterator<Item = (bool, (&'a String, &'a String), (u16, u16))> + Clone,
|
|
583
|
+
) -> Result<()> {
|
|
584
|
+
for (index, (symmetric, key, (first, last))) in spans.clone().enumerate() {
|
|
585
|
+
if !symmetric {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
for (other, (_, other_key, (other_first, other_last))) in spans.clone().enumerate() {
|
|
589
|
+
require(
|
|
590
|
+
other == index || other_key != key || last < other_first || other_last < first,
|
|
591
|
+
)?;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
Ok(())
|
|
595
|
+
}
|
|
523
596
|
/// Exact tuples only: one protocol, one unicast source and destination address
|
|
524
597
|
/// and one destination port. Addresses never carry a prefix and ports never a
|
|
525
598
|
/// range, so a grant cannot widen past its flow. `bound` checks the authority of
|
|
@@ -630,17 +703,25 @@ fn normalize_router_workload_grants(value: &mut RouterScope) -> Result<()> {
|
|
|
630
703
|
value.workload_grants = grants;
|
|
631
704
|
Ok(())
|
|
632
705
|
}
|
|
706
|
+
/// Router input bounds. Endpoints, rules and grants are set elements, so the
|
|
707
|
+
/// graph budgets bind them, not the rule reserve: the node shape of 128
|
|
708
|
+
/// workloads with local DNS, public egress and four platform endpoints each fits.
|
|
709
|
+
pub const ROUTER_LINKS: usize = 128;
|
|
710
|
+
pub const ROUTER_RULES: usize = 1024;
|
|
711
|
+
pub const ROUTER_GRANTS: usize = 1024;
|
|
712
|
+
/// Publications per scope; they are set elements on both hops.
|
|
713
|
+
pub const PUBLISHED_PORTS: usize = 1024;
|
|
633
714
|
fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
634
|
-
require(value.links.len() <=
|
|
715
|
+
require(value.links.len() <= ROUTER_LINKS && value.generations.len() <= 32)?;
|
|
635
716
|
let private = policy::Policy {
|
|
636
717
|
schema_version: 1,
|
|
637
718
|
revision,
|
|
638
719
|
endpoints: value.endpoints.clone(),
|
|
639
720
|
rules: value.rules.clone(),
|
|
640
721
|
}
|
|
641
|
-
.
|
|
642
|
-
value.endpoints = private.
|
|
643
|
-
value.rules = private.
|
|
722
|
+
.normalize(ROUTER_LINKS, ROUTER_RULES)?;
|
|
723
|
+
value.endpoints = private.endpoints;
|
|
724
|
+
value.rules = private.rules;
|
|
644
725
|
normalize_protection(&mut value.protection)?;
|
|
645
726
|
normalize_link(&mut value.handoff)?;
|
|
646
727
|
require(
|
|
@@ -725,7 +806,7 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
725
806
|
&& labels.insert(generation.conntrack_label.clone()),
|
|
726
807
|
)?;
|
|
727
808
|
count += generation.grants.len();
|
|
728
|
-
require(count <=
|
|
809
|
+
require(count <= ROUTER_GRANTS)?;
|
|
729
810
|
generation.grants.sort();
|
|
730
811
|
for grant in &generation.grants {
|
|
731
812
|
require(
|
|
@@ -903,16 +984,19 @@ fn normalize_host_host_grants(value: &mut HostScope) -> Result<()> {
|
|
|
903
984
|
/// overlap with the leased outbound source ports translated to that same address.
|
|
904
985
|
fn normalize_published(value: &mut HostScope) -> Result<()> {
|
|
905
986
|
let mut ports = std::mem::take(&mut value.published_ports);
|
|
906
|
-
require(ports.len() <=
|
|
987
|
+
require(ports.len() <= PUBLISHED_PORTS)?;
|
|
907
988
|
ports.sort();
|
|
908
989
|
for (index, port) in ports.iter().enumerate() {
|
|
909
990
|
require(
|
|
910
991
|
protocol(&port.protocol)
|
|
911
992
|
&& port.host_port > 0
|
|
912
993
|
&& port.target_port > 0
|
|
994
|
+
&& published_span(port.host_port, port.host_port_end, port.target_port)
|
|
995
|
+
// Sorted by protocol and first port, so disjoint neighbours are
|
|
996
|
+
// disjoint spans: one protocol/port is published exactly once.
|
|
913
997
|
&& (index == 0
|
|
914
998
|
|| ports[index - 1].protocol != port.protocol
|
|
915
|
-
|| ports[index - 1].
|
|
999
|
+
|| ports[index - 1].host_last() < port.host_port),
|
|
916
1000
|
)?;
|
|
917
1001
|
// rtnetlink verifies this exact uplink address at apply, recovery and
|
|
918
1002
|
// inspection. No wildcard, secondary-link or default-route inference.
|
|
@@ -934,14 +1018,39 @@ fn normalize_published(value: &mut HostScope) -> Result<()> {
|
|
|
934
1018
|
// port inside a leased outbound range is a translation conflict.
|
|
935
1019
|
require(
|
|
936
1020
|
range.protocol != port.protocol
|
|
937
|
-
|| port.
|
|
1021
|
+
|| port.host_last() < range.first
|
|
938
1022
|
|| port.host_port > range.last,
|
|
939
1023
|
)?;
|
|
940
1024
|
}
|
|
941
1025
|
}
|
|
942
1026
|
}
|
|
943
1027
|
}
|
|
1028
|
+
if port.symmetric {
|
|
1029
|
+
// The workload's flows leave from its target ports; a leased range of
|
|
1030
|
+
// the same address would claim them for the outer leased translation.
|
|
1031
|
+
for allocation in value
|
|
1032
|
+
.handoffs
|
|
1033
|
+
.iter()
|
|
1034
|
+
.flat_map(|handoff| handoff.allocations.iter())
|
|
1035
|
+
.filter(|allocation| allocation.transit_source_address == port.target_address)
|
|
1036
|
+
{
|
|
1037
|
+
for range in &allocation.source_port_ranges {
|
|
1038
|
+
require(
|
|
1039
|
+
range.protocol != port.protocol
|
|
1040
|
+
|| port.target_last() < range.first
|
|
1041
|
+
|| port.target_port > range.last,
|
|
1042
|
+
)?;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
944
1046
|
}
|
|
1047
|
+
exclusive_symmetric(ports.iter().map(|port| {
|
|
1048
|
+
(
|
|
1049
|
+
port.symmetric,
|
|
1050
|
+
(&port.protocol, &port.target_address),
|
|
1051
|
+
(port.target_port, port.target_last()),
|
|
1052
|
+
)
|
|
1053
|
+
}))?;
|
|
945
1054
|
value.published_ports = ports;
|
|
946
1055
|
Ok(())
|
|
947
1056
|
}
|
|
@@ -1062,6 +1171,12 @@ pub(crate) mod localplatform_tests;
|
|
|
1062
1171
|
#[path = "egress_localport_tests.rs"]
|
|
1063
1172
|
pub(crate) mod localport_tests;
|
|
1064
1173
|
#[cfg(test)]
|
|
1174
|
+
#[path = "egress_publishedrange_tests.rs"]
|
|
1175
|
+
mod publishedrange_tests;
|
|
1176
|
+
#[cfg(test)]
|
|
1177
|
+
#[path = "egress_routerequivalence_tests.rs"]
|
|
1178
|
+
mod routerequivalence_tests;
|
|
1179
|
+
#[cfg(test)]
|
|
1065
1180
|
#[path = "egress_tests.rs"]
|
|
1066
1181
|
pub(crate) mod tests;
|
|
1067
1182
|
#[cfg(test)]
|