File: l3_segment_linux.go

package info (click to toggle)
docker.io 28.5.2%2Bdfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 69,048 kB
  • sloc: sh: 5,867; makefile: 863; ansic: 184; python: 162; asm: 159
file content (197 lines) | stat: -rw-r--r-- 5,235 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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)
		}
	}
}