Skip to main content

qemu_command_builder/args/
netdev.rs

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/// A QEMU `-netdev` backend.
15///
16/// The parser supports the same canonical comma-separated forms that the
17/// formatter emits for the implemented backend variants in this crate,
18/// including `tap`, `bridge`, `socket`, `vhost-user`, `vhost-vdpa`, and
19/// `hubport`.
20#[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<usize>,
288    vnet_hdr: Option<OnOff>,
289    vhost: Option<OnOff>,
290    vhostfd: Option<String>,
291    vhostforce: Option<OnOff>,
292    queues: Option<usize>,
293    poll_us: Option<usize>,
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::<usize>().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::<usize>().map_err(|e| e.to_string())?),
390                "poll_us" => poll_us = Some(value.parse::<usize>().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/// A `-netdev bridge,...` backend.
416#[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
423impl ToCommand for Bridge {
424    fn to_args(&self) -> Vec<String> {
425        let mut args = vec!["bridge".to_string(), format!("id={}", self.id)];
426
427        if let Some(br) = &self.bridge {
428            args.push(format!("br={}", br));
429        }
430        if let Some(helper) = &self.helper {
431            args.push(format!("helper={}", helper));
432        }
433        vec![args.join(DELIM_COMMA)]
434    }
435}
436
437impl FromStr for Bridge {
438    type Err = String;
439
440    fn from_str(s: &str) -> Result<Self, Self::Err> {
441        let mut parts = s.split(',');
442        let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
443        if backend != "bridge" {
444            return Err(format!("expected bridge backend, got {backend}"));
445        }
446
447        let mut id = None;
448        let mut bridge = None;
449        let mut helper = None;
450
451        for part in parts {
452            let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid bridge option: {part}"))?;
453            match key {
454                "id" => id = Some(value.to_string()),
455                "br" => bridge = Some(value.to_string()),
456                "helper" => helper = Some(value.to_string()),
457                other => return Err(format!("unsupported bridge option: {other}")),
458            }
459        }
460
461        Ok(Self {
462            id: id.ok_or_else(|| "bridge netdev requires id=".to_string())?,
463            bridge,
464            helper,
465        })
466    }
467}
468
469/// A host and port pair used by socket-based `-netdev` backends.
470#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
471pub struct HostAndPort {
472    host: String,
473    port: u16,
474}
475/// A host and optional port used by `listen=` socket endpoints.
476#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
477pub struct HostAndMaybePort {
478    host: String,
479    port: Option<u16>,
480}
481/// A `-netdev socket,...` backend using `listen=` and/or `connect=`.
482#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
483pub struct SocketRegular {
484    id: String,
485    fd: Option<String>,
486    listen: Option<HostAndMaybePort>,
487    connection: Option<HostAndPort>,
488}
489
490impl ToCommand for SocketRegular {
491    fn to_args(&self) -> Vec<String> {
492        let mut args = vec!["socket".to_string(), format!("id={}", self.id)];
493
494        if let Some(fd) = &self.fd {
495            args.push(format!("fd={}", fd));
496        }
497        if let Some(listen) = &self.listen {
498            if let Some(port) = &listen.port {
499                args.push(format!("listen={}:{}", listen.host, port));
500            } else {
501                args.push(format!("listen={}", listen.host));
502            }
503        }
504        if let Some(connection) = &self.connection {
505            args.push(format!("connect={}:{}", connection.host, connection.port));
506        }
507
508        vec![args.join(DELIM_COMMA)]
509    }
510}
511
512impl FromStr for SocketRegular {
513    type Err = String;
514
515    fn from_str(s: &str) -> Result<Self, Self::Err> {
516        let mut parts = s.split(',');
517        let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
518        if backend != "socket" {
519            return Err(format!("expected socket backend, got {backend}"));
520        }
521
522        let mut id = None;
523        let mut fd = None;
524        let mut listen = None;
525        let mut connection = None;
526
527        for part in parts {
528            let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid socket option: {part}"))?;
529            match key {
530                "id" => id = Some(value.to_string()),
531                "fd" => fd = Some(value.to_string()),
532                "listen" => listen = Some(parse_host_and_maybe_port(value)?),
533                "connect" => connection = Some(parse_host_and_port(value)?),
534                "mcast" | "udp" | "localaddr" => return Err(format!("socket variant is not regular: {part}")),
535                other => return Err(format!("unsupported socket option: {other}")),
536            }
537        }
538
539        Ok(Self {
540            id: id.ok_or_else(|| "socket netdev requires id=".to_string())?,
541            fd,
542            listen,
543            connection,
544        })
545    }
546}
547
548/// A `-netdev socket,...` multicast backend using `mcast=`.
549#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
550pub struct SocketMulticast {
551    id: String,
552    fd: Option<String>,
553    mcast: Option<HostAndPort>,
554    localaddr: Option<String>,
555}
556
557impl ToCommand for SocketMulticast {
558    fn to_args(&self) -> Vec<String> {
559        let mut args = vec!["socket".to_string(), format!("id={}", self.id)];
560
561        if let Some(fd) = &self.fd {
562            args.push(format!("fd={}", fd));
563        }
564        if let Some(mcast) = &self.mcast {
565            args.push(format!("mcast={}:{}", mcast.host, mcast.port));
566        }
567        if let Some(localaddr) = &self.localaddr {
568            args.push(format!("localaddr={}", localaddr));
569        }
570        vec![args.join(DELIM_COMMA)]
571    }
572}
573
574impl FromStr for SocketMulticast {
575    type Err = String;
576
577    fn from_str(s: &str) -> Result<Self, Self::Err> {
578        let mut parts = s.split(',');
579        let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
580        if backend != "socket" {
581            return Err(format!("expected socket backend, got {backend}"));
582        }
583
584        let mut id = None;
585        let mut fd = None;
586        let mut mcast = None;
587        let mut localaddr = None;
588
589        for part in parts {
590            let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid socket option: {part}"))?;
591            match key {
592                "id" => id = Some(value.to_string()),
593                "fd" => fd = Some(value.to_string()),
594                "mcast" => mcast = Some(parse_host_and_port(value)?),
595                "localaddr" => localaddr = Some(value.to_string()),
596                "listen" | "connect" | "udp" => return Err(format!("socket variant is not multicast: {part}")),
597                other => return Err(format!("unsupported socket option: {other}")),
598            }
599        }
600
601        Ok(Self {
602            id: id.ok_or_else(|| "socket netdev requires id=".to_string())?,
603            fd,
604            mcast,
605            localaddr,
606        })
607    }
608}
609
610/// A `-netdev socket,...` UDP tunnel backend using `udp=`.
611#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
612pub struct SocketUdpTunnel {
613    id: String,
614    fd: Option<String>,
615    udp: Option<HostAndPort>,
616    localaddr: Option<HostAndPort>,
617}
618
619impl ToCommand for SocketUdpTunnel {
620    fn to_args(&self) -> Vec<String> {
621        let mut args = vec!["socket".to_string(), format!("id={}", self.id)];
622
623        if let Some(fd) = &self.fd {
624            args.push(format!("fd={}", fd));
625        }
626        if let Some(udp) = &self.udp {
627            args.push(format!("udp={}:{}", udp.host, udp.port));
628        }
629        if let Some(localaddr) = &self.localaddr {
630            args.push(format!("localaddr={}:{}", localaddr.host, localaddr.port));
631        }
632        vec![args.join(DELIM_COMMA)]
633    }
634}
635
636impl FromStr for SocketUdpTunnel {
637    type Err = String;
638
639    fn from_str(s: &str) -> Result<Self, Self::Err> {
640        let mut parts = s.split(',');
641        let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
642        if backend != "socket" {
643            return Err(format!("expected socket backend, got {backend}"));
644        }
645
646        let mut id = None;
647        let mut fd = None;
648        let mut udp = None;
649        let mut localaddr = None;
650
651        for part in parts {
652            let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid socket option: {part}"))?;
653            match key {
654                "id" => id = Some(value.to_string()),
655                "fd" => fd = Some(value.to_string()),
656                "udp" => udp = Some(parse_host_and_port(value)?),
657                "localaddr" => localaddr = Some(parse_host_and_port(value)?),
658                "listen" | "connect" | "mcast" => return Err(format!("socket variant is not udp tunnel: {part}")),
659                other => return Err(format!("unsupported socket option: {other}")),
660            }
661        }
662
663        Ok(Self {
664            id: id.ok_or_else(|| "socket netdev requires id=".to_string())?,
665            fd,
666            udp,
667            localaddr,
668        })
669    }
670}
671
672/// The `socket` backend variants supported by this crate.
673#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
674pub enum Socket {
675    SocketRegular(SocketRegular),
676    Multicast(SocketMulticast),
677    UDPTunnel(SocketUdpTunnel),
678}
679
680impl ToCommand for Socket {
681    fn to_args(&self) -> Vec<String> {
682        match self {
683            Socket::SocketRegular(s) => s.to_args(),
684            Socket::Multicast(s) => s.to_args(),
685            Socket::UDPTunnel(s) => s.to_args(),
686        }
687    }
688}
689
690impl FromStr for Socket {
691    type Err = String;
692
693    fn from_str(s: &str) -> Result<Self, Self::Err> {
694        if s.contains(",mcast=") || s.starts_with("socket,mcast=") {
695            return Ok(Self::Multicast(s.parse::<SocketMulticast>()?));
696        }
697        if s.contains(",udp=") || s.starts_with("socket,udp=") {
698            return Ok(Self::UDPTunnel(s.parse::<SocketUdpTunnel>()?));
699        }
700        Ok(Self::SocketRegular(s.parse::<SocketRegular>()?))
701    }
702}
703
704#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
705pub struct StreamOverTcp {
706    id: String,
707    server: Option<OnOff>,
708    addr_host: String,
709    addr_port: u16,
710    to: Option<u16>,
711    numeric: Option<OnOff>,
712    keep_alive: Option<OnOff>,
713    mptcp: Option<OnOff>,
714    addr_ipv4: Option<OnOff>,
715    addr_ipv6: Option<OnOff>,
716    reconnect_ms: Option<usize>,
717}
718
719impl ToCommand for StreamOverTcp {
720    fn to_args(&self) -> Vec<String> {
721        let mut args = vec!["stream".to_string(), format!("id={}", self.id)];
722
723        if let Some(server) = &self.server {
724            args.push(format!("server={}", server.to_arg()));
725        }
726        args.push("addr.type=inet".to_string());
727        args.push(format!("addr.host={}", self.addr_host));
728        args.push(format!("addr.port={}", self.addr_port));
729        if let Some(to) = &self.to {
730            args.push(format!("to={}", to));
731        }
732        if let Some(numeric) = &self.numeric {
733            args.push(format!("numeric={}", numeric.to_arg()));
734        }
735        if let Some(keep_alive) = &self.keep_alive {
736            args.push(format!("keep-alive={}", keep_alive.to_arg()));
737        }
738        if let Some(mptcp) = &self.mptcp {
739            args.push(format!("mptcp={}", mptcp.to_arg()));
740        }
741        if let Some(ipv4) = &self.addr_ipv4 {
742            args.push(format!("addr.ipv4={}", ipv4.to_arg()));
743        }
744        if let Some(ipv6) = &self.addr_ipv6 {
745            args.push(format!("addr.ipv6={}", ipv6.to_arg()));
746        }
747        if let Some(reconnect_ms) = self.reconnect_ms {
748            args.push(format!("reconnect-ms={}", reconnect_ms));
749        }
750        vec![args.join(DELIM_COMMA)]
751    }
752}
753
754impl FromStr for StreamOverTcp {
755    type Err = String;
756
757    fn from_str(s: &str) -> Result<Self, Self::Err> {
758        let props = parse_netdev_props(s, "stream")?;
759        ensure_prop_value(&props, "addr.type", "inet")?;
760        Ok(Self {
761            id: required_prop(&props, "id")?.to_string(),
762            server: parse_optional_onoff(first_prop(&props, "server"))?,
763            addr_host: required_prop(&props, "addr.host")?.to_string(),
764            addr_port: required_prop(&props, "addr.port")?.parse::<u16>().map_err(|e| e.to_string())?,
765            to: parse_optional_u16(first_prop(&props, "to"))?,
766            numeric: parse_optional_onoff(first_prop(&props, "numeric"))?,
767            keep_alive: parse_optional_onoff(first_prop(&props, "keep-alive"))?,
768            mptcp: parse_optional_onoff(first_prop(&props, "mptcp"))?,
769            addr_ipv4: parse_optional_onoff(first_prop(&props, "addr.ipv4"))?,
770            addr_ipv6: parse_optional_onoff(first_prop(&props, "addr.ipv6"))?,
771            reconnect_ms: parse_optional_usize(first_prop(&props, "reconnect-ms"))?,
772        })
773    }
774}
775
776#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
777pub struct StreamOverUds {
778    id: String,
779    server: Option<OnOff>,
780    addr_path: String,
781    abstract_arg: Option<OnOff>,
782    tight: Option<OnOff>,
783    reconnect_ms: Option<usize>,
784}
785
786impl ToCommand for StreamOverUds {
787    fn to_args(&self) -> Vec<String> {
788        let mut args = vec!["stream".to_string(), format!("id={}", self.id)];
789
790        if let Some(server) = &self.server {
791            args.push(format!("server={}", server.to_arg()));
792        }
793        args.push("addr.type=unix".to_string());
794        args.push(format!("addr.path={}", self.addr_path));
795        if let Some(abstract_arg) = &self.abstract_arg {
796            args.push(format!("abstract={}", abstract_arg.to_arg()));
797        }
798        if let Some(tight) = &self.tight {
799            args.push(format!("tight={}", tight.to_arg()));
800        }
801        if let Some(reconnect_ms) = self.reconnect_ms {
802            args.push(format!("reconnect-ms={}", reconnect_ms));
803        }
804        vec![args.join(DELIM_COMMA)]
805    }
806}
807
808impl FromStr for StreamOverUds {
809    type Err = String;
810
811    fn from_str(s: &str) -> Result<Self, Self::Err> {
812        let props = parse_netdev_props(s, "stream")?;
813        ensure_prop_value(&props, "addr.type", "unix")?;
814        Ok(Self {
815            id: required_prop(&props, "id")?.to_string(),
816            server: parse_optional_onoff(first_prop(&props, "server"))?,
817            addr_path: required_prop(&props, "addr.path")?.to_string(),
818            abstract_arg: parse_optional_onoff(first_prop(&props, "abstract"))?,
819            tight: parse_optional_onoff(first_prop(&props, "tight"))?,
820            reconnect_ms: parse_optional_usize(first_prop(&props, "reconnect-ms"))?,
821        })
822    }
823}
824
825#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
826pub struct StreamOverFd {
827    id: String,
828    server: Option<OnOff>,
829    addr_str: String,
830    reconnect_ms: Option<usize>,
831}
832
833impl ToCommand for StreamOverFd {
834    fn to_args(&self) -> Vec<String> {
835        let mut args = vec!["stream".to_string(), format!("id={}", self.id)];
836
837        if let Some(server) = &self.server {
838            args.push(format!("server={}", server.to_arg()));
839        }
840        args.push("addr.type=fd".to_string());
841        args.push(format!("addr.str={}", self.addr_str));
842        if let Some(reconnect_ms) = self.reconnect_ms {
843            args.push(format!("reconnect-ms={}", reconnect_ms));
844        }
845        vec![args.join(DELIM_COMMA)]
846    }
847}
848
849impl FromStr for StreamOverFd {
850    type Err = String;
851
852    fn from_str(s: &str) -> Result<Self, Self::Err> {
853        let props = parse_netdev_props(s, "stream")?;
854        ensure_prop_value(&props, "addr.type", "fd")?;
855        Ok(Self {
856            id: required_prop(&props, "id")?.to_string(),
857            server: parse_optional_onoff(first_prop(&props, "server"))?,
858            addr_str: required_prop(&props, "addr.str")?.to_string(),
859            reconnect_ms: parse_optional_usize(first_prop(&props, "reconnect-ms"))?,
860        })
861    }
862}
863
864#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
865pub enum Stream {
866    StreamOverTcp(StreamOverTcp),
867    StreamOverUds(StreamOverUds),
868    StreamOverFd(StreamOverFd),
869}
870
871impl ToCommand for Stream {
872    fn to_args(&self) -> Vec<String> {
873        match self {
874            Stream::StreamOverTcp(s) => s.to_args(),
875            Stream::StreamOverUds(s) => s.to_args(),
876            Stream::StreamOverFd(s) => s.to_args(),
877        }
878    }
879}
880
881impl FromStr for Stream {
882    type Err = String;
883
884    fn from_str(s: &str) -> Result<Self, Self::Err> {
885        let props = parse_props(s)?;
886        match first_prop(&props, "addr.type") {
887            Some("inet") => Ok(Self::StreamOverTcp(s.parse::<StreamOverTcp>()?)),
888            Some("unix") => Ok(Self::StreamOverUds(s.parse::<StreamOverUds>()?)),
889            Some("fd") => Ok(Self::StreamOverFd(s.parse::<StreamOverFd>()?)),
890            other => Err(format!("unsupported stream addr.type: {}", other.unwrap_or("<missing>"))),
891        }
892    }
893}
894
895#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
896pub struct DgramMulticast {
897    id: String,
898    remote_host: String,
899    remote_port: u16,
900    local_host: Option<String>,
901}
902
903impl ToCommand for DgramMulticast {
904    fn to_args(&self) -> Vec<String> {
905        let mut args = vec![
906            "dgram".to_string(),
907            format!("id={}", self.id.to_string()),
908            "remote.type=inet".to_string(),
909            format!("remote.host={}", self.remote_host),
910            format!("remote.port={}", self.remote_port),
911        ];
912
913        if let Some(local_host) = &self.local_host {
914            args.push("local.type=inet".to_string());
915            args.push(format!("local.host={}", local_host));
916        }
917        vec![args.join(DELIM_COMMA)]
918    }
919}
920
921impl FromStr for DgramMulticast {
922    type Err = String;
923
924    fn from_str(s: &str) -> Result<Self, Self::Err> {
925        let props = parse_netdev_props(s, "dgram")?;
926        ensure_prop_value(&props, "remote.type", "inet")?;
927        Ok(Self {
928            id: required_prop(&props, "id")?.to_string(),
929            remote_host: required_prop(&props, "remote.host")?.to_string(),
930            remote_port: required_prop(&props, "remote.port")?.parse::<u16>().map_err(|e| e.to_string())?,
931            local_host: first_prop(&props, "local.host").map(ToString::to_string),
932        })
933    }
934}
935
936#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
937pub struct DgramMulticastUdpFd {
938    id: String,
939    remote_host: String,
940    remote_port: u16,
941    local_str: Option<String>,
942}
943
944impl ToCommand for DgramMulticastUdpFd {
945    fn to_args(&self) -> Vec<String> {
946        let mut args = vec![
947            "dgram".to_string(),
948            format!("id={}", self.id.to_string()),
949            "remote.type=inet".to_string(),
950            format!("remote.host={}", self.remote_host),
951            format!("remote.port={}", self.remote_port),
952        ];
953
954        if let Some(local_str) = &self.local_str {
955            args.push("local.type=fd".to_string());
956            args.push(format!("local.str={}", local_str));
957        }
958        vec![args.join(DELIM_COMMA)]
959    }
960}
961
962impl FromStr for DgramMulticastUdpFd {
963    type Err = String;
964
965    fn from_str(s: &str) -> Result<Self, Self::Err> {
966        let props = parse_netdev_props(s, "dgram")?;
967        ensure_prop_value(&props, "remote.type", "inet")?;
968        Ok(Self {
969            id: required_prop(&props, "id")?.to_string(),
970            remote_host: required_prop(&props, "remote.host")?.to_string(),
971            remote_port: required_prop(&props, "remote.port")?.parse::<u16>().map_err(|e| e.to_string())?,
972            local_str: first_prop(&props, "local.str").map(ToString::to_string),
973        })
974    }
975}
976
977#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
978pub struct DgramSocket {
979    id: String,
980    local_host: String,
981    local_port: usize,
982    remote_host: Option<String>,
983    remote_port: Option<u16>,
984}
985
986impl ToCommand for DgramSocket {
987    fn to_args(&self) -> Vec<String> {
988        let mut args = vec![
989            "dgram".to_string(),
990            format!("id={}", self.id.to_string()),
991            "local.type=inet".to_string(),
992            format!("local.host={}", self.local_host),
993            format!("local.port={}", self.local_port),
994        ];
995
996        if let Some(remote_host) = &self.remote_host {
997            args.push("remote.type=inet".to_string());
998            args.push(format!("remote.host={}", remote_host));
999        }
1000        if let Some(remote_port) = &self.remote_port {
1001            args.push(format!("remote.port={}", remote_port));
1002        }
1003        vec![args.join(DELIM_COMMA)]
1004    }
1005}
1006
1007impl FromStr for DgramSocket {
1008    type Err = String;
1009
1010    fn from_str(s: &str) -> Result<Self, Self::Err> {
1011        let props = parse_netdev_props(s, "dgram")?;
1012        ensure_prop_value(&props, "local.type", "inet")?;
1013        Ok(Self {
1014            id: required_prop(&props, "id")?.to_string(),
1015            local_host: required_prop(&props, "local.host")?.to_string(),
1016            local_port: required_prop(&props, "local.port")?.parse::<usize>().map_err(|e| e.to_string())?,
1017            remote_host: first_prop(&props, "remote.host").map(ToString::to_string),
1018            remote_port: parse_optional_u16(first_prop(&props, "remote.port"))?,
1019        })
1020    }
1021}
1022
1023#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1024pub struct DgramUds {
1025    id: String,
1026    local_path: PathBuf,
1027    remote_path: Option<PathBuf>,
1028}
1029
1030impl ToCommand for DgramUds {
1031    fn to_args(&self) -> Vec<String> {
1032        let mut args = vec![
1033            "dgram".to_string(),
1034            format!("id={}", self.id.to_string()),
1035            "local.type=unix".to_string(),
1036            format!("local.path={}", self.local_path.display()),
1037        ];
1038        if let Some(remote) = &self.remote_path {
1039            args.push("remote.type=unix".to_string());
1040            args.push(format!("remote.path={}", remote.display()));
1041        }
1042        vec![args.join(DELIM_COMMA)]
1043    }
1044}
1045
1046impl FromStr for DgramUds {
1047    type Err = String;
1048
1049    fn from_str(s: &str) -> Result<Self, Self::Err> {
1050        let props = parse_netdev_props(s, "dgram")?;
1051        ensure_prop_value(&props, "local.type", "unix")?;
1052        Ok(Self {
1053            id: required_prop(&props, "id")?.to_string(),
1054            local_path: PathBuf::from(required_prop(&props, "local.path")?),
1055            remote_path: first_prop(&props, "remote.path").map(PathBuf::from),
1056        })
1057    }
1058}
1059
1060#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1061pub struct DgramFd {
1062    id: String,
1063    local_str: String,
1064}
1065
1066impl ToCommand for DgramFd {
1067    fn to_args(&self) -> Vec<String> {
1068        vec![["dgram".to_string(), format!("id={}", self.id), "local.type=fd".to_string(), format!("local.str={}", self.local_str)].join(DELIM_COMMA)]
1069    }
1070}
1071
1072impl FromStr for DgramFd {
1073    type Err = String;
1074
1075    fn from_str(s: &str) -> Result<Self, Self::Err> {
1076        let props = parse_netdev_props(s, "dgram")?;
1077        ensure_prop_value(&props, "local.type", "fd")?;
1078        Ok(Self {
1079            id: required_prop(&props, "id")?.to_string(),
1080            local_str: required_prop(&props, "local.str")?.to_string(),
1081        })
1082    }
1083}
1084
1085#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
1086pub enum Dgram {
1087    DgramMulticast(DgramMulticast),
1088    DgramMulticastUdpFd(DgramMulticastUdpFd),
1089    DgramSocket(DgramSocket),
1090    DgramUds(DgramUds),
1091    DgramFd(DgramFd),
1092}
1093
1094impl ToCommand for Dgram {
1095    fn to_args(&self) -> Vec<String> {
1096        match self {
1097            Dgram::DgramMulticast(args) => args.to_args(),
1098            Dgram::DgramMulticastUdpFd(args) => args.to_args(),
1099            Dgram::DgramSocket(args) => args.to_args(),
1100            Dgram::DgramUds(args) => args.to_args(),
1101            Dgram::DgramFd(args) => args.to_args(),
1102        }
1103    }
1104}
1105
1106impl FromStr for Dgram {
1107    type Err = String;
1108
1109    fn from_str(s: &str) -> Result<Self, Self::Err> {
1110        let props = parse_props(s)?;
1111        match (first_prop(&props, "local.type"), first_prop(&props, "remote.type")) {
1112            (Some("fd"), Some("inet")) => Ok(Self::DgramMulticastUdpFd(s.parse::<DgramMulticastUdpFd>()?)),
1113            (Some("fd"), _) => Ok(Self::DgramFd(s.parse::<DgramFd>()?)),
1114            (Some("unix"), _) => Ok(Self::DgramUds(s.parse::<DgramUds>()?)),
1115            (Some("inet"), _) => Ok(Self::DgramSocket(s.parse::<DgramSocket>()?)),
1116            (None, Some("inet")) => Ok(Self::DgramMulticast(s.parse::<DgramMulticast>()?)),
1117            _ => Err(format!("unsupported dgram backend: {s}")),
1118        }
1119    }
1120}
1121
1122#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1123pub struct Vde {
1124    id: String,
1125    sock: Option<PathBuf>,
1126    port: Option<u16>,
1127    group: Option<String>,
1128    mode: Option<String>,
1129}
1130
1131impl ToCommand for Vde {
1132    fn to_args(&self) -> Vec<String> {
1133        let mut args = vec!["vde".to_string(), format!("id={}", self.id.to_string())];
1134
1135        if let Some(sock) = &self.sock {
1136            args.push(format!("sock={}", sock.display()));
1137        }
1138        if let Some(port) = self.port {
1139            args.push(format!("port={}", port));
1140        }
1141        if let Some(group) = &self.group {
1142            args.push(format!("group={}", group));
1143        }
1144        if let Some(mode) = &self.mode {
1145            args.push(format!("mode={}", mode));
1146        }
1147        vec![args.join(DELIM_COMMA)]
1148    }
1149}
1150
1151impl FromStr for Vde {
1152    type Err = String;
1153
1154    fn from_str(s: &str) -> Result<Self, Self::Err> {
1155        let props = parse_netdev_props(s, "vde")?;
1156        Ok(Self {
1157            id: required_prop(&props, "id")?.to_string(),
1158            sock: first_prop(&props, "sock").map(PathBuf::from),
1159            port: parse_optional_u16(first_prop(&props, "port"))?,
1160            group: first_prop(&props, "group").map(ToString::to_string),
1161            mode: first_prop(&props, "mode").map(ToString::to_string),
1162        })
1163    }
1164}
1165
1166#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1167pub struct NetMap {
1168    id: String,
1169    ifname: String,
1170    devname: Option<String>,
1171}
1172
1173impl ToCommand for NetMap {
1174    fn to_args(&self) -> Vec<String> {
1175        let mut args = vec!["netmap".to_string(), format!("id={}", self.id.to_string())];
1176        args.push(format!("ifname={}", self.ifname));
1177        if let Some(devname) = &self.devname {
1178            args.push(format!("devname={}", devname));
1179        }
1180        vec![args.join(DELIM_COMMA)]
1181    }
1182}
1183
1184impl FromStr for NetMap {
1185    type Err = String;
1186
1187    fn from_str(s: &str) -> Result<Self, Self::Err> {
1188        let props = parse_netdev_props(s, "netmap")?;
1189        Ok(Self {
1190            id: required_prop(&props, "id")?.to_string(),
1191            ifname: required_prop(&props, "ifname")?.to_string(),
1192            devname: first_prop(&props, "devname").map(ToString::to_string),
1193        })
1194    }
1195}
1196
1197#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
1198pub enum NativeSkb {
1199    Native,
1200    Skb,
1201}
1202
1203impl ToArg for NativeSkb {
1204    fn to_arg(&self) -> &str {
1205        match self {
1206            NativeSkb::Native => "native",
1207            NativeSkb::Skb => "skb",
1208        }
1209    }
1210}
1211#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1212pub struct AfXdp {
1213    id: String,
1214    ifname: String,
1215    mode: Option<NativeSkb>,
1216    force_copy: Option<OnOff>,
1217    queues: Option<usize>,
1218    start_queue: Option<usize>,
1219    inhibit: Option<OnOff>,
1220    sock_fds: Option<Vec<String>>,
1221    map_path: Option<PathBuf>,
1222    map_start_index: Option<usize>,
1223}
1224
1225impl ToCommand for AfXdp {
1226    fn to_args(&self) -> Vec<String> {
1227        let mut args = vec!["af-xdp".to_string(), format!("id={}", self.id.to_string()), format!("ifname={}", self.ifname)];
1228
1229        if let Some(mode) = &self.mode {
1230            args.push(format!("mode={}", mode.to_arg()));
1231        }
1232        if let Some(force_copy) = &self.force_copy {
1233            args.push(format!("force-copy={}", force_copy.to_arg()));
1234        }
1235        if let Some(queues) = self.queues {
1236            args.push(format!("queues={}", queues));
1237        }
1238        if let Some(start_queue) = self.start_queue {
1239            args.push(format!("start-queue={}", start_queue));
1240        }
1241        if let Some(inhibit) = &self.inhibit {
1242            args.push(format!("inhibit={}", inhibit.to_arg()));
1243        }
1244        if let Some(sock_fds) = &self.sock_fds {
1245            args.push(format!("sock-fds={}", sock_fds.join(":")));
1246        }
1247        if let Some(map_path) = &self.map_path {
1248            args.push(format!("map-path={}", map_path.display()));
1249        }
1250        if let Some(map_start_index) = self.map_start_index {
1251            args.push(format!("map-start-index={}", map_start_index));
1252        }
1253        vec![args.join(DELIM_COMMA)]
1254    }
1255}
1256
1257impl FromStr for AfXdp {
1258    type Err = String;
1259
1260    fn from_str(s: &str) -> Result<Self, Self::Err> {
1261        let props = parse_netdev_props(s, "af-xdp")?;
1262        Ok(Self {
1263            id: required_prop(&props, "id")?.to_string(),
1264            ifname: required_prop(&props, "ifname")?.to_string(),
1265            mode: match first_prop(&props, "mode") {
1266                Some("native") => Some(NativeSkb::Native),
1267                Some("skb") => Some(NativeSkb::Skb),
1268                Some(other) => return Err(format!("invalid af-xdp mode: {other}")),
1269                None => None,
1270            },
1271            force_copy: parse_optional_onoff(first_prop(&props, "force-copy"))?,
1272            queues: parse_optional_usize(first_prop(&props, "queues"))?,
1273            start_queue: parse_optional_usize(first_prop(&props, "start-queue"))?,
1274            inhibit: parse_optional_onoff(first_prop(&props, "inhibit"))?,
1275            sock_fds: first_prop(&props, "sock-fds").map(|v| v.split(':').map(|part| part.to_string()).collect()),
1276            map_path: first_prop(&props, "map-path").map(PathBuf::from),
1277            map_start_index: parse_optional_usize(first_prop(&props, "map-start-index"))?,
1278        })
1279    }
1280}
1281
1282/// A `-netdev passt,...` backend.
1283#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1284pub struct Passt {
1285    id: String,
1286    path: Option<PathBuf>,
1287    quiet: Option<OnOff>,
1288    vhost_user: Option<OnOff>,
1289    mtu: Option<usize>,
1290    address: Option<String>,
1291    netmask: Option<String>,
1292    mac: Option<String>,
1293    gateway: Option<String>,
1294    interface: Option<String>,
1295    outbound: Option<String>,
1296    outbound_if4: Option<String>,
1297    outbound_if6: Option<String>,
1298    dns: Option<String>,
1299    search: Option<String>,
1300    fqdn: Option<String>,
1301    dhcp_dns: Option<OnOff>,
1302    dhcp_search: Option<OnOff>,
1303    map_host_loopback: Option<String>,
1304    map_guest_addr: Option<String>,
1305    dns_forward: Option<String>,
1306    dns_host: Option<String>,
1307    tcp: Option<OnOff>,
1308    udp: Option<OnOff>,
1309    icmp: Option<OnOff>,
1310    dhcp: Option<OnOff>,
1311    ndp: Option<OnOff>,
1312    dhcpv6: Option<OnOff>,
1313    ra: Option<OnOff>,
1314    freebind: Option<OnOff>,
1315    ipv4: Option<OnOff>,
1316    ipv6: Option<OnOff>,
1317    tcp_ports: Option<String>,
1318    udp_ports: Option<String>,
1319    param: Option<Vec<String>>,
1320}
1321
1322impl ToCommand for Passt {
1323    fn to_args(&self) -> Vec<String> {
1324        let mut args = vec!["passt".to_string(), format!("id={}", self.id)];
1325
1326        if let Some(path) = &self.path {
1327            args.push(format!("path={}", path.display()));
1328        }
1329        if let Some(quiet) = &self.quiet {
1330            args.push(format!("quiet={}", quiet.to_arg()));
1331        }
1332        if let Some(vhost_user) = &self.vhost_user {
1333            args.push(format!("vhost-user={}", vhost_user.to_arg()));
1334        }
1335        if let Some(mtu) = self.mtu {
1336            args.push(format!("mtu={}", mtu));
1337        }
1338        push_opt_string(&mut args, "address", &self.address);
1339        push_opt_string(&mut args, "netmask", &self.netmask);
1340        push_opt_string(&mut args, "mac", &self.mac);
1341        push_opt_string(&mut args, "gateway", &self.gateway);
1342        push_opt_string(&mut args, "interface", &self.interface);
1343        push_opt_string(&mut args, "outbound", &self.outbound);
1344        push_opt_string(&mut args, "outbound-if4", &self.outbound_if4);
1345        push_opt_string(&mut args, "outbound-if6", &self.outbound_if6);
1346        push_opt_string(&mut args, "dns", &self.dns);
1347        push_opt_string(&mut args, "search", &self.search);
1348        push_opt_string(&mut args, "fqdn", &self.fqdn);
1349        if let Some(dhcp_dns) = &self.dhcp_dns {
1350            args.push(format!("dhcp-dns={}", dhcp_dns.to_arg()));
1351        }
1352        if let Some(dhcp_search) = &self.dhcp_search {
1353            args.push(format!("dhcp-search={}", dhcp_search.to_arg()));
1354        }
1355        push_opt_string(&mut args, "map-host-loopback", &self.map_host_loopback);
1356        push_opt_string(&mut args, "map-guest-addr", &self.map_guest_addr);
1357        push_opt_string(&mut args, "dns-forward", &self.dns_forward);
1358        push_opt_string(&mut args, "dns-host", &self.dns_host);
1359        push_opt_onoff(&mut args, "tcp", &self.tcp);
1360        push_opt_onoff(&mut args, "udp", &self.udp);
1361        push_opt_onoff(&mut args, "icmp", &self.icmp);
1362        push_opt_onoff(&mut args, "dhcp", &self.dhcp);
1363        push_opt_onoff(&mut args, "ndp", &self.ndp);
1364        push_opt_onoff(&mut args, "dhcpv6", &self.dhcpv6);
1365        push_opt_onoff(&mut args, "ra", &self.ra);
1366        push_opt_onoff(&mut args, "freebind", &self.freebind);
1367        push_opt_onoff(&mut args, "ipv4", &self.ipv4);
1368        push_opt_onoff(&mut args, "ipv6", &self.ipv6);
1369        push_opt_string(&mut args, "tcp-ports", &self.tcp_ports);
1370        push_opt_string(&mut args, "udp-ports", &self.udp_ports);
1371        if let Some(params) = &self.param {
1372            for param in params {
1373                args.push(format!("param={}", param));
1374            }
1375        }
1376
1377        vec![args.join(DELIM_COMMA)]
1378    }
1379}
1380
1381impl FromStr for Passt {
1382    type Err = String;
1383
1384    fn from_str(s: &str) -> Result<Self, Self::Err> {
1385        let props = parse_netdev_props(s, "passt")?;
1386        Ok(Self {
1387            id: required_prop(&props, "id")?.to_string(),
1388            path: first_prop(&props, "path").map(PathBuf::from),
1389            quiet: parse_optional_onoff(first_prop(&props, "quiet"))?,
1390            vhost_user: parse_optional_onoff(first_prop(&props, "vhost-user"))?,
1391            mtu: parse_optional_usize(first_prop(&props, "mtu"))?,
1392            address: first_prop(&props, "address").map(ToString::to_string),
1393            netmask: first_prop(&props, "netmask").map(ToString::to_string),
1394            mac: first_prop(&props, "mac").map(ToString::to_string),
1395            gateway: first_prop(&props, "gateway").map(ToString::to_string),
1396            interface: first_prop(&props, "interface").map(ToString::to_string),
1397            outbound: first_prop(&props, "outbound").map(ToString::to_string),
1398            outbound_if4: first_prop(&props, "outbound-if4").map(ToString::to_string),
1399            outbound_if6: first_prop(&props, "outbound-if6").map(ToString::to_string),
1400            dns: first_prop(&props, "dns").map(ToString::to_string),
1401            search: first_prop(&props, "search").map(ToString::to_string),
1402            fqdn: first_prop(&props, "fqdn").map(ToString::to_string),
1403            dhcp_dns: parse_optional_onoff(first_prop(&props, "dhcp-dns"))?,
1404            dhcp_search: parse_optional_onoff(first_prop(&props, "dhcp-search"))?,
1405            map_host_loopback: first_prop(&props, "map-host-loopback").map(ToString::to_string),
1406            map_guest_addr: first_prop(&props, "map-guest-addr").map(ToString::to_string),
1407            dns_forward: first_prop(&props, "dns-forward").map(ToString::to_string),
1408            dns_host: first_prop(&props, "dns-host").map(ToString::to_string),
1409            tcp: parse_optional_onoff(first_prop(&props, "tcp"))?,
1410            udp: parse_optional_onoff(first_prop(&props, "udp"))?,
1411            icmp: parse_optional_onoff(first_prop(&props, "icmp"))?,
1412            dhcp: parse_optional_onoff(first_prop(&props, "dhcp"))?,
1413            ndp: parse_optional_onoff(first_prop(&props, "ndp"))?,
1414            dhcpv6: parse_optional_onoff(first_prop(&props, "dhcpv6"))?,
1415            ra: parse_optional_onoff(first_prop(&props, "ra"))?,
1416            freebind: parse_optional_onoff(first_prop(&props, "freebind"))?,
1417            ipv4: parse_optional_onoff(first_prop(&props, "ipv4"))?,
1418            ipv6: parse_optional_onoff(first_prop(&props, "ipv6"))?,
1419            tcp_ports: first_prop(&props, "tcp-ports").map(ToString::to_string),
1420            udp_ports: first_prop(&props, "udp-ports").map(ToString::to_string),
1421            param: {
1422                let params = all_props(&props, "param").into_iter().map(ToString::to_string).collect::<Vec<_>>();
1423                (!params.is_empty()).then_some(params)
1424            },
1425        })
1426    }
1427}
1428
1429/// A `-netdev vhost-user,...` backend.
1430#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1431pub struct VhostUser {
1432    id: String,
1433    chardev: String,
1434    vhostforce: Option<OnOff>,
1435    queues: Option<usize>,
1436}
1437
1438impl ToCommand for VhostUser {
1439    fn to_args(&self) -> Vec<String> {
1440        let mut args = vec!["vhost-user".to_string(), format!("id={}", self.id.to_string()), format!("chardev={}", self.chardev)];
1441
1442        if let Some(vhostforce) = &self.vhostforce {
1443            args.push(format!("vhostforce={}", vhostforce.to_arg()));
1444        }
1445        if let Some(queues) = self.queues {
1446            args.push(format!("queues={}", queues));
1447        }
1448        vec![args.join(DELIM_COMMA)]
1449    }
1450}
1451
1452impl FromStr for VhostUser {
1453    type Err = String;
1454
1455    fn from_str(s: &str) -> Result<Self, Self::Err> {
1456        let mut parts = s.split(',');
1457        let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
1458        if backend != "vhost-user" && backend != "type=vhost-user" {
1459            return Err(format!("expected vhost-user backend, got {backend}"));
1460        }
1461
1462        let mut id = None;
1463        let mut chardev = None;
1464        let mut vhostforce = None;
1465        let mut queues = None;
1466
1467        for part in parts {
1468            let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid vhost-user option: {part}"))?;
1469            match key {
1470                "id" => id = Some(value.to_string()),
1471                "chardev" => chardev = Some(value.to_string()),
1472                "vhostforce" => vhostforce = Some(value.parse::<OnOff>().map_err(|_| format!("invalid vhostforce value: {value}"))?),
1473                "queues" => queues = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
1474                other => return Err(format!("unsupported vhost-user option: {other}")),
1475            }
1476        }
1477
1478        Ok(Self {
1479            id: id.ok_or_else(|| "vhost-user netdev requires id=".to_string())?,
1480            chardev: chardev.ok_or_else(|| "vhost-user netdev requires chardev=".to_string())?,
1481            vhostforce,
1482            queues,
1483        })
1484    }
1485}
1486
1487/// A `-netdev vhost-vdpa,...` backend.
1488#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1489pub struct VhostVdpa {
1490    id: String,
1491    vhostdev: Option<PathBuf>,
1492    vhostfd: Option<String>,
1493}
1494
1495impl ToCommand for VhostVdpa {
1496    fn to_args(&self) -> Vec<String> {
1497        let mut args = vec!["vhost-vdpa".to_string(), format!("id={}", self.id.to_string())];
1498        if let Some(vhostdev) = &self.vhostdev {
1499            args.push(format!("vhostdev={}", vhostdev.to_str().unwrap()));
1500        }
1501        if let Some(vhostfd) = &self.vhostfd {
1502            args.push(format!("vhostfd={}", vhostfd));
1503        }
1504        vec![args.join(DELIM_COMMA)]
1505    }
1506}
1507
1508impl FromStr for VhostVdpa {
1509    type Err = String;
1510
1511    fn from_str(s: &str) -> Result<Self, Self::Err> {
1512        let mut parts = s.split(',');
1513        let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
1514        if backend != "vhost-vdpa" {
1515            return Err(format!("expected vhost-vdpa backend, got {backend}"));
1516        }
1517
1518        let mut id = None;
1519        let mut vhostdev = None;
1520        let mut vhostfd = None;
1521
1522        for part in parts {
1523            let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid vhost-vdpa option: {part}"))?;
1524            match key {
1525                "id" => id = Some(value.to_string()),
1526                "vhostdev" => vhostdev = Some(PathBuf::from(value)),
1527                "vhostfd" => vhostfd = Some(value.to_string()),
1528                other => return Err(format!("unsupported vhost-vdpa option: {other}")),
1529            }
1530        }
1531
1532        Ok(Self {
1533            id: id.ok_or_else(|| "vhost-vdpa netdev requires id=".to_string())?,
1534            vhostdev,
1535            vhostfd,
1536        })
1537    }
1538}
1539
1540#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1541pub struct VmnetHost {
1542    id: String,
1543    isolated: Option<OnOff>,
1544    net_uuid: Option<String>,
1545    start_address: Option<String>,
1546    end_address: Option<String>,
1547    subnet_mask: Option<String>,
1548}
1549
1550impl ToCommand for VmnetHost {
1551    fn to_args(&self) -> Vec<String> {
1552        let mut args = vec!["vmnet-host".to_string(), format!("id={}", self.id.to_string())];
1553
1554        if let Some(isolated) = &self.isolated {
1555            args.push(format!("isolated={}", isolated.to_arg()));
1556        }
1557        if let Some(net_uuid) = &self.net_uuid {
1558            args.push(format!("net_uuid={}", net_uuid));
1559        }
1560        if let Some(start_address) = &self.start_address {
1561            args.push(format!("start-address={}", start_address));
1562        }
1563        if let Some(end_address) = &self.end_address {
1564            args.push(format!("end-address={}", end_address));
1565        }
1566        if let Some(subnet_mask) = &self.subnet_mask {
1567            args.push(format!("subnet-mask={}", subnet_mask));
1568        }
1569        vec![args.join(DELIM_COMMA)]
1570    }
1571}
1572
1573impl FromStr for VmnetHost {
1574    type Err = String;
1575
1576    fn from_str(s: &str) -> Result<Self, Self::Err> {
1577        let props = parse_netdev_props(s, "vmnet-host")?;
1578        Ok(Self {
1579            id: required_prop(&props, "id")?.to_string(),
1580            isolated: parse_optional_onoff(first_prop(&props, "isolated"))?,
1581            net_uuid: first_prop(&props, "net_uuid").map(ToString::to_string),
1582            start_address: first_prop(&props, "start-address").map(ToString::to_string),
1583            end_address: first_prop(&props, "end-address").map(ToString::to_string),
1584            subnet_mask: first_prop(&props, "subnet-mask").map(ToString::to_string),
1585        })
1586    }
1587}
1588
1589#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1590pub struct VmnetShared {
1591    id: String,
1592    isolated: Option<OnOff>,
1593    nat66_prefix: Option<String>,
1594    start_address: Option<String>,
1595    end_address: Option<String>,
1596    subnet_mask: Option<String>,
1597}
1598
1599impl ToCommand for VmnetShared {
1600    fn to_args(&self) -> Vec<String> {
1601        let mut args = vec!["vmnet-shared".to_string(), format!("id={}", self.id.to_string())];
1602
1603        if let Some(isolated) = &self.isolated {
1604            args.push(format!("isolated={}", isolated.to_arg()));
1605        }
1606        if let Some(nat66_prefix) = &self.nat66_prefix {
1607            args.push(format!("nat66-prefix={}", nat66_prefix));
1608        }
1609        if let Some(start_address) = &self.start_address {
1610            args.push(format!("start-address={}", start_address));
1611        }
1612        if let Some(end_address) = &self.end_address {
1613            args.push(format!("end-address={}", end_address));
1614        }
1615        if let Some(subnet_mask) = &self.subnet_mask {
1616            args.push(format!("subnet-mask={}", subnet_mask));
1617        }
1618        vec![args.join(DELIM_COMMA)]
1619    }
1620}
1621
1622impl FromStr for VmnetShared {
1623    type Err = String;
1624
1625    fn from_str(s: &str) -> Result<Self, Self::Err> {
1626        let props = parse_netdev_props(s, "vmnet-shared")?;
1627        Ok(Self {
1628            id: required_prop(&props, "id")?.to_string(),
1629            isolated: parse_optional_onoff(first_prop(&props, "isolated"))?,
1630            nat66_prefix: first_prop(&props, "nat66-prefix").map(ToString::to_string),
1631            start_address: first_prop(&props, "start-address").map(ToString::to_string),
1632            end_address: first_prop(&props, "end-address").map(ToString::to_string),
1633            subnet_mask: first_prop(&props, "subnet-mask").map(ToString::to_string),
1634        })
1635    }
1636}
1637
1638#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1639pub struct VmnetBridged {
1640    id: String,
1641    ifname: String,
1642    isolated: Option<OnOff>,
1643}
1644
1645impl ToCommand for VmnetBridged {
1646    fn to_args(&self) -> Vec<String> {
1647        let mut args = vec!["vmnet-bridged".to_string(), format!("id={}", self.id.to_string()), format!("ifname={}", self.ifname)];
1648
1649        if let Some(isolated) = &self.isolated {
1650            args.push(format!("isolated={}", isolated.to_arg()));
1651        }
1652        vec![args.join(DELIM_COMMA)]
1653    }
1654}
1655
1656impl FromStr for VmnetBridged {
1657    type Err = String;
1658
1659    fn from_str(s: &str) -> Result<Self, Self::Err> {
1660        let props = parse_netdev_props(s, "vmnet-bridged")?;
1661        Ok(Self {
1662            id: required_prop(&props, "id")?.to_string(),
1663            ifname: required_prop(&props, "ifname")?.to_string(),
1664            isolated: parse_optional_onoff(first_prop(&props, "isolated"))?,
1665        })
1666    }
1667}
1668
1669/// A `-netdev hubport,...` backend.
1670#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
1671pub struct Hubport {
1672    id: String,
1673    hubid: usize,
1674    netdev: Option<String>,
1675}
1676
1677impl ToCommand for Hubport {
1678    fn to_args(&self) -> Vec<String> {
1679        let mut args = vec!["hubport".to_string(), format!("id={}", self.id.to_string()), format!("hubid={}", self.hubid)];
1680
1681        if let Some(netdev) = &self.netdev {
1682            args.push(format!("netdev={}", netdev));
1683        }
1684        vec![args.join(DELIM_COMMA)]
1685    }
1686}
1687
1688impl FromStr for Hubport {
1689    type Err = String;
1690
1691    fn from_str(s: &str) -> Result<Self, Self::Err> {
1692        let mut parts = s.split(',');
1693        let backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
1694        if backend != "hubport" {
1695            return Err(format!("expected hubport backend, got {backend}"));
1696        }
1697
1698        let mut id = None;
1699        let mut hubid = None;
1700        let mut netdev = None;
1701
1702        for part in parts {
1703            let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid hubport option: {part}"))?;
1704            match key {
1705                "id" => id = Some(value.to_string()),
1706                "hubid" => hubid = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
1707                "netdev" => netdev = Some(value.to_string()),
1708                other => return Err(format!("unsupported hubport option: {other}")),
1709            }
1710        }
1711
1712        Ok(Self {
1713            id: id.ok_or_else(|| "hubport netdev requires id=".to_string())?,
1714            hubid: hubid.ok_or_else(|| "hubport netdev requires hubid=".to_string())?,
1715            netdev,
1716        })
1717    }
1718}
1719
1720#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
1721pub enum NetDev {
1722    User(User),
1723    Passt(Passt),
1724    // TODO L2tpv3,
1725    Tap(Tap),
1726    Bridge(Bridge),
1727    Socket(Socket),
1728    Stream(Stream),
1729    Dgram(Dgram),
1730    Vde(Vde),
1731    Netmap(NetMap),
1732    AfXdp(AfXdp),
1733    VhostUser(VhostUser),
1734    VhostVdpa(VhostVdpa),
1735    VmnetHost(VmnetHost),
1736    VmnetShared(VmnetShared),
1737    VmnetBridged(VmnetBridged),
1738    Hubport(Hubport),
1739}
1740
1741impl ToCommand for NetDev {
1742    fn command(&self) -> String {
1743        ARG_NETDEV.to_string()
1744    }
1745    fn to_args(&self) -> Vec<String> {
1746        match self {
1747            NetDev::User(user) => user.to_args(),
1748            NetDev::Passt(passt) => passt.to_args(),
1749            //NetDev::L2tpv3 => {}
1750            NetDev::Tap(tap) => tap.to_args(),
1751            NetDev::Bridge(bridge) => bridge.to_args(),
1752            NetDev::Socket(socket) => socket.to_args(),
1753            NetDev::Stream(stream) => stream.to_args(),
1754            NetDev::Dgram(dgram) => dgram.to_args(),
1755            NetDev::Vde(vde) => vde.to_args(),
1756            NetDev::Netmap(netmap) => netmap.to_args(),
1757            NetDev::AfXdp(af_xdp) => af_xdp.to_args(),
1758            NetDev::VhostUser(vhost_user) => vhost_user.to_args(),
1759            NetDev::VhostVdpa(vhost_vdpa) => vhost_vdpa.to_args(),
1760            NetDev::VmnetHost(vmnet_host) => vmnet_host.to_args(),
1761            NetDev::VmnetShared(vmnet_shared) => vmnet_shared.to_args(),
1762            NetDev::VmnetBridged(vmnet_bridged) => vmnet_bridged.to_args(),
1763            NetDev::Hubport(hubport) => hubport.to_args(),
1764        }
1765    }
1766}
1767
1768impl FromStr for NetDev {
1769    type Err = String;
1770
1771    fn from_str(s: &str) -> Result<Self, Self::Err> {
1772        if s.starts_with("user,") || s == "user" {
1773            return Ok(Self::User(s.parse::<User>()?));
1774        }
1775        if s.starts_with("passt,") || s == "passt" {
1776            return Ok(Self::Passt(s.parse::<Passt>()?));
1777        }
1778        if s.starts_with("tap,") || s == "tap" {
1779            return Ok(Self::Tap(s.parse::<Tap>()?));
1780        }
1781        if s.starts_with("bridge,") || s == "bridge" {
1782            return Ok(Self::Bridge(s.parse::<Bridge>()?));
1783        }
1784        if s.starts_with("socket,") || s == "socket" {
1785            return Ok(Self::Socket(s.parse::<Socket>()?));
1786        }
1787        if s.starts_with("stream,") || s == "stream" {
1788            return Ok(Self::Stream(s.parse::<Stream>()?));
1789        }
1790        if s.starts_with("dgram,") || s == "dgram" {
1791            return Ok(Self::Dgram(s.parse::<Dgram>()?));
1792        }
1793        if s.starts_with("vde,") || s == "vde" {
1794            return Ok(Self::Vde(s.parse::<Vde>()?));
1795        }
1796        if s.starts_with("netmap,") || s == "netmap" {
1797            return Ok(Self::Netmap(s.parse::<NetMap>()?));
1798        }
1799        if s.starts_with("af-xdp,") || s == "af-xdp" {
1800            return Ok(Self::AfXdp(s.parse::<AfXdp>()?));
1801        }
1802        if s.starts_with("vhost-user,") || s.starts_with("type=vhost-user,") || s == "vhost-user" || s == "type=vhost-user" {
1803            return Ok(Self::VhostUser(s.parse::<VhostUser>()?));
1804        }
1805        if s.starts_with("vhost-vdpa,") || s == "vhost-vdpa" {
1806            return Ok(Self::VhostVdpa(s.parse::<VhostVdpa>()?));
1807        }
1808        if s.starts_with("vmnet-host,") || s == "vmnet-host" {
1809            return Ok(Self::VmnetHost(s.parse::<VmnetHost>()?));
1810        }
1811        if s.starts_with("vmnet-shared,") || s == "vmnet-shared" {
1812            return Ok(Self::VmnetShared(s.parse::<VmnetShared>()?));
1813        }
1814        if s.starts_with("vmnet-bridged,") || s == "vmnet-bridged" {
1815            return Ok(Self::VmnetBridged(s.parse::<VmnetBridged>()?));
1816        }
1817        if s.starts_with("hubport,") || s == "hubport" {
1818            return Ok(Self::Hubport(s.parse::<Hubport>()?));
1819        }
1820
1821        Err(format!("unsupported netdev backend: {s}"))
1822    }
1823}
1824
1825fn parse_host_and_port(value: &str) -> Result<HostAndPort, String> {
1826    let (host, port) = value.rsplit_once(':').ok_or_else(|| format!("expected host:port, got {value}"))?;
1827    Ok(HostAndPort {
1828        host: host.to_string(),
1829        port: port.parse::<u16>().map_err(|e| e.to_string())?,
1830    })
1831}
1832
1833fn parse_host_and_maybe_port(value: &str) -> Result<HostAndMaybePort, String> {
1834    if let Some((host, port)) = value.rsplit_once(':')
1835        && !port.is_empty()
1836    {
1837        return Ok(HostAndMaybePort {
1838            host: host.to_string(),
1839            port: Some(port.parse::<u16>().map_err(|e| e.to_string())?),
1840        });
1841    }
1842
1843    Ok(HostAndMaybePort { host: value.to_string(), port: None })
1844}
1845
1846fn parse_props(s: &str) -> Result<std::collections::BTreeMap<String, Vec<String>>, String> {
1847    let mut parts = s.split(DELIM_COMMA);
1848    let _backend = parts.next().ok_or_else(|| "empty netdev argument".to_string())?;
1849    let mut props = std::collections::BTreeMap::<String, Vec<String>>::new();
1850
1851    for part in parts {
1852        let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid netdev option: {part}"))?;
1853        props.entry(key.to_string()).or_default().push(value.to_string());
1854    }
1855
1856    Ok(props)
1857}
1858
1859fn parse_netdev_props(s: &str, backend_name: &str) -> Result<std::collections::BTreeMap<String, Vec<String>>, String> {
1860    let actual = s.split(DELIM_COMMA).next().ok_or_else(|| "empty netdev argument".to_string())?;
1861    if actual != backend_name {
1862        return Err(format!("expected {backend_name} backend, got {actual}"));
1863    }
1864    parse_props(s)
1865}
1866
1867fn required_prop<'a>(props: &'a std::collections::BTreeMap<String, Vec<String>>, key: &str) -> Result<&'a str, String> {
1868    props
1869        .get(key)
1870        .and_then(|values| values.first())
1871        .map(|s| s.as_str())
1872        .ok_or_else(|| format!("missing required option: {key}"))
1873}
1874
1875fn ensure_prop_value(props: &std::collections::BTreeMap<String, Vec<String>>, key: &str, expected: &str) -> Result<(), String> {
1876    let actual = required_prop(props, key)?;
1877    if actual == expected { Ok(()) } else { Err(format!("expected {key}={expected}, got {actual}")) }
1878}
1879
1880fn first_prop<'a>(props: &'a std::collections::BTreeMap<String, Vec<String>>, key: &str) -> Option<&'a str> {
1881    props.get(key).and_then(|values| values.first()).map(|s| s.as_str())
1882}
1883
1884fn all_props<'a>(props: &'a std::collections::BTreeMap<String, Vec<String>>, key: &str) -> Vec<&'a str> {
1885    props.get(key).map(|values| values.iter().map(|s| s.as_str()).collect()).unwrap_or_default()
1886}
1887
1888fn push_opt_string(args: &mut Vec<String>, key: &str, value: &Option<String>) {
1889    if let Some(value) = value {
1890        args.push(format!("{key}={value}"));
1891    }
1892}
1893
1894fn push_opt_onoff(args: &mut Vec<String>, key: &str, value: &Option<OnOff>) {
1895    if let Some(value) = value {
1896        args.push(format!("{key}={}", value.to_arg()));
1897    }
1898}
1899
1900fn parse_optional_onoff(value: Option<&str>) -> Result<Option<OnOff>, String> {
1901    value.map(|raw| raw.parse::<OnOff>().map_err(|_| format!("invalid on/off value: {raw}"))).transpose()
1902}
1903
1904fn parse_optional_usize(value: Option<&str>) -> Result<Option<usize>, String> {
1905    value.map(|raw| raw.parse::<usize>().map_err(|e| e.to_string())).transpose()
1906}
1907
1908fn parse_optional_u16(value: Option<&str>) -> Result<Option<u16>, String> {
1909    value.map(|raw| raw.parse::<u16>().map_err(|e| e.to_string())).transpose()
1910}
1911
1912fn parse_optional_ipv4(value: Option<&str>) -> Result<Option<Ipv4Addr>, String> {
1913    value.map(|raw| raw.parse::<Ipv4Addr>().map_err(|e| e.to_string())).transpose()
1914}
1915
1916fn parse_optional_ipv6(value: Option<&str>) -> Result<Option<Ipv6Addr>, String> {
1917    value.map(|raw| raw.parse::<Ipv6Addr>().map_err(|e| e.to_string())).transpose()
1918}
1919
1920fn parse_optional_qipv4net(value: Option<&str>) -> Result<Option<QIpv4Net>, String> {
1921    value
1922        .map(|raw| raw.parse::<ipnet::Ipv4Net>().map(|ip| QIpv4Net::builder().ip(ip).build()).map_err(|e| e.to_string()))
1923        .transpose()
1924}
1925
1926fn parse_optional_qipv6net(value: Option<&str>) -> Result<Option<QIpv6Net>, String> {
1927    value
1928        .map(|raw| raw.parse::<ipnet::Ipv6Net>().map(|ip| QIpv6Net::builder().ip(ip).build()).map_err(|e| e.to_string()))
1929        .transpose()
1930}
1931
1932fn parse_hostfwd(value: &str) -> Result<HostForward, String> {
1933    let (protocol, rest) = if let Some(rest) = value.strip_prefix("tcp:") {
1934        (Some(TcpUdp::Tcp), rest)
1935    } else if let Some(rest) = value.strip_prefix("udp:") {
1936        (Some(TcpUdp::Udp), rest)
1937    } else if let Some(rest) = value.strip_prefix("unix:") {
1938        (Some(TcpUdp::Unix), rest)
1939    } else {
1940        (None, value)
1941    };
1942
1943    if matches!(protocol, Some(TcpUdp::Unix)) {
1944        let (hostpath, guest_range) = rest.split_once('-').ok_or_else(|| format!("invalid hostfwd: {value}"))?;
1945        let (guestaddr, guestport) = parse_guest_host_port(guest_range)?;
1946        return Ok(HostForward {
1947            protocol,
1948            hostaddr: None,
1949            hostport: None,
1950            hostpath: Some(hostpath.to_string()),
1951            guestaddr,
1952            guestport,
1953        });
1954    }
1955
1956    let (host_range, guest_range) = rest.split_once('-').ok_or_else(|| format!("invalid hostfwd: {value}"))?;
1957    let (hostaddr, hostport) = parse_optional_host_port_range(host_range)?;
1958    let (guestaddr, guestport) = parse_guest_host_port(guest_range)?;
1959
1960    Ok(HostForward {
1961        protocol,
1962        hostaddr,
1963        hostport: Some(hostport),
1964        hostpath: None,
1965        guestaddr,
1966        guestport,
1967    })
1968}
1969
1970fn parse_optional_host_port_range(value: &str) -> Result<(Option<String>, u16), String> {
1971    if let Some((host, port)) = value.rsplit_once(':') {
1972        Ok(((!host.is_empty()).then_some(host.to_string()), port.parse::<u16>().map_err(|e| e.to_string())?))
1973    } else {
1974        Ok((None, value.parse::<u16>().map_err(|e| e.to_string())?))
1975    }
1976}
1977
1978fn parse_guest_host_port(value: &str) -> Result<(Option<String>, u16), String> {
1979    if let Some((host, port)) = value.rsplit_once(':') {
1980        Ok(((!host.is_empty()).then_some(host.to_string()), port.parse::<u16>().map_err(|e| e.to_string())?))
1981    } else {
1982        Ok((None, value.parse::<u16>().map_err(|e| e.to_string())?))
1983    }
1984}
1985
1986fn parse_guestfwd(value: &str) -> Result<GuestForward, String> {
1987    let mut parts = value.split(':');
1988    let proto = parts.next().ok_or_else(|| format!("invalid guestfwd: {value}"))?;
1989    if proto != "tcp" {
1990        return Err(format!("unsupported guestfwd protocol: {proto}"));
1991    }
1992    let server = parts.next().ok_or_else(|| format!("invalid guestfwd: {value}"))?.to_string();
1993    let port = parts.next().ok_or_else(|| format!("invalid guestfwd: {value}"))?.parse::<u16>().map_err(|e| e.to_string())?;
1994    let target = parts.collect::<Vec<_>>().join(":");
1995    if target.is_empty() {
1996        return Err(format!("invalid guestfwd target: {value}"));
1997    }
1998
1999    let target = if let Some(device) = target.strip_prefix("device=") {
2000        GuestForwardTarget::Device(device.parse::<CharDev>().map_err(|e| format!("invalid guestfwd device: {e}"))?)
2001    } else if let Some(command) = target.strip_prefix("cmd:") {
2002        let mut tokens = command.split_whitespace();
2003        let cmd = tokens.next().ok_or_else(|| format!("invalid guestfwd command: {value}"))?.to_string();
2004        let args = tokens.map(|token| token.to_string()).collect();
2005        GuestForwardTarget::Cmd((cmd, args))
2006    } else {
2007        return Err(format!("unsupported guestfwd target: {target}"));
2008    };
2009
2010    Ok(GuestForward { server, port, target })
2011}