Generate iptables documentation

In an integration test - run a daemon, capture iptables, and feed them
to a markdown text/template describing them.

Prep for repeating that, for different network configurations.

Fail the test if the generated markdown differs from a "golden" version.

(So, at-least the golden markdown will need to be updated if the
iptables rules are deliberately changed - hopefully the corresponding
description in the template will also be updated.)

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2024-10-10 18:39:40 +01:00
parent d7ff538467
commit 20571e453b
6 changed files with 579 additions and 4 deletions

View File

@@ -0,0 +1,159 @@
## iptables for a new Daemon
When the daemon starts, it creates custom chains, and rules for the
default bridge network.
Table `filter`:
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER-USER 0 -- * * 0.0.0.0/0 0.0.0.0/0
2 0 0 DOCKER-ISOLATION-STAGE-1 0 -- * * 0.0.0.0/0 0.0.0.0/0
3 0 0 ACCEPT 0 -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
4 0 0 DOCKER 0 -- * docker0 0.0.0.0/0 0.0.0.0/0
5 0 0 ACCEPT 0 -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
6 0 0 ACCEPT 0 -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain DOCKER (1 references)
num pkts bytes target prot opt in out source destination
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER-ISOLATION-STAGE-2 0 -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
2 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DROP 0 -- * docker0 0.0.0.0/0 0.0.0.0/0
2 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
<details>
<summary>iptables commands</summary>
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
</details>
The FORWARD chain's policy shown above is ACCEPT. However:
- For IPv4, [setupIPForwarding][1] sets the POLICY to DROP if the sysctl
net.ipv4.ip_forward was not set to '1', and the daemon set it itself.
- For IPv6, the policy is always DROP.
[1]: https://github.com/moby/moby/blob/cff4f20c44a3a7c882ed73934dec6a77246c6323/libnetwork/drivers/bridge/setup_ip_forwarding.go#L44
The FORWARD chain rules are numbered in the output above, they are:
1. Unconditional jump to DOCKER-USER.
This is set up by libnetwork, in [setupUserChain][10].
Docker won't add rules to the DOCKER-USER chain, it's only for user-defined rules.
It's (mostly) kept at the top of the by deleting it and re-creating after each
new network is created, while traffic may be running for other networks.
2. Unconditional jump to DOCKER-ISOLATION-STAGE-1.
Set up during network creation by [setupIPTables][11], which ensures it appears
after the jump to DOCKER-USER (by deleting it and re-creating, while traffic
may be running for other networks).
3. ACCEPT RELATED,ESTABLISHED packets into a specific bridge network.
Allows responses to outgoing requests, and continuation of incoming requests,
without needing to process any further rules.
This rule is also added during network creation, but the code to do it
is in libnetwork, [ProgramChain][12].
4. Jump to DOCKER, for any packet destined for a bridge network. Added when
the network is created, in [ProgramChain][13] ("filterChain" is the DOCKER chain).
The DOCKER chain implements per-port/protocol filtering for each container.
5. ACCEPT any packet leaving a network, also set up when the network is created, in
[setupIPTablesInternal][14].
6. ACCEPT packets flowing between containers within a network, because by default
container isolation is disabled. Also set up when the network is created, in
[setIcc][15].
[10]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/firewall_linux.go#L50
[11]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L201
[12]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/iptables/iptables.go#L270
[13]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/iptables/iptables.go#L251-L255
[14]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L264
[15]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L343
_With ICC enabled 5 and 6 could be combined, to ACCEPT anything from the bridge.
But, when ICC is disabled, rule 6 is DROP, so it would need to be placed before
rule 5. Because the rules are generated in different places, that's a slightly
bigger change than it should be._
The DOCKER chain is empty, because there are no containers with port mappings yet.
The DOCKER-ISOLATION chains implement inter-network isolation, all (unrelated)
packets are processed by these chains. The rule are inserted at the head of the
chain when a network is created, in [setINC][20].
- DOCKER-ISOLATION-STAGE-1 jumps to DOCKER-ISOLATION-STAGE-2 for any packet
routed to a docker network that has not come from that docker network.
- DOCKER-ISOLATION-STAGE-2 processes all packets leaving a bridge network,
packets that are destined for any other network are dropped.
[20]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L369
Table nat:
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER 0 -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER 0 -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 MASQUERADE 0 -- * !docker0 172.17.0.0/16 0.0.0.0/0
Chain DOCKER (2 references)
num pkts bytes target prot opt in out source destination
1 0 0 RETURN 0 -- docker0 * 0.0.0.0/0 0.0.0.0/0
<details>
<summary>iptables commands</summary>
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
</details>

