@push.rocks/smartnftables 1.4.0 → 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.
Files changed (41) hide show
  1. package/changelog.md +14 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/classes.managednftables.d.ts +7 -7
  8. package/dist_ts/classes.managednftables.js +1 -1
  9. package/dist_ts/index.d.ts +1 -0
  10. package/dist_ts/index.js +1 -1
  11. package/dist_ts/managed.egress.types.d.ts +91 -0
  12. package/dist_ts/managed.egress.types.js +2 -0
  13. package/dist_ts/managed.types.d.ts +20 -18
  14. package/package.json +2 -2
  15. package/readme.md +75 -4
  16. package/rust/src/egress.compile.rs +288 -0
  17. package/rust/src/egress.host.rs +103 -0
  18. package/rust/src/egress.router.rs +231 -0
  19. package/rust/src/egress.rs +682 -0
  20. package/rust/src/egress_tests.rs +332 -0
  21. package/rust/src/main.rs +3 -1
  22. package/rust/src/managed.rs +105 -0
  23. package/rust/src/owner.rs +96 -6
  24. package/rust/src/owner_coexistence_tests.rs +77 -0
  25. package/rust/src/owner_egress_identity_tests.rs +155 -0
  26. package/rust/src/owner_egress_tests.rs +153 -0
  27. package/rust/src/owner_egress_traffic_tests.rs +470 -0
  28. package/rust/src/owner_host_traffic_tests.rs +305 -0
  29. package/rust/src/owner_identity_tests.rs +169 -0
  30. package/rust/src/owner_link_tests.rs +119 -0
  31. package/rust/src/owner_packet_fixture.rs +200 -0
  32. package/rust/src/owner_tests.rs +33 -7
  33. package/rust/src/policy.rs +120 -81
  34. package/rust/src/tests.rs +22 -0
  35. package/rust/src/wire.links.rs +268 -0
  36. package/rust/src/wire.rs +3 -64
  37. package/ts/00_commitinfo_data.ts +1 -1
  38. package/ts/classes.managednftables.ts +12 -12
  39. package/ts/index.ts +1 -0
  40. package/ts/managed.egress.types.ts +85 -0
  41. package/ts/managed.types.ts +20 -16
