Move internal/testutils/networking to integration/internal/testutils/networking

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan
2025-07-24 12:16:06 -07:00
parent 14eb2770b9
commit 6514282136
11 changed files with 9 additions and 9 deletions

View File

@@ -1,127 +0,0 @@
package networking
import (
"fmt"
"os/exec"
"regexp"
"strings"
"testing"
"github.com/docker/docker/testutil/daemon"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)
const (
// The name of the bridge driver's nftables tables.
nftTable = "docker-bridges"
// The name of the filter-FORWARD chain in nftTable.
nftFFChain = "filter-FORWARD"
)
// Find the policy in, for example "Chain FORWARD (policy ACCEPT)".
var rePolicy = regexp.MustCompile("policy ([A-Za-z]+)")
// SetFilterForwardPolicies sets the default policy for the FORWARD chain in
// the filter tables for both IPv4 and IPv6. The original policy is restored
// using t.Cleanup().
//
// There's only one filter-FORWARD policy, so this won't behave well if used by
// tests running in parallel in a single network namespace that expect different
// behaviour.
func SetFilterForwardPolicies(t *testing.T, firewallBackend string, policy string) {
t.Helper()
if strings.HasPrefix(firewallBackend, "iptables") {
setIptablesFFP(t, policy)
return
}
if strings.HasPrefix(firewallBackend, "nftables") {
setNftablesFFP(t, policy)
return
}
t.Fatalf("unknown firewall backend %s", firewallBackend)
}
func setIptablesFFP(t *testing.T, policy string) {
t.Helper()
for _, iptablesCmd := range []string{"iptables", "ip6tables"} {
origPolicy, err := getChainPolicy(t, exec.Command(iptablesCmd, "-L", "FORWARD"))
assert.NilError(t, err, "failed to get iptables policy")
if origPolicy == policy {
continue
}
if err := exec.Command(iptablesCmd, "-P", "FORWARD", policy).Run(); err != nil {
t.Fatalf("Failed to set %s FORWARD policy: %v", iptablesCmd, err)
}
t.Cleanup(func() {
if err := exec.Command(iptablesCmd, "-P", "FORWARD", origPolicy).Run(); err != nil {
t.Logf("Failed to restore %s FORWARD policy: %v", iptablesCmd, err)
}
})
}
}
func setNftablesFFP(t *testing.T, policy string) {
t.Helper()
policy = strings.ToLower(policy)
for _, family := range []string{"ip", "ip6"} {
origPolicy, err := getChainPolicy(t, exec.Command("nft", "list", "chain", family, nftTable, nftFFChain))
assert.NilError(t, err, "failed to get nftables policy")
if origPolicy == policy {
continue
}
cmd := func(p string) *exec.Cmd {
return exec.Command("nft", "add", "chain", family, nftTable, nftFFChain, "{", "policy", p, ";", "}")
}
if err := cmd(policy).Run(); err != nil {
t.Fatalf("Failed to set %s filter-FORWARD policy: %v", family, err)
}
t.Cleanup(func() {
if err := cmd(origPolicy).Run(); err != nil {
t.Logf("Failed to restore %s filter-FORWARD policy: %v", family, err)
}
})
}
}
func getChainPolicy(t *testing.T, cmd *exec.Cmd) (string, error) {
t.Helper()
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("getting policy: %w", err)
}
opMatch := rePolicy.FindSubmatch(out)
if len(opMatch) != 2 {
return "", fmt.Errorf("searching for policy: %w", err)
}
return string(opMatch[1]), nil
}
// FirewalldRunning returns true if "firewall-cmd --state" reports "running".
func FirewalldRunning() bool {
state, err := exec.Command("firewall-cmd", "--state").CombinedOutput()
return err == nil && strings.TrimSpace(string(state)) == "running"
}
// FirewalldReload reloads firewalld and waits for the daemon to re-create its rules.
// It's a no-op if firewalld is not running, and the test fails if the reload does
// not complete.
func FirewalldReload(t *testing.T, d *daemon.Daemon) {
t.Helper()
if !FirewalldRunning() {
return
}
lastReload := d.FirewallReloadedAt(t)
res := icmd.RunCommand("firewall-cmd", "--reload")
assert.NilError(t, res.Error)
poll.WaitOn(t, func(_ poll.LogT) poll.Result {
latestReload := d.FirewallReloadedAt(t)
if latestReload != "" && latestReload != lastReload {
t.Log("Firewalld reload completed at", latestReload)
return poll.Success()
}
return poll.Continue("firewalld reload not complete")
})
}

View File

