@@ -42,7 +42,8 @@ use tracing::{debug, error, info, warn};
4242
4343pub use image:: { Image , ImageInfo } ;
4444pub ( crate ) use network:: {
45- resolve_networking, resolved_networks, validate_resolved_network, validate_resolved_networks,
45+ clamp_queues_without_netd, needs_netd_interface, netd_available, resolve_networking,
46+ resolved_networks, validate_resolved_network, validate_resolved_networks,
4647} ;
4748pub use qemu:: VmConfig ;
4849pub use workdir:: VmWorkDir ;
@@ -355,7 +356,7 @@ impl App {
355356 let vm_id = manifest. id . clone ( ) ;
356357 let mut runtime_networks = vm_work_dir. runtime_networks ( ) ;
357358 if runtime_networks. is_empty ( ) && cids_assigned. contains_key ( & vm_id) {
358- runtime_networks = resolved_networks ( & manifest, & self . config . cvm ) ;
359+ runtime_networks = self . runtime_networks ( & manifest) ;
359360 if let Err ( err) = vm_work_dir. set_runtime_networks ( & runtime_networks) {
360361 warn ! ( id = %vm_id, "failed to persist inferred runtime networks: {err}" ) ;
361362 }
@@ -454,7 +455,7 @@ impl App {
454455 append_boot_separator ( & path) ;
455456 }
456457
457- let mut runtime_networks = resolved_networks ( & vm_config. manifest , & self . config . cvm ) ;
458+ let mut runtime_networks = self . runtime_networks ( & vm_config. manifest ) ;
458459 let devices = self . try_allocate_gpus ( & vm_config. manifest ) ?;
459460 let gpu_host_config = self . config . cvm . gpu . clone ( ) ;
460461 let devices_to_sanitize = devices. clone ( ) ;
@@ -547,19 +548,16 @@ impl App {
547548 vm : & VmConfig ,
548549 networks : & mut [ Networking ] ,
549550 ) -> Result < ( ) > {
550- if self . config . cvm . network_filter . mode == NetworkFilterMode :: None
551- && !networks
552- . iter ( )
553- . any ( |network| network. mode == NetworkingMode :: Macvtap )
551+ if !networks
552+ . iter ( )
553+ . any ( |network| needs_netd_interface ( network, & self . config . cvm ) )
554554 {
555555 return Ok ( ( ) ) ;
556556 }
557557 let qemu_uid = Uid :: effective ( ) . as_raw ( ) ;
558558 let mut prepared = Vec :: new ( ) ;
559559 for ( nic_index, network) in networks. iter_mut ( ) . enumerate ( ) {
560- if network. mode == NetworkingMode :: Bridge
561- && self . config . cvm . network_filter . mode == NetworkFilterMode :: None
562- {
560+ if !needs_netd_interface ( network, & self . config . cvm ) {
563561 continue ;
564562 }
565563 let identity = InterfaceIdentity {
@@ -572,21 +570,35 @@ impl App {
572570 & network. mac_prefix_bytes ( ) ,
573571 nic_index,
574572 ) ;
573+ let queues = network. queue_pairs ( ) ;
574+ let filtered = self . filters_bridge_traffic ( network) ;
575575 let request = match network. mode {
576576 NetworkingMode :: Bridge => NetdRequest :: PrepareBridge ( PrepareBridgeRequest {
577577 identity : identity. clone ( ) ,
578578 bridge : network. bridge . clone ( ) ,
579579 mac,
580580 qemu_uid,
581- filter : self . config . cvm . network_filter . filter . clone ( ) ,
582- parameters : self . config . cvm . network_filter . parameters . clone ( ) ,
581+ // An unfiltered TAP is only created for multiqueue, where
582+ // the node may not run libvirt at all.
583+ filter : if filtered {
584+ self . config . cvm . network_filter . filter . clone ( )
585+ } else {
586+ String :: new ( )
587+ } ,
588+ parameters : if filtered {
589+ self . config . cvm . network_filter . parameters . clone ( )
590+ } else {
591+ BTreeMap :: new ( )
592+ } ,
593+ queues,
583594 } ) ,
584595 NetworkingMode :: Macvtap => NetdRequest :: PrepareMacvtap ( PrepareMacvtapRequest {
585596 identity : identity. clone ( ) ,
586597 parent : network. parent . clone ( ) ,
587598 mac,
588599 qemu_uid,
589600 mode : network. macvtap_mode . clone ( ) ,
601+ queues,
590602 } ) ,
591603 NetworkingMode :: User | NetworkingMode :: Custom => continue ,
592604 } ;
@@ -600,73 +612,127 @@ impl App {
600612 & self . config . netd . socket ,
601613 & NetdRequest :: Remove {
602614 identity : identity. clone ( ) ,
615+ filtered,
603616 } ,
604617 )
605618 . await
606619 {
607620 warn ! ( %cleanup_error, "failed to roll back in-flight filtered network" ) ;
608621 }
609- for identity in prepared. into_iter ( ) . rev ( ) {
610- if let Err ( cleanup_error) = netd:: request (
611- & self . config . netd . socket ,
612- & NetdRequest :: Remove { identity } ,
613- )
614- . await
615- {
616- warn ! ( %cleanup_error, "failed to roll back prepared filtered network" ) ;
617- }
618- }
619- return Err ( error) . context ( "failed to prepare libvirt-filtered networking" ) ;
622+ self . roll_back_prepared_networks ( prepared) . await ;
623+ return Err ( error) . context ( "failed to prepare netd-managed networking" ) ;
620624 }
621625 } ;
622- if network. mode == NetworkingMode :: Macvtap {
623- network. device = response
624- . device
625- . context ( "netd response omitted macvtap device" ) ?;
626+ prepared. push ( ( identity. clone ( ) , filtered) ) ;
627+ // Everything below runs after netd already built a host interface,
628+ // so a failure has to unwind the same way a failed Prepare does.
629+ let accepted = ( || {
630+ if network. mode == NetworkingMode :: Macvtap {
631+ network. device = response
632+ . device
633+ . clone ( )
634+ . context ( "netd response omitted macvtap device" ) ?;
635+ }
636+ // QEMU refuses a TAP whose IFF_MULTI_QUEUE state disagrees with
637+ // its own `queues=`, and reports it from inside the per-VM
638+ // launcher. netd echoes what it built, so a netd too old to
639+ // understand the request fails here, where the reason is
640+ // legible.
641+ if queues > 1 && response. queues != Some ( queues) {
642+ bail ! (
643+ "netd prepared interface {nic_index} with {} queue pairs instead of \
644+ {queues}; its version may predate multiqueue support",
645+ response. queues. map_or_else(
646+ || "an unreported number of" . to_string( ) ,
647+ |q| q. to_string( )
648+ )
649+ ) ;
650+ }
651+ Ok ( ( ) )
652+ } ) ( ) ;
653+ if let Err ( error) = accepted {
654+ self . roll_back_prepared_networks ( prepared) . await ;
655+ return Err ( error) ;
626656 }
627- prepared. push ( identity) ;
628657 }
629658 Ok ( ( ) )
630659 }
631660
661+ /// Launch-time view of a VM's NICs: node defaults merged in, the
662+ /// vCPU-scaled queue count made concrete, and multiqueue dropped when this
663+ /// node has no netd to build the interface.
664+ pub ( crate ) fn runtime_networks ( & self , manifest : & Manifest ) -> Vec < Networking > {
665+ let requested = if manifest. networks . is_empty ( ) {
666+ vec ! [ self . config. cvm. networking. clone( ) ]
667+ } else {
668+ manifest. networks . clone ( )
669+ } ;
670+ let mut resolved = resolved_networks ( manifest, & self . config . cvm ) ;
671+ clamp_queues_without_netd (
672+ & requested,
673+ & mut resolved,
674+ & self . config . cvm ,
675+ netd_available ( & self . config . netd . socket ) ,
676+ ) ;
677+ resolved
678+ }
679+
680+ /// Removes interfaces netd already built for a launch that then failed.
681+ async fn roll_back_prepared_networks ( & self , prepared : Vec < ( InterfaceIdentity , bool ) > ) {
682+ for ( identity, filtered) in prepared. into_iter ( ) . rev ( ) {
683+ if let Err ( cleanup_error) = netd:: request (
684+ & self . config . netd . socket ,
685+ & NetdRequest :: Remove { identity, filtered } ,
686+ )
687+ . await
688+ {
689+ warn ! ( %cleanup_error, "failed to roll back prepared network interface" ) ;
690+ }
691+ }
692+ }
693+
694+ /// Whether this NIC's host interface carries a libvirt nwfilter binding.
695+ /// Macvtap never does, and a bridge NIC only does when the node filters.
696+ fn filters_bridge_traffic ( & self , network : & Networking ) -> bool {
697+ network. mode == NetworkingMode :: Bridge
698+ && self . config . cvm . network_filter . mode == NetworkFilterMode :: Libvirt
699+ }
700+
632701 pub ( crate ) async fn remove_filtered_networks (
633702 & self ,
634703 vm_id : & str ,
635704 networks : & [ Networking ] ,
636705 ) -> Result < ( ) > {
637- if self . config . cvm . network_filter . mode == NetworkFilterMode :: None
638- && !networks
639- . iter ( )
640- . any ( |network| network. mode == NetworkingMode :: Macvtap )
706+ if !networks
707+ . iter ( )
708+ . any ( |network| needs_netd_interface ( network, & self . config . cvm ) )
641709 {
642710 return Ok ( ( ) ) ;
643711 }
644712 let mut first_error = None ;
645713 for ( nic_index, network) in networks. iter ( ) . enumerate ( ) . rev ( ) {
646- if network. mode == NetworkingMode :: Bridge
647- && self . config . cvm . network_filter . mode == NetworkFilterMode :: None
648- {
649- continue ;
650- }
651- if !matches ! (
652- network. mode,
653- NetworkingMode :: Bridge | NetworkingMode :: Macvtap
654- ) {
714+ if !needs_netd_interface ( network, & self . config . cvm ) {
655715 continue ;
656716 }
657717 let identity = InterfaceIdentity {
658718 instance_id : self . config . cvm . instance_id . clone ( ) ,
659719 vm_id : vm_id. to_string ( ) ,
660720 nic_index,
661721 } ;
662- if let Err ( error) =
663- netd:: request ( & self . config . netd . socket , & NetdRequest :: Remove { identity } ) . await
722+ if let Err ( error) = netd:: request (
723+ & self . config . netd . socket ,
724+ & NetdRequest :: Remove {
725+ identity,
726+ filtered : self . filters_bridge_traffic ( network) ,
727+ } ,
728+ )
729+ . await
664730 {
665731 first_error. get_or_insert ( error) ;
666732 }
667733 }
668734 if let Some ( error) = first_error {
669- return Err ( error) . context ( "failed to remove libvirt-filtered networking" ) ;
735+ return Err ( error) . context ( "failed to remove netd-managed networking" ) ;
670736 }
671737 Ok ( ( ) )
672738 }
@@ -1059,7 +1125,7 @@ impl App {
10591125 let already_running = cids_assigned. contains_key ( & vm_id) ;
10601126 let mut runtime_networks = vm_work_dir. runtime_networks ( ) ;
10611127 if runtime_networks. is_empty ( ) && already_running {
1062- runtime_networks = resolved_networks ( & manifest, & self . config . cvm ) ;
1128+ runtime_networks = self . runtime_networks ( & manifest) ;
10631129 if let Err ( err) = vm_work_dir. set_runtime_networks ( & runtime_networks) {
10641130 warn ! ( id = %vm_id, "failed to persist inferred runtime networks: {err}" ) ;
10651131 }
@@ -2241,6 +2307,8 @@ mod tests {
22412307 dhcp_start: String :: new( ) ,
22422308 restrict: false ,
22432309 netdev: String :: new( ) ,
2310+ vhost: None ,
2311+ queues: None ,
22442312 } ] ;
22452313
22462314 workdir. put_manifest ( & manifest) ?;
@@ -2506,6 +2574,8 @@ mod tests {
25062574 dhcp_start: String :: new( ) ,
25072575 restrict: false ,
25082576 netdev: String :: new( ) ,
2577+ vhost: None ,
2578+ queues: None ,
25092579 } ] ;
25102580 let user_manifest = test_manifest ( 2048 ) ;
25112581 let image = test_tdx_image ( true ) ;
0 commit comments