arblant
firefox-vpn-client / core / internal / tunapi / socket_unix_test.go
socket_unix_test.go147 lines · 3.5 KB
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
//go:build linux || darwin

package tunapi

import (
	"context"
	"errors"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"testing"
	"time"
)

// useTestSocket serves the socket in a folder of the test's, as this user.
// It is short: a socket's path has a limit (104 bytes on macOS).
func useTestSocket(t *testing.T) {
	t.Helper()
	dir, err := os.MkdirTemp("", "tun")
	if err != nil {
		t.Fatal(err)
	}
	oldPath, oldUID := socketPath, serverUID
	socketPath, serverUID = filepath.Join(dir, "s"), os.Getuid()
	t.Cleanup(func() {
		socketPath, serverUID = oldPath, oldUID
		os.RemoveAll(dir)
	})
}

// accepted dials the listener and returns the connection it accepted.
func accepted(t *testing.T, ln net.Listener) (server, client net.Conn) {
	t.Helper()
	got := make(chan net.Conn, 1)
	go func() {
		c, err := ln.Accept()
		if err != nil {
			t.Error(err)
		}
		got <- c
	}()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	client, err := Dial(ctx)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { client.Close() })
	server = <-got
	if server == nil {
		t.FailNow()
	}
	t.Cleanup(func() { server.Close() })
	return server, client
}

func TestTheSocketTellsWhoConnected(t *testing.T) {
	useTestSocket(t)
	ln, err := Listen([]string{strconv.Itoa(os.Getuid())})
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()
	if st, err := os.Stat(socketPath); err != nil || st.Mode().Perm() != 0o666 {
		t.Fatalf("the socket's mode is %v (%v), want every user's", st.Mode(), err)
	}
	c, _ := accepted(t, ln)
	if !Allowed(c) {
		t.Fatal("the user who installed it is not allowed")
	}
	pid, err := ClientProcess(c)
	if err != nil || pid != uint32(os.Getpid()) {
		t.Fatalf("client process %d (%v), want %d", pid, err, os.Getpid())
	}
	path, err := ProcessPath(pid)
	self, _ := os.Executable()
	if err != nil || filepath.Base(path) != filepath.Base(self) {
		t.Fatalf("process path %q (%v), want %q", path, err, self)
	}
}

func TestAUserWhoDidNotInstallItIsNotAllowed(t *testing.T) {
	if os.Getuid() == 0 {
		t.Skip("root is always allowed")
	}
	useTestSocket(t)
	ln, err := Listen([]string{strconv.Itoa(os.Getuid() + 1)})
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()
	if c, _ := accepted(t, ln); Allowed(c) {
		t.Fatal("another user was allowed")
	}
	if _, err := Listen([]string{"someone"}); err == nil {
		t.Fatal("a bad user id was taken")
	}
}

// Only root may serve the socket: a program of another user standing in
// for the service is refused.
func TestASocketServedByAnotherUserIsRefused(t *testing.T) {
	useTestSocket(t)
	ln, err := Listen(nil)
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()
	go func() {
		if c, err := ln.Accept(); err == nil {
			defer c.Close()
			time.Sleep(time.Second)
		}
	}()
	serverUID = os.Getuid() + 1
	if c, err := Dial(context.Background()); err == nil {
		c.Close()
		t.Fatal("a socket of another user was trusted")
	}
}

func TestNoServiceIsErrNoService(t *testing.T) {
	useTestSocket(t)
	if _, err := Dial(context.Background()); !errors.Is(err, ErrNoService) {
		t.Fatalf("no socket: %v", err)
	}
	// A socket left by a service that ended.
	ln, err := Listen(nil)
	if err != nil {
		t.Fatal(err)
	}
	ln.(*listener).Listener.(*net.UnixListener).SetUnlinkOnClose(false)
	ln.Close()
	if _, err := os.Stat(socketPath); err != nil {
		t.Fatalf("the socket file is gone: %v", err)
	}
	if _, err := Dial(context.Background()); !errors.Is(err, ErrNoService) {
		t.Fatalf("a dead socket: %v", err)
	}
	// The next service replaces it.
	ln, err = Listen(nil)
	if err != nil {
		t.Fatalf("a left socket was not replaced: %v", err)
	}
	ln.Close()
}