@push.rocks/smartnftables 2.6.0 → 4.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 +33 -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/classes.manageddockerforwarding.js +3 -4
- package/dist_ts/classes.managednftables.d.ts +14 -2
- package/dist_ts/classes.managednftables.js +48 -8
- package/dist_ts/managed.egress.types.d.ts +25 -3
- package/dist_ts/managed.types.d.ts +14 -0
- package/package.json +7 -7
- package/readme.md +262 -64
- package/rust/src/docker.policy.rs +2 -1
- package/rust/src/egress.compile.rs +153 -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 +140 -19
- package/rust/src/egress_hostgrant_tests.rs +158 -20
- package/rust/src/egress_localport_tests.rs +11 -2
- package/rust/src/egress_publishedrange_tests.rs +812 -0
- package/rust/src/egress_routerequivalence_tests.rs +448 -0
- package/rust/src/egress_tests.rs +289 -230
- package/rust/src/egress_workloadgrant_tests.rs +21 -13
- package/rust/src/main.rs +57 -3
- 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 +30 -29
- package/rust/src/tests.rs +100 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +2 -3
- package/ts/classes.managednftables.ts +46 -8
- package/ts/managed.egress.types.ts +25 -3
- package/ts/managed.types.ts +17 -0
|
@@ -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,6 +24,12 @@ 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
|
|
package/rust/src/policy.rs
CHANGED
|
@@ -90,15 +90,37 @@ fn covers(parent: &str, child: &str) -> Result<bool> {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
impl Policy {
|
|
93
|
-
pub fn prepare(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{
|
|
93
|
+
pub fn prepare(self) -> Result<Prepared> {
|
|
94
|
+
let policy = self.normalize(32, 128)?;
|
|
95
|
+
let digest = format!(
|
|
96
|
+
"sha256:{:x}",
|
|
97
|
+
Sha256::digest(serde_json::to_vec(&policy).map_err(|_| Error::Invalid)?)
|
|
98
|
+
);
|
|
99
|
+
let prepared = Prepared { policy, digest };
|
|
100
|
+
// Compile during preparation; an oversized native batch never reaches the kernel.
|
|
101
|
+
let program = prepared.program(&format!("snft_{}", "x".repeat(59)))?;
|
|
102
|
+
// Reserve enough of the batch for deleting a previous maximum
|
|
103
|
+
// graph, then installing this complete graph in the same transaction.
|
|
104
|
+
crate::capacity("operations", 768, program.len())?;
|
|
105
|
+
crate::capacity(
|
|
106
|
+
"ruleBytes",
|
|
107
|
+
100_000,
|
|
108
|
+
program
|
|
109
|
+
.iter()
|
|
110
|
+
.map(|(_, attrs)| wire::encode_attrs(attrs).len() + 20)
|
|
111
|
+
.sum(),
|
|
112
|
+
)?;
|
|
113
|
+
Ok(prepared)
|
|
114
|
+
}
|
|
115
|
+
/// Validates and orders the endpoints and rules. Schema v1 compiles one rule
|
|
116
|
+
/// per endpoint and grant, so it admits 32 endpoints and 128 rules; the
|
|
117
|
+
/// set-backed router scope of schema v2 admits more.
|
|
118
|
+
pub(crate) fn normalize(mut self, endpoints: usize, rules: usize) -> Result<Self> {
|
|
119
|
+
if self.schema_version != 1 || self.revision == 0 || self.revision > 9_007_199_254_740_991 {
|
|
100
120
|
return Err(Error::Invalid);
|
|
101
121
|
}
|
|
122
|
+
crate::capacity("endpoints", endpoints, self.endpoints.len())?;
|
|
123
|
+
crate::capacity("rules", rules, self.rules.len())?;
|
|
102
124
|
let mut ids = BTreeSet::new();
|
|
103
125
|
let mut indices = BTreeSet::new();
|
|
104
126
|
let mut names = BTreeSet::new();
|
|
@@ -185,28 +207,7 @@ impl Policy {
|
|
|
185
207
|
}
|
|
186
208
|
}
|
|
187
209
|
}
|
|
188
|
-
|
|
189
|
-
"sha256:{:x}",
|
|
190
|
-
Sha256::digest(serde_json::to_vec(&self).map_err(|_| Error::Invalid)?)
|
|
191
|
-
);
|
|
192
|
-
let prepared = Prepared {
|
|
193
|
-
policy: self,
|
|
194
|
-
digest,
|
|
195
|
-
};
|
|
196
|
-
// Compile during preparation; an oversized native batch never reaches the kernel.
|
|
197
|
-
let program = prepared.program(&format!("snft_{}", "x".repeat(59)))?;
|
|
198
|
-
// Reserve enough of the batch for deleting a previous maximum
|
|
199
|
-
// graph, then installing this complete graph in the same transaction.
|
|
200
|
-
if program.len() > 768
|
|
201
|
-
|| program
|
|
202
|
-
.iter()
|
|
203
|
-
.map(|(_, attrs)| wire::encode_attrs(attrs).len() + 20)
|
|
204
|
-
.sum::<usize>()
|
|
205
|
-
> 100_000
|
|
206
|
-
{
|
|
207
|
-
return Err(Error::Invalid);
|
|
208
|
-
}
|
|
209
|
-
Ok(prepared)
|
|
210
|
+
Ok(self)
|
|
210
211
|
}
|
|
211
212
|
}
|
|
212
213
|
|
package/rust/src/tests.rs
CHANGED
|
@@ -117,3 +117,103 @@ fn compiler_bounds_full_policy_and_keeps_directed_port_grants() {
|
|
|
117
117
|
value.revision = 9_007_199_254_740_992;
|
|
118
118
|
assert_eq!(value.prepare(), Err(Error::Invalid));
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
fn exhausted<T>(name: &'static str, limit: usize, actual: usize) -> crate::Result<T> {
|
|
122
|
+
Err(Error::Exhausted(crate::Bound {
|
|
123
|
+
name,
|
|
124
|
+
limit,
|
|
125
|
+
actual,
|
|
126
|
+
}))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Every endpoint with 16 prefixes and `rules` distinct directed rules between
|
|
130
|
+
/// them: well-formed input whose only fault can be its size.
|
|
131
|
+
fn sized(endpoints: u32, rules: u16) -> Policy {
|
|
132
|
+
let mut value = policy();
|
|
133
|
+
let template = value.endpoints[0].clone();
|
|
134
|
+
value.endpoints = (0..endpoints)
|
|
135
|
+
.map(|index| {
|
|
136
|
+
let mut endpoint = template.clone();
|
|
137
|
+
endpoint.id = format!("e{index}");
|
|
138
|
+
endpoint.interface_index = 10 + index;
|
|
139
|
+
endpoint.interface_name = format!("w{index}");
|
|
140
|
+
endpoint.source_prefixes = (0..16)
|
|
141
|
+
.map(|part| format!("10.{}.{part}.0/24", 100 + index))
|
|
142
|
+
.collect();
|
|
143
|
+
endpoint
|
|
144
|
+
})
|
|
145
|
+
.collect();
|
|
146
|
+
let template = value.rules[0].clone();
|
|
147
|
+
value.rules = (0..rules)
|
|
148
|
+
.map(|index| {
|
|
149
|
+
let (from, to) = (
|
|
150
|
+
u32::from(index) % endpoints,
|
|
151
|
+
(u32::from(index) + 1) % endpoints,
|
|
152
|
+
);
|
|
153
|
+
let mut rule = template.clone();
|
|
154
|
+
rule.source_endpoint = Some(format!("e{from}"));
|
|
155
|
+
rule.destination_endpoint = Some(format!("e{to}"));
|
|
156
|
+
rule.source_prefix = format!("10.{}.0.0/24", 100 + from);
|
|
157
|
+
rule.destination_prefix = format!("10.{}.0.0/24", 100 + to);
|
|
158
|
+
rule.destination_port = Some(1000 + index);
|
|
159
|
+
rule
|
|
160
|
+
})
|
|
161
|
+
.collect();
|
|
162
|
+
value
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn v1_capacity_refusals_name_their_bound_and_invalid_input_stays_invalid() {
|
|
167
|
+
assert_eq!(sized(2, 2).prepare().map(|_| ()), Ok(()));
|
|
168
|
+
// Within the schema's counts, the complete graph exceeds the byte and the
|
|
169
|
+
// operation budget; the complete program is measured, so both are exact.
|
|
170
|
+
assert_eq!(
|
|
171
|
+
sized(8, 16).prepare(),
|
|
172
|
+
exhausted("ruleBytes", 100_000, 186_952)
|
|
173
|
+
);
|
|
174
|
+
assert_eq!(sized(32, 128).prepare(), exhausted("operations", 768, 1667));
|
|
175
|
+
assert_eq!(sized(33, 16).prepare(), exhausted("endpoints", 32, 33));
|
|
176
|
+
assert_eq!(sized(8, 129).prepare(), exhausted("rules", 128, 129));
|
|
177
|
+
// Invalid input is never reported as capacity, whatever its size.
|
|
178
|
+
let mut value = sized(33, 129);
|
|
179
|
+
value.schema_version = 2;
|
|
180
|
+
assert_eq!(value.prepare(), Err(Error::Invalid));
|
|
181
|
+
let mut value = sized(2, 2);
|
|
182
|
+
value.endpoints[0].interface_name = "w".repeat(16);
|
|
183
|
+
assert_eq!(value.prepare(), Err(Error::Invalid));
|
|
184
|
+
let mut value = sized(2, 2);
|
|
185
|
+
value.endpoints[0]
|
|
186
|
+
.source_prefixes
|
|
187
|
+
.push("10.200.0.0/24".into());
|
|
188
|
+
assert_eq!(value.prepare(), Err(Error::Invalid));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn failure_responses_carry_the_refusal_reason_and_capacity_bound() {
|
|
193
|
+
let bound = crate::Bound {
|
|
194
|
+
name: "targetBytes",
|
|
195
|
+
limit: 212_000,
|
|
196
|
+
actual: 212_345,
|
|
197
|
+
};
|
|
198
|
+
assert_eq!(
|
|
199
|
+
crate::failure("7", Error::Exhausted(bound)),
|
|
200
|
+
serde_json::json!({"id":"7","success":false,
|
|
201
|
+
"error":"Managed policy exceeds the targetBytes capacity of 212000: it reached 212345.",
|
|
202
|
+
"errorCode":"EXHAUSTED","errorData":{"bound":"targetBytes","limit":212_000,"actual":212_345}})
|
|
203
|
+
);
|
|
204
|
+
assert_eq!(
|
|
205
|
+
crate::failure("8", Error::Invalid),
|
|
206
|
+
serde_json::json!({"id":"8","success":false,"error":"Managed policy input is invalid.","errorCode":"INVALID"})
|
|
207
|
+
);
|
|
208
|
+
for error in [
|
|
209
|
+
Error::Conflict,
|
|
210
|
+
Error::Permission,
|
|
211
|
+
Error::Protocol,
|
|
212
|
+
Error::Unavailable,
|
|
213
|
+
] {
|
|
214
|
+
assert_eq!(
|
|
215
|
+
crate::failure("9", error),
|
|
216
|
+
serde_json::json!({"id":"9","success":false,"error":"Managed policy operation failed.","errorCode":error.code()})
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import { snapshot, managedIpcBytes, ManagedNftablesError } from './classes.managednftables.js';
|
|
2
|
+
import { snapshot, managedIpcBytes, ManagedNftablesError, nativeError } from './classes.managednftables.js';
|
|
3
3
|
import type { IManagedDockerForwardingOptions, IManagedDockerForwardingStatus, IManagedDockerForwardingPolicy, IPreparedManagedDockerForwardingPolicy, IManagedDockerForwardingTransition, IAppliedManagedDockerForwardingPolicy, TManagedDockerForwardingCommands } from './managed.docker.types.js';
|
|
4
4
|
|
|
5
5
|
/** One node-level Docker DOCKER-USER contribution. Caller owns durable intents and handoff lifetimes. */
|
|
@@ -103,8 +103,7 @@ export class ManagedDockerForwarding {
|
|
|
103
103
|
#track<T>(work: Promise<T>): Promise<T> {
|
|
104
104
|
const owned = work.catch((error) => {
|
|
105
105
|
if (this.#state !== 'closing') this.#state = 'failed-owned';
|
|
106
|
-
|
|
107
|
-
throw new ManagedNftablesError(typeof code === 'string' && /^[A-Z_]{1,64}$/.test(code) ? code : 'UNAVAILABLE');
|
|
106
|
+
throw nativeError(error);
|
|
108
107
|
});
|
|
109
108
|
this.#work = owned;
|
|
110
109
|
void owned.then(() => { this.#work = null; }, () => { this.#work = null; });
|