Skip to content

Commit

Permalink
Merge pull request #2910 from rawahars/feature/ec2-awsvpc-windows
Browse files Browse the repository at this point in the history
EC2 task networking on Windows: Implementation change for blocking IMDS
  • Loading branch information
fenxiong authored Jun 22, 2021
2 parents ed69574 + 4dbd7d4 commit 4c22a2b
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 268 deletions.
28 changes: 0 additions & 28 deletions agent/ecscni/mocks/namespace_helper_mocks.go

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

10 changes: 2 additions & 8 deletions agent/ecscni/namespace_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,19 @@ import (
"github.com/containernetworking/cni/pkg/types/current"
)

// execCmdExecutorFnType is the method signature for execCmdExecutorFn.
type execCmdExecutorFnType func(commands []string, separator string) error

// NamespaceHelper defines the methods for performing additional actions to setup/clean the task namespace.
// Task namespace in awsvpc network mode is configured using pause container which is the first container
// launched for the task. These commands are executed inside that container.
type NamespaceHelper interface {
ConfigureTaskNamespaceRouting(ctx context.Context, taskENI *apieni.ENI, config *Config, result *current.Result) error
ConfigureFirewallForTaskNSSetup(taskENI *apieni.ENI, config *Config) error
ConfigureFirewallForTaskNSCleanup(taskENI *apieni.ENI, config *Config) error
}

// helper is the client for executing methods of NamespaceHelper interface.
type helper struct {
dockerClient dockerapi.DockerClient
execCmdExecutor execCmdExecutorFnType
dockerClient dockerapi.DockerClient
}

// NewNamespaceHelper returns a new instance of NamespaceHelper interface.
func NewNamespaceHelper(client dockerapi.DockerClient) NamespaceHelper {
return &helper{dockerClient: client, execCmdExecutor: execCmdExecutorFn}
return &helper{dockerClient: client}
}
14 changes: 0 additions & 14 deletions agent/ecscni/namespace_helper_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,8 @@ import (
"github.com/containernetworking/cni/pkg/types/current"
)

var execCmdExecutorFn execCmdExecutorFnType = nil

// ConfigureTaskNamespaceRouting executes the commands required for setting up appropriate routing inside task namespace.
// This is applicable only for Windows.
func (nsHelper *helper) ConfigureTaskNamespaceRouting(ctx context.Context, taskENI *apieni.ENI, config *Config, result *current.Result) error {
return nil
}

// ConfigureFirewallForTaskNSSetup executes the commands, if required, to setup firewall rules for disabling IMDS access from task.
// This is applicable only for Windows.
func (nsHelper *helper) ConfigureFirewallForTaskNSSetup(taskENI *apieni.ENI, config *Config) error {
return nil
}

// ConfigureFirewallForTaskNSCleanup executes the commands, if required, to cleanup the firewall rules created during setup.
// This is applicable only for Windows.
func (nsHelper *helper) ConfigureFirewallForTaskNSCleanup(taskENI *apieni.ENI, config *Config) error {
return nil
}
14 changes: 0 additions & 14 deletions agent/ecscni/namespace_helper_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,8 @@ import (
"github.com/containernetworking/cni/pkg/types/current"
)

var execCmdExecutorFn execCmdExecutorFnType = nil

// ConfigureTaskNamespaceRouting executes the commands required for setting up appropriate routing inside task namespace.
// This is applicable only for Windows.
func (nsHelper *helper) ConfigureTaskNamespaceRouting(ctx context.Context, taskENI *apieni.ENI, config *Config, result *current.Result) error {
return nil
}

// ConfigureFirewallForTaskNSSetup executes the commands, if required, to setup firewall rules for disabling IMDS access from task.
// This is applicable only for Windows.
func (nsHelper *helper) ConfigureFirewallForTaskNSSetup(taskENI *apieni.ENI, config *Config) error {
return nil
}

