Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
module github.com/tailscale/tscert

go 1.15
go 1.23.0

toolchain go1.24.3

require (
github.com/Microsoft/go-winio v0.6.0
github.com/Microsoft/go-winio v0.6.2
github.com/mitchellh/go-ps v1.0.0
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f
golang.org/x/sys v0.33.0
)
37 changes: 4 additions & 33 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,35 +1,6 @@
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
9 changes: 6 additions & 3 deletions internal/safesocket/basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
"testing"
)

// downgradeSDDL is a no-op test helper on non-Windows systems.
var downgradeSDDL = func() func() { return func() {} }

func TestBasics(t *testing.T) {
// Make the socket in a temp dir rather than the cwd
// so that the test can be run from a mounted filesystem (#2367).
Expand All @@ -19,10 +22,11 @@ func TestBasics(t *testing.T) {
if runtime.GOOS != "windows" {
sock = filepath.Join(dir, "test")
} else {
sock = fmt.Sprintf(`\\.\pipe\tailscale-test`)
sock = `\\.\pipe\tailscale-test`
t.Cleanup(downgradeSDDL())
}

l, port, err := Listen(sock, 0)
l, err := Listen(sock)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -55,7 +59,6 @@ func TestBasics(t *testing.T) {

go func() {
s := DefaultConnectionStrategy(sock)
s.UsePort(port)
c, err := Connect(s)
if err != nil {
errs <- err
Expand Down
131 changes: 123 additions & 8 deletions internal/safesocket/pipe_windows.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package safesocket

//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go pipe_windows.go

import (
"context"
"fmt"
"net"
"runtime"
"syscall"
"time"

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

func connect(s *ConnectionStrategy) (net.Conn, error) {
return winio.DialPipe(s.path, nil)
dl := time.Now().Add(20 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), dl)
defer cancel()
// We use the identification impersonation level so that tailscaled may
// obtain information about our token for access control purposes.
return winio.DialPipeAccessImpLevel(ctx, s.path, windows.GENERIC_READ|windows.GENERIC_WRITE, winio.PipeImpLevelIdentification)
}

func setFlags(network, address string, c syscall.RawConn) error {
Expand All @@ -25,9 +35,10 @@ func setFlags(network, address string, c syscall.RawConn) error {

// windowsSDDL is the Security Descriptor set on the namedpipe.
// It provides read/write access to all users and the local system.
const windowsSDDL = "O:BAG:BAD:PAI(A;OICI;GWGR;;;BU)(A;OICI;GWGR;;;SY)"
// It is a var for testing, do not change this value.
var windowsSDDL = "O:BAG:BAD:PAI(A;OICI;GWGR;;;BU)(A;OICI;GWGR;;;SY)"

func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) {
func listen(path string) (net.Listener, error) {
lc, err := winio.ListenPipe(
path,
&winio.PipeConfig{
Expand All @@ -37,7 +48,111 @@ func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error)
},
)
if err != nil {
return nil, 0, fmt.Errorf("namedpipe.Listen: %w", err)
return nil, fmt.Errorf("namedpipe.Listen: %w", err)
}
return &winIOPipeListener{Listener: lc}, nil
}

// WindowsClientConn is an implementation of net.Conn that permits retrieval of
// the Windows access token associated with the connection's client. The
// embedded net.Conn must be a go-winio PipeConn.
type WindowsClientConn struct {
net.Conn
token windows.Token
}

// winioPipeHandle is fulfilled by the underlying code implementing go-winio's
// PipeConn interface.
type winioPipeHandle interface {
// Fd returns the Windows handle associated with the connection.
Fd() uintptr
}

func resolvePipeHandle(c net.Conn) windows.Handle {
wph, ok := c.(winioPipeHandle)
if !ok {
return 0
}
return windows.Handle(wph.Fd())
}

func (conn *WindowsClientConn) handle() windows.Handle {
return resolvePipeHandle(conn.Conn)
}

// ClientPID returns the pid of conn's client, or else an error.
func (conn *WindowsClientConn) ClientPID() (int, error) {
var pid uint32
if err := getNamedPipeClientProcessId(conn.handle(), &pid); err != nil {
return -1, fmt.Errorf("GetNamedPipeClientProcessId: %w", err)
}
return int(pid), nil
}

// Token returns the Windows access token of the client user.
func (conn *WindowsClientConn) Token() windows.Token {
return conn.token
}

func (conn *WindowsClientConn) Close() error {
if conn.token != 0 {
conn.token.Close()
conn.token = 0
}
return conn.Conn.Close()
}

type winIOPipeListener struct {
net.Listener
}

func (lw *winIOPipeListener) Accept() (net.Conn, error) {
conn, err := lw.Listener.Accept()
if err != nil {
return nil, err
}
return lc, 0, nil

token, err := clientUserAccessToken(conn)
if err != nil {
conn.Close()
return nil, err
}

return &WindowsClientConn{
Conn: conn,
token: token,
}, nil
}

func clientUserAccessToken(c net.Conn) (windows.Token, error) {
h := resolvePipeHandle(c)
if h == 0 {
return 0, fmt.Errorf("not a windows handle: %T", c)
}

// Impersonation touches thread-local state, so we need to lock until the
// client access token has been extracted.
runtime.LockOSThread()
defer runtime.UnlockOSThread()

if err := impersonateNamedPipeClient(h); err != nil {
return 0, err
}
defer func() {
// Revert the current thread's impersonation.
if err := windows.RevertToSelf(); err != nil {
panic(fmt.Errorf("could not revert impersonation: %w", err))
}
}()

// Extract the client's access token from the thread-local state.
var token windows.Token
if err := windows.OpenThreadToken(windows.CurrentThread(), windows.TOKEN_DUPLICATE|windows.TOKEN_QUERY, true, &token); err != nil {
return 0, err
}

return token, nil
}

//sys getNamedPipeClientProcessId(h windows.Handle, clientPid *uint32) (err error) [int32(failretval)==0] = kernel32.GetNamedPipeClientProcessId
//sys impersonateNamedPipeClient(h windows.Handle) (err error) [int32(failretval)==0] = advapi32.ImpersonateNamedPipeClient
34 changes: 34 additions & 0 deletions internal/safesocket/pipe_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package safesocket

import (
"golang.org/x/sys/windows"
)

func init() {
// downgradeSDDL is a test helper that downgrades the windowsSDDL variable if
// the currently running user does not have sufficient priviliges to set the
// SDDL.
downgradeSDDL = func() (cleanup func()) {
// The current default descriptor can not be set by mere mortal users,
// so we need to undo that for executing tests as a regular user.
if !isCurrentProcessElevated() {
var orig string
orig, windowsSDDL = windowsSDDL, ""
return func() { windowsSDDL = orig }
}
return func() {}
}
}

func isCurrentProcessElevated() bool {
token, err := windows.OpenCurrentProcessToken()
if err != nil {
return false
}
defer token.Close()

return token.IsElevated()
}
10 changes: 3 additions & 7 deletions internal/safesocket/safesocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ import (
"time"
)

// WindowsLocalPort is the default localhost TCP port
// used by safesocket on Windows.
const WindowsLocalPort = 41112

type closeable interface {
CloseRead() error
CloseWrite() error
Expand Down Expand Up @@ -90,7 +86,7 @@ type ConnectionStrategy struct {
// It falls back to auto-discovery across sandbox boundaries on macOS.
// TODO: maybe take no arguments, since path is irrelevant on Windows? Discussion in PR 3499.
func DefaultConnectionStrategy(path string) *ConnectionStrategy {
return &ConnectionStrategy{path: path, port: WindowsLocalPort, fallback: true}
return &ConnectionStrategy{path: path, fallback: true}
}

// UsePort modifies s to use port for the TCP port when applicable.
Expand Down Expand Up @@ -127,8 +123,8 @@ func Connect(s *ConnectionStrategy) (net.Conn, error) {
// Listen returns a listener either on Unix socket path (on Unix), or
// the localhost port (on Windows).
// If port is 0, the returned gotPort says which port was selected on Windows.
func Listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) {
return listen(path, port)
func Listen(path string) (_ net.Listener, _ error) {
return listen(path)
}

var (
Expand Down
30 changes: 15 additions & 15 deletions internal/safesocket/unixsocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func connect(s *ConnectionStrategy) (net.Conn, error) {
}

// TODO(apenwarr): handle magic cookie auth
func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
func listen(path string) (ln net.Listener, err error) {
// Unix sockets hang around in the filesystem even after nobody
// is listening on them. (Which is really unfortunate but long-
// entrenched semantics.) Try connecting first; if it works, then
Expand All @@ -60,9 +60,9 @@ func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
if err == nil {
c.Close()
if tailscaledRunningUnderLaunchd() {
return nil, 0, fmt.Errorf("%v: address already in use; tailscaled already running under launchd (to stop, run: $ sudo launchctl stop com.tailscale.tailscaled)", path)
return nil, fmt.Errorf("%v: address already in use; tailscaled already running under launchd (to stop, run: $ sudo launchctl stop com.tailscale.tailscaled)", path)
}
return nil, 0, fmt.Errorf("%v: address already in use", path)
return nil, fmt.Errorf("%v: address already in use", path)
}
_ = os.Remove(path)

Expand All @@ -88,10 +88,10 @@ func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
}
pipe, err := net.Listen("unix", path)
if err != nil {
return nil, 0, err
return nil, err
}
os.Chmod(path, perm)
return pipe, 0, err
return pipe, err
}

func tailscaledRunningUnderLaunchd() bool {
Expand Down Expand Up @@ -119,10 +119,10 @@ func socketPermissionsForOS() os.FileMode {
// little dance to connect a regular user binary to the sandboxed
// network extension is:
//
// * the sandboxed IPNExtension picks a random localhost:0 TCP port
// - the sandboxed IPNExtension picks a random localhost:0 TCP port
// to listen on
// * it also picks a random hex string that acts as an auth token
// * it then creates a file named "sameuserproof-$PORT-$TOKEN" and leaves
// - it also picks a random hex string that acts as an auth token
// - it then creates a file named "sameuserproof-$PORT-$TOKEN" and leaves
// that file descriptor open forever.
//
// Then, we do different things depending on whether the user is
Expand All @@ -134,15 +134,15 @@ func socketPermissionsForOS() os.FileMode {
//
// If we're outside the App Sandbox:
//
// * then we come along here, running as the same UID, but outside
// - then we come along here, running as the same UID, but outside
// of the sandbox, and look for it. We can run lsof on our own processes,
// but other users on the system can't.
// * we parse out the localhost port number and the auth token
// * we connect to TCP localhost:$PORT
// * we send $TOKEN + "\n"
// * server verifies $TOKEN, sends "#IPN\n" if okay.
// * server is now protocol switched
// * we return the net.Conn and the caller speaks the normal protocol
// - we parse out the localhost port number and the auth token
// - we connect to TCP localhost:$PORT
// - we send $TOKEN + "\n"
// - server verifies $TOKEN, sends "#IPN\n" if okay.
// - server is now protocol switched
// - we return the net.Conn and the caller speaks the normal protocol
//
// If we're inside the App Sandbox, then TS_MACOS_CLI_SHARED_DIR has
// been set to our shared directory. We now have to find the most
Expand Down
Loading