Skip to content

Commit

Permalink
Added gSMA support while using awsvpc network mode.
Browse files Browse the repository at this point in the history
We use the instance ENIs DNS settings since both the ENIs would be in the same VPC and would be additionally beneficial during domain join.
  • Loading branch information
Harsh Rawat committed May 24, 2021
1 parent 6c045de commit 7871ec0
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 116 deletions.
13 changes: 7 additions & 6 deletions agent/app/agent_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ import (
"sync"
"time"

fsxfactory "github.com/aws/amazon-ecs-agent/agent/fsx/factory"

asmfactory "github.com/aws/amazon-ecs-agent/agent/asm/factory"
"github.com/aws/amazon-ecs-agent/agent/credentials"
"github.com/aws/amazon-ecs-agent/agent/data"
"github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs"
"github.com/aws/amazon-ecs-agent/agent/ecscni"
"github.com/aws/amazon-ecs-agent/agent/engine"
"github.com/aws/amazon-ecs-agent/agent/engine/dockerstate"
"github.com/aws/amazon-ecs-agent/agent/eni/networkutils"
"github.com/aws/amazon-ecs-agent/agent/eni/watcher"
fsxfactory "github.com/aws/amazon-ecs-agent/agent/fsx/factory"
s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory"
"github.com/aws/amazon-ecs-agent/agent/sighandlers"
"github.com/aws/amazon-ecs-agent/agent/sighandlers/exitcodes"
Expand Down Expand Up @@ -76,12 +76,12 @@ func (agent *ecsAgent) initializeTaskENIDependencies(state dockerstate.TaskEngin
return err, false
}

// Query the VPC's primary IPv4 CIDR using IMDS
primaryIPv4VPCCIDR, err := agent.ec2MetadataClient.PrimaryIPV4VPCCIDR(agent.mac)
dnsServerList, err := agent.resourceFields.NetworkUtils.GetDNSServerAddressList(agent.mac)
if err != nil {
return fmt.Errorf("unable to get primary ipv4 cidr of the vpc: %v", err), false
// An error at this point is terminal as the tasks need the access to domain controllers.
return fmt.Errorf("unable to get dns server addresses of instance eni: %v", err), true
}
agent.cfg.PrimaryIPv4VPCCIDR = primaryIPv4VPCCIDR
agent.cfg.InstanceENIDNSServerList = dnsServerList

return nil, false
}
Expand Down Expand Up @@ -305,6 +305,7 @@ func (agent *ecsAgent) initializeResourceFields(credentialsManager credentials.M
Ctx: agent.ctx,
DockerClient: agent.dockerClient,
S3ClientCreator: s3factory.NewS3ClientCreator(),
NetworkUtils: networkutils.New(),
}
}

Expand Down
7 changes: 3 additions & 4 deletions agent/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
package config

