From fa9c8c094337f02b894d04dab77a7118d93ccde0 Mon Sep 17 00:00:00 2001 From: Sean McGrail Date: Tue, 13 Jul 2021 21:21:00 -0700 Subject: [PATCH] EC2 IMDS IPv6 Support (#4006) --- CHANGELOG_PENDING.md | 5 ++ aws/endpoints/decode.go | 14 ---- aws/endpoints/decode_test.go | 5 -- aws/endpoints/defaults.go | 55 ---------------- aws/endpoints/endpoints.go | 55 +++++++++++++++- aws/endpoints/endpoints_test.go | 49 +++++++++++++- aws/endpoints/v3model.go | 36 ++++++++++ aws/endpoints/v3model_test.go | 90 +++++++++++++++++++++++++ aws/session/doc.go | 2 +- aws/session/env_config.go | 27 +++++++- aws/session/env_config_test.go | 68 +++++++++++++++++-- aws/session/session.go | 53 +++++++++++---- aws/session/session_test.go | 101 +++++++++++++++++++++++++++++ aws/session/shared_config.go | 30 +++++++++ aws/session/shared_config_test.go | 41 ++++++++++++ aws/session/testdata/shared_config | 16 +++++ 16 files changed, 551 insertions(+), 96 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8a1927a39ca..65369076006 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,4 +1,9 @@ ### SDK Features +* `aws/session`: Support has been added for EC2 IPv6-enabled Instance Metadata Service Endpoints ([#4006](https://github.com/aws/aws-sdk-go/pull/4006)) + * Adds support for `AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE` environment variable which may specify `IPv6` or `IPv4` for selecting the desired endpoint. + * Adds support for `ec2_metadata_service_endpoint_mode` AWS profile key, which may specify `IPv6` or `IPv4` for selecting the desired endpoint. Has lower precedence then `AWS_EC2_METADATA_SERVICE_ENDPOINT`. + * Adds support for `ec2_metadata_service_endpoint` AWS profile key, which may specify an explicit endpoint URI. Has higher precedence then `ec2_metadata_service_endpoint_mode`. +* `aws/endpoints`: Supported has been added for EC2 IPv6-enabled Instance Metadata Service Endpoints ([#4006](https://github.com/aws/aws-sdk-go/pull/4006)) ### SDK Enhancements diff --git a/aws/endpoints/decode.go b/aws/endpoints/decode.go index 654fb1ad52d..b98ea869810 100644 --- a/aws/endpoints/decode.go +++ b/aws/endpoints/decode.go @@ -81,7 +81,6 @@ func decodeV3Endpoints(modelDef modelDefinition, opts DecodeModelOptions) (Resol // Customization for i := 0; i < len(ps); i++ { p := &ps[i] - custAddEC2Metadata(p) custAddS3DualStack(p) custRegionalS3(p) custRmIotDataService(p) @@ -140,19 +139,6 @@ func custAddDualstack(p *partition, svcName string) { p.Services[svcName] = s } -func custAddEC2Metadata(p *partition) { - p.Services["ec2metadata"] = service{ - IsRegionalized: boxedFalse, - PartitionEndpoint: "aws-global", - Endpoints: endpoints{ - "aws-global": endpoint{ - Hostname: "169.254.169.254/latest", - Protocols: []string{"http"}, - }, - }, - } -} - func custRmIotDataService(p *partition) { delete(p.Services, "data.iot") } diff --git a/aws/endpoints/decode_test.go b/aws/endpoints/decode_test.go index bed125c7e9c..44376b3223c 100644 --- a/aws/endpoints/decode_test.go +++ b/aws/endpoints/decode_test.go @@ -68,11 +68,6 @@ func TestDecodeEndpoints_V3(t *testing.T) { if a, e := s3Defaults.DualStackHostname, "{service}.dualstack.{region}.{dnsSuffix}"; a != e { t.Errorf("expect s3 dualstack host pattern to be %q, got %q", e, a) } - - ec2metaEndpoint := p.Services["ec2metadata"].Endpoints["aws-global"] - if a, e := ec2metaEndpoint.Hostname, "169.254.169.254/latest"; a != e { - t.Errorf("expect ec2metadata host to be %q, got %q", e, a) - } } func TestDecodeEndpoints_NoPartitions(t *testing.T) { diff --git a/aws/endpoints/defaults.go b/aws/endpoints/defaults.go index 8e9ad170b52..4693c43f183 100644 --- a/aws/endpoints/defaults.go +++ b/aws/endpoints/defaults.go @@ -2396,17 +2396,6 @@ var awsPartition = partition{ "us-west-2": endpoint{}, }, }, - "ec2metadata": service{ - PartitionEndpoint: "aws-global", - IsRegionalized: boxedFalse, - - Endpoints: endpoints{ - "aws-global": endpoint{ - Hostname: "169.254.169.254/latest", - Protocols: []string{"http"}, - }, - }, - }, "ecs": service{ Endpoints: endpoints{ @@ -7862,17 +7851,6 @@ var awscnPartition = partition{ "cn-northwest-1": endpoint{}, }, }, - "ec2metadata": service{ - PartitionEndpoint: "aws-global", - IsRegionalized: boxedFalse, - - Endpoints: endpoints{ - "aws-global": endpoint{ - Hostname: "169.254.169.254/latest", - Protocols: []string{"http"}, - }, - }, - }, "ecs": service{ Endpoints: endpoints{ @@ -9113,17 +9091,6 @@ var awsusgovPartition = partition{ }, }, }, - "ec2metadata": service{ - PartitionEndpoint: "aws-global", - IsRegionalized: boxedFalse, - - Endpoints: endpoints{ - "aws-global": endpoint{ - Hostname: "169.254.169.254/latest", - Protocols: []string{"http"}, - }, - }, - }, "ecs": service{ Endpoints: endpoints{ @@ -10596,17 +10563,6 @@ var awsisoPartition = partition{ "us-iso-east-1": endpoint{}, }, }, - "ec2metadata": service{ - PartitionEndpoint: "aws-global", - IsRegionalized: boxedFalse, - - Endpoints: endpoints{ - "aws-global": endpoint{ - Hostname: "169.254.169.254/latest", - Protocols: []string{"http"}, - }, - }, - }, "ecs": service{ Endpoints: endpoints{ @@ -11013,17 +10969,6 @@ var awsisobPartition = partition{ "us-isob-east-1": endpoint{}, }, }, - "ec2metadata": service{ - PartitionEndpoint: "aws-global", - IsRegionalized: boxedFalse, - - Endpoints: endpoints{ - "aws-global": endpoint{ - Hostname: "169.254.169.254/latest", - Protocols: []string{"http"}, - }, - }, - }, "ecs": service{ Endpoints: endpoints{ diff --git a/aws/endpoints/endpoints.go b/aws/endpoints/endpoints.go index ca956e5f12a..8e8636f5f85 100644 --- a/aws/endpoints/endpoints.go +++ b/aws/endpoints/endpoints.go @@ -48,6 +48,9 @@ type Options struct { // This option is ignored if StrictMatching is enabled. ResolveUnknownService bool + // Specifies the EC2 Instance Metadata Service default endpoint selection mode (IPv4 or IPv6) + EC2MetadataEndpointMode EC2IMDSEndpointModeState + // STS Regional Endpoint flag helps with resolving the STS endpoint STSRegionalEndpoint STSRegionalEndpoint @@ -55,6 +58,33 @@ type Options struct { S3UsEast1RegionalEndpoint S3UsEast1RegionalEndpoint } +// EC2IMDSEndpointModeState is an enum configuration variable describing the client endpoint mode. +type EC2IMDSEndpointModeState uint + +// Enumeration values for EC2IMDSEndpointModeState +const ( + EC2IMDSEndpointModeStateUnset EC2IMDSEndpointModeState = iota + EC2IMDSEndpointModeStateIPv4 + EC2IMDSEndpointModeStateIPv6 +) + +// SetFromString sets the EC2IMDSEndpointModeState based on the provided string value. Unknown values will default to EC2IMDSEndpointModeStateUnset +func (e *EC2IMDSEndpointModeState) SetFromString(v string) error { + v = strings.TrimSpace(v) + + switch { + case len(v) == 0: + *e = EC2IMDSEndpointModeStateUnset + case strings.EqualFold(v, "IPv6"): + *e = EC2IMDSEndpointModeStateIPv6 + case strings.EqualFold(v, "IPv4"): + *e = EC2IMDSEndpointModeStateIPv4 + default: + return fmt.Errorf("unknown EC2 IMDS endpoint mode, must be either IPv6 or IPv4") + } + return nil +} + // STSRegionalEndpoint is an enum for the states of the STS Regional Endpoint // options. type STSRegionalEndpoint int @@ -247,7 +277,7 @@ func RegionsForService(ps []Partition, partitionID, serviceID string) (map[strin if p.ID() != partitionID { continue } - if _, ok := p.p.Services[serviceID]; !ok { + if _, ok := p.p.Services[serviceID]; !(ok || serviceID == Ec2metadataServiceID) { break } @@ -333,6 +363,7 @@ func (p Partition) Regions() map[string]Region { // enumerating over the services in a partition. func (p Partition) Services() map[string]Service { ss := make(map[string]Service, len(p.p.Services)) + for id := range p.p.Services { ss[id] = Service{ id: id, @@ -340,6 +371,15 @@ func (p Partition) Services() map[string]Service { } } + // Since we have removed the customization that injected this into the model + // we still need to pretend that this is a modeled service. + if _, ok := ss[Ec2metadataServiceID]; !ok { + ss[Ec2metadataServiceID] = Service{ + id: Ec2metadataServiceID, + p: p.p, + } + } + return ss } @@ -400,7 +440,18 @@ func (s Service) ResolveEndpoint(region string, opts ...func(*Options)) (Resolve // an URL that can be resolved to a instance of a service. func (s Service) Regions() map[string]Region { rs := map[string]Region{} - for id := range s.p.Services[s.id].Endpoints { + + service, ok := s.p.Services[s.id] + + // Since ec2metadata customization has been removed we need to check + // if it was defined in non-standard endpoints.json file. If it's not + // then we can return the empty map as there is no regional-endpoints for IMDS. + // Otherwise, we iterate need to iterate the non-standard model. + if s.id == Ec2metadataServiceID && !ok { + return rs + } + + for id := range service.Endpoints { if r, ok := s.p.Regions[id]; ok { rs[id] = Region{ id: id, diff --git a/aws/endpoints/endpoints_test.go b/aws/endpoints/endpoints_test.go index 7c569258db1..b2002ca05cc 100644 --- a/aws/endpoints/endpoints_test.go +++ b/aws/endpoints/endpoints_test.go @@ -38,7 +38,8 @@ func TestEnumPartitionServices(t *testing.T) { svcEnum := partEnum.Services() - if a, e := len(svcEnum), len(expectPart.Services); a != e { + // Expect the number of services in the partition + ec2metadata + if a, e := len(svcEnum), len(expectPart.Services)+1; a != e { t.Errorf("expected %d regions, got %d", e, a) } } @@ -109,7 +110,8 @@ func TestEnumServicesEndpoints(t *testing.T) { ss := p.Services() - if a, e := len(ss), 5; a != e { + // Expect the number of services in the partition + ec2metadata + if a, e := len(ss), 6; a != e { t.Errorf("expect %d regions got %d", e, a) } @@ -346,3 +348,46 @@ func TestPartitionForRegion_NotFound(t *testing.T) { t.Errorf("expect no partition to be found, got %v", actual) } } + +func TestEC2MetadataEndpoint(t *testing.T) { + cases := []struct { + Options Options + Expected string + }{ + { + Expected: ec2MetadataEndpointIPv4, + }, + { + Options: Options{ + EC2MetadataEndpointMode: EC2IMDSEndpointModeStateIPv4, + }, + Expected: ec2MetadataEndpointIPv4, + }, + { + Options: Options{ + EC2MetadataEndpointMode: EC2IMDSEndpointModeStateIPv6, + }, + Expected: ec2MetadataEndpointIPv6, + }, + } + + for _, p := range DefaultPartitions() { + var region string + for r := range p.Regions() { + region = r + break + } + + for _, c := range cases { + endpoint, err := p.EndpointFor("ec2metadata", region, func(options *Options) { + *options = c.Options + }) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + if e, a := c.Expected, endpoint.URL; e != a { + t.Errorf("exect %v, got %v", e, a) + } + } + } +} diff --git a/aws/endpoints/v3model.go b/aws/endpoints/v3model.go index aaff6826081..c6c6a033876 100644 --- a/aws/endpoints/v3model.go +++ b/aws/endpoints/v3model.go @@ -7,6 +7,11 @@ import ( "strings" ) +const ( + ec2MetadataEndpointIPv6 = "http://[fd00:ec2::254]/latest" + ec2MetadataEndpointIPv4 = "http://169.254.169.254/latest" +) + var regionValidationRegex = regexp.MustCompile(`^[[:alnum:]]([[:alnum:]\-]*[[:alnum:]])?$`) type partitions []partition @@ -102,6 +107,12 @@ func (p partition) EndpointFor(service, region string, opts ...func(*Options)) ( opt.Set(opts...) s, hasService := p.Services[service] + + if service == Ec2metadataServiceID && !hasService { + endpoint := getEC2MetadataEndpoint(p.ID, service, opt.EC2MetadataEndpointMode) + return endpoint, nil + } + if len(service) == 0 || !(hasService || opt.ResolveUnknownService) { // Only return error if the resolver will not fallback to creating // endpoint based on service endpoint ID passed in. @@ -129,6 +140,31 @@ func (p partition) EndpointFor(service, region string, opts ...func(*Options)) ( return e.resolve(service, p.ID, region, p.DNSSuffix, defs, opt) } +func getEC2MetadataEndpoint(partitionID, service string, mode EC2IMDSEndpointModeState) ResolvedEndpoint { + switch mode { + case EC2IMDSEndpointModeStateIPv6: + return ResolvedEndpoint{ + URL: ec2MetadataEndpointIPv6, + PartitionID: partitionID, + SigningRegion: "aws-global", + SigningName: service, + SigningNameDerived: true, + SigningMethod: "v4", + } + case EC2IMDSEndpointModeStateIPv4: + fallthrough + default: + return ResolvedEndpoint{ + URL: ec2MetadataEndpointIPv4, + PartitionID: partitionID, + SigningRegion: "aws-global", + SigningName: service, + SigningNameDerived: true, + SigningMethod: "v4", + } + } +} + func serviceList(ss services) []string { list := make([]string, 0, len(ss)) for k := range ss { diff --git a/aws/endpoints/v3model_test.go b/aws/endpoints/v3model_test.go index ef3da0c1e64..196196d9c4e 100644 --- a/aws/endpoints/v3model_test.go +++ b/aws/endpoints/v3model_test.go @@ -666,3 +666,93 @@ func TestResolveEndpoint_FipsAwsGlobal(t *testing.T) { t.Errorf("expect the signing name to be derived") } } + +func TestEC2MetadataService(t *testing.T) { + unmodelled := partition{ + ID: "unmodelled", + Name: "partition with unmodelled ec2metadata", + Services: map[string]service{ + "foo": { + Endpoints: endpoints{ + "us-west-2": endpoint{ + Hostname: "foo.us-west-2.amazonaws.com", + Protocols: []string{"http"}, + SignatureVersions: []string{"v4"}, + }, + }, + }, + }, + Regions: map[string]region{ + "us-west-2": {Description: "us-west-2 region"}, + }, + } + + modelled := partition{ + ID: "modelled", + Name: "partition with modelled ec2metadata", + Services: map[string]service{ + "ec2metadata": { + Endpoints: endpoints{ + "us-west-2": endpoint{ + Hostname: "custom.localhost/latest", + Protocols: []string{"http"}, + SignatureVersions: []string{"v4"}, + }, + }, + }, + "foo": { + Endpoints: endpoints{ + "us-west-2": endpoint{ + Hostname: "foo.us-west-2.amazonaws.com", + Protocols: []string{"http"}, + SignatureVersions: []string{"v4"}, + }, + }, + }, + }, + Regions: map[string]region{ + "us-west-2": {Description: "us-west-2 region"}, + }, + } + + uServices := unmodelled.Partition().Services() + + if s, ok := uServices[Ec2metadataServiceID]; !ok { + t.Errorf("expect ec2metadata to be present") + } else { + if regions := s.Regions(); len(regions) != 0 { + t.Errorf("expect no regions for ec2metadata, got %v", len(regions)) + } + if resolved, err := unmodelled.EndpointFor(Ec2metadataServiceID, "us-west-2"); err != nil { + t.Errorf("expect no error, got %v", err) + } else if e, a := ec2MetadataEndpointIPv4, resolved.URL; e != a { + t.Errorf("expect %v, got %v", e, a) + } + } + + if s, ok := uServices["foo"]; !ok { + t.Errorf("expect foo to be present") + } else if regions := s.Regions(); len(regions) == 0 { + t.Errorf("expect region endpoints for foo. got none") + } + + mServices := modelled.Partition().Services() + + if s, ok := mServices[Ec2metadataServiceID]; !ok { + t.Errorf("expect ec2metadata to be present") + } else if regions := s.Regions(); len(regions) == 0 { + t.Errorf("expect region for ec2metadata, got none") + } else { + if resolved, err := modelled.EndpointFor(Ec2metadataServiceID, "us-west-2"); err != nil { + t.Errorf("expect no error, got %v", err) + } else if e, a := "http://custom.localhost/latest", resolved.URL; e != a { + t.Errorf("expect %v, got %v", e, a) + } + } + + if s, ok := mServices["foo"]; !ok { + t.Errorf("expect foo to be present") + } else if regions := s.Regions(); len(regions) == 0 { + t.Errorf("expect region endpoints for foo, got none") + } +} diff --git a/aws/session/doc.go b/aws/session/doc.go index 9419b518d58..43b56863e43 100644 --- a/aws/session/doc.go +++ b/aws/session/doc.go @@ -283,7 +283,7 @@ component must be enclosed in square brackets. The custom EC2 IMDS endpoint can also be specified via the Session options. sess, err := session.NewSessionWithOptions(session.Options{ - EC2IMDSEndpoint: "http://[::1]", + EC2MetadataEndpoint: "http://[::1]", }) */ package session diff --git a/aws/session/env_config.go b/aws/session/env_config.go index 3cd5d4b5ae1..fffe2f350c0 100644 --- a/aws/session/env_config.go +++ b/aws/session/env_config.go @@ -161,10 +161,15 @@ type envConfig struct { // AWS_S3_USE_ARN_REGION=true S3UseARNRegion bool - // Specifies the alternative endpoint to use for EC2 IMDS. + // Specifies the EC2 Instance Metadata Service endpoint to use. If specified it overrides EC2IMDSEndpointMode. // // AWS_EC2_METADATA_SERVICE_ENDPOINT=http://[::1] EC2IMDSEndpoint string + + // Specifies the EC2 Instance Metadata Service default endpoint selection mode (IPv4 or IPv6) + // + // AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE=IPv6 + EC2IMDSEndpointMode endpoints.EC2IMDSEndpointModeState } var ( @@ -231,6 +236,9 @@ var ( ec2IMDSEndpointEnvKey = []string{ "AWS_EC2_METADATA_SERVICE_ENDPOINT", } + ec2IMDSEndpointModeEnvKey = []string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE", + } useCABundleKey = []string{ "AWS_CA_BUNDLE", } @@ -364,6 +372,9 @@ func envConfigLoad(enableSharedConfig bool) (envConfig, error) { } setFromEnvVal(&cfg.EC2IMDSEndpoint, ec2IMDSEndpointEnvKey) + if err := setEC2IMDSEndpointMode(&cfg.EC2IMDSEndpointMode, ec2IMDSEndpointModeEnvKey); err != nil { + return envConfig{}, err + } return cfg, nil } @@ -376,3 +387,17 @@ func setFromEnvVal(dst *string, keys []string) { } } } + +func setEC2IMDSEndpointMode(mode *endpoints.EC2IMDSEndpointModeState, keys []string) error { + for _, k := range keys { + value := os.Getenv(k) + if len(value) == 0 { + continue + } + if err := mode.SetFromString(value); err != nil { + return fmt.Errorf("invalid value for environment variable, %s=%s, %v", k, value, err) + } + return nil + } + return nil +} diff --git a/aws/session/env_config_test.go b/aws/session/env_config_test.go index a1d30a45e83..deb201109cc 100644 --- a/aws/session/env_config_test.go +++ b/aws/session/env_config_test.go @@ -106,6 +106,7 @@ func TestLoadEnvConfig(t *testing.T) { Env map[string]string UseSharedConfigCall bool Config envConfig + WantErr bool }{ 0: { Env: map[string]string{ @@ -356,6 +357,63 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, + 21: { + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv6", + }, + Config: envConfig{ + EC2IMDSEndpointMode: endpoints.EC2IMDSEndpointModeStateIPv6, + SharedCredentialsFile: shareddefaults.SharedCredentialsFilename(), + SharedConfigFile: shareddefaults.SharedConfigFilename(), + }, + }, + 22: { + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv4", + }, + Config: envConfig{ + EC2IMDSEndpointMode: endpoints.EC2IMDSEndpointModeStateIPv4, + SharedCredentialsFile: shareddefaults.SharedCredentialsFilename(), + SharedConfigFile: shareddefaults.SharedConfigFilename(), + }, + }, + 23: { + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "foobar", + }, + WantErr: true, + }, + 24: { + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT": "http://endpoint.localhost", + }, + Config: envConfig{ + EC2IMDSEndpoint: "http://endpoint.localhost", + SharedCredentialsFile: shareddefaults.SharedCredentialsFilename(), + SharedConfigFile: shareddefaults.SharedConfigFilename(), + }, + }, + 25: { + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv6", + "AWS_EC2_METADATA_SERVICE_ENDPOINT": "http://endpoint.localhost", + }, + Config: envConfig{ + EC2IMDSEndpoint: "http://endpoint.localhost", + EC2IMDSEndpointMode: endpoints.EC2IMDSEndpointModeStateIPv6, + SharedCredentialsFile: shareddefaults.SharedCredentialsFilename(), + SharedConfigFile: shareddefaults.SharedConfigFilename(), + }, + }, + 26: { + Env: map[string]string{ + "AWS_EC2_METADATA_DISABLED": "false", + }, + Config: envConfig{ + SharedCredentialsFile: shareddefaults.SharedCredentialsFilename(), + SharedConfigFile: shareddefaults.SharedConfigFilename(), + }, + }, } for i, c := range cases { @@ -370,13 +428,15 @@ func TestLoadEnvConfig(t *testing.T) { var err error if c.UseSharedConfigCall { cfg, err = loadSharedEnvConfig() - if err != nil { - t.Errorf("failed to load shared env config, %v", err) + if (err != nil) != c.WantErr { + t.Errorf("WantErr=%v, got err=%v", c.WantErr, err) + return } } else { cfg, err = loadEnvConfig() - if err != nil { - t.Errorf("failed to load env config, %v", err) + if (err != nil) != c.WantErr { + t.Errorf("WantErr=%v, got err=%v", c.WantErr, err) + return } } diff --git a/aws/session/session.go b/aws/session/session.go index 038ae222ffc..4b2e057e936 100644 --- a/aws/session/session.go +++ b/aws/session/session.go @@ -283,8 +283,8 @@ type Options struct { Handlers request.Handlers // Allows specifying a custom endpoint to be used by the EC2 IMDS client - // when making requests to the EC2 IMDS API. The must endpoint value must - // include protocol prefix. + // when making requests to the EC2 IMDS API. The endpoint value should + // include the URI scheme. If the scheme is not present it will be defaulted to http. // // If unset, will the EC2 IMDS client will use its default endpoint. // @@ -298,6 +298,11 @@ type Options struct { // // AWS_EC2_METADATA_SERVICE_ENDPOINT=http://[::1] EC2IMDSEndpoint string + + // Specifies the EC2 Instance Metadata Service default endpoint selection mode (IPv4 or IPv6) + // + // AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE=IPv6 + EC2IMDSEndpointMode endpoints.EC2IMDSEndpointModeState } // NewSessionWithOptions returns a new Session created from SDK defaults, config files, @@ -375,19 +380,23 @@ func Must(sess *Session, err error) *Session { // Wraps the endpoint resolver with a resolver that will return a custom // endpoint for EC2 IMDS. -func wrapEC2IMDSEndpoint(resolver endpoints.Resolver, endpoint string) endpoints.Resolver { +func wrapEC2IMDSEndpoint(resolver endpoints.Resolver, endpoint string, mode endpoints.EC2IMDSEndpointModeState) endpoints.Resolver { return endpoints.ResolverFunc( func(service, region string, opts ...func(*endpoints.Options)) ( endpoints.ResolvedEndpoint, error, ) { - if service == ec2MetadataServiceID { + if service == ec2MetadataServiceID && len(endpoint) > 0 { return endpoints.ResolvedEndpoint{ URL: endpoint, SigningName: ec2MetadataServiceID, SigningRegion: region, }, nil + } else if service == ec2MetadataServiceID { + opts = append(opts, func(o *endpoints.Options) { + o.EC2MetadataEndpointMode = mode + }) } - return resolver.EndpointFor(service, region) + return resolver.EndpointFor(service, region, opts...) }) } @@ -404,8 +413,8 @@ func deprecatedNewSession(envCfg envConfig, cfgs ...*aws.Config) *Session { cfg.EndpointResolver = endpoints.DefaultResolver() } - if len(envCfg.EC2IMDSEndpoint) != 0 { - cfg.EndpointResolver = wrapEC2IMDSEndpoint(cfg.EndpointResolver, envCfg.EC2IMDSEndpoint) + if !(len(envCfg.EC2IMDSEndpoint) == 0 && envCfg.EC2IMDSEndpointMode == endpoints.EC2IMDSEndpointModeStateUnset) { + cfg.EndpointResolver = wrapEC2IMDSEndpoint(cfg.EndpointResolver, envCfg.EC2IMDSEndpoint, envCfg.EC2IMDSEndpointMode) } cfg.Credentials = defaults.CredChain(cfg, handlers) @@ -737,12 +746,32 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config, endpoints.LegacyS3UsEast1Endpoint, }) - ec2IMDSEndpoint := sessOpts.EC2IMDSEndpoint - if len(ec2IMDSEndpoint) == 0 { - ec2IMDSEndpoint = envCfg.EC2IMDSEndpoint + var ec2IMDSEndpoint string + for _, v := range []string{ + sessOpts.EC2IMDSEndpoint, + envCfg.EC2IMDSEndpoint, + sharedCfg.EC2IMDSEndpoint, + } { + if len(v) != 0 { + ec2IMDSEndpoint = v + break + } } - if len(ec2IMDSEndpoint) != 0 { - cfg.EndpointResolver = wrapEC2IMDSEndpoint(cfg.EndpointResolver, ec2IMDSEndpoint) + + var endpointMode endpoints.EC2IMDSEndpointModeState + for _, v := range []endpoints.EC2IMDSEndpointModeState{ + sessOpts.EC2IMDSEndpointMode, + envCfg.EC2IMDSEndpointMode, + sharedCfg.EC2IMDSEndpointMode, + } { + if v != endpoints.EC2IMDSEndpointModeStateUnset { + endpointMode = v + break + } + } + + if len(ec2IMDSEndpoint) != 0 || endpointMode != endpoints.EC2IMDSEndpointModeStateUnset { + cfg.EndpointResolver = wrapEC2IMDSEndpoint(cfg.EndpointResolver, ec2IMDSEndpoint, endpointMode) } // Configure credentials if not already set by the user when creating the diff --git a/aws/session/session_test.go b/aws/session/session_test.go index fd1df282099..a03d6d014db 100644 --- a/aws/session/session_test.go +++ b/aws/session/session_test.go @@ -699,6 +699,107 @@ func TestSession_ClientConfig_ResolveEndpoint(t *testing.T) { }, ExpectEndpoint: "http://correct.example.aws", }, + "IMDS custom endpoint from profile": { + Service: ec2MetadataServiceID, + Options: Options{ + SharedConfigState: SharedConfigEnable, + SharedConfigFiles: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpoint", + }, + Region: "ignored", + ExpectEndpoint: "http://endpoint.localhost", + }, + "IMDS custom endpoint from profile and env": { + Service: ec2MetadataServiceID, + Options: Options{ + SharedConfigFiles: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpoint", + }, + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT": "http://endpoint-env.localhost", + }, + Region: "ignored", + ExpectEndpoint: "http://endpoint-env.localhost", + }, + "IMDS IPv6 mode from profile": { + Service: ec2MetadataServiceID, + Options: Options{ + SharedConfigState: SharedConfigEnable, + SharedConfigFiles: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpointModeIPv6", + }, + Region: "ignored", + ExpectEndpoint: "http://[fd00:ec2::254]/latest", + }, + "IMDS IPv4 mode from profile": { + Service: ec2MetadataServiceID, + Options: Options{ + SharedConfigFiles: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpointModeIPv4", + }, + Region: "ignored", + ExpectEndpoint: "http://169.254.169.254/latest", + }, + "IMDS IPv6 mode in env": { + Service: ec2MetadataServiceID, + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv6", + }, + Region: "ignored", + ExpectEndpoint: "http://[fd00:ec2::254]/latest", + }, + "IMDS IPv4 mode in env": { + Service: ec2MetadataServiceID, + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv4", + }, + Region: "ignored", + ExpectEndpoint: "http://169.254.169.254/latest", + }, + "IMDS mode in env and profile": { + Service: ec2MetadataServiceID, + Options: Options{ + SharedConfigFiles: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpointModeIPv4", + }, + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv6", + }, + Region: "ignored", + ExpectEndpoint: "http://[fd00:ec2::254]/latest", + }, + "IMDS mode and endpoint profile": { + Service: ec2MetadataServiceID, + Options: Options{ + SharedConfigState: SharedConfigEnable, + SharedConfigFiles: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpointAndModeMixed", + }, + Region: "ignored", + ExpectEndpoint: "http://endpoint.localhost", + }, + "IMDS mode session option and env": { + Service: ec2MetadataServiceID, + Options: Options{ + EC2IMDSEndpointMode: endpoints.EC2IMDSEndpointModeStateIPv6, + }, + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv4", + }, + Region: "ignored", + ExpectEndpoint: "http://[fd00:ec2::254]/latest", + }, + "IMDS endpoint session option and env": { + Service: ec2MetadataServiceID, + Options: Options{ + EC2IMDSEndpoint: "http://endpoint.localhost", + }, + Env: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT": "http://endpoint-env.localhost", + }, + Region: "ignored", + ExpectEndpoint: "http://endpoint.localhost", + }, } for name, c := range cases { diff --git a/aws/session/shared_config.go b/aws/session/shared_config.go index 42b16a7db9e..6830ece70ff 100644 --- a/aws/session/shared_config.go +++ b/aws/session/shared_config.go @@ -66,6 +66,12 @@ const ( // S3 ARN Region Usage s3UseARNRegionKey = "s3_use_arn_region" + + // EC2 IMDS Endpoint Mode + ec2MetadataServiceEndpointModeKey = "ec2_metadata_service_endpoint_mode" + + // EC2 IMDS Endpoint + ec2MetadataServiceEndpointKey = "ec2_metadata_service_endpoint" ) // sharedConfig represents the configuration fields of the SDK config files. @@ -145,6 +151,16 @@ type sharedConfig struct { // // s3_use_arn_region=true S3UseARNRegion bool + + // Specifies the EC2 Instance Metadata Service default endpoint selection mode (IPv4 or IPv6) + // + // ec2_metadata_service_endpoint_mode=IPv6 + EC2IMDSEndpointMode endpoints.EC2IMDSEndpointModeState + + // Specifies the EC2 Instance Metadata Service endpoint to use. If specified it overrides EC2IMDSEndpointMode. + // + // ec2_metadata_service_endpoint=http://fd00:ec2::254 + EC2IMDSEndpoint string } type sharedConfigFile struct { @@ -334,6 +350,12 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile, e updateString(&cfg.SSORegion, section, ssoRegionKey) updateString(&cfg.SSORoleName, section, ssoRoleNameKey) updateString(&cfg.SSOStartURL, section, ssoStartURL) + + if err := updateEC2MetadataServiceEndpointMode(&cfg.EC2IMDSEndpointMode, section, ec2MetadataServiceEndpointModeKey); err != nil { + return fmt.Errorf("failed to load %s from shared config, %s, %v", + ec2MetadataServiceEndpointModeKey, file.Filename, err) + } + updateString(&cfg.EC2IMDSEndpoint, section, ec2MetadataServiceEndpointKey) } updateString(&cfg.CredentialProcess, section, credentialProcessKey) @@ -364,6 +386,14 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile, e return nil } +func updateEC2MetadataServiceEndpointMode(endpointMode *endpoints.EC2IMDSEndpointModeState, section ini.Section, key string) error { + if !section.Has(key) { + return nil + } + value := section.String(key) + return endpointMode.SetFromString(value) +} + func (cfg *sharedConfig) validateCredentialsConfig(profile string) error { if err := cfg.validateCredentialsRequireARN(profile); err != nil { return err diff --git a/aws/session/shared_config_test.go b/aws/session/shared_config_test.go index 00bdfb0d0ba..2fc198218c0 100644 --- a/aws/session/shared_config_test.go +++ b/aws/session/shared_config_test.go @@ -294,6 +294,47 @@ func TestLoadSharedConfig(t *testing.T) { CredentialProcess: "/path/to/process", }, }, + { + Filenames: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpoint", + Expected: sharedConfig{ + Profile: "EC2MetadataServiceEndpoint", + EC2IMDSEndpoint: "http://endpoint.localhost", + }, + }, + { + Filenames: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpointModeIPv6", + Expected: sharedConfig{ + Profile: "EC2MetadataServiceEndpointModeIPv6", + EC2IMDSEndpointMode: endpoints.EC2IMDSEndpointModeStateIPv6, + }, + }, + { + Filenames: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpointModeIPv4", + Expected: sharedConfig{ + Profile: "EC2MetadataServiceEndpointModeIPv4", + EC2IMDSEndpointMode: endpoints.EC2IMDSEndpointModeStateIPv4, + }, + }, + { + Filenames: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpointModeUnknown", + Expected: sharedConfig{ + Profile: "EC2MetadataServiceEndpointModeUnknown", + }, + Err: fmt.Errorf("failed to load ec2_metadata_service_endpoint_mode from shared config"), + }, + { + Filenames: []string{testConfigFilename}, + Profile: "EC2MetadataServiceEndpointAndModeMixed", + Expected: sharedConfig{ + Profile: "EC2MetadataServiceEndpointAndModeMixed", + EC2IMDSEndpoint: "http://endpoint.localhost", + EC2IMDSEndpointMode: endpoints.EC2IMDSEndpointModeStateIPv6, + }, + }, } for i, c := range cases { diff --git a/aws/session/testdata/shared_config b/aws/session/testdata/shared_config index 63148032bc6..62324c9c919 100644 --- a/aws/session/testdata/shared_config +++ b/aws/session/testdata/shared_config @@ -147,3 +147,19 @@ sso_region = us-west-2 sso_role_name = TestRole sso_start_url = https://127.0.0.1/start credential_process = /path/to/process + +[profile EC2MetadataServiceEndpoint] +ec2_metadata_service_endpoint = http://endpoint.localhost + +[profile EC2MetadataServiceEndpointModeIPv6] +ec2_metadata_service_endpoint_mode = IPv6 + +[profile EC2MetadataServiceEndpointModeIPv4] +ec2_metadata_service_endpoint_mode = IPv4 + +[profile EC2MetadataServiceEndpointModeUnknown] +ec2_metadata_service_endpoint_mode = foobar + +[profile EC2MetadataServiceEndpointAndModeMixed] +ec2_metadata_service_endpoint = http://endpoint.localhost +ec2_metadata_service_endpoint_mode = IPv6