@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
|
@@ -389,6 +389,8 @@ fn v2_host_publishes_exact_uplink_ports_and_removes_them_with_the_generation() {
|
|
|
389
389
|
target_port: 8443,
|
|
390
390
|
target_address: "10.240.0.2".into(),
|
|
391
391
|
host_ip: "192.0.2.2".into(),
|
|
392
|
+
host_port_end: None,
|
|
393
|
+
symmetric: false,
|
|
392
394
|
},
|
|
393
395
|
egress::PublishedPort {
|
|
394
396
|
protocol: "udp".into(),
|
|
@@ -396,6 +398,8 @@ fn v2_host_publishes_exact_uplink_ports_and_removes_them_with_the_generation() {
|
|
|
396
398
|
target_port: 8125,
|
|
397
399
|
target_address: "10.240.0.2".into(),
|
|
398
400
|
host_ip: "192.0.2.2".into(),
|
|
401
|
+
host_port_end: None,
|
|
402
|
+
symmetric: false,
|
|
399
403
|
},
|
|
400
404
|
];
|
|
401
405
|
let mut owner = Owner::new(options("host_published")).unwrap();
|
|
@@ -168,94 +168,162 @@ fn egress_identity_rejects_unknown_duplicate_or_incomplete_operands() {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/// The compiled chains, rules, and sets with their element identities of one
|
|
172
|
+
/// prepared policy, as the owner's graph comparison sees them.
|
|
173
|
+
type Compiled = (
|
|
174
|
+
Vec<Vec<u8>>,
|
|
175
|
+
Vec<(String, Vec<u8>)>,
|
|
176
|
+
std::collections::BTreeMap<String, (Vec<u8>, Vec<Vec<u8>>)>,
|
|
177
|
+
);
|
|
178
|
+
fn compiled(value: serde_json::Value) -> Compiled {
|
|
179
|
+
let prepared = serde_json::from_value::<crate::egress::Policy>(value)
|
|
180
|
+
.unwrap()
|
|
181
|
+
.prepare()
|
|
182
|
+
.unwrap();
|
|
183
|
+
let (mut chains, mut rules) = (Vec::new(), Vec::new());
|
|
184
|
+
let mut sets = std::collections::BTreeMap::new();
|
|
185
|
+
for (kind, attributes) in prepared.program("snft_identity").unwrap() {
|
|
186
|
+
match kind {
|
|
187
|
+
3 => chains.push(super::chain_identity(&attributes).unwrap()),
|
|
188
|
+
6 => {
|
|
189
|
+
// The stamp carries the policy digest; compare the rule itself.
|
|
190
|
+
let unstamped: Vec<Attr> = attributes
|
|
191
|
+
.iter()
|
|
192
|
+
.map(|attribute| match attribute.id() {
|
|
193
|
+
7 => Attr::bytes(7, b"stamp".to_vec()),
|
|
194
|
+
_ => attribute.clone(),
|
|
195
|
+
})
|
|
196
|
+
.collect();
|
|
197
|
+
rules.push(rule_identity(&unstamped).unwrap());
|
|
198
|
+
}
|
|
199
|
+
9 => {
|
|
200
|
+
// The kernel's dump of the same set has the same identity.
|
|
201
|
+
let identity = set_identity(&attributes).unwrap();
|
|
202
|
+
assert_eq!(set_identity(&dumped_set(&attributes)).unwrap(), identity);
|
|
203
|
+
sets.insert(wire::text(&attributes, 2).unwrap(), (identity, Vec::new()));
|
|
204
|
+
}
|
|
205
|
+
12 => {
|
|
206
|
+
let list = wire::attrs(&wire::one(&attributes, 3).unwrap().value).unwrap();
|
|
207
|
+
sets.get_mut(&wire::text(&attributes, 2).unwrap())
|
|
208
|
+
.unwrap()
|
|
209
|
+
.1
|
|
210
|
+
.extend(element_keys(&list).unwrap());
|
|
211
|
+
}
|
|
212
|
+
other => panic!("operation {other}"),
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
(chains, rules, sets)
|
|
216
|
+
}
|
|
217
|
+
|
|
171
218
|
/// A published host-transit generation must survive the same apply-time graph
|
|
172
|
-
/// identity as the rest of the table:
|
|
173
|
-
/// operand, so destination NAT and its admissions cannot be
|
|
219
|
+
/// identity as the rest of the table: its rules and every set element are
|
|
220
|
+
/// compared operand by operand, so destination NAT and its admissions cannot be
|
|
221
|
+
/// silently rewritten, while the rules stay the same for any publication.
|
|
174
222
|
#[test]
|
|
175
223
|
fn published_host_graph_identity_preserves_destination_nat_and_admission_operands() {
|
|
176
|
-
let
|
|
177
|
-
|
|
224
|
+
let host = |target_port: u16| {
|
|
225
|
+
compiled(crate::egress::tests::published_host(serde_json::json!([
|
|
178
226
|
crate::egress::tests::published_port("tcp", 443, target_port)
|
|
179
|
-
]))
|
|
180
|
-
let prepared = serde_json::from_value::<crate::egress::Policy>(value)
|
|
181
|
-
.unwrap()
|
|
182
|
-
.prepare()
|
|
183
|
-
.unwrap();
|
|
184
|
-
let mut chains = Vec::new();
|
|
185
|
-
let mut rules = Vec::new();
|
|
186
|
-
for (kind, attributes) in prepared.program("snft_identity").unwrap() {
|
|
187
|
-
if kind == 3 {
|
|
188
|
-
chains.push(super::chain_identity(&attributes).unwrap());
|
|
189
|
-
} else {
|
|
190
|
-
rules.push(rule_identity(&attributes).unwrap());
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
(chains, rules)
|
|
227
|
+
])))
|
|
194
228
|
};
|
|
195
|
-
let (chains, rules) =
|
|
229
|
+
let (chains, rules, sets) = host(8443);
|
|
196
230
|
assert_eq!(rules.iter().filter(|(chain, _)| chain == "pre").count(), 1);
|
|
197
231
|
assert_eq!(chains.len(), 7);
|
|
198
|
-
let
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
232
|
+
let (_, changed_rules, changed_sets) = host(9443);
|
|
233
|
+
assert_eq!(rules, changed_rules);
|
|
234
|
+
// The translation's port is map data and the admission's port is a key.
|
|
235
|
+
for name in ["published_port", "published_forward"] {
|
|
236
|
+
assert_eq!(sets[name].0, changed_sets[name].0);
|
|
237
|
+
assert_ne!(sets[name].1, changed_sets[name].1, "{name}");
|
|
238
|
+
}
|
|
206
239
|
}
|
|
207
240
|
|
|
208
241
|
/// A published router generation must survive the same apply-time graph identity
|
|
209
|
-
/// as the rest of the table:
|
|
210
|
-
/// the workload translation
|
|
242
|
+
/// as the rest of the table: its rules and every set element are compared operand
|
|
243
|
+
/// by operand, so the workload translation, both classifications and the
|
|
244
|
+
/// admissions cannot be silently rewritten.
|
|
211
245
|
#[test]
|
|
212
246
|
fn published_router_graph_identity_preserves_destination_nat_and_admission_operands() {
|
|
213
|
-
let
|
|
214
|
-
|
|
247
|
+
let router = |endpoint_port: u16| {
|
|
248
|
+
compiled(crate::egress::tests::published_router(serde_json::json!([
|
|
215
249
|
crate::egress::tests::router_published_port("tcp", 443, endpoint_port)
|
|
216
|
-
]))
|
|
217
|
-
let prepared = serde_json::from_value::<crate::egress::Policy>(value)
|
|
218
|
-
.unwrap()
|
|
219
|
-
.prepare()
|
|
220
|
-
.unwrap();
|
|
221
|
-
let mut chains = Vec::new();
|
|
222
|
-
let mut rules = Vec::new();
|
|
223
|
-
for (kind, attributes) in prepared.program("snft_identity").unwrap() {
|
|
224
|
-
if kind == 3 {
|
|
225
|
-
chains.push(super::chain_identity(&attributes).unwrap());
|
|
226
|
-
} else {
|
|
227
|
-
rules.push(rule_identity(&attributes).unwrap());
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
(chains, rules)
|
|
250
|
+
])))
|
|
231
251
|
};
|
|
232
|
-
let (chains, rules) =
|
|
252
|
+
let (chains, rules, sets) = router(8443);
|
|
233
253
|
assert_eq!(rules.iter().filter(|(chain, _)| chain == "pre").count(), 1);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
let (_,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
254
|
+
// The router's base chains, the pre chain and its three regular chains.
|
|
255
|
+
assert_eq!(chains.len(), 10);
|
|
256
|
+
let (_, changed_rules, changed_sets) = router(9443);
|
|
257
|
+
assert_eq!(rules, changed_rules);
|
|
258
|
+
for name in ["published_port", "published_forward", "published_endpoint"] {
|
|
259
|
+
assert_ne!(sets[name].1, changed_sets[name].1, "{name}");
|
|
260
|
+
}
|
|
261
|
+
assert_eq!(sets["published_arrival"], changed_sets["published_arrival"]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// Interval elements carry their last key and a map's data in their identity;
|
|
265
|
+
/// a changed bound or datum is a changed element, and a set description is only
|
|
266
|
+
/// ever a concatenation.
|
|
267
|
+
#[test]
|
|
268
|
+
fn interval_sets_and_elements_have_exact_graph_identity() {
|
|
269
|
+
let (_, _, sets) = compiled(crate::egress::tests::published_router(serde_json::json!([
|
|
270
|
+
crate::egress::tests::router_published_port("tcp", 443, 8443)
|
|
271
|
+
])));
|
|
244
272
|
assert_eq!(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.iter()
|
|
248
|
-
.filter(|(chain, _)| chain == "raw_return")
|
|
249
|
-
.count()
|
|
273
|
+
sets.keys().filter(|name| name.starts_with("published_")).count(),
|
|
274
|
+
4
|
|
250
275
|
);
|
|
276
|
+
let element = |last: u8, data: Option<u8>| {
|
|
277
|
+
let value = |kind: u16, byte: u8| Attr::nested(kind, vec![Attr::bytes(1, vec![byte; 8])]);
|
|
278
|
+
let mut fields = vec![value(1, 1), value(10, last)];
|
|
279
|
+
if let Some(data) = data {
|
|
280
|
+
fields.push(value(2, data));
|
|
281
|
+
}
|
|
282
|
+
Attr::nested(1, fields)
|
|
283
|
+
};
|
|
284
|
+
let identity = |attribute: Attr| element_keys(&[attribute]).unwrap();
|
|
285
|
+
let exact = identity(element(1, Some(4)));
|
|
286
|
+
assert_ne!(identity(element(2, Some(4))), exact);
|
|
287
|
+
assert_ne!(identity(element(1, Some(5))), exact);
|
|
288
|
+
assert_ne!(identity(element(1, None)), exact);
|
|
289
|
+
for extension in [
|
|
290
|
+
Attr::u32(3, 1),
|
|
291
|
+
Attr::nested(10, vec![Attr::bytes(1, vec![1; 8])]),
|
|
292
|
+
] {
|
|
293
|
+
let Attr { value, .. } = element(1, Some(4));
|
|
294
|
+
let mut fields = wire::attrs(&value).unwrap();
|
|
295
|
+
fields.push(extension);
|
|
296
|
+
assert!(element_keys(&[Attr::nested(1, fields)]).is_err());
|
|
297
|
+
}
|
|
251
298
|
}
|
|
252
299
|
|
|
253
|
-
/// A
|
|
254
|
-
///
|
|
300
|
+
/// A set as Linux dumps it: the compiled creation plus the kernel's handle, its
|
|
301
|
+
/// description (empty for a plain key, the concatenation's field lengths without
|
|
302
|
+
/// nesting flags otherwise), the backend type and the element count.
|
|
255
303
|
fn dumped_set(created: &[Attr]) -> Vec<Attr> {
|
|
256
|
-
let mut dump: Vec<Attr> = created
|
|
304
|
+
let mut dump: Vec<Attr> = created
|
|
305
|
+
.iter()
|
|
306
|
+
.filter(|attribute| ![9, 10].contains(&attribute.id()))
|
|
307
|
+
.cloned()
|
|
308
|
+
.collect();
|
|
257
309
|
dump.push(Attr::u64(16, 7));
|
|
258
|
-
|
|
310
|
+
let fields = created
|
|
311
|
+
.iter()
|
|
312
|
+
.find(|attribute| attribute.id() == 9)
|
|
313
|
+
.map(|description| {
|
|
314
|
+
let concatenation = wire::attrs(&description.value).unwrap();
|
|
315
|
+
wire::attrs(&wire::one(&concatenation, 2).unwrap().value)
|
|
316
|
+
.unwrap()
|
|
317
|
+
.iter()
|
|
318
|
+
.map(|field| {
|
|
319
|
+
Attr::bytes(1, wire::encode_attrs(&[Attr::u32(1, wire::number(&wire::attrs(&field.value).unwrap(), 1).unwrap())]))
|
|
320
|
+
})
|
|
321
|
+
.collect::<Vec<_>>()
|
|
322
|
+
});
|
|
323
|
+
dump.push(match fields {
|
|
324
|
+
Some(fields) => Attr::bytes(9, wire::encode_attrs(&[Attr::bytes(2, wire::encode_attrs(&fields))])),
|
|
325
|
+
None => Attr::nested(9, vec![]),
|
|
326
|
+
});
|
|
259
327
|
dump.push(Attr::string(19, "nft_rhash_type"));
|
|
260
328
|
dump.push(Attr::u32(20, 1024));
|
|
261
329
|
dump
|
|
@@ -273,7 +341,11 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
|
|
|
273
341
|
.prepare()
|
|
274
342
|
.unwrap();
|
|
275
343
|
let program = prepared.program("snft_identity").unwrap();
|
|
276
|
-
let sets: Vec<_> = program
|
|
344
|
+
let sets: Vec<_> = program
|
|
345
|
+
.iter()
|
|
346
|
+
.filter(|(kind, attributes)| *kind == 9 && wire::text(attributes, 2).unwrap().starts_with("host_grant"))
|
|
347
|
+
.map(|(_, attributes)| attributes.clone())
|
|
348
|
+
.collect();
|
|
277
349
|
assert_eq!(sets.len(), 2);
|
|
278
350
|
for created in &sets {
|
|
279
351
|
// The kernel's informational attributes are not identity.
|
|
@@ -294,21 +366,30 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
|
|
|
294
366
|
assert!(set_identity(&changed).is_err());
|
|
295
367
|
}
|
|
296
368
|
}
|
|
297
|
-
// Elements are plain keys;
|
|
298
|
-
let (_, elements) = program
|
|
369
|
+
// Elements are plain keys; flags reject, and data is a different element.
|
|
370
|
+
let (_, elements) = program
|
|
371
|
+
.iter()
|
|
372
|
+
.find(|(kind, attributes)| *kind == 12 && wire::text(attributes, 2).unwrap() == "host_grant")
|
|
373
|
+
.unwrap();
|
|
299
374
|
let list = wire::attrs(&wire::one(elements, 3).unwrap().value).unwrap();
|
|
300
375
|
assert_eq!(element_keys(&list).unwrap().len(), 1);
|
|
301
376
|
let key = wire::attrs(&list[0].value).unwrap();
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
377
|
+
let mut flagged = key.clone();
|
|
378
|
+
flagged.push(Attr::u32(3, 1));
|
|
379
|
+
assert!(element_keys(&[Attr::nested(1, flagged)]).is_err());
|
|
380
|
+
let mut mapped = key.clone();
|
|
381
|
+
mapped.push(Attr::nested(2, vec![Attr::bytes(1, vec![1; 4])]));
|
|
382
|
+
assert_ne!(
|
|
383
|
+
element_keys(&[Attr::nested(1, mapped)]).unwrap(),
|
|
384
|
+
element_keys(&list[..1]).unwrap()
|
|
385
|
+
);
|
|
386
|
+
// Five rules look up the host grant sets (the router's private and leased
|
|
387
|
+
// sets have their own lookups); the set name and key registers are identity.
|
|
309
388
|
let lookups: Vec<_> = program
|
|
310
389
|
.iter()
|
|
311
|
-
.filter(|(kind, attributes)|
|
|
390
|
+
.filter(|(kind, attributes)| {
|
|
391
|
+
*kind == 6 && wire::encode_attrs(attributes).windows(10).any(|window| window == b"host_grant")
|
|
392
|
+
})
|
|
312
393
|
.collect();
|
|
313
394
|
assert_eq!(lookups.len(), 5);
|
|
314
395
|
let lookup = |data: Vec<Attr>| rule(vec![expression("lookup", data)]);
|
|
@@ -317,13 +398,18 @@ fn host_grant_sets_elements_and_lookups_have_exact_graph_identity() {
|
|
|
317
398
|
for changed in [
|
|
318
399
|
vec![Attr::string(1, "host_grant_arrival"), Attr::u32(2, 1), Attr::u32(5, 0)],
|
|
319
400
|
vec![Attr::string(1, "host_grant"), Attr::u32(2, 9), Attr::u32(5, 0)],
|
|
401
|
+
// A map lookup's data register and an absence test are identity.
|
|
402
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2), Attr::u32(5, 0)],
|
|
403
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(5, 1)],
|
|
320
404
|
] {
|
|
321
405
|
assert_ne!(rule_identity(&lookup(changed)).unwrap(), identity);
|
|
322
406
|
}
|
|
323
407
|
for invalid in [
|
|
324
|
-
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(5,
|
|
408
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(5, 2)],
|
|
325
409
|
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1)],
|
|
326
|
-
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2)
|
|
410
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2)],
|
|
411
|
+
// A map lookup is never inverted.
|
|
412
|
+
vec![Attr::string(1, "host_grant"), Attr::u32(2, 1), Attr::u32(3, 2), Attr::u32(5, 1)],
|
|
327
413
|
] {
|
|
328
414
|
assert!(rule_identity(&lookup(invalid)).is_err());
|
|
329
415
|
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
//! Published port ranges and symmetric publications across both hops, in the
|
|
2
|
+
//! four-namespace topology of the router publication qualification.
|
|
3
|
+
use super::egress_traffic_tests::{allocation, binding, connect, denied, host_policy, ns_ip};
|
|
4
|
+
use super::egress_traffic_tests::{protection, roundtrip, socket};
|
|
5
|
+
use super::host_traffic_tests::{control, dark_udp, published_udp};
|
|
6
|
+
use super::router_traffic_tests::connect_from;
|
|
7
|
+
use super::*;
|
|
8
|
+
use crate::egress;
|
|
9
|
+
use serde_json::json;
|
|
10
|
+
use std::io::{Read, Write};
|
|
11
|
+
use std::net::TcpListener;
|
|
12
|
+
use std::time::Duration;
|
|
13
|
+
|
|
14
|
+
const WORK: &str = "pr_work";
|
|
15
|
+
const ROUTER: &str = "pr_router";
|
|
16
|
+
const HOST: &str = "pr_host";
|
|
17
|
+
const PEER: &str = "pr_peer";
|
|
18
|
+
|
|
19
|
+
/// The SIP/RTP shape on both hops: UDP and TCP signalling on one port and one UDP
|
|
20
|
+
/// RTP range, all symmetric, beside a plain publication and leased egress.
|
|
21
|
+
fn host_ranges() -> egress::Policy {
|
|
22
|
+
let mut policy = host_policy();
|
|
23
|
+
let egress::Scope::HostTransit(scope) = &mut policy.scope else {
|
|
24
|
+
panic!()
|
|
25
|
+
};
|
|
26
|
+
scope.published_ports = [
|
|
27
|
+
("udp", 20000, Some(20200), 20000, true),
|
|
28
|
+
("udp", 5070, None, 5070, true),
|
|
29
|
+
("tcp", 5070, None, 5070, true),
|
|
30
|
+
("udp", 8125, None, 8125, false),
|
|
31
|
+
]
|
|
32
|
+
.into_iter()
|
|
33
|
+
.map(
|
|
34
|
+
|(protocol, host_port, host_port_end, target_port, symmetric)| egress::PublishedPort {
|
|
35
|
+
protocol: protocol.into(),
|
|
36
|
+
host_port,
|
|
37
|
+
host_port_end,
|
|
38
|
+
target_port,
|
|
39
|
+
target_address: "10.240.0.2".into(),
|
|
40
|
+
host_ip: "192.0.2.2".into(),
|
|
41
|
+
symmetric,
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
.collect();
|
|
45
|
+
policy
|
|
46
|
+
}
|
|
47
|
+
fn router_ranges() -> egress::Policy {
|
|
48
|
+
let link = binding("work", "10.241.0.1", "veth");
|
|
49
|
+
let mut generation = allocation();
|
|
50
|
+
generation["conntrackZone"] = json!(17);
|
|
51
|
+
generation["conntrackLabel"] = json!("a".repeat(32));
|
|
52
|
+
generation["grants"] = json!([
|
|
53
|
+
{"sourceEndpoint":"work","sourcePrefix":"10.241.0.2/32","destination":{"kind":"public","prefix":"203.0.113.2/32"},
|
|
54
|
+
"protocol":"udp","sourcePort":null,"destinationPort":42000,"sourcePortRange":{"first":10000,"last":10015}}
|
|
55
|
+
]);
|
|
56
|
+
let publication = |protocol: &str,
|
|
57
|
+
port: u16,
|
|
58
|
+
end: Option<u16>,
|
|
59
|
+
endpoint: u16,
|
|
60
|
+
symmetric: bool| {
|
|
61
|
+
let mut value = json!({"protocol":protocol,"transitPort":port,"endpointPort":endpoint,
|
|
62
|
+
"endpointAddress":"10.241.0.2","transitSourceAddress":"10.240.0.2","symmetric":symmetric});
|
|
63
|
+
if let Some(end) = end {
|
|
64
|
+
value["transitPortEnd"] = json!(end);
|
|
65
|
+
}
|
|
66
|
+
value
|
|
67
|
+
};
|
|
68
|
+
serde_json::from_value(
|
|
69
|
+
json!({"schemaVersion":2,"revision":1,"scope":{"kind":"routerEgress",
|
|
70
|
+
"endpoints":[{"id":"work","interfaceIndex":link["interfaceIndex"],"interfaceName":"work",
|
|
71
|
+
"interfaceKind":"veth","sourcePrefixes":["10.241.0.2/32"]}],
|
|
72
|
+
"rules":[],"links":[link],"handoff":binding("handoff","10.240.0.2","veth"),
|
|
73
|
+
"protection":protection(),"generations":[generation],"publishedPorts":[
|
|
74
|
+
publication("udp", 20000, Some(20200), 20000, true),
|
|
75
|
+
publication("udp", 5070, None, 5070, true),
|
|
76
|
+
publication("tcp", 5070, None, 5070, true),
|
|
77
|
+
publication("udp", 8125, None, 9125, false)
|
|
78
|
+
]}}),
|
|
79
|
+
)
|
|
80
|
+
.unwrap()
|
|
81
|
+
}
|
|
82
|
+
/// Binds and applies a policy inside its namespace, where its links exist, and
|
|
83
|
+
/// returns the owned sets with their element counts as the kernel dumps them.
|
|
84
|
+
/// Enforcement means the dump, every interval element and map datum included,
|
|
85
|
+
/// matched the compiled graph.
|
|
86
|
+
fn apply(
|
|
87
|
+
namespace: &str,
|
|
88
|
+
name: &'static str,
|
|
89
|
+
policy: fn() -> egress::Policy,
|
|
90
|
+
) -> (Owner, Applied, Vec<(String, usize)>) {
|
|
91
|
+
in_namespace(namespace, move || {
|
|
92
|
+
let mut owner = Owner::new(options(name)).unwrap();
|
|
93
|
+
let target: Prepared = policy().prepare().unwrap().into();
|
|
94
|
+
target
|
|
95
|
+
.validate_interfaces()
|
|
96
|
+
.expect("published range fixture binding");
|
|
97
|
+
let applied = owner
|
|
98
|
+
.reconcile(Transition {
|
|
99
|
+
previous: None,
|
|
100
|
+
target,
|
|
101
|
+
})
|
|
102
|
+
.unwrap();
|
|
103
|
+
assert!(owner.inspect().enforced);
|
|
104
|
+
let sets = owner
|
|
105
|
+
.graph()
|
|
106
|
+
.unwrap()
|
|
107
|
+
.expect("owned table present")
|
|
108
|
+
.sets
|
|
109
|
+
.iter()
|
|
110
|
+
.map(|(attributes, elements)| (wire::text(attributes, 2).unwrap(), elements.len()))
|
|
111
|
+
.collect();
|
|
112
|
+
(owner, applied, sets)
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
118
|
+
fn v2_published_ranges_and_symmetric_publications_cross_both_hops() {
|
|
119
|
+
isolated();
|
|
120
|
+
for name in [WORK, ROUTER, HOST, PEER] {
|
|
121
|
+
ip(&["netns", "add", name]);
|
|
122
|
+
ns_ip(name, &["link", "set", "lo", "up"]);
|
|
123
|
+
in_namespace(name, || {
|
|
124
|
+
std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap()
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
connect(
|
|
128
|
+
WORK,
|
|
129
|
+
"client",
|
|
130
|
+
"10.241.0.2/24",
|
|
131
|
+
ROUTER,
|
|
132
|
+
"work",
|
|
133
|
+
"10.241.0.1/24",
|
|
134
|
+
);
|
|
135
|
+
connect(
|
|
136
|
+
ROUTER,
|
|
137
|
+
"handoff",
|
|
138
|
+
"10.240.0.2/30",
|
|
139
|
+
HOST,
|
|
140
|
+
"router",
|
|
141
|
+
"10.240.0.1/30",
|
|
142
|
+
);
|
|
143
|
+
connect(
|
|
144
|
+
HOST,
|
|
145
|
+
"uplink",
|
|
146
|
+
"192.0.2.2/24",
|
|
147
|
+
PEER,
|
|
148
|
+
"underlay",
|
|
149
|
+
"192.0.2.3/24",
|
|
150
|
+
);
|
|
151
|
+
ns_ip(WORK, &["route", "add", "default", "via", "10.241.0.1"]);
|
|
152
|
+
ns_ip(ROUTER, &["route", "add", "default", "via", "10.240.0.1"]);
|
|
153
|
+
// The workload route exists throughout, so every later denial is caused by
|
|
154
|
+
// policy and not by a missing route.
|
|
155
|
+
ns_ip(
|
|
156
|
+
HOST,
|
|
157
|
+
&["route", "add", "10.241.0.0/24", "via", "10.240.0.2"],
|
|
158
|
+
);
|
|
159
|
+
ns_ip(
|
|
160
|
+
HOST,
|
|
161
|
+
&["route", "add", "203.0.113.2/32", "via", "192.0.2.3"],
|
|
162
|
+
);
|
|
163
|
+
ns_ip(PEER, &["address", "add", "203.0.113.2/32", "dev", "lo"]);
|
|
164
|
+
ns_ip(PEER, &["route", "add", "default", "via", "192.0.2.2"]);
|
|
165
|
+
let first = socket(WORK, "10.241.0.2:20000");
|
|
166
|
+
let middle = socket(WORK, "10.241.0.2:20100");
|
|
167
|
+
let last = socket(WORK, "10.241.0.2:20200");
|
|
168
|
+
let above = socket(WORK, "10.241.0.2:20201");
|
|
169
|
+
let below = socket(WORK, "10.241.0.2:19999");
|
|
170
|
+
let rtp = socket(WORK, "10.241.0.2:20150");
|
|
171
|
+
let sip = socket(WORK, "10.241.0.2:5070");
|
|
172
|
+
let plain = socket(WORK, "10.241.0.2:9125");
|
|
173
|
+
let work_egress = socket(WORK, "10.241.0.2:40001");
|
|
174
|
+
let public = socket(PEER, "203.0.113.2:42000");
|
|
175
|
+
let rtp_peer = socket(PEER, "203.0.113.2:42001");
|
|
176
|
+
let sip_peer = socket(PEER, "203.0.113.2:42002");
|
|
177
|
+
let plain_peer = socket(PEER, "203.0.113.2:42003");
|
|
178
|
+
let protected_peer = socket(PEER, "192.0.2.3:40300");
|
|
179
|
+
// Positive controls: without policy the workload is routed directly and its
|
|
180
|
+
// packets reach the protected peer address, so every later result is caused
|
|
181
|
+
// by policy. Each probe uses its own tuple, so no unreplied conntrack entry
|
|
182
|
+
// carries a null translation across the policy change.
|
|
183
|
+
control(
|
|
184
|
+
&socket(PEER, "192.0.2.3:40010"),
|
|
185
|
+
"10.241.0.2:20201",
|
|
186
|
+
&above,
|
|
187
|
+
true,
|
|
188
|
+
);
|
|
189
|
+
control(
|
|
190
|
+
&socket(PEER, "192.0.2.3:40011"),
|
|
191
|
+
"10.241.0.2:19999",
|
|
192
|
+
&below,
|
|
193
|
+
true,
|
|
194
|
+
);
|
|
195
|
+
control(
|
|
196
|
+
&socket(WORK, "10.241.0.2:5071"),
|
|
197
|
+
"192.0.2.3:40300",
|
|
198
|
+
&protected_peer,
|
|
199
|
+
true,
|
|
200
|
+
);
|
|
201
|
+
dark_udp(&socket(PEER, "192.0.2.3:40012"), &middle, "192.0.2.2:20100");
|
|
202
|
+
let (host_owner, host_applied, host_sets) = apply(HOST, "published_range_host", host_ranges);
|
|
203
|
+
let (router_owner, router_applied, router_sets) =
|
|
204
|
+
apply(ROUTER, "published_range", router_ranges);
|
|
205
|
+
// Publications are set elements: one-port translations, range translations,
|
|
206
|
+
// the inbound admissions and the symmetric map on both hops, and the two raw
|
|
207
|
+
// classifications in the router.
|
|
208
|
+
let named = |sets: &[(String, usize)], name: &str| {
|
|
209
|
+
sets.iter()
|
|
210
|
+
.find(|(set, _)| set == name)
|
|
211
|
+
.map(|(_, count)| *count)
|
|
212
|
+
};
|
|
213
|
+
for (name, host, router) in [
|
|
214
|
+
("published_port", Some(3), Some(3)),
|
|
215
|
+
("published_address", Some(1), Some(1)),
|
|
216
|
+
("published_forward", Some(4), Some(4)),
|
|
217
|
+
("published_symmetric", Some(3), Some(3)),
|
|
218
|
+
("published_arrival", None, Some(4)),
|
|
219
|
+
// 9125 and the 5070 ports of both protocols and the 20000 range.
|
|
220
|
+
("published_endpoint", None, Some(4)),
|
|
221
|
+
] {
|
|
222
|
+
assert_eq!(named(&host_sets, name), host, "host {name}");
|
|
223
|
+
assert_eq!(named(&router_sets, name), router, "router {name}");
|
|
224
|
+
}
|
|
225
|
+
// First, middle and last port of the range reach the listener behind both
|
|
226
|
+
// hops with the client address preserved, and the reply comes back from the
|
|
227
|
+
// published address and port.
|
|
228
|
+
for (port, client_port, workload) in [
|
|
229
|
+
(20000, 40100, &first),
|
|
230
|
+
(20100, 40101, &middle),
|
|
231
|
+
(20200, 40102, &last),
|
|
232
|
+
] {
|
|
233
|
+
let client = in_namespace(PEER, move || {
|
|
234
|
+
std::net::UdpSocket::bind(format!("192.0.2.3:{client_port}")).unwrap()
|
|
235
|
+
});
|
|
236
|
+
client
|
|
237
|
+
.set_read_timeout(Some(Duration::from_millis(250)))
|
|
238
|
+
.unwrap();
|
|
239
|
+
published_udp(
|
|
240
|
+
&client,
|
|
241
|
+
workload,
|
|
242
|
+
&format!("192.0.2.2:{port}"),
|
|
243
|
+
&format!("192.0.2.3:{client_port}"),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
// The ports just outside the range stay dark.
|
|
247
|
+
dark_udp(&socket(PEER, "192.0.2.3:40103"), &above, "192.0.2.2:20201");
|
|
248
|
+
dark_udp(&socket(PEER, "192.0.2.3:40104"), &below, "192.0.2.2:19999");
|
|
249
|
+
// Symmetric outbound flows reach the uplink with their own port, from a range
|
|
250
|
+
// port and from the single signalling port, and their replies return.
|
|
251
|
+
let observed = roundtrip(&rtp, &rtp_peer);
|
|
252
|
+
assert_eq!(observed.to_string(), "192.0.2.2:20150");
|
|
253
|
+
let observed = roundtrip(&sip, &sip_peer);
|
|
254
|
+
assert_eq!(observed.to_string(), "192.0.2.2:5070");
|
|
255
|
+
// A later request from the same peer tuple toward the published port is the
|
|
256
|
+
// flow's own reply direction and reaches the workload from that peer.
|
|
257
|
+
assert_eq!(sip_peer.send_to(b"invite", "192.0.2.2:5070").unwrap(), 6);
|
|
258
|
+
let mut bytes = [0; 64];
|
|
259
|
+
let (size, from) = sip
|
|
260
|
+
.recv_from(&mut bytes)
|
|
261
|
+
.expect("request on the symmetric flow");
|
|
262
|
+
assert_eq!(
|
|
263
|
+
(&bytes[..size], from.to_string().as_str()),
|
|
264
|
+
(&b"invite"[..], "203.0.113.2:42002")
|
|
265
|
+
);
|
|
266
|
+
// An unrelated client still reaches the publication inbound.
|
|
267
|
+
let unrelated = socket(PEER, "192.0.2.3:40105");
|
|
268
|
+
published_udp(&unrelated, &sip, "192.0.2.2:5070", "192.0.2.3:40105");
|
|
269
|
+
let listener = in_namespace(PEER, || TcpListener::bind("203.0.113.2:5060").unwrap());
|
|
270
|
+
let mut outbound = in_namespace(WORK, || connect_from("10.241.0.2:5070", "203.0.113.2:5060"))
|
|
271
|
+
.expect("symmetric tcp opening");
|
|
272
|
+
let (mut served, peer) = listener.accept().unwrap();
|
|
273
|
+
assert_eq!(peer.to_string(), "192.0.2.2:5070");
|
|
274
|
+
for stream in [&outbound, &served] {
|
|
275
|
+
stream
|
|
276
|
+
.set_read_timeout(Some(Duration::from_secs(1)))
|
|
277
|
+
.unwrap();
|
|
278
|
+
}
|
|
279
|
+
outbound.write_all(b"ping").unwrap();
|
|
280
|
+
served.read_exact(&mut [0; 4]).unwrap();
|
|
281
|
+
served.write_all(b"pong").unwrap();
|
|
282
|
+
outbound.read_exact(&mut [0; 4]).unwrap();
|
|
283
|
+
drop(outbound);
|
|
284
|
+
drop(served);
|
|
285
|
+
// Negative controls: a plain publication's workload port opens nothing, and a
|
|
286
|
+
// symmetric flow into the protected union stays denied from either port kind.
|
|
287
|
+
denied(&plain, &plain_peer, false);
|
|
288
|
+
denied(&sip, &protected_peer, false);
|
|
289
|
+
denied(&rtp, &protected_peer, false);
|
|
290
|
+
// Leased egress keeps its own leased source range.
|
|
291
|
+
let outer = roundtrip(&work_egress, &public);
|
|
292
|
+
assert_eq!(outer.ip().to_string(), "192.0.2.2");
|
|
293
|
+
assert!((10000..=10015).contains(&outer.port()));
|
|
294
|
+
in_namespace(ROUTER, move || {
|
|
295
|
+
let mut owner = router_owner;
|
|
296
|
+
assert!(owner.inspect().enforced);
|
|
297
|
+
owner.release(router_applied).unwrap();
|
|
298
|
+
});
|
|
299
|
+
in_namespace(HOST, move || {
|
|
300
|
+
let mut owner = host_owner;
|
|
301
|
+
assert!(owner.inspect().enforced);
|
|
302
|
+
owner.release(host_applied).unwrap();
|
|
303
|
+
});
|
|
304
|
+
dark_udp(&socket(PEER, "192.0.2.3:40106"), &middle, "192.0.2.2:20100");
|
|
305
|
+
println!("V2_PUBLISHED_RANGE_PROOF set_backed_graph_verified=true range_first_delivered=true range_middle_delivered=true range_last_delivered=true client_address_preserved=true range_outside_dark=true symmetric_range_source_port_kept=true symmetric_udp_source_port_kept=true symmetric_tcp_source_port_kept=true peer_request_on_symmetric_flow=true inbound_beside_symmetric=true plain_publication_outbound_denied=true symmetric_protected_denied=true leased_egress_unaffected=true protected_positive_control=true released_range_dark=true");
|
|
306
|
+
}
|