Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EC2 IMDS IPv6 Support #4006

Merged
merged 4 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
14 changes: 0 additions & 14 deletions aws/endpoints/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}
Expand Down
5 changes: 0 additions & 5 deletions aws/endpoints/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
55 changes: 0 additions & 55 deletions aws/endpoints/defaults.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 53 additions & 2 deletions aws/endpoints/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,43 @@ 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

// S3 Regional Endpoint flag helps with resolving the S3 endpoint
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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -333,13 +363,23 @@ 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,
p: p.p,
}
}

// 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
}

Expand Down Expand Up @@ -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,
Expand Down
49 changes: 47 additions & 2 deletions aws/endpoints/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
}
}
}
36 changes: 36 additions & 0 deletions aws/endpoints/v3model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
Loading