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