From c9159a5b864bf4e4f743a55d2d444668603baeb8 Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Sun, 3 May 2020 23:07:19 -0400 Subject: [WIP] device: run testns.sh from a Go test harness 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 --- device/ns_test.go | 292 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 device/ns_test.go diff --git a/device/ns_test.go b/device/ns_test.go new file mode 100644 index 0000000..eb10a87 --- /dev/null +++ b/device/ns_test.go @@ -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 +} -- cgit v1.2.3-59-g8ed1b