diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 45403489e..ab5c4e405 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -2,6 +2,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus filippo.io/edwards25519/field from filippo.io/edwards25519 + W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket + W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio + W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+ W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+ W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 9fea75f37..9ee2dcc50 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -17,6 +17,7 @@ "log" "net" "net/http" + "net/http/httputil" "net/netip" "net/url" "os" @@ -27,6 +28,7 @@ "github.com/peterbourgon/ff/v3/ffcli" "golang.org/x/net/http/httpproxy" + "tailscale.com/client/tailscale/apitype" "tailscale.com/control/controlhttp" "tailscale.com/hostinfo" "tailscale.com/ipn" @@ -261,13 +263,42 @@ func runLocalCreds(ctx context.Context, args []string) error { return nil } if runtime.GOOS == "windows" { - printf("curl http://localhost:%v/localapi/v0/status\n", safesocket.WindowsLocalPort) + runLocalAPIProxy() return nil } printf("curl --unix-socket %s http://local-tailscaled.sock/localapi/v0/status\n", paths.DefaultTailscaledSocket()) return nil } +type localClientRoundTripper struct{} + +func (localClientRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return localClient.DoLocalRequest(req) +} + +func runLocalAPIProxy() { + rp := httputil.NewSingleHostReverseProxy(&url.URL{ + Scheme: "http", + Host: apitype.LocalAPIHost, + Path: "/", + }) + dir := rp.Director + rp.Director = func(req *http.Request) { + dir(req) + req.Host = "" + req.RequestURI = "" + } + rp.Transport = localClientRoundTripper{} + lc, err := net.Listen("tcp", "localhost:0") + if err != nil { + log.Fatal(err) + } + fmt.Printf("Serving LocalAPI proxy on http://%s\n", lc.Addr()) + fmt.Printf("curl.exe http://%v/localapi/v0/status\n", lc.Addr()) + fmt.Printf("Ctrl+C to stop") + http.Serve(lc, rp) +} + var prefsArgs struct { pretty bool } diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index ff565ccc9..286a9238c 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -2,6 +2,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus filippo.io/edwards25519/field from filippo.io/edwards25519 + W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket + W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio + W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+ W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+ W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy @@ -220,7 +223,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep net/http from expvar+ net/http/cgi from tailscale.com/cmd/tailscale/cli net/http/httptrace from github.com/tcnksm/go-httpstat+ - net/http/internal from net/http + net/http/httputil from tailscale.com/cmd/tailscale/cli + net/http/internal from net/http+ net/netip from net+ net/textproto from golang.org/x/net/http/httpguts+ net/url from crypto/x509+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 33d82db5b..051bdc0bd 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -2,6 +2,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus filippo.io/edwards25519/field from filippo.io/edwards25519 + W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket + W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio + W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+ W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+ W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy @@ -200,7 +203,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal tailscale.com/hostinfo from tailscale.com/control/controlclient+ tailscale.com/ipn from tailscale.com/ipn/ipnlocal+ - tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnserver+ + 💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnserver+ tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+ tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+ @@ -291,7 +294,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/mak from tailscale.com/control/controlclient+ tailscale.com/util/multierr from tailscale.com/control/controlclient+ tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+ - tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth + W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth tailscale.com/util/racebuild from tailscale.com/logpolicy tailscale.com/util/set from tailscale.com/health+ tailscale.com/util/singleflight from tailscale.com/control/controlclient+ diff --git a/go.mod b/go.mod index 74c6f08ce..85da17cfb 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( filippo.io/mkcert v1.4.3 + github.com/Microsoft/go-winio v0.6.0 github.com/akutz/memconn v0.1.0 github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 github.com/andybalholm/brotli v1.0.3 @@ -71,7 +72,7 @@ require ( golang.org/x/term v0.1.0 golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 golang.org/x/tools v0.1.12 - golang.zx2c4.com/wireguard v0.0.0-20220904105730-b51010ba13f0 + golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c golang.zx2c4.com/wireguard/windows v0.5.3 gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5 honnef.co/go/tools v0.4.0-0.dev.0.20220517111757-f4a2f64ce238 @@ -92,7 +93,6 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect - github.com/Microsoft/go-winio v0.5.2 // indirect github.com/OpenPeeDeeP/depguard v1.0.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect diff --git a/go.sum b/go.sum index ee4121ec0..85fda39b9 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jB github.com/Microsoft/go-winio v0.4.15/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us= github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= @@ -1656,8 +1656,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20220904105730-b51010ba13f0 h1:5ZkdpbduT/g+9OtbSDvbF3KvfQG45CtH/ppO8FUmvCQ= -golang.zx2c4.com/wireguard v0.0.0-20220904105730-b51010ba13f0/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg= +golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c h1:Okh6a1xpnJslG9Mn84pId1Mn+Q8cvpo4HCeeFWHo0cA= +golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= diff --git a/ipn/ipnauth/ipnauth.go b/ipn/ipnauth/ipnauth.go index 84de43ce9..136ab3c2b 100644 --- a/ipn/ipnauth/ipnauth.go +++ b/ipn/ipnauth/ipnauth.go @@ -6,7 +6,6 @@ package ipnauth import ( - "errors" "fmt" "net" "net/netip" @@ -23,7 +22,6 @@ "tailscale.com/types/logger" "tailscale.com/util/clientmetric" "tailscale.com/util/groupmember" - "tailscale.com/util/pidowner" "tailscale.com/util/winutil" "tailscale.com/version/distro" ) @@ -73,55 +71,6 @@ func (ci *ConnIdentity) Pid() int { return ci.pid } func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock } func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds } -// GetConnIdentity returns the localhost TCP connection's identity information -// (pid, userid, user). If it's not Windows (for now), it returns a nil error -// and a ConnIdentity with NotWindows set true. It's only an error if we expected -// to be able to map it and couldn't. -func GetConnIdentity(logf logger.Logf, c net.Conn) (ci *ConnIdentity, err error) { - ci = &ConnIdentity{conn: c} - if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes - ci.notWindows = true - _, ci.isUnixSock = c.(*net.UnixConn) - ci.creds, _ = peercred.Get(c) - return ci, nil - } - la, err := netip.ParseAddrPort(c.LocalAddr().String()) - if err != nil { - return ci, fmt.Errorf("parsing local address: %w", err) - } - ra, err := netip.ParseAddrPort(c.RemoteAddr().String()) - if err != nil { - return ci, fmt.Errorf("parsing local remote: %w", err) - } - if !la.Addr().IsLoopback() || !ra.Addr().IsLoopback() { - return ci, errors.New("non-loopback connection") - } - tab, err := netstat.Get() - if err != nil { - return ci, fmt.Errorf("failed to get local connection table: %w", err) - } - pid := peerPid(tab.Entries, la, ra) - if pid == 0 { - return ci, errors.New("no local process found matching localhost connection") - } - ci.pid = pid - uid, err := pidowner.OwnerOfPID(pid) - if err != nil { - var hint string - if runtime.GOOS == "windows" { - hint = " (WSL?)" - } - return ci, fmt.Errorf("failed to map connection's pid to a user%s: %w", hint, err) - } - ci.userID = ipn.WindowsUserID(uid) - u, err := LookupUserFromID(logf, uid) - if err != nil { - return ci, fmt.Errorf("failed to look up user from userid: %w", err) - } - ci.user = u - return ci, nil -} - var metricIssue869Workaround = clientmetric.NewCounter("issue_869_workaround") // LookupUserFromID is a wrapper around os/user.LookupId that works around some diff --git a/ipn/ipnauth/ipnauth_notwindows.go b/ipn/ipnauth/ipnauth_notwindows.go new file mode 100644 index 000000000..f0ef9c58c --- /dev/null +++ b/ipn/ipnauth/ipnauth_notwindows.go @@ -0,0 +1,24 @@ +// Copyright (c) 2022 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. + +//go:build !windows + +package ipnauth + +import ( + "net" + + "inet.af/peercred" + "tailscale.com/types/logger" +) + +// GetConnIdentity extracts the identity information from the connection +// based on the user who owns the other end of the connection. +// and couldn't. The returned connIdentity has NotWindows set to true. +func GetConnIdentity(_ logger.Logf, c net.Conn) (ci *ConnIdentity, err error) { + ci = &ConnIdentity{conn: c, notWindows: true} + _, ci.isUnixSock = c.(*net.UnixConn) + ci.creds, _ = peercred.Get(c) + return ci, nil +} diff --git a/ipn/ipnauth/ipnauth_windows.go b/ipn/ipnauth/ipnauth_windows.go new file mode 100644 index 000000000..6349b399a --- /dev/null +++ b/ipn/ipnauth/ipnauth_windows.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 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. + +package ipnauth + +import ( + "fmt" + "net" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + "tailscale.com/ipn" + "tailscale.com/types/logger" + "tailscale.com/util/pidowner" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetNamedPipeClientProcessId = kernel32.NewProc("GetNamedPipeClientProcessId") +) + +func getNamedPipeClientProcessId(h windows.Handle) (pid uint32, err error) { + r1, _, err := procGetNamedPipeClientProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(&pid))) + if r1 > 0 { + return pid, nil + } + return 0, err +} + +// GetConnIdentity extracts the identity information from the connection +// based on the user who owns the other end of the connection. +// If c is not backed by a named pipe, an error is returned. +func GetConnIdentity(logf logger.Logf, c net.Conn) (ci *ConnIdentity, err error) { + ci = &ConnIdentity{conn: c} + h, ok := c.(interface { + Fd() uintptr + }) + if !ok { + return ci, fmt.Errorf("not a windows handle: %T", c) + } + pid, err := getNamedPipeClientProcessId(windows.Handle(h.Fd())) + if err != nil { + return ci, fmt.Errorf("getNamedPipeClientProcessId: %v", err) + } + ci.pid = int(pid) + uid, err := pidowner.OwnerOfPID(ci.pid) + if err != nil { + return ci, fmt.Errorf("failed to map connection's pid to a user (WSL?): %w", err) + } + ci.userID = ipn.WindowsUserID(uid) + u, err := LookupUserFromID(logf, uid) + if err != nil { + return ci, fmt.Errorf("failed to look up user from userid: %w", err) + } + ci.user = u + return ci, nil +} diff --git a/paths/paths.go b/paths/paths.go index dfb584f21..e6b461856 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -23,7 +23,7 @@ // or the empty string if there's no reasonable default. func DefaultTailscaledSocket() string { if runtime.GOOS == "windows" { - return "" + return `\\.\pipe\ProtectedPrefix\Administrators\Tailscale\tailscaled` } if runtime.GOOS == "darwin" { return "/var/run/tailscaled.socket" diff --git a/safesocket/basic_test.go b/safesocket/basic_test.go index 6d0a09a7f..bcdd2a397 100644 --- a/safesocket/basic_test.go +++ b/safesocket/basic_test.go @@ -7,6 +7,7 @@ import ( "fmt" "path/filepath" + "runtime" "testing" ) @@ -14,7 +15,12 @@ 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). dir := t.TempDir() - sock := filepath.Join(dir, "test") + var sock string + if runtime.GOOS != "windows" { + sock = filepath.Join(dir, "test") + } else { + sock = fmt.Sprintf(`\\.\pipe\tailscale-test`) + } l, port, err := Listen(sock, 0) if err != nil { diff --git a/safesocket/pipe_windows.go b/safesocket/pipe_windows.go index fb27e9413..2851a0b2f 100644 --- a/safesocket/pipe_windows.go +++ b/safesocket/pipe_windows.go @@ -5,18 +5,15 @@ package safesocket import ( - "context" "fmt" "net" "syscall" + + "github.com/Microsoft/go-winio" ) func connect(s *ConnectionStrategy) (net.Conn, error) { - pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", s.port)) - if err != nil { - return nil, err - } - return pipe, err + return winio.DialPipe(s.path, nil) } func setFlags(network, address string, c syscall.RawConn) error { @@ -26,20 +23,21 @@ func setFlags(network, address string, c syscall.RawConn) error { }) } -// TODO(apenwarr): use named pipes instead of sockets? -// -// I tried to use winio.ListenPipe() here, but that code is a disaster, -// built on top of an API that's a disaster. So for now we'll hack it by -// just always using a TCP session on a fixed port on localhost. As a -// result, on Windows we ignore the vendor and name strings. -// NOTE(bradfitz): Jason did a new pipe package: https://go-review.googlesource.com/c/sys/+/299009 +// 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)" + func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) { - lc := net.ListenConfig{ - Control: setFlags, - } - pipe, err := lc.Listen(context.Background(), "tcp", fmt.Sprintf("127.0.0.1:%d", port)) + lc, err := winio.ListenPipe( + path, + &winio.PipeConfig{ + SecurityDescriptor: windowsSDDL, + InputBufferSize: 256 * 1024, + OutputBufferSize: 256 * 1024, + }, + ) if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("namedpipe.Listen: %w", err) } - return pipe, uint16(pipe.Addr().(*net.TCPAddr).Port), err + return lc, 0, nil }