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<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/// 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
423/// Linux Ethernet-over-L2TPv3 pseudowire backend.
424#[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/// A host and port pair used by socket-based `-netdev` backends.
597#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
598pub struct HostAndPort {
599    host: String,
600    port: u16,
601}
602/// A host and optional port used by `listen=` socket endpoints.
603#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
604pub struct HostAndMaybePort {
605    host: String,
606    port: Option<u16>,
607}
608/// A `-netdev socket,...` backend using `listen=` and/or `connect=`.
609#[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/// A `-netdev socket,...` multicast backend using `mcast=`.
676#[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/// A `-netdev socket,...` UDP tunnel backend using `udp=`.
738#[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/// The `socket` backend variants supported by this crate.
800#[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/// A `-netdev passt,...` backend.
1410#[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/// A `-netdev vhost-user,...` backend.
1557#[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/// A `-netdev vhost-vdpa,...` backend.
1615#[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/// A `-netdev hubport,...` backend.
1797#[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    /// Raw QMP JSON form, preserved verbatim.
1850    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}