@@ -37,14 +37,15 @@ mod ec2;
3737mod eks;
3838
3939use api:: { settings_view_get, settings_view_set, SettingsViewDelta } ;
40+ use aws_sdk_eks:: types:: IpFamily ;
4041use aws_smithy_experimental:: hyper_1_0:: CryptoMode ;
4142use base64:: Engine ;
4243use bottlerocket_modeled_types:: { KubernetesClusterDnsIp , KubernetesHostnameOverrideSource } ;
4344use imdsclient:: ImdsClient ;
4445use snafu:: { ensure, OptionExt , ResultExt } ;
4546use std:: fs:: File ;
4647use std:: io:: { BufRead , BufReader , Write } ;
47- use std:: net:: IpAddr ;
48+ use std:: net:: { IpAddr , Ipv6Addr } ;
4849use std:: path:: Path ;
4950use std:: str:: FromStr ;
5051use std:: string:: String ;
@@ -220,10 +221,10 @@ async fn get_max_pods_from_file(instance_type: &str, pods_file: &'static str) ->
220221
221222/// Returns the cluster's DNS address.
222223///
223- /// For IPv4 clusters, first it attempts to call EKS describe-cluster to find the `serviceIpv4Cidr` .
224+ /// First attempts to call EKS describe-cluster to find the `ipFamily` and its corresponding CIDR .
224225/// If that works, it returns the expected cluster DNS IP address which is obtained by substituting
225226/// `10` for the last octet. If the EKS call is not successful, it falls back to using IMDS MAC CIDR
226- /// blocks to return one of two default addresses.
227+ /// blocks to return one of two default IPv4 addresses.
227228async fn generate_cluster_dns_ip (
228229 client : & mut ImdsClient ,
229230 aws_k8s_info : & mut SettingsViewDelta ,
@@ -267,17 +268,27 @@ async fn get_eks_network_config(aws_k8s_info: &SettingsViewDelta) -> Result<Opti
267268 . await
268269 . context ( error:: EksSnafu )
269270 {
270- // Derive cluster-dns-ip from the service IPv4 CIDR
271- if let Some ( ipv4_cidr) = config. service_ipv4_cidr {
272- if let Ok ( dns_ip) = get_dns_from_ipv4_cidr ( & ipv4_cidr) {
273- return Ok ( Some ( dns_ip) ) ;
274- }
275- }
271+ return Ok ( get_dns_from_cluster_config ( & config) ) ;
276272 }
277273 }
278274 Ok ( None )
279275}
280276
277+ /// Gets the DNS IP address based on the kubernetes network configuration for
278+ /// the EKS Cluster
279+ fn get_dns_from_cluster_config ( config : & eks:: ClusterNetworkConfig ) -> Option < String > {
280+ match config. ip_family {
281+ Some ( IpFamily :: Ipv6 ) => config
282+ . service_ipv6_cidr
283+ . as_ref ( )
284+ . and_then ( |cidr| get_dns_from_ipv6_cidr ( cidr) . ok ( ) ) ,
285+ _ => config
286+ . service_ipv4_cidr
287+ . as_ref ( )
288+ . and_then ( |cidr| get_dns_from_ipv4_cidr ( cidr) . ok ( ) ) ,
289+ }
290+ }
291+
281292/// Replicates [this] logic from the EKS AMI:
282293///
283294/// ```sh
@@ -296,6 +307,29 @@ fn get_dns_from_ipv4_cidr(cidr: &str) -> Result<String> {
296307 split[ 3 ] = "10" ;
297308 Ok ( split. join ( "." ) )
298309}
310+ /// Replicates [this] logic from the EKS AMI:
311+ ///
312+ /// ```sh
313+ /// DNS_CLUSTER_IP=$(awk -F/ '{print $1}' <<< $SERVICE_IPV6_CIDR)a
314+ /// ```
315+ /// [this]: https://github.com/awslabs/amazon-eks-ami/blob/0f5c129/templates/al2/runtime/bootstrap.sh#L463
316+ fn get_dns_from_ipv6_cidr ( cidr : & str ) -> Result < String > {
317+ // Extract the address part before the slash
318+ let addr_str = cidr. split ( '/' ) . next ( ) . context ( error:: CidrParseSnafu {
319+ cidr,
320+ reason : "missing address component" ,
321+ } ) ?;
322+
323+ let base_addr: Ipv6Addr = addr_str
324+ . parse ( )
325+ . context ( error:: BadIpSnafu { ip : addr_str } ) ?;
326+
327+ // Set the last segment to 0xa (10 in hex)
328+ let mut segments = base_addr. segments ( ) ;
329+ segments[ 7 ] = 0xa ;
330+
331+ Ok ( Ipv6Addr :: from ( segments) . to_string ( ) )
332+ }
299333
300334/// Gets gets the the first VPC IPV4 CIDR block from IMDS. If it starts with `10`, returns
301335/// `10.100.0.10`, otherwise returns `172.20.0.10`
@@ -538,9 +572,11 @@ mod test {
538572 use super :: * ;
539573 use crate :: api:: SettingsViewDelta ;
540574 use api:: SettingsView ;
575+ use aws_sdk_eks:: types:: KubernetesNetworkConfigResponse ;
541576 use bottlerocket_modeled_types:: ValidBase64 ;
542577 use bottlerocket_settings_models:: AwsSettingsV1 ;
543578 use httptest:: { matchers:: * , responders:: * , Expectation , Server } ;
579+ use test_case:: test_case;
544580
545581 #[ test]
546582 fn test_get_dns_from_cidr_ok ( ) {
@@ -557,6 +593,23 @@ mod test {
557593 assert ! ( result. is_err( ) ) ;
558594 }
559595
596+ #[ test_case( "fd00::/108" , "fd00::a" ; "compressed ULA - common in EKS" ) ]
597+ #[ test_case( "2001:db8::/108" , "2001:db8::a" ; "compressed middle" ) ]
598+ #[ test_case( "2001:db8:1234::/108" , "2001:db8:1234::a" ; "partially expanded" ) ]
599+ #[ test_case( "2001:db8:1234:5678::/108" , "2001:db8:1234:5678::a" ; "fully expanded" ) ]
600+ #[ test_case( "fe80::/108" , "fe80::a" ; "link local" ) ]
601+ fn test_get_dns_from_ipv6_cidr_ok ( input : & str , expected : & str ) {
602+ let actual = get_dns_from_ipv6_cidr ( input) . unwrap ( ) ;
603+ assert_eq ! ( expected, actual) ;
604+ }
605+
606+ #[ test_case( "null" ; "no slash" ) ]
607+ #[ test_case( "" ; "empty string" ) ]
608+ fn test_get_dns_from_ipv6_cidr_err ( input : & str ) {
609+ let result = get_dns_from_ipv6_cidr ( input) ;
610+ assert ! ( result. is_err( ) ) ;
611+ }
612+
560613 // Because of test parallelization, serialize the AWS config tests such that
561614 // the AWS_CONFIG_FILE env variable is deterministically set or unset.
562615 #[ test]
@@ -609,6 +662,28 @@ mod test {
609662 assert ! ( env:: var( AWS_CONFIG_FILE_ENV_VAR ) . is_err( ) ) ; // NotPresent
610663 }
611664
665+ #[ test_case( Some ( IpFamily :: Ipv4 ) , Some ( "10.100.0.0/16" ) , None , Some ( "10.100.0.10" ) ; "ipv4 with valid cidr" ) ]
666+ #[ test_case( Some ( IpFamily :: Ipv6 ) , None , Some ( "2001:db8:1234::/108" ) , Some ( "2001:db8:1234::a" ) ; "ipv6 with valid cidr" ) ]
667+ #[ test_case( None , Some ( "172.20.0.0/16" ) , None , Some ( "172.20.0.10" ) ; "default ipv4 family with valid cidr" ) ]
668+ #[ test_case( Some ( IpFamily :: Ipv4 ) , None , None , None ; "missing ipv4 cidr" ) ]
669+ #[ test_case( Some ( IpFamily :: Ipv6 ) , None , None , None ; "missing ipv6 cidr" ) ]
670+ #[ test_case( Some ( IpFamily :: Ipv4 ) , Some ( "invalid" ) , None , None ; "invalid ipv4 cidr" ) ]
671+ #[ test_case( Some ( IpFamily :: Ipv6 ) , None , Some ( "invalid" ) , None ; "invalid ipv6 cidr" ) ]
672+ fn test_dns_from_cluster_config (
673+ ip_family : Option < IpFamily > ,
674+ service_ipv4_cidr : Option < & str > ,
675+ service_ipv6_cidr : Option < & str > ,
676+ expected : Option < & str > ,
677+ ) {
678+ let config = KubernetesNetworkConfigResponse :: builder ( )
679+ . set_ip_family ( ip_family)
680+ . set_service_ipv4_cidr ( service_ipv4_cidr. map ( String :: from) )
681+ . set_service_ipv6_cidr ( service_ipv6_cidr. map ( String :: from) )
682+ . build ( ) ;
683+ let result = get_dns_from_cluster_config ( & config) ;
684+ assert_eq ! ( result. as_deref( ) , expected) ;
685+ }
686+
612687 #[ tokio:: test]
613688 async fn test_hostname_override_source ( ) {
614689 let server = Server :: run ( ) ;
0 commit comments