View File

@@ -0,0 +1,41 @@
# Docker Engine's use of iptables
> [!WARNING]
> This is intended for development use - the structure of docker's iptables
> (and ip6tables) rules will change between releases, it is not a stable
> interface.
> [!NOTE]
> This document is generated by `TestBridgeIptablesDoc` by running a
> daemon, creating networks and containers, and capturing iptables.
> The iptables are then merged with a text/template for each section.
> The resulting document is diffed against one in the repo, so the
> test will fail if there are differences in the generated rules (but
> changes in the templates may go unnoticed).
>
> Links to code are permalinks - they will be out of date, and may not
> point to the master branch. But, it's difficult to work out where
> some of the rules come from, the links are intended as hints.
ip6tables rules follow the same pattern as iptables rules. So, only the
IPv4 rules are shown here.
The bridge driver deletes its custom chains during its initialisation, in
[configure][100]. Rules are then re-created as networks are restored. However,
the filter-FORWARD chain is not cleared. The order in which networks are
re-created is not the order in which they were originally created. So,
rules may be arranged differently following a daemon restart.
When firewalld is running, if it's reloaded, iptables rules are cleared.
The daemon registers handlers for its reload event (received via dbus)
to reconstruct the rules.
The filter-INPUT chain is not used by Docker. Packets arriving from the host's
physical network or the host itself hit the filter-FORWARD chain, as they are
routed into the bridge network. Similarly, filter-OUTPUT is not used.
[100]: https://github.com/moby/moby/blob/fe09cab7fe04c3911417061f7c7ef60a8acc6bf3/libnetwork/drivers/bridge/bridge_linux.go#L508
Scenarios:
- [New daemon](generated/new-daemon.md)

View File

