@@ -372,118 +372,76 @@ pub(crate) struct IfaceBuild {
372372
373373/// Per-device DNS host entries for `/etc/hosts` overlay.
374374///
375- /// Each device gets a persistent hosts file at `<hosts_dir>/<node_id>.hosts`.
376- /// A shared `resolv.conf` lives at `<hosts_dir>/resolv.conf`. Files are created
377- /// at init with default content and bind-mounted into worker threads at startup.
378- /// Subsequent `dns_entry()` / `set_nameserver()` calls just rewrite the file —
379- /// glibc picks up changes via mtime check on next `getaddrinfo()`.
380- pub ( crate ) struct DnsEntries {
381- /// Lab-wide entries applied to every device.
382- pub global : Vec < ( String , IpAddr ) > ,
383- /// Per-device entries, keyed by device `NodeId`.
384- pub per_device : HashMap < NodeId , Vec < ( String , IpAddr ) > > ,
385- /// Optional nameserver IP for `/etc/resolv.conf` overlay.
386- pub nameserver : Option < IpAddr > ,
375+ /// Per-device `/etc/hosts` overlay and shared `resolv.conf`.
376+ ///
377+ /// Each device gets a hosts file at `<hosts_dir>/<node_id>.hosts` and a shared
378+ /// `resolv.conf` at `<hosts_dir>/resolv.conf`, bind-mounted into worker threads.
379+ /// Lab-wide DNS records live in [`DnsServer`](crate::dns_server::DnsServer).
380+ pub ( crate ) struct DnsOverlayDir {
381+ /// Nameservers for `/etc/resolv.conf` overlay.
382+ pub nameservers : Vec < IpAddr > ,
387383 /// Directory for generated hosts/resolv files.
388384 pub hosts_dir : PathBuf ,
389385}
390386
391- impl DnsEntries {
387+ impl DnsOverlayDir {
392388 fn new ( prefix : & str ) -> Result < Self > {
393389 let hosts_dir = std:: env:: temp_dir ( ) . join ( format ! ( "patchbay-{prefix}-hosts" ) ) ;
394390 std:: fs:: create_dir_all ( & hosts_dir) . context ( "create hosts dir" ) ?;
395- // Create initial resolv.conf with localhost as fallback nameserver.
396- // glibc and hickory-resolver need at least one nameserver entry.
397- let resolv_path = hosts_dir. join ( "resolv.conf" ) ;
398391 std:: fs:: write (
399- & resolv_path ,
392+ hosts_dir . join ( "resolv.conf" ) ,
400393 "# generated by patchbay\n nameserver 127.0.0.53\n " ,
401394 )
402395 . context ( "write initial resolv.conf" ) ?;
403396 Ok ( Self {
404- global : Vec :: new ( ) ,
405- per_device : HashMap :: new ( ) ,
406- nameserver : None ,
397+ nameservers : Vec :: new ( ) ,
407398 hosts_dir,
408399 } )
409400 }
410401
411- /// Returns the path to the hosts file for a device. Always valid after
412- /// `ensure_hosts_file()` has been called for this device.
413402 pub ( crate ) fn hosts_path_for ( & self , device_id : NodeId ) -> PathBuf {
414403 self . hosts_dir . join ( format ! ( "{}.hosts" , device_id. 0 ) )
415404 }
416405
417- /// Returns the path to the shared resolv.conf overlay.
418406 pub ( crate ) fn resolv_path ( & self ) -> PathBuf {
419407 self . hosts_dir . join ( "resolv.conf" )
420408 }
421409
422- /// Creates the hosts file for a device with default content if it doesn't exist.
410+ /// Creates the default hosts file for a device if it doesn't exist.
423411 pub ( crate ) fn ensure_hosts_file ( & self , device_id : NodeId ) -> Result < ( ) > {
424412 let path = self . hosts_path_for ( device_id) ;
425413 if !path. exists ( ) {
426- self . write_hosts_file ( device_id) ?;
414+ std:: fs:: write (
415+ & path,
416+ "# generated by patchbay\n 127.0.0.1\t localhost\n ::1\t localhost\n " ,
417+ )
418+ . with_context ( || format ! ( "write {}" , path. display( ) ) ) ?;
427419 }
428420 Ok ( ( ) )
429421 }
430422
431- /// Writes (or rewrites) the hosts file for a single device.
432- pub ( crate ) fn write_hosts_file ( & self , device_id : NodeId ) -> Result < ( ) > {
423+ /// Appends a host entry to a device's hosts file.
424+ pub ( crate ) fn append_host ( & self , device_id : NodeId , name : & str , ip : IpAddr ) -> Result < ( ) > {
425+ use std:: io:: Write ;
433426 let path = self . hosts_path_for ( device_id) ;
434- let mut buf =
435- String :: from ( "# generated by patchbay\n 127.0.0.1\t localhost\n ::1\t localhost\n " ) ;
436- for ( name, ip) in & self . global {
437- buf. push_str ( & format ! ( "{ip}\t {name}\n " ) ) ;
438- }
439- if let Some ( entries) = self . per_device . get ( & device_id) {
440- for ( name, ip) in entries {
441- buf. push_str ( & format ! ( "{ip}\t {name}\n " ) ) ;
442- }
443- }
444- std:: fs:: write ( & path, buf. as_bytes ( ) )
445- . with_context ( || format ! ( "write {}" , path. display( ) ) ) ?;
446- Ok ( ( ) )
447- }
448-
449- /// Rewrites hosts files for all given devices.
450- pub ( crate ) fn write_all_hosts_files ( & self , device_ids : & [ NodeId ] ) -> Result < ( ) > {
451- for & id in device_ids {
452- self . write_hosts_file ( id) ?;
453- }
427+ let mut f = std:: fs:: OpenOptions :: new ( )
428+ . append ( true )
429+ . open ( & path)
430+ . with_context ( || format ! ( "open {}" , path. display( ) ) ) ?;
431+ writeln ! ( f, "{ip}\t {name}" ) . with_context ( || format ! ( "append to {}" , path. display( ) ) ) ?;
454432 Ok ( ( ) )
455433 }
456434
457- /// Writes the resolv.conf file with the current nameserver setting.
458435 pub ( crate ) fn write_resolv_conf ( & self ) -> Result < ( ) > {
459436 let path = self . resolv_path ( ) ;
460- let content = match self . nameserver {
461- Some ( ip ) => format ! ( "# generated by patchbay \n nameserver {ip} \n " ) ,
462- None => "# generated by patchbay \n ". to_string ( ) ,
463- } ;
437+ let mut content = String :: from ( "# generated by patchbay \n " ) ;
438+ for ip in & self . nameservers {
439+ content . push_str ( & format ! ( "nameserver {ip} \n ") ) ;
440+ }
464441 std:: fs:: write ( & path, content. as_bytes ( ) )
465442 . with_context ( || format ! ( "write {}" , path. display( ) ) ) ?;
466443 Ok ( ( ) )
467444 }
468-
469- /// Resolves a name using global + per-device entries. Returns the first match.
470- pub ( crate ) fn resolve ( & self , device_id : Option < NodeId > , name : & str ) -> Option < IpAddr > {
471- if let Some ( id) = device_id {
472- if let Some ( entries) = self . per_device . get ( & id) {
473- for ( n, ip) in entries {
474- if n == name {
475- return Some ( * ip) ;
476- }
477- }
478- }
479- }
480- for ( n, ip) in & self . global {
481- if n == name {
482- return Some ( * ip) ;
483- }
484- }
485- None
486- }
487445}
488446
489447/// Per-region metadata stored in `NetworkCore`.
@@ -567,6 +525,8 @@ pub(crate) struct LabInner {
567525 pub ipv6_dad_mode : Ipv6DadMode ,
568526 /// IPv6 provisioning behavior.
569527 pub ipv6_provisioning_mode : Ipv6ProvisioningMode ,
528+ /// In-process DNS server on the IX bridge (lazy, started on first access).
529+ pub dns_server : std:: sync:: Mutex < Option < crate :: dns_server:: DnsServer > > ,
570530 /// Writer task handle (kept alive until lab is dropped).
571531 pub writer_handle : std:: sync:: Mutex < Option < tokio:: task:: JoinHandle < ( ) > > > ,
572532 /// Test outcome flag shared with the writer and [`TestGuard`].
@@ -580,6 +540,9 @@ pub(crate) struct LabInner {
580540impl Drop for LabInner {
581541 fn drop ( & mut self ) {
582542 self . cancel . cancel ( ) ;
543+ if let Some ( dns) = self . dns_server . get_mut ( ) . unwrap ( ) . take ( ) {
544+ dns. shutdown ( ) ;
545+ }
583546
584547 // Determine final status from the test guard.
585548 let status = match self . test_status . load ( Ordering :: Acquire ) {
@@ -652,7 +615,7 @@ impl LabInner {
652615pub ( crate ) struct NetworkCore {
653616 pub ( crate ) cfg : CoreConfig ,
654617 /// DNS host entries for `/etc/hosts` overlay in spawned commands.
655- pub ( crate ) dns : DnsEntries ,
618+ pub ( crate ) dns : DnsOverlayDir ,
656619 next_id : u64 ,
657620 next_private_subnet : u16 ,
658621 next_public_subnet : u16 ,
@@ -688,7 +651,7 @@ impl Drop for NetworkCore {
688651impl NetworkCore {
689652 /// Constructs a new topology core and pre-creates the IX switch.
690653 pub ( crate ) fn new ( cfg : CoreConfig ) -> Result < Self > {
691- let dns = DnsEntries :: new ( & cfg. prefix ) . context ( "create DNS entries dir" ) ?;
654+ let dns = DnsOverlayDir :: new ( & cfg. prefix ) . context ( "create DNS entries dir" ) ?;
692655 let mut core = Self {
693656 cfg,
694657 dns,
@@ -1682,13 +1645,6 @@ impl NetworkCore {
16821645 Ok ( ( ns, old_ip, new_ip, prefix_len) )
16831646 }
16841647
1685- /// Adds a global DNS entry and writes all hosts files.
1686- pub ( crate ) fn add_dns_entry ( & mut self , name : & str , ip : IpAddr ) -> Result < ( ) > {
1687- self . dns . global . push ( ( name. to_string ( ) , ip) ) ;
1688- let ids: Vec < _ > = self . all_device_ids ( ) ;
1689- self . dns . write_all_hosts_files ( & ids)
1690- }
1691-
16921648 // ── Link target resolution ───────────────────────────────────────
16931649
16941650 /// Resolves the `(namespace, ifname)` for impairment between two connected nodes.
0 commit comments