@push.rocks/smartnftables 1.3.1 → 1.5.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 +24 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.managednftables.d.ts +7 -7
- package/dist_ts/classes.managednftables.js +9 -3
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +1 -1
- package/dist_ts/managed.egress.types.d.ts +91 -0
- package/dist_ts/managed.egress.types.js +2 -0
- package/dist_ts/managed.types.d.ts +25 -19
- package/package.json +5 -5
- package/readme.md +97 -3
- package/rust/src/egress.compile.rs +288 -0
- package/rust/src/egress.host.rs +103 -0
- package/rust/src/egress.router.rs +231 -0
- package/rust/src/egress.rs +682 -0
- package/rust/src/egress_tests.rs +332 -0
- package/rust/src/main.rs +23 -3
- package/rust/src/managed.rs +105 -0
- package/rust/src/namespace.rs +51 -0
- package/rust/src/owner.rs +96 -6
- package/rust/src/owner_coexistence_tests.rs +77 -0
- package/rust/src/owner_egress_identity_tests.rs +155 -0
- package/rust/src/owner_egress_tests.rs +153 -0
- package/rust/src/owner_egress_traffic_tests.rs +470 -0
- package/rust/src/owner_host_traffic_tests.rs +305 -0
- package/rust/src/owner_identity_tests.rs +169 -0
- package/rust/src/owner_link_tests.rs +119 -0
- package/rust/src/owner_namespace_tests.rs +242 -0
- package/rust/src/owner_packet_fixture.rs +200 -0
- package/rust/src/owner_tests.rs +36 -7
- package/rust/src/policy.rs +120 -81
- package/rust/src/tests.rs +22 -0
- package/rust/src/wire.links.rs +268 -0
- package/rust/src/wire.rs +3 -64
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.managednftables.ts +19 -14
- package/ts/index.ts +1 -0
- package/ts/managed.egress.types.ts +85 -0
- package/ts/managed.types.ts +24 -16
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
use super::{Policy, Scope};
|
|
2
|
+
use serde_json::{json, Value};
|
|
3
|
+
|
|
4
|
+
fn link(index: u32, name: &str, kind: &str, address: &str) -> Value {
|
|
5
|
+
json!({"interfaceIndex":index,"interfaceName":name,"interfaceKind":kind,
|
|
6
|
+
"macAddress":if kind == "tun" { Value::Null } else { json!(format!("02:00:00:00:00:{index:02x}")) },
|
|
7
|
+
"interfaceLinkIndex":index+100,"requiredIpv4Addresses":[address]})
|
|
8
|
+
}
|
|
9
|
+
fn allocation() -> Value {
|
|
10
|
+
json!({"lease":{"id":"lease-a","generation":1,"digest":format!("sha256:{}","a".repeat(64))},
|
|
11
|
+
"transitSourceAddress":"10.240.0.2",
|
|
12
|
+
"sourcePortRanges":[{"protocol":"tcp","first":10000,"last":10999},{"protocol":"udp","first":10000,"last":10999}]})
|
|
13
|
+
}
|
|
14
|
+
fn protection() -> Value {
|
|
15
|
+
json!({"authorityDigest":format!("sha256:{}","b".repeat(64)),
|
|
16
|
+
"prefixes":["10.0.0.0/8","192.0.2.0/24","203.0.113.10/32"],
|
|
17
|
+
"platformEndpoints":[{"id":"dns","address":"192.0.2.1","protocol":"udp","port":53},
|
|
18
|
+
{"id":"relay","address":"203.0.113.10","protocol":"tcp","port":443}]})
|
|
19
|
+
}
|
|
20
|
+
pub(crate) fn router() -> Value {
|
|
21
|
+
let mut generation = allocation();
|
|
22
|
+
generation["conntrackZone"] = json!(1);
|
|
23
|
+
generation["conntrackLabel"] = json!("a".repeat(32));
|
|
24
|
+
generation["grants"] = json!([
|
|
25
|
+
{"sourceEndpoint":"work","sourcePrefix":"10.241.0.2/32","destination":{"kind":"public","prefix":"0.0.0.0/0"},
|
|
26
|
+
"protocol":"tcp","sourcePort":null,"destinationPort":443,"sourcePortRange":{"first":10000,"last":10999}},
|
|
27
|
+
{"sourceEndpoint":null,"sourcePrefix":"10.240.0.2/32","destination":{"kind":"platform","endpointId":"dns"},
|
|
28
|
+
"protocol":"udp","sourcePort":null,"destinationPort":53,"sourcePortRange":{"first":10000,"last":10999}},
|
|
29
|
+
{"sourceEndpoint":null,"sourcePrefix":"10.240.0.2/32","destination":{"kind":"platform","endpointId":"relay"},
|
|
30
|
+
"protocol":"tcp","sourcePort":null,"destinationPort":443,"sourcePortRange":{"first":10000,"last":10999}}
|
|
31
|
+
]);
|
|
32
|
+
json!({"schemaVersion":2,"revision":1,"scope":{"kind":"routerEgress",
|
|
33
|
+
"endpoints":[
|
|
34
|
+
{"id":"work","interfaceIndex":2,"interfaceName":"work","interfaceKind":"veth","sourcePrefixes":["10.241.0.2/32"]},
|
|
35
|
+
{"id":"vpn","interfaceIndex":3,"interfaceName":"vpn","interfaceKind":"tun","sourcePrefixes":["10.242.0.0/24"]}],
|
|
36
|
+
"rules":[
|
|
37
|
+
{"sourceEndpoint":"work","destinationEndpoint":"vpn","sourcePrefix":"10.241.0.2/32","destinationPrefix":"10.242.0.2/32","protocol":"udp","sourcePort":null,"destinationPort":53},
|
|
38
|
+
{"sourceEndpoint":"work","destinationEndpoint":null,"sourcePrefix":"10.241.0.2/32","destinationPrefix":"10.241.0.1/32","protocol":"udp","sourcePort":null,"destinationPort":53},
|
|
39
|
+
{"sourceEndpoint":null,"destinationEndpoint":"work","sourcePrefix":"10.241.0.1/32","destinationPrefix":"10.241.0.2/32","protocol":"udp","sourcePort":53,"destinationPort":null}],
|
|
40
|
+
"links":[link(2,"work","veth","10.241.0.1"),link(3,"vpn","tun","10.242.0.1")],
|
|
41
|
+
"handoff":link(4,"handoff","veth","10.240.0.2"),"protection":protection(),"generations":[generation]}})
|
|
42
|
+
}
|
|
43
|
+
pub(crate) fn host() -> Value {
|
|
44
|
+
json!({"schemaVersion":2,"revision":1,"scope":{"kind":"hostTransit","protection":protection(),
|
|
45
|
+
"handoffs":[{"link":link(4,"handoff","veth","10.240.0.1"),"allocations":[allocation()]}],
|
|
46
|
+
"uplink":link(2,"ens18","ethernet","192.0.2.2"),"snatAddress":"192.0.2.2"}})
|
|
47
|
+
}
|
|
48
|
+
fn normalized(value: Value) -> super::Result<Policy> {
|
|
49
|
+
serde_json::from_value::<Policy>(value)
|
|
50
|
+
.map_err(|_| crate::Error::Invalid)?
|
|
51
|
+
.normalize()
|
|
52
|
+
}
|
|
53
|
+
fn changed(mut value: Value, path: &str, replacement: Value) -> Value {
|
|
54
|
+
*value.pointer_mut(path).unwrap() = replacement;
|
|
55
|
+
value
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn combined_router_preserves_private_tun_dns_and_explicit_local_egress() {
|
|
60
|
+
let expected = normalized(router()).unwrap();
|
|
61
|
+
let mut reordered = router();
|
|
62
|
+
for path in [
|
|
63
|
+
"/scope/endpoints",
|
|
64
|
+
"/scope/rules",
|
|
65
|
+
"/scope/links",
|
|
66
|
+
"/scope/protection/prefixes",
|
|
67
|
+
"/scope/protection/platformEndpoints",
|
|
68
|
+
"/scope/generations/0/sourcePortRanges",
|
|
69
|
+
"/scope/generations/0/grants",
|
|
70
|
+
] {
|
|
71
|
+
reordered
|
|
72
|
+
.pointer_mut(path)
|
|
73
|
+
.unwrap()
|
|
74
|
+
.as_array_mut()
|
|
75
|
+
.unwrap()
|
|
76
|
+
.reverse();
|
|
77
|
+
}
|
|
78
|
+
assert_eq!(normalized(reordered).unwrap(), expected);
|
|
79
|
+
let denied = normalized(changed(router(), "/scope/generations", json!([]))).unwrap();
|
|
80
|
+
let Scope::RouterEgress(denied) = denied.scope else {
|
|
81
|
+
panic!()
|
|
82
|
+
};
|
|
83
|
+
assert_eq!(denied.endpoints.len(), 2);
|
|
84
|
+
assert_eq!(denied.rules.len(), 3);
|
|
85
|
+
assert!(denied.generations.is_empty());
|
|
86
|
+
assert!(normalized(host()).is_ok());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn v2_rejects_unknown_fields_missing_nulls_and_wrong_scope_shapes() {
|
|
91
|
+
let mut unknown = router();
|
|
92
|
+
unknown["commands"] = json!([]);
|
|
93
|
+
let mut no_null = router();
|
|
94
|
+
no_null["scope"]["generations"][0]["grants"][0]
|
|
95
|
+
.as_object_mut()
|
|
96
|
+
.unwrap()
|
|
97
|
+
.remove("sourcePort");
|
|
98
|
+
let mut mixed = host();
|
|
99
|
+
mixed["scope"]["generations"] = json!([]);
|
|
100
|
+
for invalid in [
|
|
101
|
+
unknown,
|
|
102
|
+
no_null,
|
|
103
|
+
mixed,
|
|
104
|
+
changed(router(), "/schemaVersion", json!(1)),
|
|
105
|
+
changed(router(), "/revision", json!(0)),
|
|
106
|
+
changed(router(), "/revision", json!(9007199254740992_u64)),
|
|
107
|
+
changed(router(), "/scope/kind", json!("egress")),
|
|
108
|
+
] {
|
|
109
|
+
assert!(normalized(invalid).is_err());
|
|
110
|
+
}
|
|
111
|
+
let duplicate = serde_json::to_string(&router()).unwrap().replace(
|
|
112
|
+
"\"schemaVersion\":2",
|
|
113
|
+
"\"schemaVersion\":2,\"schemaVersion\":2",
|
|
114
|
+
);
|
|
115
|
+
assert!(serde_json::from_str::<Policy>(&duplicate).is_err());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn v2_binds_exact_local_links_addresses_and_complete_protection() {
|
|
120
|
+
for (path, replacement) in [
|
|
121
|
+
("/scope/handoff/interfaceKind", json!("ethernet")),
|
|
122
|
+
("/scope/handoff/interfaceIndex", json!(2)),
|
|
123
|
+
("/scope/handoff/macAddress", json!("03:00:00:00:00:04")),
|
|
124
|
+
("/scope/handoff/macAddress", json!("00:00:00:00:00:00")),
|
|
125
|
+
("/scope/handoff/interfaceLinkIndex", json!(0)),
|
|
126
|
+
("/scope/links/0/interfaceName", json!("foreign")),
|
|
127
|
+
("/scope/links/1/macAddress", json!("02:00:00:00:00:03")),
|
|
128
|
+
(
|
|
129
|
+
"/scope/links/0/requiredIpv4Addresses",
|
|
130
|
+
json!(["10.241.0.1", "10.241.0.1"]),
|
|
131
|
+
),
|
|
132
|
+
("/scope/protection/prefixes", json!(["10.0.0.0/8"])),
|
|
133
|
+
(
|
|
134
|
+
"/scope/protection/prefixes",
|
|
135
|
+
json!([
|
|
136
|
+
"10.0.0.0/8",
|
|
137
|
+
"10.240.0.0/16",
|
|
138
|
+
"192.0.2.0/24",
|
|
139
|
+
"203.0.113.10/32"
|
|
140
|
+
]),
|
|
141
|
+
),
|
|
142
|
+
(
|
|
143
|
+
"/scope/protection/platformEndpoints/0/address",
|
|
144
|
+
json!("10.241.0.1"),
|
|
145
|
+
),
|
|
146
|
+
(
|
|
147
|
+
"/scope/protection/platformEndpoints/0/address",
|
|
148
|
+
json!("10.241.0.2"),
|
|
149
|
+
),
|
|
150
|
+
("/scope/protection/authorityDigest", json!("sha256:bad")),
|
|
151
|
+
] {
|
|
152
|
+
assert!(
|
|
153
|
+
normalized(changed(router(), path, replacement)).is_err(),
|
|
154
|
+
"{path}"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
for (path, replacement) in [
|
|
158
|
+
("/scope/snatAddress", json!("192.0.2.4")),
|
|
159
|
+
("/scope/uplink/interfaceKind", json!("tun")),
|
|
160
|
+
(
|
|
161
|
+
"/scope/handoffs/0/allocations/0/transitSourceAddress",
|
|
162
|
+
json!("10.240.0.1"),
|
|
163
|
+
),
|
|
164
|
+
(
|
|
165
|
+
"/scope/protection/platformEndpoints/0/address",
|
|
166
|
+
json!("192.0.2.2"),
|
|
167
|
+
),
|
|
168
|
+
] {
|
|
169
|
+
assert!(
|
|
170
|
+
normalized(changed(host(), path, replacement)).is_err(),
|
|
171
|
+
"{path}"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#[test]
|
|
177
|
+
fn v2_rejects_unleased_ports_unbound_sources_and_ambiguous_platform_grants() {
|
|
178
|
+
for (path, replacement) in [
|
|
179
|
+
("/scope/generations/0/lease/generation", json!(0)),
|
|
180
|
+
("/scope/generations/0/conntrackZone", json!(0)),
|
|
181
|
+
("/scope/generations/0/conntrackZone", json!(65536)),
|
|
182
|
+
("/scope/generations/0/conntrackLabel", json!("0".repeat(32))),
|
|
183
|
+
(
|
|
184
|
+
"/scope/generations/0/transitSourceAddress",
|
|
185
|
+
json!("10.240.0.3"),
|
|
186
|
+
),
|
|
187
|
+
("/scope/generations/0/sourcePortRanges/0/first", json!(0)),
|
|
188
|
+
("/scope/generations/0/sourcePortRanges/0/last", json!(9999)),
|
|
189
|
+
(
|
|
190
|
+
"/scope/generations/0/grants/0/sourcePortRange/last",
|
|
191
|
+
json!(11000),
|
|
192
|
+
),
|
|
193
|
+
("/scope/generations/0/grants/0/sourceEndpoint", json!("vpn")),
|
|
194
|
+
(
|
|
195
|
+
"/scope/generations/0/grants/0/sourcePrefix",
|
|
196
|
+
json!("10.241.0.0/24"),
|
|
197
|
+
),
|
|
198
|
+
("/scope/generations/0/grants/0/sourcePort", json!(0)),
|
|
199
|
+
(
|
|
200
|
+
"/scope/generations/0/grants/0/destination/prefix",
|
|
201
|
+
json!("10.242.0.0/24"),
|
|
202
|
+
),
|
|
203
|
+
(
|
|
204
|
+
"/scope/generations/0/grants/1/sourcePrefix",
|
|
205
|
+
json!("10.240.0.0/24"),
|
|
206
|
+
),
|
|
207
|
+
("/scope/generations/0/grants/1/destinationPort", Value::Null),
|
|
208
|
+
("/scope/generations/0/grants/1/protocol", json!("tcp")),
|
|
209
|
+
] {
|
|
210
|
+
assert!(
|
|
211
|
+
normalized(changed(router(), path, replacement)).is_err(),
|
|
212
|
+
"{path}"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn v2_classification_and_return_ranges_are_disjoint_between_active_generations() {
|
|
219
|
+
let mut value = router();
|
|
220
|
+
value["scope"]["generations"][0]["grants"]
|
|
221
|
+
.as_array_mut()
|
|
222
|
+
.unwrap()
|
|
223
|
+
.truncate(1);
|
|
224
|
+
let mut next = value["scope"]["generations"][0].clone();
|
|
225
|
+
next["lease"]["id"] = json!("lease-b");
|
|
226
|
+
next["conntrackZone"] = json!(2);
|
|
227
|
+
next["conntrackLabel"] = json!("b".repeat(32));
|
|
228
|
+
next["sourcePortRanges"] = json!([{"protocol":"tcp","first":11000,"last":11999}]);
|
|
229
|
+
next["grants"][0]["sourcePortRange"] = json!({"first":11000,"last":11999});
|
|
230
|
+
value["scope"]["generations"]
|
|
231
|
+
.as_array_mut()
|
|
232
|
+
.unwrap()
|
|
233
|
+
.push(next);
|
|
234
|
+
assert!(normalized(value.clone()).is_err());
|
|
235
|
+
value["scope"]["generations"][0]["grants"][0]["sourcePort"] = json!(1000);
|
|
236
|
+
value["scope"]["generations"][1]["grants"][0]["sourcePort"] = json!(2000);
|
|
237
|
+
assert!(normalized(value.clone()).is_ok());
|
|
238
|
+
for (path, replacement) in [
|
|
239
|
+
("/scope/generations/1/conntrackZone", json!(1)),
|
|
240
|
+
("/scope/generations/1/conntrackLabel", json!("a".repeat(32))),
|
|
241
|
+
("/scope/generations/1/lease/id", json!("lease-a")),
|
|
242
|
+
] {
|
|
243
|
+
assert!(normalized(changed(value.clone(), path, replacement)).is_err());
|
|
244
|
+
}
|
|
245
|
+
value["scope"]["generations"][1]["sourcePortRanges"][0] =
|
|
246
|
+
json!({"protocol":"tcp","first":10500,"last":11500});
|
|
247
|
+
value["scope"]["generations"][1]["grants"][0]["sourcePortRange"] =
|
|
248
|
+
json!({"first":10500,"last":11500});
|
|
249
|
+
assert!(normalized(value).is_err());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn host_never_assigns_the_same_transit_source_to_different_active_handoff_links() {
|
|
254
|
+
let mut value = host();
|
|
255
|
+
let mut other = value["scope"]["handoffs"][0].clone();
|
|
256
|
+
other["link"] = link(5, "handoff-b", "veth", "10.240.0.5");
|
|
257
|
+
other["allocations"][0]["lease"]["id"] = json!("lease-b");
|
|
258
|
+
other["allocations"][0]["sourcePortRanges"] =
|
|
259
|
+
json!([{"protocol":"udp","first":12000,"last":12999}]);
|
|
260
|
+
value["scope"]["handoffs"]
|
|
261
|
+
.as_array_mut()
|
|
262
|
+
.unwrap()
|
|
263
|
+
.push(other);
|
|
264
|
+
assert!(normalized(value.clone()).is_err());
|
|
265
|
+
value["scope"]["handoffs"][1]["allocations"][0]["transitSourceAddress"] = json!("10.240.0.6");
|
|
266
|
+
assert!(normalized(value).is_ok());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#[test]
|
|
270
|
+
fn prepared_v2_compiles_both_scopes_with_bounded_and_identifiable_graphs() {
|
|
271
|
+
for fixture in [router(), host()] {
|
|
272
|
+
let prepared = serde_json::from_value::<Policy>(fixture)
|
|
273
|
+
.unwrap()
|
|
274
|
+
.prepare()
|
|
275
|
+
.unwrap();
|
|
276
|
+
prepared.validate().unwrap();
|
|
277
|
+
let program = prepared.program("snft_v2_fixture").unwrap();
|
|
278
|
+
assert!(program.len() <= 768);
|
|
279
|
+
assert!(
|
|
280
|
+
program
|
|
281
|
+
.iter()
|
|
282
|
+
.map(|(_, attributes)| crate::wire::encode_attrs(attributes).len() + 20)
|
|
283
|
+
.sum::<usize>()
|
|
284
|
+
<= 100_000
|
|
285
|
+
);
|
|
286
|
+
assert!(program.iter().all(|(kind, _)| [3, 6].contains(kind)));
|
|
287
|
+
let mut changed = prepared.clone();
|
|
288
|
+
changed.policy.revision += 1;
|
|
289
|
+
assert!(changed.validate().is_err());
|
|
290
|
+
assert_ne!(changed.policy.prepare().unwrap().digest, prepared.digest);
|
|
291
|
+
println!("V2_GRAPH operations={}", program.len());
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn v2_compiled_graph_limit_rejects_bounded_but_excessive_cross_products() {
|
|
297
|
+
let mut value = router();
|
|
298
|
+
let mut grants = Vec::new();
|
|
299
|
+
for port in 1000..1032 {
|
|
300
|
+
let mut grant = value["scope"]["generations"][0]["grants"][0].clone();
|
|
301
|
+
grant["destinationPort"] = json!(port);
|
|
302
|
+
grants.push(grant);
|
|
303
|
+
}
|
|
304
|
+
value["scope"]["generations"][0]["grants"] = json!(grants);
|
|
305
|
+
let policy = normalized(value).unwrap();
|
|
306
|
+
assert!(policy.prepare().is_err());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[test]
|
|
310
|
+
fn managed_union_keeps_strict_duplicate_and_unknown_field_rejection() {
|
|
311
|
+
for value in [router(), host()] {
|
|
312
|
+
let text = serde_json::to_string(&value).unwrap();
|
|
313
|
+
assert!(serde_json::from_str::<crate::managed::Policy>(&text).is_ok());
|
|
314
|
+
for invalid in [
|
|
315
|
+
text.replace(
|
|
316
|
+
"\"schemaVersion\":2",
|
|
317
|
+
"\"schemaVersion\":2,\"schemaVersion\":2",
|
|
318
|
+
),
|
|
319
|
+
text.replace("\"revision\":1", "\"revision\":1,\"revision\":2"),
|
|
320
|
+
text.replace(
|
|
321
|
+
"\"interfaceIndex\":2",
|
|
322
|
+
"\"interfaceIndex\":2,\"interfaceIndex\":2",
|
|
323
|
+
),
|
|
324
|
+
] {
|
|
325
|
+
assert!(serde_json::from_str::<crate::managed::Policy>(&invalid).is_err());
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
assert!(serde_json::from_str::<crate::managed::Policy>(
|
|
329
|
+
r#"{"schemaVersion":1,"schemaVersion":1,"revision":1,"endpoints":[],"rules":[]}"#
|
|
330
|
+
)
|
|
331
|
+
.is_err());
|
|
332
|
+
}
|
package/rust/src/main.rs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
mod egress;
|
|
2
|
+
mod managed;
|
|
3
|
+
mod namespace;
|
|
1
4
|
mod owner;
|
|
2
5
|
mod policy;
|
|
3
6
|
#[cfg(test)]
|
|
@@ -58,7 +61,7 @@ fn encode(value: impl serde::Serialize) -> Result<serde_json::Value> {
|
|
|
58
61
|
}
|
|
59
62
|
fn dispatch(owner: &mut Option<owner::Owner>, request: Request) -> Result<serde_json::Value> {
|
|
60
63
|
match request.method.as_str() {
|
|
61
|
-
"preparePolicy" => encode(decode::<
|
|
64
|
+
"preparePolicy" => encode(decode::<managed::Policy>(request.params)?.prepare()?),
|
|
62
65
|
"openOwner" => {
|
|
63
66
|
if owner.is_some() {
|
|
64
67
|
return Err(Error::Conflict);
|
|
@@ -97,9 +100,26 @@ fn dispatch(owner: &mut Option<owner::Owner>, request: Request) -> Result<serde_
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
fn main() {
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
let arguments = std::env::args().skip(1).collect::<Vec<_>>();
|
|
104
|
+
let descriptor = match arguments.as_slice() {
|
|
105
|
+
[management] if management == "--management" => None,
|
|
106
|
+
[management, option, fd]
|
|
107
|
+
if management == "--management" && option == "--network-namespace-fd" =>
|
|
108
|
+
{
|
|
109
|
+
match fd.parse::<i32>() {
|
|
110
|
+
Ok(value) => Some(value),
|
|
111
|
+
Err(_) => std::process::exit(2),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
_ => std::process::exit(2),
|
|
115
|
+
};
|
|
116
|
+
if let Some(fd) = descriptor {
|
|
117
|
+
if let Err(error) = namespace::enter(fd) {
|
|
118
|
+
eprintln!("Network namespace startup failed: {error}");
|
|
119
|
+
std::process::exit(1);
|
|
120
|
+
}
|
|
102
121
|
}
|
|
122
|
+
// The single native owner must enter before readiness, context capture or sockets.
|
|
103
123
|
let mut output = std::io::stdout().lock();
|
|
104
124
|
writeln!(output, "{{\"event\":\"ready\",\"data\":{{}}}}").unwrap();
|
|
105
125
|
output.flush().unwrap();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
use crate::{egress, policy, wire, Result};
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
|
|
4
|
+
/// The v1 object remains byte-for-byte unchanged inside this input union.
|
|
5
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
6
|
+
#[serde(untagged)]
|
|
7
|
+
pub enum Policy {
|
|
8
|
+
Private(policy::Policy),
|
|
9
|
+
Egress(egress::Policy),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
13
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
14
|
+
pub struct Prepared {
|
|
15
|
+
pub policy: Policy,
|
|
16
|
+
pub digest: String,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl Policy {
|
|
20
|
+
pub fn revision(&self) -> u64 {
|
|
21
|
+
match self {
|
|
22
|
+
Self::Private(value) => value.revision,
|
|
23
|
+
Self::Egress(value) => value.revision,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
pub fn kind(&self) -> &'static str {
|
|
27
|
+
match self {
|
|
28
|
+
Self::Private(_) => "privateV1",
|
|
29
|
+
Self::Egress(egress::Policy {
|
|
30
|
+
scope: egress::Scope::RouterEgress(_),
|
|
31
|
+
..
|
|
32
|
+
}) => "routerEgressV2",
|
|
33
|
+
Self::Egress(egress::Policy {
|
|
34
|
+
scope: egress::Scope::HostTransit(_),
|
|
35
|
+
..
|
|
36
|
+
}) => "hostTransitV2",
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
pub fn prepare(self) -> Result<Prepared> {
|
|
40
|
+
match self {
|
|
41
|
+
Self::Private(value) => Ok(value.prepare()?.into()),
|
|
42
|
+
Self::Egress(value) => Ok(value.prepare()?.into()),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
impl From<policy::Prepared> for Prepared {
|
|
47
|
+
fn from(value: policy::Prepared) -> Self {
|
|
48
|
+
Self {
|
|
49
|
+
policy: Policy::Private(value.policy),
|
|
50
|
+
digest: value.digest,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
impl From<egress::Prepared> for Prepared {
|
|
55
|
+
fn from(value: egress::Prepared) -> Self {
|
|
56
|
+
Self {
|
|
57
|
+
policy: Policy::Egress(value.policy),
|
|
58
|
+
digest: value.digest,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
impl Prepared {
|
|
63
|
+
pub fn validate(&self) -> Result<()> {
|
|
64
|
+
match &self.policy {
|
|
65
|
+
Policy::Private(policy) => policy::Prepared {
|
|
66
|
+
policy: policy.clone(),
|
|
67
|
+
digest: self.digest.clone(),
|
|
68
|
+
}
|
|
69
|
+
.validate(),
|
|
70
|
+
Policy::Egress(policy) => egress::Prepared {
|
|
71
|
+
policy: policy.clone(),
|
|
72
|
+
digest: self.digest.clone(),
|
|
73
|
+
}
|
|
74
|
+
.validate(),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
pub fn program(&self, table: &str) -> Result<Vec<(u16, Vec<wire::Attr>)>> {
|
|
78
|
+
match &self.policy {
|
|
79
|
+
Policy::Private(policy) => policy::Prepared {
|
|
80
|
+
policy: policy.clone(),
|
|
81
|
+
digest: self.digest.clone(),
|
|
82
|
+
}
|
|
83
|
+
.program(table),
|
|
84
|
+
Policy::Egress(policy) => egress::Prepared {
|
|
85
|
+
policy: policy.clone(),
|
|
86
|
+
digest: self.digest.clone(),
|
|
87
|
+
}
|
|
88
|
+
.program(table),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
pub fn validate_interfaces(&self) -> Result<()> {
|
|
92
|
+
match &self.policy {
|
|
93
|
+
Policy::Private(policy) => policy::Prepared {
|
|
94
|
+
policy: policy.clone(),
|
|
95
|
+
digest: self.digest.clone(),
|
|
96
|
+
}
|
|
97
|
+
.validate_interfaces(),
|
|
98
|
+
Policy::Egress(policy) => egress::Prepared {
|
|
99
|
+
policy: policy.clone(),
|
|
100
|
+
digest: self.digest.clone(),
|
|
101
|
+
}
|
|
102
|
+
.validate_interfaces(),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//! Startup-only entry. No thread, runtime, socket or ready event may precede it.
|
|
2
|
+
use std::io;
|
|
3
|
+
|
|
4
|
+
#[cfg(target_os = "linux")]
|
|
5
|
+
pub fn enter(fd: i32) -> io::Result<()> {
|
|
6
|
+
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
|
7
|
+
|
|
8
|
+
// Only an explicitly inherited extra descriptor is accepted. Never consume IPC.
|
|
9
|
+
if fd < 3 {
|
|
10
|
+
return Err(io::Error::new(
|
|
11
|
+
io::ErrorKind::InvalidInput,
|
|
12
|
+
"network namespace descriptor must be at least 3",
|
|
13
|
+
));
|
|
14
|
+
}
|
|
15
|
+
if unsafe { libc::fcntl(fd, libc::F_GETFD) } < 0 {
|
|
16
|
+
return Err(io::Error::new(
|
|
17
|
+
io::ErrorKind::InvalidInput,
|
|
18
|
+
"network namespace descriptor is not open",
|
|
19
|
+
));
|
|
20
|
+
}
|
|
21
|
+
// Startup is single-threaded; this is the child's owned copy, not the parent's.
|
|
22
|
+
let descriptor = unsafe { OwnedFd::from_raw_fd(fd) };
|
|
23
|
+
let namespace_type = unsafe { libc::ioctl(descriptor.as_raw_fd(), libc::NS_GET_NSTYPE) };
|
|
24
|
+
if namespace_type != libc::CLONE_NEWNET {
|
|
25
|
+
return Err(io::Error::new(
|
|
26
|
+
io::ErrorKind::InvalidInput,
|
|
27
|
+
"descriptor is not a Linux network namespace",
|
|
28
|
+
));
|
|
29
|
+
}
|
|
30
|
+
if unsafe { libc::setns(descriptor.as_raw_fd(), libc::CLONE_NEWNET) } != 0 {
|
|
31
|
+
return Err(io::Error::new(
|
|
32
|
+
io::ErrorKind::PermissionDenied,
|
|
33
|
+
format!(
|
|
34
|
+
"network namespace entry failed: {}",
|
|
35
|
+
io::Error::last_os_error()
|
|
36
|
+
),
|
|
37
|
+
));
|
|
38
|
+
}
|
|
39
|
+
// Close the inherited capability before creating threads. Namespace membership
|
|
40
|
+
// itself keeps the namespace alive until the complete process has exited.
|
|
41
|
+
drop(descriptor);
|
|
42
|
+
Ok(())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[cfg(not(target_os = "linux"))]
|
|
46
|
+
pub fn enter(_fd: i32) -> io::Result<()> {
|
|
47
|
+
Err(io::Error::new(
|
|
48
|
+
io::ErrorKind::Unsupported,
|
|
49
|
+
"network namespace entry requires Linux",
|
|
50
|
+
))
|
|
51
|
+
}
|
package/rust/src/owner.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
use crate::
|
|
1
|
+
use crate::managed::Prepared;
|
|
2
|
+
use crate::policy::identifier;
|
|
2
3
|
use crate::wire::{self, Attr, Socket};
|
|
3
4
|
use crate::{Error, Result};
|
|
4
5
|
use serde::{Deserialize, Serialize};
|
|
@@ -171,7 +172,15 @@ impl Owner {
|
|
|
171
172
|
Err(error) => return Err(error),
|
|
172
173
|
_ => return Err(Error::Protocol),
|
|
173
174
|
};
|
|
174
|
-
|
|
175
|
+
// Linux's NFT_MSG_GETCHAIN dump is family-wide: it ignores
|
|
176
|
+
// NFTA_CHAIN_TABLE. Select exact table identity before examining
|
|
177
|
+
// chain contents; another owner's chains are not our graph.
|
|
178
|
+
let mut chains = Vec::new();
|
|
179
|
+
for chain in socket.query(4, vec![Attr::string(1, &table_name)], true)? {
|
|
180
|
+
if wire::text(&chain, 1)? == table_name {
|
|
181
|
+
chains.push(chain);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
175
184
|
let rules = socket.query(7, vec![Attr::string(1, &table_name)], true)?;
|
|
176
185
|
// The managed compiler never creates any of these. Foreign objects reject.
|
|
177
186
|
for operation in [10, 19, 23] {
|
|
@@ -222,7 +231,7 @@ impl Owner {
|
|
|
222
231
|
if handle == 0
|
|
223
232
|
|| handle.to_string() != expected.receipt.table_handle
|
|
224
233
|
|| expected.receipt.identity != self.identity
|
|
225
|
-
|| expected.receipt.revision != expected.prepared.policy.revision
|
|
234
|
+
|| expected.receipt.revision != expected.prepared.policy.revision()
|
|
226
235
|
|| expected.receipt.digest != expected.prepared.digest
|
|
227
236
|
{
|
|
228
237
|
return Err(Error::Conflict);
|
|
@@ -316,7 +325,7 @@ impl Owner {
|
|
|
316
325
|
receipt: Receipt {
|
|
317
326
|
identity: self.identity.clone(),
|
|
318
327
|
table_handle: wire::handle(&graph.table, 4)?.to_string(),
|
|
319
|
-
revision: prepared.policy.revision,
|
|
328
|
+
revision: prepared.policy.revision(),
|
|
320
329
|
digest: prepared.digest.clone(),
|
|
321
330
|
},
|
|
322
331
|
prepared,
|
|
@@ -334,7 +343,9 @@ impl Owner {
|
|
|
334
343
|
}
|
|
335
344
|
if let Some(previous) = &transition.previous {
|
|
336
345
|
self.validate_receipt(previous)?;
|
|
337
|
-
if transition.target.policy.
|
|
346
|
+
if transition.target.policy.kind() != previous.prepared.policy.kind()
|
|
347
|
+
|| transition.target.policy.revision() <= previous.prepared.policy.revision()
|
|
348
|
+
{
|
|
338
349
|
return Err(Error::Conflict);
|
|
339
350
|
}
|
|
340
351
|
}
|
|
@@ -568,11 +579,13 @@ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
|
|
|
568
579
|
}
|
|
569
580
|
let name = wire::text(&fields, 1)?;
|
|
570
581
|
let mut data = wire::attrs(&wire::one(&fields, 2)?.value)?;
|
|
582
|
+
validate_egress_expression(&name, &data)?;
|
|
571
583
|
let nested: &[u16] = match name.as_str() {
|
|
572
584
|
"immediate" => &[2],
|
|
573
585
|
"cmp" => &[3],
|
|
574
586
|
"bitwise" => &[4, 5],
|
|
575
|
-
"
|
|
587
|
+
"range" => &[3, 4],
|
|
588
|
+
"meta" | "payload" | "ct" | "nat" => &[],
|
|
576
589
|
_ => return Err(Error::Conflict),
|
|
577
590
|
};
|
|
578
591
|
for attribute in &mut data {
|
|
@@ -599,6 +612,83 @@ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
|
|
|
599
612
|
Ok((chain, encoded))
|
|
600
613
|
}
|
|
601
614
|
|
|
615
|
+
/// These expressions are compiler-owned. Preserve all operands in graph identity;
|
|
616
|
+
/// neither unknown fields nor default repair may hide a changed kernel rule.
|
|
617
|
+
fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
|
|
618
|
+
let allowed: &[u16] = match name {
|
|
619
|
+
"ct" | "range" => &[1, 2, 3, 4],
|
|
620
|
+
"nat" => &[1, 2, 3, 4, 5, 6, 7],
|
|
621
|
+
_ => return Ok(()),
|
|
622
|
+
};
|
|
623
|
+
for attribute in data {
|
|
624
|
+
if !allowed.contains(&attribute.id()) {
|
|
625
|
+
return Err(Error::Conflict);
|
|
626
|
+
}
|
|
627
|
+
wire::one(data, attribute.id())?; // Duplicate fields are never canonical identity.
|
|
628
|
+
}
|
|
629
|
+
match name {
|
|
630
|
+
"ct" => {
|
|
631
|
+
let read = data.iter().any(|item| item.id() == 1);
|
|
632
|
+
let write = data.iter().any(|item| item.id() == 4);
|
|
633
|
+
if read == write {
|
|
634
|
+
return Err(Error::Conflict);
|
|
635
|
+
}
|
|
636
|
+
wire::number(data, if read { 1 } else { 4 })?;
|
|
637
|
+
wire::number(data, 2)?;
|
|
638
|
+
if data.iter().any(|item| item.id() == 3)
|
|
639
|
+
&& ![vec![0], vec![1]].contains(&wire::one(data, 3)?.value)
|
|
640
|
+
{
|
|
641
|
+
return Err(Error::Conflict);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
"nat" => {
|
|
645
|
+
// Emit min/max registers and derived flags explicitly, just as Linux
|
|
646
|
+
// dumps them. This surface only compiles address-bearing IPv4 NAT.
|
|
647
|
+
for id in [1, 2, 3, 4, 7] {
|
|
648
|
+
wire::number(data, id)?;
|
|
649
|
+
}
|
|
650
|
+
if wire::number(data, 1)? > 1 || wire::number(data, 2)? != 2 {
|
|
651
|
+
return Err(Error::Conflict);
|
|
652
|
+
}
|
|
653
|
+
let first_port = data.iter().any(|item| item.id() == 5);
|
|
654
|
+
let last_port = data.iter().any(|item| item.id() == 6);
|
|
655
|
+
if first_port != last_port {
|
|
656
|
+
return Err(Error::Conflict);
|
|
657
|
+
}
|
|
658
|
+
if first_port {
|
|
659
|
+
wire::number(data, 5)?;
|
|
660
|
+
wire::number(data, 6)?;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
"range" => {
|
|
664
|
+
wire::number(data, 1)?;
|
|
665
|
+
if wire::number(data, 2)? > 1 || data.len() != 4 {
|
|
666
|
+
return Err(Error::Conflict);
|
|
667
|
+
}
|
|
668
|
+
let mut length = None;
|
|
669
|
+
for id in [3, 4] {
|
|
670
|
+
let value = wire::attrs(&wire::one(data, id)?.value)?;
|
|
671
|
+
if value.len() != 1 {
|
|
672
|
+
return Err(Error::Conflict);
|
|
673
|
+
}
|
|
674
|
+
let bytes = &wire::one(&value, 1)?.value;
|
|
675
|
+
if !(1..=16).contains(&bytes.len())
|
|
676
|
+
|| length.is_some_and(|size| size != bytes.len())
|
|
677
|
+
{
|
|
678
|
+
return Err(Error::Conflict);
|
|
679
|
+
}
|
|
680
|
+
length = Some(bytes.len());
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
_ => unreachable!(),
|
|
684
|
+
}
|
|
685
|
+
Ok(())
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
#[cfg(test)]
|
|
689
|
+
#[path = "owner_identity_tests.rs"]
|
|
690
|
+
mod identity_tests;
|
|
691
|
+
|
|
602
692
|
#[cfg(test)]
|
|
603
693
|
#[path = "owner_tests.rs"]
|
|
604
694
|
mod native_tests;
|