@@ -0,0 +1,233 @@
// Package iptablesdoc runs docker, creates networks, runs containers and
// captures iptables output for various configurations.
//
// The iptables output is then used with a markdown text/template from the
// "templates" directory for each configuration (for each "section" in "index"),
// to generate a markdown document for each section.
//
// The newly generated documents are placed in:
//
// bundles/test-integration/TestBridgeIptablesDoc/iptables.md
//
// If the generated doc differs from the "golden" reference in "generated/",
// the test fails. When that happens:
//
// - check the iptables rules changes in the diff
// - update the description in the corresponding "_templ.md" file
// - re-run with TESTFLAGS='-update' to update the reference docs
package iptablesdoc
import (
"context"
"fmt"
"net/netip"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"text/template"
containertypes "github.com/docker/docker/api/types/container"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/integration/internal/network"
"github.com/docker/docker/internal/testutils/networking"
"github.com/docker/docker/libnetwork/drivers/bridge"
"github.com/docker/docker/testutil"
"github.com/docker/docker/testutil/daemon"
"github.com/docker/go-connections/nat"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
"gotest.tools/v3/skip"
)
var (
docNetworks = []string{"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"}
docGateways = []string{"192.0.2.1", "198.51.100.1", "203.0.113.1"}
)
type ctr struct {
name string
portMappings nat.PortMap
}
type bridgeNetwork struct {
bridge string
gwMode string
noICC bool
internal bool
containers []ctr
}
type section struct {
name string
noUserlandProxy bool
networks []bridgeNetwork
}
var index = []section{
{
name: "new-daemon.md",
},
}
// iptCmdType is used to look up iptCmds in the markdown (can't use an int
// type, or a new string type, so it's just an alias).
type iptCmdType = string
const (
iptCmdLFilter4 iptCmdType = "LFilter4"
iptCmdSFilter4 iptCmdType = "SFilter4"
iptCmdSFilterForward4 iptCmdType = "SFilterForward4"
iptCmdSFilterDocker4 iptCmdType = "SFilterDocker4"
iptCmdLNat4 iptCmdType = "LNat4"
iptCmdSNat4 iptCmdType = "SNat4"
)
var iptCmds = map[iptCmdType][]string{
iptCmdLFilter4: {"iptables", "-nvL", "--line-numbers", "-t", "filter"},
iptCmdSFilter4: {"iptables", "-S", "-t", "filter"},
iptCmdSFilterForward4: {"iptables", "-S", "FORWARD"},
iptCmdSFilterDocker4: {"iptables", "-S", "DOCKER"},
iptCmdLNat4: {"iptables", "-nvL", "--line-numbers", "-t", "nat"},
iptCmdSNat4: {"iptables", "-S", "-t", "nat"},
}
func TestBridgeIptablesDoc(t *testing.T) {
skip.If(t, testEnv.IsRootless)
ctx := setupTest(t)
// Get the full path for "bundles/TestBridgeIptablesDoc".
dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST")
if dest == "" {
dest = os.Getenv("DEST")
}
dest = filepath.Join(dest, t.Name())
// Set up an L3Segment, which will have a netns for each "section".
addr4 := netip.MustParseAddr("192.168.124.1")
addr6 := netip.MustParseAddr("fdc0:36dc:a4dd::1")
l3 := networking.NewL3Segment(t, "gen-iptables-doc",
netip.PrefixFrom(addr4, 24),
netip.PrefixFrom(addr6, 64),
)
t.Cleanup(func() { l3.Destroy(t) })
for i, sec := range index {
// Create a netns for this section.
addr4 = addr4.Next()
addr6 = addr6.Next()
hostname := fmt.Sprintf("docker%d", i)
l3.AddHost(t, hostname, hostname+"-host", "eth0",
netip.PrefixFrom(addr4, 24),
netip.PrefixFrom(addr6, 64),
)
host := l3.Hosts[hostname]
// Stop the interface, to reduce the chances of stray packets getting counted by iptables.
host.Run(t, "ip", "link", "set", "eth0", "down")
t.Run("gen_"+sec.name, func(t *testing.T) {
// t.Parallel() - doesn't speed things up, startup times just extend
runTestNet(t, testutil.StartSpan(ctx, t), dest, sec, host)
})
}
}
func runTestNet(t *testing.T, ctx context.Context, bundlesDir string, section section, host networking.Host) {
var dArgs []string
if section.noUserlandProxy {
dArgs = append(dArgs, "--userland-proxy=false")
}
// Start the daemon in its own network namespace.
var d *daemon.Daemon
host.Do(t, func() {
// Run without OTEL because there's no routing from this netns for it - which
// means the daemon doesn't shut down cleanly, causing the test to fail.
d = daemon.New(t, daemon.WithEnvVars("OTEL_EXPORTER_OTLP_ENDPOINT="))
d.StartWithBusybox(ctx, t, dArgs...)
t.Cleanup(func() { d.Stop(t) })
})
c := d.NewClientT(t)
t.Cleanup(func() { c.Close() })
assert.Assert(t, len(section.networks) < len(docNetworks), "Don't have enough container network addresses")
for i, nw := range section.networks {
gwMode := nw.gwMode
if gwMode == "" {
gwMode = "nat"
}
netOpts := []func(*networktypes.CreateOptions){
network.WithIPAM(docNetworks[i], docGateways[i]),
network.WithOption(bridge.BridgeName, nw.bridge),
network.WithOption(bridge.IPv4GatewayMode, gwMode),
}
if nw.noICC {
netOpts = append(netOpts, network.WithOption(bridge.EnableICC, "false"))
}
if nw.internal {
netOpts = append(netOpts, network.WithInternal())
}
network.CreateNoError(ctx, t, c, nw.bridge, netOpts...)
t.Cleanup(func() { network.RemoveNoError(ctx, t, c, nw.bridge) })
for _, ctr := range nw.containers {
var exposedPorts []string
for ep := range ctr.portMappings {
exposedPorts = append(exposedPorts, ep.Port()+"/"+ep.Proto())
}
id := container.Run(ctx, t, c,
container.WithNetworkMode(nw.bridge),
container.WithExposedPorts(exposedPorts...),
container.WithPortMap(ctr.portMappings),
)
t.Cleanup(func() {
c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
})
}
}
iptablesOutput := runIptables(t, host)
generated := generate(t, section.name, iptablesOutput)
// Write the output to the 'bundles' directory for easy reference.
outFile := filepath.Join(bundlesDir, section.name)
err := os.WriteFile(outFile, []byte(generated), 0o644)
assert.NilError(t, err)
t.Log("Wrote ", outFile)
// Compare against "golden" results.
// Use full path so that the directory containing generated docs doesn't
// have to be called 'testdata'.
wd, err := os.Getwd()
assert.NilError(t, err)
golden.Assert(t, generated, filepath.Join(wd, "generated", section.name))
}
var rePacketByteCounts = regexp.MustCompile(`\d+ packets, \d+ bytes`)
func runIptables(t *testing.T, host networking.Host) map[iptCmdType]string {
host.Run(t, "iptables", "-Z")
host.Run(t, "iptables", "-Z", "-t", "nat")
res := map[iptCmdType]string{}
for k, cmd := range iptCmds {
d := host.Run(t, cmd[0], cmd[1:]...)
// In CI, the OUTPUT chain sometimes sees a packet. Remove the counts.
d = rePacketByteCounts.ReplaceAllString(d, "0 packets, 0 bytes")
// Indent the result, so that it's treated as preformatted markdown.
res[k] = strings.ReplaceAll(d, "\n", "\n ")
}
return res
}
func generate(t *testing.T, name string, data map[iptCmdType]string) string {
t.Helper()
templ, err := template.New(name).ParseFiles(filepath.Join("templates", name))
assert.NilError(t, err)
wr := strings.Builder{}
err = templ.ExecuteTemplate(&wr, name, data)
assert.NilError(t, err)
return wr.String()
}

