1use crate::args::chardev::CharDev;
2use crate::common::OnOff;
3use crate::parsers::ARG_NETDEV;
4use crate::parsers::DELIM_COMMA;
5use crate::to_command::ToArg;
6use crate::to_command::ToCommand;
7use crate::{QIpv4Net, QIpv6Net};
8use bon::Builder;
9use proptest_derive::Arbitrary;
10use std::net::{Ipv4Addr, Ipv6Addr};
11use std::path::PathBuf;
12use std::str::FromStr;
13
14#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
21pub struct SMB {
22 dir: PathBuf,
23 smbserver: Option<String>,
24}
25
26#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Arbitrary)]
27pub enum TcpUdp {
28 #[default]
29 Tcp,
30 Udp,
31 Unix,
32}
33
34#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
35pub enum ScriptOrNot {
36 Script(PathBuf),
37 None,
38}
39
40impl ToCommand for ScriptOrNot {
41 fn to_args(&self) -> Vec<String> {
42 match self {
43 ScriptOrNot::Script(path) => {
44 vec![path.display().to_string()]
45 }
46 ScriptOrNot::None => {
47 vec!["no".to_string()]
48 }
49 }
50 }
51}
52
53impl FromStr for ScriptOrNot {
54 type Err = String;
55
56 fn from_str(s: &str) -> Result<Self, Self::Err> {
57 if s == "no" { Ok(Self::None) } else { Ok(Self::Script(PathBuf::from(s))) }
58 }
59}
60
61#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
62pub struct HostForward {
63 protocol: Option<TcpUdp>,
64 hostaddr: Option<String>,
65 hostport: Option<u16>,
66 hostpath: Option<String>,
67 guestaddr: Option<String>,
68 guestport: u16,
69}
70
71#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
72pub enum GuestForwardTarget {
73 Device(CharDev),
74 Cmd((String, Vec<String>)),
75}
76#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
77pub struct GuestForward {
78 server: String,
79 port: u16,
80 target: GuestForwardTarget,
81}
82#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
83pub struct User {
84 id: String,
85 ipv4: Option<OnOff>,
86 net: Option<QIpv4Net>,
87 host: Option<Ipv4Addr>,
88 ipv6: Option<OnOff>,
89 ipv6_net: Option<QIpv6Net>,
90 ipv6_host: Option<Ipv6Addr>,
91 restrict: Option<OnOff>,
92 hostname: Option<String>,
93 dhcpstart: Option<Ipv4Addr>,
94 dns: Option<Ipv4Addr>,
95 ipv6_dns: Option<Ipv6Addr>,
96 dnssearch: Option<Vec<String>>,
97 domainname: Option<String>,
98 tftp: Option<PathBuf>,
99 tftp_server_name: Option<String>,
100 bootfile: Option<PathBuf>,
101 smb: Option<SMB>,
102 hostfwd: Option<Vec<HostForward>>,
103 guestfwd: Option<Vec<GuestForward>>,
104}
105
106impl ToCommand for User {
107 fn to_args(&self) -> Vec<String> {
108 let mut args = vec!["user".to_string(), format!("id={}", self.id.to_string())];
109
110 if let Some(ipv4) = &self.ipv4 {
111 args.push(format!("ipv4={}", ipv4.to_arg()));
112 }
113 if let Some(net) = &self.net {
114 args.push(format!("net={}", net.ip));
115 }
116 if let Some(host) = &self.host {
117 args.push(format!("host={}", host));
118 }
119 if let Some(ipv6) = &self.ipv6 {
120 args.push(format!("ipv6={}", ipv6.to_arg()));
121 }
122 if let Some(ipv6_net) = &self.ipv6_net {
123 args.push(format!("ipv6-net={}", ipv6_net.ip));
124 }
125 if let Some(ipv6_host) = &self.ipv6_host {
126 args.push(format!("ipv6-host={}", ipv6_host));
127 }
128 if let Some(restrict) = &self.restrict {
129 args.push(format!("restrict={}", restrict.to_arg()));
130 }
131 if let Some(hostname) = &self.hostname {
132 args.push(format!("hostname={}", hostname));
133 }
134 if let Some(dhcpstart) = &self.dhcpstart {
135 args.push(format!("dhcpstart={}", dhcpstart));
136 }
137 if let Some(dns) = &self.dns {
138 args.push(format!("dns={}", dns));
139 }
140 if let Some(ipv6_dns) = &self.ipv6_dns {
141 args.push(format!("ipv6-dns={}", ipv6_dns));
142 }
143 if let Some(dnssearch) = &self.dnssearch {
144 args.push(format!("dnssearch={}", dnssearch.join(",")));
145 }
146 if let Some(domainname) = &self.domainname {
147 args.push(format!("domainname={}", domainname));
148 }
149 if let Some(tftp) = &self.tftp {
150 args.push(format!("tftp={}", tftp.display()));
151 }
152 if let Some(tftp_server_name) = &self.tftp_server_name {
153 args.push(format!("tftp-server-name={}", tftp_server_name));
154 }
155 if let Some(bootfile) = &self.bootfile {
156 args.push(format!("bootfile={}", bootfile.display()));
157 }
158 if let Some(smb) = &self.smb {
159 args.push(format!("smb={}", smb.dir.display()));
160 if let Some(smbserver) = &smb.smbserver {
161 args.push(format!("smbserver={}", smbserver));
162 }
163 }
164 if let Some(hostfwds) = &self.hostfwd {
165 for hostfwd in hostfwds {
166 let mut value = String::new();
167 match &hostfwd.protocol {
168 Some(TcpUdp::Tcp) => value.push_str("tcp:"),
169 Some(TcpUdp::Udp) => value.push_str("udp:"),
170 Some(TcpUdp::Unix) => value.push_str("unix:"),
171 None => {}
172 }
173
174 if matches!(hostfwd.protocol, Some(TcpUdp::Unix)) {
175 if let Some(hostpath) = &hostfwd.hostpath {
176 value.push_str(hostpath);
177 }
178 value.push('-');
179 if let Some(guestaddr) = &hostfwd.guestaddr {
180 value.push_str(guestaddr);
181 }
182 value.push(':');
183 value.push_str(&hostfwd.guestport.to_string());
184 } else {
185 if let Some(hostaddr) = &hostfwd.hostaddr {
186 value.push_str(hostaddr);
187 }
188 if hostfwd.hostaddr.is_some() || hostfwd.hostport.is_some() {
189 value.push(':');
190 }
191 if let Some(hostport) = hostfwd.hostport {
192 value.push_str(&hostport.to_string());
193 }
194 value.push('-');
195 if let Some(guestaddr) = &hostfwd.guestaddr {
196 value.push_str(guestaddr);
197 }
198 value.push(':');
199 value.push_str(&hostfwd.guestport.to_string());
200 }
201
202 args.push(format!("hostfwd={value}"));
203 }
204 }
205 if let Some(guestfwds) = &self.guestfwd {
206 for guestfwd in guestfwds {
207 let mut subargs = vec!["tcp".to_string()];
208
209 subargs.push(guestfwd.server.to_string());
210 subargs.push(format!("{}", guestfwd.port));
211
212 match &guestfwd.target {
213 GuestForwardTarget::Device(dev) => {
214 subargs.push(format!("device={}", dev.to_command().join(" ")));
215 }
216 GuestForwardTarget::Cmd((cmd, args)) => {
217 subargs.push(format!("cmd:{} {}", cmd, args.join(" ")));
218 }
219 }
220 args.push(format!("guestfwd={}", subargs.join(":")));
221 }
222 }
223 vec![args.join(DELIM_COMMA)]
224 }
225}
226
227impl FromStr for User {
228 type Err = String;
229
230 fn from_str(s: &str) -> Result<Self, Self::Err> {
231 let props = parse_netdev_props(s, "user")?;
232 let id = required_prop(&props, "id")?.to_string();
233 let mut dnssearch = vec![];
234 let mut hostfwd = vec![];
235 let mut guestfwd = vec![];
236
237 for value in all_props(&props, "dnssearch") {
238 dnssearch.push(value.to_string());
239 }
240 for value in all_props(&props, "hostfwd") {
241 hostfwd.push(parse_hostfwd(value)?);
242 }
243 for value in all_props(&props, "guestfwd") {
244 guestfwd.push(parse_guestfwd(value)?);
245 }
246
247 let smb = first_prop(&props, "smb").map(|dir| SMB {
248 dir: PathBuf::from(dir),
249 smbserver: first_prop(&props, "smbserver").map(ToString::to_string),
250 });
251
252 Ok(Self {
253 id,
254 ipv4: parse_optional_onoff(first_prop(&props, "ipv4"))?,
255 net: parse_optional_qipv4net(first_prop(&props, "net"))?,
256 host: parse_optional_ipv4(first_prop(&props, "host"))?,
257 ipv6: parse_optional_onoff(first_prop(&props, "ipv6"))?,
258 ipv6_net: parse_optional_qipv6net(first_prop(&props, "ipv6-net"))?,
259 ipv6_host: parse_optional_ipv6(first_prop(&props, "ipv6-host"))?,
260 restrict: parse_optional_onoff(first_prop(&props, "restrict"))?,
261 hostname: first_prop(&props, "hostname").map(ToString::to_string),
262 dhcpstart: parse_optional_ipv4(first_prop(&props, "dhcpstart"))?,
263 dns: parse_optional_ipv4(first_prop(&props, "dns"))?,
264 ipv6_dns: parse_optional_ipv6(first_prop(&props, "ipv6-dns"))?,
265 dnssearch: (!dnssearch.is_empty()).then_some(dnssearch),
266 domainname: first_prop(&props, "domainname").map(ToString::to_string),
267 tftp: first_prop(&props, "tftp").map(PathBuf::from),
268 tftp_server_name: first_prop(&props, "tftp-server-name").map(ToString::to_string),
269 bootfile: first_prop(&props, "bootfile").map(PathBuf::from),
270 smb,
271 hostfwd: (!hostfwd.is_empty()).then_some(hostfwd),
272 guestfwd: (!guestfwd.is_empty()).then_some(guestfwd),
273 })
274 }
275}
276
277#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
278pub struct Tap {
279 id: String,
280 fd: Option<String>,
281 fds: Option<Vec<String>>,
282 ifname: Option<String>,
283 script: Option<ScriptOrNot>,
284 downscript: Option<ScriptOrNot>,
285 br: Option<String>,
286 helper: Option<String>,
287 sndbuf: Option<u64>,
288 vnet_hdr: Option<OnOff>,
289 vhost: Option<OnOff>,
290 vhostfd: Option<String>,
291 vhostforce: Option<OnOff>,
292 queues: Option<u64>,
293 poll_us: Option<u64>,
294}
295
296impl ToCommand for Tap {
297 fn to_args(&self) -> Vec<String> {
298 let mut args = vec!["tap".to_string(), format!("id={}", self.id.to_string())];
299
300 if let Some(fd) = &self.fd {
301 args.push(format!("fd={}", fd));
302 }
303 if let Some(fds) = &self.fds {
304 args.push(format!("fds={}", fds.join(":")));
305 }
306 if let Some(ifname) = &self.ifname {
307 args.push(format!("ifname={}", ifname));
308 }
309 if let Some(script) = &self.script {
310 args.push(format!("script={}", script.to_command().join("")));
311 }
312 if let Some(downscript) = &self.downscript {
313 args.push(format!("downscript={}", downscript.to_command().join("")));
314 }
315 if let Some(br) = &self.br {
316 args.push(format!("br={}", br));
317 }
318 if let Some(helper) = &self.helper {
319 args.push(format!("helper={}", helper));
320 }
321 if let Some(sndbuf) = self.sndbuf {
322 args.push(format!("sndbuf={}", sndbuf));
323 }
324 if let Some(vnet_hdr) = &self.vnet_hdr {
325 args.push(format!("vnet_hdr={}", vnet_hdr.to_arg()));
326 }
327 if let Some(vhost) = &self.vhost {
328 args.push(format!("vhost={}", vhost.to_arg()));
329 }
330 if let Some(vhostfd) = &self.vhostfd {
331 args.push(format!("vhostfd={}", vhostfd));
332 }
333 if let Some(vhostforce) = &self.vhostforce {
334 args.push(format!("vhostforce={}", vhostforce.to_arg()));
335 }
336 if let Some(queues) = self.queues {
337 args.push(format!("queues={}", queues));
338 }
339 if let Some(poll_us) = self.poll_us {
340 args.push(format!("poll_us={}", poll_us));
341 }
342
343 vec![args.join(DELIM_COMMA)]
344 }
345}
346
347impl FromStr for Tap {
348 type Err = String;
349
350 fn from_str(s: &str) -> Result<Self, Self::Err> {
351 let mut parts = s.split(',');
352 let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
353 if backend != "tap" {
354 return Err(format!("expected tap backend, got {backend}"));
355 }
356
357 let mut id = None;
358 let mut fd = None;
359 let mut fds = None;
360 let mut ifname = None;
361 let mut script = None;
362 let mut downscript = None;
363 let mut br = None;
364 let mut helper = None;
365 let mut sndbuf = None;
366 let mut vnet_hdr = None;
367 let mut vhost = None;
368 let mut vhostfd = None;
369 let mut vhostforce = None;
370 let mut queues = None;
371 let mut poll_us = None;
372
373 for part in parts {
374 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid tap option: {part}"))?;
375 match key {
376 "id" => id = Some(value.to_string()),
377 "fd" => fd = Some(value.to_string()),
378 "fds" => fds = Some(value.split(':').map(|v| v.to_string()).collect()),
379 "ifname" => ifname = Some(value.to_string()),
380 "script" => script = Some(value.parse::<ScriptOrNot>()?),
381 "downscript" => downscript = Some(value.parse::<ScriptOrNot>()?),
382 "br" => br = Some(value.to_string()),
383 "helper" => helper = Some(value.to_string()),
384 "sndbuf" => sndbuf = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
385 "vnet_hdr" => vnet_hdr = Some(value.parse::<OnOff>().map_err(|_| format!("invalid vnet_hdr value: {value}"))?),
386 "vhost" => vhost = Some(value.parse::<OnOff>().map_err(|_| format!("invalid vhost value: {value}"))?),
387 "vhostfd" => vhostfd = Some(value.to_string()),
388 "vhostforce" => vhostforce = Some(value.parse::<OnOff>().map_err(|_| format!("invalid vhostforce value: {value}"))?),
389 "queues" => queues = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
390 "poll_us" => poll_us = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
391 other => return Err(format!("unsupported tap option: {other}")),
392 }
393 }
394
395 Ok(Self {
396 id: id.ok_or_else(|| "tap netdev requires id=".to_string())?,
397 fd,
398 fds,
399 ifname,
400 script,
401 downscript,
402 br,
403 helper,
404 sndbuf,
405 vnet_hdr,
406 vhost,
407 vhostfd,
408 vhostforce,
409 queues,
410 poll_us,
411 })
412 }
413}
414
415#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
417pub struct Bridge {
418 id: String,
419 bridge: Option<String>,
420 helper: Option<String>,
421}
422
423#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
425pub struct L2tpv3 {
426 id: String,
427 src: String,
428 dst: String,
429 srcport: Option<u16>,
430 dstport: Option<u16>,
431 rxsession: Option<u32>,
432 txsession: u32,
433 ipv6: Option<OnOff>,
434 udp: Option<OnOff>,
435 cookie64: Option<OnOff>,
436 counter: Option<OnOff>,
437 pincounter: Option<OnOff>,
438 txcookie: Option<u64>,
439 rxcookie: Option<u64>,
440 offset: Option<u32>,
441}
442
443impl ToCommand for L2tpv3 {
444 fn command(&self) -> String {
445 ARG_NETDEV.to_string()
446 }
447
448 fn to_args(&self) -> Vec<String> {
449 let mut parts = vec!["l2tpv3".to_string(), format!("id={}", self.id), format!("src={}", self.src), format!("dst={}", self.dst)];
450 macro_rules! push {
451 ($field:ident) => {
452 if let Some(value) = &self.$field {
453 parts.push(format!("{}={}", stringify!($field), value));
454 }
455 };
456 }
457 push!(srcport);
458 push!(dstport);
459 push!(rxsession);
460 parts.push(format!("txsession={}", self.txsession));
461 for (key, value) in [
462 ("ipv6", &self.ipv6),
463 ("udp", &self.udp),
464 ("cookie64", &self.cookie64),
465 ("counter", &self.counter),
466 ("pincounter", &self.pincounter),
467 ] {
468 if let Some(value) = value {
469 parts.push(format!("{}={}", key, value.to_arg()));
470 }
471 }
472 push!(txcookie);
473 push!(rxcookie);
474 push!(offset);
475 vec![parts.join(DELIM_COMMA)]
476 }
477}
478
479impl FromStr for L2tpv3 {
480 type Err = String;
481
482 fn from_str(value: &str) -> Result<Self, Self::Err> {
483 let mut id = None;
484 let mut src = None;
485 let mut dst = None;
486 let mut srcport = None;
487 let mut dstport = None;
488 let mut rxsession = None;
489 let mut txsession = None;
490 let mut ipv6 = None;
491 let mut udp = None;
492 let mut cookie64 = None;
493 let mut counter = None;
494 let mut pincounter = None;
495 let mut txcookie = None;
496 let mut rxcookie = None;
497 let mut offset = None;
498 for part in value.strip_prefix("l2tpv3,").unwrap_or(value).split(DELIM_COMMA) {
499 let (key, raw) = part.split_once('=').ok_or_else(|| format!("invalid l2tpv3 option: {part}"))?;
500 macro_rules! number {
501 ($slot:ident, $type:ty) => {
502 $slot = Some({
503 let number = match raw.strip_prefix("0x") {
504 Some(hex) => u64::from_str_radix(hex, 16).map_err(|e| e.to_string())?,
505 None => raw.parse::<u64>().map_err(|e| e.to_string())?,
506 };
507 <$type>::try_from(number).map_err(|_| format!("{key} value is out of range: {raw}"))?
508 })
509 };
510 }
511 match key {
512 "id" => id = Some(raw.to_string()),
513 "src" => src = Some(raw.to_string()),
514 "dst" => dst = Some(raw.to_string()),
515 "srcport" => number!(srcport, u16),
516 "dstport" => number!(dstport, u16),
517 "rxsession" => number!(rxsession, u32),
518 "txsession" => number!(txsession, u32),
519 "ipv6" => ipv6 = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid ipv6 value: {raw}"))?),
520 "udp" => udp = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid udp value: {raw}"))?),
521 "cookie64" => cookie64 = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid cookie64 value: {raw}"))?),
522 "counter" => counter = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid counter value: {raw}"))?),
523 "pincounter" => pincounter = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid pincounter value: {raw}"))?),
524 "txcookie" => number!(txcookie, u64),
525 "rxcookie" => number!(rxcookie, u64),
526 "offset" => number!(offset, u32),
527 other => return Err(format!("unsupported l2tpv3 option: {other}")),
528 }
529 }
530 Ok(Self {
531 id: id.ok_or_else(|| "l2tpv3 requires id=".to_string())?,
532 src: src.ok_or_else(|| "l2tpv3 requires src=".to_string())?,
533 dst: dst.ok_or_else(|| "l2tpv3 requires dst=".to_string())?,
534 srcport,
535 dstport,
536 rxsession,
537 txsession: txsession.ok_or_else(|| "l2tpv3 requires txsession=".to_string())?,
538 ipv6,
539 udp,
540 cookie64,
541 counter,
542 pincounter,
543 txcookie,
544 rxcookie,
545 offset,
546 })
547 }
548}
549
550impl ToCommand for Bridge {
551 fn to_args(&self) -> Vec<String> {
552 let mut args = vec!["bridge".to_string(), format!("id={}", self.id)];
553
554 if let Some(br) = &self.bridge {
555 args.push(format!("br={}", br));
556 }
557 if let Some(helper) = &self.helper {
558 args.push(format!("helper={}", helper));
559 }
560 vec![args.join(DELIM_COMMA)]
561 }
562}
563
564impl FromStr for Bridge {
565 type Err = String;
566
567 fn from_str(s: &str) -> Result<Self, Self::Err> {
568 let mut parts = s.split(',');
569 let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
570 if backend != "bridge" {
571 return Err(format!("expected bridge backend, got {backend}"));
572 }
573
574 let mut id = None;
575 let mut bridge = None;
576 let mut helper = None;
577
578 for part in parts {
579 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid bridge option: {part}"))?;
580 match key {
581 "id" => id = Some(value.to_string()),
582 "br" => bridge = Some(value.to_string()),
583 "helper" => helper = Some(value.to_string()),
584 other => return Err(format!("unsupported bridge option: {other}")),
585 }
586 }
587
588 Ok(Self {
589 id: id.ok_or_else(|| "bridge netdev requires id=".to_string())?,
590 bridge,
591 helper,
592 })
593 }
594}
595
596#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
598pub struct HostAndPort {
599 host: String,
600 port: u16,
601}
602#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
604pub struct HostAndMaybePort {
605 host: String,
606 port: Option<u16>,
607}
608#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
610pub struct SocketRegular {
611 id: String,
612 fd: Option<String>,
613 listen: Option<HostAndMaybePort>,
614 connection: Option<HostAndPort>,
615}
616
617impl ToCommand for SocketRegular {
618 fn to_args(&self) -> Vec<String> {
619 let mut args = vec!["socket".to_string(), format!("id={}", self.id)];
620
621 if let Some(fd) = &self.fd {
622 args.push(format!("fd={}", fd));
623 }
624 if let Some(listen) = &self.listen {
625 if let Some(port) = &listen.port {
626 args.push(format!("listen={}:{}", listen.host, port));
627 } else {
628 args.push(format!("listen={}", listen.host));
629 }
630 }
631 if let Some(connection) = &self.connection {
632 args.push(format!("connect={}:{}", connection.host, connection.port));
633 }
634
635 vec![args.join(DELIM_COMMA)]
636 }
637}
638
639impl FromStr for SocketRegular {
640 type Err = String;
641
642 fn from_str(s: &str) -> Result<Self, Self::Err> {
643 let mut parts = s.split(',');
644 let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
645 if backend != "socket" {
646 return Err(format!("expected socket backend, got {backend}"));
647 }
648
649 let mut id = None;
650 let mut fd = None;
651 let mut listen = None;
652 let mut connection = None;
653
654 for part in parts {
655 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid socket option: {part}"))?;
656 match key {
657 "id" => id = Some(value.to_string()),
658 "fd" => fd = Some(value.to_string()),
659 "listen" => listen = Some(parse_host_and_maybe_port(value)?),
660 "connect" => connection = Some(parse_host_and_port(value)?),
661 "mcast" | "udp" | "localaddr" => return Err(format!("socket variant is not regular: {part}")),
662 other => return Err(format!("unsupported socket option: {other}")),
663 }
664 }
665
666 Ok(Self {
667 id: id.ok_or_else(|| "socket netdev requires id=".to_string())?,
668 fd,
669 listen,
670 connection,
671 })
672 }
673}
674
675#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
677pub struct SocketMulticast {
678 id: String,
679 fd: Option<String>,
680 mcast: Option<HostAndPort>,
681 localaddr: Option<String>,
682}
683
684impl ToCommand for SocketMulticast {
685 fn to_args(&self) -> Vec<String> {
686 let mut args = vec!["socket".to_string(), format!("id={}", self.id)];
687
688 if let Some(fd) = &self.fd {
689 args.push(format!("fd={}", fd));
690 }
691 if let Some(mcast) = &self.mcast {
692 args.push(format!("mcast={}:{}", mcast.host, mcast.port));
693 }
694 if let Some(localaddr) = &self.localaddr {
695 args.push(format!("localaddr={}", localaddr));
696 }
697 vec![args.join(DELIM_COMMA)]
698 }
699}
700
701impl FromStr for SocketMulticast {
702 type Err = String;
703
704 fn from_str(s: &str) -> Result<Self, Self::Err> {
705 let mut parts = s.split(',');
706 let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
707 if backend != "socket" {
708 return Err(format!("expected socket backend, got {backend}"));
709 }
710
711 let mut id = None;
712 let mut fd = None;
713 let mut mcast = None;
714 let mut localaddr = None;
715
716 for part in parts {
717 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid socket option: {part}"))?;
718 match key {
719 "id" => id = Some(value.to_string()),
720 "fd" => fd = Some(value.to_string()),
721 "mcast" => mcast = Some(parse_host_and_port(value)?),
722 "localaddr" => localaddr = Some(value.to_string()),
723 "listen" | "connect" | "udp" => return Err(format!("socket variant is not multicast: {part}")),
724 other => return Err(format!("unsupported socket option: {other}")),
725 }
726 }
727
728 Ok(Self {
729 id: id.ok_or_else(|| "socket netdev requires id=".to_string())?,
730 fd,
731 mcast,
732 localaddr,
733 })
734 }
735}
736
737#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
739pub struct SocketUdpTunnel {
740 id: String,
741 fd: Option<String>,
742 udp: Option<HostAndPort>,
743 localaddr: Option<HostAndPort>,
744}
745
746impl ToCommand for SocketUdpTunnel {
747 fn to_args(&self) -> Vec<String> {
748 let mut args = vec!["socket".to_string(), format!("id={}", self.id)];
749
750 if let Some(fd) = &self.fd {
751 args.push(format!("fd={}", fd));
752 }
753 if let Some(udp) = &self.udp {
754 args.push(format!("udp={}:{}", udp.host, udp.port));
755 }
756 if let Some(localaddr) = &self.localaddr {
757 args.push(format!("localaddr={}:{}", localaddr.host, localaddr.port));
758 }
759 vec![args.join(DELIM_COMMA)]
760 }
761}
762
763impl FromStr for SocketUdpTunnel {
764 type Err = String;
765
766 fn from_str(s: &str) -> Result<Self, Self::Err> {
767 let mut parts = s.split(',');
768 let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
769 if backend != "socket" {
770 return Err(format!("expected socket backend, got {backend}"));
771 }
772
773 let mut id = None;
774 let mut fd = None;
775 let mut udp = None;
776 let mut localaddr = None;
777
778 for part in parts {
779 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid socket option: {part}"))?;
780 match key {
781 "id" => id = Some(value.to_string()),
782 "fd" => fd = Some(value.to_string()),
783 "udp" => udp = Some(parse_host_and_port(value)?),
784 "localaddr" => localaddr = Some(parse_host_and_port(value)?),
785 "listen" | "connect" | "mcast" => return Err(format!("socket variant is not udp tunnel: {part}")),
786 other => return Err(format!("unsupported socket option: {other}")),
787 }
788 }
789
790 Ok(Self {
791 id: id.ok_or_else(|| "socket netdev requires id=".to_string())?,
792 fd,
793 udp,
794 localaddr,
795 })
796 }
797}
798
799#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
801pub enum Socket {
802 SocketRegular(SocketRegular),
803 Multicast(SocketMulticast),
804 UDPTunnel(SocketUdpTunnel),
805}
806
807impl ToCommand for Socket {
808 fn to_args(&self) -> Vec<String> {
809 match self {
810 Socket::SocketRegular(s) => s.to_args(),
811 Socket::Multicast(s) => s.to_args(),
812 Socket::UDPTunnel(s) => s.to_args(),
813 }
814 }
815}
816
817impl FromStr for Socket {
818 type Err = String;
819
820 fn from_str(s: &str) -> Result<Self, Self::Err> {
821 if s.contains(",mcast=") || s.starts_with("socket,mcast=") {
822 return Ok(Self::Multicast(s.parse::<SocketMulticast>()?));
823 }
824 if s.contains(",udp=") || s.starts_with("socket,udp=") {
825 return Ok(Self::UDPTunnel(s.parse::<SocketUdpTunnel>()?));
826 }
827 Ok(Self::SocketRegular(s.parse::<SocketRegular>()?))
828 }
829}
830
831#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
832pub struct StreamOverTcp {
833 id: String,
834 server: Option<OnOff>,
835 addr_host: String,
836 addr_port: u16,
837 to: Option<u16>,
838 numeric: Option<OnOff>,
839 keep_alive: Option<OnOff>,
840 mptcp: Option<OnOff>,
841 addr_ipv4: Option<OnOff>,
842 addr_ipv6: Option<OnOff>,
843 reconnect_ms: Option<u64>,
844}
845
846impl ToCommand for StreamOverTcp {
847 fn to_args(&self) -> Vec<String> {
848 let mut args = vec!["stream".to_string(), format!("id={}", self.id)];
849
850 if let Some(server) = &self.server {
851 args.push(format!("server={}", server.to_arg()));
852 }
853 args.push("addr.type=inet".to_string());
854 args.push(format!("addr.host={}", self.addr_host));
855 args.push(format!("addr.port={}", self.addr_port));
856 if let Some(to) = &self.to {
857 args.push(format!("to={}", to));
858 }
859 if let Some(numeric) = &self.numeric {
860 args.push(format!("numeric={}", numeric.to_arg()));
861 }
862 if let Some(keep_alive) = &self.keep_alive {
863 args.push(format!("keep-alive={}", keep_alive.to_arg()));
864 }
865 if let Some(mptcp) = &self.mptcp {
866 args.push(format!("mptcp={}", mptcp.to_arg()));
867 }
868 if let Some(ipv4) = &self.addr_ipv4 {
869 args.push(format!("addr.ipv4={}", ipv4.to_arg()));
870 }
871 if let Some(ipv6) = &self.addr_ipv6 {
872 args.push(format!("addr.ipv6={}", ipv6.to_arg()));
873 }
874 if let Some(reconnect_ms) = self.reconnect_ms {
875 args.push(format!("reconnect-ms={}", reconnect_ms));
876 }
877 vec![args.join(DELIM_COMMA)]
878 }
879}
880
881impl FromStr for StreamOverTcp {
882 type Err = String;
883
884 fn from_str(s: &str) -> Result<Self, Self::Err> {
885 let props = parse_netdev_props(s, "stream")?;
886 ensure_prop_value(&props, "addr.type", "inet")?;
887 Ok(Self {
888 id: required_prop(&props, "id")?.to_string(),
889 server: parse_optional_onoff(first_prop(&props, "server"))?,
890 addr_host: required_prop(&props, "addr.host")?.to_string(),
891 addr_port: required_prop(&props, "addr.port")?.parse::<u16>().map_err(|e| e.to_string())?,
892 to: parse_optional_u16(first_prop(&props, "to"))?,
893 numeric: parse_optional_onoff(first_prop(&props, "numeric"))?,
894 keep_alive: parse_optional_onoff(first_prop(&props, "keep-alive"))?,
895 mptcp: parse_optional_onoff(first_prop(&props, "mptcp"))?,
896 addr_ipv4: parse_optional_onoff(first_prop(&props, "addr.ipv4"))?,
897 addr_ipv6: parse_optional_onoff(first_prop(&props, "addr.ipv6"))?,
898 reconnect_ms: parse_optional_u64(first_prop(&props, "reconnect-ms"))?,
899 })
900 }
901}
902
903#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
904pub struct StreamOverUds {
905 id: String,
906 server: Option<OnOff>,
907 addr_path: String,
908 abstract_arg: Option<OnOff>,
909 tight: Option<OnOff>,
910 reconnect_ms: Option<u64>,
911}
912
913impl ToCommand for StreamOverUds {
914 fn to_args(&self) -> Vec<String> {
915 let mut args = vec!["stream".to_string(), format!("id={}", self.id)];
916
917 if let Some(server) = &self.server {
918 args.push(format!("server={}", server.to_arg()));
919 }
920 args.push("addr.type=unix".to_string());
921 args.push(format!("addr.path={}", self.addr_path));
922 if let Some(abstract_arg) = &self.abstract_arg {
923 args.push(format!("abstract={}", abstract_arg.to_arg()));
924 }
925 if let Some(tight) = &self.tight {
926 args.push(format!("tight={}", tight.to_arg()));
927 }
928 if let Some(reconnect_ms) = self.reconnect_ms {
929 args.push(format!("reconnect-ms={}", reconnect_ms));
930 }
931 vec![args.join(DELIM_COMMA)]
932 }
933}
934
935impl FromStr for StreamOverUds {
936 type Err = String;
937
938 fn from_str(s: &str) -> Result<Self, Self::Err> {
939 let props = parse_netdev_props(s, "stream")?;
940 ensure_prop_value(&props, "addr.type", "unix")?;
941 Ok(Self {
942 id: required_prop(&props, "id")?.to_string(),
943 server: parse_optional_onoff(first_prop(&props, "server"))?,
944 addr_path: required_prop(&props, "addr.path")?.to_string(),
945 abstract_arg: parse_optional_onoff(first_prop(&props, "abstract"))?,
946 tight: parse_optional_onoff(first_prop(&props, "tight"))?,
947 reconnect_ms: parse_optional_u64(first_prop(&props, "reconnect-ms"))?,
948 })
949 }
950}
951
952#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
953pub struct StreamOverFd {
954 id: String,
955 server: Option<OnOff>,
956 addr_str: String,
957 reconnect_ms: Option<u64>,
958}
959
960impl ToCommand for StreamOverFd {
961 fn to_args(&self) -> Vec<String> {
962 let mut args = vec!["stream".to_string(), format!("id={}", self.id)];
963
964 if let Some(server) = &self.server {
965 args.push(format!("server={}", server.to_arg()));
966 }
967 args.push("addr.type=fd".to_string());
968 args.push(format!("addr.str={}", self.addr_str));
969 if let Some(reconnect_ms) = self.reconnect_ms {
970 args.push(format!("reconnect-ms={}", reconnect_ms));
971 }
972 vec![args.join(DELIM_COMMA)]
973 }
974}
975
976impl FromStr for StreamOverFd {
977 type Err = String;
978
979 fn from_str(s: &str) -> Result<Self, Self::Err> {
980 let props = parse_netdev_props(s, "stream")?;
981 ensure_prop_value(&props, "addr.type", "fd")?;
982 Ok(Self {
983 id: required_prop(&props, "id")?.to_string(),
984 server: parse_optional_onoff(first_prop(&props, "server"))?,
985 addr_str: required_prop(&props, "addr.str")?.to_string(),
986 reconnect_ms: parse_optional_u64(first_prop(&props, "reconnect-ms"))?,
987 })
988 }
989}
990
991#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
992pub enum Stream {
993 StreamOverTcp(StreamOverTcp),
994 StreamOverUds(StreamOverUds),
995 StreamOverFd(StreamOverFd),
996}
997
998impl ToCommand for Stream {
999 fn to_args(&self) -> Vec<String> {
1000 match self {
1001 Stream::StreamOverTcp(s) => s.to_args(),
1002 Stream::StreamOverUds(s) => s.to_args(),
1003 Stream::StreamOverFd(s) => s.to_args(),
1004 }
1005 }
1006}
1007
1008impl FromStr for Stream {
1009 type Err = String;
1010
1011 fn from_str(s: &str) -> Result<Self, Self::Err> {
1012 let props = parse_props(s)?;
1013 match first_prop(&props, "addr.type") {
1014 Some("inet") => Ok(Self::StreamOverTcp(s.parse::<StreamOverTcp>()?)),
1015 Some("unix") => Ok(Self::StreamOverUds(s.parse::<StreamOverUds>()?)),
1016 Some("fd") => Ok(Self::StreamOverFd(s.parse::<StreamOverFd>()?)),
1017 other => Err(format!("unsupported stream addr.type: {}", other.unwrap_or("<missing>"))),
1018 }
1019 }
1020}
1021
1022#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1023pub struct DgramMulticast {
1024 id: String,
1025 remote_host: String,
1026 remote_port: u16,
1027 local_host: Option<String>,
1028}
1029
1030impl ToCommand for DgramMulticast {
1031 fn to_args(&self) -> Vec<String> {
1032 let mut args = vec![
1033 "dgram".to_string(),
1034 format!("id={}", self.id.to_string()),
1035 "remote.type=inet".to_string(),
1036 format!("remote.host={}", self.remote_host),
1037 format!("remote.port={}", self.remote_port),
1038 ];
1039
1040 if let Some(local_host) = &self.local_host {
1041 args.push("local.type=inet".to_string());
1042 args.push(format!("local.host={}", local_host));
1043 }
1044 vec![args.join(DELIM_COMMA)]
1045 }
1046}
1047
1048impl FromStr for DgramMulticast {
1049 type Err = String;
1050
1051 fn from_str(s: &str) -> Result<Self, Self::Err> {
1052 let props = parse_netdev_props(s, "dgram")?;
1053 ensure_prop_value(&props, "remote.type", "inet")?;
1054 Ok(Self {
1055 id: required_prop(&props, "id")?.to_string(),
1056 remote_host: required_prop(&props, "remote.host")?.to_string(),
1057 remote_port: required_prop(&props, "remote.port")?.parse::<u16>().map_err(|e| e.to_string())?,
1058 local_host: first_prop(&props, "local.host").map(ToString::to_string),
1059 })
1060 }
1061}
1062
1063#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1064pub struct DgramMulticastUdpFd {
1065 id: String,
1066 remote_host: String,
1067 remote_port: u16,
1068 local_str: Option<String>,
1069}
1070
1071impl ToCommand for DgramMulticastUdpFd {
1072 fn to_args(&self) -> Vec<String> {
1073 let mut args = vec![
1074 "dgram".to_string(),
1075 format!("id={}", self.id.to_string()),
1076 "remote.type=inet".to_string(),
1077 format!("remote.host={}", self.remote_host),
1078 format!("remote.port={}", self.remote_port),
1079 ];
1080
1081 if let Some(local_str) = &self.local_str {
1082 args.push("local.type=fd".to_string());
1083 args.push(format!("local.str={}", local_str));
1084 }
1085 vec![args.join(DELIM_COMMA)]
1086 }
1087}
1088
1089impl FromStr for DgramMulticastUdpFd {
1090 type Err = String;
1091
1092 fn from_str(s: &str) -> Result<Self, Self::Err> {
1093 let props = parse_netdev_props(s, "dgram")?;
1094 ensure_prop_value(&props, "remote.type", "inet")?;
1095 Ok(Self {
1096 id: required_prop(&props, "id")?.to_string(),
1097 remote_host: required_prop(&props, "remote.host")?.to_string(),
1098 remote_port: required_prop(&props, "remote.port")?.parse::<u16>().map_err(|e| e.to_string())?,
1099 local_str: first_prop(&props, "local.str").map(ToString::to_string),
1100 })
1101 }
1102}
1103
1104#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1105pub struct DgramSocket {
1106 id: String,
1107 local_host: String,
1108 local_port: u64,
1109 remote_host: Option<String>,
1110 remote_port: Option<u16>,
1111}
1112
1113impl ToCommand for DgramSocket {
1114 fn to_args(&self) -> Vec<String> {
1115 let mut args = vec![
1116 "dgram".to_string(),
1117 format!("id={}", self.id.to_string()),
1118 "local.type=inet".to_string(),
1119 format!("local.host={}", self.local_host),
1120 format!("local.port={}", self.local_port),
1121 ];
1122
1123 if let Some(remote_host) = &self.remote_host {
1124 args.push("remote.type=inet".to_string());
1125 args.push(format!("remote.host={}", remote_host));
1126 }
1127 if let Some(remote_port) = &self.remote_port {
1128 args.push(format!("remote.port={}", remote_port));
1129 }
1130 vec![args.join(DELIM_COMMA)]
1131 }
1132}
1133
1134impl FromStr for DgramSocket {
1135 type Err = String;
1136
1137 fn from_str(s: &str) -> Result<Self, Self::Err> {
1138 let props = parse_netdev_props(s, "dgram")?;
1139 ensure_prop_value(&props, "local.type", "inet")?;
1140 Ok(Self {
1141 id: required_prop(&props, "id")?.to_string(),
1142 local_host: required_prop(&props, "local.host")?.to_string(),
1143 local_port: required_prop(&props, "local.port")?.parse::<u64>().map_err(|e| e.to_string())?,
1144 remote_host: first_prop(&props, "remote.host").map(ToString::to_string),
1145 remote_port: parse_optional_u16(first_prop(&props, "remote.port"))?,
1146 })
1147 }
1148}
1149
1150#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1151pub struct DgramUds {
1152 id: String,
1153 local_path: PathBuf,
1154 remote_path: Option<PathBuf>,
1155}
1156
1157impl ToCommand for DgramUds {
1158 fn to_args(&self) -> Vec<String> {
1159 let mut args = vec![
1160 "dgram".to_string(),
1161 format!("id={}", self.id.to_string()),
1162 "local.type=unix".to_string(),
1163 format!("local.path={}", self.local_path.display()),
1164 ];
1165 if let Some(remote) = &self.remote_path {
1166 args.push("remote.type=unix".to_string());
1167 args.push(format!("remote.path={}", remote.display()));
1168 }
1169 vec![args.join(DELIM_COMMA)]
1170 }
1171}
1172
1173impl FromStr for DgramUds {
1174 type Err = String;
1175
1176 fn from_str(s: &str) -> Result<Self, Self::Err> {
1177 let props = parse_netdev_props(s, "dgram")?;
1178 ensure_prop_value(&props, "local.type", "unix")?;
1179 Ok(Self {
1180 id: required_prop(&props, "id")?.to_string(),
1181 local_path: PathBuf::from(required_prop(&props, "local.path")?),
1182 remote_path: first_prop(&props, "remote.path").map(PathBuf::from),
1183 })
1184 }
1185}
1186
1187#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1188pub struct DgramFd {
1189 id: String,
1190 local_str: String,
1191}
1192
1193impl ToCommand for DgramFd {
1194 fn to_args(&self) -> Vec<String> {
1195 vec![["dgram".to_string(), format!("id={}", self.id), "local.type=fd".to_string(), format!("local.str={}", self.local_str)].join(DELIM_COMMA)]
1196 }
1197}
1198
1199impl FromStr for DgramFd {
1200 type Err = String;
1201
1202 fn from_str(s: &str) -> Result<Self, Self::Err> {
1203 let props = parse_netdev_props(s, "dgram")?;
1204 ensure_prop_value(&props, "local.type", "fd")?;
1205 Ok(Self {
1206 id: required_prop(&props, "id")?.to_string(),
1207 local_str: required_prop(&props, "local.str")?.to_string(),
1208 })
1209 }
1210}
1211
1212#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
1213pub enum Dgram {
1214 DgramMulticast(DgramMulticast),
1215 DgramMulticastUdpFd(DgramMulticastUdpFd),
1216 DgramSocket(DgramSocket),
1217 DgramUds(DgramUds),
1218 DgramFd(DgramFd),
1219}
1220
1221impl ToCommand for Dgram {
1222 fn to_args(&self) -> Vec<String> {
1223 match self {
1224 Dgram::DgramMulticast(args) => args.to_args(),
1225 Dgram::DgramMulticastUdpFd(args) => args.to_args(),
1226 Dgram::DgramSocket(args) => args.to_args(),
1227 Dgram::DgramUds(args) => args.to_args(),
1228 Dgram::DgramFd(args) => args.to_args(),
1229 }
1230 }
1231}
1232
1233impl FromStr for Dgram {
1234 type Err = String;
1235
1236 fn from_str(s: &str) -> Result<Self, Self::Err> {
1237 let props = parse_props(s)?;
1238 match (first_prop(&props, "local.type"), first_prop(&props, "remote.type")) {
1239 (Some("fd"), Some("inet")) => Ok(Self::DgramMulticastUdpFd(s.parse::<DgramMulticastUdpFd>()?)),
1240 (Some("fd"), _) => Ok(Self::DgramFd(s.parse::<DgramFd>()?)),
1241 (Some("unix"), _) => Ok(Self::DgramUds(s.parse::<DgramUds>()?)),
1242 (Some("inet"), _) => Ok(Self::DgramSocket(s.parse::<DgramSocket>()?)),
1243 (None, Some("inet")) => Ok(Self::DgramMulticast(s.parse::<DgramMulticast>()?)),
1244 _ => Err(format!("unsupported dgram backend: {s}")),
1245 }
1246 }
1247}
1248
1249#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1250pub struct Vde {
1251 id: String,
1252 sock: Option<PathBuf>,
1253 port: Option<u16>,
1254 group: Option<String>,
1255 mode: Option<String>,
1256}
1257
1258impl ToCommand for Vde {
1259 fn to_args(&self) -> Vec<String> {
1260 let mut args = vec!["vde".to_string(), format!("id={}", self.id.to_string())];
1261
1262 if let Some(sock) = &self.sock {
1263 args.push(format!("sock={}", sock.display()));
1264 }
1265 if let Some(port) = self.port {
1266 args.push(format!("port={}", port));
1267 }
1268 if let Some(group) = &self.group {
1269 args.push(format!("group={}", group));
1270 }
1271 if let Some(mode) = &self.mode {
1272 args.push(format!("mode={}", mode));
1273 }
1274 vec![args.join(DELIM_COMMA)]
1275 }
1276}
1277
1278impl FromStr for Vde {
1279 type Err = String;
1280
1281 fn from_str(s: &str) -> Result<Self, Self::Err> {
1282 let props = parse_netdev_props(s, "vde")?;
1283 Ok(Self {
1284 id: required_prop(&props, "id")?.to_string(),
1285 sock: first_prop(&props, "sock").map(PathBuf::from),
1286 port: parse_optional_u16(first_prop(&props, "port"))?,
1287 group: first_prop(&props, "group").map(ToString::to_string),
1288 mode: first_prop(&props, "mode").map(ToString::to_string),
1289 })
1290 }
1291}
1292
1293#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1294pub struct NetMap {
1295 id: String,
1296 ifname: String,
1297 devname: Option<String>,
1298}
1299
1300impl ToCommand for NetMap {
1301 fn to_args(&self) -> Vec<String> {
1302 let mut args = vec!["netmap".to_string(), format!("id={}", self.id.to_string())];
1303 args.push(format!("ifname={}", self.ifname));
1304 if let Some(devname) = &self.devname {
1305 args.push(format!("devname={}", devname));
1306 }
1307 vec![args.join(DELIM_COMMA)]
1308 }
1309}
1310
1311impl FromStr for NetMap {
1312 type Err = String;
1313
1314 fn from_str(s: &str) -> Result<Self, Self::Err> {
1315 let props = parse_netdev_props(s, "netmap")?;
1316 Ok(Self {
1317 id: required_prop(&props, "id")?.to_string(),
1318 ifname: required_prop(&props, "ifname")?.to_string(),
1319 devname: first_prop(&props, "devname").map(ToString::to_string),
1320 })
1321 }
1322}
1323
1324#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
1325pub enum NativeSkb {
1326 Native,
1327 Skb,
1328}
1329
1330impl ToArg for NativeSkb {
1331 fn to_arg(&self) -> &str {
1332 match self {
1333 NativeSkb::Native => "native",
1334 NativeSkb::Skb => "skb",
1335 }
1336 }
1337}
1338#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1339pub struct AfXdp {
1340 id: String,
1341 ifname: String,
1342 mode: Option<NativeSkb>,
1343 force_copy: Option<OnOff>,
1344 queues: Option<u64>,
1345 start_queue: Option<u64>,
1346 inhibit: Option<OnOff>,
1347 sock_fds: Option<Vec<String>>,
1348 map_path: Option<PathBuf>,
1349 map_start_index: Option<u64>,
1350}
1351
1352impl ToCommand for AfXdp {
1353 fn to_args(&self) -> Vec<String> {
1354 let mut args = vec!["af-xdp".to_string(), format!("id={}", self.id.to_string()), format!("ifname={}", self.ifname)];
1355
1356 if let Some(mode) = &self.mode {
1357 args.push(format!("mode={}", mode.to_arg()));
1358 }
1359 if let Some(force_copy) = &self.force_copy {
1360 args.push(format!("force-copy={}", force_copy.to_arg()));
1361 }
1362 if let Some(queues) = self.queues {
1363 args.push(format!("queues={}", queues));
1364 }
1365 if let Some(start_queue) = self.start_queue {
1366 args.push(format!("start-queue={}", start_queue));
1367 }
1368 if let Some(inhibit) = &self.inhibit {
1369 args.push(format!("inhibit={}", inhibit.to_arg()));
1370 }
1371 if let Some(sock_fds) = &self.sock_fds {
1372 args.push(format!("sock-fds={}", sock_fds.join(":")));
1373 }
1374 if let Some(map_path) = &self.map_path {
1375 args.push(format!("map-path={}", map_path.display()));
1376 }
1377 if let Some(map_start_index) = self.map_start_index {
1378 args.push(format!("map-start-index={}", map_start_index));
1379 }
1380 vec![args.join(DELIM_COMMA)]
1381 }
1382}
1383
1384impl FromStr for AfXdp {
1385 type Err = String;
1386
1387 fn from_str(s: &str) -> Result<Self, Self::Err> {
1388 let props = parse_netdev_props(s, "af-xdp")?;
1389 Ok(Self {
1390 id: required_prop(&props, "id")?.to_string(),
1391 ifname: required_prop(&props, "ifname")?.to_string(),
1392 mode: match first_prop(&props, "mode") {
1393 Some("native") => Some(NativeSkb::Native),
1394 Some("skb") => Some(NativeSkb::Skb),
1395 Some(other) => return Err(format!("invalid af-xdp mode: {other}")),
1396 None => None,
1397 },
1398 force_copy: parse_optional_onoff(first_prop(&props, "force-copy"))?,
1399 queues: parse_optional_u64(first_prop(&props, "queues"))?,
1400 start_queue: parse_optional_u64(first_prop(&props, "start-queue"))?,
1401 inhibit: parse_optional_onoff(first_prop(&props, "inhibit"))?,
1402 sock_fds: first_prop(&props, "sock-fds").map(|v| v.split(':').map(|part| part.to_string()).collect()),
1403 map_path: first_prop(&props, "map-path").map(PathBuf::from),
1404 map_start_index: parse_optional_u64(first_prop(&props, "map-start-index"))?,
1405 })
1406 }
1407}
1408
1409#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1411pub struct Passt {
1412 id: String,
1413 path: Option<PathBuf>,
1414 quiet: Option<OnOff>,
1415 vhost_user: Option<OnOff>,
1416 mtu: Option<u64>,
1417 address: Option<String>,
1418 netmask: Option<String>,
1419 mac: Option<String>,
1420 gateway: Option<String>,
1421 interface: Option<String>,
1422 outbound: Option<String>,
1423 outbound_if4: Option<String>,
1424 outbound_if6: Option<String>,
1425 dns: Option<String>,
1426 search: Option<String>,
1427 fqdn: Option<String>,
1428 dhcp_dns: Option<OnOff>,
1429 dhcp_search: Option<OnOff>,
1430 map_host_loopback: Option<String>,
1431 map_guest_addr: Option<String>,
1432 dns_forward: Option<String>,
1433 dns_host: Option<String>,
1434 tcp: Option<OnOff>,
1435 udp: Option<OnOff>,
1436 icmp: Option<OnOff>,
1437 dhcp: Option<OnOff>,
1438 ndp: Option<OnOff>,
1439 dhcpv6: Option<OnOff>,
1440 ra: Option<OnOff>,
1441 freebind: Option<OnOff>,
1442 ipv4: Option<OnOff>,
1443 ipv6: Option<OnOff>,
1444 tcp_ports: Option<String>,
1445 udp_ports: Option<String>,
1446 param: Option<Vec<String>>,
1447}
1448
1449impl ToCommand for Passt {
1450 fn to_args(&self) -> Vec<String> {
1451 let mut args = vec!["passt".to_string(), format!("id={}", self.id)];
1452
1453 if let Some(path) = &self.path {
1454 args.push(format!("path={}", path.display()));
1455 }
1456 if let Some(quiet) = &self.quiet {
1457 args.push(format!("quiet={}", quiet.to_arg()));
1458 }
1459 if let Some(vhost_user) = &self.vhost_user {
1460 args.push(format!("vhost-user={}", vhost_user.to_arg()));
1461 }
1462 if let Some(mtu) = self.mtu {
1463 args.push(format!("mtu={}", mtu));
1464 }
1465 push_opt_string(&mut args, "address", &self.address);
1466 push_opt_string(&mut args, "netmask", &self.netmask);
1467 push_opt_string(&mut args, "mac", &self.mac);
1468 push_opt_string(&mut args, "gateway", &self.gateway);
1469 push_opt_string(&mut args, "interface", &self.interface);
1470 push_opt_string(&mut args, "outbound", &self.outbound);
1471 push_opt_string(&mut args, "outbound-if4", &self.outbound_if4);
1472 push_opt_string(&mut args, "outbound-if6", &self.outbound_if6);
1473 push_opt_string(&mut args, "dns", &self.dns);
1474 push_opt_string(&mut args, "search", &self.search);
1475 push_opt_string(&mut args, "fqdn", &self.fqdn);
1476 if let Some(dhcp_dns) = &self.dhcp_dns {
1477 args.push(format!("dhcp-dns={}", dhcp_dns.to_arg()));
1478 }
1479 if let Some(dhcp_search) = &self.dhcp_search {
1480 args.push(format!("dhcp-search={}", dhcp_search.to_arg()));
1481 }
1482 push_opt_string(&mut args, "map-host-loopback", &self.map_host_loopback);
1483 push_opt_string(&mut args, "map-guest-addr", &self.map_guest_addr);
1484 push_opt_string(&mut args, "dns-forward", &self.dns_forward);
1485 push_opt_string(&mut args, "dns-host", &self.dns_host);
1486 push_opt_onoff(&mut args, "tcp", &self.tcp);
1487 push_opt_onoff(&mut args, "udp", &self.udp);
1488 push_opt_onoff(&mut args, "icmp", &self.icmp);
1489 push_opt_onoff(&mut args, "dhcp", &self.dhcp);
1490 push_opt_onoff(&mut args, "ndp", &self.ndp);
1491 push_opt_onoff(&mut args, "dhcpv6", &self.dhcpv6);
1492 push_opt_onoff(&mut args, "ra", &self.ra);
1493 push_opt_onoff(&mut args, "freebind", &self.freebind);
1494 push_opt_onoff(&mut args, "ipv4", &self.ipv4);
1495 push_opt_onoff(&mut args, "ipv6", &self.ipv6);
1496 push_opt_string(&mut args, "tcp-ports", &self.tcp_ports);
1497 push_opt_string(&mut args, "udp-ports", &self.udp_ports);
1498 if let Some(params) = &self.param {
1499 for param in params {
1500 args.push(format!("param={}", param));
1501 }
1502 }
1503
1504 vec![args.join(DELIM_COMMA)]
1505 }
1506}
1507
1508impl FromStr for Passt {
1509 type Err = String;
1510
1511 fn from_str(s: &str) -> Result<Self, Self::Err> {
1512 let props = parse_netdev_props(s, "passt")?;
1513 Ok(Self {
1514 id: required_prop(&props, "id")?.to_string(),
1515 path: first_prop(&props, "path").map(PathBuf::from),
1516 quiet: parse_optional_onoff(first_prop(&props, "quiet"))?,
1517 vhost_user: parse_optional_onoff(first_prop(&props, "vhost-user"))?,
1518 mtu: parse_optional_u64(first_prop(&props, "mtu"))?,
1519 address: first_prop(&props, "address").map(ToString::to_string),
1520 netmask: first_prop(&props, "netmask").map(ToString::to_string),
1521 mac: first_prop(&props, "mac").map(ToString::to_string),
1522 gateway: first_prop(&props, "gateway").map(ToString::to_string),
1523 interface: first_prop(&props, "interface").map(ToString::to_string),
1524 outbound: first_prop(&props, "outbound").map(ToString::to_string),
1525 outbound_if4: first_prop(&props, "outbound-if4").map(ToString::to_string),
1526 outbound_if6: first_prop(&props, "outbound-if6").map(ToString::to_string),
1527 dns: first_prop(&props, "dns").map(ToString::to_string),
1528 search: first_prop(&props, "search").map(ToString::to_string),
1529 fqdn: first_prop(&props, "fqdn").map(ToString::to_string),
1530 dhcp_dns: parse_optional_onoff(first_prop(&props, "dhcp-dns"))?,
1531 dhcp_search: parse_optional_onoff(first_prop(&props, "dhcp-search"))?,
1532 map_host_loopback: first_prop(&props, "map-host-loopback").map(ToString::to_string),
1533 map_guest_addr: first_prop(&props, "map-guest-addr").map(ToString::to_string),
1534 dns_forward: first_prop(&props, "dns-forward").map(ToString::to_string),
1535 dns_host: first_prop(&props, "dns-host").map(ToString::to_string),
1536 tcp: parse_optional_onoff(first_prop(&props, "tcp"))?,
1537 udp: parse_optional_onoff(first_prop(&props, "udp"))?,
1538 icmp: parse_optional_onoff(first_prop(&props, "icmp"))?,
1539 dhcp: parse_optional_onoff(first_prop(&props, "dhcp"))?,
1540 ndp: parse_optional_onoff(first_prop(&props, "ndp"))?,
1541 dhcpv6: parse_optional_onoff(first_prop(&props, "dhcpv6"))?,
1542 ra: parse_optional_onoff(first_prop(&props, "ra"))?,
1543 freebind: parse_optional_onoff(first_prop(&props, "freebind"))?,
1544 ipv4: parse_optional_onoff(first_prop(&props, "ipv4"))?,
1545 ipv6: parse_optional_onoff(first_prop(&props, "ipv6"))?,
1546 tcp_ports: first_prop(&props, "tcp-ports").map(ToString::to_string),
1547 udp_ports: first_prop(&props, "udp-ports").map(ToString::to_string),
1548 param: {
1549 let params = all_props(&props, "param").into_iter().map(ToString::to_string).collect::<Vec<_>>();
1550 (!params.is_empty()).then_some(params)
1551 },
1552 })
1553 }
1554}
1555
1556#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1558pub struct VhostUser {
1559 id: String,
1560 chardev: String,
1561 vhostforce: Option<OnOff>,
1562 queues: Option<u64>,
1563}
1564
1565impl ToCommand for VhostUser {
1566 fn to_args(&self) -> Vec<String> {
1567 let mut args = vec!["vhost-user".to_string(), format!("id={}", self.id.to_string()), format!("chardev={}", self.chardev)];
1568
1569 if let Some(vhostforce) = &self.vhostforce {
1570 args.push(format!("vhostforce={}", vhostforce.to_arg()));
1571 }
1572 if let Some(queues) = self.queues {
1573 args.push(format!("queues={}", queues));
1574 }
1575 vec![args.join(DELIM_COMMA)]
1576 }
1577}
1578
1579impl FromStr for VhostUser {
1580 type Err = String;
1581
1582 fn from_str(s: &str) -> Result<Self, Self::Err> {
1583 let mut parts = s.split(',');
1584 let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
1585 if backend != "vhost-user" && backend != "type=vhost-user" {
1586 return Err(format!("expected vhost-user backend, got {backend}"));
1587 }
1588
1589 let mut id = None;
1590 let mut chardev = None;
1591 let mut vhostforce = None;
1592 let mut queues = None;
1593
1594 for part in parts {
1595 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid vhost-user option: {part}"))?;
1596 match key {
1597 "id" => id = Some(value.to_string()),
1598 "chardev" => chardev = Some(value.to_string()),
1599 "vhostforce" => vhostforce = Some(value.parse::<OnOff>().map_err(|_| format!("invalid vhostforce value: {value}"))?),
1600 "queues" => queues = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
1601 other => return Err(format!("unsupported vhost-user option: {other}")),
1602 }
1603 }
1604
1605 Ok(Self {
1606 id: id.ok_or_else(|| "vhost-user netdev requires id=".to_string())?,
1607 chardev: chardev.ok_or_else(|| "vhost-user netdev requires chardev=".to_string())?,
1608 vhostforce,
1609 queues,
1610 })
1611 }
1612}
1613
1614#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1616pub struct VhostVdpa {
1617 id: String,
1618 vhostdev: Option<PathBuf>,
1619 vhostfd: Option<String>,
1620}
1621
1622impl ToCommand for VhostVdpa {
1623 fn to_args(&self) -> Vec<String> {
1624 let mut args = vec!["vhost-vdpa".to_string(), format!("id={}", self.id.to_string())];
1625 if let Some(vhostdev) = &self.vhostdev {
1626 args.push(format!("vhostdev={}", vhostdev.to_str().unwrap()));
1627 }
1628 if let Some(vhostfd) = &self.vhostfd {
1629 args.push(format!("vhostfd={}", vhostfd));
1630 }
1631 vec![args.join(DELIM_COMMA)]
1632 }
1633}
1634
1635impl FromStr for VhostVdpa {
1636 type Err = String;
1637
1638 fn from_str(s: &str) -> Result<Self, Self::Err> {
1639 let mut parts = s.split(',');
1640 let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
1641 if backend != "vhost-vdpa" {
1642 return Err(format!("expected vhost-vdpa backend, got {backend}"));
1643 }
1644
1645 let mut id = None;
1646 let mut vhostdev = None;
1647 let mut vhostfd = None;
1648
1649 for part in parts {
1650 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid vhost-vdpa option: {part}"))?;
1651 match key {
1652 "id" => id = Some(value.to_string()),
1653 "vhostdev" => vhostdev = Some(PathBuf::from(value)),
1654 "vhostfd" => vhostfd = Some(value.to_string()),
1655 other => return Err(format!("unsupported vhost-vdpa option: {other}")),
1656 }
1657 }
1658
1659 Ok(Self {
1660 id: id.ok_or_else(|| "vhost-vdpa netdev requires id=".to_string())?,
1661 vhostdev,
1662 vhostfd,
1663 })
1664 }
1665}
1666
1667#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1668pub struct VmnetHost {
1669 id: String,
1670 isolated: Option<OnOff>,
1671 net_uuid: Option<String>,
1672 start_address: Option<String>,
1673 end_address: Option<String>,
1674 subnet_mask: Option<String>,
1675}
1676
1677impl ToCommand for VmnetHost {
1678 fn to_args(&self) -> Vec<String> {
1679 let mut args = vec!["vmnet-host".to_string(), format!("id={}", self.id.to_string())];
1680
1681 if let Some(isolated) = &self.isolated {
1682 args.push(format!("isolated={}", isolated.to_arg()));
1683 }
1684 if let Some(net_uuid) = &self.net_uuid {
1685 args.push(format!("net_uuid={}", net_uuid));
1686 }
1687 if let Some(start_address) = &self.start_address {
1688 args.push(format!("start-address={}", start_address));
1689 }
1690 if let Some(end_address) = &self.end_address {
1691 args.push(format!("end-address={}", end_address));
1692 }
1693 if let Some(subnet_mask) = &self.subnet_mask {
1694 args.push(format!("subnet-mask={}", subnet_mask));
1695 }
1696 vec![args.join(DELIM_COMMA)]
1697 }
1698}
1699
1700impl FromStr for VmnetHost {
1701 type Err = String;
1702
1703 fn from_str(s: &str) -> Result<Self, Self::Err> {
1704 let props = parse_netdev_props(s, "vmnet-host")?;
1705 Ok(Self {
1706 id: required_prop(&props, "id")?.to_string(),
1707 isolated: parse_optional_onoff(first_prop(&props, "isolated"))?,
1708 net_uuid: first_prop(&props, "net_uuid").map(ToString::to_string),
1709 start_address: first_prop(&props, "start-address").map(ToString::to_string),
1710 end_address: first_prop(&props, "end-address").map(ToString::to_string),
1711 subnet_mask: first_prop(&props, "subnet-mask").map(ToString::to_string),
1712 })
1713 }
1714}
1715
1716#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1717pub struct VmnetShared {
1718 id: String,
1719 isolated: Option<OnOff>,
1720 nat66_prefix: Option<String>,
1721 start_address: Option<String>,
1722 end_address: Option<String>,
1723 subnet_mask: Option<String>,
1724}
1725
1726impl ToCommand for VmnetShared {
1727 fn to_args(&self) -> Vec<String> {
1728 let mut args = vec!["vmnet-shared".to_string(), format!("id={}", self.id.to_string())];
1729
1730 if let Some(isolated) = &self.isolated {
1731 args.push(format!("isolated={}", isolated.to_arg()));
1732 }
1733 if let Some(nat66_prefix) = &self.nat66_prefix {
1734 args.push(format!("nat66-prefix={}", nat66_prefix));
1735 }
1736 if let Some(start_address) = &self.start_address {
1737 args.push(format!("start-address={}", start_address));
1738 }
1739 if let Some(end_address) = &self.end_address {
1740 args.push(format!("end-address={}", end_address));
1741 }
1742 if let Some(subnet_mask) = &self.subnet_mask {
1743 args.push(format!("subnet-mask={}", subnet_mask));
1744 }
1745 vec![args.join(DELIM_COMMA)]
1746 }
1747}
1748
1749impl FromStr for VmnetShared {
1750 type Err = String;
1751
1752 fn from_str(s: &str) -> Result<Self, Self::Err> {
1753 let props = parse_netdev_props(s, "vmnet-shared")?;
1754 Ok(Self {
1755 id: required_prop(&props, "id")?.to_string(),
1756 isolated: parse_optional_onoff(first_prop(&props, "isolated"))?,
1757 nat66_prefix: first_prop(&props, "nat66-prefix").map(ToString::to_string),
1758 start_address: first_prop(&props, "start-address").map(ToString::to_string),
1759 end_address: first_prop(&props, "end-address").map(ToString::to_string),
1760 subnet_mask: first_prop(&props, "subnet-mask").map(ToString::to_string),
1761 })
1762 }
1763}
1764
1765#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1766pub struct VmnetBridged {
1767 id: String,
1768 ifname: String,
1769 isolated: Option<OnOff>,
1770}
1771
1772impl ToCommand for VmnetBridged {
1773 fn to_args(&self) -> Vec<String> {
1774 let mut args = vec!["vmnet-bridged".to_string(), format!("id={}", self.id.to_string()), format!("ifname={}", self.ifname)];
1775
1776 if let Some(isolated) = &self.isolated {
1777 args.push(format!("isolated={}", isolated.to_arg()));
1778 }
1779 vec![args.join(DELIM_COMMA)]
1780 }
1781}
1782
1783impl FromStr for VmnetBridged {
1784 type Err = String;
1785
1786 fn from_str(s: &str) -> Result<Self, Self::Err> {
1787 let props = parse_netdev_props(s, "vmnet-bridged")?;
1788 Ok(Self {
1789 id: required_prop(&props, "id")?.to_string(),
1790 ifname: required_prop(&props, "ifname")?.to_string(),
1791 isolated: parse_optional_onoff(first_prop(&props, "isolated"))?,
1792 })
1793 }
1794}
1795
1796#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1798pub struct Hubport {
1799 id: String,
1800 hubid: u64,
1801 netdev: Option<String>,
1802}
1803
1804impl ToCommand for Hubport {
1805 fn to_args(&self) -> Vec<String> {
1806 let mut args = vec!["hubport".to_string(), format!("id={}", self.id.to_string()), format!("hubid={}", self.hubid)];
1807
1808 if let Some(netdev) = &self.netdev {
1809 args.push(format!("netdev={}", netdev));
1810 }
1811 vec![args.join(DELIM_COMMA)]
1812 }
1813}
1814
1815impl FromStr for Hubport {
1816 type Err = String;
1817
1818 fn from_str(s: &str) -> Result<Self, Self::Err> {
1819 let mut parts = s.split(',');
1820 let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
1821 if backend != "hubport" {
1822 return Err(format!("expected hubport backend, got {backend}"));
1823 }
1824
1825 let mut id = None;
1826 let mut hubid = None;
1827 let mut netdev = None;
1828
1829 for part in parts {
1830 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid hubport option: {part}"))?;
1831 match key {
1832 "id" => id = Some(value.to_string()),
1833 "hubid" => hubid = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
1834 "netdev" => netdev = Some(value.to_string()),
1835 other => return Err(format!("unsupported hubport option: {other}")),
1836 }
1837 }
1838
1839 Ok(Self {
1840 id: id.ok_or_else(|| "hubport netdev requires id=".to_string())?,
1841 hubid: hubid.ok_or_else(|| "hubport netdev requires hubid=".to_string())?,
1842 netdev,
1843 })
1844 }
1845}
1846
1847#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
1848pub enum NetDev {
1849 Json(String),
1851 User(User),
1852 Passt(Passt),
1853 L2tpv3(L2tpv3),
1854 Tap(Tap),
1855 Bridge(Bridge),
1856 Socket(Socket),
1857 Stream(Stream),
1858 Dgram(Dgram),
1859 Vde(Vde),
1860 Netmap(NetMap),
1861 AfXdp(AfXdp),
1862 VhostUser(VhostUser),
1863 VhostVdpa(VhostVdpa),
1864 VmnetHost(VmnetHost),
1865 VmnetShared(VmnetShared),
1866 VmnetBridged(VmnetBridged),
1867 Hubport(Hubport),
1868}
1869
1870impl ToCommand for NetDev {
1871 fn command(&self) -> String {
1872 ARG_NETDEV.to_string()
1873 }
1874 fn to_args(&self) -> Vec<String> {
1875 match self {
1876 NetDev::Json(json) => vec![json.clone()],
1877 NetDev::User(user) => user.to_args(),
1878 NetDev::Passt(passt) => passt.to_args(),
1879 NetDev::L2tpv3(l2tpv3) => l2tpv3.to_args(),
1880 NetDev::Tap(tap) => tap.to_args(),
1881 NetDev::Bridge(bridge) => bridge.to_args(),
1882 NetDev::Socket(socket) => socket.to_args(),
1883 NetDev::Stream(stream) => stream.to_args(),
1884 NetDev::Dgram(dgram) => dgram.to_args(),
1885 NetDev::Vde(vde) => vde.to_args(),
1886 NetDev::Netmap(netmap) => netmap.to_args(),
1887 NetDev::AfXdp(af_xdp) => af_xdp.to_args(),
1888 NetDev::VhostUser(vhost_user) => vhost_user.to_args(),
1889 NetDev::VhostVdpa(vhost_vdpa) => vhost_vdpa.to_args(),
1890 NetDev::VmnetHost(vmnet_host) => vmnet_host.to_args(),
1891 NetDev::VmnetShared(vmnet_shared) => vmnet_shared.to_args(),
1892 NetDev::VmnetBridged(vmnet_bridged) => vmnet_bridged.to_args(),
1893 NetDev::Hubport(hubport) => hubport.to_args(),
1894 }
1895 }
1896}
1897
1898impl FromStr for NetDev {
1899 type Err = String;
1900
1901 fn from_str(s: &str) -> Result<Self, Self::Err> {
1902 if s.trim().starts_with('{') && s.trim().ends_with('}') {
1903 return Ok(Self::Json(s.to_string()));
1904 }
1905 if s.starts_with("user,") || s == "user" {
1906 return Ok(Self::User(s.parse::<User>()?));
1907 }
1908 if s.starts_with("passt,") || s == "passt" {
1909 return Ok(Self::Passt(s.parse::<Passt>()?));
1910 }
1911 if s.starts_with("l2tpv3,") || s == "l2tpv3" {
1912 return Ok(Self::L2tpv3(s.parse::<L2tpv3>()?));
1913 }
1914 if s.starts_with("tap,") || s == "tap" {
1915 return Ok(Self::Tap(s.parse::<Tap>()?));
1916 }
1917 if s.starts_with("bridge,") || s == "bridge" {
1918 return Ok(Self::Bridge(s.parse::<Bridge>()?));
1919 }
1920 if s.starts_with("socket,") || s == "socket" {
1921 return Ok(Self::Socket(s.parse::<Socket>()?));
1922 }
1923 if s.starts_with("stream,") || s == "stream" {
1924 return Ok(Self::Stream(s.parse::<Stream>()?));
1925 }
1926 if s.starts_with("dgram,") || s == "dgram" {
1927 return Ok(Self::Dgram(s.parse::<Dgram>()?));
1928 }
1929 if s.starts_with("vde,") || s == "vde" {
1930 return Ok(Self::Vde(s.parse::<Vde>()?));
1931 }
1932 if s.starts_with("netmap,") || s == "netmap" {
1933 return Ok(Self::Netmap(s.parse::<NetMap>()?));
1934 }
1935 if s.starts_with("af-xdp,") || s == "af-xdp" {
1936 return Ok(Self::AfXdp(s.parse::<AfXdp>()?));
1937 }
1938 if s.starts_with("vhost-user,") || s.starts_with("type=vhost-user,") || s == "vhost-user" || s == "type=vhost-user" {
1939 return Ok(Self::VhostUser(s.parse::<VhostUser>()?));
1940 }
1941 if s.starts_with("vhost-vdpa,") || s == "vhost-vdpa" {
1942 return Ok(Self::VhostVdpa(s.parse::<VhostVdpa>()?));
1943 }
1944 if s.starts_with("vmnet-host,") || s == "vmnet-host" {
1945 return Ok(Self::VmnetHost(s.parse::<VmnetHost>()?));
1946 }
1947 if s.starts_with("vmnet-shared,") || s == "vmnet-shared" {
1948 return Ok(Self::VmnetShared(s.parse::<VmnetShared>()?));
1949 }
1950 if s.starts_with("vmnet-bridged,") || s == "vmnet-bridged" {
1951 return Ok(Self::VmnetBridged(s.parse::<VmnetBridged>()?));
1952 }
1953 if s.starts_with("hubport,") || s == "hubport" {
1954 return Ok(Self::Hubport(s.parse::<Hubport>()?));
1955 }
1956
1957 Err(format!("unsupported netdev backend: {s}"))
1958 }
1959}
1960
1961fn parse_host_and_port(value: &str) -> Result<HostAndPort, String> {
1962 let (host, port) = value.rsplit_once(':').ok_or_else(|| format!("expected host:port, got {value}"))?;
1963 Ok(HostAndPort {
1964 host: host.to_string(),
1965 port: port.parse::<u16>().map_err(|e| e.to_string())?,
1966 })
1967}
1968
1969fn parse_host_and_maybe_port(value: &str) -> Result<HostAndMaybePort, String> {
1970 if let Some((host, port)) = value.rsplit_once(':')
1971 && !port.is_empty()
1972 {
1973 return Ok(HostAndMaybePort {
1974 host: host.to_string(),
1975 port: Some(port.parse::<u16>().map_err(|e| e.to_string())?),
1976 });
1977 }
1978
1979 Ok(HostAndMaybePort { host: value.to_string(), port: None })
1980}
1981
1982fn parse_props(s: &str) -> Result<std::collections::BTreeMap<String, Vec<String>>, String> {
1983 let mut parts = s.split(DELIM_COMMA);
1984 let _backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
1985 let mut props = std::collections::BTreeMap::<String, Vec<String>>::new();
1986
1987 for part in parts {
1988 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid netdev option: {part}"))?;
1989 props.entry(key.to_string()).or_default().push(value.to_string());
1990 }
1991
1992 Ok(props)
1993}
1994
1995fn parse_netdev_props(s: &str, backend_name: &str) -> Result<std::collections::BTreeMap<String, Vec<String>>, String> {
1996 let actual = s.split(DELIM_COMMA).next().ok_or_else(|| "empty netdev argument".to_string())?;
1997 if actual != backend_name {
1998 return Err(format!("expected {backend_name} backend, got {actual}"));
1999 }
2000 parse_props(s)
2001}
2002
2003fn required_prop<'a>(props: &'a std::collections::BTreeMap<String, Vec<String>>, key: &str) -> Result<&'a str, String> {
2004 props
2005 .get(key)
2006 .and_then(|values| values.first())
2007 .map(|s| s.as_str())
2008 .ok_or_else(|| format!("missing required option: {key}"))
2009}
2010
2011fn ensure_prop_value(props: &std::collections::BTreeMap<String, Vec<String>>, key: &str, expected: &str) -> Result<(), String> {
2012 let actual = required_prop(props, key)?;
2013 if actual == expected { Ok(()) } else { Err(format!("expected {key}={expected}, got {actual}")) }
2014}
2015
2016fn first_prop<'a>(props: &'a std::collections::BTreeMap<String, Vec<String>>, key: &str) -> Option<&'a str> {
2017 props.get(key).and_then(|values| values.first()).map(|s| s.as_str())
2018}
2019
2020fn all_props<'a>(props: &'a std::collections::BTreeMap<String, Vec<String>>, key: &str) -> Vec<&'a str> {
2021 props.get(key).map(|values| values.iter().map(|s| s.as_str()).collect()).unwrap_or_default()
2022}
2023
2024fn push_opt_string(args: &mut Vec<String>, key: &str, value: &Option<String>) {
2025 if let Some(value) = value {
2026 args.push(format!("{key}={value}"));
2027 }
2028}
2029
2030fn push_opt_onoff(args: &mut Vec<String>, key: &str, value: &Option<OnOff>) {
2031 if let Some(value) = value {
2032 args.push(format!("{key}={}", value.to_arg()));
2033 }
2034}
2035
2036fn parse_optional_onoff(value: Option<&str>) -> Result<Option<OnOff>, String> {
2037 value.map(|raw| raw.parse::<OnOff>().map_err(|_| format!("invalid on/off value: {raw}"))).transpose()
2038}
2039
2040fn parse_optional_u64(value: Option<&str>) -> Result<Option<u64>, String> {
2041 value.map(|raw| raw.parse::<u64>().map_err(|e| e.to_string())).transpose()
2042}
2043
2044fn parse_optional_u16(value: Option<&str>) -> Result<Option<u16>, String> {
2045 value.map(|raw| raw.parse::<u16>().map_err(|e| e.to_string())).transpose()
2046}
2047
2048fn parse_optional_ipv4(value: Option<&str>) -> Result<Option<Ipv4Addr>, String> {
2049 value.map(|raw| raw.parse::<Ipv4Addr>().map_err(|e| e.to_string())).transpose()
2050}
2051
2052fn parse_optional_ipv6(value: Option<&str>) -> Result<Option<Ipv6Addr>, String> {
2053 value.map(|raw| raw.parse::<Ipv6Addr>().map_err(|e| e.to_string())).transpose()
2054}
2055
2056fn parse_optional_qipv4net(value: Option<&str>) -> Result<Option<QIpv4Net>, String> {
2057 value
2058 .map(|raw| raw.parse::<ipnet::Ipv4Net>().map(|ip| QIpv4Net::builder().ip(ip).build()).map_err(|e| e.to_string()))
2059 .transpose()
2060}
2061
2062fn parse_optional_qipv6net(value: Option<&str>) -> Result<Option<QIpv6Net>, String> {
2063 value
2064 .map(|raw| raw.parse::<ipnet::Ipv6Net>().map(|ip| QIpv6Net::builder().ip(ip).build()).map_err(|e| e.to_string()))
2065 .transpose()
2066}
2067
2068fn parse_hostfwd(value: &str) -> Result<HostForward, String> {
2069 let (protocol, rest) = if let Some(rest) = value.strip_prefix("tcp:") {
2070 (Some(TcpUdp::Tcp), rest)
2071 } else if let Some(rest) = value.strip_prefix("udp:") {
2072 (Some(TcpUdp::Udp), rest)
2073 } else if let Some(rest) = value.strip_prefix("unix:") {
2074 (Some(TcpUdp::Unix), rest)
2075 } else {
2076 (None, value)
2077 };
2078
2079 if matches!(protocol, Some(TcpUdp::Unix)) {
2080 let (hostpath, guest_range) = rest.split_once('-').ok_or_else(|| format!("invalid hostfwd: {value}"))?;
2081 let (guestaddr, guestport) = parse_guest_host_port(guest_range)?;
2082 return Ok(HostForward {
2083 protocol,
2084 hostaddr: None,
2085 hostport: None,
2086 hostpath: Some(hostpath.to_string()),
2087 guestaddr,
2088 guestport,
2089 });
2090 }
2091
2092 let (host_range, guest_range) = rest.split_once('-').ok_or_else(|| format!("invalid hostfwd: {value}"))?;
2093 let (hostaddr, hostport) = parse_optional_host_port_range(host_range)?;
2094 let (guestaddr, guestport) = parse_guest_host_port(guest_range)?;
2095
2096 Ok(HostForward {
2097 protocol,
2098 hostaddr,
2099 hostport: Some(hostport),
2100 hostpath: None,
2101 guestaddr,
2102 guestport,
2103 })
2104}
2105
2106fn parse_optional_host_port_range(value: &str) -> Result<(Option<String>, u16), String> {
2107 if let Some((host, port)) = value.rsplit_once(':') {
2108 Ok(((!host.is_empty()).then_some(host.to_string()), port.parse::<u16>().map_err(|e| e.to_string())?))
2109 } else {
2110 Ok((None, value.parse::<u16>().map_err(|e| e.to_string())?))
2111 }
2112}
2113
2114fn parse_guest_host_port(value: &str) -> Result<(Option<String>, u16), String> {
2115 if let Some((host, port)) = value.rsplit_once(':') {
2116 Ok(((!host.is_empty()).then_some(host.to_string()), port.parse::<u16>().map_err(|e| e.to_string())?))
2117 } else {
2118 Ok((None, value.parse::<u16>().map_err(|e| e.to_string())?))
2119 }
2120}
2121
2122fn parse_guestfwd(value: &str) -> Result<GuestForward, String> {
2123 let mut parts = value.split(':');
2124 let proto = parts.next().ok_or_else(|| format!("invalid guestfwd: {value}"))?;
2125 if proto != "tcp" {
2126 return Err(format!("unsupported guestfwd protocol: {proto}"));
2127 }
2128 let server = parts.next().ok_or_else(|| format!("invalid guestfwd: {value}"))?.to_string();
2129 let port = parts.next().ok_or_else(|| format!("invalid guestfwd: {value}"))?.parse::<u16>().map_err(|e| e.to_string())?;
2130 let target = parts.collect::<Vec<_>>().join(":");
2131 if target.is_empty() {
2132 return Err(format!("invalid guestfwd target: {value}"));
2133 }
2134
2135 let target = if let Some(device) = target.strip_prefix("device=") {
2136 GuestForwardTarget::Device(device.parse::<CharDev>().map_err(|e| format!("invalid guestfwd device: {e}"))?)
2137 } else if let Some(command) = target.strip_prefix("cmd:") {
2138 let mut tokens = command.split_whitespace();
2139 let cmd = tokens.next().ok_or_else(|| format!("invalid guestfwd command: {value}"))?.to_string();
2140 let args = tokens.map(|token| token.to_string()).collect();
2141 GuestForwardTarget::Cmd((cmd, args))
2142 } else {
2143 return Err(format!("unsupported guestfwd target: {target}"));
2144 };
2145
2146 Ok(GuestForward { server, port, target })
2147}