arblant
firefox-vpn-client / core / internal / tunapi / pipe_windows_test.go
pipe_windows_test.go154 lines · 4.0 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 148 149 150 151 152 153 154
package tunapi

import (
	"context"
	"errors"
	"fmt"
	"net"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/Microsoft/go-winio"
	"golang.org/x/sys/windows"
)

// testPipe gives the test a pipe name of its own.
func testPipe(t *testing.T) {
	t.Helper()
	old := pipeName
	pipeName = fmt.Sprintf(`\\.\pipe\firefox-vpn-tun-test-%d-%d`, os.Getpid(), time.Now().UnixNano())
	t.Cleanup(func() { pipeName = old })
}

func currentUser(t *testing.T) string {
	t.Helper()
	user, err := windows.GetCurrentProcessToken().GetTokenUser()
	if err != nil {
		t.Fatal(err)
	}
	return user.User.Sid.String()
}

// listenAsUser serves the test pipe as this test's user, its owner.
func listenAsUser(t *testing.T) net.Listener {
	t.Helper()
	user := currentUser(t)
	sddl, err := securityDescriptor(user, []string{user}, nil)
	if err != nil {
		t.Fatal(err)
	}
	ln, err := listenSDDL(sddl)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ln.Close() })
	return ln
}

// A pipe made under the name by another program (owned by a user, not the
// system) is refused.
func TestAPipeOwnedByAUserIsRefused(t *testing.T) {
	testPipe(t)
	ln := listenAsUser(t)
	go func() {
		if c, err := ln.Accept(); err == nil {
			c.Close()
		}
	}()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	c, err := Dial(ctx)
	if err == nil {
		c.Close()
		t.Fatal("a user's pipe was accepted as the service")
	}
	if !strings.Contains(err.Error(), "not to the system") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestNoPipeMeansNoService(t *testing.T) {
	testPipe(t)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if _, err := Dial(ctx); !errors.Is(err, ErrNoService) {
		t.Fatalf("got %v, want ErrNoService", err)
	}
}

// A client with the access users get talks to the service, and the service
// learns which process it is.
func TestTheServiceSeesWhichProcessConnected(t *testing.T) {
	testPipe(t)
	ln := listenAsUser(t)
	served := make(chan error, 1)
	go func() {
		c, err := ln.Accept()
		if err != nil {
			served <- err
			return
		}
		defer c.Close()
		pid, err := ClientProcess(c)
		if err != nil || pid != uint32(os.Getpid()) {
			served <- fmt.Errorf("client process %d, %v; want %d", pid, err, os.Getpid())
			return
		}
		path, err := ProcessPath(pid)
		if self, _ := os.Executable(); err != nil || !strings.EqualFold(path, self) {
			served <- fmt.Errorf("client path %q, %v; want %q", path, err, self)
			return
		}
		conn := NewConn(c)
		m, err := conn.Receive()
		if err == nil {
			err = conn.Send(Message{Type: TypeHello, Version: m.Version})
		}
		served <- err
	}()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	raw, err := winio.DialPipeAccess(ctx, pipeName, clientAccess)
	if err != nil {
		t.Fatal(err)
	}
	conn := NewConn(raw)
	defer conn.Close()
	if err := conn.Send(Hello("9.9.9")); err != nil {
		t.Fatal(err)
	}
	reply, err := conn.Receive()
	if err != nil || reply.Version != "9.9.9" {
		t.Fatalf("reply %+v, %v", reply, err)
	}
	if err := <-served; err != nil {
		t.Fatal(err)
	}
}

// Users get exactly what a client asks for: never FILE_APPEND_DATA, which
// on a pipe is the right to create another server end and take clients.
func TestUsersMayNotServeThePipe(t *testing.T) {
	if clientAccess&windows.FILE_APPEND_DATA != 0 || userAccess&windows.FILE_APPEND_DATA != 0 {
		t.Fatal("the user access includes FILE_APPEND_DATA")
	}
	// GENERIC_READ as the pipe file system maps it, and FILE_WRITE_DATA.
	if userAccess != 0x12008b {
		t.Fatalf("user access %#x", userAccess)
	}
	sddl, err := securityDescriptor("SY", nil, []string{"S-1-5-21-1-2-3-1001"})
	if err != nil {
		t.Fatal(err)
	}
	if want := "O:SYG:SYD:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x12008b;;;S-1-5-21-1-2-3-1001)"; sddl != want {
		t.Fatalf("security descriptor %s, want %s", sddl, want)
	}
	if _, err := windows.SecurityDescriptorFromString(sddl); err != nil {
		t.Fatalf("the security descriptor does not parse: %v", err)
	}
	if _, err := securityDescriptor("SY", nil, []string{"not a SID"}); err == nil {
		t.Fatal("a bad SID was accepted")
	}
}