View File

@@ -0,0 +1,56 @@
package iptablesdoc // import "github.com/docker/docker/integration/network/bridge/iptablesdoc"
import (
"context"
"os"
"testing"
"github.com/docker/docker/testutil"
"github.com/docker/docker/testutil/environment"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
)
var (
testEnv *environment.Execution
baseContext context.Context
)
func TestMain(m *testing.M) {
shutdown := testutil.ConfigureTracing()
ctx, span := otel.Tracer("").Start(context.Background(), "integration/network/bridge/iptablesdoc.TestMain")
baseContext = ctx
var err error
testEnv, err = environment.New(ctx)
if err != nil {
span.SetStatus(codes.Error, err.Error())
span.End()
shutdown(ctx)
panic(err)
}
err = environment.EnsureFrozenImagesLinux(ctx, testEnv)
if err != nil {
span.SetStatus(codes.Error, err.Error())
span.End()
shutdown(ctx)
panic(err)
}
testEnv.Print()
code := m.Run()
if code != 0 {
span.SetStatus(codes.Error, "m.Run() returned non-zero exit code")
}
span.End()
shutdown(ctx)
os.Exit(code)
}
func setupTest(t *testing.T) context.Context {
ctx := testutil.StartSpan(baseContext, t)
environment.ProtectAll(ctx, t, testEnv)
t.Cleanup(func() { testEnv.Clean(ctx, t) })
return ctx
}

View File

