@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
|
@@ -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,12 +90,37 @@ fn covers(parent: &str, child: &str) -> Result<bool> {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
impl Policy {
|
|
93
|
-
pub fn prepare(
|
|
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
|
+
if program.len() > 768
|
|
105
|
+
|| program
|
|
106
|
+
.iter()
|
|
107
|
+
.map(|(_, attrs)| wire::encode_attrs(attrs).len() + 20)
|
|
108
|
+
.sum::<usize>()
|
|
109
|
+
> 100_000
|
|
110
|
+
{
|
|
111
|
+
return Err(Error::Invalid);
|
|
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> {
|
|
94
119
|
if self.schema_version != 1
|
|
95
120
|
|| self.revision == 0
|
|
96
121
|
|| self.revision > 9_007_199_254_740_991
|
|
97
|
-
|| self.endpoints.len() >
|
|
98
|
-
|| self.rules.len() >
|
|
122
|
+
|| self.endpoints.len() > endpoints
|
|
123
|
+
|| self.rules.len() > rules
|
|
99
124
|
{
|
|
100
125
|
return Err(Error::Invalid);
|
|
101
126
|
}
|
|
@@ -185,28 +210,7 @@ impl Policy {
|
|
|
185
210
|
}
|
|
186
211
|
}
|
|
187
212
|
}
|
|
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)
|
|
213
|
+
Ok(self)
|
|
210
214
|
}
|
|
211
215
|
}
|
|
212
216
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -60,9 +60,14 @@ export interface IManagedNftEgressGenerationV2 extends IManagedNftHandoffAllocat
|
|
|
60
60
|
* `transitSourceAddress:transitPort`, this translates on to the workload. */
|
|
61
61
|
export interface IManagedNftRouterPublishedPortV2 {
|
|
62
62
|
protocol: 'tcp' | 'udp';
|
|
63
|
-
/** Published handoff port, 1–65535
|
|
64
|
-
* It is the `targetPort` of the matching host-transit publication. */
|
|
63
|
+
/** Published handoff port, 1–65535; published ports never overlap per protocol across the
|
|
64
|
+
* scope. It is the `targetPort` of the matching host-transit publication. */
|
|
65
65
|
transitPort: number;
|
|
66
|
+
/** Optional last port of a published range `transitPort..transitPortEnd`, greater than
|
|
67
|
+
* `transitPort`. A range keeps every port, so `endpointPort` must equal `transitPort`; like one
|
|
68
|
+
* port it is one interval set element per set, and its translation keeps the port. Absent for
|
|
69
|
+
* one port, which is its only canonical form. */
|
|
70
|
+
transitPortEnd?: number;
|
|
66
71
|
endpointPort: number;
|
|
67
72
|
/** Exact current workload endpoint address behind one veth endpoint of this
|
|
68
73
|
* scope. Never a router-local address, a gateway or a platform endpoint. */
|
|
@@ -70,6 +75,12 @@ export interface IManagedNftRouterPublishedPortV2 {
|
|
|
70
75
|
/** Exact leased transit source address of one current generation, present on
|
|
71
76
|
* `handoff`. No wildcard, secondary-address or route inference. */
|
|
72
77
|
transitSourceAddress: string;
|
|
78
|
+
/** Optional: the workload may open flows from `endpointAddress` and its published endpoint
|
|
79
|
+
* port(s) toward unprotected space, source-translated to `transitSourceAddress` and the
|
|
80
|
+
* published transit port (a range keeps the port). The protected union stays denied, and its
|
|
81
|
+
* endpoint ports may not overlap another publication's on the same address. Absent and
|
|
82
|
+
* `false` are the same canonical policy. */
|
|
83
|
+
symmetric?: boolean;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
/** One exact host-origin flow: the node's own host network namespace dials one port of one local
|
|
@@ -136,13 +147,24 @@ export interface IManagedNftRouterEgressScopeV2 {
|
|
|
136
147
|
/** One inbound uplink publication, compiled inside the same host-transit generation. */
|
|
137
148
|
export interface IManagedNftPublishedPortV2 {
|
|
138
149
|
protocol: 'tcp' | 'udp';
|
|
139
|
-
/** Published uplink port, 1–65535
|
|
150
|
+
/** Published uplink port, 1–65535; published ports never overlap per protocol across the scope. */
|
|
140
151
|
hostPort: number;
|
|
152
|
+
/** Optional last port of a published range `hostPort..hostPortEnd`, greater than `hostPort`.
|
|
153
|
+
* A range keeps every port, so `targetPort` must equal `hostPort`; like one port it is one
|
|
154
|
+
* interval set element per set, and its translation keeps the port. Absent for one port, which
|
|
155
|
+
* is its only canonical form. */
|
|
156
|
+
hostPortEnd?: number;
|
|
141
157
|
targetPort: number;
|
|
142
158
|
/** Exact leased transit source address of one current allocation; it selects the handoff. */
|
|
143
159
|
targetAddress: string;
|
|
144
160
|
/** Exact current uplink address that publishes this port. No wildcard or route inference. */
|
|
145
161
|
hostIp: string;
|
|
162
|
+
/** Optional: flows from `targetAddress` and the published target port(s) may leave through the
|
|
163
|
+
* uplink toward unprotected space, source-translated to `hostIp` and the published uplink port
|
|
164
|
+
* (a range keeps the port). The protected union stays denied, its target ports may not fall in
|
|
165
|
+
* a leased range of `targetAddress` or overlap another publication's. Absent and `false` are
|
|
166
|
+
* the same canonical policy. */
|
|
167
|
+
symmetric?: boolean;
|
|
146
168
|
}
|
|
147
169
|
|
|
148
170
|
/** Shared-host capture barrier and explicit outer SNAT; Docker forwarding remains a separate owner. */
|