diff --git a/ecs-agent/netlib/platform/cniconf_linux_test.go b/ecs-agent/netlib/platform/cniconf_linux_test.go index d4022831baa..2ced996a5b8 100644 --- a/ecs-agent/netlib/platform/cniconf_linux_test.go +++ b/ecs-agent/netlib/platform/cniconf_linux_test.go @@ -202,6 +202,7 @@ func getTestV2NInterface() *networkinterface.NetworkInterface { IPV4Addresses: []*networkinterface.IPV4Address{ { Address: networkinterface.DefaultGeneveInterfaceIPAddress, + Primary: true, }, }, TunnelProperties: &networkinterface.TunnelProperties{ @@ -209,6 +210,9 @@ func getTestV2NInterface() *networkinterface.NetworkInterface { DestinationIPAddress: destinationIP, DestinationPort: destinationPort, }, - DeviceName: fmt.Sprintf(networkinterface.GeneveInterfaceNamePattern, vni, destinationPort), + DeviceName: fmt.Sprintf(networkinterface.GeneveInterfaceNamePattern, vni, destinationPort), + Name: secondaryENIName, + DomainNameServers: []string{nameServer}, + DomainNameSearchList: []string{searchDomainName}, } } diff --git a/ecs-agent/netlib/platform/common_linux.go b/ecs-agent/netlib/platform/common_linux.go index c864db10364..1620ebca60f 100644 --- a/ecs-agent/netlib/platform/common_linux.go +++ b/ecs-agent/netlib/platform/common_linux.go @@ -407,8 +407,7 @@ func (c *common) createDNSConfig( // Next, copy these files into a task volume, which can be used by containers as well, to // configure their network. - configFiles := []string{HostsFileName, ResolveConfFileName, HostnameFileName} - if err := c.copyNetworkConfigFilesToTask(taskID, netNS.Name, configFiles); err != nil { + if err := c.copyNetworkConfigFilesToTask(taskID, netNS.Name); err != nil { return err } return nil @@ -454,7 +453,8 @@ func (c *common) createNetworkConfigFiles(netNSName string, primaryIF *networkin // copyNetworkConfigFilesToTask copies the contents of the DNS config files for a // task into the task volume. -func (c *common) copyNetworkConfigFilesToTask(taskID, netNSName string, configFiles []string) error { +func (c *common) copyNetworkConfigFilesToTask(taskID, netNSName string) error { + configFiles := []string{HostsFileName, ResolveConfFileName, HostnameFileName} for _, file := range configFiles { source := filepath.Join(networkConfigFileDirectory, netNSName, file) err := c.dnsVolumeAccessor.CopyToVolume(taskID, source, file, networkConfigFileMode) @@ -583,8 +583,11 @@ func (c *common) configureInterface( err = c.configureBranchENI(ctx, netNSPath, iface) case networkinterface.V2NInterfaceAssociationProtocol: err = c.configureGENEVEInterface(ctx, netNSPath, iface, netDAO) + case networkinterface.VETHInterfaceAssociationProtocol: + // Do nothing. + return nil default: - err = errors.New("invalid interface association protocol %s" + iface.InterfaceAssociationProtocol) + err = errors.New("invalid interface association protocol " + iface.InterfaceAssociationProtocol) } return err } diff --git a/ecs-agent/netlib/platform/common_test.go b/ecs-agent/netlib/platform/common_test.go index 367505df8e1..732902d26e7 100644 --- a/ecs-agent/netlib/platform/common_test.go +++ b/ecs-agent/netlib/platform/common_test.go @@ -35,6 +35,8 @@ const ( deviceName = "eth1" eniMAC = "f0:5c:89:a3:ab:01" subnetGatewayCIDR = "10.1.0.1/24" + primaryENIName = "primary-eni" + secondaryENIName = "secondary-eni" ) func getTestInterface() *networkinterface.NetworkInterface { @@ -69,5 +71,6 @@ func getTestInterface() *networkinterface.NetworkInterface { InterfaceAssociationProtocol: networkinterface.DefaultInterfaceAssociationProtocol, KnownStatus: status.NetworkNone, DesiredStatus: status.NetworkReadyPull, + Name: primaryENIName, } } diff --git a/ecs-agent/netlib/platform/firecracker_debug_linux.go b/ecs-agent/netlib/platform/firecracker_debug_linux.go index 1f1f3588d3c..cb4f8bd0e86 100644 --- a/ecs-agent/netlib/platform/firecracker_debug_linux.go +++ b/ecs-agent/netlib/platform/firecracker_debug_linux.go @@ -7,5 +7,10 @@ type firecrackerDebug struct { } func (fc *firecrackerDebug) CreateDNSConfig(taskID string, netNS *tasknetworkconfig.NetworkNamespace) error { - return fc.common.createDNSConfig(taskID, true, netNS) + err := fc.common.createDNSConfig(taskID, true, netNS) + if err != nil { + return err + } + + return fc.configureSecondaryDNSConfig(taskID, netNS) } diff --git a/ecs-agent/netlib/platform/firecracker_linux.go b/ecs-agent/netlib/platform/firecracker_linux.go index 127d063cfa7..85915782d80 100644 --- a/ecs-agent/netlib/platform/firecracker_linux.go +++ b/ecs-agent/netlib/platform/firecracker_linux.go @@ -15,7 +15,6 @@ package platform import ( "context" - "errors" "fmt" netlibdata "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/data" @@ -27,6 +26,7 @@ import ( "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/tasknetworkconfig" "github.com/aws/aws-sdk-go/aws" + "github.com/pkg/errors" ) type firecraker struct { @@ -51,7 +51,12 @@ func (f *firecraker) BuildTaskNetworkConfiguration( } func (f *firecraker) CreateDNSConfig(taskID string, netNS *tasknetworkconfig.NetworkNamespace) error { - return f.common.createDNSConfig(taskID, false, netNS) + err := f.common.createDNSConfig(taskID, false, netNS) + if err != nil { + return err + } + + return f.configureSecondaryDNSConfig(taskID, netNS) } func (f *firecraker) ConfigureInterface( @@ -76,6 +81,33 @@ func (f *firecraker) ConfigureServiceConnect( return errors.New("not implemented") } +// configureSecondaryDNSConfig creates DNS config files for secondary interfaces. This is required because +// on FoF, secondary interfaces reside in their own network namespace inside the microVM. The DNS config +// inside the namespace will need to be the secondary interface DNS config. +func (f *firecraker) configureSecondaryDNSConfig(taskID string, netNS *tasknetworkconfig.NetworkNamespace) error { + for _, iface := range netNS.NetworkInterfaces { + // Omit primary interface and veth interfaces. + if iface.IsPrimary() || iface.VETHProperties != nil { + continue + } + + // Create DNS files. + dnsDirName := networkinterface.NetNSName(taskID, iface.Name) + err := f.common.createNetworkConfigFiles(dnsDirName, iface) + if err != nil { + return errors.Wrapf(err, "failed to create DNS config for interface %s", iface.Name) + } + + // Copy to task volume. + err = f.common.copyNetworkConfigFilesToTask(taskID, dnsDirName) + if err != nil { + return errors.Wrapf(err, "failed to create DNS config for interface %s", iface.Name) + } + } + + return nil +} + // assignInterfacesToNamespaces computes how many network namespaces the task needs and assigns // each network interface to a network namespace. func assignInterfacesToNamespaces(taskPayload *ecsacs.Task) (map[string]string, error) { diff --git a/ecs-agent/netlib/platform/firecracker_linux_test.go b/ecs-agent/netlib/platform/firecracker_linux_test.go new file mode 100644 index 00000000000..49f5df3bf26 --- /dev/null +++ b/ecs-agent/netlib/platform/firecracker_linux_test.go @@ -0,0 +1,144 @@ +//go:build !windows && unit +// +build !windows,unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package platform + +import ( + "fmt" + "io/fs" + "os" + "testing" + + mock_ecscni "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/ecscni/mocks_nsutil" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/tasknetworkconfig" + mock_ioutilwrapper "github.com/aws/amazon-ecs-agent/ecs-agent/utils/ioutilwrapper/mocks" + mock_oswrapper "github.com/aws/amazon-ecs-agent/ecs-agent/utils/oswrapper/mocks" + mock_volume "github.com/aws/amazon-ecs-agent/ecs-agent/volume/mocks" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +// TestFirecracker_CreateDNSConfig checks if DNS config files gets created for +// both primary and secondary interfaces. +func TestFirecracker_CreateDNSConfig(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + taskID := "task-id" + iface := getTestInterface() + primaryNetNSName := networkinterface.NetNSName(taskID, iface.Name) + primaryNetNSPath := "/etc/netns/" + primaryNetNSName + + v2nIface := getTestV2NInterface() + secondaryNetNSName := networkinterface.NetNSName(taskID, v2nIface.Name) + secondaryNetNSPath := "/etc/netns/" + secondaryNetNSName + + netns := &tasknetworkconfig.NetworkNamespace{ + Name: primaryNetNSName, + Path: primaryNetNSPath, + NetworkInterfaces: []*networkinterface.NetworkInterface{iface, v2nIface}, + } + + ioutil := mock_ioutilwrapper.NewMockIOUtil(ctrl) + nsUtil := mock_ecscni.NewMockNetNSUtil(ctrl) + osWrapper := mock_oswrapper.NewMockOS(ctrl) + mockFile := mock_oswrapper.NewMockFile(ctrl) + volumeAccessor := mock_volume.NewMockTaskVolumeAccessor(ctrl) + commonPlatform := common{ + ioutil: ioutil, + nsUtil: nsUtil, + os: osWrapper, + dnsVolumeAccessor: volumeAccessor, + } + + fc := &firecraker{ + common: commonPlatform, + } + + // Test creation of hosts file. + primaryHostsData := fmt.Sprintf("%s\n%s %s\n%s %s\n%s %s\n", + HostsLocalhostEntry, + ipv4Addr, dnsName, + addr, hostName, + addr2, hostName2, + ) + primaryResolvData := fmt.Sprintf("nameserver %s\nnameserver %s\nsearch %s\n", + nameServer, + nameServer2, + searchDomainName+" "+searchDomainName2, + ) + primaryHostnameData := fmt.Sprintf("%s\n", iface.GetHostname()) + + secondaryHostsData := fmt.Sprintf("%s\n%s %s\n", + HostsLocalhostEntry, + networkinterface.DefaultGeneveInterfaceIPAddress, "", + ) + secondaryResolvData := fmt.Sprintf("nameserver %s\nsearch %s\n", + nameServer, + searchDomainName, + ) + secondaryHostnameData := "\n" + + gomock.InOrder( + // Creation of netns path. + osWrapper.EXPECT().Stat(primaryNetNSPath).Return(nil, os.ErrNotExist).Times(1), + osWrapper.EXPECT().IsNotExist(os.ErrNotExist).Return(true).Times(1), + osWrapper.EXPECT().MkdirAll(primaryNetNSPath, fs.FileMode(0644)), + + // Creation of resolv.conf file for primary interface. + nsUtil.EXPECT().BuildResolvConfig(iface.DomainNameServers, iface.DomainNameSearchList).Return(primaryResolvData).Times(1), + ioutil.EXPECT().WriteFile(primaryNetNSPath+"/resolv.conf", []byte(primaryResolvData), fs.FileMode(0644)), + + // Creation of hostname file for primary interface. + ioutil.EXPECT().WriteFile(primaryNetNSPath+"/hostname", []byte(primaryHostnameData), fs.FileMode(0644)), + osWrapper.EXPECT().OpenFile("/etc/hostname", os.O_RDONLY|os.O_CREATE, fs.FileMode(0644)).Return(mockFile, nil).Times(1), + + // Creation of hosts file for primary interface. + mockFile.EXPECT().Close().Times(1), + ioutil.EXPECT().WriteFile(primaryNetNSPath+"/hosts", []byte(primaryHostsData), fs.FileMode(0644)), + + // CopyToVolume created files into task volume for primary interface. + volumeAccessor.EXPECT().CopyToVolume(taskID, primaryNetNSPath+"/hosts", "hosts", fs.FileMode(0644)).Return(nil).Times(1), + volumeAccessor.EXPECT().CopyToVolume(taskID, primaryNetNSPath+"/resolv.conf", "resolv.conf", fs.FileMode(0644)).Return(nil).Times(1), + volumeAccessor.EXPECT().CopyToVolume(taskID, primaryNetNSPath+"/hostname", "hostname", fs.FileMode(0644)).Return(nil).Times(1), + + // Creation of secondary netns path. + osWrapper.EXPECT().Stat(secondaryNetNSPath).Return(nil, os.ErrNotExist).Times(1), + osWrapper.EXPECT().IsNotExist(os.ErrNotExist).Return(true).Times(1), + osWrapper.EXPECT().MkdirAll(secondaryNetNSPath, fs.FileMode(0644)), + + // Creation of resolv.conf file for secondary interface. + nsUtil.EXPECT().BuildResolvConfig(v2nIface.DomainNameServers, v2nIface.DomainNameSearchList).Return(secondaryResolvData).Times(1), + ioutil.EXPECT().WriteFile(secondaryNetNSPath+"/resolv.conf", []byte(secondaryResolvData), fs.FileMode(0644)), + + // Creation of hostname file for secondary interface. + ioutil.EXPECT().WriteFile(secondaryNetNSPath+"/hostname", []byte(secondaryHostnameData), fs.FileMode(0644)), + osWrapper.EXPECT().OpenFile("/etc/hostname", os.O_RDONLY|os.O_CREATE, fs.FileMode(0644)).Return(mockFile, nil).Times(1), + + // Creation of hosts file for secondary interface. + mockFile.EXPECT().Close().Times(1), + ioutil.EXPECT().WriteFile(secondaryNetNSPath+"/hosts", []byte(secondaryHostsData), fs.FileMode(0644)), + + // CopyToVolume created files into task volume for secondary interface. + volumeAccessor.EXPECT().CopyToVolume(taskID, secondaryNetNSPath+"/hosts", "hosts", fs.FileMode(0644)).Return(nil).Times(1), + volumeAccessor.EXPECT().CopyToVolume(taskID, secondaryNetNSPath+"/resolv.conf", "resolv.conf", fs.FileMode(0644)).Return(nil).Times(1), + volumeAccessor.EXPECT().CopyToVolume(taskID, secondaryNetNSPath+"/hostname", "hostname", fs.FileMode(0644)).Return(nil).Times(1), + ) + err := fc.CreateDNSConfig(taskID, netns) + require.NoError(t, err) +}