@@ -0,0 +1,83 @@
## iptables for a new Daemon
When the daemon starts, it creates custom chains, and rules for the
default bridge network.
Table `filter`:
{{index . "LFilter4"}}
<details>
<summary>iptables commands</summary>
{{index . "SFilter4"}}
</details>
The FORWARD chain's policy shown above is ACCEPT. However:
- For IPv4, [setupIPForwarding][1] sets the POLICY to DROP if the sysctl
net.ipv4.ip_forward was not set to '1', and the daemon set it itself.
- For IPv6, the policy is always DROP.
[1]: https://github.com/moby/moby/blob/cff4f20c44a3a7c882ed73934dec6a77246c6323/libnetwork/drivers/bridge/setup_ip_forwarding.go#L44
The FORWARD chain rules are numbered in the output above, they are:
1. Unconditional jump to DOCKER-USER.
This is set up by libnetwork, in [setupUserChain][10].
Docker won't add rules to the DOCKER-USER chain, it's only for user-defined rules.
It's (mostly) kept at the top of the by deleting it and re-creating after each
new network is created, while traffic may be running for other networks.
2. Unconditional jump to DOCKER-ISOLATION-STAGE-1.
Set up during network creation by [setupIPTables][11], which ensures it appears
after the jump to DOCKER-USER (by deleting it and re-creating, while traffic
may be running for other networks).
3. ACCEPT RELATED,ESTABLISHED packets into a specific bridge network.
Allows responses to outgoing requests, and continuation of incoming requests,
without needing to process any further rules.
This rule is also added during network creation, but the code to do it
is in libnetwork, [ProgramChain][12].
4. Jump to DOCKER, for any packet destined for a bridge network. Added when
the network is created, in [ProgramChain][13] ("filterChain" is the DOCKER chain).
The DOCKER chain implements per-port/protocol filtering for each container.
5. ACCEPT any packet leaving a network, also set up when the network is created, in
[setupIPTablesInternal][14].
6. ACCEPT packets flowing between containers within a network, because by default
container isolation is disabled. Also set up when the network is created, in
[setIcc][15].
[10]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/firewall_linux.go#L50
[11]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L201
[12]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/iptables/iptables.go#L270
[13]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/iptables/iptables.go#L251-L255
[14]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L264
[15]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L343
_With ICC enabled 5 and 6 could be combined, to ACCEPT anything from the bridge.
But, when ICC is disabled, rule 6 is DROP, so it would need to be placed before
rule 5. Because the rules are generated in different places, that's a slightly
bigger change than it should be._
The DOCKER chain is empty, because there are no containers with port mappings yet.
The DOCKER-ISOLATION chains implement inter-network isolation, all (unrelated)
packets are processed by these chains. The rule are inserted at the head of the
chain when a network is created, in [setINC][20].
- DOCKER-ISOLATION-STAGE-1 jumps to DOCKER-ISOLATION-STAGE-2 for any packet
routed to a docker network that has not come from that docker network.
- DOCKER-ISOLATION-STAGE-2 processes all packets leaving a bridge network,
packets that are destined for any other network are dropped.
[20]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L369
Table nat:
{{index . "LNat4"}}
<details>
<summary>iptables commands</summary>
{{index . "SNat4"}}
</details>

View File

@@ -16,7 +16,8 @@ import (
// host lives in the current network namespace (eg. where dockerd runs).
const CurrentNetns = ""
func runCommand(t *testing.T, cmd string, args ...string) {
func runCommand(t *testing.T, cmd string, args ...string) string {
t.Helper()
t.Log(strings.Join(append([]string{cmd}, args...), " "))
var b bytes.Buffer
@@ -28,6 +29,7 @@ func runCommand(t *testing.T, cmd string, args ...string) {
t.Log(b.String())
t.Fatalf("Error: %v", err)
}
return b.String()
}
// L3Segment simulates a switched, dual-stack capable network that
@@ -113,15 +115,16 @@ func newHost(t *testing.T, nsName, ifname string) Host {
}
}
// Run executes the provided command in the host's network namespace.
func (h Host) Run(t *testing.T, cmd string, args ...string) {
// Run executes the provided command in the host's network namespace
// and returns its combined stdout/stderr.
func (h Host) Run(t *testing.T, cmd string, args ...string) string {
t.Helper()
if h.ns != CurrentNetns {
args = append([]string{"netns", "exec", h.ns, cmd}, args...)
cmd = "ip"
}
runCommand(t, cmd, args...)
return runCommand(t, cmd, args...)
}
// Do run the provided function in the host's network namespace.