diff --git a/README.md b/README.md index 427045c8f04..af94837f425 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,6 @@ additional details on each available environment variable. | `ECS_WARM_POOLS_CHECK` | `true` | Whether to ensure instances going into an [EC2 Auto Scaling group warm pool](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-warm-pools.html) are prevented from being registered with the cluster. Set to true only if using EC2 Autoscaling | `false` | `false` | | `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` | `false` | By default, the ecs-init service adds an iptable rule to drop non-local packets to localhost if they're not part of an existing forwarded connection or DNAT, and removes the rule upon stop. If this is set to true, the rule will not be added or removed. | `false` | `false` | | `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` | `true` | By default, the ecs-init service adds an iptable rule to block access to the agent introspection port from off-host (or containers in awsvpc network mode), and removes the rule upon stop. If this is set to true, the rule will not be added or removed | `false` | `false` | -| `ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME` | `eth0` | The primary network interface name to be used for blocking offhost agent introspection port access | `eth0` | `eth0` | | `ECS_ENABLE_GPU_SUPPORT` | `true` | Whether you use container instances with GPU support. This parameter is specified for the agent. You must also configure your task definitions for GPU. For more information, see [working with Amazon ECS task definitions for GPU workloads](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-gpu.html). | `false` | `Not applicable` | | `HTTP_PROXY` | `10.0.0.131:3128` | The hostname (or IP address) and port number of an HTTP proxy to use for the Amazon ECS agent to connect to the internet. For example, this proxy will be used if your container instances do not have external network access through an Amazon VPC internet gateway or NAT gateway or instance. If this variable is set, you must also set the NO_PROXY variable to filter Amazon EC2 instance metadata and Docker daemon traffic from the proxy. | `null` | `null` | | `NO_PROXY` | | The HTTP traffic that should not be forwarded to the specified HTTP_PROXY. You must specify 169.254.169.254,/var/run/docker.sock to filter Amazon EC2 instance metadata and Docker daemon traffic from the proxy. | `null` | `null` | diff --git a/ecs-init/docker/dependencies.go b/ecs-init/docker/dependencies.go index e1d2efc4b23..dc905a589c8 100644 --- a/ecs-init/docker/dependencies.go +++ b/ecs-init/docker/dependencies.go @@ -48,6 +48,7 @@ type dockerclient interface { WaitContainer(id string) (int, error) StopContainer(id string, timeout uint) error Ping() error + FilteredListNetworks(opts godocker.NetworkFilterOpts) ([]godocker.Network, error) } type _dockerclient struct { @@ -248,3 +249,7 @@ func isRetryablePingError(err error) bool { } return false } + +func (d *_dockerclient) FilteredListNetworks(opts godocker.NetworkFilterOpts) ([]godocker.Network, error) { + return d.docker.FilteredListNetworks(opts) +} diff --git a/ecs-init/docker/dependencies_mocks.go b/ecs-init/docker/dependencies_mocks.go index a426f1a9c8e..2f87fcd4663 100644 --- a/ecs-init/docker/dependencies_mocks.go +++ b/ecs-init/docker/dependencies_mocks.go @@ -62,6 +62,21 @@ func (mr *MockdockerclientMockRecorder) CreateContainer(opts interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateContainer", reflect.TypeOf((*Mockdockerclient)(nil).CreateContainer), opts) } +// FilteredListNetworks mocks base method. +func (m *Mockdockerclient) FilteredListNetworks(opts docker.NetworkFilterOpts) ([]docker.Network, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FilteredListNetworks", opts) + ret0, _ := ret[0].([]docker.Network) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FilteredListNetworks indicates an expected call of FilteredListNetworks. +func (mr *MockdockerclientMockRecorder) FilteredListNetworks(opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilteredListNetworks", reflect.TypeOf((*Mockdockerclient)(nil).FilteredListNetworks), opts) +} + // ListContainers mocks base method. func (m *Mockdockerclient) ListContainers(opts docker.ListContainersOptions) ([]docker.APIContainers, error) { m.ctrl.T.Helper() diff --git a/ecs-init/docker/docker.go b/ecs-init/docker/docker.go index ff4c8ea5cb4..bf2fcb9f151 100644 --- a/ecs-init/docker/docker.go +++ b/ecs-init/docker/docker.go @@ -16,6 +16,7 @@ package docker import ( "bytes" "encoding/json" + "fmt" "io" "os" "os/exec" @@ -130,6 +131,10 @@ const ( // fault inject functionality. Ref: https://man7.org/linux/man-pages/man8/modinfo.8.html modInfoSbinDir = "/sbin/modinfo" modInfoUsrSbinDir = "/usr/sbin/modinfo" + + // Docker Network options to filter for the default bridge network interface of docker + dockerDefaultBridgeInterfaceOption = "com.docker.network.bridge.default_bridge" + dockerInterfaceNameOption = "com.docker.network.bridge.name" ) // Do NOT include "CAP_" in capability string @@ -667,3 +672,26 @@ func isDomainJoined() bool { return true } + +// FindDefaultBridgeNetworkInterfaceName is used to find the name of the default network interface +// for docker bridge network mode. +func (c *client) FindDefaultBridgeNetworkInterfaceName() (string, error) { + networks, err := c.docker.FilteredListNetworks(godocker.NetworkFilterOpts{ + "driver": map[string]bool{"bridge": true}, + }) + if err != nil { + return "", err + } + for _, network := range networks { + val, ok := network.Options[dockerDefaultBridgeInterfaceOption] + if ok { + if val == "true" { + interfaceName, ok := network.Options[dockerInterfaceNameOption] + if ok { + return interfaceName, nil + } + } + } + } + return "", fmt.Errorf("unable to find any virtual docker bridge network interfaces on the host") +} diff --git a/ecs-init/docker/docker_test.go b/ecs-init/docker/docker_test.go index 858c3335b9f..e5320c89054 100644 --- a/ecs-init/docker/docker_test.go +++ b/ecs-init/docker/docker_test.go @@ -28,6 +28,10 @@ import ( "github.com/stretchr/testify/assert" ) +const ( + testDockerBridgeInterfaceName = "docker0" +) + func TestIsAgentImageLoadedListFailure(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -1098,3 +1102,54 @@ func fakeExecCommand(command string, args ...string) *exec.Cmd { cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} return cmd } + +func TestFindDefaultBridgeNetworkInterfaceName(t *testing.T) { + + testCases := []struct { + name string + expectedError error + expectedDockerBridgeInterfaceName string + mockExpectations func(mockDocker *Mockdockerclient) + }{ + { + name: "success", + expectedError: nil, + expectedDockerBridgeInterfaceName: testDockerBridgeInterfaceName, + mockExpectations: func(mockDocker *Mockdockerclient) { + mockDocker.EXPECT().FilteredListNetworks(gomock.Any()).Return(append(make([]godocker.Network, 0), godocker.Network{ + Options: map[string]string{ + dockerDefaultBridgeInterfaceOption: "true", + dockerInterfaceNameOption: testDockerBridgeInterfaceName, + }, + }), nil) + }, + }, + { + name: "error", + expectedError: fmt.Errorf("error unable to find docker bridge interface"), + expectedDockerBridgeInterfaceName: "", + mockExpectations: func(mockDocker *Mockdockerclient) { + mockDocker.EXPECT().FilteredListNetworks(gomock.Any()).Return(nil, fmt.Errorf("error unable to find docker bridge interface")) + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockDocker := NewMockdockerclient(mockCtrl) + + tc.mockExpectations(mockDocker) + + client := &client{ + docker: mockDocker, + } + dockerBridgeInterfaceName, err := client.FindDefaultBridgeNetworkInterfaceName() + + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedDockerBridgeInterfaceName, dockerBridgeInterfaceName) + }) + } + +} diff --git a/ecs-init/engine/dependencies.go b/ecs-init/engine/dependencies.go index 47f23e6f906..057be8821ea 100644 --- a/ecs-init/engine/dependencies.go +++ b/ecs-init/engine/dependencies.go @@ -38,6 +38,7 @@ type dockerClient interface { StartAgent() (int, error) StopAgent() error LoadEnvVars() map[string]string + FindDefaultBridgeNetworkInterfaceName() (string, error) } type loopbackRouting interface { diff --git a/ecs-init/engine/dependencies_mocks.go b/ecs-init/engine/dependencies_mocks.go index 5b8160869a3..7e0584b0e1d 100644 --- a/ecs-init/engine/dependencies_mocks.go +++ b/ecs-init/engine/dependencies_mocks.go @@ -157,6 +157,21 @@ func (m *MockdockerClient) EXPECT() *MockdockerClientMockRecorder { return m.recorder } +// FindDefaultBridgeNetworkInterfaceName mocks base method. +func (m *MockdockerClient) FindDefaultBridgeNetworkInterfaceName() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindDefaultBridgeNetworkInterfaceName") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindDefaultBridgeNetworkInterfaceName indicates an expected call of FindDefaultBridgeNetworkInterfaceName. +func (mr *MockdockerClientMockRecorder) FindDefaultBridgeNetworkInterfaceName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindDefaultBridgeNetworkInterfaceName", reflect.TypeOf((*MockdockerClient)(nil).FindDefaultBridgeNetworkInterfaceName)) +} + // GetContainerLogTail mocks base method. func (m *MockdockerClient) GetContainerLogTail(logWindowSize string) string { m.ctrl.T.Helper() diff --git a/ecs-init/engine/engine.go b/ecs-init/engine/engine.go index 93ce9613f40..7c035c1507a 100644 --- a/ecs-init/engine/engine.go +++ b/ecs-init/engine/engine.go @@ -21,6 +21,7 @@ import ( "os" "time" + "github.com/aws/amazon-ecs-agent/ecs-agent/utils/netlinkwrapper" "github.com/aws/amazon-ecs-agent/ecs-init/apparmor" "github.com/aws/amazon-ecs-agent/ecs-init/backoff" "github.com/aws/amazon-ecs-agent/ecs-init/cache" @@ -99,7 +100,15 @@ func New() (*Engine, error) { if err != nil { return nil, err } - credentialsProxyRoute, err := iptables.NewNetfilterRoute(cmdExec) + docker, err := getDockerClient() + if err != nil { + return nil, err + } + dockerBridgeNetworkName, err := docker.FindDefaultBridgeNetworkInterfaceName() + if err != nil { + return nil, err + } + credentialsProxyRoute, err := iptables.NewNetfilterRoute(cmdExec, netlinkwrapper.New(), dockerBridgeNetworkName) if err != nil { return nil, err } diff --git a/ecs-init/engine/engine_test.go b/ecs-init/engine/engine_test.go index a6e62288152..ad6784189f2 100644 --- a/ecs-init/engine/engine_test.go +++ b/ecs-init/engine/engine_test.go @@ -1,5 +1,5 @@ -//go:build test -// +build test +//go:build unit +// +build unit // Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. // diff --git a/ecs-init/exec/iptables/iptables.go b/ecs-init/exec/iptables/iptables.go index 2ced8a965cf..888c6de1e57 100644 --- a/ecs-init/exec/iptables/iptables.go +++ b/ecs-init/exec/iptables/iptables.go @@ -14,15 +14,15 @@ package iptables import ( - "bufio" "fmt" - "io" "os" "strconv" "strings" - "unicode" + netutils "github.com/aws/amazon-ecs-agent/ecs-agent/utils/net" + "github.com/aws/amazon-ecs-agent/ecs-agent/utils/netlinkwrapper" "github.com/aws/amazon-ecs-agent/ecs-init/exec" + log "github.com/cihub/seelog" "github.com/pkg/errors" ) @@ -33,6 +33,7 @@ type iptablesAction string const ( iptablesExecutable = "iptables" CredentialsProxyIpAddress = "169.254.170.2" + ip6tablesExecutable = "ip6tables" credentialsProxyPort = "80" localhostIpAddress = "127.0.0.1" localhostCredentialsProxyPort = "51679" @@ -50,23 +51,18 @@ const ( offhostIntrospectionAccessConfigEnv = "ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS" offhostIntrospectonAccessInterfaceEnv = "ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME" agentIntrospectionServerPort = "51678" - - ipv4RouteFile = "/proc/net/route" - ipv4ZeroAddrInHex = "00000000" - loopbackInterfaceName = "lo" - fallbackOffhostIntrospectionInterface = "eth0" - - ifNameSize = 16 ) var ( - defaultOffhostIntrospectionInterface = "" + defaultLoopbackInterfaceName = "" + defaultDockerBridgeNetworkName = "" ) // NetfilterRoute implements the engine.credentialsProxyRoute interface by // running the external 'iptables' command type NetfilterRoute struct { - cmdExec exec.Exec + cmdExec exec.Exec + nlWrapper netlinkwrapper.NetLink } // getNetfilterChainArgsFunc defines a function pointer type that returns @@ -74,7 +70,7 @@ type NetfilterRoute struct { type getNetfilterChainArgsFunc func() []string // NewNetfilterRoute creates a new NetfilterRoute object -func NewNetfilterRoute(cmdExec exec.Exec) (*NetfilterRoute, error) { +func NewNetfilterRoute(cmdExec exec.Exec, nlWrapper netlinkwrapper.NetLink, dockerBridgeNetworkName string) (*NetfilterRoute, error) { // Return an error if 'iptables' command cannot be found in the path _, err := cmdExec.LookPath(iptablesExecutable) if err != nil { @@ -82,72 +78,89 @@ func NewNetfilterRoute(cmdExec exec.Exec) (*NetfilterRoute, error) { return nil, err } - defaultOffhostIntrospectionInterface, err = getOffhostIntrospectionInterface() + // Obtain the loopback interface on the host to restrict introspection server access + loopbackInterface, err := netutils.GetLoopbackInterface(nlWrapper) if err != nil { - log.Warnf("Error resolving default offhost introspection network interface, will use eth0 as fallback: %+v", err) - // fall back to the previous behavior (always use 'eth0') in the rare case that it - // might affect some customer with a special routing setup that's previously working. - defaultOffhostIntrospectionInterface = fallbackOffhostIntrospectionInterface + log.Errorf("Error resolving loopback interface on the host: %w", err) + return nil, err } + defaultLoopbackInterfaceName = loopbackInterface.Attrs().Name + defaultDockerBridgeNetworkName = dockerBridgeNetworkName return &NetfilterRoute{ - cmdExec: cmdExec, + cmdExec: cmdExec, + nlWrapper: nlWrapper, }, nil } // Create creates the credentials proxy endpoint route in the netfilter table func (route *NetfilterRoute) Create() error { - err := route.modifyNetfilterEntry(iptablesTableNat, iptablesAppend, getPreroutingChainArgs) + err := route.modifyNetfilterEntry(iptablesTableNat, iptablesAppend, getPreroutingChainArgs, false) if err != nil { return err } if !skipLocalhostTrafficFilter() { - err = route.modifyNetfilterEntry(iptablesTableFilter, iptablesInsert, getLocalhostTrafficFilterInputChainArgs) + err = route.modifyNetfilterEntry(iptablesTableFilter, iptablesInsert, getLocalhostTrafficFilterInputChainArgs, false) if err != nil { return err } } if !allowOffhostIntrospection() { - err = route.modifyNetfilterEntry(iptablesTableFilter, iptablesInsert, getBlockIntrospectionOffhostAccessInputChainArgs) + // Drop all connections to introspection server except the loopback interface. + err = route.modifyNetfilterEntry(iptablesTableFilter, iptablesInsert, getBlockIntrospectionOffhostAccessInputChainArgs, true) if err != nil { log.Errorf("Error adding input chain entry to block offhost introspection access: %v", err) + return err + } + + // Allow docker's virtual bridge interface to access the introspection server. Inserting it after applying + // the rule to drop all connections other than loopback interface will push it on top of priority. + err = route.modifyNetfilterEntry(iptablesTableFilter, iptablesInsert, allowIntrospectionForDockerIptablesInputChainArgs, true) + if err != nil { + log.Errorf("Error adding input chain entry to allow %s access to introspection server: %w", err) + return err } } - return route.modifyNetfilterEntry(iptablesTableNat, iptablesAppend, getOutputChainArgs) + return route.modifyNetfilterEntry(iptablesTableNat, iptablesAppend, getOutputChainArgs, false) } // Remove removes the route for the credentials endpoint from the netfilter // table func (route *NetfilterRoute) Remove() error { - preroutingErr := route.modifyNetfilterEntry(iptablesTableNat, iptablesDelete, getPreroutingChainArgs) + preroutingErr := route.modifyNetfilterEntry(iptablesTableNat, iptablesDelete, getPreroutingChainArgs, false) if preroutingErr != nil { // Add more context for error in modifying the prerouting chain preroutingErr = fmt.Errorf("error removing prerouting chain entry: %v", preroutingErr) } - var localhostInputError, introspectionInputError error + var localhostInputError, introspectionInputError, dockerIntrospectionInputError error if !skipLocalhostTrafficFilter() { - localhostInputError = route.modifyNetfilterEntry(iptablesTableFilter, iptablesDelete, getLocalhostTrafficFilterInputChainArgs) + localhostInputError = route.modifyNetfilterEntry(iptablesTableFilter, iptablesDelete, getLocalhostTrafficFilterInputChainArgs, false) if localhostInputError != nil { localhostInputError = fmt.Errorf("error removing input chain entry: %v", localhostInputError) } } - introspectionInputError = route.modifyNetfilterEntry(iptablesTableFilter, iptablesDelete, getBlockIntrospectionOffhostAccessInputChainArgs) + introspectionInputError = route.modifyNetfilterEntry(iptablesTableFilter, iptablesDelete, getBlockIntrospectionOffhostAccessInputChainArgs, true) if introspectionInputError != nil { introspectionInputError = fmt.Errorf("error removing input chain entry: %v", introspectionInputError) } - outputErr := route.modifyNetfilterEntry(iptablesTableNat, iptablesDelete, getOutputChainArgs) + dockerIntrospectionInputError = route.modifyNetfilterEntry(iptablesTableFilter, iptablesDelete, allowIntrospectionForDockerIptablesInputChainArgs, true) + if dockerIntrospectionInputError != nil { + dockerIntrospectionInputError = fmt.Errorf("error removing input chain entry: %v", dockerIntrospectionInputError) + } + + outputErr := route.modifyNetfilterEntry(iptablesTableNat, iptablesDelete, getOutputChainArgs, false) if outputErr != nil { // Add more context for error in modifying the output chain outputErr = fmt.Errorf("error removing output chain entry: %v", outputErr) } - return combinedError(preroutingErr, localhostInputError, introspectionInputError, outputErr) + return combinedError(preroutingErr, localhostInputError, dockerIntrospectionInputError, introspectionInputError, outputErr) } func combinedError(errs ...error) error { @@ -168,16 +181,33 @@ func combinedError(errs ...error) error { // modifyNetfilterEntry modifies an entry in the netfilter table based on // the action and the function pointer to get arguments for modifying the // chain -func (route *NetfilterRoute) modifyNetfilterEntry(table string, action iptablesAction, getNetfilterChainArgs getNetfilterChainArgsFunc) error { +func (route *NetfilterRoute) modifyNetfilterEntry(table string, action iptablesAction, getNetfilterChainArgs getNetfilterChainArgsFunc, useIp6tables bool) error { args := append(getTableArgs(table), string(action)) args = append(args, getNetfilterChainArgs()...) cmd := route.cmdExec.Command(iptablesExecutable, args...) out, err := cmd.CombinedOutput() if err != nil { log.Errorf("Error performing action '%s' for iptables route: %v; raw output: %s", getActionName(action), err, out) + return err + } + + // Checking if we need to apply the netfilter table action for IPv6 as well. + if useIp6tables { + _, err = route.cmdExec.LookPath(ip6tablesExecutable) + if err != nil { + log.Warnf("%s unable to be found on the host. Assuming IPv6 isn't available on the host and will not apply %s.", ip6tablesExecutable, getActionName(action)) + } else { + cmd = route.cmdExec.Command(ip6tablesExecutable, args...) + out, err = cmd.CombinedOutput() + if err != nil { + log.Errorf("Error performing action '%s' for ip6tables route: %v; raw output: %s", getActionName(action), err, out) + return err + } + log.Infof("Successfully blocked IPv6 off-host access for introspection server with %s.", ip6tablesExecutable) + } } - return err + return nil } func getTableArgs(table string) []string { @@ -210,79 +240,20 @@ func getBlockIntrospectionOffhostAccessInputChainArgs() []string { return []string{ "INPUT", "-p", "tcp", - "-i", defaultOffhostIntrospectionInterface, "--dport", agentIntrospectionServerPort, + "!", "-i", defaultLoopbackInterfaceName, "-j", "DROP", } } -// checkValidIfname checks that the string is a valid eth interface name. Returns -// an error if the interface name is invalid. -// see kernel dev_valid_name function for reference. -// https://github.com/torvalds/linux/blob/d7e78951a8b8b53e4d52c689d927a6887e6cfadf/net/core/dev.c#L1056-L1079 -func checkValidIfname(name string) error { - if name == "" { - return fmt.Errorf("Invalid ifname [%s]: empty string not allowed", name) - } - if len(name) >= ifNameSize { - return fmt.Errorf("Invalid ifname [%s]: length of name must be less than %d chars", name, ifNameSize) - } - if name == "." || name == ".." { - return fmt.Errorf("Invalid ifname [%s]: '.' or '..' not allowed", name) - } - - for _, char := range name { - if char == '/' || char == ':' || unicode.IsSpace(char) { - return fmt.Errorf("Invalid ifname [%s]: invalid character found: space, ':', and '/' not allowed", name) - } - } - return nil -} - -func getOffhostIntrospectionInterface() (string, error) { - s := os.Getenv(offhostIntrospectonAccessInterfaceEnv) - if s != "" { - err := checkValidIfname(s) - if err != nil { - log.Errorf("ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME interface name is invalid, falling back to default. %s", err) - } else { - return s, nil - } - } - return getDefaultNetworkInterfaceIPv4() -} - -// Parse /proc/net/route file and retrieves a non-loopback default network interface for IPv4 (which maps to default 0.0.0.0/0 destination) -// Example file content: -// $ sudo cat /proc/net/route -// Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT -// ens5 00000000 01201FAC 0003 0 0 512 00000000 0 0 0 -// ... -// -// 1st column contains interface name -// 2nd column contains destination network in hex -var getDefaultNetworkInterfaceIPv4 = func() (string, error) { - input, err := os.Open(ipv4RouteFile) - if err != nil { - return "", fmt.Errorf("could not get IPv4 route input: %v", err) - } - defer input.Close() - return scanIPv4RoutesForDefaultInterface(input) -} - -func scanIPv4RoutesForDefaultInterface(input io.Reader) (string, error) { - scanner := bufio.NewScanner(input) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "Iface") { // skip header line - continue - } - fields := strings.Fields(line) - if (fields[1] == ipv4ZeroAddrInHex) && (fields[0] != loopbackInterfaceName) { - return fields[0], nil - } +func allowIntrospectionForDockerIptablesInputChainArgs() []string { + return []string{ + "INPUT", + "-p", "tcp", + "--dport", agentIntrospectionServerPort, + "-i", defaultDockerBridgeNetworkName, + "-j", "ACCEPT", } - return "", fmt.Errorf("could not find a default IPv4 route through non-loopback interface") } func getOutputChainArgs() []string { diff --git a/ecs-init/exec/iptables/iptables_test.go b/ecs-init/exec/iptables/iptables_test.go index 6cc404e65ee..f4653b3e8d6 100644 --- a/ecs-init/exec/iptables/iptables_test.go +++ b/ecs-init/exec/iptables/iptables_test.go @@ -15,14 +15,22 @@ package iptables import ( "fmt" + "net" "os" - "strings" "testing" + mock_nlwrapper "github.com/aws/amazon-ecs-agent/ecs-agent/utils/netlinkwrapper/mocks" + "github.com/golang/mock/gomock" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vishvananda/netlink" +) + +const ( + testLoopbackInterfaceName = "lo0" + testDockerBridgeInterfaceName = "docker0" ) var ( @@ -41,19 +49,27 @@ var ( "!", "--ctstate", "RELATED,ESTABLISHED,DNAT", "-j", "DROP", } - offhostIntrospectionInterface = "ens5" - blockIntrospectionOffhostAccessInputRouteArgs = []string{ + + allowIntrospectionForDockerArgs = []string{ "-p", "tcp", - "-i", offhostIntrospectionInterface, "--dport", agentIntrospectionServerPort, - "-j", "DROP", + "-i", testDockerBridgeInterfaceName, + "-j", "ACCEPT", } - blockIntrospectionOffhostAccessInterfaceInputRouteArgs = []string{ + blockIntrospectionOffhostAccessInputRouteArgs = []string{ "-p", "tcp", - "-i", "sn0", "--dport", agentIntrospectionServerPort, + "!", "-i", testLoopbackInterfaceName, "-j", "DROP", } + + defaultLoLink = &netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Index: 1, + Flags: net.FlagLoopback, + Name: testLoopbackInterfaceName, + }, + } outputRouteArgs = []string{ "-p", "tcp", "-d", CredentialsProxyIpAddress, @@ -61,192 +77,156 @@ var ( "-j", "REDIRECT", "--to-ports", localhostCredentialsProxyPort, } - - testIPV4RouteInput = `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT -ens5 00000000 01201FAC 0003 0 0 0 00000000 0 0 0 -ens5 FEA9FEA9 00000000 0005 0 0 0 FFFFFFFF 0 0 0 -ens5 00201FAC 00000000 0001 0 0 0 00F0FFFF 0 0 0 -` ) -func overrideIPRouteInput(ipv4RouteInput string) func() { - originalv4 := getDefaultNetworkInterfaceIPv4 - - getDefaultNetworkInterfaceIPv4 = func() (string, error) { - return scanIPv4RoutesForDefaultInterface(strings.NewReader(ipv4RouteInput)) - } - +func resetDefaultNetworkInterfaceVariables() func() { return func() { - getDefaultNetworkInterfaceIPv4 = originalv4 - // in real environment we'll only set it once, for testing we unset it after executing relevant test cases - defaultOffhostIntrospectionInterface = "" + defaultLoopbackInterfaceName = "" + defaultDockerBridgeNetworkName = "" } } -func TestValidIfName(t *testing.T) { +func TestNewNetfilterRoute(t *testing.T) { + testCases := []struct { - ifname string - expectValid bool + name string + shouldError bool + setMockExpectations func(mockExec *MockExec, mockNetlink *mock_nlwrapper.MockNetLink) + expectedLoopbackInterfaceName string }{ { - ifname: "eth0", - expectValid: true, - }, - { - ifname: "eth1", - expectValid: true, - }, - { - ifname: "eth24", - expectValid: true, - }, - { - ifname: "eth255", - expectValid: true, - }, - { - ifname: "myinterfacename", - expectValid: true, - }, - { - ifname: "12345", - expectValid: true, - }, - { - ifname: "e", - expectValid: true, + name: "success", + shouldError: false, + setMockExpectations: func(mockExec *MockExec, mockNetlink *mock_nlwrapper.MockNetLink) { + mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil) + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil) + }, + expectedLoopbackInterfaceName: testLoopbackInterfaceName, }, { - ifname: "ens5", - expectValid: true, + name: "executable not found", + shouldError: true, + setMockExpectations: func(mockExec *MockExec, mockNetlink *mock_nlwrapper.MockNetLink) { + mockExec.EXPECT().LookPath(iptablesExecutable).Return("", fmt.Errorf("Not found")) + }, }, { - ifname: "invalidchar", - expectValid: true, - }, - { - ifname: "invalid char", - expectValid: false, - }, - { - ifname: "invalid:char", - expectValid: false, - }, - { - ifname: "invalid/char", - expectValid: false, - }, - { - ifname: "ethnameistoolong", - expectValid: false, - }, - { - ifname: "ethnameiswaywaywaywaytoolong", - expectValid: false, - }, - { - ifname: "", - expectValid: false, - }, - { - ifname: ".", - expectValid: false, - }, - { - ifname: "..", - expectValid: false, + name: "default loopback not found", + shouldError: true, + setMockExpectations: func(mockExec *MockExec, mockNetlink *mock_nlwrapper.MockNetLink) { + mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil) + mockNetlink.EXPECT().LinkList().Return(nil, fmt.Errorf("loopback not found")) + }, }, } for _, tc := range testCases { - err := checkValidIfname(tc.ifname) - if tc.expectValid { - require.NoError(t, err) - } else { - require.Error(t, err) - } + t.Run(tc.name, func(t *testing.T) { + defer resetDefaultNetworkInterfaceVariables() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) + if tc.setMockExpectations != nil { + tc.setMockExpectations(mockExec, mockNetlink) + } + + netRoute, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) + if tc.shouldError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, netRoute) + assert.Equal(t, tc.expectedLoopbackInterfaceName, defaultLoopbackInterfaceName) + } + }) } -} - -func TestNewNetfilterRouteFailsWhenExecutableNotFound(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - mockExec := NewMockExec(ctrl) - mockExec.EXPECT().LookPath(iptablesExecutable).Return("", fmt.Errorf("Not found")) - - _, err := NewNetfilterRoute(mockExec) - assert.Error(t, err, "Expected error when executable's path lookup fails") } -func TestNewNetfilterRouteWithDefaultOffhostIntrospectionInterfaceFallback(t *testing.T) { - defer overrideIPRouteInput("")() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockExec := NewMockExec(ctrl) - mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil) - - _, err := NewNetfilterRoute(mockExec) - assert.NoError(t, err) - assert.Equal(t, defaultOffhostIntrospectionInterface, fallbackOffhostIntrospectionInterface) +func appendIpv6Mocks(mockExec *MockExec, mockCmd *MockCmd, mocks []*gomock.Call, table, action, chain string, args []string, useIp6tables bool) []*gomock.Call { + mocks = append(mocks, + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs(table, action, chain, args)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + ) + if useIp6tables { + mocks = append(mocks, + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs(table, action, chain, args)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + ) + } else { + mocks = append(mocks, + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", errors.New("error")), + ) + } + return mocks } func TestCreate(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() + testCases := []struct { - setOffhostInterface bool - inputRouteArgs []string + name string + useIp6tables bool }{ { - setOffhostInterface: false, - inputRouteArgs: blockIntrospectionOffhostAccessInputRouteArgs, + name: "create iptables route", + useIp6tables: false, }, { - setOffhostInterface: true, - inputRouteArgs: blockIntrospectionOffhostAccessInterfaceInputRouteArgs, + name: "create ip6tables route", + useIp6tables: true, }, } for _, tc := range testCases { - if tc.setOffhostInterface { - os.Setenv(offhostIntrospectonAccessInterfaceEnv, "sn0") - defer os.Unsetenv(offhostIntrospectonAccessInterfaceEnv) - } - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockCmd := NewMockCmd(ctrl) - // Mock a successful execution of the iptables command to create the - // route - mockExec := NewMockExec(ctrl) - gomock.InOrder( - mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("nat", "-A", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("filter", "-I", "INPUT", localhostTrafficFilterInputRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("filter", "-I", "INPUT", tc.inputRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("nat", "-A", "OUTPUT", outputRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - ) - - route, err := NewNetfilterRoute(mockExec) - require.NoError(t, err, "Error creating netfilter route object") - - err = route.Create() - assert.NoError(t, err, "Error creating route") + t.Run(tc.name, func(t *testing.T) { + defer resetDefaultNetworkInterfaceVariables() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCmd := NewMockCmd(ctrl) + // Mock a successful execution of the iptables command to create the + // route + mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) + mocks := []*gomock.Call{ + mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), + + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("nat", "-A", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-I", "INPUT", localhostTrafficFilterInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + } + mocks = appendIpv6Mocks(mockExec, mockCmd, mocks, "filter", string(iptablesInsert), "INPUT", blockIntrospectionOffhostAccessInputRouteArgs, tc.useIp6tables) + mocks = appendIpv6Mocks(mockExec, mockCmd, mocks, "filter", string(iptablesInsert), "INPUT", allowIntrospectionForDockerArgs, tc.useIp6tables) + mocks = append(mocks, + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("nat", "-A", "OUTPUT", outputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + ) + + gomock.InOrder( + mocks..., + ) + + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) + require.NoError(t, err, "Error creating netfilter route object") + + err = route.Create() + assert.NoError(t, err, "Error creating route") + }) } - } func TestCreateSkipLocalTrafficFilter(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() + defer resetDefaultNetworkInterfaceVariables() os.Setenv("ECS_SKIP_LOCALHOST_TRAFFIC_FILTER", "true") defer os.Unsetenv("ECS_SKIP_LOCALHOST_TRAFFIC_FILTER") @@ -255,20 +235,34 @@ func TestCreateSkipLocalTrafficFilter(t *testing.T) { mockCmd := NewMockCmd(ctrl) mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) gomock.InOrder( mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-A", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("filter", "-I", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-I", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-I", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-I", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-A", "OUTPUT", outputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), ) - route, err := NewNetfilterRoute(mockExec) + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) require.NoError(t, err, "Error creating netfilter route object") err = route.Create() @@ -276,6 +270,7 @@ func TestCreateSkipLocalTrafficFilter(t *testing.T) { } func TestCreateAllowOffhostIntrospectionAccess(t *testing.T) { + defer resetDefaultNetworkInterfaceVariables() os.Setenv(offhostIntrospectionAccessConfigEnv, "true") defer os.Unsetenv(offhostIntrospectionAccessConfigEnv) @@ -284,8 +279,10 @@ func TestCreateAllowOffhostIntrospectionAccess(t *testing.T) { mockCmd := NewMockCmd(ctrl) mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) gomock.InOrder( mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-A", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), @@ -297,28 +294,102 @@ func TestCreateAllowOffhostIntrospectionAccess(t *testing.T) { mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), ) - route, err := NewNetfilterRoute(mockExec) + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) require.NoError(t, err, "Error creating netfilter route object") err = route.Create() assert.NoError(t, err, "Error creating route") } +func TestCreateBlockOffhostIntrospectionAccessErrors(t *testing.T) { + testCases := []struct { + name string + mockExpectations func(mockExec *MockExec, mockCmd *MockCmd, mockNetlink *mock_nlwrapper.MockNetLink) + }{ + { + name: "iptables drop connections fail", + mockExpectations: func(mockExec *MockExec, mockCmd *MockCmd, mockNetlink *mock_nlwrapper.MockNetLink) { + gomock.InOrder( + mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("nat", "-A", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-I", "INPUT", localhostTrafficFilterInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-I", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, fmt.Errorf("error unable to drop connections for offhost access to introspection server")), + ) + }, + }, + { + name: "iptables allow connections from docker bridge fail", + mockExpectations: func(mockExec *MockExec, mockCmd *MockCmd, mockNetlink *mock_nlwrapper.MockNetLink) { + gomock.InOrder( + mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("nat", "-A", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-I", "INPUT", localhostTrafficFilterInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-I", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-I", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-I", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, fmt.Errorf("error unable to accept connections from docker bridge interface to introspection server")), + ) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer resetDefaultNetworkInterfaceVariables() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockExec := NewMockExec(ctrl) + mockCmd := NewMockCmd(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) + + tc.mockExpectations(mockExec, mockCmd, mockNetlink) + + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) + require.NoError(t, err, "Error creating netfilter route object") + + err = route.Create() + assert.Error(t, err) + }) + } +} + func TestCreateErrorOnPreRoutingCommandError(t *testing.T) { + defer resetDefaultNetworkInterfaceVariables() ctrl := gomock.NewController(t) defer ctrl.Finish() mockCmd := NewMockCmd(ctrl) mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) gomock.InOrder( mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-A", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), // Mock a failed execution of the iptables command to create the route mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, fmt.Errorf("didn't expect this, did you?")), ) - route, err := NewNetfilterRoute(mockExec) + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) require.NoError(t, err, "Error creating netfilter route object") err = route.Create() @@ -326,13 +397,16 @@ func TestCreateErrorOnPreRoutingCommandError(t *testing.T) { } func TestCreateErrorOnInputChainCommandError(t *testing.T) { + defer resetDefaultNetworkInterfaceVariables() ctrl := gomock.NewController(t) defer ctrl.Finish() mockCmd := NewMockCmd(ctrl) mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) gomock.InOrder( mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-A", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), @@ -341,7 +415,7 @@ func TestCreateErrorOnInputChainCommandError(t *testing.T) { mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, testErr), ) - route, err := NewNetfilterRoute(mockExec) + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) require.NoError(t, err) err = route.Create() @@ -349,14 +423,16 @@ func TestCreateErrorOnInputChainCommandError(t *testing.T) { } func TestCreateErrorOnOutputChainCommandError(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() + defer resetDefaultNetworkInterfaceVariables() ctrl := gomock.NewController(t) defer ctrl.Finish() mockCmd := NewMockCmd(ctrl) mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) gomock.InOrder( mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-A", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), @@ -366,13 +442,24 @@ func TestCreateErrorOnOutputChainCommandError(t *testing.T) { mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("filter", "-I", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-I", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-I", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-I", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-A", "OUTPUT", outputRouteArgs)).Return(mockCmd), // Mock a failed execution of the iptables command to create the route mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, fmt.Errorf("didn't expect this, did you?")), ) - route, err := NewNetfilterRoute(mockExec) + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) require.NoError(t, err, "Error creating netfilter route object") err = route.Create() @@ -380,39 +467,68 @@ func TestCreateErrorOnOutputChainCommandError(t *testing.T) { } func TestRemove(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockCmd := NewMockCmd(ctrl) - mockExec := NewMockExec(ctrl) - gomock.InOrder( - mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("nat", "-D", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), - // Mock a successful execution of the iptables command to delete the - // route - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("filter", "-D", "INPUT", localhostTrafficFilterInputRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("nat", "-D", "OUTPUT", outputRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - ) - - route, err := NewNetfilterRoute(mockExec) - require.NoError(t, err, "Error creating netfilter route object") + testCases := []struct { + name string + useIp6tables bool + }{ + { + name: "test remove with iptables only", + useIp6tables: false, + }, + { + name: "test remove with ip6tables", + useIp6tables: true, + }, + } - err = route.Remove() - assert.NoError(t, err, "Error removing route") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer resetDefaultNetworkInterfaceVariables() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCmd := NewMockCmd(ctrl) + mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) + + mocks := []*gomock.Call{ + mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), + + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("nat", "-D", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), + // Mock a successful execution of the iptables command to delete the + // route + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-D", "INPUT", localhostTrafficFilterInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + } + + mocks = appendIpv6Mocks(mockExec, mockCmd, mocks, "filter", string(iptablesDelete), "INPUT", blockIntrospectionOffhostAccessInputRouteArgs, tc.useIp6tables) + mocks = appendIpv6Mocks(mockExec, mockCmd, mocks, "filter", string(iptablesDelete), "INPUT", allowIntrospectionForDockerArgs, tc.useIp6tables) + + mocks = append(mocks, + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("nat", "-D", "OUTPUT", outputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + ) + + gomock.InOrder( + mocks..., + ) + + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) + require.NoError(t, err, "Error creating netfilter route object") + + err = route.Remove() + assert.NoError(t, err, "Error removing route") + }) + } } func TestRemoveSkipLocalTrafficFilter(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() + defer resetDefaultNetworkInterfaceVariables() os.Setenv("ECS_SKIP_LOCALHOST_TRAFFIC_FILTER", "true") defer os.Unsetenv("ECS_SKIP_LOCALHOST_TRAFFIC_FILTER") @@ -421,20 +537,33 @@ func TestRemoveSkipLocalTrafficFilter(t *testing.T) { mockCmd := NewMockCmd(ctrl) mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) gomock.InOrder( mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-D", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-D", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-D", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-D", "OUTPUT", outputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), ) - route, err := NewNetfilterRoute(mockExec) + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) require.NoError(t, err, "Error creating netfilter route object") err = route.Remove() @@ -442,47 +571,76 @@ func TestRemoveSkipLocalTrafficFilter(t *testing.T) { } func TestRemoveAllowIntrospectionOffhostAccess(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() + os.Setenv(offhostIntrospectionAccessConfigEnv, "true") defer os.Unsetenv(offhostIntrospectionAccessConfigEnv) - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockCmd := NewMockCmd(ctrl) - mockExec := NewMockExec(ctrl) - gomock.InOrder( - mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("nat", "-D", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("filter", "-D", "INPUT", localhostTrafficFilterInputRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - mockExec.EXPECT().Command(iptablesExecutable, - expectedArgs("nat", "-D", "OUTPUT", outputRouteArgs)).Return(mockCmd), - mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), - ) - - route, err := NewNetfilterRoute(mockExec) - require.NoError(t, err, "Error creating netfilter route object") + testCases := []struct { + name string + useIp6tables bool + }{ + { + name: "remove allow instrocpection off-host access with iptables only", + useIp6tables: false, + }, + { + name: "remove allow instrocpection off-host access with ip6tables", + useIp6tables: true, + }, + } - err = route.Remove() - assert.NoError(t, err, "Error removing route") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer resetDefaultNetworkInterfaceVariables() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCmd := NewMockCmd(ctrl) + mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) + + mocks := []*gomock.Call{ + mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("nat", "-D", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-D", "INPUT", localhostTrafficFilterInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + } + mocks = appendIpv6Mocks(mockExec, mockCmd, mocks, "filter", string(iptablesDelete), "INPUT", blockIntrospectionOffhostAccessInputRouteArgs, tc.useIp6tables) + mocks = appendIpv6Mocks(mockExec, mockCmd, mocks, "filter", string(iptablesDelete), "INPUT", allowIntrospectionForDockerArgs, tc.useIp6tables) + mocks = append(mocks, + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("nat", "-D", "OUTPUT", outputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + ) + + gomock.InOrder( + mocks..., + ) + + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) + require.NoError(t, err, "Error creating netfilter route object") + + err = route.Remove() + assert.NoError(t, err, "Error removing route") + }) + } } func TestRemoveErrorOnPreroutingChainCommandError(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() + defer resetDefaultNetworkInterfaceVariables() ctrl := gomock.NewController(t) defer ctrl.Finish() mockCmd := NewMockCmd(ctrl) mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) gomock.InOrder( mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-D", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), // Mock a failed execution of the iptables command to delete the route @@ -493,12 +651,23 @@ func TestRemoveErrorOnPreroutingChainCommandError(t *testing.T) { mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-D", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-D", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-D", "OUTPUT", outputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), ) - route, err := NewNetfilterRoute(mockExec) + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) require.NoError(t, err, "Error creating netfilter route object") err = route.Remove() @@ -506,14 +675,16 @@ func TestRemoveErrorOnPreroutingChainCommandError(t *testing.T) { } func TestRemoveErrorOnOutputChainCommandError(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() + defer resetDefaultNetworkInterfaceVariables() ctrl := gomock.NewController(t) defer ctrl.Finish() mockCmd := NewMockCmd(ctrl) mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) gomock.InOrder( mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-D", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), @@ -523,13 +694,24 @@ func TestRemoveErrorOnOutputChainCommandError(t *testing.T) { mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-D", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-D", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-D", "OUTPUT", outputRouteArgs)).Return(mockCmd), // Mock a failed execution of the iptables command to delete the route mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, fmt.Errorf("no cpu cycles to spare, sorry")), ) - route, err := NewNetfilterRoute(mockExec) + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) require.NoError(t, err, "Error creating netfilter route object") err = route.Remove() @@ -537,14 +719,16 @@ func TestRemoveErrorOnOutputChainCommandError(t *testing.T) { } func TestRemoveErrorOnInputChainCommandsErrors(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() + defer resetDefaultNetworkInterfaceVariables() ctrl := gomock.NewController(t) defer ctrl.Finish() mockCmd := NewMockCmd(ctrl) mockExec := NewMockExec(ctrl) + mockNetlink := mock_nlwrapper.NewMockNetLink(ctrl) gomock.InOrder( mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil), + mockNetlink.EXPECT().LinkList().Return([]netlink.Link{defaultLoLink}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-D", "PREROUTING", preroutingRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), @@ -554,12 +738,23 @@ func TestRemoveErrorOnInputChainCommandsErrors(t *testing.T) { mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-D", "INPUT", blockIntrospectionOffhostAccessInputRouteArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().Command(iptablesExecutable, + expectedArgs("filter", "-D", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), + mockExec.EXPECT().LookPath(ip6tablesExecutable).Return("", nil), + mockExec.EXPECT().Command(ip6tablesExecutable, + expectedArgs("filter", "-D", "INPUT", allowIntrospectionForDockerArgs)).Return(mockCmd), + mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), mockExec.EXPECT().Command(iptablesExecutable, expectedArgs("nat", "-D", "OUTPUT", outputRouteArgs)).Return(mockCmd), mockCmd.EXPECT().CombinedOutput().Return([]byte{0}, nil), ) - route, err := NewNetfilterRoute(mockExec) + route, err := NewNetfilterRoute(mockExec, mockNetlink, testDockerBridgeInterfaceName) require.NoError(t, err, "Error creating netfilter route object") err = route.Remove() @@ -603,17 +798,29 @@ func TestGetLocalhostTrafficFilterInputChainArgs(t *testing.T) { } func TestGetBlockIntrospectionOffhostAccessInputChainArgs(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() - defaultOffhostIntrospectionInterface, _ = getOffhostIntrospectionInterface() + defer resetDefaultNetworkInterfaceVariables() + defaultLoopbackInterfaceName = testLoopbackInterfaceName assert.Equal(t, []string{ "INPUT", "-p", "tcp", - "-i", "ens5", - "--dport", "51678", + "--dport", agentIntrospectionServerPort, + "!", "-i", testLoopbackInterfaceName, "-j", "DROP", }, getBlockIntrospectionOffhostAccessInputChainArgs()) } +func TestAllowIntrospectionForDocker(t *testing.T) { + defer resetDefaultNetworkInterfaceVariables() + defaultDockerBridgeNetworkName = testDockerBridgeInterfaceName + assert.Equal(t, []string{ + "INPUT", + "-p", "tcp", + "--dport", agentIntrospectionServerPort, + "-i", testDockerBridgeInterfaceName, + "-j", "ACCEPT", + }, allowIntrospectionForDockerIptablesInputChainArgs()) +} + func TestGetOutputChainArgs(t *testing.T) { outputChainAgrs := []string{ "OUTPUT", @@ -636,60 +843,3 @@ func TestGetActionName(t *testing.T) { func expectedArgs(table, action, chain string, args []string) []string { return append([]string{"-t", table, action, chain}, args...) } - -func TestScanIPv4RoutesHappyCase(t *testing.T) { - iface, err := scanIPv4RoutesForDefaultInterface(strings.NewReader(testIPV4RouteInput)) - assert.NoError(t, err) - assert.Equal(t, offhostIntrospectionInterface, iface) -} - -func TestScanIPv4RoutesNoDefaultRoute(t *testing.T) { - iface, err := scanIPv4RoutesForDefaultInterface(strings.NewReader("")) - assert.Error(t, err) - assert.Equal(t, "", iface) -} - -func TestScanIPv4RoutesNoDefaultRouteExceptLoopback(t *testing.T) { - var testInput = `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT -lo 00000000 01201FAC 0003 0 0 0 00000000 0 0 0 -` - iface, err := scanIPv4RoutesForDefaultInterface(strings.NewReader(testInput)) - assert.Error(t, err) - assert.Equal(t, "", iface) -} - -func TestGetOffhostIntrospectionInterfaceWithEnvOverride(t *testing.T) { - os.Setenv(offhostIntrospectonAccessInterfaceEnv, "test_iface") - defer os.Unsetenv(offhostIntrospectonAccessInterfaceEnv) - - iface, err := getOffhostIntrospectionInterface() - assert.NoError(t, err) - assert.Equal(t, "test_iface", iface) -} - -func TestGetOffhostIntrospectionInterfaceWithEnvOverride_InvalidIfname(t *testing.T) { - os.Setenv(offhostIntrospectonAccessInterfaceEnv, "invalid ifname") - defer os.Unsetenv(offhostIntrospectonAccessInterfaceEnv) - defer overrideIPRouteInput(testIPV4RouteInput)() - - iface, err := getOffhostIntrospectionInterface() - assert.NoError(t, err) - // falls back to default - assert.Equal(t, offhostIntrospectionInterface, iface) -} - -func TestGetOffhostIntrospectionInterfaceUseDefaultV4(t *testing.T) { - defer overrideIPRouteInput(testIPV4RouteInput)() - - iface, err := getOffhostIntrospectionInterface() - assert.NoError(t, err) - assert.Equal(t, offhostIntrospectionInterface, iface) -} - -func TestGetOffhostIntrospectionInterfaceFailure(t *testing.T) { - defer overrideIPRouteInput("")() - - iface, err := getOffhostIntrospectionInterface() - assert.Error(t, err) - assert.Equal(t, "", iface) -}