client/tailscale: move/copy all package funcs to new LocalClient type

Remove all global variables, and clean up tsnet and cmd/tailscale's usage.

This is in prep for using this package for the web API too (it has the
best package name).

RELNOTE=tailscale.com/client/tailscale package refactored w/ LocalClient type

Change-Id: Iba9f162fff0c520a09d1d4bd8862f5c5acc9d7cd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/4580/head
Brad Fitzpatrick 2 years ago committed by Brad Fitzpatrick
parent 373176ea54
commit 87ba528ae0

@ -38,24 +38,59 @@ import (
"tailscale.com/tailcfg"
)
var (
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
TailscaledSocket = paths.DefaultTailscaledSocket()
// defaultLocalClient is the default LocalClient when using the legacy
// package-level functions.
var defaultLocalClient LocalClient
// LocalClient is a client to Tailscale's "local API", communicating with the
// Tailscale daemon on the local machine. Its API is not necessarily stable and
// subject to changes between releases. Some API calls have stricter
// compatibility guarantees, once they've been widely adopted. See method docs
// for details.
//
// Its zero value is valid to use.
//
// Any exported fields should be set before using methods on the type
// and not changed thereafter.
type LocalClient struct {
// Dial optionally specifies an alternate func that connects to the local
// machine's tailscaled or equivalent. If nil, a default is used.
Dial func(ctx context.Context, network, addr string) (net.Conn, error)
// Socket specifies an alternate path to the local Tailscale socket.
// If empty, a platform-specific default is used.
Socket string
// UseSocketOnly, if true, tries to only connect to tailscaled via the
// Unix socket and not via fallback mechanisms as done on macOS when
// connecting to the GUI client variants.
UseSocketOnly bool
// tsClient does HTTP requests to the local Tailscale daemon.
// It's lazily initialized on first use.
tsClient *http.Client
tsClientOnce sync.Once
}
// TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket.
TailscaledSocketSetExplicitly bool
func (lc *LocalClient) socket() string {
if lc.Socket != "" {
return lc.Socket
}
return paths.DefaultTailscaledSocket()
}
// TailscaledDialer is the DialContext func that connects to the local machine's
// tailscaled or equivalent.
TailscaledDialer = defaultDialer
)
func (lc *LocalClient) dialer() func(ctx context.Context, network, addr string) (net.Conn, error) {
if lc.Dial != nil {
return lc.Dial
}
return lc.defaultDialer
}
func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) {
func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) {
if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr)
}
// TODO: make this part of a safesocket.ConnectionStrategy
if !TailscaledSocketSetExplicitly {
if !lc.UseSocketOnly {
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
// a TCP server on a random port, find the random port. For HTTP connections,
// we don't send the token. It gets added in an HTTP Basic-Auth header.
@ -64,21 +99,13 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
}
s := safesocket.DefaultConnectionStrategy(TailscaledSocket)
s := safesocket.DefaultConnectionStrategy(lc.socket())
// The user provided a non-default tailscaled socket address.
// Connect only to exactly what they provided.
s.UseFallback(false)
return safesocket.Connect(s)
}
var (
// tsClient does HTTP requests to the local Tailscale daemon.
// We lazily initialize the client in case the caller wants to
// override TailscaledDialer.
tsClient *http.Client
tsClientOnce sync.Once
)
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
//
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
@ -88,22 +115,22 @@ var (
// authenticating to the local Tailscale daemon vary by platform.
//
// DoLocalRequest may mutate the request to add Authorization headers.
func DoLocalRequest(req *http.Request) (*http.Response, error) {
tsClientOnce.Do(func() {
tsClient = &http.Client{
func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error) {
lc.tsClientOnce.Do(func() {
lc.tsClient = &http.Client{
Transport: &http.Transport{
DialContext: TailscaledDialer,
DialContext: lc.dialer(),
},
}
})
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
req.SetBasicAuth("", token)
}
return tsClient.Do(req)
return lc.tsClient.Do(req)
}
func doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := DoLocalRequest(req)
func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := lc.DoLocalRequest(req)
if err == nil {
if server := res.Header.Get("Tailscale-Version"); server != "" && server != ipn.IPCVersion() && onVersionMismatch != nil {
onVersionMismatch(ipn.IPCVersion(), server)
@ -169,12 +196,12 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
onVersionMismatch = f
}
func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
if err != nil {
return nil, err
}
res, err := doLocalRequestNiceError(req)
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, err
}
@ -190,13 +217,20 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
return slurp, nil
}
func get200(ctx context.Context, path string) ([]byte, error) {
return send(ctx, "GET", path, 200, nil)
func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) {
return lc.send(ctx, "GET", path, 200, nil)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
//
// Deprecated: use LocalClient.WhoIs.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
return defaultLocalClient.WhoIs(ctx, remoteAddr)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
func (lc *LocalClient) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
if err != nil {
return nil, err
}
@ -211,18 +245,18 @@ func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, erro
}
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
func Goroutines(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/goroutines")
func (lc *LocalClient) Goroutines(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/goroutines")
}
// DaemonMetrics returns the Tailscale daemon's metrics in
// the Prometheus text exposition format.
func DaemonMetrics(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/metrics")
func (lc *LocalClient) DaemonMetrics(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/metrics")
}
// Profile returns a pprof profile of the Tailscale daemon.
func Profile(ctx context.Context, pprofType string, sec int) ([]byte, error) {
func (lc *LocalClient) Profile(ctx context.Context, pprofType string, sec int) ([]byte, error) {
var secArg string
if sec < 0 || sec > 300 {
return nil, errors.New("duration out of range")
@ -230,12 +264,12 @@ func Profile(ctx context.Context, pprofType string, sec int) ([]byte, error) {
if sec != 0 || pprofType == "profile" {
secArg = fmt.Sprint(sec)
}
return get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
return lc.get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
}
// BugReport logs and returns a log marker that can be shared by the user with support.
func BugReport(ctx context.Context, note string) (string, error) {
body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
if err != nil {
return "", err
}
@ -244,8 +278,8 @@ func BugReport(ctx context.Context, note string) (string, error) {
// DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time.
func DebugAction(ctx context.Context, action string) error {
body, err := send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
@ -254,16 +288,26 @@ func DebugAction(ctx context.Context, action string) error {
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "")
return defaultLocalClient.Status(ctx)
}
// Status returns the Tailscale daemon's status.
func (lc *LocalClient) Status(ctx context.Context) (*ipnstate.Status, error) {
return lc.status(ctx, "")
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "?peers=false")
return defaultLocalClient.StatusWithoutPeers(ctx)
}
func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
body, err := get200(ctx, "/localapi/v0/status"+queryString)
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func (lc *LocalClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return lc.status(ctx, "?peers=false")
}
func (lc *LocalClient) status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
body, err := lc.get200(ctx, "/localapi/v0/status"+queryString)
if err != nil {
return nil, err
}
@ -277,8 +321,8 @@ func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
// IDToken is a request to get an OIDC ID token for an audience.
// The token can be presented to any resource provider which offers OIDC
// Federation.
func IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
body, err := get200(ctx, "/localapi/v0/id-token?aud="+url.QueryEscape(aud))
func (lc *LocalClient) IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/id-token?aud="+url.QueryEscape(aud))
if err != nil {
return nil, err
}
@ -289,8 +333,8 @@ func IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
return tr, nil
}
func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
body, err := get200(ctx, "/localapi/v0/files/")
func (lc *LocalClient) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
body, err := lc.get200(ctx, "/localapi/v0/files/")
if err != nil {
return nil, err
}
@ -301,17 +345,17 @@ func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
return wfs, nil
}
func DeleteWaitingFile(ctx context.Context, baseName string) error {
_, err := send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) error {
_, err := lc.send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
return err
}
func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
if err != nil {
return nil, 0, err
}
res, err := doLocalRequestNiceError(req)
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, 0, err
}
@ -327,8 +371,8 @@ func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, siz
return res.Body, res.ContentLength, nil
}
func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := get200(ctx, "/localapi/v0/file-targets")
func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := lc.get200(ctx, "/localapi/v0/file-targets")
if err != nil {
return nil, err
}
@ -343,7 +387,7 @@ func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
//
// A size of -1 means unknown.
// The name parameter is the original filename, not escaped.
func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
func (lc *LocalClient) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil {
return err
@ -351,7 +395,7 @@ func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name
if size != -1 {
req.ContentLength = size
}
res, err := doLocalRequestNiceError(req)
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return err
}
@ -363,8 +407,11 @@ func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name
return bestError(fmt.Errorf("%s: %s", res.Status, all), all)
}
func CheckIPForwarding(ctx context.Context) error {
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
// CheckIPForwarding asks the local Tailscale daemon whether it looks like the
// machine is properly configured to forward IP packets as a subnet router
// or exit node.
func (lc *LocalClient) CheckIPForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {
return err
}
@ -386,17 +433,17 @@ func CheckIPForwarding(ctx context.Context) error {
// work. Currently (2022-04-18) this only checks for SSH server compatibility.
// Note that EditPrefs does the same validation as this, so call CheckPrefs before
// EditPrefs is not necessary.
func CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
func (lc *LocalClient) CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
pj, err := json.Marshal(p)
if err != nil {
return err
}
_, err = send(ctx, "POST", "/localapi/v0/check-prefs", http.StatusOK, bytes.NewReader(pj))
_, err = lc.send(ctx, "POST", "/localapi/v0/check-prefs", http.StatusOK, bytes.NewReader(pj))
return err
}
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := get200(ctx, "/localapi/v0/prefs")
func (lc *LocalClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := lc.get200(ctx, "/localapi/v0/prefs")
if err != nil {
return nil, err
}
@ -407,12 +454,12 @@ func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
return &p, nil
}
func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
mpj, err := json.Marshal(mp)
if err != nil {
return nil, err
}
body, err := send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, bytes.NewReader(mpj))
body, err := lc.send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, bytes.NewReader(mpj))
if err != nil {
return nil, err
}
@ -423,8 +470,8 @@ func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
return &p, nil
}
func Logout(ctx context.Context) error {
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
func (lc *LocalClient) Logout(ctx context.Context) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
}
@ -442,11 +489,11 @@ func Logout(ctx context.Context) error {
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func SetDNS(ctx context.Context, name, value string) error {
func (lc *LocalClient) SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
_, err := send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
_, err := lc.send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
return err
}
@ -456,7 +503,7 @@ func SetDNS(ctx context.Context, name, value string) error {
// tailscaled), a FQDN, or an IP address.
//
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
func DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
connCh := make(chan net.Conn, 1)
trace := httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
@ -474,7 +521,7 @@ func DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
"Dial-Host": []string{host},
"Dial-Port": []string{fmt.Sprint(port)},
}
res, err := DoLocalRequest(req)
res, err := lc.DoLocalRequest(req)
if err != nil {
return nil, err
}
@ -506,9 +553,9 @@ func DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
// It is intended to be used with netcheck to see availability of DERPs.
func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
func (lc *LocalClient) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
res, err := send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
res, err := lc.send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
if err != nil {
return nil, err
}
@ -521,8 +568,19 @@ func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
//
// Deprecated: use LocalClient.CertPair.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
res, err := send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
return defaultLocalClient.CertPair(ctx, domain)
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
res, err := lc.send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
if err != nil {
return nil, nil, err
}
@ -546,7 +604,21 @@ func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err e
//
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
//
// Deprecated: use LocalClient.GetCertificate.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return defaultLocalClient.GetCertificate(hi)
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
@ -555,11 +627,11 @@ func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
name := hi.ServerName
if !strings.Contains(name, ".") {
if v, ok := ExpandSNIName(ctx, name); ok {
if v, ok := lc.ExpandSNIName(ctx, name); ok {
name = v
}
}
certPEM, keyPEM, err := CertPair(ctx, name)
certPEM, keyPEM, err := lc.CertPair(ctx, name)
if err != nil {
return nil, err
}
@ -571,7 +643,14 @@ func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
//
// Deprecated: use LocalClient.ExpandSNIName.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return defaultLocalClient.ExpandSNIName(ctx, name)
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
func (lc *LocalClient) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := StatusWithoutPeers(ctx)
if err != nil {
return "", false

@ -62,6 +62,12 @@ func main() {
Hostname: *hostname,
}
// TODO(bradfitz,maisem): move this to a method on tsnet.Server probably.
if err := ts.Start(); err != nil {
log.Fatalf("Error starting tsnet.Server: %v", err)
}
localClient, _ := ts.LocalClient()
url, err := url.Parse(fmt.Sprintf("http://%s", *backendAddr))
if err != nil {
log.Fatalf("couldn't parse backend address: %v", err)
@ -71,7 +77,7 @@ func main() {
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
modifyRequest(req)
modifyRequest(req, localClient)
}
var ln net.Listener
@ -84,7 +90,7 @@ func main() {
go func() {
// wait for tailscale to start before trying to fetch cert names
for i := 0; i < 60; i++ {
st, err := tailscale.Status(context.Background())
st, err := localClient.Status(context.Background())
if err != nil {
log.Printf("error retrieving tailscale status; retrying: %v", err)
} else {
@ -100,7 +106,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
name, ok := tailscale.ExpandSNIName(context.Background(), *hostname)
name, ok := localClient.ExpandSNIName(context.Background(), *hostname)
if !ok {
log.Fatalf("can't get hostname for https redirect")
}
@ -120,14 +126,14 @@ func main() {
log.Fatal(http.Serve(ln, proxy))
}
func modifyRequest(req *http.Request) {
func modifyRequest(req *http.Request, localClient *tailscale.LocalClient) {
// with enable_login_token set to true, we get a cookie that handles
// auth for paths that are not /login
if req.URL.Path != "/login" {
return
}
user, err := getTailscaleUser(req.Context(), req.RemoteAddr)
user, err := getTailscaleUser(req.Context(), localClient, req.RemoteAddr)
if err != nil {
log.Printf("error getting Tailscale user: %v", err)
return
@ -137,8 +143,8 @@ func modifyRequest(req *http.Request) {
req.Header.Set("X-Webauth-Name", user.DisplayName)
}
func getTailscaleUser(ctx context.Context, ipPort string) (*tailcfg.UserProfile, error) {
whois, err := tailscale.WhoIs(ctx, ipPort)
func getTailscaleUser(ctx context.Context, localClient *tailscale.LocalClient, ipPort string) (*tailcfg.UserProfile, error) {
whois, err := localClient.WhoIs(ctx, ipPort)
if err != nil {
return nil, fmt.Errorf("failed to identify remote host: %w", err)
}

@ -9,7 +9,6 @@ import (
"errors"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var bugReportCmd = &ffcli.Command{
@ -28,7 +27,7 @@ func runBugReport(ctx context.Context, args []string) error {
default:
return errors.New("unknown argumets")
}
logMarker, err := tailscale.BugReport(ctx, note)
logMarker, err := localClient.BugReport(ctx, note)
if err != nil {
return err
}

@ -50,7 +50,7 @@ func runCert(ctx context.Context, args []string) error {
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
if v, ok := tailscale.ExpandSNIName(r.Context(), r.Host); ok {
if v, ok := localClient.ExpandSNIName(r.Context(), r.Host); ok {
http.Redirect(w, r, "https://"+v+r.URL.Path, http.StatusTemporaryRedirect)
return
}
@ -64,7 +64,7 @@ func runCert(ctx context.Context, args []string) error {
if len(args) != 1 {
var hint bytes.Buffer
if st, err := tailscale.Status(ctx); err == nil {
if st, err := localClient.Status(ctx); err == nil {
if st.BackendState != ipn.Running.String() {
fmt.Fprintf(&hint, "\nTailscale is not running.\n")
} else if len(st.CertDomains) == 0 {

@ -125,6 +125,8 @@ func CleanUpArgs(args []string) []string {
return out
}
var localClient tailscale.LocalClient
// Run runs the CLI. The args do not include the binary name.
func Run(args []string) (err error) {
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
@ -193,10 +195,10 @@ change in the future.
return err
}
tailscale.TailscaledSocket = rootArgs.socket
localClient.Socket = rootArgs.socket
rootfs.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
tailscale.TailscaledSocketSetExplicitly = true
localClient.UseSocketOnly = true
}
})

@ -23,7 +23,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
@ -155,7 +154,7 @@ func runDebug(ctx context.Context, args []string) error {
if out := debugArgs.cpuFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
log.Printf("Capturing CPU profile for %v seconds ...", debugArgs.cpuSec)
if v, err := tailscale.Profile(ctx, "profile", debugArgs.cpuSec); err != nil {
if v, err := localClient.Profile(ctx, "profile", debugArgs.cpuSec); err != nil {
return err
} else {
if err := writeProfile(out, v); err != nil {
@ -167,7 +166,7 @@ func runDebug(ctx context.Context, args []string) error {
if out := debugArgs.memFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
log.Printf("Capturing memory profile ...")
if v, err := tailscale.Profile(ctx, "heap", 0); err != nil {
if v, err := localClient.Profile(ctx, "heap", 0); err != nil {
return err
} else {
if err := writeProfile(out, v); err != nil {
@ -179,7 +178,7 @@ func runDebug(ctx context.Context, args []string) error {
if debugArgs.file != "" {
usedFlag = true // TODO(bradfitz): add "file" subcommand
if debugArgs.file == "get" {
wfs, err := tailscale.WaitingFiles(ctx)
wfs, err := localClient.WaitingFiles(ctx)
if err != nil {
fatalf("%v\n", err)
}
@ -190,9 +189,9 @@ func runDebug(ctx context.Context, args []string) error {
}
delete := strings.HasPrefix(debugArgs.file, "delete:")
if delete {
return tailscale.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
return localClient.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
}
rc, size, err := tailscale.GetWaitingFile(ctx, debugArgs.file)
rc, size, err := localClient.GetWaitingFile(ctx, debugArgs.file)
if err != nil {
return err
}
@ -227,7 +226,7 @@ var prefsArgs struct {
}
func runPrefs(ctx context.Context, args []string) error {
prefs, err := tailscale.GetPrefs(ctx)
prefs, err := localClient.GetPrefs(ctx)
if err != nil {
return err
}
@ -261,7 +260,7 @@ func runWatchIPN(ctx context.Context, args []string) error {
}
func runDERPMap(ctx context.Context, args []string) error {
dm, err := tailscale.CurrentDERPMap(ctx)
dm, err := localClient.CurrentDERPMap(ctx)
if err != nil {
return fmt.Errorf(
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
@ -278,7 +277,7 @@ func localAPIAction(action string) func(context.Context, []string) error {
if len(args) > 0 {
return errors.New("unexpected arguments")
}
return tailscale.DebugAction(ctx, action)
return localClient.DebugAction(ctx, action)
}
}
@ -319,7 +318,7 @@ func runHostinfo(ctx context.Context, args []string) error {
}
func runDaemonGoroutines(ctx context.Context, args []string) error {
goroutines, err := tailscale.Goroutines(ctx)
goroutines, err := localClient.Goroutines(ctx)
if err != nil {
return err
}
@ -334,7 +333,7 @@ var metricsArgs struct {
func runDaemonMetrics(ctx context.Context, args []string) error {
last := map[string]int64{}
for {
out, err := tailscale.DaemonMetrics(ctx)
out, err := localClient.DaemonMetrics(ctx)
if err != nil {
return err
}

@ -9,7 +9,6 @@ import (
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
)
@ -26,7 +25,7 @@ func runDown(ctx context.Context, args []string) error {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return fmt.Errorf("error fetching current status: %w", err)
}
@ -34,7 +33,7 @@ func runDown(ctx context.Context, args []string) error {
fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
return nil
}
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
_, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: false,
},

@ -24,7 +24,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/ipn"
@ -157,7 +156,7 @@ func runCp(ctx context.Context, args []string) error {
if cpArgs.verbose {
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
}
err := tailscale.PushFile(ctx, stableID, contentLength, name, fileContents)
err := localClient.PushFile(ctx, stableID, contentLength, name, fileContents)
if err != nil {
return err
}
@ -173,7 +172,7 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
if err != nil {
return "", false, err
}
fts, err := tailscale.FileTargets(ctx)
fts, err := localClient.FileTargets(ctx)
if err != nil {
return "", false, err
}
@ -194,7 +193,7 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
// invalid file sharing target.
func fileTargetErrorDetail(ctx context.Context, ip netaddr.IP) error {
found := false
if st, err := tailscale.Status(ctx); err == nil && st.Self != nil {
if st, err := localClient.Status(ctx); err == nil && st.Self != nil {
for _, peer := range st.Peer {
for _, pip := range peer.TailscaleIPs {
if pip == ip {
@ -261,7 +260,7 @@ func runCpTargets(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("invalid arguments with --targets")
}
fts, err := tailscale.FileTargets(ctx)
fts, err := localClient.FileTargets(ctx)
if err != nil {
return err
}
@ -385,7 +384,7 @@ func openFileOrSubstitute(dir, base string, action onConflict) (*os.File, error)
}
func receiveFile(ctx context.Context, wf apitype.WaitingFile, dir string) (targetFile string, size int64, err error) {
rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name)
rc, size, err := localClient.GetWaitingFile(ctx, wf.Name)
if err != nil {
return "", 0, fmt.Errorf("opening inbox file %q: %w", wf.Name, err)
}
@ -407,7 +406,7 @@ func runFileGetOneBatch(ctx context.Context, dir string) []error {
var err error
var errs []error
for len(errs) == 0 {
wfs, err = tailscale.WaitingFiles(ctx)
wfs, err = localClient.WaitingFiles(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("getting WaitingFiles: %w", err))
break
@ -439,7 +438,7 @@ func runFileGetOneBatch(ctx context.Context, dir string) []error {
if getArgs.verbose {
printf("wrote %v as %v (%d bytes)\n", wf.Name, writtenFile, size)
}
if err = tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
if err = localClient.DeleteWaitingFile(ctx, wf.Name); err != nil {
errs = append(errs, fmt.Errorf("deleting %q from inbox: %v", wf.Name, err))
continue
}
@ -503,7 +502,7 @@ func wipeInbox(ctx context.Context) error {
if getArgs.wait {
return errors.New("can't use --wait with /dev/null target")
}
wfs, err := tailscale.WaitingFiles(ctx)
wfs, err := localClient.WaitingFiles(ctx)
if err != nil {
return fmt.Errorf("getting WaitingFiles: %w", err)
}
@ -512,7 +511,7 @@ func wipeInbox(ctx context.Context) error {
if getArgs.verbose {
log.Printf("deleting %v ...", wf.Name)
}
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
if err := localClient.DeleteWaitingFile(ctx, wf.Name); err != nil {
return fmt.Errorf("deleting %q: %v", wf.Name, err)
}
deleted++

@ -10,7 +10,6 @@ import (
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var idTokenCmd = &ffcli.Command{
@ -25,7 +24,7 @@ func runIDToken(ctx context.Context, args []string) error {
return errors.New("usage: id-token <aud>")
}
tr, err := tailscale.IDToken(ctx, args[0])
tr, err := localClient.IDToken(ctx, args[0])
if err != nil {
return err
}

@ -12,7 +12,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn/ipnstate"
)
@ -59,7 +58,7 @@ func runIP(ctx context.Context, args []string) error {
if !v4 && !v6 {
v4, v6 = true, true
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return err
}

@ -10,7 +10,6 @@ import (
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var logoutCmd = &ffcli.Command{
@ -30,5 +29,5 @@ func runLogout(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
return tailscale.Logout(ctx)
return localClient.Logout(ctx)
}

@ -13,7 +13,6 @@ import (
"strconv"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var ncCmd = &ffcli.Command{
@ -24,7 +23,7 @@ var ncCmd = &ffcli.Command{
}
func runNC(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
@ -45,7 +44,7 @@ func runNC(ctx context.Context, args []string) error {
}
// TODO(bradfitz): also add UDP too, via flag?
c, err := tailscale.DialTCP(ctx, hostOrIP, uint16(port))
c, err := localClient.DialTCP(ctx, hostOrIP, uint16(port))
if err != nil {
return fmt.Errorf("Dial(%q, %v): %w", hostOrIP, port, err)
}

@ -18,7 +18,6 @@ import (
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/netcheck"
@ -63,7 +62,7 @@ func runNetcheck(ctx context.Context, args []string) error {
fmt.Fprintln(Stderr, "# Warning: this JSON format is not yet considered a stable interface")
}
dm, err := tailscale.CurrentDERPMap(ctx)
dm, err := localClient.CurrentDERPMap(ctx)
noRegions := dm != nil && len(dm.Regions) == 0
if noRegions {
log.Printf("No DERP map from tailscaled; using default.")

@ -16,7 +16,6 @@ import (
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
)
@ -65,7 +64,7 @@ var pingArgs struct {
}
func runPing(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
@ -173,7 +172,7 @@ func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, self b
}
// Otherwise, try to resolve it first from the network peer list.
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return "", false, err
}

@ -20,7 +20,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
)
@ -47,7 +46,7 @@ func runSSH(ctx context.Context, args []string) error {
username = lu.Username
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return err
}

@ -19,7 +19,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/toqueteos/webbrowser"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/interfaces"
@ -73,9 +72,9 @@ func runStatus(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unexpected non-flag arguments to 'tailscale status'")
}
getStatus := tailscale.Status
getStatus := localClient.Status
if !statusArgs.peers {
getStatus = tailscale.StatusWithoutPeers
getStatus = localClient.StatusWithoutPeers
}
st, err := getStatus(ctx)
if err != nil {
@ -115,7 +114,7 @@ func runStatus(ctx context.Context, args []string) error {
http.NotFound(w, r)
return
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
http.Error(w, err.Error(), 500)
return

@ -24,7 +24,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
qrcode "github.com/skip2/go-qrcode"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsaddr"
@ -406,7 +405,7 @@ func runUp(ctx context.Context, args []string) error {
fatalf("too many non-flag arguments: %q", args)
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
@ -447,12 +446,12 @@ func runUp(ctx context.Context, args []string) error {
}
if len(prefs.AdvertiseRoutes) > 0 {
if err := tailscale.CheckIPForwarding(context.Background()); err != nil {
if err := localClient.CheckIPForwarding(context.Background()); err != nil {
warnf("%v", err)
}
}
curPrefs, err := tailscale.GetPrefs(ctx)
curPrefs, err := localClient.GetPrefs(ctx)
if err != nil {
return err
}
@ -471,7 +470,7 @@ func runUp(ctx context.Context, args []string) error {
fatalf("%s", err)
}
if justEditMP != nil {
_, err := tailscale.EditPrefs(ctx, justEditMP)
_, err := localClient.EditPrefs(ctx, justEditMP)
return err
}
@ -582,7 +581,7 @@ func runUp(ctx context.Context, args []string) error {
// Special case: bare "tailscale up" means to just start
// running, if there's ever been a login.
if simpleUp {
_, err := tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
_, err := localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: true,
},
@ -592,7 +591,7 @@ func runUp(ctx context.Context, args []string) error {
return err
}
} else {
if err := tailscale.CheckPrefs(ctx, prefs); err != nil {
if err := localClient.CheckPrefs(ctx, prefs); err != nil {
return err
}

@ -10,7 +10,6 @@ import (
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/version"
)
@ -41,7 +40,7 @@ func runVersion(ctx context.Context, args []string) error {
printf("Client: %s\n", version.String())
st, err := tailscale.StatusWithoutPeers(ctx)
st, err := localClient.StatusWithoutPeers(ctx)
if err != nil {
return err
}

@ -28,7 +28,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype"
@ -318,7 +317,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
prefs, err := tailscale.GetPrefs(r.Context())
prefs, err := localClient.GetPrefs(r.Context())
if err != nil && !postData.Reauthenticate {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
@ -348,12 +347,12 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
return
}
st, err := tailscale.Status(r.Context())
st, err := localClient.Status(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
prefs, err := tailscale.GetPrefs(r.Context())
prefs, err := localClient.GetPrefs(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -406,7 +405,7 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU
prefs.NetfilterMode = preftype.NetfilterOff
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return "", fmt.Errorf("can't fetch status: %v", err)
}

@ -75,6 +75,7 @@ type Server struct {
hostname string
shutdownCtx context.Context
shutdownCancel context.CancelFunc
localClient *tailscale.LocalClient
mu sync.Mutex
listeners map[listenKey]*listener
@ -90,6 +91,17 @@ func (s *Server) Dial(ctx context.Context, network, address string) (net.Conn, e
return s.dialer.UserDial(ctx, network, address)
}
// LocalClient returns a LocalClient that speaks to s.
//
// It will start the server if it has not been started yet. If the server's
// already been started successfully, it doesn't return an error.
func (s *Server) LocalClient() (*tailscale.LocalClient, error) {
if err := s.Start(); err != nil {
return nil, err
}
return s.localClient, nil
}
// Start connects the server to the tailnet.
// Optional: any calls to Dial/Listen will also call Start.
func (s *Server) Start() error {
@ -261,9 +273,7 @@ func (s *Server) start() error {
// TODO(maisem): Rename nettest package to remove "test".
lal := nettest.Listen("local-tailscaled.sock:80")
s.localAPIListener = lal
// Override the Tailscale client to use the in-process listener.
tailscale.TailscaledDialer = lal.Dial
s.localClient = &tailscale.LocalClient{Dial: lal.Dial}
go func() {
if err := http.Serve(lal, lah); err != nil {
logf("localapi serve error: %v", err)

Loading…
Cancel
Save