@@ -1,197 +0,0 @@
package networking
import (
"bytes"
"net/netip"
"os/exec"
"runtime"
"strings"
"syscall"
"testing"
"github.com/vishvananda/netns"
)
// CurrentNetns can be passed to L3Segment.AddHost to indicate that the
// host lives in the current network namespace (eg. where dockerd runs).
const CurrentNetns = ""
func runCommand(t *testing.T, cmd string, args ...string) (string, error) {
t.Helper()
t.Log(strings.Join(append([]string{cmd}, args...), " "))
var b bytes.Buffer
c := exec.Command(cmd, args...)
c.Stdout = &b
c.Stderr = &b
err := c.Run()
return b.String(), err
}
// L3Segment simulates a switched, dual-stack capable network that
// interconnects multiple hosts running in their own network namespace.
type L3Segment struct {
Hosts map[string]Host
bridge Host
}
// NewL3Segment creates a new L3Segment. The bridge interface interconnecting
// all the hosts is created in a new network namespace named nsName and it's
// assigned one or more IP addresses. Those need to be unmasked netip.Prefix.
func NewL3Segment(t *testing.T, nsName string, addrs ...netip.Prefix) *L3Segment {
t.Helper()
l3 := &L3Segment{
Hosts: map[string]Host{},
}
l3.bridge = newHost(t, "bridge", nsName, "br0")
defer func() {
if t.Failed() {
l3.Destroy(t)
}
}()
l3.bridge.MustRun(t, "ip", "link", "add", l3.bridge.Iface, "type", "bridge")
for _, addr := range addrs {
l3.bridge.MustRun(t, "ip", "addr", "add", addr.String(), "dev", l3.bridge.Iface, "nodad")
l3.bridge.MustRun(t, "ip", "link", "set", l3.bridge.Iface, "up")
}
return l3
}
func (l3 *L3Segment) AddHost(t *testing.T, hostname, nsName, ifname string, addrs ...netip.Prefix) {
t.Helper()
if len(hostname) >= syscall.IFNAMSIZ {
// hostname is reused as the name for the veth interface added to the
// bridge. Hence, it needs to be shorter than ifnamsiz.
t.Fatalf("hostname too long")
}
host := newHost(t, hostname, nsName, ifname)
l3.Hosts[hostname] = host
host.MustRun(t, "ip", "link", "add", hostname, "netns", l3.bridge.ns, "type", "veth", "peer", "name", host.Iface)
l3.bridge.MustRun(t, "ip", "link", "set", hostname, "up", "master", l3.bridge.Iface)
host.MustRun(t, "ip", "link", "set", host.Iface, "up")
host.MustRun(t, "ip", "link", "set", "lo", "up")
for _, addr := range addrs {
host.MustRun(t, "ip", "addr", "add", addr.String(), "dev", host.Iface, "nodad")
}
}
func (l3 *L3Segment) Destroy(t *testing.T) {
t.Helper()
for _, host := range l3.Hosts {
host.Destroy(t)
}
l3.bridge.Destroy(t)
}
type Host struct {
Name string
Iface string // Iface is the interface name in the host network namespace.
ns string // ns is the network namespace name.
}
func newHost(t *testing.T, hostname, nsName, ifname string) Host {
t.Helper()
if len(ifname) >= syscall.IFNAMSIZ {
t.Fatalf("ifname too long")
}
if nsName != CurrentNetns {
if out, err := runCommand(t, "ip", "netns", "add", nsName); err != nil {
t.Log(out)
t.Fatalf("Error: %v", err)
}
}
return Host{
Name: hostname,
Iface: ifname,
ns: nsName,
}
}
// Run executes the provided command in the host's network namespace,
// returns its combined stdout/stderr, and error.
func (h Host) Run(t *testing.T, cmd string, args ...string) (string, error) {
t.Helper()
if h.ns != CurrentNetns {
args = append([]string{"netns", "exec", h.ns, cmd}, args...)
cmd = "ip"
}
return runCommand(t, cmd, args...)
}
// MustRun executes the provided command in the host's network namespace
// and returns its combined stdout/stderr, failing the test if the
// command returns an error.
func (h Host) MustRun(t *testing.T, cmd string, args ...string) string {
t.Helper()
out, err := h.Run(t, cmd, args...)
if err != nil {
t.Log(out)
t.Fatalf("Error: %v", err)
}
return out
}
// Do run the provided function in the host's network namespace.
func (h Host) Do(t *testing.T, fn func()) {
t.Helper()
if h.ns != CurrentNetns {
targetNs, err := netns.GetFromName(h.ns)
if err != nil {
t.Fatalf("failed to get netns handle: %v", err)
}
defer targetNs.Close()
origNs, err := netns.Get()
if err != nil {
t.Fatalf("failed to get current netns: %v", err)
}
defer origNs.Close()
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := netns.Set(targetNs); err != nil {
t.Fatalf("failed to enter netns: %v", err)
}
defer netns.Set(origNs)
}
fn()
}
func (h Host) Destroy(t *testing.T) {
t.Helper()
// When a netns is deleted while there's still veth interfaces in it, the
// kernel delete both ends of the veth pairs. The veth interface living in
// that netns will be deleted instantaneously, but the other end will be
// reclaimed after a short delay.
//
// If, in the meantime, a new test is spun up, and tries to create a new
// veth pair with the same peer name, the kernel will return -EEXIST.
//
// But, if the veth pair is explicitly deleted _before_ the netns, then
// both veth ends will be deleted instantaneously.
//
// Hence, we need to do just that here.
h.MustRun(t, "ip", "link", "delete", h.Iface)
if h.ns != CurrentNetns {
if out, err := runCommand(t, "ip", "netns", "delete", h.ns); err != nil {
t.Log(out)
t.Fatalf("Error: %v", err)
}
}
}