@push.rocks/smartnftables 4.3.1 → 4.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 +17 -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 -4
- package/package.json +4 -4
- package/readme.md +89 -2
- package/rust/src/egress.compile.rs +105 -38
- package/rust/src/egress.hostgrant.rs +74 -34
- package/rust/src/egress.keys.rs +383 -52
- package/rust/src/egress.leased.rs +71 -42
- package/rust/src/egress.poolguard.rs +72 -0
- package/rust/src/egress.private.rs +32 -37
- package/rust/src/egress.published.rs +74 -47
- package/rust/src/egress.router.rs +1 -1
- package/rust/src/egress.rs +65 -1
- package/rust/src/egress.workloadgrant.rs +30 -28
- package/rust/src/egress_hostgrant_tests.rs +12 -0
- package/rust/src/egress_keys_tests.rs +150 -0
- package/rust/src/egress_poolguardpublished_tests.rs +418 -0
- package/rust/src/egress_tests.rs +47 -4
- package/rust/src/owner.rs +96 -28
- package/rust/src/owner_listing_tests.rs +634 -0
- package/rust/src/owner_tests.rs +3 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/managed.egress.types.ts +26 -4
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
//! Typed sets. nft must decode every set the compiler declares, and the graph
|
|
2
|
+
//! of a release before typed sets (LEGACY <=4.4.x) must stay adoptable and be
|
|
3
|
+
//! replaced by typed sets in one batch.
|
|
4
|
+
//!
|
|
5
|
+
//! The ignored cases run in the isolated qualification guest of
|
|
6
|
+
//! `test/native/qualify.py`, which carries the host's nft 1.0.9. They compare
|
|
7
|
+
//! every declared set with the set nft creates from the same `typeof`
|
|
8
|
+
//! declaration, list every set, chain and the ruleset, and pin nft's own
|
|
9
|
+
//! defect: it crashes listing a rule that looks up a concatenation of seven or
|
|
10
|
+
//! more fields, whatever the set's types (nftables `expr_postprocess_concat`).
|
|
11
|
+
use super::*;
|
|
12
|
+
use crate::egress::{self, hostgrant_tests, tests, workloadgrant_tests};
|
|
13
|
+
use serde_json::{json, Value};
|
|
14
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
15
|
+
use std::os::unix::process::ExitStatusExt;
|
|
16
|
+
|
|
17
|
+
const TABLE: &str = "snft_listing";
|
|
18
|
+
/// The longest concatenation whose lookups nft 1.0.9 lists.
|
|
19
|
+
const LISTED_FIELDS: usize = 6;
|
|
20
|
+
/// The longest key nft 1.0.9 describes by `typeof`.
|
|
21
|
+
const TYPEOF_FIELDS: usize = 4;
|
|
22
|
+
|
|
23
|
+
/// Policies that between them declare every kind of set the compiler emits.
|
|
24
|
+
fn fixtures() -> Vec<(&'static str, Value)> {
|
|
25
|
+
let host_grant = json!([hostgrant_tests::host_grant(
|
|
26
|
+
"tcp",
|
|
27
|
+
"10.240.0.1",
|
|
28
|
+
"10.241.0.2",
|
|
29
|
+
8080
|
|
30
|
+
)]);
|
|
31
|
+
let mut router = workloadgrant_tests::granted(
|
|
32
|
+
workloadgrant_tests::two_workloads(),
|
|
33
|
+
json!([workloadgrant_tests::workload_grant(
|
|
34
|
+
"tcp",
|
|
35
|
+
"10.241.0.2",
|
|
36
|
+
"10.241.1.2",
|
|
37
|
+
8080
|
|
38
|
+
)]),
|
|
39
|
+
);
|
|
40
|
+
router = hostgrant_tests::granted(router, host_grant.clone());
|
|
41
|
+
// A leased grant naming its source port adds the sourced maps.
|
|
42
|
+
router["scope"]["generations"][0]["grants"]
|
|
43
|
+
.as_array_mut()
|
|
44
|
+
.unwrap()
|
|
45
|
+
.push(
|
|
46
|
+
json!({"sourceEndpoint":"work","sourcePrefix":"10.241.0.2/32",
|
|
47
|
+
"destination":{"kind":"public","prefix":"0.0.0.0/0"},"protocol":"udp","sourcePort":5000,
|
|
48
|
+
"destinationPort":443,"sourcePortRange":{"first":10000,"last":10999}}),
|
|
49
|
+
);
|
|
50
|
+
let mut range = tests::router_published_port("udp", 20000, 20000);
|
|
51
|
+
range["transitPortEnd"] = json!(20200);
|
|
52
|
+
range["symmetric"] = json!(true);
|
|
53
|
+
router["scope"]["publishedPorts"] =
|
|
54
|
+
json!([tests::router_published_port("tcp", 443, 8443), range]);
|
|
55
|
+
let mut host = hostgrant_tests::granted(tests::host(), host_grant.clone());
|
|
56
|
+
let mut host_range = tests::published_port("udp", 20000, 20000);
|
|
57
|
+
host_range["hostPortEnd"] = json!(20200);
|
|
58
|
+
host_range["symmetric"] = json!(true);
|
|
59
|
+
host["scope"]["publishedPorts"] = json!([tests::published_port("tcp", 443, 8443), host_range]);
|
|
60
|
+
vec![
|
|
61
|
+
("router", router),
|
|
62
|
+
("combined", tests::combined_router_capacity()),
|
|
63
|
+
("host", host),
|
|
64
|
+
("pool", pool(1)),
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
/// An allocation-pool guard with a host grant and a publication: it binds no
|
|
68
|
+
/// interface, so any namespace can apply it.
|
|
69
|
+
fn pool(revision: u64) -> Value {
|
|
70
|
+
let mut value = hostgrant_tests::granted(
|
|
71
|
+
tests::pool_guard(),
|
|
72
|
+
json!([hostgrant_tests::host_grant(
|
|
73
|
+
"tcp",
|
|
74
|
+
"10.240.0.1",
|
|
75
|
+
"10.241.0.2",
|
|
76
|
+
8080
|
|
77
|
+
)]),
|
|
78
|
+
);
|
|
79
|
+
value["scope"]["publishedPorts"] =
|
|
80
|
+
json!([{"protocol":"tcp","hostPort":443,"targetPort":8443,"targetAddress":"10.240.0.1"}]);
|
|
81
|
+
value["revision"] = json!(revision);
|
|
82
|
+
value
|
|
83
|
+
}
|
|
84
|
+
fn policy(value: Value) -> egress::Prepared {
|
|
85
|
+
serde_json::from_value::<egress::Policy>(value)
|
|
86
|
+
.unwrap()
|
|
87
|
+
.prepare()
|
|
88
|
+
.unwrap()
|
|
89
|
+
}
|
|
90
|
+
fn options(table: &str) -> Options {
|
|
91
|
+
Options {
|
|
92
|
+
owner_id: "fixture".into(),
|
|
93
|
+
instance_id: "listing_qualification_20261006".into(),
|
|
94
|
+
table_name: table.into(),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/// The compiled graph as the owner reads it back, each set declaration passed
|
|
98
|
+
/// through `set`.
|
|
99
|
+
fn graph_of(program: &[(u16, Vec<Attr>)], set: impl Fn(&str, &[Attr]) -> Vec<Attr>) -> Graph {
|
|
100
|
+
let (mut chains, mut rules) = (Vec::new(), Vec::new());
|
|
101
|
+
let mut sets: Vec<(Vec<Attr>, Vec<Vec<u8>>)> = Vec::new();
|
|
102
|
+
for (kind, attributes) in program {
|
|
103
|
+
match kind {
|
|
104
|
+
3 => chains.push(attributes.clone()),
|
|
105
|
+
6 => rules.push(attributes.clone()),
|
|
106
|
+
9 => sets.push((
|
|
107
|
+
set(&wire::text(attributes, 2).unwrap(), attributes),
|
|
108
|
+
Vec::new(),
|
|
109
|
+
)),
|
|
110
|
+
12 => {
|
|
111
|
+
let name = wire::text(attributes, 2).unwrap();
|
|
112
|
+
let list = wire::attrs(&wire::one(attributes, 3).unwrap().value).unwrap();
|
|
113
|
+
sets.iter_mut()
|
|
114
|
+
.find(|(set, _)| wire::text(set, 2).unwrap() == name)
|
|
115
|
+
.unwrap()
|
|
116
|
+
.1
|
|
117
|
+
.extend(element_keys(&list).unwrap());
|
|
118
|
+
}
|
|
119
|
+
other => panic!("operation {other}"),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
Graph {
|
|
123
|
+
generation: 1,
|
|
124
|
+
table: Vec::new(),
|
|
125
|
+
chains,
|
|
126
|
+
rules,
|
|
127
|
+
sets,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
fn retyped(attributes: &[Attr], key_type: u32) -> Vec<Attr> {
|
|
131
|
+
attributes
|
|
132
|
+
.iter()
|
|
133
|
+
.map(|attribute| match attribute.id() {
|
|
134
|
+
4 => Attr::u32(4, key_type),
|
|
135
|
+
_ => attribute.clone(),
|
|
136
|
+
})
|
|
137
|
+
.collect()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[test]
|
|
141
|
+
fn the_fixtures_declare_every_kind_of_set() {
|
|
142
|
+
let mut declared = BTreeSet::new();
|
|
143
|
+
for (_, value) in fixtures() {
|
|
144
|
+
declared.extend(policy(value).declared(TABLE).unwrap().into_keys());
|
|
145
|
+
}
|
|
146
|
+
for name in [
|
|
147
|
+
"host_grant",
|
|
148
|
+
"host_grant_arrival",
|
|
149
|
+
"workload_grant",
|
|
150
|
+
"workload_link",
|
|
151
|
+
"private_link",
|
|
152
|
+
"private_index",
|
|
153
|
+
"private_name",
|
|
154
|
+
"private_source",
|
|
155
|
+
"private_any",
|
|
156
|
+
"private_port",
|
|
157
|
+
"leased_label",
|
|
158
|
+
"leased_generation",
|
|
159
|
+
"leased_platform_zone",
|
|
160
|
+
"leased_platform_flow",
|
|
161
|
+
"leased_platform_return",
|
|
162
|
+
"leased_public_zone",
|
|
163
|
+
"leased_public_flow",
|
|
164
|
+
"leased_public_sourced_zone",
|
|
165
|
+
"leased_public_sourced_flow",
|
|
166
|
+
"leased_public_return",
|
|
167
|
+
"published_arrival",
|
|
168
|
+
"published_endpoint",
|
|
169
|
+
"published_port",
|
|
170
|
+
"published_address",
|
|
171
|
+
"published_forward",
|
|
172
|
+
"published_symmetric",
|
|
173
|
+
"pool_published",
|
|
174
|
+
] {
|
|
175
|
+
assert!(declared.contains(name), "{name} in {declared:?}");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// A typed graph agrees exactly. LEGACY(<=4.4.x): the same graph with every set
|
|
180
|
+
/// untyped agrees as untyped; anything between or beside them is different.
|
|
181
|
+
#[test]
|
|
182
|
+
fn typed_graphs_agree_exactly_and_legacy_untyped_graphs_as_untyped() {
|
|
183
|
+
let owner = Owner::new(options(TABLE)).unwrap();
|
|
184
|
+
for (name, value) in fixtures() {
|
|
185
|
+
let prepared: Prepared = policy(value).into();
|
|
186
|
+
let program = prepared.program(TABLE).unwrap();
|
|
187
|
+
let first = program
|
|
188
|
+
.iter()
|
|
189
|
+
.find(|(kind, _)| *kind == 9)
|
|
190
|
+
.map(|(_, attributes)| wire::text(attributes, 2).unwrap())
|
|
191
|
+
.unwrap();
|
|
192
|
+
let typed = graph_of(&program, |_, set| set.to_vec());
|
|
193
|
+
assert_eq!(
|
|
194
|
+
owner.agreement(&typed, &prepared).unwrap(),
|
|
195
|
+
Agreement::Exact,
|
|
196
|
+
"{name}"
|
|
197
|
+
);
|
|
198
|
+
let untyped = graph_of(&program, |_, set| untyped_set(set));
|
|
199
|
+
assert_eq!(
|
|
200
|
+
owner.agreement(&untyped, &prepared).unwrap(),
|
|
201
|
+
Agreement::Untyped,
|
|
202
|
+
"{name}"
|
|
203
|
+
);
|
|
204
|
+
assert!(owner.matches(&untyped, &prepared).unwrap());
|
|
205
|
+
// A graph is typed or untyped as a whole.
|
|
206
|
+
let mixed = graph_of(&program, |set, attributes| {
|
|
207
|
+
if set == first {
|
|
208
|
+
untyped_set(attributes)
|
|
209
|
+
} else {
|
|
210
|
+
attributes.to_vec()
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
assert_eq!(
|
|
214
|
+
owner.agreement(&mixed, &prepared).unwrap(),
|
|
215
|
+
Agreement::Different,
|
|
216
|
+
"{name}"
|
|
217
|
+
);
|
|
218
|
+
// Untyped is exactly the legacy declaration: any other type differs,
|
|
219
|
+
// with or without userdata.
|
|
220
|
+
for keep_userdata in [false, true] {
|
|
221
|
+
let other = graph_of(&program, |set, attributes| {
|
|
222
|
+
let declared = if keep_userdata {
|
|
223
|
+
attributes.to_vec()
|
|
224
|
+
} else {
|
|
225
|
+
untyped_set(attributes)
|
|
226
|
+
};
|
|
227
|
+
if set == first {
|
|
228
|
+
retyped(&declared, 7)
|
|
229
|
+
} else {
|
|
230
|
+
declared
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
assert_eq!(
|
|
234
|
+
owner.agreement(&other, &prepared).unwrap(),
|
|
235
|
+
Agreement::Different,
|
|
236
|
+
"{name}"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
// Elements stay identity in a legacy graph.
|
|
240
|
+
let mut changed = graph_of(&program, |_, set| untyped_set(set));
|
|
241
|
+
let (_, keys) = changed
|
|
242
|
+
.sets
|
|
243
|
+
.iter_mut()
|
|
244
|
+
.find(|(_, keys)| !keys.is_empty())
|
|
245
|
+
.unwrap();
|
|
246
|
+
keys.pop();
|
|
247
|
+
assert_eq!(
|
|
248
|
+
owner.agreement(&changed, &prepared).unwrap(),
|
|
249
|
+
Agreement::Different,
|
|
250
|
+
"{name}"
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// Userdata is identity: a set whose types or `typeof` descriptions changed
|
|
256
|
+
/// is a different set, and a legacy untyped set differs from its typed one.
|
|
257
|
+
#[test]
|
|
258
|
+
fn set_identity_includes_types_and_userdata() {
|
|
259
|
+
let prepared: Prepared = policy(pool(1)).into();
|
|
260
|
+
for (kind, attributes) in prepared.program(TABLE).unwrap() {
|
|
261
|
+
if kind != 9 {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
let identity = set_identity(&attributes).unwrap();
|
|
265
|
+
assert_ne!(set_identity(&untyped_set(&attributes)).unwrap(), identity);
|
|
266
|
+
let mut userdata = wire::one(&attributes, 13).unwrap().value.clone();
|
|
267
|
+
let last = userdata.len() - 1;
|
|
268
|
+
userdata[last] ^= 1;
|
|
269
|
+
let changed: Vec<Attr> = attributes
|
|
270
|
+
.iter()
|
|
271
|
+
.map(|attribute| match attribute.id() {
|
|
272
|
+
13 => Attr::bytes(13, userdata.clone()),
|
|
273
|
+
_ => attribute.clone(),
|
|
274
|
+
})
|
|
275
|
+
.collect();
|
|
276
|
+
assert_ne!(set_identity(&changed).unwrap(), identity);
|
|
277
|
+
assert_ne!(set_identity(&retyped(&attributes, 7)).unwrap(), identity);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// A fresh guest network namespace and the guest's nft, the host's 1.0.9.
|
|
282
|
+
fn listing_guest() {
|
|
283
|
+
isolated();
|
|
284
|
+
let version = nft(&["--version"]);
|
|
285
|
+
assert!(version.status.success());
|
|
286
|
+
assert!(String::from_utf8_lossy(&version.stdout).starts_with("nftables v1.0.9 "));
|
|
287
|
+
}
|
|
288
|
+
fn nft(arguments: &[&str]) -> std::process::Output {
|
|
289
|
+
std::process::Command::new("/bin/nft")
|
|
290
|
+
.args(arguments)
|
|
291
|
+
.output()
|
|
292
|
+
.unwrap()
|
|
293
|
+
}
|
|
294
|
+
/// Applies a compiled program into a plain table of its own.
|
|
295
|
+
fn apply(program: Vec<(u16, Vec<Attr>)>) {
|
|
296
|
+
let mut socket = Socket::open().unwrap();
|
|
297
|
+
let generation = socket.generation().unwrap();
|
|
298
|
+
let mut operations = vec![(0, 0x600, vec![Attr::string(1, TABLE)])];
|
|
299
|
+
for (operation, attributes) in program {
|
|
300
|
+
operations.push((
|
|
301
|
+
operation,
|
|
302
|
+
if operation == 6 { 0xc00 } else { 0x600 },
|
|
303
|
+
attributes,
|
|
304
|
+
));
|
|
305
|
+
}
|
|
306
|
+
socket.batch(generation, operations).unwrap();
|
|
307
|
+
}
|
|
308
|
+
/// Every set of `table` as Linux dumps it, by name.
|
|
309
|
+
fn dumped(table: &str) -> BTreeMap<String, Vec<Attr>> {
|
|
310
|
+
let mut socket = Socket::open().unwrap();
|
|
311
|
+
socket
|
|
312
|
+
.query(10, vec![Attr::string(1, table)], true)
|
|
313
|
+
.unwrap()
|
|
314
|
+
.into_iter()
|
|
315
|
+
.map(|set| (wire::text(&set, 2).unwrap(), set))
|
|
316
|
+
.collect()
|
|
317
|
+
}
|
|
318
|
+
/// Userdata attributes without one type.
|
|
319
|
+
fn without(userdata: &[u8], kind: u8) -> Vec<u8> {
|
|
320
|
+
let mut result = Vec::new();
|
|
321
|
+
let mut rest = userdata;
|
|
322
|
+
while let [entry, length, ..] = rest {
|
|
323
|
+
let end = 2 + usize::from(*length);
|
|
324
|
+
if *entry != kind {
|
|
325
|
+
result.extend(&rest[..end]);
|
|
326
|
+
}
|
|
327
|
+
rest = &rest[end..];
|
|
328
|
+
}
|
|
329
|
+
result
|
|
330
|
+
}
|
|
331
|
+
/// The declaration lines of an nft listing: its `type`/`typeof` and flags.
|
|
332
|
+
fn header(output: &std::process::Output) -> Vec<String> {
|
|
333
|
+
String::from_utf8_lossy(&output.stdout)
|
|
334
|
+
.lines()
|
|
335
|
+
.map(str::trim)
|
|
336
|
+
.filter(|line| line.starts_with("type") || line.starts_with("flags"))
|
|
337
|
+
.map(String::from)
|
|
338
|
+
.collect()
|
|
339
|
+
}
|
|
340
|
+
/// The sets each rule of a chain looks up, by chain, and whether the lookup is a
|
|
341
|
+
/// membership test (no data register), the expression nft crashes listing for a
|
|
342
|
+
/// long concatenation.
|
|
343
|
+
fn lookups(program: &[(u16, Vec<Attr>)]) -> BTreeMap<String, BTreeSet<(String, bool)>> {
|
|
344
|
+
let mut result: BTreeMap<String, BTreeSet<(String, bool)>> = BTreeMap::new();
|
|
345
|
+
for (kind, attributes) in program {
|
|
346
|
+
match kind {
|
|
347
|
+
3 => {
|
|
348
|
+
result
|
|
349
|
+
.entry(wire::text(attributes, 3).unwrap())
|
|
350
|
+
.or_default();
|
|
351
|
+
}
|
|
352
|
+
6 => {
|
|
353
|
+
let chain = wire::text(attributes, 2).unwrap();
|
|
354
|
+
for expression in wire::attrs(&wire::one(attributes, 4).unwrap().value).unwrap() {
|
|
355
|
+
let parts = wire::attrs(&expression.value).unwrap();
|
|
356
|
+
if wire::text(&parts, 1).unwrap() == "lookup" {
|
|
357
|
+
let data = wire::attrs(&wire::one(&parts, 2).unwrap().value).unwrap();
|
|
358
|
+
let membership = data.iter().all(|attribute| attribute.id() != 3);
|
|
359
|
+
result
|
|
360
|
+
.entry(chain.clone())
|
|
361
|
+
.or_default()
|
|
362
|
+
.insert((wire::text(&data, 1).unwrap(), membership));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
_ => {}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
result
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#[test]
|
|
373
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
374
|
+
fn nft_decodes_every_typed_set_and_lists_lookups_up_to_six_fields() {
|
|
375
|
+
listing_guest();
|
|
376
|
+
for (fixture, value) in fixtures() {
|
|
377
|
+
let prepared = policy(value);
|
|
378
|
+
let declared = prepared.declared(TABLE).unwrap();
|
|
379
|
+
let program = prepared.program(TABLE).unwrap();
|
|
380
|
+
apply(program.clone());
|
|
381
|
+
assert!(nft(&["add", "table", "inet", "reference"]).status.success());
|
|
382
|
+
for (name, set) in &declared {
|
|
383
|
+
let mut specification = format!("{{ typeof {}", set.key.join(" . "));
|
|
384
|
+
if let Some(data) = &set.data {
|
|
385
|
+
specification.push_str(&format!(" : {}", data.join(" . ")));
|
|
386
|
+
}
|
|
387
|
+
specification.push_str(if set.interval {
|
|
388
|
+
"; flags constant,interval; }"
|
|
389
|
+
} else {
|
|
390
|
+
"; flags constant; }"
|
|
391
|
+
});
|
|
392
|
+
let kind = if set.data.is_some() { "map" } else { "set" };
|
|
393
|
+
let created = nft(&["add", kind, "inet", "reference", name, &specification]);
|
|
394
|
+
assert!(created.status.success(), "{fixture} {name}: {created:?}");
|
|
395
|
+
}
|
|
396
|
+
// Every declaration is nft's own for the same `typeof`.
|
|
397
|
+
let (ours, theirs) = (dumped(TABLE), dumped("reference"));
|
|
398
|
+
assert_eq!(
|
|
399
|
+
ours.keys().collect::<Vec<_>>(),
|
|
400
|
+
theirs.keys().collect::<Vec<_>>()
|
|
401
|
+
);
|
|
402
|
+
for (name, set) in &declared {
|
|
403
|
+
let (ours, theirs) = (&ours[name], &theirs[name]);
|
|
404
|
+
for id in [3, 4, 6, 7, 9] {
|
|
405
|
+
assert_eq!(
|
|
406
|
+
ours.iter().find(|attribute| attribute.id() == id),
|
|
407
|
+
theirs.iter().find(|attribute| attribute.id() == id),
|
|
408
|
+
"{fixture} {name} attribute {id}"
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
// The generation map keeps the zone padded to its register; nft
|
|
412
|
+
// declares the zone alone by its two bytes.
|
|
413
|
+
let padded_zone = set.key == ["ct zone"];
|
|
414
|
+
assert_eq!(
|
|
415
|
+
(wire::number(ours, 5).unwrap(), padded_zone),
|
|
416
|
+
(
|
|
417
|
+
wire::number(theirs, 5).unwrap() + if padded_zone { 2 } else { 0 },
|
|
418
|
+
padded_zone
|
|
419
|
+
),
|
|
420
|
+
"{fixture} {name} key length"
|
|
421
|
+
);
|
|
422
|
+
let mut expected = wire::one(theirs, 13).unwrap().value.clone();
|
|
423
|
+
if set.key.len() > TYPEOF_FIELDS {
|
|
424
|
+
// nft writes the first four fields of a longer `typeof` and
|
|
425
|
+
// discards them on reading; the declaration carries none.
|
|
426
|
+
expected = without(&expected, 3);
|
|
427
|
+
}
|
|
428
|
+
assert_eq!(
|
|
429
|
+
wire::one(ours, 13).unwrap().value,
|
|
430
|
+
expected,
|
|
431
|
+
"{fixture} {name} userdata"
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
// Every set lists, declared as nft declares the same set.
|
|
435
|
+
let (mut described, mut typed) = (0, 0);
|
|
436
|
+
for (name, set) in &declared {
|
|
437
|
+
let kind = if set.data.is_some() { "map" } else { "set" };
|
|
438
|
+
let ours = nft(&["list", kind, "inet", TABLE, name]);
|
|
439
|
+
assert!(ours.status.success(), "{fixture} {name}: {ours:?}");
|
|
440
|
+
let theirs = nft(&["list", kind, "inet", "reference", name]);
|
|
441
|
+
assert!(theirs.status.success(), "{fixture} {name}: {theirs:?}");
|
|
442
|
+
let declaration = header(&ours);
|
|
443
|
+
if set.key == ["ct zone"] {
|
|
444
|
+
// nft cannot name a zone padded to its register: it lists the
|
|
445
|
+
// key's type as a bare integer of no size.
|
|
446
|
+
assert_eq!(declaration, ["type 0 : ct_label", "flags constant"]);
|
|
447
|
+
typed += 1;
|
|
448
|
+
} else {
|
|
449
|
+
assert_eq!(declaration, header(&theirs), "{fixture} {name}");
|
|
450
|
+
if set.key.len() <= TYPEOF_FIELDS {
|
|
451
|
+
assert!(
|
|
452
|
+
declaration[0].starts_with("typeof "),
|
|
453
|
+
"{fixture} {name}: {declaration:?}"
|
|
454
|
+
);
|
|
455
|
+
described += 1;
|
|
456
|
+
} else {
|
|
457
|
+
assert!(
|
|
458
|
+
declaration[0].starts_with("type "),
|
|
459
|
+
"{fixture} {name}: {declaration:?}"
|
|
460
|
+
);
|
|
461
|
+
typed += 1;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Chains list unless a rule tests membership of a concatenation of
|
|
466
|
+
// seven or more fields: nft 1.0.9 then crashes, for its own sets as
|
|
467
|
+
// for these. Map lookups of the same keys list.
|
|
468
|
+
let (mut listed, mut crashed) = (0, 0);
|
|
469
|
+
for (chain, sets) in lookups(&program) {
|
|
470
|
+
let longest = sets
|
|
471
|
+
.iter()
|
|
472
|
+
.filter(|(_, membership)| *membership)
|
|
473
|
+
.map(|(set, _)| declared[set].key.len())
|
|
474
|
+
.max()
|
|
475
|
+
.unwrap_or(0);
|
|
476
|
+
let listing = nft(&["list", "chain", "inet", TABLE, &chain]);
|
|
477
|
+
if longest <= LISTED_FIELDS {
|
|
478
|
+
assert!(listing.status.success(), "{fixture} {chain}: {listing:?}");
|
|
479
|
+
let text = String::from_utf8_lossy(&listing.stdout);
|
|
480
|
+
// A map lookup of a longer key lists, but nft 1.0.9 renders it
|
|
481
|
+
// wrongly: its data garbled, or the statement dropped.
|
|
482
|
+
for (set, membership) in &sets {
|
|
483
|
+
if *membership || declared[set].key.len() <= TYPEOF_FIELDS {
|
|
484
|
+
assert!(
|
|
485
|
+
text.contains(&format!("@{set}")),
|
|
486
|
+
"{fixture} {chain} @{set}: {text}"
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
listed += 1;
|
|
491
|
+
} else {
|
|
492
|
+
assert_eq!(
|
|
493
|
+
listing.status.signal(),
|
|
494
|
+
Some(libc::SIGSEGV),
|
|
495
|
+
"{fixture} {chain}: {listing:?}"
|
|
496
|
+
);
|
|
497
|
+
crashed += 1;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
let ruleset = nft(&["list", "ruleset"]);
|
|
501
|
+
if crashed == 0 {
|
|
502
|
+
assert!(ruleset.status.success(), "{fixture}: {ruleset:?}");
|
|
503
|
+
} else {
|
|
504
|
+
assert_eq!(
|
|
505
|
+
ruleset.status.signal(),
|
|
506
|
+
Some(libc::SIGSEGV),
|
|
507
|
+
"{fixture}: {ruleset:?}"
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
println!(
|
|
511
|
+
"SMARTNFTABLES_LISTING fixture={fixture} sets={} typeof={described} type={typed} \
|
|
512
|
+
chains_listed={listed} chains_nft_crash={crashed} ruleset={}",
|
|
513
|
+
declared.len(),
|
|
514
|
+
if crashed == 0 { "listed" } else { "nft_crash" }
|
|
515
|
+
);
|
|
516
|
+
for table in [TABLE, "reference"] {
|
|
517
|
+
assert!(nft(&["delete", "table", "inet", table]).status.success());
|
|
518
|
+
}
|
|
519
|
+
// The control: the same graph with the untyped sets of 4.4.x does not
|
|
520
|
+
// list. nft aborts on the first rule that tests membership of one
|
|
521
|
+
// (`netlink_delinearize.c`: `dtype != &invalid_type`).
|
|
522
|
+
apply(
|
|
523
|
+
program
|
|
524
|
+
.iter()
|
|
525
|
+
.map(|(kind, attributes)| match kind {
|
|
526
|
+
9 => (*kind, untyped_set(attributes)),
|
|
527
|
+
_ => (*kind, attributes.clone()),
|
|
528
|
+
})
|
|
529
|
+
.collect(),
|
|
530
|
+
);
|
|
531
|
+
let mut aborted = 0;
|
|
532
|
+
for (chain, sets) in lookups(&program) {
|
|
533
|
+
if sets.iter().any(|(_, membership)| *membership) {
|
|
534
|
+
let listing = nft(&["list", "chain", "inet", TABLE, &chain]);
|
|
535
|
+
assert!(
|
|
536
|
+
listing.status.signal().is_some(),
|
|
537
|
+
"{fixture} untyped {chain}: {listing:?}"
|
|
538
|
+
);
|
|
539
|
+
aborted += usize::from(listing.status.signal() == Some(libc::SIGABRT));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
assert!(aborted > 0, "{fixture}");
|
|
543
|
+
assert!(nft(&["delete", "table", "inet", TABLE]).status.success());
|
|
544
|
+
println!("SMARTNFTABLES_LISTING_UNTYPED fixture={fixture} chains_nft_abort={aborted}");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/// LEGACY(<=4.4.x): applies `prepared` as a release before typed sets did, as
|
|
549
|
+
/// owner of `table`, and leaves the table ownerless as a stopped engine does.
|
|
550
|
+
fn apply_legacy(table: &str, prepared: &Prepared, receipt: &Prepared) -> Applied {
|
|
551
|
+
let mut owner = Owner::new(options(table)).unwrap();
|
|
552
|
+
let mut operations = vec![(
|
|
553
|
+
0,
|
|
554
|
+
0x600,
|
|
555
|
+
vec![
|
|
556
|
+
Attr::string(1, table),
|
|
557
|
+
Attr::u32(2, 6),
|
|
558
|
+
Attr::bytes(6, owner.userdata()),
|
|
559
|
+
],
|
|
560
|
+
)];
|
|
561
|
+
for (operation, attributes) in prepared.program(table).unwrap() {
|
|
562
|
+
let attributes = if operation == 9 {
|
|
563
|
+
untyped_set(&attributes)
|
|
564
|
+
} else {
|
|
565
|
+
attributes
|
|
566
|
+
};
|
|
567
|
+
operations.push((
|
|
568
|
+
operation,
|
|
569
|
+
if operation == 6 { 0xc00 } else { 0x600 },
|
|
570
|
+
attributes,
|
|
571
|
+
));
|
|
572
|
+
}
|
|
573
|
+
let socket = owner.socket().unwrap();
|
|
574
|
+
let generation = socket.generation().unwrap();
|
|
575
|
+
socket.batch(generation, operations).unwrap();
|
|
576
|
+
let graph = owner.graph().unwrap().unwrap();
|
|
577
|
+
assert_eq!(
|
|
578
|
+
owner.agreement(&graph, prepared).unwrap(),
|
|
579
|
+
Agreement::Untyped
|
|
580
|
+
);
|
|
581
|
+
owner.applied(&graph, receipt.clone()).unwrap()
|
|
582
|
+
}
|
|
583
|
+
fn generation() -> u32 {
|
|
584
|
+
Socket::open().unwrap().generation().unwrap()
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#[test]
|
|
588
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
589
|
+
fn legacy_untyped_graphs_are_adopted_and_replaced_by_typed_sets_in_one_batch() {
|
|
590
|
+
listing_guest();
|
|
591
|
+
let first: Prepared = policy(pool(1)).into();
|
|
592
|
+
let second: Prepared = policy(pool(2)).into();
|
|
593
|
+
|
|
594
|
+
// A new generation after the upgrade: adopt the previous untyped graph,
|
|
595
|
+
// replace it with the typed target in one batch after the ownership claim.
|
|
596
|
+
let previous = apply_legacy("snft_legacy_next", &first, &first);
|
|
597
|
+
let mut owner = Owner::new(options("snft_legacy_next")).unwrap();
|
|
598
|
+
let before = generation();
|
|
599
|
+
let applied = owner
|
|
600
|
+
.reconcile(Transition {
|
|
601
|
+
previous: Some(previous.clone()),
|
|
602
|
+
target: second.clone(),
|
|
603
|
+
})
|
|
604
|
+
.unwrap();
|
|
605
|
+
assert_eq!(generation(), before + 2);
|
|
606
|
+
assert_eq!(applied.receipt.table_handle, previous.receipt.table_handle);
|
|
607
|
+
let graph = owner.graph().unwrap().unwrap();
|
|
608
|
+
assert_eq!(owner.agreement(&graph, &second).unwrap(), Agreement::Exact);
|
|
609
|
+
assert!(owner.inspect().enforced);
|
|
610
|
+
owner.release(applied).unwrap();
|
|
611
|
+
|
|
612
|
+
// The same transition again after the upgrade, its target applied untyped:
|
|
613
|
+
// never already applied, replaced like a previous graph.
|
|
614
|
+
let previous = apply_legacy("snft_legacy_same", &second, &first);
|
|
615
|
+
let mut owner = Owner::new(options("snft_legacy_same")).unwrap();
|
|
616
|
+
let before = generation();
|
|
617
|
+
let applied = owner
|
|
618
|
+
.reconcile(Transition {
|
|
619
|
+
previous: Some(previous),
|
|
620
|
+
target: second.clone(),
|
|
621
|
+
})
|
|
622
|
+
.unwrap();
|
|
623
|
+
assert_eq!(generation(), before + 2);
|
|
624
|
+
let graph = owner.graph().unwrap().unwrap();
|
|
625
|
+
assert_eq!(owner.agreement(&graph, &second).unwrap(), Agreement::Exact);
|
|
626
|
+
owner.release(applied).unwrap();
|
|
627
|
+
|
|
628
|
+
// An untyped graph verifies externally and releases as itself.
|
|
629
|
+
let previous = apply_legacy("snft_legacy_release", &first, &first);
|
|
630
|
+
Owner::verify_external(&previous).unwrap();
|
|
631
|
+
let mut owner = Owner::new(options("snft_legacy_release")).unwrap();
|
|
632
|
+
owner.release(previous).unwrap();
|
|
633
|
+
assert!(owner.graph().unwrap().is_none());
|
|
634
|
+
}
|
package/rust/src/owner_tests.rs
CHANGED
|
@@ -57,6 +57,9 @@ mod transitionrelease_tests;
|
|
|
57
57
|
#[path = "owner_coexistence_tests.rs"]
|
|
58
58
|
mod coexistence_tests;
|
|
59
59
|
|
|
60
|
+
#[path = "owner_listing_tests.rs"]
|
|
61
|
+
mod listing_tests;
|
|
62
|
+
|
|
60
63
|
fn guest_guard() {
|
|
61
64
|
assert_eq!(
|
|
62
65
|
std::env::var("SMARTNFTABLES_ISOLATED_NATIVE").as_deref(),
|
package/ts/00_commitinfo_data.ts
CHANGED