@@ -236,6 +236,49 @@ fn home_dir() -> Option<String> {
236236 std:: env:: var ( "HOME" ) . ok ( )
237237}
238238
239+ /// Discover upstream DNS resolvers from the host's resolver configuration.
240+ ///
241+ /// Reads systemd-resolved's upstream config first (`/run/systemd/resolve/resolv.conf`),
242+ /// then falls back to `/etc/resolv.conf`. Filters out loopback addresses (127.x.x.x
243+ /// and ::1) that are unreachable from k3s pod network namespaces.
244+ ///
245+ /// Returns an empty vec if no usable resolvers are found.
246+ fn resolve_upstream_dns ( ) -> Vec < String > {
247+ let paths = [ "/run/systemd/resolve/resolv.conf" , "/etc/resolv.conf" ] ;
248+
249+ for path in & paths {
250+ if let Ok ( contents) = std:: fs:: read_to_string ( path) {
251+ let resolvers: Vec < String > = contents
252+ . lines ( )
253+ . filter_map ( |line| {
254+ let line = line. trim ( ) ;
255+ if !line. starts_with ( "nameserver" ) {
256+ return None ;
257+ }
258+ let ip = line. split_whitespace ( ) . nth ( 1 ) ?;
259+ if ip. starts_with ( "127." ) || ip == "::1" {
260+ return None ;
261+ }
262+ Some ( ip. to_string ( ) )
263+ } )
264+ . collect ( ) ;
265+
266+ if !resolvers. is_empty ( ) {
267+ tracing:: debug!(
268+ "Discovered {} upstream DNS resolver(s) from {}: {}" ,
269+ resolvers. len( ) ,
270+ path,
271+ resolvers. join( ", " ) ,
272+ ) ;
273+ return resolvers;
274+ }
275+ }
276+ }
277+
278+ tracing:: debug!( "No upstream DNS resolvers found in host resolver config" ) ;
279+ Vec :: new ( )
280+ }
281+
239282/// Create an SSH Docker client from remote options.
240283pub async fn create_ssh_docker_client ( remote : & RemoteOptions ) -> Result < Docker > {
241284 // Ensure destination has ssh:// prefix
@@ -675,6 +718,13 @@ pub async fn ensure_container(
675718 env_vars. push ( "GPU_ENABLED=true" . to_string ( ) ) ;
676719 }
677720
721+ // Pass upstream DNS resolvers discovered on the host so the entrypoint
722+ // can configure k3s without probing files inside the container.
723+ let upstream_dns = resolve_upstream_dns ( ) ;
724+ if !upstream_dns. is_empty ( ) {
725+ env_vars. push ( format ! ( "UPSTREAM_DNS={}" , upstream_dns. join( "," ) ) ) ;
726+ }
727+
678728 let env = Some ( env_vars) ;
679729
680730 let config = ContainerCreateBody {
@@ -1195,4 +1245,112 @@ mod tests {
11951245 "should return a reasonable number of sockets"
11961246 ) ;
11971247 }
1248+
1249+ #[ test]
1250+ fn resolve_upstream_dns_filters_loopback ( ) {
1251+ // This test validates the function runs without panic on the current host.
1252+ // The exact output depends on the host's DNS config, but loopback
1253+ // addresses must never appear in the result.
1254+ let resolvers = resolve_upstream_dns ( ) ;
1255+ for r in & resolvers {
1256+ assert ! (
1257+ !r. starts_with( "127." ) ,
1258+ "IPv4 loopback should be filtered: {r}"
1259+ ) ;
1260+ assert_ne ! ( r, "::1" , "IPv6 loopback should be filtered" ) ;
1261+ }
1262+ }
1263+
1264+ #[ test]
1265+ fn resolve_upstream_dns_returns_vec ( ) {
1266+ // Verify the function returns a vec (may be empty in some CI environments
1267+ // where no resolv.conf exists, but should never panic).
1268+ let resolvers = resolve_upstream_dns ( ) ;
1269+ assert ! (
1270+ resolvers. len( ) <= 20 ,
1271+ "should return a reasonable number of resolvers"
1272+ ) ;
1273+ }
1274+
1275+ /// Helper: parse resolv.conf content using the same logic as resolve_upstream_dns().
1276+ /// Allows deterministic testing without depending on host DNS config.
1277+ fn parse_resolv_conf ( contents : & str ) -> Vec < String > {
1278+ contents
1279+ . lines ( )
1280+ . filter_map ( |line| {
1281+ let line = line. trim ( ) ;
1282+ if !line. starts_with ( "nameserver" ) {
1283+ return None ;
1284+ }
1285+ let ip = line. split_whitespace ( ) . nth ( 1 ) ?;
1286+ if ip. starts_with ( "127." ) || ip == "::1" {
1287+ return None ;
1288+ }
1289+ Some ( ip. to_string ( ) )
1290+ } )
1291+ . collect ( )
1292+ }
1293+
1294+ #[ test]
1295+ fn parse_resolv_conf_filters_ipv4_loopback ( ) {
1296+ let input = "nameserver 127.0.0.1\n nameserver 127.0.0.53\n nameserver 127.0.0.11\n " ;
1297+ assert ! ( parse_resolv_conf( input) . is_empty( ) ) ;
1298+ }
1299+
1300+ #[ test]
1301+ fn parse_resolv_conf_filters_ipv6_loopback ( ) {
1302+ let input = "nameserver ::1\n " ;
1303+ assert ! ( parse_resolv_conf( input) . is_empty( ) ) ;
1304+ }
1305+
1306+ #[ test]
1307+ fn parse_resolv_conf_passes_real_resolvers ( ) {
1308+ let input = "nameserver 8.8.8.8\n nameserver 1.1.1.1\n " ;
1309+ assert_eq ! ( parse_resolv_conf( input) , vec![ "8.8.8.8" , "1.1.1.1" ] ) ;
1310+ }
1311+
1312+ #[ test]
1313+ fn parse_resolv_conf_mixed_loopback_and_real ( ) {
1314+ let input =
1315+ "nameserver 127.0.0.53\n nameserver ::1\n nameserver 10.0.0.1\n nameserver 172.16.0.1\n " ;
1316+ assert_eq ! ( parse_resolv_conf( input) , vec![ "10.0.0.1" , "172.16.0.1" ] ) ;
1317+ }
1318+
1319+ #[ test]
1320+ fn parse_resolv_conf_ignores_comments_and_other_lines ( ) {
1321+ let input =
1322+ "# nameserver 8.8.8.8\n search example.com\n options ndots:5\n nameserver 1.1.1.1\n " ;
1323+ assert_eq ! ( parse_resolv_conf( input) , vec![ "1.1.1.1" ] ) ;
1324+ }
1325+
1326+ #[ test]
1327+ fn parse_resolv_conf_handles_tabs_and_extra_spaces ( ) {
1328+ let input = "nameserver\t 8.8.8.8\n nameserver 1.1.1.1\n " ;
1329+ assert_eq ! ( parse_resolv_conf( input) , vec![ "8.8.8.8" , "1.1.1.1" ] ) ;
1330+ }
1331+
1332+ #[ test]
1333+ fn parse_resolv_conf_empty_input ( ) {
1334+ assert ! ( parse_resolv_conf( "" ) . is_empty( ) ) ;
1335+ assert ! ( parse_resolv_conf( " \n \n " ) . is_empty( ) ) ;
1336+ }
1337+
1338+ #[ test]
1339+ fn parse_resolv_conf_bare_nameserver_keyword ( ) {
1340+ assert ! ( parse_resolv_conf( "nameserver\n " ) . is_empty( ) ) ;
1341+ assert ! ( parse_resolv_conf( "nameserver \n " ) . is_empty( ) ) ;
1342+ }
1343+
1344+ #[ test]
1345+ fn parse_resolv_conf_systemd_resolved_typical ( ) {
1346+ let input =
1347+ "# This is /run/systemd/resolve/resolv.conf\n nameserver 192.168.1.1\n search lan\n " ;
1348+ assert_eq ! ( parse_resolv_conf( input) , vec![ "192.168.1.1" ] ) ;
1349+ }
1350+
1351+ #[ test]
1352+ fn parse_resolv_conf_crlf_line_endings ( ) {
1353+ let input = "nameserver 8.8.8.8\r \n nameserver 1.1.1.1\r \n " ;
1354+ assert_eq ! ( parse_resolv_conf( input) , vec![ "8.8.8.8" , "1.1.1.1" ] ) ;
1355+ }
11981356}
0 commit comments