@push.rocks/smartnftables 2.5.2 → 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 +31 -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 +69 -3
- package/package.json +6 -6
- package/readme.md +342 -61
- package/rust/src/egress.compile.rs +157 -76
- package/rust/src/egress.host.rs +104 -44
- package/rust/src/egress.hostgrant.rs +14 -14
- package/rust/src/egress.keys.rs +305 -0
- package/rust/src/egress.leased.rs +399 -0
- package/rust/src/egress.poolguard.rs +45 -0
- package/rust/src/egress.private.rs +197 -0
- package/rust/src/egress.published.rs +370 -0
- package/rust/src/egress.router.rs +64 -295
- package/rust/src/egress.rs +248 -17
- package/rust/src/egress.workloadgrant.rs +88 -0
- package/rust/src/egress_hostgrant_tests.rs +196 -47
- package/rust/src/egress_localplatform_tests.rs +204 -0
- package/rust/src/egress_localport_tests.rs +266 -0
- 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 +313 -0
- package/rust/src/owner.rs +77 -15
- package/rust/src/owner_host_traffic_tests.rs +4 -0
- package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
- package/rust/src/owner_identity_tests.rs +228 -80
- package/rust/src/owner_localplatform_tests.rs +274 -0
- package/rust/src/owner_localport_tests.rs +241 -0
- 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 +15 -0
- package/rust/src/owner_workloadgrant_tests.rs +278 -0
- package/rust/src/policy.rs +29 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/managed.egress.types.ts +71 -3
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
//! A 64-workload router: every workload with local DNS, public TCP and UDP
|
|
2
|
+
//! egress, four platform endpoints and a publication, the first with the SIP
|
|
3
|
+
//! shape, applied in one batch and proven on first, middle and last workloads.
|
|
4
|
+
use super::egress_traffic_tests::{allocation, binding, connect, denied, ns_ip, roundtrip};
|
|
5
|
+
use super::host_traffic_tests::{control, published_udp};
|
|
6
|
+
use super::*;
|
|
7
|
+
use crate::egress;
|
|
8
|
+
use serde_json::{json, Value};
|
|
9
|
+
use std::io::{Read, Write};
|
|
10
|
+
use std::net::{TcpListener, TcpStream, UdpSocket};
|
|
11
|
+
use std::time::Duration;
|
|
12
|
+
|
|
13
|
+
const WORKLOADS: u32 = 64;
|
|
14
|
+
const ROUTER: &str = "sc_router";
|
|
15
|
+
const HOST: &str = "sc_host";
|
|
16
|
+
const PEER: &str = "sc_peer";
|
|
17
|
+
/// The proven workloads: first, middle and last.
|
|
18
|
+
const PROBED: [u32; 3] = [0, 31, 63];
|
|
19
|
+
|
|
20
|
+
fn workload(index: u32) -> String {
|
|
21
|
+
format!("sc_w{index}")
|
|
22
|
+
}
|
|
23
|
+
fn socket_at(namespace: &str, address: String) -> UdpSocket {
|
|
24
|
+
let socket = in_namespace(namespace, move || UdpSocket::bind(address).unwrap());
|
|
25
|
+
socket
|
|
26
|
+
.set_read_timeout(Some(Duration::from_millis(250)))
|
|
27
|
+
.unwrap();
|
|
28
|
+
socket
|
|
29
|
+
}
|
|
30
|
+
/// Four platform endpoints on the peer, beside the protected prefixes.
|
|
31
|
+
fn protection() -> Value {
|
|
32
|
+
json!({"authorityDigest":format!("sha256:{}","b".repeat(64)),"prefixes":["10.0.0.0/8","192.0.2.0/24"],
|
|
33
|
+
"platformEndpoints":[{"id":"dns","address":"192.0.2.3","protocol":"udp","port":5300},
|
|
34
|
+
{"id":"mail","address":"192.0.2.3","protocol":"tcp","port":3000},
|
|
35
|
+
{"id":"metrics","address":"192.0.2.3","protocol":"tcp","port":9100},
|
|
36
|
+
{"id":"registry","address":"192.0.2.3","protocol":"udp","port":5301}]})
|
|
37
|
+
}
|
|
38
|
+
/// Publication `index`: one TCP port per workload, and the SIP shape on the
|
|
39
|
+
/// first.
|
|
40
|
+
fn publications(router: bool) -> Vec<Value> {
|
|
41
|
+
let entry = |protocol: &str,
|
|
42
|
+
port: u16,
|
|
43
|
+
end: Option<u16>,
|
|
44
|
+
endpoint: u16,
|
|
45
|
+
address: String,
|
|
46
|
+
symmetric: bool| {
|
|
47
|
+
let mut value = if router {
|
|
48
|
+
json!({"protocol":protocol,"transitPort":port,"endpointPort":endpoint,
|
|
49
|
+
"endpointAddress":address,"transitSourceAddress":"10.240.0.2","symmetric":symmetric})
|
|
50
|
+
} else {
|
|
51
|
+
json!({"protocol":protocol,"hostPort":port,"targetPort":port,
|
|
52
|
+
"targetAddress":"10.240.0.2","hostIp":"192.0.2.2","symmetric":symmetric})
|
|
53
|
+
};
|
|
54
|
+
if let Some(end) = end {
|
|
55
|
+
value[if router {
|
|
56
|
+
"transitPortEnd"
|
|
57
|
+
} else {
|
|
58
|
+
"hostPortEnd"
|
|
59
|
+
}] = json!(end);
|
|
60
|
+
}
|
|
61
|
+
value
|
|
62
|
+
};
|
|
63
|
+
let mut result = vec![
|
|
64
|
+
entry("udp", 5070, None, 5070, "10.241.0.2".into(), true),
|
|
65
|
+
entry("tcp", 5070, None, 5070, "10.241.0.2".into(), true),
|
|
66
|
+
entry("udp", 20000, Some(20200), 20000, "10.241.0.2".into(), true),
|
|
67
|
+
];
|
|
68
|
+
for index in 0..WORKLOADS {
|
|
69
|
+
let port = 30000 + index as u16;
|
|
70
|
+
result.push(entry(
|
|
71
|
+
"tcp",
|
|
72
|
+
port,
|
|
73
|
+
None,
|
|
74
|
+
if router { 8080 } else { port },
|
|
75
|
+
format!("10.241.{index}.2"),
|
|
76
|
+
false,
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
result
|
|
80
|
+
}
|
|
81
|
+
fn router_policy() -> egress::Policy {
|
|
82
|
+
let mut endpoints = Vec::new();
|
|
83
|
+
let mut links = Vec::new();
|
|
84
|
+
let mut rules = Vec::new();
|
|
85
|
+
let mut grants = Vec::new();
|
|
86
|
+
for index in 0..WORKLOADS {
|
|
87
|
+
let name = format!("w{index}");
|
|
88
|
+
let gateway = format!("10.241.{index}.1");
|
|
89
|
+
let address = format!("10.241.{index}.2");
|
|
90
|
+
let link = binding(&name, &gateway, "veth");
|
|
91
|
+
endpoints.push(
|
|
92
|
+
json!({"id":name,"interfaceIndex":link["interfaceIndex"],"interfaceName":name,
|
|
93
|
+
"interfaceKind":"veth","sourcePrefixes":[format!("{address}/32")]}),
|
|
94
|
+
);
|
|
95
|
+
links.push(link);
|
|
96
|
+
for protocol in ["tcp", "udp"] {
|
|
97
|
+
rules.push(json!({"sourceEndpoint":name,"destinationEndpoint":null,
|
|
98
|
+
"sourcePrefix":format!("{address}/32"),"destinationPrefix":format!("{gateway}/32"),
|
|
99
|
+
"protocol":protocol,"sourcePort":null,"destinationPort":53}));
|
|
100
|
+
rules.push(json!({"sourceEndpoint":null,"destinationEndpoint":name,
|
|
101
|
+
"sourcePrefix":format!("{gateway}/32"),"destinationPrefix":format!("{address}/32"),
|
|
102
|
+
"protocol":protocol,"sourcePort":53,"destinationPort":null}));
|
|
103
|
+
let range = if protocol == "udp" {
|
|
104
|
+
(10000, 10015)
|
|
105
|
+
} else {
|
|
106
|
+
(10016, 10031)
|
|
107
|
+
};
|
|
108
|
+
grants.push(
|
|
109
|
+
json!({"sourceEndpoint":name,"sourcePrefix":format!("{address}/32"),
|
|
110
|
+
"destination":{"kind":"public","prefix":"0.0.0.0/0"},"protocol":protocol,
|
|
111
|
+
"sourcePort":null,"destinationPort":null,
|
|
112
|
+
"sourcePortRange":{"first":range.0,"last":range.1}}),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
for (id, protocol, port) in [
|
|
116
|
+
("dns", "udp", 5300),
|
|
117
|
+
("mail", "tcp", 3000),
|
|
118
|
+
("metrics", "tcp", 9100),
|
|
119
|
+
("registry", "udp", 5301),
|
|
120
|
+
] {
|
|
121
|
+
let range = if protocol == "udp" {
|
|
122
|
+
(10000, 10015)
|
|
123
|
+
} else {
|
|
124
|
+
(10016, 10031)
|
|
125
|
+
};
|
|
126
|
+
grants.push(
|
|
127
|
+
json!({"sourceEndpoint":name,"sourcePrefix":format!("{address}/32"),
|
|
128
|
+
"destination":{"kind":"platform","endpointId":id},"protocol":protocol,
|
|
129
|
+
"sourcePort":null,"destinationPort":port,
|
|
130
|
+
"sourcePortRange":{"first":range.0,"last":range.1}}),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
let mut generation = allocation();
|
|
135
|
+
generation["conntrackZone"] = json!(17);
|
|
136
|
+
generation["conntrackLabel"] = json!("a".repeat(32));
|
|
137
|
+
generation["grants"] = json!(grants);
|
|
138
|
+
serde_json::from_value(json!({"schemaVersion":2,"revision":1,"scope":{"kind":"routerEgress",
|
|
139
|
+
"endpoints":endpoints,"rules":rules,"links":links,"handoff":binding("handoff","10.240.0.2","veth"),
|
|
140
|
+
"protection":protection(),"generations":[generation],"publishedPorts":publications(true)}}))
|
|
141
|
+
.unwrap()
|
|
142
|
+
}
|
|
143
|
+
fn host_policy() -> egress::Policy {
|
|
144
|
+
let mut value = egress::tests::host();
|
|
145
|
+
value["scope"]["protection"] = protection();
|
|
146
|
+
value["scope"]["handoffs"][0]["link"] = binding("router", "10.240.0.1", "veth");
|
|
147
|
+
value["scope"]["handoffs"][0]["allocations"] = json!([allocation()]);
|
|
148
|
+
value["scope"]["uplink"] = binding("uplink", "192.0.2.2", "veth");
|
|
149
|
+
value["scope"]["publishedPorts"] = json!(publications(false));
|
|
150
|
+
serde_json::from_value(value).unwrap()
|
|
151
|
+
}
|
|
152
|
+
fn apply(
|
|
153
|
+
namespace: &str,
|
|
154
|
+
name: &'static str,
|
|
155
|
+
policy: fn() -> egress::Policy,
|
|
156
|
+
) -> (Owner, Applied, usize, Vec<(String, usize)>) {
|
|
157
|
+
in_namespace(namespace, move || {
|
|
158
|
+
let mut owner = Owner::new(options(name)).unwrap();
|
|
159
|
+
let target: Prepared = policy().prepare().unwrap().into();
|
|
160
|
+
target.validate_interfaces().expect("scale fixture binding");
|
|
161
|
+
let applied = owner
|
|
162
|
+
.reconcile(Transition {
|
|
163
|
+
previous: None,
|
|
164
|
+
target,
|
|
165
|
+
})
|
|
166
|
+
.unwrap();
|
|
167
|
+
assert!(owner.inspect().enforced);
|
|
168
|
+
let graph = owner.graph().unwrap().expect("owned table present");
|
|
169
|
+
let sets = graph
|
|
170
|
+
.sets
|
|
171
|
+
.iter()
|
|
172
|
+
.map(|(attributes, elements)| (wire::text(attributes, 2).unwrap(), elements.len()))
|
|
173
|
+
.collect();
|
|
174
|
+
(owner, applied, graph.rules.len(), sets)
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
180
|
+
fn v2_sixty_four_workload_router_enforces_every_workload_from_set_elements() {
|
|
181
|
+
isolated();
|
|
182
|
+
for name in [ROUTER, HOST, PEER] {
|
|
183
|
+
ip(&["netns", "add", name]);
|
|
184
|
+
ns_ip(name, &["link", "set", "lo", "up"]);
|
|
185
|
+
in_namespace(name, || {
|
|
186
|
+
std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap()
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for index in 0..WORKLOADS {
|
|
190
|
+
let name = workload(index);
|
|
191
|
+
ip(&["netns", "add", &name]);
|
|
192
|
+
connect(
|
|
193
|
+
&name,
|
|
194
|
+
"client",
|
|
195
|
+
&format!("10.241.{index}.2/24"),
|
|
196
|
+
ROUTER,
|
|
197
|
+
&format!("w{index}"),
|
|
198
|
+
&format!("10.241.{index}.1/24"),
|
|
199
|
+
);
|
|
200
|
+
ns_ip(
|
|
201
|
+
&name,
|
|
202
|
+
&[
|
|
203
|
+
"route",
|
|
204
|
+
"add",
|
|
205
|
+
"default",
|
|
206
|
+
"via",
|
|
207
|
+
&format!("10.241.{index}.1"),
|
|
208
|
+
],
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
connect(
|
|
212
|
+
ROUTER,
|
|
213
|
+
"handoff",
|
|
214
|
+
"10.240.0.2/30",
|
|
215
|
+
HOST,
|
|
216
|
+
"router",
|
|
217
|
+
"10.240.0.1/30",
|
|
218
|
+
);
|
|
219
|
+
connect(
|
|
220
|
+
HOST,
|
|
221
|
+
"uplink",
|
|
222
|
+
"192.0.2.2/24",
|
|
223
|
+
PEER,
|
|
224
|
+
"underlay",
|
|
225
|
+
"192.0.2.3/24",
|
|
226
|
+
);
|
|
227
|
+
ns_ip(ROUTER, &["route", "add", "default", "via", "10.240.0.1"]);
|
|
228
|
+
ns_ip(
|
|
229
|
+
HOST,
|
|
230
|
+
&["route", "add", "10.241.0.0/16", "via", "10.240.0.2"],
|
|
231
|
+
);
|
|
232
|
+
ns_ip(
|
|
233
|
+
HOST,
|
|
234
|
+
&["route", "add", "203.0.113.2/32", "via", "192.0.2.3"],
|
|
235
|
+
);
|
|
236
|
+
ns_ip(PEER, &["address", "add", "203.0.113.2/32", "dev", "lo"]);
|
|
237
|
+
ns_ip(PEER, &["route", "add", "default", "via", "192.0.2.2"]);
|
|
238
|
+
// Positive controls before any policy: a workload reaches another workload,
|
|
239
|
+
// the protected peer address, and the peer with another workload's source.
|
|
240
|
+
let neighbour = socket_at(&workload(1), "10.241.1.2:8081".into());
|
|
241
|
+
let protected_peer = socket_at(PEER, "192.0.2.3:40300".into());
|
|
242
|
+
let spoof_target = socket_at(PEER, "203.0.113.2:42100".into());
|
|
243
|
+
control(
|
|
244
|
+
&socket_at(&workload(0), "10.241.0.2:40400".into()),
|
|
245
|
+
"10.241.1.2:8081",
|
|
246
|
+
&neighbour,
|
|
247
|
+
true,
|
|
248
|
+
);
|
|
249
|
+
control(
|
|
250
|
+
&socket_at(&workload(0), "10.241.0.2:40401".into()),
|
|
251
|
+
"192.0.2.3:40300",
|
|
252
|
+
&protected_peer,
|
|
253
|
+
true,
|
|
254
|
+
);
|
|
255
|
+
ns_ip(
|
|
256
|
+
&workload(0),
|
|
257
|
+
&["address", "add", "10.241.1.99/32", "dev", "client"],
|
|
258
|
+
);
|
|
259
|
+
control(
|
|
260
|
+
&socket_at(&workload(0), "10.241.1.99:40402".into()),
|
|
261
|
+
"203.0.113.2:42100",
|
|
262
|
+
&spoof_target,
|
|
263
|
+
true,
|
|
264
|
+
);
|
|
265
|
+
let (host_owner, host_applied, _, _) = apply(HOST, "scale_host", host_policy);
|
|
266
|
+
let (router_owner, router_applied, router_rules, router_sets) =
|
|
267
|
+
apply(ROUTER, "scale_router", router_policy);
|
|
268
|
+
// Every workload is set elements: links, endpoint indices and names, source
|
|
269
|
+
// prefixes, the four DNS rules each, and six leased grants each.
|
|
270
|
+
let named = |name: &str| {
|
|
271
|
+
router_sets
|
|
272
|
+
.iter()
|
|
273
|
+
.find(|(set, _)| set == name)
|
|
274
|
+
.map(|(_, count)| *count)
|
|
275
|
+
};
|
|
276
|
+
assert_eq!(named("private_link"), Some(WORKLOADS as usize + 1));
|
|
277
|
+
assert_eq!(named("private_source"), Some(WORKLOADS as usize));
|
|
278
|
+
assert_eq!(named("private_port"), Some(WORKLOADS as usize * 4));
|
|
279
|
+
assert_eq!(named("leased_public_flow"), Some(WORKLOADS as usize * 2));
|
|
280
|
+
assert_eq!(named("leased_platform_flow"), Some(WORKLOADS as usize * 4));
|
|
281
|
+
assert_eq!(named("published_forward"), Some(WORKLOADS as usize + 3));
|
|
282
|
+
// The rule graph a single workload compiles, independent of the count.
|
|
283
|
+
assert!(router_rules < 100, "{router_rules}");
|
|
284
|
+
let public = socket_at(PEER, "203.0.113.2:42000".into());
|
|
285
|
+
let platform = socket_at(PEER, "192.0.2.3:5300".into());
|
|
286
|
+
let registry = socket_at(PEER, "192.0.2.3:5301".into());
|
|
287
|
+
let metrics = in_namespace(PEER, || TcpListener::bind("192.0.2.3:9100").unwrap());
|
|
288
|
+
for index in PROBED {
|
|
289
|
+
let name = workload(index);
|
|
290
|
+
let address = format!("10.241.{index}.2");
|
|
291
|
+
// Local DNS on the workload's own gateway.
|
|
292
|
+
let dns = socket_at(ROUTER, format!("10.241.{index}.1:53"));
|
|
293
|
+
let client = socket_at(&name, format!("{address}:40001"));
|
|
294
|
+
assert_eq!(
|
|
295
|
+
roundtrip(&client, &dns).to_string(),
|
|
296
|
+
format!("{address}:40001")
|
|
297
|
+
);
|
|
298
|
+
// Public egress through both hops from the leased range.
|
|
299
|
+
let outer = roundtrip(&socket_at(&name, format!("{address}:40002")), &public);
|
|
300
|
+
assert_eq!(outer.ip().to_string(), "192.0.2.2");
|
|
301
|
+
assert!((10000..=10015).contains(&outer.port()), "{outer}");
|
|
302
|
+
// Two platform endpoints over UDP and one over TCP.
|
|
303
|
+
for target in [&platform, ®istry] {
|
|
304
|
+
let outer = roundtrip(&socket_at(&name, format!("{address}:40003")), target);
|
|
305
|
+
assert_eq!(outer.ip().to_string(), "192.0.2.2");
|
|
306
|
+
}
|
|
307
|
+
let mut stream = in_namespace(&name, || {
|
|
308
|
+
TcpStream::connect_timeout(&"192.0.2.3:9100".parse().unwrap(), Duration::from_secs(1))
|
|
309
|
+
})
|
|
310
|
+
.expect("platform tcp opening");
|
|
311
|
+
let (mut served, peer) = metrics.accept().unwrap();
|
|
312
|
+
assert_eq!(peer.ip().to_string(), "192.0.2.2");
|
|
313
|
+
assert!((10016..=10031).contains(&peer.port()));
|
|
314
|
+
stream
|
|
315
|
+
.set_read_timeout(Some(Duration::from_secs(1)))
|
|
316
|
+
.unwrap();
|
|
317
|
+
served.write_all(b"pong").unwrap();
|
|
318
|
+
stream.read_exact(&mut [0; 4]).unwrap();
|
|
319
|
+
// The workload's publication, with the client address preserved.
|
|
320
|
+
let listener = in_namespace(&name, move || {
|
|
321
|
+
TcpListener::bind(format!("10.241.{index}.2:8080")).unwrap()
|
|
322
|
+
});
|
|
323
|
+
let mut inbound = in_namespace(PEER, move || {
|
|
324
|
+
TcpStream::connect_timeout(
|
|
325
|
+
&format!("192.0.2.2:{}", 30000 + index).parse().unwrap(),
|
|
326
|
+
Duration::from_secs(1),
|
|
327
|
+
)
|
|
328
|
+
})
|
|
329
|
+
.expect("published tcp opening");
|
|
330
|
+
let (mut accepted, client) = listener.accept().unwrap();
|
|
331
|
+
assert_eq!(client.ip().to_string(), "192.0.2.3");
|
|
332
|
+
accepted.write_all(b"pong").unwrap();
|
|
333
|
+
inbound
|
|
334
|
+
.set_read_timeout(Some(Duration::from_secs(1)))
|
|
335
|
+
.unwrap();
|
|
336
|
+
inbound.read_exact(&mut [0; 4]).unwrap();
|
|
337
|
+
// Nothing unnamed: another workload, a protected address outside the
|
|
338
|
+
// platform endpoints, and a platform port that is not granted.
|
|
339
|
+
denied(
|
|
340
|
+
&socket_at(&name, format!("{address}:40005")),
|
|
341
|
+
&neighbour,
|
|
342
|
+
false,
|
|
343
|
+
);
|
|
344
|
+
denied(
|
|
345
|
+
&socket_at(&name, format!("{address}:40006")),
|
|
346
|
+
&protected_peer,
|
|
347
|
+
false,
|
|
348
|
+
);
|
|
349
|
+
let ungranted = socket_at(PEER, format!("192.0.2.3:{}", 5400 + index));
|
|
350
|
+
denied(
|
|
351
|
+
&socket_at(&name, format!("{address}:40007")),
|
|
352
|
+
&ungranted,
|
|
353
|
+
false,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
// The first workload's SIP publication leaves from its own port.
|
|
357
|
+
let sip = socket_at(&workload(0), "10.241.0.2:5070".into());
|
|
358
|
+
let sip_peer = socket_at(PEER, "203.0.113.2:42002".into());
|
|
359
|
+
assert_eq!(roundtrip(&sip, &sip_peer).to_string(), "192.0.2.2:5070");
|
|
360
|
+
published_udp(
|
|
361
|
+
&socket_at(PEER, "192.0.2.3:40500".into()),
|
|
362
|
+
&socket_at(&workload(0), "10.241.0.2:20100".into()),
|
|
363
|
+
"192.0.2.2:20100",
|
|
364
|
+
"192.0.2.3:40500",
|
|
365
|
+
);
|
|
366
|
+
// Another workload's source address stays behind the anti-spoof guard.
|
|
367
|
+
denied(
|
|
368
|
+
&socket_at(&workload(0), "10.241.1.99:40403".into()),
|
|
369
|
+
&spoof_target,
|
|
370
|
+
false,
|
|
371
|
+
);
|
|
372
|
+
in_namespace(ROUTER, move || {
|
|
373
|
+
let mut owner = router_owner;
|
|
374
|
+
assert!(owner.inspect().enforced);
|
|
375
|
+
owner.release(router_applied).unwrap();
|
|
376
|
+
});
|
|
377
|
+
in_namespace(HOST, move || {
|
|
378
|
+
let mut owner = host_owner;
|
|
379
|
+
assert!(owner.inspect().enforced);
|
|
380
|
+
owner.release(host_applied).unwrap();
|
|
381
|
+
});
|
|
382
|
+
println!("V2_SCALE_PROOF workloads=64 router_rules={router_rules} set_elements_verified=true private_dns=true public_egress=true platform_udp=true platform_tcp=true published_inbound=true client_address_preserved=true cross_workload_denied=true protected_denied=true ungranted_platform_port_denied=true symmetric_sip=true published_range=true spoof_denied=true positive_controls=true released=true");
|
|
383
|
+
}
|
package/rust/src/owner_tests.rs
CHANGED
|
@@ -24,9 +24,24 @@ mod host_traffic_tests;
|
|
|
24
24
|
#[path = "owner_router_traffic_tests.rs"]
|
|
25
25
|
mod router_traffic_tests;
|
|
26
26
|
|
|
27
|
+
#[path = "owner_publishedrange_traffic_tests.rs"]
|
|
28
|
+
mod publishedrange_traffic_tests;
|
|
29
|
+
|
|
30
|
+
#[path = "owner_scale_traffic_tests.rs"]
|
|
31
|
+
mod scale_traffic_tests;
|
|
32
|
+
|
|
27
33
|
#[path = "owner_poolguard_tests.rs"]
|
|
28
34
|
mod poolguard_tests;
|
|
29
35
|
|
|
36
|
+
#[path = "owner_localport_tests.rs"]
|
|
37
|
+
mod localport_tests;
|
|
38
|
+
|
|
39
|
+
#[path = "owner_localplatform_tests.rs"]
|
|
40
|
+
mod localplatform_tests;
|
|
41
|
+
|
|
42
|
+
#[path = "owner_workloadgrant_tests.rs"]
|
|
43
|
+
mod workloadgrant_tests;
|
|
44
|
+
|
|
30
45
|
#[path = "owner_hostgrant_traffic_tests.rs"]
|
|
31
46
|
mod hostgrant_traffic_tests;
|
|
32
47
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
use super::egress_traffic_tests::{
|
|
2
|
+
allocation, binding, connect, denied, ns_ip, protection, roundtrip,
|
|
3
|
+
};
|
|
4
|
+
use super::hostgrant_traffic_tests::Hop;
|
|
5
|
+
use super::router_traffic_tests::connect_from;
|
|
6
|
+
use super::*;
|
|
7
|
+
use crate::egress;
|
|
8
|
+
use serde_json::{json, Value};
|
|
9
|
+
use std::io::{Read, Write};
|
|
10
|
+
use std::net::{TcpListener, TcpStream, UdpSocket};
|
|
11
|
+
use std::time::Duration;
|
|
12
|
+
|
|
13
|
+
const A: &str = "wg_a";
|
|
14
|
+
const B: &str = "wg_b";
|
|
15
|
+
const C: &str = "wg_c";
|
|
16
|
+
const ROUTER: &str = "wg_router";
|
|
17
|
+
const HOST: &str = "wg_host";
|
|
18
|
+
|
|
19
|
+
fn endpoint(name: &str, prefix: &str, link: &Value) -> Value {
|
|
20
|
+
json!({"id":name,"interfaceIndex":link["interfaceIndex"],"interfaceName":name,
|
|
21
|
+
"interfaceKind":"veth","sourcePrefixes":[prefix]})
|
|
22
|
+
}
|
|
23
|
+
/// Three workloads behind the router, each with leased public egress on every
|
|
24
|
+
/// port of both protocols, exactly as a node composes it: a leased classifier
|
|
25
|
+
/// would claim a workload's packets if grants were not default-zone flows.
|
|
26
|
+
fn router_policy(revision: u64, granted: bool) -> egress::Policy {
|
|
27
|
+
let links: Vec<_> = [
|
|
28
|
+
("wka", "10.241.0.1"),
|
|
29
|
+
("wkb", "10.241.1.1"),
|
|
30
|
+
("wkc", "10.241.2.1"),
|
|
31
|
+
]
|
|
32
|
+
.into_iter()
|
|
33
|
+
.map(|(name, address)| binding(name, address, "veth"))
|
|
34
|
+
.collect();
|
|
35
|
+
let mut generation = allocation();
|
|
36
|
+
generation["conntrackZone"] = json!(17);
|
|
37
|
+
generation["conntrackLabel"] = json!("a".repeat(32));
|
|
38
|
+
let mut grants = Vec::new();
|
|
39
|
+
for (name, source) in [
|
|
40
|
+
("wka", "10.241.0.2"),
|
|
41
|
+
("wkb", "10.241.1.2"),
|
|
42
|
+
("wkc", "10.241.2.2"),
|
|
43
|
+
] {
|
|
44
|
+
for (protocol, first, last) in [("udp", 10000, 10015), ("tcp", 10016, 10031)] {
|
|
45
|
+
grants.push(json!({"sourceEndpoint":name,"sourcePrefix":format!("{source}/32"),
|
|
46
|
+
"destination":{"kind":"public","prefix":"0.0.0.0/0"},"protocol":protocol,
|
|
47
|
+
"sourcePort":null,"destinationPort":null,"sourcePortRange":{"first":first,"last":last}}));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
generation["grants"] = json!(grants);
|
|
51
|
+
let workload_grants = if granted {
|
|
52
|
+
json!([
|
|
53
|
+
{"protocol":"tcp","sourceAddress":"10.241.0.2","destinationAddress":"10.241.1.2","destinationPort":8080},
|
|
54
|
+
{"protocol":"udp","sourceAddress":"10.241.0.2","destinationAddress":"10.241.1.2","destinationPort":5353}
|
|
55
|
+
])
|
|
56
|
+
} else {
|
|
57
|
+
json!([])
|
|
58
|
+
};
|
|
59
|
+
serde_json::from_value(json!({"schemaVersion":2,"revision":revision,"scope":{"kind":"routerEgress",
|
|
60
|
+
"endpoints":[endpoint("wka","10.241.0.2/32",&links[0]),endpoint("wkb","10.241.1.2/32",&links[1]),
|
|
61
|
+
endpoint("wkc","10.241.2.2/32",&links[2])],
|
|
62
|
+
"rules":[],"links":links,"handoff":binding("handoff","10.240.0.2","veth"),
|
|
63
|
+
"protection":protection(),"generations":[generation],"workloadGrants":workload_grants}}))
|
|
64
|
+
.unwrap()
|
|
65
|
+
}
|
|
66
|
+
fn udp(namespace: &str, address: &str) -> UdpSocket {
|
|
67
|
+
let address = address.to_owned();
|
|
68
|
+
let socket = in_namespace(namespace, move || UdpSocket::bind(address).unwrap());
|
|
69
|
+
socket
|
|
70
|
+
.set_read_timeout(Some(Duration::from_millis(250)))
|
|
71
|
+
.unwrap();
|
|
72
|
+
socket
|
|
73
|
+
}
|
|
74
|
+
/// A TCP flow from an exact source; on success the listener observes it and
|
|
75
|
+
/// data crosses both ways.
|
|
76
|
+
fn dial(
|
|
77
|
+
namespace: &str,
|
|
78
|
+
source: &str,
|
|
79
|
+
destination: &str,
|
|
80
|
+
listener: &TcpListener,
|
|
81
|
+
) -> Option<(TcpStream, TcpStream)> {
|
|
82
|
+
let (from, to) = (source.to_owned(), destination.to_owned());
|
|
83
|
+
let client = in_namespace(namespace, move || connect_from(&from, &to)).ok()?;
|
|
84
|
+
let (server, peer) = listener.accept().unwrap();
|
|
85
|
+
assert_eq!(peer.to_string(), source);
|
|
86
|
+
let (mut client, mut server) = (client, server);
|
|
87
|
+
for stream in [&client, &server] {
|
|
88
|
+
stream
|
|
89
|
+
.set_read_timeout(Some(Duration::from_secs(1)))
|
|
90
|
+
.unwrap();
|
|
91
|
+
}
|
|
92
|
+
client.write_all(b"ping").unwrap();
|
|
93
|
+
server.read_exact(&mut [0; 4]).unwrap();
|
|
94
|
+
server.write_all(b"pong").unwrap();
|
|
95
|
+
client.read_exact(&mut [0; 4]).unwrap();
|
|
96
|
+
Some((client, server))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
101
|
+
fn v2_workload_grant_opens_one_way_between_workloads_and_never_in_reverse() {
|
|
102
|
+
isolated();
|
|
103
|
+
for name in [A, B, C, ROUTER, HOST] {
|
|
104
|
+
ip(&["netns", "add", name]);
|
|
105
|
+
ns_ip(name, &["link", "set", "lo", "up"]);
|
|
106
|
+
}
|
|
107
|
+
in_namespace(ROUTER, || {
|
|
108
|
+
std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap()
|
|
109
|
+
});
|
|
110
|
+
for (namespace, name, octet) in [(A, "wka", 0), (B, "wkb", 1), (C, "wkc", 2)] {
|
|
111
|
+
connect(
|
|
112
|
+
namespace,
|
|
113
|
+
"eth0",
|
|
114
|
+
&format!("10.241.{octet}.2/24"),
|
|
115
|
+
ROUTER,
|
|
116
|
+
name,
|
|
117
|
+
&format!("10.241.{octet}.1/24"),
|
|
118
|
+
);
|
|
119
|
+
ns_ip(
|
|
120
|
+
namespace,
|
|
121
|
+
&[
|
|
122
|
+
"route",
|
|
123
|
+
"add",
|
|
124
|
+
"default",
|
|
125
|
+
"via",
|
|
126
|
+
&format!("10.241.{octet}.1"),
|
|
127
|
+
],
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
connect(
|
|
131
|
+
ROUTER,
|
|
132
|
+
"handoff",
|
|
133
|
+
"10.240.0.2/30",
|
|
134
|
+
HOST,
|
|
135
|
+
"router",
|
|
136
|
+
"10.240.0.1/30",
|
|
137
|
+
);
|
|
138
|
+
let granted = in_namespace(B, || TcpListener::bind("10.241.1.2:8080").unwrap());
|
|
139
|
+
let other = in_namespace(B, || TcpListener::bind("10.241.1.2:8081").unwrap());
|
|
140
|
+
let third = in_namespace(C, || TcpListener::bind("10.241.2.2:8080").unwrap());
|
|
141
|
+
let source = in_namespace(A, || TcpListener::bind("10.241.0.2:9000").unwrap());
|
|
142
|
+
let served = udp(B, "10.241.1.2:5353");
|
|
143
|
+
let other_served = udp(B, "10.241.1.2:5354");
|
|
144
|
+
// Each phase uses its own client ports; TCP ports that closed stay in TIME_WAIT
|
|
145
|
+
// and an unreplied UDP entry would carry a decision over a policy change.
|
|
146
|
+
let exact = |phase: u16| {
|
|
147
|
+
let result = dial(
|
|
148
|
+
A,
|
|
149
|
+
&format!("10.241.0.2:{}", 40000 + phase),
|
|
150
|
+
"10.241.1.2:8080",
|
|
151
|
+
&granted,
|
|
152
|
+
);
|
|
153
|
+
let client = udp(A, &format!("10.241.0.2:{}", 41000 + phase));
|
|
154
|
+
(result, client)
|
|
155
|
+
};
|
|
156
|
+
let refused = |phase: u16| {
|
|
157
|
+
// Another port, another source workload, another destination workload.
|
|
158
|
+
assert!(dial(
|
|
159
|
+
A,
|
|
160
|
+
&format!("10.241.0.2:{}", 42000 + phase),
|
|
161
|
+
"10.241.1.2:8081",
|
|
162
|
+
&other
|
|
163
|
+
)
|
|
164
|
+
.is_none());
|
|
165
|
+
assert!(dial(
|
|
166
|
+
C,
|
|
167
|
+
&format!("10.241.2.2:{}", 40000 + phase),
|
|
168
|
+
"10.241.1.2:8080",
|
|
169
|
+
&granted
|
|
170
|
+
)
|
|
171
|
+
.is_none());
|
|
172
|
+
assert!(dial(
|
|
173
|
+
A,
|
|
174
|
+
&format!("10.241.0.2:{}", 43000 + phase),
|
|
175
|
+
"10.241.2.2:8080",
|
|
176
|
+
&third
|
|
177
|
+
)
|
|
178
|
+
.is_none());
|
|
179
|
+
// No reverse opening: the destination cannot open toward the source.
|
|
180
|
+
assert!(dial(
|
|
181
|
+
B,
|
|
182
|
+
&format!("10.241.1.2:{}", 44000 + phase),
|
|
183
|
+
"10.241.0.2:9000",
|
|
184
|
+
&source
|
|
185
|
+
)
|
|
186
|
+
.is_none());
|
|
187
|
+
let reverse = udp(B, &format!("10.241.1.2:{}", 45000 + phase));
|
|
188
|
+
denied(
|
|
189
|
+
&reverse,
|
|
190
|
+
&udp(A, &format!("10.241.0.2:{}", 46000 + phase)),
|
|
191
|
+
false,
|
|
192
|
+
);
|
|
193
|
+
denied(
|
|
194
|
+
&udp(A, &format!("10.241.0.2:{}", 47000 + phase)),
|
|
195
|
+
&other_served,
|
|
196
|
+
false,
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
// Positive controls without policy: every probe below can succeed.
|
|
200
|
+
let (flow, client) = exact(0);
|
|
201
|
+
assert!(flow.is_some());
|
|
202
|
+
roundtrip(&client, &served);
|
|
203
|
+
assert!(dial(A, "10.241.0.2:42000", "10.241.1.2:8081", &other).is_some());
|
|
204
|
+
assert!(dial(C, "10.241.2.2:40000", "10.241.1.2:8080", &granted).is_some());
|
|
205
|
+
assert!(dial(A, "10.241.0.2:43000", "10.241.2.2:8080", &third).is_some());
|
|
206
|
+
assert!(dial(B, "10.241.1.2:44000", "10.241.0.2:9000", &source).is_some());
|
|
207
|
+
roundtrip(&udp(B, "10.241.1.2:45000"), &udp(A, "10.241.0.2:46000"));
|
|
208
|
+
roundtrip(&udp(A, "10.241.0.2:47000"), &other_served);
|
|
209
|
+
|
|
210
|
+
let mut router = Hop::new(ROUTER, "workload_grant_router");
|
|
211
|
+
// Without grants the private endpoint denials refuse every workload flow.
|
|
212
|
+
router.apply(router_policy, 1, false);
|
|
213
|
+
let (flow, client) = exact(1);
|
|
214
|
+
assert!(flow.is_none());
|
|
215
|
+
denied(&client, &served, false);
|
|
216
|
+
refused(1);
|
|
217
|
+
// With them, exactly the source's openings and their answers pass.
|
|
218
|
+
router.apply(router_policy, 2, true);
|
|
219
|
+
let (flow, client) = exact(2);
|
|
220
|
+
let (mut established, mut accepted) = flow.expect("granted workload tcp");
|
|
221
|
+
let observed = roundtrip(&client, &served);
|
|
222
|
+
assert_eq!(observed.to_string(), "10.241.0.2:41002");
|
|
223
|
+
refused(2);
|
|
224
|
+
// Withdrawal stops new flows and the established one.
|
|
225
|
+
router.apply(router_policy, 3, false);
|
|
226
|
+
let (flow, client) = exact(3);
|
|
227
|
+
assert!(flow.is_none());
|
|
228
|
+
denied(&client, &served, false);
|
|
229
|
+
established.write_all(b"late").unwrap();
|
|
230
|
+
assert!(accepted.read_exact(&mut [0; 4]).is_err());
|
|
231
|
+
drop((established, accepted));
|
|
232
|
+
router.release();
|
|
233
|
+
// Release control.
|
|
234
|
+
let (flow, client) = exact(4);
|
|
235
|
+
assert!(flow.is_some());
|
|
236
|
+
roundtrip(&client, &served);
|
|
237
|
+
println!("V2_WORKLOAD_GRANT_PROOF positive_controls=true ungranted_dark=true exact_tcp=true exact_udp=true source_preserved=true leased_egress_unaffected=true other_port_denied=true other_source_denied=true other_destination_denied=true reverse_tcp_denied=true reverse_udp_denied=true withdrawn_dark=true established_withdrawn=true release_control=true");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// A full grant set of 1024 persists across owner loss, is re-verified element
|
|
241
|
+
/// by element before adoption, and is replaced and released like the rest.
|
|
242
|
+
#[test]
|
|
243
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
244
|
+
fn v2_workload_grant_set_persist_and_lost_ack_recovery() {
|
|
245
|
+
isolated();
|
|
246
|
+
// Peers live in another namespace, as the bound veth facts expect.
|
|
247
|
+
ip(&["netns", "add", HOST]);
|
|
248
|
+
for (name, address) in [
|
|
249
|
+
("wka", "10.241.0.1/24"),
|
|
250
|
+
("wkb", "10.241.1.1/24"),
|
|
251
|
+
("wkc", "10.241.2.1/24"),
|
|
252
|
+
("handoff", "10.240.0.2/30"),
|
|
253
|
+
] {
|
|
254
|
+
ip(&[
|
|
255
|
+
"link", "add", name, "type", "veth", "peer", "name", name, "netns", HOST,
|
|
256
|
+
]);
|
|
257
|
+
ip(&["address", "add", address, "dev", name]);
|
|
258
|
+
ip(&["link", "set", name, "up"]);
|
|
259
|
+
}
|
|
260
|
+
let mut policy = router_policy(1, true);
|
|
261
|
+
let egress::Scope::RouterEgress(scope) = &mut policy.scope else {
|
|
262
|
+
panic!()
|
|
263
|
+
};
|
|
264
|
+
scope.workload_grants = (0..1024_u16)
|
|
265
|
+
.map(|index| egress::WorkloadGrant {
|
|
266
|
+
protocol: if index % 2 == 0 { "tcp" } else { "udp" }.into(),
|
|
267
|
+
source_address: "10.241.0.2".into(),
|
|
268
|
+
destination_address: if index < 512 {
|
|
269
|
+
"10.241.1.2"
|
|
270
|
+
} else {
|
|
271
|
+
"10.241.2.2"
|
|
272
|
+
}
|
|
273
|
+
.into(),
|
|
274
|
+
destination_port: 20000 + index,
|
|
275
|
+
})
|
|
276
|
+
.collect();
|
|
277
|
+
super::egress_tests::recover("workload_grant_v2", policy);
|
|
278
|
+
}
|