Skip to content
This repository was archived by the owner on Sep 13, 2024. It is now read-only.

Commit dba1dfd

Browse files
authored
check ipv4 routes for default network interface, only fall back to eth0 if none can be found (#498)
check ipv4 routes for default network interface, only fall back to eth0 if none can be found
1 parent b290eaf commit dba1dfd

File tree

3 files changed

+152
-8
lines changed

3 files changed

+152
-8
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Additionally, the following environment variable(s) can be used to configure the
2525
|:----------------|:----------------------------|:------------|:-----------------------|
2626
| `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` | <true | 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 `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` is set to true, this rule will not be added/removed. | false |
2727
| `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` | <true | false> | By default, the ecs-init service adds an iptable rule to block access to ECS Agent's introspection port from off-host (or containers in awsvpc network mode), and removes the rule upon stop. If `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` is set to true, this rule will not be added/removed. | false |
28-
| `ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME` | `eth0` | Primary network interface name to be used for blocking offhost agent introspection port access. By default, this value is `eth0` | `eth0` |
28+
| `ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME` | `eth0` | Primary network interface name to be used for blocking offhost agent introspection port access. By default, this value is the interface that handles the default route (`0.0.0.0/0`) in kernel routing table (`/proc/net/route`). If none could be found, we fall back to `eth0` | - (Resolved at runtime) |
2929

3030
The above environment variable(s) can be used in the following way
3131
- On Amazon Linux 1, the flag `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` can be turned on by adding `env ECS_SKIP_LOCALHOST_TRAFFIC_FILTER=true` to /etc/init/ecs.conf.

ecs-init/exec/iptables/iptables.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
package iptables
1515

1616
import (
17+
"bufio"
1718
"fmt"
19+
"io"
1820
"os"
1921
"strconv"
2022
"strings"
@@ -47,7 +49,15 @@ const (
4749
offhostIntrospectionAccessConfigEnv = "ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS"
4850
offhostIntrospectonAccessInterfaceEnv = "ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME"
4951
agentIntrospectionServerPort = "51678"
50-
defaultOffhostIntrospectionInterface = "eth0"
52+
53+
ipv4RouteFile = "/proc/net/route"
54+
ipv4ZeroAddrInHex = "00000000"
55+
loopbackInterfaceName = "lo"
56+
fallbackOffhostIntrospectionInterface = "eth0"
57+
)
58+
59+
var (
60+
defaultOffhostIntrospectionInterface = ""
5161
)
5262

5363
// NetfilterRoute implements the engine.credentialsProxyRoute interface by
@@ -69,6 +79,14 @@ func NewNetfilterRoute(cmdExec exec.Exec) (*NetfilterRoute, error) {
6979
return nil, err
7080
}
7181

82+
defaultOffhostIntrospectionInterface, err = getOffhostIntrospectionInterface()
83+
if err != nil {
84+
log.Warnf("Error resolving default offhost introspection network interface, will use eth0 as fallback: %+v", err)
85+
// fall back to the previous behavior (always use 'eth0') in the rare case that it
86+
// might affect some customer with a special routing setup that's previously working.
87+
defaultOffhostIntrospectionInterface = fallbackOffhostIntrospectionInterface
88+
}
89+
7290
return &NetfilterRoute{
7391
cmdExec: cmdExec,
7492
}, nil
@@ -189,18 +207,51 @@ func getBlockIntrospectionOffhostAccessInputChainArgs() []string {
189207
return []string{
190208
"INPUT",
191209
"-p", "tcp",
192-
"-i", getOffhostIntrospectionInterface(),
210+
"-i", defaultOffhostIntrospectionInterface,
193211
"--dport", agentIntrospectionServerPort,
194212
"-j", "DROP",
195213
}
196214
}
197215

198-
func getOffhostIntrospectionInterface() string {
216+
func getOffhostIntrospectionInterface() (string, error) {
199217
s := os.Getenv(offhostIntrospectonAccessInterfaceEnv)
200218
if s != "" {
201-
return s
219+
return s, nil
220+
}
221+
return getDefaultNetworkInterfaceIPv4()
222+
}
223+
224+
// 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)
225+
// Example file content:
226+
// $ sudo cat /proc/net/route
227+
// Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
228+
// ens5 00000000 01201FAC 0003 0 0 512 00000000 0 0 0
229+
// ...
230+
//
231+
// 1st column contains interface name
232+
// 2nd column contains destination network in hex
233+
var getDefaultNetworkInterfaceIPv4 = func() (string, error) {
234+
input, err := os.Open(ipv4RouteFile)
235+
if err != nil {
236+
return "", fmt.Errorf("could not get IPv4 route input: %v", err)
237+
}
238+
defer input.Close()
239+
return scanIPv4RoutesForDefaultInterface(input)
240+
}
241+
242+
func scanIPv4RoutesForDefaultInterface(input io.Reader) (string, error) {
243+
scanner := bufio.NewScanner(input)
244+
for scanner.Scan() {
245+
line := scanner.Text()
246+
if strings.HasPrefix(line, "Iface") { // skip header line
247+
continue
248+
}
249+
fields := strings.Fields(line)
250+
if (fields[1] == ipv4ZeroAddrInHex) && (fields[0] != loopbackInterfaceName) {
251+
return fields[0], nil
252+
}
202253
}
203-
return defaultOffhostIntrospectionInterface
254+
return "", fmt.Errorf("could not find a default IPv4 route through non-loopback interface")
204255
}
205256

206257
func getOutputChainArgs() []string {

ecs-init/exec/iptables/iptables_test.go

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package iptables
1616
import (
1717
"fmt"
1818
"os"
19+
"strings"
1920
"testing"
2021

2122
"github.com/golang/mock/gomock"
@@ -40,9 +41,10 @@ var (
4041
"!", "--ctstate", "RELATED,ESTABLISHED,DNAT",
4142
"-j", "DROP",
4243
}
44+
offhostIntrospectionInterface = "ens5"
4345
blockIntrospectionOffhostAccessInputRouteArgs = []string{
4446
"-p", "tcp",
45-
"-i", "eth0",
47+
"-i", offhostIntrospectionInterface,
4648
"--dport", agentIntrospectionServerPort,
4749
"-j", "DROP",
4850
}
@@ -59,9 +61,30 @@ var (
5961
"-j", "REDIRECT",
6062
"--to-ports", localhostCredentialsProxyPort,
6163
}
64+
65+
testIPV4RouteInput = `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
66+
ens5 00000000 01201FAC 0003 0 0 0 00000000 0 0 0
67+
ens5 FEA9FEA9 00000000 0005 0 0 0 FFFFFFFF 0 0 0
68+
ens5 00201FAC 00000000 0001 0 0 0 00F0FFFF 0 0 0
69+
`
6270
)
6371

72+
func overrideIPRouteInput(ipv4RouteInput string) func() {
73+
originalv4 := getDefaultNetworkInterfaceIPv4
74+
75+
getDefaultNetworkInterfaceIPv4 = func() (string, error) {
76+
return scanIPv4RoutesForDefaultInterface(strings.NewReader(ipv4RouteInput))
77+
}
78+
79+
return func() {
80+
getDefaultNetworkInterfaceIPv4 = originalv4
81+
// in real environment we'll only set it once, for testing we unset it after executing relevant test cases
82+
defaultOffhostIntrospectionInterface = ""
83+
}
84+
}
85+
6486
func TestNewNetfilterRouteFailsWhenExecutableNotFound(t *testing.T) {
87+
defer overrideIPRouteInput(testIPV4RouteInput)()
6588
ctrl := gomock.NewController(t)
6689
defer ctrl.Finish()
6790

@@ -72,7 +95,21 @@ func TestNewNetfilterRouteFailsWhenExecutableNotFound(t *testing.T) {
7295
assert.Error(t, err, "Expected error when executable's path lookup fails")
7396
}
7497

98+
func TestNewNetfilterRouteWithDefaultOffhostIntrospectionInterfaceFallback(t *testing.T) {
99+
defer overrideIPRouteInput("")()
100+
ctrl := gomock.NewController(t)
101+
defer ctrl.Finish()
102+
103+
mockExec := NewMockExec(ctrl)
104+
mockExec.EXPECT().LookPath(iptablesExecutable).Return("", nil)
105+
106+
_, err := NewNetfilterRoute(mockExec)
107+
assert.NoError(t, err)
108+
assert.Equal(t, defaultOffhostIntrospectionInterface, fallbackOffhostIntrospectionInterface)
109+
}
110+
75111
func TestCreate(t *testing.T) {
112+
defer overrideIPRouteInput(testIPV4RouteInput)()
76113
testCases := []struct {
77114
setOffhostInterface bool
78115
inputRouteArgs []string
@@ -124,6 +161,7 @@ func TestCreate(t *testing.T) {
124161
}
125162

126163
func TestCreateSkipLocalTrafficFilter(t *testing.T) {
164+
defer overrideIPRouteInput(testIPV4RouteInput)()
127165
os.Setenv("ECS_SKIP_LOCALHOST_TRAFFIC_FILTER", "true")
128166
defer os.Unsetenv("ECS_SKIP_LOCALHOST_TRAFFIC_FILTER")
129167

@@ -226,6 +264,7 @@ func TestCreateErrorOnInputChainCommandError(t *testing.T) {
226264
}
227265

228266
func TestCreateErrorOnOutputChainCommandError(t *testing.T) {
267+
defer overrideIPRouteInput(testIPV4RouteInput)()
229268
ctrl := gomock.NewController(t)
230269
defer ctrl.Finish()
231270

@@ -256,6 +295,7 @@ func TestCreateErrorOnOutputChainCommandError(t *testing.T) {
256295
}
257296

258297
func TestRemove(t *testing.T) {
298+
defer overrideIPRouteInput(testIPV4RouteInput)()
259299
ctrl := gomock.NewController(t)
260300
defer ctrl.Finish()
261301

@@ -287,6 +327,7 @@ func TestRemove(t *testing.T) {
287327
}
288328

289329
func TestRemoveSkipLocalTrafficFilter(t *testing.T) {
330+
defer overrideIPRouteInput(testIPV4RouteInput)()
290331
os.Setenv("ECS_SKIP_LOCALHOST_TRAFFIC_FILTER", "true")
291332
defer os.Unsetenv("ECS_SKIP_LOCALHOST_TRAFFIC_FILTER")
292333

@@ -316,6 +357,7 @@ func TestRemoveSkipLocalTrafficFilter(t *testing.T) {
316357
}
317358

318359
func TestRemoveAllowIntrospectionOffhostAccess(t *testing.T) {
360+
defer overrideIPRouteInput(testIPV4RouteInput)()
319361
os.Setenv(offhostIntrospectionAccessConfigEnv, "true")
320362
defer os.Unsetenv(offhostIntrospectionAccessConfigEnv)
321363

@@ -348,6 +390,7 @@ func TestRemoveAllowIntrospectionOffhostAccess(t *testing.T) {
348390
}
349391

350392
func TestRemoveErrorOnPreroutingChainCommandError(t *testing.T) {
393+
defer overrideIPRouteInput(testIPV4RouteInput)()
351394
ctrl := gomock.NewController(t)
352395
defer ctrl.Finish()
353396

@@ -378,6 +421,7 @@ func TestRemoveErrorOnPreroutingChainCommandError(t *testing.T) {
378421
}
379422

380423
func TestRemoveErrorOnOutputChainCommandError(t *testing.T) {
424+
defer overrideIPRouteInput(testIPV4RouteInput)()
381425
ctrl := gomock.NewController(t)
382426
defer ctrl.Finish()
383427

@@ -408,6 +452,7 @@ func TestRemoveErrorOnOutputChainCommandError(t *testing.T) {
408452
}
409453

410454
func TestRemoveErrorOnInputChainCommandsErrors(t *testing.T) {
455+
defer overrideIPRouteInput(testIPV4RouteInput)()
411456
ctrl := gomock.NewController(t)
412457
defer ctrl.Finish()
413458

@@ -473,10 +518,12 @@ func TestGetLocalhostTrafficFilterInputChainArgs(t *testing.T) {
473518
}
474519

475520
func TestGetBlockIntrospectionOffhostAccessInputChainArgs(t *testing.T) {
521+
defer overrideIPRouteInput(testIPV4RouteInput)()
522+
defaultOffhostIntrospectionInterface, _ = getOffhostIntrospectionInterface()
476523
assert.Equal(t, []string{
477524
"INPUT",
478525
"-p", "tcp",
479-
"-i", "eth0",
526+
"-i", "ens5",
480527
"--dport", "51678",
481528
"-j", "DROP",
482529
}, getBlockIntrospectionOffhostAccessInputChainArgs())
@@ -504,3 +551,49 @@ func TestGetActionName(t *testing.T) {
504551
func expectedArgs(table, action, chain string, args []string) []string {
505552
return append([]string{"-t", table, action, chain}, args...)
506553
}
554+
555+
func TestScanIPv4RoutesHappyCase(t *testing.T) {
556+
iface, err := scanIPv4RoutesForDefaultInterface(strings.NewReader(testIPV4RouteInput))
557+
assert.NoError(t, err)
558+
assert.Equal(t, offhostIntrospectionInterface, iface)
559+
}
560+
561+
func TestScanIPv4RoutesNoDefaultRoute(t *testing.T) {
562+
iface, err := scanIPv4RoutesForDefaultInterface(strings.NewReader(""))
563+
assert.Error(t, err)
564+
assert.Equal(t, "", iface)
565+
}
566+
567+
func TestScanIPv4RoutesNoDefaultRouteExceptLoopback(t *testing.T) {
568+
var testInput = `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
569+
lo 00000000 01201FAC 0003 0 0 0 00000000 0 0 0
570+
`
571+
iface, err := scanIPv4RoutesForDefaultInterface(strings.NewReader(testInput))
572+
assert.Error(t, err)
573+
assert.Equal(t, "", iface)
574+
}
575+
576+
func TestGetOffhostIntrospectionInterfaceWithEnvOverride(t *testing.T) {
577+
os.Setenv(offhostIntrospectonAccessInterfaceEnv, "test_iface")
578+
defer os.Unsetenv(offhostIntrospectonAccessInterfaceEnv)
579+
580+
iface, err := getOffhostIntrospectionInterface()
581+
assert.NoError(t, err)
582+
assert.Equal(t, "test_iface", iface)
583+
}
584+
585+
func TestGetOffhostIntrospectionInterfaceUseDefaultV4(t *testing.T) {
586+
defer overrideIPRouteInput(testIPV4RouteInput)()
587+
588+
iface, err := getOffhostIntrospectionInterface()
589+
assert.NoError(t, err)
590+
assert.Equal(t, offhostIntrospectionInterface, iface)
591+
}
592+
593+
func TestGetOffhostIntrospectionInterfaceFailure(t *testing.T) {
594+
defer overrideIPRouteInput("")()
595+
596+
iface, err := getOffhostIntrospectionInterface()
597+
assert.Error(t, err)
598+
assert.Equal(t, "", iface)
599+
}

0 commit comments

Comments
 (0)