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 3 years ago committed by Brad Fitzpatrick
parent 373176ea54
commit 87ba528ae0

@ -38,24 +38,59 @@ import (
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
var ( // defaultLocalClient is the default LocalClient when using the legacy
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer. // package-level functions.
TailscaledSocket = paths.DefaultTailscaledSocket() 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. func (lc *LocalClient) socket() string {
TailscaledSocketSetExplicitly bool if lc.Socket != "" {
return lc.Socket
}
return paths.DefaultTailscaledSocket()
}
// TailscaledDialer is the DialContext func that connects to the local machine's func (lc *LocalClient) dialer() func(ctx context.Context, network, addr string) (net.Conn, error) {
// tailscaled or equivalent. if lc.Dial != nil {
TailscaledDialer = defaultDialer 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" { if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr) return nil, fmt.Errorf("unexpected URL address %q", addr)
} }
// TODO: make this part of a safesocket.ConnectionStrategy if !lc.UseSocketOnly {
if !TailscaledSocketSetExplicitly {
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running // 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, // 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. // 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)) 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. // The user provided a non-default tailscaled socket address.
// Connect only to exactly what they provided. // Connect only to exactly what they provided.
s.UseFallback(false) s.UseFallback(false)
return safesocket.Connect(s) 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. // 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. // 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. // authenticating to the local Tailscale daemon vary by platform.
// //
// DoLocalRequest may mutate the request to add Authorization headers. // DoLocalRequest may mutate the request to add Authorization headers.
func DoLocalRequest(req *http.Request) (*http.Response, error) { func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error) {
tsClientOnce.Do(func() { lc.tsClientOnce.Do(func() {
tsClient = &http.Client{ lc.tsClient = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: TailscaledDialer, DialContext: lc.dialer(),
}, },
} }
}) })
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil { if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
req.SetBasicAuth("", token) req.SetBasicAuth("", token)
} }
return tsClient.Do(req) return lc.tsClient.Do(req)
} }
func doLocalRequestNiceError(req *http.Request) (*http.Response, error) { func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := DoLocalRequest(req) res, err := lc.DoLocalRequest(req)
if err == nil { if err == nil {
if server := res.Header.Get("Tailscale-Version"); server != "" && server != ipn.IPCVersion() && onVersionMismatch != nil { if server := res.Header.Get("Tailscale-Version"); server != "" && server != ipn.IPCVersion() && onVersionMismatch != nil {
onVersionMismatch(ipn.IPCVersion(), server) onVersionMismatch(ipn.IPCVersion(), server)
@ -169,12 +196,12 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
onVersionMismatch = f 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) req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
res, err := doLocalRequestNiceError(req) res, err := lc.doLocalRequestNiceError(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -190,13 +217,20 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
return slurp, nil return slurp, nil
} }
func get200(ctx context.Context, path string) ([]byte, error) { func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) {
return send(ctx, "GET", path, 200, nil) return lc.send(ctx, "GET", path, 200, nil)
} }
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port. // 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) { 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 { if err != nil {
return nil, err 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. // Goroutines returns a dump of the Tailscale daemon's current goroutines.
func Goroutines(ctx context.Context) ([]byte, error) { func (lc *LocalClient) Goroutines(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/goroutines") return lc.get200(ctx, "/localapi/v0/goroutines")
} }
// DaemonMetrics returns the Tailscale daemon's metrics in // DaemonMetrics returns the Tailscale daemon's metrics in
// the Prometheus text exposition format. // the Prometheus text exposition format.
func DaemonMetrics(ctx context.Context) ([]byte, error) { func (lc *LocalClient) DaemonMetrics(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/metrics") return lc.get200(ctx, "/localapi/v0/metrics")
} }
// Profile returns a pprof profile of the Tailscale daemon. // 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 var secArg string
if sec < 0 || sec > 300 { if sec < 0 || sec > 300 {
return nil, errors.New("duration out of range") 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" { if sec != 0 || pprofType == "profile" {
secArg = fmt.Sprint(sec) 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. // 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) { func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil) body, err := lc.send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
if err != nil { if err != nil {
return "", err 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". // DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time. // These are development tools and subject to change or removal over time.
func DebugAction(ctx context.Context, action string) error { func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
body, err := send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil) body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil { if err != nil {
return fmt.Errorf("error %w: %s", err, body) 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. // Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) { 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. // StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) { 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) { // StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
body, err := get200(ctx, "/localapi/v0/status"+queryString) 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 { if err != nil {
return nil, err 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. // 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 // The token can be presented to any resource provider which offers OIDC
// Federation. // Federation.
func IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) { func (lc *LocalClient) IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
body, err := get200(ctx, "/localapi/v0/id-token?aud="+url.QueryEscape(aud)) body, err := lc.get200(ctx, "/localapi/v0/id-token?aud="+url.QueryEscape(aud))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -289,8 +333,8 @@ func IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
return tr, nil return tr, nil
} }
func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) { func (lc *LocalClient) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
body, err := get200(ctx, "/localapi/v0/files/") body, err := lc.get200(ctx, "/localapi/v0/files/")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -301,17 +345,17 @@ func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
return wfs, nil return wfs, nil
} }
func DeleteWaitingFile(ctx context.Context, baseName string) error { func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) error {
_, err := send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil) _, err := lc.send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
return err 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) req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
res, err := doLocalRequestNiceError(req) res, err := lc.doLocalRequestNiceError(req)
if err != nil { if err != nil {
return nil, 0, err 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 return res.Body, res.ContentLength, nil
} }
func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) { func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := get200(ctx, "/localapi/v0/file-targets") body, err := lc.get200(ctx, "/localapi/v0/file-targets")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -343,7 +387,7 @@ func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
// //
// A size of -1 means unknown. // A size of -1 means unknown.
// The name parameter is the original filename, not escaped. // 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) req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil { if err != nil {
return err return err
@ -351,7 +395,7 @@ func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name
if size != -1 { if size != -1 {
req.ContentLength = size req.ContentLength = size
} }
res, err := doLocalRequestNiceError(req) res, err := lc.doLocalRequestNiceError(req)
if err != nil { if err != nil {
return err 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) return bestError(fmt.Errorf("%s: %s", res.Status, all), all)
} }
func CheckIPForwarding(ctx context.Context) error { // CheckIPForwarding asks the local Tailscale daemon whether it looks like the
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding") // 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 { if err != nil {
return err return err
} }
@ -386,17 +433,17 @@ func CheckIPForwarding(ctx context.Context) error {
// work. Currently (2022-04-18) this only checks for SSH server compatibility. // 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 // Note that EditPrefs does the same validation as this, so call CheckPrefs before
// EditPrefs is not necessary. // 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) pj, err := json.Marshal(p)
if err != nil { if err != nil {
return err 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 return err
} }
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) { func (lc *LocalClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := get200(ctx, "/localapi/v0/prefs") body, err := lc.get200(ctx, "/localapi/v0/prefs")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -407,12 +454,12 @@ func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
return &p, nil 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) mpj, err := json.Marshal(mp)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -423,8 +470,8 @@ func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
return &p, nil return &p, nil
} }
func Logout(ctx context.Context) error { func (lc *LocalClient) Logout(ctx context.Context) error {
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil) _, err := lc.send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err return err
} }
@ -442,11 +489,11 @@ func Logout(ctx context.Context) error {
// This is a low-level interface; it's expected that most Tailscale // This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS // users use a higher level interface to getting/using TLS
// certificates. // 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 := url.Values{}
v.Set("name", name) v.Set("name", name)
v.Set("value", value) 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 return err
} }
@ -456,7 +503,7 @@ func SetDNS(ctx context.Context, name, value string) error {
// tailscaled), a FQDN, or an IP address. // 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. // 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) connCh := make(chan net.Conn, 1)
trace := httptrace.ClientTrace{ trace := httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) { 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-Host": []string{host},
"Dial-Port": []string{fmt.Sprint(port)}, "Dial-Port": []string{fmt.Sprint(port)},
} }
res, err := DoLocalRequest(req) res, err := lc.DoLocalRequest(req)
if err != nil { if err != nil {
return nil, err 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. // 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. // 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 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 { if err != nil {
return nil, err 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. // CertPair returns a cert and private key for the provided DNS domain.
// //
// It returns a cached certificate from disk if it's still valid. // 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) { 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 { if err != nil {
return nil, nil, err 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 // It's the right signature to use as the value of
// tls.Config.GetCertificate. // tls.Config.GetCertificate.
//
// Deprecated: use LocalClient.GetCertificate.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { 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 == "" { if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName") return nil, errors.New("no SNI ServerName")
} }
@ -555,11 +627,11 @@ func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
name := hi.ServerName name := hi.ServerName
if !strings.Contains(name, ".") { if !strings.Contains(name, ".") {
if v, ok := ExpandSNIName(ctx, name); ok { if v, ok := lc.ExpandSNIName(ctx, name); ok {
name = v name = v
} }
} }
certPEM, keyPEM, err := CertPair(ctx, name) certPEM, keyPEM, err := lc.CertPair(ctx, name)
if err != nil { if err != nil {
return nil, err 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. // 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) { 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) st, err := StatusWithoutPeers(ctx)
if err != nil { if err != nil {
return "", false return "", false

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

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

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

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

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

@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn" "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) return fmt.Errorf("too many non-flag arguments: %q", args)
} }
st, err := tailscale.Status(ctx) st, err := localClient.Status(ctx)
if err != nil { if err != nil {
return fmt.Errorf("error fetching current status: %w", err) 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") fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
return nil return nil
} }
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{ _, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{ Prefs: ipn.Prefs{
WantRunning: false, WantRunning: false,
}, },

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

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

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

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

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

@ -18,7 +18,6 @@ import (
"time" "time"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/net/netcheck" "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") 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 noRegions := dm != nil && len(dm.Regions) == 0
if noRegions { if noRegions {
log.Printf("No DERP map from tailscaled; using default.") log.Printf("No DERP map from tailscaled; using default.")

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

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

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

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

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

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

@ -75,6 +75,7 @@ type Server struct {
hostname string hostname string
shutdownCtx context.Context shutdownCtx context.Context
shutdownCancel context.CancelFunc shutdownCancel context.CancelFunc
localClient *tailscale.LocalClient
mu sync.Mutex mu sync.Mutex
listeners map[listenKey]*listener 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) 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. // Start connects the server to the tailnet.
// Optional: any calls to Dial/Listen will also call Start. // Optional: any calls to Dial/Listen will also call Start.
func (s *Server) Start() error { func (s *Server) Start() error {
@ -261,9 +273,7 @@ func (s *Server) start() error {
// TODO(maisem): Rename nettest package to remove "test". // TODO(maisem): Rename nettest package to remove "test".
lal := nettest.Listen("local-tailscaled.sock:80") lal := nettest.Listen("local-tailscaled.sock:80")
s.localAPIListener = lal s.localAPIListener = lal
s.localClient = &tailscale.LocalClient{Dial: lal.Dial}
// Override the Tailscale client to use the in-process listener.
tailscale.TailscaledDialer = lal.Dial
go func() { go func() {
if err := http.Serve(lal, lah); err != nil { if err := http.Serve(lal, lah); err != nil {
logf("localapi serve error: %v", err) logf("localapi serve error: %v", err)

Loading…
Cancel
Save