Skip to content

Commit

Permalink
EC2 IMDS IPv6 Support (#4006)
Browse files Browse the repository at this point in the history
  • Loading branch information
skmcgrail authored Jul 14, 2021
1 parent 341af66 commit fa9c8c0
Show file tree
Hide file tree
Showing 16 changed files with 551 additions and 96 deletions.
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

0 comments on commit fa9c8c0

Please sign in to comment.