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