]> git.ipfire.org Git - thirdparty/wireguard-go.git/commitdiff
[WIP] device: run testns.sh from a Go test harness dc/nstest
authorDavid Crawshaw <crawshaw@tailscale.com>
Mon, 4 May 2020 03:07:19 +0000 (23:07 -0400)
committerDavid Crawshaw <crawshaw@tailscale.com>
Mon, 4 May 2020 03:08:38 +0000 (23:08 -0400)
objectives:
- to make it easy to run these tests during go development
  (TODO: running sudo go test is weird, is there some
  alternative? does docker provide a namespace hole for
  normal users if it's installed?)
- to make it easy to run some part of the script, e.g.
  just run the sticky sockets test without waiting on
  the prior iperf tests

not clear yet if the shell-inside-go is worth it

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
device/ns_test.go [new file with mode: 0644]

diff --git a/device/ns_test.go b/device/ns_test.go
new file mode 100644 (file)
index 0000000..eb10a87
--- /dev/null
@@ -0,0 +1,292 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2017-2020 WireGuard LLC. All Rights Reserved.
+ */
+
+package device_test
+
+import (
+       "bytes"
+       "fmt"
+       "io/ioutil"
+       "log"
+       "os"
+       "os/exec"
+       "runtime"
+       "sort"
+       "strings"
+       "testing"
+       "time"
+
+       "golang.zx2c4.com/wireguard/device"
+       "golang.zx2c4.com/wireguard/ipc"
+       "golang.zx2c4.com/wireguard/tun"
+)
+
+// Test the following topology:
+//
+// ┌─────────────────────┐   ┌──────────────────────────────────┐   ┌─────────────────────┐
+// │  $netns1 namespace  │   │         $netns0 namespace        │   │  $netns2 namespace  │
+// │                     │   │                                  │   │                     │
+// │┌────────┐           │   │            ┌────────┐            │   │           ┌────────┐│
+// ││  $wg1  │───────────┼───┼────────────│   lo   │────────────┼───┼───────────│  $wg2  ││
+// │├────────┴──────────┐│   │    ┌───────┴────────┴────────┐   │   │┌──────────┴────────┤│
+// ││192.168.241.1/24   ││   │    │(ns1)         (ns2)      │   │   ││192.168.241.2/24   ││
+// ││fd00::1/24         ││   │    │127.0.0.1:1   127.0.0.1:2│   │   ││fd00::2/24         ││
+// │└───────────────────┘│   │    │[::]:1        [::]:2     │   │   │└───────────────────┘│
+// └─────────────────────┘   │    └─────────────────────────┘   │   └─────────────────────┘
+//                           └──────────────────────────────────┘
+//
+// Note: $netns0 is the endpoint for the wg1 interfaces in $netns1 and $netns2.
+// See https://www.wireguard.com/netns/ for further details.
+func TestNS(t *testing.T) {
+       checkRootOnLinux(t)
+
+       mustsh := func(t *testing.T, cmd string, arg, stdin string) string {
+               t.Helper()
+               sh := exec.Command(cmd, arg)
+               if stdin != "" {
+                       sh.Stdin = strings.NewReader(stdin)
+               }
+               out, err := sh.CombinedOutput()
+               if err != nil {
+                       t.Fatalf("%s %s: %v", cmd, arg, err)
+               }
+               return strings.TrimSpace(string(out))
+       }
+
+       netns := func(num int) string {
+               return fmt.Sprintf("wg-test-%d-%d", os.Getpid(), num)
+       }
+       wg := func(num int) string {
+               return fmt.Sprintf("wg%d%d", 1, num)
+       }
+       key1 := mustsh(t, "wg", "genkey", "")
+       key2 := mustsh(t, "wg", "genkey", "")
+       script := scriptScope{
+               vars: map[string]string{
+                       "netns0": netns(0),
+                       "netns1": netns(1),
+                       "netns2": netns(2),
+                       "wg1":    wg(1),
+                       "wg2":    wg(2),
+                       "key1":   key1,
+                       "key2":   key2,
+                       "pub1":   mustsh(t, "wg", "pubkey", key1),
+                       "pub2":   mustsh(t, "wg", "pubkey", key2),
+                       "psk":    mustsh(t, "wg", "genpsk", ""),
+               },
+       }
+
+       // TODO: orig_message_cost
+
+       script.run(t, "setup namespace", `
+ip netns del $netns0 2>/dev/null || true
+ip netns del $netns1 2>/dev/null || true
+ip netns del $netns2 2>/dev/null || true
+ip netns add $netns0
+ip netns add $netns1
+ip netns add $netns2
+ip0 link set up dev lo
+`)
+
+       wg1cmd := startWG(t, netns(0), "$wg1", wg(1))
+       defer wg1cmd.Process.Kill()
+       script.run(t, "setup $wg1", `ip0 link set $wg1 netns $netns1`)
+
+       wg2cmd := startWG(t, netns(0), "$wg2", wg(2))
+       defer wg2cmd.Process.Kill()
+       script.run(t, "setup $wg2", `ip0 link set $wg2 netns $netns2`)
+
+       script.run(t, "configure", `
+ip1 addr add 192.168.241.1/24 dev $wg1
+ip1 addr add fd00::1/24 dev $wg1
+
+ip2 addr add 192.168.241.2/24 dev $wg2
+ip2 addr add fd00::2/24 dev $wg2
+
+n0 wg set $wg1 \
+       private-key <(echo "$key1") \
+       listen-port 10000 \
+       peer "$pub2" \
+               preshared-key <(echo "$psk") \
+               allowed-ips 192.168.241.2/32,fd00::2/128
+n0 wg set $wg2 \
+       private-key <(echo "$key2") \
+       listen-port 20000 \
+       peer "$pub1" \
+               preshared-key <(echo "$psk") \
+               allowed-ips 192.168.241.1/32,fd00::1/128
+
+ip1 link set up dev $wg1
+ip2 link set up dev $wg2
+
+sleep 1
+
+# Test using IPv4 as outer transport
+#n0 wg set $wg1 peer "$pub2" endpoint 127.0.0.1:20000
+n0 wg set $wg2 peer "$pub1" endpoint 127.0.0.1:10000
+
+n0 wg showconf $wg1
+n0 wg showconf $wg2
+`)
+
+       time.Sleep(1 * time.Second)
+
+       // TODO: counter test
+
+       script.run(t, "ping test", `
+# Ping over IPv4
+n2 ping -c 10 -f -W 1 192.168.241.1
+n1 ping -c 10 -f -W 1 192.168.241.2
+
+# Ping over IPv6
+n2 ping6 -c 10 -f -W 1 fd00::1
+n1 ping6 -c 10 -f -W 1 fd00::2
+`)
+
+}
+
+// TODO
+// TestSticky tests sticky sockets work.
+//
+// We start with this topology:
+//
+// ┌────────────────────────────────────────┐    ┌────────────────────────────────────────┐
+// │           $netns1 namespace            │    │           $netns2 namespace            │
+// │                                        │    │                                        │
+// │  ┌──────┐            ┌─────┐           │    │  ┌─────┐            ┌──────┐           │
+// │  │ $wg1 |────────────│veth1│───────────┼────┼──│veth2│────────────│ $wg2 │           │
+// │  ├──────┴─────────┐  ├─────┴──────────┐│    │  ├─────┴──────────┐ ├──────┴─────────┐ │
+// │  │192.168.241.1/24│  │10.0.0.1/24     ││    │  │10.0.0.2/24     │ │192.168.241.2/24│ │
+// │  │fd00::1/24      │  │fd00:aa::1/96   ││    │  │fd00:aa::2/96   │ │fd00::2/24      │ │
+// │  └────────────────┘  └────────────────┘│    │  └────────────────┘ └────────────────┘ │
+// └────────────────────────────────────────┘    └────────────────────────────────────────┘
+// TODO
+
+// startWG starts wireguard-go by forking the test process and executing
+// TestChild with a magic env variable. We do this instead of starting
+// wireguard directly in the process because we want to run under a
+// linux namespace.
+//
+// We cannot use setns(2) for this, because the syscall is per-thread.
+// The initial tun binding works, but then device.NewDevice creates
+// goroutines which run on other OS threads where setns(2) wasn't applied.
+func startWG(t *testing.T, netns, dispname, name string) *exec.Cmd {
+       t.Helper()
+       cmd := exec.Command("ip", "netns", "exec", netns, os.Args[0], "-test.run=TestChild$")
+       cmd.Env = append([]string{
+               "TEST_WG_CHILD=" + name,
+       }, os.Environ()...)
+       cmd.Stdout = logWriter{logf: func(format string, args ...interface{}) {
+               t.Logf(dispname+": "+format, args...)
+       }}
+       cmd.Stderr = cmd.Stdout
+       if err := cmd.Start(); err != nil {
+               t.Fatal(err)
+       }
+       time.Sleep(1 * time.Second) // eww TODO
+       return cmd
+}
+
+// TestChild is a fake test. See startWG for details.
+func TestChild(t *testing.T) {
+       name := os.Getenv("TEST_WG_CHILD")
+       if name == "" {
+               t.Skip("TestChild is a fake test used to start the tun $TEST_WG_CHILD, skipping")
+       }
+       log.Printf("starting wireguard-go on tun %s", name)
+
+       tun, err := tun.CreateTUN(name, device.DefaultMTU)
+       if err != nil {
+               t.Fatal(err)
+       }
+       d := device.NewDevice(tun, device.NewLogger(device.LogLevelInfo, "")) // TODO: LogLevelDebug option
+
+       fileUAPI, err := ipc.UAPIOpen(name)
+       if err != nil {
+               t.Fatal(err)
+       }
+       uapi, err := ipc.UAPIListen(name, fileUAPI)
+       if err != nil {
+               t.Fatal(err)
+       }
+       for {
+               conn, err := uapi.Accept()
+               if err != nil {
+                       break
+               }
+               go d.IpcHandle(conn)
+       }
+       // TODO: listen for a user signal to know shutdown is clean
+       os.Exit(0)
+}
+
+type scriptScope struct {
+       vars map[string]string
+}
+
+func (s scriptScope) run(t *testing.T, name, script string) {
+       const header = `#!/bin/sh
+set -e
+
+export WG_HIDE_KEYS=never
+
+n0() { ip netns exec $netns0 "$@"; }
+n1() { ip netns exec $netns1 "$@"; }
+n2() { ip netns exec $netns2 "$@"; }
+ip0() { ip -n $netns0 "$@"; }
+ip1() { ip -n $netns1 "$@"; }
+ip2() { ip -n $netns2 "$@"; }
+
+`
+
+       buf := new(bytes.Buffer)
+       buf.WriteString(header)
+       var varNames []string
+       for name := range s.vars {
+               varNames = append(varNames, name)
+       }
+       sort.Strings(varNames)
+       for _, name := range varNames {
+               fmt.Fprintf(buf, "%s=%q\n", name, s.vars[name])
+       }
+       buf.WriteString("set -x\n")
+       buf.WriteString(script)
+
+       sh := exec.Command("/bin/bash")
+       sh.Stdin = buf
+       sh.Stdout = logWriter{logf: t.Logf}
+       sh.Stderr = logWriter{logf: t.Logf}
+       if err := sh.Run(); err != nil {
+               t.Fatalf("%s failed: %v", name, err)
+       }
+}
+
+func checkRootOnLinux(t *testing.T) {
+       t.Helper()
+       if runtime.GOOS != "linux" {
+               t.Skip("SKIPPING test, requires GOOS=linux")
+       }
+       if os.Getuid() != 0 {
+               t.Skip("SKIPPING test, requires root")
+       }
+}
+
+func logger(name string, logf func(format string, args ...interface{})) *device.Logger {
+       w := logWriter{logf: logf}
+       return &device.Logger{
+               Debug: log.New(ioutil.Discard, "DEBUG("+name+"): ", 0),
+               Info:  log.New(w, "INFO("+name+"): ", 0),
+               Error: log.New(w, "ERROR("+name+"): ", 0),
+       }
+}
+
+type logWriter struct {
+       logf func(format string, args ...interface{})
+}
+
+func (lw logWriter) Write(b []byte) (int, error) {
+       lw.logf("%s", b)
+       return len(b), nil
+}