tstest/integration: add tests for tun mode (requiring root)

Updates #7894

Change-Id: Iff0b07b21ae28c712dd665b12918fa28d6f601d0
Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/9803/head
Brad Fitzpatrick 1 year ago committed by Brad Fitzpatrick
parent a6270826a3
commit c363b9055d

@ -90,6 +90,8 @@ jobs:
sudo apt-get -y install qemu-user sudo apt-get -y install qemu-user
- name: build test wrapper - name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: integration tests as root
run: PATH=$PWD/tool:$PATH /tmp/testwrapper --sudo ./tstest/integration/ ${{matrix.buildflags}}
- name: test all - name: test all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
env: env:

@ -73,11 +73,15 @@ var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// It calls close(ch) when it's done. // It calls close(ch) when it's done.
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) error { func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) error {
defer close(ch) defer close(ch)
args := []string{"test", "-json", pt.Pattern} args := []string{"test", "--json"}
if *flagSudo {
args = append(args, "--exec", "sudo -E")
}
args = append(args, pt.Pattern)
args = append(args, otherArgs...) args = append(args, otherArgs...)
if len(pt.Tests) > 0 { if len(pt.Tests) > 0 {
runArg := strings.Join(pt.Tests, "|") runArg := strings.Join(pt.Tests, "|")
args = append(args, "-run", runArg) args = append(args, "--run", runArg)
} }
if debug { if debug {
fmt.Println("running", strings.Join(args, " ")) fmt.Println("running", strings.Join(args, " "))
@ -177,6 +181,11 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
return nil return nil
} }
var (
flagVerbose = flag.Bool("v", false, "verbose")
flagSudo = flag.Bool("sudo", false, "run tests with -exec=sudo")
)
func main() { func main() {
ctx := context.Background() ctx := context.Background()
@ -187,7 +196,6 @@ func main() {
// We run `go test -json` which returns the same information as `go test -v`, // We run `go test -json` which returns the same information as `go test -v`,
// but in a machine-readable format. So this flag is only for testwrapper's // but in a machine-readable format. So this flag is only for testwrapper's
// output. // output.
v := flag.Bool("v", false, "verbose")
flag.Usage = func() { flag.Usage = func() {
fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]") fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
@ -285,7 +293,7 @@ func main() {
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt) printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt)
continue continue
} }
if *v || tr.outcome == "fail" { if *flagVerbose || tr.outcome == "fail" {
io.Copy(os.Stdout, &tr.logs) io.Copy(os.Stdout, &tr.logs)
} }
if tr.outcome != "fail" { if tr.outcome != "fail" {

@ -142,6 +142,16 @@ func findGo() (string, error) {
// 2. Look for a go binary in runtime.GOROOT()/bin if runtime.GOROOT() is non-empty. // 2. Look for a go binary in runtime.GOROOT()/bin if runtime.GOROOT() is non-empty.
// 3. Look for a go binary in $PATH. // 3. Look for a go binary in $PATH.
// For tests we want to run as root on GitHub actions, we run with -exec=sudo,
// but that results in this test running with a different PATH and picking the
// wrong Go. So hard code the GitHub Actions case.
if os.Getuid() == 0 && os.Getenv("GITHUB_ACTIONS") == "true" {
const sudoGithubGo = "/home/runner/.cache/tailscale-go/bin/go"
if _, err := os.Stat(sudoGithubGo); err == nil {
return sudoGithubGo, nil
}
}
paths := strings.FieldsFunc(os.Getenv("PATH"), func(r rune) bool { return os.IsPathSeparator(uint8(r)) }) paths := strings.FieldsFunc(os.Getenv("PATH"), func(r rune) bool { return os.IsPathSeparator(uint8(r)) })
if len(paths) > 0 { if len(paths) > 0 {
candidate := filepath.Join(paths[0], "go"+exe()) candidate := filepath.Join(paths[0], "go"+exe())

@ -68,6 +68,27 @@ func TestMain(m *testing.M) {
os.Exit(0) os.Exit(0)
} }
// Tests that tailscaled starts up in TUN mode, and also without data races:
// https://github.com/tailscale/tailscale/issues/7894
func TestTUNMode(t *testing.T) {
if os.Getuid() != 0 {
t.Skip("skipping when not root")
}
t.Parallel()
env := newTestEnv(t)
env.tunMode = true
n1 := newTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
func TestOneNodeUpNoAuth(t *testing.T) { func TestOneNodeUpNoAuth(t *testing.T) {
t.Parallel() t.Parallel()
env := newTestEnv(t) env := newTestEnv(t)
@ -808,9 +829,10 @@ func TestLogoutRemovesAllPeers(t *testing.T) {
// testEnv contains the test environment (set of servers) used by one // testEnv contains the test environment (set of servers) used by one
// or more nodes. // or more nodes.
type testEnv struct { type testEnv struct {
t testing.TB t testing.TB
cli string tunMode bool
daemon string cli string
daemon string
LogCatcher *LogCatcher LogCatcher *LogCatcher
LogCatcherServer *httptest.Server LogCatcherServer *httptest.Server
@ -899,12 +921,25 @@ func newTestNode(t *testing.T, env *testEnv) *testNode {
sockFile = filepath.Join(os.TempDir(), rands.HexString(8)+".sock") sockFile = filepath.Join(os.TempDir(), rands.HexString(8)+".sock")
t.Cleanup(func() { os.Remove(sockFile) }) t.Cleanup(func() { os.Remove(sockFile) })
} }
return &testNode{ n := &testNode{
env: env, env: env,
dir: dir, dir: dir,
sockFile: sockFile, sockFile: sockFile,
stateFile: filepath.Join(dir, "tailscale.state"), stateFile: filepath.Join(dir, "tailscale.state"),
} }
// Look for a data race. Once we see the start marker, start logging the rest.
var sawRace bool
n.addLogLineHook(func(line []byte) {
if mem.Contains(mem.B(line), mem.S("WARNING: DATA RACE")) {
sawRace = true
}
if sawRace {
t.Logf("%s", line)
}
})
return n
} }
func (n *testNode) diskPrefs() *ipn.Prefs { func (n *testNode) diskPrefs() *ipn.Prefs {
@ -963,7 +998,7 @@ func (n *testNode) socks5AddrChan() <-chan string {
if i == -1 { if i == -1 {
return return
} }
addr := string(line)[i+len(sub):] addr := strings.TrimSpace(string(line)[i+len(sub):])
select { select {
case ch <- addr: case ch <- addr:
default: default:
@ -1010,11 +1045,10 @@ func (op *nodeOutputParser) parseLines() {
} }
line := buf[:nl+1] line := buf[:nl+1]
buf = buf[nl+1:] buf = buf[nl+1:]
lineTrim := bytes.TrimSpace(line)
n.mu.Lock() n.mu.Lock()
for _, f := range n.onLogLine { for _, f := range n.onLogLine {
f(lineTrim) f(line)
} }
n.mu.Unlock() n.mu.Unlock()
} }
@ -1048,8 +1082,8 @@ func (n *testNode) StartDaemon() *Daemon {
func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon { func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
t := n.env.t t := n.env.t
cmd := exec.Command(n.env.daemon, cmd := exec.Command(n.env.daemon)
"--tun=userspace-networking", cmd.Args = append(cmd.Args,
"--state="+n.stateFile, "--state="+n.stateFile,
"--socket="+n.sockFile, "--socket="+n.sockFile,
"--socks5-server=localhost:0", "--socks5-server=localhost:0",
@ -1057,6 +1091,11 @@ func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
if *verboseTailscaled { if *verboseTailscaled {
cmd.Args = append(cmd.Args, "-verbose=2") cmd.Args = append(cmd.Args, "-verbose=2")
} }
if !n.env.tunMode {
cmd.Args = append(cmd.Args,
"--tun=userspace-networking",
)
}
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
"TS_DEBUG_PERMIT_HTTP_C2N=1", "TS_DEBUG_PERMIT_HTTP_C2N=1",
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL, "TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
@ -1067,10 +1106,7 @@ func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
"TS_NETCHECK_GENERATE_204_URL="+n.env.ControlServer.URL+"/generate_204", "TS_NETCHECK_GENERATE_204_URL="+n.env.ControlServer.URL+"/generate_204",
) )
if version.IsRace() { if version.IsRace() {
const knownBroken = true // TODO(bradfitz,maisem): enable this once we fix all the races :( cmd.Env = append(cmd.Env, "GORACE=halt_on_error=1")
if !knownBroken {
cmd.Env = append(cmd.Env, "GORACE=halt_on_error=1")
}
} }
cmd.Stderr = &nodeOutputParser{n: n} cmd.Stderr = &nodeOutputParser{n: n}
if *verboseTailscaled { if *verboseTailscaled {
@ -1143,11 +1179,10 @@ func (n *testNode) AwaitListening() {
s := safesocket.DefaultConnectionStrategy(n.sockFile) s := safesocket.DefaultConnectionStrategy(n.sockFile)
if err := tstest.WaitFor(20*time.Second, func() (err error) { if err := tstest.WaitFor(20*time.Second, func() (err error) {
c, err := safesocket.Connect(s) c, err := safesocket.Connect(s)
if err != nil { if err == nil {
return err c.Close()
} }
c.Close() return err
return nil
}); err != nil { }); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1241,7 +1276,8 @@ func (n *testNode) AwaitNeedsLogin() {
// Tailscale returns a command that runs the tailscale CLI with the provided arguments. // Tailscale returns a command that runs the tailscale CLI with the provided arguments.
// It does not start the process. // It does not start the process.
func (n *testNode) Tailscale(arg ...string) *exec.Cmd { func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
cmd := exec.Command(n.env.cli, "--socket="+n.sockFile) cmd := exec.Command(n.env.cli)
cmd.Args = append(cmd.Args, "--socket="+n.sockFile)
cmd.Args = append(cmd.Args, arg...) cmd.Args = append(cmd.Args, arg...)
cmd.Dir = n.dir cmd.Dir = n.dir
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),

@ -484,9 +484,9 @@ func (p *pingResultAndCallback) reply() bool {
return p != nil && p.taken.CompareAndSwap(false, true) return p != nil && p.taken.CompareAndSwap(false, true)
} }
// discoPing starts a disc-level ping for the "tailscale ping" command (or other // discoPing starts a disco-level ping for the "tailscale ping" command (or other
// callers, such as c2n). res is value to call cb with, already partially // callers, such as c2n). res is value to call cb with, already partially
// filled. cb must be called at most once. Once called, ownership of res passes to db. // filled. cb must be called at most once. Once called, ownership of res passes to cb.
func (de *endpoint) discoPing(res *ipnstate.PingResult, size int, cb func(*ipnstate.PingResult)) { func (de *endpoint) discoPing(res *ipnstate.PingResult, size int, cb func(*ipnstate.PingResult)) {
de.mu.Lock() de.mu.Lock()
defer de.mu.Unlock() defer de.mu.Unlock()

Loading…
Cancel
Save