// ConfigureFirewallForTaskNSCleanup executes the commands, if required, to cleanup the firewall rules created during setup.
// This is applicable only for Windows.
func (nsHelper *helper) ConfigureFirewallForTaskNSCleanup(taskENI *apieni.ENI, config *Config) error {
return nil
}
120 changes: 21 additions & 99 deletions agent/ecscni/namespace_helper_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@
package ecscni

import (
"bytes"
"context"
"fmt"
"net"
"os/exec"
"strings"

apieni "github.com/aws/amazon-ecs-agent/agent/api/eni"
Expand All @@ -41,31 +39,24 @@ const (
// imdsEndpointIPAddress is the IP address of the endpoint for accessing IMDS.
imdsEndpointIPAddress = "169.254.169.254/32"
// ecsBridgeEndpointNameFormat is the name format of the ecs-bridge endpoint in the task namespace.
ecsBridgeEndpointNameFormat = "%s-ep-%s"
ecsBridgeEndpointNameFormat = "vEthernet (%s-ep-%s)"
// taskPrimaryEndpointNameFormat is the name format of the primary endpoint in the task namespace.
taskPrimaryEndpointNameFormat = "%sbr%s-ep-%s"
// blockIMDSFirewallRuleNameFormat is the format of firewall rule name for blocking IMDS from task namespace.
blockIMDSFirewallRuleNameFormat = "Disable IMDS for %s"
// ecsBridgeRouteAddCmdFormat is the format of command for adding route entry through ECS Bridge.
ecsBridgeRouteAddCmdFormat = `netsh interface ipv4 add route prefix=%s interface="vEthernet (%s)"`
// ecsBridgeRouteDeleteCmdFormat is the format of command for deleting route entry of ECS bridge endpoint.
ecsBridgeRouteDeleteCmdFormat = `netsh interface ipv4 delete route prefix=%s interface="vEthernet (%s)"`
// checkExistingFirewallRuleCmdFormat is the format of the command to check if the firewall rule exists.
checkExistingFirewallRuleCmdFormat = `netsh advfirewall firewall show rule name="%s" >nul`
// addFirewallRuleCmdFormat is the format of command for creating firewall rule on Windows.
addFirewallRuleCmdFormat = `netsh advfirewall firewall add rule name="%s" dir=out localip=%s remoteip=%s action=block`
// deleteFirewallRuleCmdFormat is the format of the command to delete a firewall rule on Windows.
deleteFirewallRuleCmdFormat = `netsh advfirewall firewall delete rule name="%s" dir=out`
taskPrimaryEndpointNameFormat = "vEthernet (%sbr%s-ep-%s)"
// loopbackInterfaceName is the name of the loopback interface.
loopbackInterfaceName = "Loopback"
// windowsRouteAddCmdFormat is the format of command for adding route entry on Windows.
windowsRouteAddCmdFormat = `netsh interface ipv4 add route prefix=%s interface="%s"`
// windowsRouteDeleteCmdFormat is the format of command for deleting route entry on Windowsx.
windowsRouteDeleteCmdFormat = `netsh interface ipv4 delete route prefix=%s interface="%s"`
)

var execCmdExecutorFn execCmdExecutorFnType = execCmdExecutor

// ConfigureTaskNamespaceRouting executes the commands required for setting up appropriate routing inside task namespace.
// The commands currently executed are-
// netsh interface ipv4 delete route prefix=0.0.0.0/0 interface="vEthernet (nat-ep-<container_id>)
// netsh interface ipv4 delete route prefix=<ecs-bridge-subnet-cidr> interface="vEthernet (nat-ep-<container_id>)
// netsh interface ipv4 add route prefix=169.254.170.2/32 interface="vEthernet (nat-ep-<container_id>)
// netsh interface ipv4 add route prefix=169.254.169.254/32 interface="vEthernet (task-br-<mac>-ep-<container_id>)
// netsh interface ipv4 add route prefix=169.254.169.254/32 interface="Loopback"
// netsh interface ipv4 add route prefix=<local-route> interface="vEthernet (nat-ep-<container_id>)
func (nsHelper *helper) ConfigureTaskNamespaceRouting(ctx context.Context, taskENI *apieni.ENI, config *Config, result *current.Result) error {
// Obtain the ecs-bridge endpoint's subnet IP address from the CNI plugin execution result.
Expand All @@ -76,19 +67,25 @@ func (nsHelper *helper) ConfigureTaskNamespaceRouting(ctx context.Context, taskE
ecsBridgeEndpointName := fmt.Sprintf(ecsBridgeEndpointNameFormat, ECSBridgeNetworkName, config.ContainerID)

// Prepare the commands to be executed inside task namespace to setup the ECS Bridge.
defaultRouteDeletionCmd := fmt.Sprintf(ecsBridgeRouteDeleteCmdFormat, windowsDefaultRoute, ecsBridgeEndpointName)
defaultSubnetRouteDeletionCmd := fmt.Sprintf(ecsBridgeRouteDeleteCmdFormat, ecsBridgeSubnetIPAddress.String(),
defaultRouteDeletionCmd := fmt.Sprintf(windowsRouteDeleteCmdFormat, windowsDefaultRoute, ecsBridgeEndpointName)
defaultSubnetRouteDeletionCmd := fmt.Sprintf(windowsRouteDeleteCmdFormat, ecsBridgeSubnetIPAddress.String(),
ecsBridgeEndpointName)
credentialsAddressRouteAdditionCmd := fmt.Sprintf(ecsBridgeRouteAddCmdFormat, credentialsEndpointRoute, ecsBridgeEndpointName)
credentialsAddressRouteAdditionCmd := fmt.Sprintf(windowsRouteAddCmdFormat, credentialsEndpointRoute, ecsBridgeEndpointName)
commands := []string{defaultRouteDeletionCmd, defaultSubnetRouteDeletionCmd, credentialsAddressRouteAdditionCmd}

if !config.BlockInstanceMetadata {
// For blocking instance metadata, create a black hole route inside the task namespace.
// This route will redirect all the packets sent to IMDS endpoint through its loopback interface.
// If IMDS is required, then create an explicit route through the primary interface of the task.
if config.BlockInstanceMetadata {
blockIMDSRouteCommand := fmt.Sprintf(windowsRouteAddCmdFormat, imdsEndpointIPAddress, loopbackInterfaceName)
commands = append(commands, blockIMDSRouteCommand)
} else {
// This naming convention is drawn from the way CNI plugin names the endpoints.
// https://github.com/aws/amazon-vpc-cni-plugins/blob/master/plugins/vpc-eni/network/network_windows.go
taskPrimaryEndpointId := strings.Replace(strings.ToLower(taskENI.MacAddress), ":", "", -1)
taskPrimaryEndpointName := fmt.Sprintf(taskPrimaryEndpointNameFormat, TaskHNSNetworkNamePrefix,
taskPrimaryEndpointId, config.ContainerID)
imdsRouteAdditionCmd := fmt.Sprintf(ecsBridgeRouteAddCmdFormat, imdsEndpointIPAddress, taskPrimaryEndpointName)
imdsRouteAdditionCmd := fmt.Sprintf(windowsRouteAddCmdFormat, imdsEndpointIPAddress, taskPrimaryEndpointName)
commands = append(commands, imdsRouteAdditionCmd)
}

Expand All @@ -98,7 +95,7 @@ func (nsHelper *helper) ConfigureTaskNamespaceRouting(ctx context.Context, taskE
IP: route.IP,
Mask: route.Mask,
}
additionalRouteAdditionCmd := fmt.Sprintf(ecsBridgeRouteAddCmdFormat, ipRoute.String(), ecsBridgeEndpointName)
additionalRouteAdditionCmd := fmt.Sprintf(windowsRouteAddCmdFormat, ipRoute.String(), ecsBridgeEndpointName)
commands = append(commands, additionalRouteAdditionCmd)
}

Expand All @@ -110,56 +107,6 @@ func (nsHelper *helper) ConfigureTaskNamespaceRouting(ctx context.Context, taskE
return nil
}

// ConfigureFirewallForTaskNSSetup executes the commands, if required, to setup firewall rules for disabling IMDS access from task.
// The commands executed are-
// netsh advfirewall firewall add rule name="Disable IMDS for <ENI IP>" dir=out localip=<ENI IP> remoteip=169.254.170.2/32 action=block
func (nsHelper *helper) ConfigureFirewallForTaskNSSetup(taskENI *apieni.ENI, config *Config) error {
if config.BlockInstanceMetadata {
if taskENI == nil {
return errors.New("failed to configure firewall due to invalid task eni")
}

firewallRuleName := fmt.Sprintf(blockIMDSFirewallRuleNameFormat, taskENI.GetPrimaryIPv4Address())

checkExistingFirewallRule := fmt.Sprintf(checkExistingFirewallRuleCmdFormat, firewallRuleName)
blockIMDSFirewallRuleCreationCmd := fmt.Sprintf(addFirewallRuleCmdFormat, firewallRuleName,
taskENI.GetPrimaryIPv4Address(), imdsEndpointIPAddress)

// Invoke the generated command on the host to add the firewall rule.
// Separator is "||" as either the firewall rule should exist or a new one should be created.
err := nsHelper.invokeCommandsOnHost([]string{checkExistingFirewallRule, blockIMDSFirewallRuleCreationCmd}, " || ")
if err != nil {
return errors.Wrapf(err, "failed to create firewall rule to disable imds")
}
}

return nil
}

// ConfigureFirewallForTaskNSCleanup executes the commands, if required, to cleanup the firewall rules created during setup.
// The commands executed are-
// netsh advfirewall firewall delete rule name="Disable IMDS for <ENI IP>" dir=out
func (nsHelper *helper) ConfigureFirewallForTaskNSCleanup(taskENI *apieni.ENI, config *Config) error {
if config.BlockInstanceMetadata {
if taskENI == nil {
return errors.New("failed to configure firewall due to invalid task eni")
}

firewallRuleName := fmt.Sprintf(blockIMDSFirewallRuleNameFormat, taskENI.GetPrimaryIPv4Address())

// Delete the firewall rule created for blocking IMDS access by the task.
checkExistingFirewallRule := fmt.Sprintf(checkExistingFirewallRuleCmdFormat, firewallRuleName)
blockIMDSFirewallRuleDeletionCmd := fmt.Sprintf(deleteFirewallRuleCmdFormat, firewallRuleName)

// The separator would be "&&" to ensure if the firewall rule exists then delete it.
// An error at this point means that the firewall rule was not present and was therefore not deleted.
// Hence, skip returning the error as it is redundant.
nsHelper.invokeCommandsOnHost([]string{checkExistingFirewallRule, blockIMDSFirewallRuleDeletionCmd}, " && ")
}

return nil
}

// invokeCommandsInsideContainer executes a set of commands inside the container namespace.
func (nsHelper *helper) invokeCommandsInsideContainer(ctx context.Context, containerID string, commands []string, separator string) error {

Expand Down Expand Up @@ -202,28 +149,3 @@ func (nsHelper *helper) invokeCommandsInsideContainer(ctx context.Context, conta

return nil
}

// invokeCommandsOnHost invokes given commands on the host instance using the executeFn.
func (nsHelper *helper) invokeCommandsOnHost(commands []string, separator string) error {
return nsHelper.execCmdExecutor(commands, separator)
}

// execCmdExecutor invokes given commands on the host instance.
func execCmdExecutor(commands []string, separator string) error {
seelog.Debugf("[ECSCNI] Executing commands on host: %v", commands)

// Concatenate all the commands into a single command.
execCommands := strings.Join(commands, separator)

cmd := exec.Command("cmd", "/C", execCommands)
var stdout bytes.Buffer
cmd.Stdout = &stdout

err := cmd.Run()
if err != nil {
seelog.Errorf("[ECSCNI] Failed to execute command on host: %v: %s", err, stdout.String())
return err
}

return nil
}
Loading

0 comments on commit 4c22a2b

Please sign in to comment.