66use crate :: {
77 app:: Manifest ,
88 config:: {
9- BridgeNetworking , CvmConfig , GatewayConfig , Networking , PasstNetworking ,
10- ProcessAnnotation , Protocol ,
9+ CvmConfig , GatewayConfig , Networking , NetworkingMode , ProcessAnnotation , Protocol ,
1110 } ,
1211} ;
1312use std:: { collections:: HashMap , os:: unix:: fs:: PermissionsExt } ;
@@ -31,17 +30,43 @@ use dstack_types::{
3130 AppCompose , KeyProviderKind ,
3231} ;
3332use dstack_vmm_rpc as pb;
33+ use sha2:: { Digest , Sha256 } ;
34+
35+ /// Derive a deterministic MAC address from a VM ID using SHA256.
36+ /// Sets locally-administered + unicast bits (0x02) per IEEE 802.
37+ /// Derive a deterministic MAC address from a VM ID.
38+ ///
39+ /// `prefix` may contain 0-3 fixed bytes. The first byte always has the
40+ /// locally-administered + unicast bits set (0x02). Remaining bytes are
41+ /// filled from SHA256(vm_id).
42+ pub fn mac_address_for_vm ( vm_id : & str , prefix : & [ u8 ] ) -> String {
43+ let hash = Sha256 :: digest ( vm_id. as_bytes ( ) ) ;
44+ let prefix_len = prefix. len ( ) . min ( 3 ) ;
45+ let mut bytes = [ 0u8 ; 6 ] ;
46+ // Fill prefix bytes
47+ bytes[ ..prefix_len] . copy_from_slice ( & prefix[ ..prefix_len] ) ;
48+ // Fill remaining bytes from hash
49+ for i in prefix_len..6 {
50+ bytes[ i] = hash[ i - prefix_len] ;
51+ }
52+ // Ensure locally-administered + unicast on first byte
53+ bytes[ 0 ] = ( bytes[ 0 ] & 0xfe ) | 0x02 ;
54+ format ! (
55+ "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}" ,
56+ bytes[ 0 ] , bytes[ 1 ] , bytes[ 2 ] , bytes[ 3 ] , bytes[ 4 ] , bytes[ 5 ]
57+ )
58+ }
3459use fs_err as fs;
3560use serde:: { Deserialize , Serialize } ;
3661use serde_human_bytes as hex_bytes;
3762use supervisor_client:: supervisor:: { ProcessConfig , ProcessInfo } ;
3863
3964fn networking_to_proto ( n : & Networking ) -> pb:: NetworkingConfig {
40- let mode = match n {
41- Networking :: Bridge ( _ ) => "bridge" ,
42- Networking :: User ( _ ) => "user" ,
43- Networking :: Passt ( _ ) => "passt" ,
44- Networking :: Custom ( _ ) => "custom" ,
65+ let mode = match n. mode {
66+ NetworkingMode :: Bridge => "bridge" ,
67+ NetworkingMode :: User => "user" ,
68+ NetworkingMode :: Passt => "passt" ,
69+ NetworkingMode :: Custom => "custom" ,
4570 } ;
4671 pb:: NetworkingConfig {
4772 mode : mode. into ( ) ,
@@ -332,8 +357,8 @@ impl VmState {
332357}
333358
334359impl VmConfig {
335- fn config_passt ( & self , workdir : & VmWorkDir , netcfg : & PasstNetworking ) -> Result < ProcessConfig > {
336- let PasstNetworking {
360+ fn config_passt ( & self , workdir : & VmWorkDir , netcfg : & Networking ) -> Result < ProcessConfig > {
361+ let Networking {
337362 passt_exec,
338363 interface,
339364 address,
@@ -344,6 +369,7 @@ impl VmConfig {
344369 map_guest_addr,
345370 no_map_gw,
346371 ipv4_only,
372+ ..
347373 } = netcfg;
348374
349375 let passt_socket = workdir. passt_socket ( ) ;
@@ -438,46 +464,6 @@ impl VmConfig {
438464 Ok ( process_config)
439465 }
440466
441- /// Configure bridge networking using QEMU's built-in bridge helper.
442- /// The helper (qemu-bridge-helper) creates and manages TAP devices automatically,
443- /// so VMM does not need root or CAP_NET_ADMIN. Requires /etc/qemu/bridge.conf
444- /// to allow the bridge (e.g., "allow virbr0").
445- /// Guest IP is assigned by an external DHCP server on the bridge.
446- /// Returns (netdev_arg, device_arg).
447- fn config_bridge (
448- & self ,
449- _workdir : & VmWorkDir ,
450- netcfg : & BridgeNetworking ,
451- ) -> Result < ( String , String ) > {
452- use sha2:: { Digest , Sha256 } ;
453-
454- let vm_id = & self . manifest . id ;
455-
456- // Derive MAC from VM ID. Set locally-administered + unicast bits (0x02).
457- let hash = Sha256 :: digest ( vm_id. as_bytes ( ) ) ;
458- let mac = format ! (
459- "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}" ,
460- ( hash[ 0 ] & 0xfe ) | 0x02 ,
461- hash[ 1 ] ,
462- hash[ 2 ] ,
463- hash[ 3 ] ,
464- hash[ 4 ] ,
465- hash[ 5 ]
466- ) ;
467-
468- // Use QEMU's bridge netdev: QEMU calls qemu-bridge-helper to create/attach
469- // the TAP device. TAP is automatically destroyed when QEMU exits.
470- let netdev = format ! ( "bridge,id=net0,br={}" , netcfg. bridge) ;
471- let device = format ! ( "virtio-net-pci,netdev=net0,mac={mac}" ) ;
472-
473- tracing:: info!(
474- "bridge networking: mac={mac} bridge={}" ,
475- netcfg. bridge
476- ) ;
477-
478- Ok ( ( netdev, device) )
479- }
480-
481467 pub fn config_qemu (
482468 & self ,
483469 workdir : impl AsRef < Path > ,
@@ -569,30 +555,37 @@ impl VmConfig {
569555 . arg ( format ! ( "file={},if=none,id=hd1" , hda_path. display( ) ) )
570556 . arg ( "-device" )
571557 . arg ( "virtio-blk-pci,drive=hd1" ) ;
572- let mut net_device = "virtio-net-pci,netdev=net0" . to_string ( ) ;
558+ // Resolve per-VM networking override against global config.
559+ // Per-VM only sets mode; shared fields (bridge name, mac_prefix, etc.)
560+ // are merged from global config.
573561 let resolved_networking;
574562 let networking = match self . manifest . networking . as_ref ( ) {
575- Some ( Networking :: Bridge ( vm_br) ) if vm_br. bridge . is_empty ( ) => {
576- // per-VM selected bridge mode but bridge name comes from global config
577- if let Networking :: Bridge ( global_br) = & cfg. networking {
578- resolved_networking = Networking :: Bridge ( global_br. clone ( ) ) ;
579- } else {
580- resolved_networking = Networking :: Bridge ( BridgeNetworking {
581- bridge : "virbr0" . to_string ( ) ,
582- } ) ;
583- }
563+ Some ( vm_net) => {
564+ // Per-VM override: take mode from VM, fill other fields from global
565+ resolved_networking = Networking {
566+ mode : vm_net. mode ,
567+ bridge : if vm_net. bridge . is_empty ( ) {
568+ cfg. networking . bridge . clone ( )
569+ } else {
570+ vm_net. bridge . clone ( )
571+ } ,
572+ ..cfg. networking . clone ( )
573+ } ;
584574 & resolved_networking
585575 }
586- Some ( n) => n,
587576 None => & cfg. networking ,
588577 } ;
589- let netdev = match networking {
590- Networking :: User ( netcfg) => {
578+ // Generate deterministic MAC for all networking modes
579+ let prefix = networking. mac_prefix_bytes ( ) ;
580+ let mac = mac_address_for_vm ( & self . manifest . id , & prefix) ;
581+ let net_device = format ! ( "virtio-net-pci,netdev=net0,mac={mac}" ) ;
582+ let netdev = match networking. mode {
583+ NetworkingMode :: User => {
591584 let mut netdev = format ! (
592585 "user,id=net0,net={},dhcpstart={},restrict={}" ,
593- netcfg . net,
594- netcfg . dhcp_start,
595- if netcfg . restrict { "yes" } else { "no" }
586+ networking . net,
587+ networking . dhcp_start,
588+ if networking . restrict { "yes" } else { "no" }
596589 ) ;
597590 for pm in & self . manifest . port_map {
598591 netdev. push_str ( & format ! (
@@ -605,24 +598,24 @@ impl VmConfig {
605598 }
606599 netdev
607600 }
608- Networking :: Passt ( netcfg ) => {
601+ NetworkingMode :: Passt => {
609602 processes. push (
610- self . config_passt ( & workdir, netcfg )
603+ self . config_passt ( & workdir, networking )
611604 . context ( "Failed to configure passt" ) ?,
612605 ) ;
613606 format ! (
614607 "stream,id=net0,server=off,addr.type=unix,addr.path={}" ,
615608 workdir. passt_socket( ) . display( )
616609 )
617610 }
618- Networking :: Bridge ( netcfg ) => {
619- let ( netdev , device ) = self
620- . config_bridge ( & workdir , netcfg )
621- . context ( "failed to configure bridge networking" ) ? ;
622- net_device = device ;
623- netdev
611+ NetworkingMode :: Bridge => {
612+ tracing :: info! (
613+ "bridge networking: mac={mac} bridge={}" ,
614+ networking . bridge
615+ ) ;
616+ format ! ( "bridge,id=net0,br={}" , networking . bridge )
624617 }
625- Networking :: Custom ( netcfg ) => netcfg . netdev . clone ( ) ,
618+ NetworkingMode :: Custom => networking . netdev . clone ( ) ,
626619 } ;
627620 command. arg ( "-netdev" ) . arg ( netdev) ;
628621 command. arg ( "-device" ) . arg ( net_device) ;
0 commit comments