import (
"net"
"time"

"github.com/aws/amazon-ecs-agent/agent/dockerclient"
Expand Down Expand Up @@ -334,7 +333,7 @@ type Config struct {
// External specifies whether agent is running on external compute capacity (i.e. outside of aws).
External BooleanDefaultFalse

// PrimaryIPv4VPCCIDR stores the primary IPv4 CIDR of the VPC in which agent is running
// Currently, this field is only populated for Windows and is used during task networking setup
PrimaryIPv4VPCCIDR *net.IPNet
// InstanceENIDNSServerList stores the list of dns servers for the primary instance ENI.
// Currently, this field is only populated for Windows and is used during task networking setup.
InstanceENIDNSServerList []string
}
25 changes: 2 additions & 23 deletions agent/ecscni/netconfig_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
package ecscni

import (
"net"

"github.com/containernetworking/cni/pkg/types"

"github.com/aws/amazon-ecs-agent/agent/api/eni"
Expand All @@ -31,12 +29,8 @@ func NewVPCENIPluginConfigForTaskNSSetup(eni *eni.ENI, cfg *Config) (*libcni.Net
Nameservers: eni.DomainNameServers,
}

if len(eni.DomainNameServers) == 0 && cfg.PrimaryIPv4VPCCIDR != nil {
constructedDNS, err := constructDNSFromVPCCIDR(cfg.PrimaryIPv4VPCCIDR)
if err != nil {
return nil, errors.Wrapf(err, "cannot create vpc-eni network config")
}
dns.Nameservers = constructedDNS
if len(eni.DomainNameSearchList) == 0 && cfg.InstanceENIDNSServerList != nil {
dns.Nameservers = cfg.InstanceENIDNSServerList
}

eniConf := VPCENIPluginConfig{
Expand Down Expand Up @@ -74,18 +68,3 @@ func NewVPCENIPluginConfigForECSBridgeSetup(cfg *Config) (*libcni.NetworkConfig,
networkConfig.Network.Name = ECSBridgeNetworkName
return networkConfig, nil
}

// constructDNSFromVPCCIDR is used to construct DNS server from the primary ipv4 cidr of the vpc.
func constructDNSFromVPCCIDR(vpcCIDR *net.IPNet) ([]string, error) {
// The DNS server maps to a reserved IP address at the base of the VPC IPv4 network rage plus 2
// https://docs.aws.amazon.com/vpc/latest/userguide/VPC_DHCP_Options.html#AmazonDNS

if vpcCIDR == nil {
return nil, errors.Errorf("unable to contruct dns from invalid vpc cidr")
}
mask := net.CIDRMask(24, 32)
maskedIPv4 := vpcCIDR.IP.Mask(mask).To4()
maskedIPv4[3] = 2

return []string{maskedIPv4.String()}, nil
}
29 changes: 4 additions & 25 deletions agent/ecscni/netconfig_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package ecscni

import (
"encoding/json"
"net"
"testing"

apieni "github.com/aws/amazon-ecs-agent/agent/api/eni"
Expand All @@ -28,8 +27,6 @@ const (
linkName = "Ethernet 4"
validVPCGatewayCIDR = "10.0.0.1/24"
validVPCGatewayIPv4Addr = "10.0.0.1"
invalidVPCGatewayCIDR = "10.0.0.300/24"
invalidVPCGatewayAddr = "10.0.0.300"
validDNSServer = "10.0.0.2"
ipv4 = "10.0.0.120"
ipv4CIDR = "10.0.0.120/24"
Expand All @@ -55,12 +52,11 @@ func getTaskENI() *apieni.ENI {
}

func getCNIConfig() *Config {
_, cidr, _ := net.ParseCIDR(vpcCIDR)
return &Config{
MinSupportedCNIVersion: cniMinSupportedVersion,
ContainerID: containerID,
BlockInstanceMetadata: false,
PrimaryIPv4VPCCIDR: cidr,
MinSupportedCNIVersion: cniMinSupportedVersion,
ContainerID: containerID,
BlockInstanceMetadata: false,
InstanceENIDNSServerList: []string{validDNSServer},
}
}

Expand Down Expand Up @@ -100,20 +96,3 @@ func TestNewVPCENIPluginConfigForECSBridgeSetup(t *testing.T) {
assert.True(t, netConfig.UseExistingNetwork)
assert.False(t, netConfig.NoInfraContainer)
}

// TestConstructDNSFromVPCCIDRSuccess tests if the dns is constructed properly from the given vpc's primary cidr
func TestConstructDNSFromVPCCIDRSuccess(t *testing.T) {
_, cidr, _ := net.ParseCIDR(vpcCIDR)
result, err := constructDNSFromVPCCIDR(cidr)

assert.NoError(t, err)
assert.EqualValues(t, []string{validDNSServer}, result)
}

// TestConstructDNSFromVPCCIDRError tests if an error is thrown if the vpc cidr is invalid
func TestConstructDNSFromVPCCIDRError(t *testing.T) {
result, err := constructDNSFromVPCCIDR(nil)

assert.Error(t, err)
assert.Nil(t, result)
}
7 changes: 3 additions & 4 deletions agent/ecscni/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
package ecscni

import (
"net"

"github.com/containernetworking/cni/libcni"
cnitypes "github.com/containernetworking/cni/pkg/types"
)
Expand Down Expand Up @@ -62,8 +60,9 @@ type Config struct {
AdditionalLocalRoutes []cnitypes.IPNet
// NetworkConfigs is the list of CNI network configurations to be invoked
NetworkConfigs []*NetworkConfig
// PrimaryIPv4VPCCIDR is the primary ipv4 cidr of the vpc in which agent is running
PrimaryIPv4VPCCIDR *net.IPNet
// InstanceENIDNSServerList stores the list of dns servers for the primary instance ENI.
// Currently, this field is only populated for Windows and is used during task networking setup.
InstanceENIDNSServerList []string
}

// NetworkConfig wraps CNI library's NetworkConfig object. It tracks the interface device
Expand Down
6 changes: 3 additions & 3 deletions agent/engine/docker_task_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -1526,9 +1526,9 @@ func (engine *DockerTaskEngine) buildCNIConfigFromTaskContainer(
containerInspectOutput *types.ContainerJSON,
includeIPAMConfig bool) (*ecscni.Config, error) {
cniConfig := &ecscni.Config{
BlockInstanceMetadata: engine.cfg.AWSVPCBlockInstanceMetdata.Enabled(),
MinSupportedCNIVersion: config.DefaultMinSupportedCNIVersion,
PrimaryIPv4VPCCIDR: engine.cfg.PrimaryIPv4VPCCIDR,
BlockInstanceMetadata: engine.cfg.AWSVPCBlockInstanceMetdata.Enabled(),
MinSupportedCNIVersion: config.DefaultMinSupportedCNIVersion,
InstanceENIDNSServerList: engine.cfg.InstanceENIDNSServerList,
}
if engine.cfg.OverrideAWSVPCLocalIPv4Address != nil &&
len(engine.cfg.OverrideAWSVPCLocalIPv4Address.IP) != 0 &&
Expand Down
72 changes: 61 additions & 11 deletions agent/eni/networkutils/utils_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ package networkutils
import (
"context"
"net"
"strings"
"syscall"
"time"
"unsafe"

"golang.org/x/sys/windows"

apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors"
"github.com/aws/amazon-ecs-agent/agent/eni/netwrapper"
Expand All @@ -27,14 +32,12 @@ import (
"github.com/pkg/errors"
)

// NetworkUtils is the interface used for accessing network utils
// NetworkUtils is the interface used for accessing network related functionality on Windows.
// The methods declared in this package may or may not add any additional logic over the actual networking api calls.
// Moreover, we will use a wrapper over Golang's net package. This is done to ensure that any future change which
// requires a package different from Golang's net, can be easily implemented by changing the underlying wrapper without
// impacting watcher
type NetworkUtils interface {
GetInterfaceMACByIndex(int, context.Context, time.Duration) (string, error)
GetAllNetworkInterfaces() ([]net.Interface, error)
GetDNSServerAddressList(macAddress string) ([]string, error)
SetNetWrapper(netWrapper netwrapper.NetWrapper)
}

Expand All @@ -50,15 +53,15 @@ type networkUtils struct {
netWrapper netwrapper.NetWrapper
}

// New creates a new network utils
// New creates a new network utils.
func New() NetworkUtils {
return &networkUtils{
netWrapper: netwrapper.New(),
}
}

// This method is used for obtaining the MAC address of an interface with a given interface index
// We internally call net.InterfaceByIndex for this purpose
// GetInterfaceMACByIndex is used for obtaining the MAC address of an interface with a given interface index.
// We internally call net.InterfaceByIndex for this purpose.
func (utils *networkUtils) GetInterfaceMACByIndex(index int, ctx context.Context,
timeout time.Duration) (mac string, err error) {

Expand All @@ -69,8 +72,8 @@ func (utils *networkUtils) GetInterfaceMACByIndex(index int, ctx context.Context
return utils.retrieveMAC()
}

// This method is used to retrieve MAC address using retry with backoff.
// We use retry logic in order to account for any delay in MAC Address becoming available after the interface addition notification is received
// retrieveMAC is used to retrieve MAC address using retry with backoff.
// We use retry logic in order to account for any delay in MAC Address becoming available after the interface addition notification is received.
func (utils *networkUtils) retrieveMAC() (string, error) {
backoff := retry.NewExponentialBackoff(macAddressBackoffMin, macAddressBackoffMax,
macAddressBackoffJitter, macAddressBackoffMultiple)
Expand Down Expand Up @@ -106,12 +109,59 @@ func (utils *networkUtils) retrieveMAC() (string, error) {
return utils.macAddress, nil
}

// Returns all the network interfaces
// GetAllNetworkInterfaces returns all the network interfaces.
func (utils *networkUtils) GetAllNetworkInterfaces() ([]net.Interface, error) {
return utils.netWrapper.GetAllNetworkInterfaces()
}

// This method is used to inject netWrapper instance. This will be handy while testing to inject mocks.
// SetNetWrapper is used to inject netWrapper instance. This will be handy while testing to inject mocks.
func (utils *networkUtils) SetNetWrapper(netWrapper netwrapper.NetWrapper) {
utils.netWrapper = netWrapper
}

// GetDNSServerAddressList returns the DNS server addresses of the queried interface.
func (utils *networkUtils) GetDNSServerAddressList(macAddress string) ([]string, error) {
addresses, err := utils.netWrapper.GetAdapterAddresses()
if err != nil {
return nil, err
}

var firstDnsNode *windows.IpAdapterDnsServerAdapter
// Find the adapter which has the same mac as queried.
for _, adapterAddr := range addresses {
if strings.EqualFold(utils.parseMACAddress(adapterAddr).String(), macAddress) {
firstDnsNode = adapterAddr.FirstDnsServerAddress
break
}
}

dnsServerAddressList := make([]string, 0)
for firstDnsNode != nil {
dnsServerAddressList = append(dnsServerAddressList, utils.parseSocketAddress(firstDnsNode.Address))
firstDnsNode = firstDnsNode.Next
}

return dnsServerAddressList, nil
}

// parseMACAddress parses the physical address of windows.IpAdapterAddresses into net.HardwareAddr.
func (utils *networkUtils) parseMACAddress(adapterAddress *windows.IpAdapterAddresses) net.HardwareAddr {
hardwareAddr := make(net.HardwareAddr, adapterAddress.PhysicalAddressLength)
if adapterAddress.PhysicalAddressLength > 0 {
copy(hardwareAddr, adapterAddress.PhysicalAddress[:])
return hardwareAddr
}
return hardwareAddr
}

// parseSocketAddress parses the SocketAddress into its string representation.
// This method needs to be deprecated in favour of IP() method of SocketAdress introduced in Go 1.13+.
// The method details have been taken from https://github.com/golang/sys/blob/release-branch.go1.13/windows/types_windows.go
func (utils *networkUtils) parseSocketAddress(addr windows.SocketAddress) string {
var ipAddr string
if uintptr(addr.SockaddrLength) >= unsafe.Sizeof(syscall.RawSockaddrInet4{}) && addr.Sockaddr.Addr.Family == syscall.AF_INET {
ip := net.IP((*syscall.RawSockaddrInet4)(unsafe.Pointer(addr.Sockaddr)).Addr[:])
ipAddr = ip.String()
}
return ipAddr
}
40 changes: 38 additions & 2 deletions agent/eni/networkutils/utils_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import (
"context"
"errors"
"net"
"syscall"
"testing"

"golang.org/x/sys/windows"

mock_netwrapper "github.com/aws/amazon-ecs-agent/agent/eni/netwrapper/mocks"

"github.com/golang/mock/gomock"
Expand All @@ -30,6 +33,7 @@ import (
const (
interfaceIndex = 9
macAddress = "02:22:ea:8c:81:dc"
validDnsServer = "10.0.0.2"
)

// This is a success test. We receive the appropriate MAC address corresponding to the interface index.
Expand Down Expand Up @@ -145,7 +149,7 @@ func TestGetInterfaceMACByIndexWithGolangNetError(t *testing.T) {
netUtils.SetNetWrapper(mocknetwrapper)

mocknetwrapper.EXPECT().FindInterfaceByIndex(interfaceIndex).Return(
nil, errors.New("Unable to retrieve interface"))
nil, errors.New("unable to retrieve interface"))

mac, err := netUtils.GetInterfaceMACByIndex(interfaceIndex, ctx, macAddressBackoffMax)

Expand Down Expand Up @@ -190,11 +194,43 @@ func TestGetAllNetworkInterfacesError(t *testing.T) {
netUtils.SetNetWrapper(mocknetwrapper)

mocknetwrapper.EXPECT().GetAllNetworkInterfaces().Return(
nil, errors.New("Error occured while fetching interfaces"),
nil, errors.New("error occurred while fetching interfaces"),
)

inf, err := netUtils.GetAllNetworkInterfaces()

assert.Nil(t, inf)
assert.Error(t, err)
}

// TestGetDNSServerAddressList tests the success path of GetDNSServerAddressList.
func TestGetDNSServerAddressList(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mocknetwrapper := mock_netwrapper.NewMockNetWrapper(mockCtrl)
netUtils := networkUtils{netWrapper: mocknetwrapper}

mocknetwrapper.EXPECT().GetAdapterAddresses().Return([]*windows.IpAdapterAddresses{
{
PhysicalAddressLength: 6,
PhysicalAddress: [8]byte{2, 34, 234, 140, 129, 220, 0, 0},
FirstDnsServerAddress: &windows.IpAdapterDnsServerAdapter{
Address: windows.SocketAddress{
Sockaddr: &syscall.RawSockaddrAny{
Addr: syscall.RawSockaddr{
Family: syscall.AF_INET,
Data: [14]int8{0, 0, 10, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0},
},
},
SockaddrLength: 16,
},
},
},
}, nil)

dnsServerList, err := netUtils.GetDNSServerAddressList(macAddress)
assert.NoError(t, err)
assert.Len(t, dnsServerList, 1)
assert.EqualValues(t, dnsServerList[0], validDnsServer)
}
File renamed without changes.
Loading

0 comments on commit 7871ec0

Please sign in to comment.