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}
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#[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#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
452pub struct HostAndPort {
453 host: String,
454 port: u16,
455}
456#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
458pub struct HostAndMaybePort {
459 host: String,
460 port: Option<u16>,
461}
462#[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#[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#[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#[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#[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#[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#[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 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::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}