@@ -0,0 +1,305 @@
1
+ use super::egress_traffic_tests::{connect, denied, host_policy, ns_ip, roundtrip, socket};
2
+ use super::*;
3
+ use crate::policy::{compare, expr, meta};
4
+ use std::net::UdpSocket;
5
+
6
+ const SOURCE: &str = "hs_source";
7
+ const HOST: &str = "hs_host";
8
+ const PEER: &str = "hs_peer";
9
+
10
+ fn payload(offset: u32, bytes: Vec<u8>, transport: bool) -> Vec<Attr> {
11
+ vec![
12
+ expr(
13
+ "payload",
14
+ vec![
15
+ Attr::u32(1, 1),
16
+ Attr::u32(2, if transport { 2 } else { 1 }),
17
+ Attr::u32(3, offset),
18
+ Attr::u32(4, bytes.len() as u32),
19
+ ],
20
+ ),
21
+ compare(bytes),
22
+ ]
23
+ }
24
+ fn immediate(value: Vec<u8>) -> Attr {
25
+ expr(
26
+ "immediate",
27
+ vec![
28
+ Attr::u32(1, 1),
29
+ Attr::nested(2, vec![Attr::bytes(1, value)]),
30
+ ],
31
+ )
32
+ }
33
+ fn hostile_capture_fixture() {
34
+ let mut socket = wire::Socket::open().unwrap();
35
+ let table = "snft_foreign_capture";
36
+ let mut operations = vec![(0, 0x600, vec![Attr::string(1, table), Attr::u32(2, 6)])];
37
+ for (chain, priority, kind) in [("raw", -300_i32, "filter"), ("pre", -100, "nat")] {
38
+ operations.push((
39
+ 3,
40
+ 0x600,
41
+ vec![
42
+ Attr::string(1, table),
43
+ Attr::string(3, chain),
44
+ Attr::nested(4, vec![Attr::u32(1, 0), Attr::u32(2, priority as u32)]),
45
+ Attr::u32(5, 1),
46
+ Attr::string(7, kind),
47
+ Attr::u32(10, 1),
48
+ ],
49
+ ));
50
+ }
51
+ // Foreign DNAT changes either side of the protected comparison. These
52
+ // paths have positive packet controls before the managed owner is applied.
53
+ for (port, destination, target) in [
54
+ (10001, [10, 250, 0, 2], [203, 0, 113, 2]),
55
+ (10002, [203, 0, 113, 2], [10, 250, 0, 2]),
56
+ (10004, [203, 0, 113, 2], [192, 0, 2, 2]),
57
+ ] {
58
+ let mut expressions = meta(
59
+ 4,
60
+ unsafe { libc::if_nametoindex(c"router".as_ptr()) }
61
+ .to_ne_bytes()
62
+ .to_vec(),
63
+ );
64
+ expressions.extend(meta(16, vec![17]));
65
+ expressions.extend(payload(12, vec![10, 240, 0, 2], false));
66
+ expressions.extend(payload(16, destination.to_vec(), false));
67
+ expressions.extend(payload(0, (port as u16).to_be_bytes().to_vec(), true));
68
+ expressions.push(immediate(target.to_vec()));
69
+ expressions.push(expr(
70
+ "nat",
71
+ vec![
72
+ Attr::u32(1, 1),
73
+ Attr::u32(2, 2),
74
+ Attr::u32(3, 1),
75
+ Attr::u32(4, 1),
76
+ Attr::u32(7, 1),
77
+ ],
78
+ ));
79
+ operations.push((
80
+ 6,
81
+ 0xc00,
82
+ vec![
83
+ Attr::string(1, table),
84
+ Attr::string(2, "pre"),
85
+ Attr::nested(4, expressions),
86
+ ],
87
+ ));
88
+ }
89
+ let mut expressions = meta(
90
+ 4,
91
+ unsafe { libc::if_nametoindex(c"router".as_ptr()) }
92
+ .to_ne_bytes()
93
+ .to_vec(),
94
+ );
95
+ expressions.extend(meta(16, vec![17]));
96
+ expressions.extend(payload(0, 10003_u16.to_be_bytes().to_vec(), true));
97
+ expressions.push(immediate(19_u16.to_ne_bytes().to_vec()));
98
+ expressions.push(expr("ct", vec![Attr::u32(2, 17), Attr::u32(4, 1)]));
99
+ operations.push((
100
+ 6,
101
+ 0xc00,
102
+ vec![
103
+ Attr::string(1, table),
104
+ Attr::string(2, "raw"),
105
+ Attr::nested(4, expressions),
106
+ ],
107
+ ));
108
+ let generation = socket.generation().unwrap();
109
+ socket.batch(generation, operations).unwrap();
110
+ // This is a persistent *foreign fixture* table. Drop its process socket;
111
+ // native managed-table ownership never adopts or removes this graph.
112
+ }
113
+ fn control(source: &UdpSocket, destination: &str, target: &UdpSocket, allowed: bool) {
114
+ assert_eq!(source.send_to(b"probe", destination).unwrap(), 5);
115
+ if allowed {
116
+ assert_eq!(target.recv(&mut [0; 64]).unwrap(), 5);
117
+ } else {
118
+ assert!(target.recv(&mut [0; 64]).is_err());
119
+ }
120
+ }
121
+ fn ipv6_pair(a: &str, aname: &str, aip: &str, b: &str, bname: &str, bip: &str) {
122
+ let aname_owned = aname.to_owned();
123
+ let bname_owned = bname.to_owned();
124
+ let amac = in_namespace(a, move || packet_fixture::mac(&aname_owned));
125
+ let bmac = in_namespace(b, move || packet_fixture::mac(&bname_owned));
126
+ let mac = |value: [u8; 6]| {
127
+ value
128
+ .iter()
129
+ .map(|byte| format!("{byte:02x}"))
130
+ .collect::<Vec<_>>()
131
+ .join(":")
132
+ };
133
+ for (ns, name, address, peer, peer_mac) in
134
+ [(a, aname, aip, bip, bmac), (b, bname, bip, aip, amac)]
135
+ {
136
+ ns_ip(
137
+ ns,
138
+ &[
139
+ "-6",
140
+ "address",
141
+ "add",
142
+ &format!("{address}/64"),
143
+ "dev",
144
+ name,
145
+ "nodad",
146
+ ],
147
+ );
148
+ ns_ip(
149
+ ns,
150
+ &[
151
+ "-6",
152
+ "neigh",
153
+ "replace",
154
+ peer,
155
+ "lladdr",
156
+ &mac(peer_mac),
157
+ "nud",
158
+ "permanent",
159
+ "dev",
160
+ name,
161
+ ],
162
+ );
163
+ }
164
+ }
165
+
166
+ #[test]
167
+ #[ignore = "requires disposable isolated native qualification guest"]
168
+ fn v2_host_rejects_foreign_dnat_zone_local_diversion_and_ipv6() {
169
+ isolated();
170
+ for name in [SOURCE, HOST, PEER] {
171
+ ip(&["netns", "add", name]);
172
+ ns_ip(name, &["link", "set", "lo", "up"]);
173
+ in_namespace(name, || {
174
+ std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap();
175
+ std::fs::write("/proc/sys/net/ipv6/conf/all/forwarding", "1").unwrap();
176
+ });
177
+ }
178
+ connect(
179
+ SOURCE,
180
+ "source",
181
+ "10.240.0.2/30",
182
+ HOST,
183
+ "router",
184
+ "10.240.0.1/30",
185
+ );
186
+ connect(
187
+ HOST,
188
+ "uplink",
189
+ "192.0.2.2/24",
190
+ PEER,
191
+ "underlay",
192
+ "192.0.2.3/24",
193
+ );
194
+ ns_ip(SOURCE, &["route", "add", "default", "via", "10.240.0.1"]);
195
+ ns_ip(
196
+ HOST,
197
+ &["route", "add", "203.0.113.2/32", "via", "192.0.2.3"],
198
+ );
199
+ ns_ip(
200
+ HOST,
201
+ &["route", "add", "10.250.0.2/32", "via", "192.0.2.3"],
202
+ );
203
+ for address in ["203.0.113.2/32", "10.250.0.2/32"] {
204
+ ns_ip(PEER, &["address", "add", address, "dev", "lo"]);
205
+ }
206
+ ns_ip(PEER, &["route", "add", "default", "via", "192.0.2.2"]);
207
+ ipv6_pair(
208
+ SOURCE,
209
+ "source",
210
+ "fd00:240::2",
211
+ HOST,
212
+ "router",
213
+ "fd00:240::1",
214
+ );
215
+ ipv6_pair(
216
+ HOST,
217
+ "uplink",
218
+ "2001:db8:190::108",
219
+ PEER,
220
+ "underlay",
221
+ "2001:db8:190::3",
222
+ );
223
+ ns_ip(
224
+ SOURCE,
225
+ &["-6", "route", "add", "default", "via", "fd00:240::1"],
226
+ );
227
+ ns_ip(
228
+ PEER,
229
+ &["-6", "route", "add", "default", "via", "2001:db8:190::108"],
230
+ );
231
+ in_namespace(HOST, hostile_capture_fixture);
232
+ let public = socket(PEER, "203.0.113.2:42000");
233
+ let protected = socket(PEER, "10.250.0.2:42000");
234
+ let proxy = socket(HOST, "192.0.2.2:42000");
235
+ let source_original = socket(SOURCE, "10.240.0.2:10001");
236
+ let source_current = socket(SOURCE, "10.240.0.2:10002");
237
+ let source_zone = socket(SOURCE, "10.240.0.2:10003");
238
+ let source_proxy = socket(SOURCE, "10.240.0.2:10004");
239
+ let source_v6 = socket(SOURCE, "[fd00:240::2]:10000");
240
+ let target_v6 = socket(PEER, "[2001:db8:190::3]:42000");
241
+ for (source, destination, target) in [
242
+ (&source_original, "10.250.0.2:42000", &public),
243
+ (&source_current, "203.0.113.2:42000", &protected),
244
+ (&source_zone, "203.0.113.2:42000", &public),
245
+ (&source_proxy, "203.0.113.2:42000", &proxy),
246
+ (&source_v6, "[2001:db8:190::3]:42000", &target_v6),
247
+ ] {
248
+ control(source, destination, target, true);
249
+ }
250
+ let (owner, applied) = in_namespace(HOST, || {
251
+ let mut owner = Owner::new(options("host_isolation")).unwrap();
252
+ let target: Prepared = host_policy().prepare().unwrap().into();
253
+ target
254
+ .validate_interfaces()
255
+ .expect("host isolation fixture binding");
256
+ let result = owner.reconcile(Transition {
257
+ previous: None,
258
+ target: target.clone(),
259
+ });
260
+ if result.is_err() {
261
+ let graph = owner.graph().unwrap();
262
+ if let Some(graph) = graph {
263
+ println!(
264
+ "host graph rules={} chains={} matches={:?} table={:?}",
265
+ graph.rules.len(),
266
+ graph.chains.len(),
267
+ owner.matches(&graph, &target),
268
+ graph.table
269
+ );
270
+ } else {
271
+ println!("host graph absent");
272
+ }
273
+ }
274
+ let applied = result.unwrap();
275
+ assert!(owner.inspect().enforced);
276
+ (owner, applied)
277
+ });
278
+ for (source, destination, target) in [
279
+ (&source_original, "10.250.0.2:42000", &public),
280
+ (&source_current, "203.0.113.2:42000", &protected),
281
+ (&source_zone, "203.0.113.2:42000", &public),
282
+ (&source_proxy, "203.0.113.2:42000", &proxy),
283
+ (&source_v6, "[2001:db8:190::3]:42000", &target_v6),
284
+ ] {
285
+ control(source, destination, target, false);
286
+ }
287
+ let allowed = socket(SOURCE, "10.240.0.2:10000");
288
+ assert_eq!(
289
+ roundtrip(&allowed, &public).ip().to_string(),
290
+ "192.0.2.2"
291
+ );
292
+ let platform = socket(PEER, "192.0.2.3:5300");
293
+ roundtrip(&allowed, &platform);
294
+ let unleased = socket(SOURCE, "10.240.0.2:9999");
295
+ denied(&unleased, &public, false);
296
+ let local_proxy = socket(HOST, "10.240.0.1:43000");
297
+ denied(&allowed, &local_proxy, false);
298
+ denied(&local_proxy, &allowed, true);
299
+ in_namespace(HOST, move || {
300
+ let mut owner = owner;
301
+ assert!(owner.inspect().enforced);
302
+ owner.release(applied).unwrap();
303
+ });
304
+ println!("V2_HOST_PROOF original_protected_dnat_denied=true current_protected_dnat_denied=true nonzero_zone_denied=true input_diversion_denied=true output_spoof_denied=true ipv6_forward_denied=true unleased_port_denied=true exact_platform_allowed=true positive_controls=true");
305
+ }
@@ -0,0 +1,169 @@
1
+ use super::rule_identity;
2
+ use crate::wire::{self, Attr};
3
+
4
+ fn expression(name: &str, data: Vec<Attr>) -> Attr {
5
+ Attr::nested(1, vec![Attr::string(1, name), Attr::nested(2, data)])
6
+ }
7
+ fn rule(expressions: Vec<Attr>) -> Vec<Attr> {
8
+ vec![
9
+ Attr::string(1, "snft_identity"),
10
+ Attr::string(2, "forward"),
11
+ Attr::nested(4, expressions),
12
+ Attr::bytes(7, b"exact-intent".to_vec()),
13
+ ]
14
+ }
15
+ fn range() -> Vec<Attr> {
16
+ vec![
17
+ Attr::u32(1, 1),
18
+ Attr::u32(2, 0),
19
+ Attr::nested(3, vec![Attr::bytes(1, 10000_u16.to_be_bytes().to_vec())]),
20
+ Attr::nested(4, vec![Attr::bytes(1, 10999_u16.to_be_bytes().to_vec())]),
21
+ ]
22
+ }
23
+ fn nat() -> Vec<Attr> {
24
+ vec![
25
+ Attr::u32(1, 0),
26
+ Attr::u32(2, 2),
27
+ Attr::u32(3, 1),
28
+ Attr::u32(4, 1),
29
+ Attr::u32(5, 2),
30
+ Attr::u32(6, 3),
31
+ Attr::u32(7, 3),
32
+ ]
33
+ }
34
+
35
+ #[test]
36
+ fn egress_identity_preserves_ct_direction_registers_nat_ranges_and_expression_order() {
37
+ let expressions = vec![
38
+ expression(
39
+ "ct",
40
+ vec![Attr::u32(1, 1), Attr::u32(2, 9), Attr::bytes(3, vec![0])],
41
+ ),
42
+ expression("range", range()),
43
+ expression("nat", nat()),
44
+ ];
45
+ let expected = rule_identity(&rule(expressions.clone())).unwrap();
46
+ let reordered = rule(
47
+ expressions
48
+ .iter()
49
+ .map(|item| {
50
+ let mut fields = wire::attrs(&item.value).unwrap();
51
+ let mut data = wire::attrs(&fields[1].value).unwrap();
52
+ data.reverse();
53
+ fields[1] = Attr::nested(2, data);
54
+ expression(
55
+ &wire::text(&fields, 1).unwrap(),
56
+ wire::attrs(&fields[1].value).unwrap(),
57
+ )
58
+ })
59
+ .collect(),
60
+ );
61
+ assert_eq!(rule_identity(&reordered).unwrap(), expected);
62
+ let mut order = expressions.clone();
63
+ order.reverse();
64
+ assert_ne!(rule_identity(&rule(order)).unwrap(), expected);
65
+ for (index, changed) in [
66
+ (
67
+ 0,
68
+ expression(
69
+ "ct",
70
+ vec![Attr::u32(1, 1), Attr::u32(2, 9), Attr::bytes(3, vec![1])],
71
+ ),
72
+ ),
73
+ (
74
+ 0,
75
+ expression(
76
+ "ct",
77
+ vec![Attr::u32(1, 2), Attr::u32(2, 9), Attr::bytes(3, vec![0])],
78
+ ),
79
+ ),
80
+ (
81
+ 2,
82
+ expression("nat", {
83
+ let mut value = nat();
84
+ value[5] = Attr::u32(6, 2);
85
+ value
86
+ }),
87
+ ),
88
+ (
89
+ 2,
90
+ expression("nat", {
91
+ let mut value = nat();
92
+ value[6] = Attr::u32(7, 7);
93
+ value
94
+ }),
95
+ ),
96
+ ] {
97
+ let mut value = expressions.clone();
98
+ value[index] = changed;
99
+ assert_ne!(rule_identity(&rule(value)).unwrap(), expected);
100
+ }
101
+ }
102
+
103
+ #[test]
104
+ fn range_identity_normalizes_nesting_flags_without_losing_inclusive_bounds() {
105
+ let original = rule_identity(&rule(vec![expression("range", range())])).unwrap();
106
+ let mut kernel = range();
107
+ kernel[2].kind &= 0x3fff;
108
+ kernel[3].kind &= 0x3fff;
109
+ assert_eq!(
110
+ rule_identity(&rule(vec![expression("range", kernel)])).unwrap(),
111
+ original
112
+ );
113
+ let mut changed = range();
114
+ changed[3] = Attr::nested(4, vec![Attr::bytes(1, 11000_u16.to_be_bytes().to_vec())]);
115
+ assert_ne!(
116
+ rule_identity(&rule(vec![expression("range", changed)])).unwrap(),
117
+ original
118
+ );
119
+ }
120
+
121
+ #[test]
122
+ fn egress_identity_rejects_unknown_duplicate_or_incomplete_operands() {
123
+ for (name, value) in [
124
+ (
125
+ "ct",
126
+ vec![Attr::u32(1, 1), Attr::u32(2, 17), Attr::u32(4, 1)],
127
+ ),
128
+ ("ct", vec![Attr::u32(2, 17)]),
129
+ (
130
+ "ct",
131
+ vec![Attr::u32(1, 1), Attr::u32(2, 17), Attr::bytes(3, vec![2])],
132
+ ),
133
+ ("nat", {
134
+ let mut value = nat();
135
+ value.remove(3);
136
+ value
137
+ }),
138
+ ("nat", {
139
+ let mut value = nat();
140
+ value.remove(5);
141
+ value
142
+ }),
143
+ ("range", {
144
+ let mut value = range();
145
+ value[3] = Attr::nested(4, vec![Attr::bytes(1, vec![1])]);
146
+ value
147
+ }),
148
+ ("range", {
149
+ let mut value = range();
150
+ value[2] = Attr::nested(3, vec![Attr::bytes(2, vec![1, 2])]);
151
+ value
152
+ }),
153
+ ] {
154
+ assert!(rule_identity(&rule(vec![expression(name, value)])).is_err());
155
+ }
156
+ for (name, value) in [
157
+ ("ct", vec![Attr::u32(2, 13), Attr::u32(4, 1)]),
158
+ ("nat", nat()),
159
+ ("range", range()),
160
+ ] {
161
+ assert!(rule_identity(&rule(vec![expression(name, value.clone())])).is_ok());
162
+ let mut duplicate = value.clone();
163
+ duplicate.push(value[0].clone());
164
+ assert!(rule_identity(&rule(vec![expression(name, duplicate)])).is_err());
165
+ let mut extra = value;
166
+ extra.push(Attr::u32(99, 0));
167
+ assert!(rule_identity(&rule(vec![expression(name, extra)])).is_err());
168
+ }
169
+ }
@@ -0,0 +1,119 @@
1
+ use super::*;
2
+
3
+ fn observed(name: &str) -> serde_json::Value {
4
+ let result = std::process::Command::new("/bin/ip")
5
+ .args(["-j", "-d", "link", "show", "dev", name])
6
+ .output()
7
+ .unwrap();
8
+ assert!(result.status.success());
9
+ serde_json::from_slice::<serde_json::Value>(&result.stdout).unwrap()[0].clone()
10
+ }
11
+
12
+ #[test]
13
+ #[ignore = "requires disposable isolated native qualification guest"]
14
+ fn bound_veth_and_tun_reject_changed_local_facts() {
15
+ isolated();
16
+ let endpoint = endpoint("a", 1);
17
+ let link = observed("worka");
18
+ let index = link["ifindex"].as_u64().unwrap() as u32;
19
+ assert_eq!(index, endpoint.interface_index);
20
+ let peer = link["link_index"].as_u64().unwrap() as u32;
21
+ let mac = link["address"].as_str().unwrap();
22
+ let addresses = vec!["10.81.1.1".to_owned()];
23
+ let mut binding = wire::BoundInterface {
24
+ index,
25
+ name: "worka",
26
+ kind: "veth",
27
+ mac: Some(mac),
28
+ link_index: peer,
29
+ addresses: &addresses,
30
+ };
31
+ let check = |binding| wire::verify_bound_interfaces(std::iter::once(binding));
32
+ assert!(check(binding).is_ok());
33
+ binding.mac = Some("02:00:00:00:00:01");
34
+ assert!(check(binding).is_err());
35
+ binding.mac = Some(mac);
36
+ binding.link_index = peer + 100;
37
+ assert!(check(binding).is_err());
38
+ binding.link_index = peer;
39
+ binding.kind = "ethernet";
40
+ assert!(check(binding).is_err());
41
+ binding.kind = "veth";
42
+ binding.name = "changed";
43
+ assert!(check(binding).is_err());
44
+ binding.name = "worka";
45
+ binding.index = index + 100;
46
+ assert!(check(binding).is_err());
47
+ binding.index = index;
48
+ let missing = vec!["10.81.1.2".to_owned()];
49
+ binding.addresses = &missing;
50
+ assert!(check(binding).is_err());
51
+ binding.addresses = &addresses;
52
+ ip(&["address", "del", "10.81.1.1/24", "dev", "worka"]);
53
+ assert!(check(binding).is_err());
54
+ ip(&["address", "add", "10.81.1.1/24", "dev", "worka"]);
55
+ assert!(check(binding).is_ok());
56
+ ip(&["link", "add", "brguard", "type", "bridge"]);
57
+ ip(&["link", "set", "worka", "master", "brguard"]);
58
+ assert!(check(binding).is_err());
59
+ ip(&["link", "set", "worka", "nomaster"]);
60
+ assert!(check(binding).is_ok());
61
+
62
+ ip(&["tuntap", "add", "dev", "relaya", "mode", "tun"]);
63
+ ip(&["address", "add", "10.90.0.1/24", "dev", "relaya"]);
64
+ let tun = observed("relaya");
65
+ let index = tun["ifindex"].as_u64().unwrap() as u32;
66
+ let addresses = vec!["10.90.0.1".to_owned()];
67
+ let mut binding = wire::BoundInterface {
68
+ index,
69
+ name: "relaya",
70
+ kind: "tun",
71
+ mac: None,
72
+ link_index: index,
73
+ addresses: &addresses,
74
+ };
75
+ assert!(check(binding).is_ok());
76
+ binding.mac = Some("02:00:00:00:00:01");
77
+ assert!(check(binding).is_err());
78
+ ip(&["tuntap", "add", "dev", "badlayer", "mode", "tap"]);
79
+ let tap = observed("badlayer");
80
+ binding.index = tap["ifindex"].as_u64().unwrap() as u32;
81
+ binding.link_index = binding.index;
82
+ binding.name = "badlayer";
83
+ binding.mac = None;
84
+ binding.addresses = &[];
85
+ assert!(check(binding).is_err());
86
+ }
87
+
88
+ #[test]
89
+ #[ignore = "requires disposable isolated native qualification guest"]
90
+ fn bound_virtio_ethernet_uplink_matches_driver_and_ipv4() {
91
+ // This test alone uses the initial *guest* namespace. Other tests unshare.
92
+ // The fixture has one singleton QEMU hubport and no host-facing backend.
93
+ guest_guard();
94
+ let link = observed("ens18");
95
+ assert_eq!(link["address"], "02:00:00:00:ee:01");
96
+ assert_eq!(link["ifindex"], 2);
97
+ assert!(link.get("linkinfo").is_none());
98
+ assert!(link.get("master").is_none());
99
+ let addresses = vec!["192.0.2.2".to_owned()];
100
+ let mut binding = wire::BoundInterface {
101
+ index: 2,
102
+ name: "ens18",
103
+ kind: "ethernet",
104
+ mac: Some("02:00:00:00:ee:01"),
105
+ link_index: 2,
106
+ addresses: &addresses,
107
+ };
108
+ let check = |binding| wire::verify_bound_interfaces(std::iter::once(binding));
109
+ assert!(check(binding).is_ok());
110
+ binding.kind = "veth";
111
+ assert!(check(binding).is_err());
112
+ binding.kind = "ethernet";
113
+ binding.link_index = 3;
114
+ assert!(check(binding).is_err());
115
+ binding.link_index = 2;
116
+ let wrong_addresses = vec!["198.51.100.85".to_owned()];
117
+ binding.addresses = &wrong_addresses;
118
+ assert!(check(binding).is_err());
119
+ }