2021-03-01 19:00:37 -08:00
|
|
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
2022-03-18 07:44:05 -07:00
|
|
|
//go:build go1.18
|
|
|
|
// +build go1.18
|
|
|
|
|
2021-03-01 19:00:37 -08:00
|
|
|
// Package tailscale contains Tailscale client code.
|
|
|
|
package tailscale
|
|
|
|
|
|
|
|
import (
|
2021-04-11 16:10:31 -07:00
|
|
|
"bytes"
|
2021-03-01 19:00:37 -08:00
|
|
|
"context"
|
2021-08-17 15:03:28 -07:00
|
|
|
"crypto/tls"
|
2021-03-01 19:00:37 -08:00
|
|
|
"encoding/json"
|
2021-03-31 11:55:21 -07:00
|
|
|
"errors"
|
2021-03-01 19:00:37 -08:00
|
|
|
"fmt"
|
2021-03-30 12:56:00 -07:00
|
|
|
"io"
|
2021-03-01 19:00:37 -08:00
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2021-09-13 17:35:55 -07:00
|
|
|
"os/exec"
|
|
|
|
"runtime"
|
2021-03-01 19:00:37 -08:00
|
|
|
"strconv"
|
2021-03-30 15:59:44 -07:00
|
|
|
"strings"
|
2021-10-05 21:40:19 -07:00
|
|
|
"sync"
|
2021-08-17 15:03:28 -07:00
|
|
|
"time"
|
2021-03-01 19:00:37 -08:00
|
|
|
|
2021-08-17 15:03:28 -07:00
|
|
|
"go4.org/mem"
|
2021-04-13 08:13:46 -07:00
|
|
|
"tailscale.com/client/tailscale/apitype"
|
2021-04-07 08:27:35 -07:00
|
|
|
"tailscale.com/ipn"
|
2021-03-18 19:34:59 -07:00
|
|
|
"tailscale.com/ipn/ipnstate"
|
2021-03-30 09:21:22 -07:00
|
|
|
"tailscale.com/paths"
|
2021-03-01 19:00:37 -08:00
|
|
|
"tailscale.com/safesocket"
|
2021-06-25 11:44:40 -07:00
|
|
|
"tailscale.com/tailcfg"
|
2021-08-19 08:36:13 -07:00
|
|
|
"tailscale.com/version"
|
2021-03-01 19:00:37 -08:00
|
|
|
)
|
|
|
|
|
2021-10-05 21:40:19 -07:00
|
|
|
var (
|
|
|
|
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
|
|
|
|
TailscaledSocket = paths.DefaultTailscaledSocket()
|
2021-03-30 09:21:22 -07:00
|
|
|
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 13:55:55 -08:00
|
|
|
// TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket.
|
|
|
|
TailscaledSocketSetExplicitly bool
|
|
|
|
|
2021-10-05 21:40:19 -07:00
|
|
|
// TailscaledDialer is the DialContext func that connects to the local machine's
|
|
|
|
// tailscaled or equivalent.
|
|
|
|
TailscaledDialer = defaultDialer
|
|
|
|
)
|
|
|
|
|
|
|
|
func 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)
|
|
|
|
}
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 13:55:55 -08:00
|
|
|
// TODO: make this part of a safesocket.ConnectionStrategy
|
|
|
|
if !TailscaledSocketSetExplicitly {
|
2021-10-05 21:40:19 -07:00
|
|
|
// 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.
|
|
|
|
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
|
|
|
var d net.Dialer
|
|
|
|
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
|
|
|
}
|
|
|
|
}
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 13:55:55 -08:00
|
|
|
s := safesocket.DefaultConnectionStrategy(TailscaledSocket)
|
|
|
|
// The user provided a non-default tailscaled socket address.
|
|
|
|
// Connect only to exactly what they provided.
|
|
|
|
s.UseFallback(false)
|
|
|
|
return safesocket.Connect(s)
|
2021-03-01 19:00:37 -08:00
|
|
|
}
|
|
|
|
|
2021-10-05 21:40:19 -07:00
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2021-03-01 19:00:37 -08:00
|
|
|
// 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.
|
|
|
|
//
|
|
|
|
// The hostname must be "local-tailscaled.sock", even though it
|
|
|
|
// doesn't actually do any DNS lookup. The actual means of connecting to and
|
|
|
|
// 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) {
|
2021-10-05 21:40:19 -07:00
|
|
|
tsClientOnce.Do(func() {
|
|
|
|
tsClient = &http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
DialContext: TailscaledDialer,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
})
|
2021-03-01 19:00:37 -08:00
|
|
|
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
|
|
|
req.SetBasicAuth("", token)
|
|
|
|
}
|
|
|
|
return tsClient.Do(req)
|
|
|
|
}
|
|
|
|
|
2021-11-30 08:47:52 -08:00
|
|
|
func doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
|
|
|
|
res, err := DoLocalRequest(req)
|
|
|
|
if err == nil {
|
|
|
|
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
|
|
|
|
onVersionMismatch(version.Long, server)
|
|
|
|
}
|
2022-01-25 09:58:21 -08:00
|
|
|
if res.StatusCode == 403 {
|
|
|
|
all, _ := ioutil.ReadAll(res.Body)
|
|
|
|
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
|
|
|
|
}
|
2021-11-30 08:47:52 -08:00
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
if ue, ok := err.(*url.Error); ok {
|
|
|
|
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
|
|
|
|
path := req.URL.Path
|
|
|
|
pathPrefix := path
|
|
|
|
if i := strings.Index(path, "?"); i != -1 {
|
|
|
|
pathPrefix = path[:i]
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-04-16 10:57:46 -07:00
|
|
|
type errorJSON struct {
|
|
|
|
Error string
|
|
|
|
}
|
|
|
|
|
2021-10-01 09:48:18 -07:00
|
|
|
// AccessDeniedError is an error due to permissions.
|
|
|
|
type AccessDeniedError struct {
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *AccessDeniedError) Error() string { return fmt.Sprintf("Access denied: %v", e.err) }
|
|
|
|
func (e *AccessDeniedError) Unwrap() error { return e.err }
|
|
|
|
|
|
|
|
// IsAccessDeniedError reports whether err is or wraps an AccessDeniedError.
|
|
|
|
func IsAccessDeniedError(err error) bool {
|
|
|
|
var ae *AccessDeniedError
|
|
|
|
return errors.As(err, &ae)
|
|
|
|
}
|
|
|
|
|
2021-04-16 10:57:46 -07:00
|
|
|
// bestError returns either err, or if body contains a valid JSON
|
|
|
|
// object of type errorJSON, its non-empty error body.
|
|
|
|
func bestError(err error, body []byte) error {
|
|
|
|
var j errorJSON
|
|
|
|
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
|
|
|
|
return errors.New(j.Error)
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-10-01 09:48:18 -07:00
|
|
|
func errorMessageFromBody(body []byte) string {
|
|
|
|
var j errorJSON
|
|
|
|
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
|
|
|
|
return j.Error
|
|
|
|
}
|
|
|
|
return strings.TrimSpace(string(body))
|
|
|
|
}
|
|
|
|
|
2021-08-31 15:21:27 -07:00
|
|
|
var onVersionMismatch func(clientVer, serverVer string)
|
|
|
|
|
|
|
|
// SetVersionMismatchHandler sets f as the version mismatch handler
|
|
|
|
// to be called when the client (the current process) has a version
|
|
|
|
// number that doesn't match the server's declared version.
|
|
|
|
func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
|
|
|
|
onVersionMismatch = f
|
|
|
|
}
|
|
|
|
|
2021-04-07 08:19:36 -07:00
|
|
|
func 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)
|
2021-03-01 19:00:37 -08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-11-30 08:47:52 -08:00
|
|
|
res, err := doLocalRequestNiceError(req)
|
2021-03-01 19:00:37 -08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
2021-04-07 08:19:36 -07:00
|
|
|
slurp, err := ioutil.ReadAll(res.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if res.StatusCode != wantStatus {
|
2021-04-16 10:57:46 -07:00
|
|
|
return nil, bestError(err, slurp)
|
2021-04-07 08:19:36 -07:00
|
|
|
}
|
|
|
|
return slurp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func get200(ctx context.Context, path string) ([]byte, error) {
|
|
|
|
return send(ctx, "GET", path, 200, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
|
2021-04-13 08:13:46 -07:00
|
|
|
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
|
2021-04-07 08:19:36 -07:00
|
|
|
body, err := get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2021-03-01 19:00:37 -08:00
|
|
|
}
|
2021-04-13 08:13:46 -07:00
|
|
|
r := new(apitype.WhoIsResponse)
|
2021-04-07 08:19:36 -07:00
|
|
|
if err := json.Unmarshal(body, r); err != nil {
|
|
|
|
if max := 200; len(body) > max {
|
|
|
|
body = append(body[:max], "..."...)
|
2021-03-01 19:00:37 -08:00
|
|
|
}
|
2021-04-07 08:19:36 -07:00
|
|
|
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", body)
|
2021-03-01 19:00:37 -08:00
|
|
|
}
|
|
|
|
return r, nil
|
|
|
|
}
|
2021-03-05 12:07:00 -08:00
|
|
|
|
2021-03-18 19:34:59 -07:00
|
|
|
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
|
2021-03-05 12:07:00 -08:00
|
|
|
func Goroutines(ctx context.Context) ([]byte, error) {
|
2021-04-07 08:19:36 -07:00
|
|
|
return get200(ctx, "/localapi/v0/goroutines")
|
2021-03-05 12:07:00 -08:00
|
|
|
}
|
2021-03-18 19:34:59 -07:00
|
|
|
|
2021-11-15 15:06:37 -08:00
|
|
|
// 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")
|
|
|
|
}
|
|
|
|
|
2021-09-23 09:20:14 -07:00
|
|
|
// Profile returns a pprof profile of the Tailscale daemon.
|
|
|
|
func 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")
|
|
|
|
}
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2021-03-30 15:59:44 -07:00
|
|
|
// 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) {
|
2021-04-07 08:19:36 -07:00
|
|
|
body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
|
2021-03-30 15:59:44 -07:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return strings.TrimSpace(string(body)), nil
|
|
|
|
}
|
|
|
|
|
2021-12-28 19:41:41 -08:00
|
|
|
// 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)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error %w: %s", err, body)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-03-18 19:34:59 -07:00
|
|
|
// Status returns the Tailscale daemon's status.
|
|
|
|
func Status(ctx context.Context) (*ipnstate.Status, error) {
|
2021-03-18 21:07:58 -07:00
|
|
|
return status(ctx, "")
|
|
|
|
}
|
|
|
|
|
2021-12-01 00:15:47 -06:00
|
|
|
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
|
2021-03-18 21:07:58 -07:00
|
|
|
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
|
|
|
return status(ctx, "?peers=false")
|
|
|
|
}
|
|
|
|
|
|
|
|
func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
|
2021-04-07 08:19:36 -07:00
|
|
|
body, err := get200(ctx, "/localapi/v0/status"+queryString)
|
2021-03-18 19:34:59 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
st := new(ipnstate.Status)
|
2021-04-07 08:19:36 -07:00
|
|
|
if err := json.Unmarshal(body, st); err != nil {
|
2021-03-18 19:34:59 -07:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return st, nil
|
|
|
|
}
|
2021-03-30 12:56:00 -07:00
|
|
|
|
2021-04-13 08:13:46 -07:00
|
|
|
func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
|
2021-04-07 08:19:36 -07:00
|
|
|
body, err := get200(ctx, "/localapi/v0/files/")
|
2021-03-30 12:56:00 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-04-13 08:13:46 -07:00
|
|
|
var wfs []apitype.WaitingFile
|
2021-04-07 08:19:36 -07:00
|
|
|
if err := json.Unmarshal(body, &wfs); err != nil {
|
2021-03-30 12:56:00 -07:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return wfs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func DeleteWaitingFile(ctx context.Context, baseName string) error {
|
2021-04-07 08:19:36 -07:00
|
|
|
_, err := send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
|
|
|
|
return err
|
2021-03-30 12:56:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
func 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
|
|
|
|
}
|
2022-01-25 09:58:21 -08:00
|
|
|
res, err := doLocalRequestNiceError(req)
|
2021-03-30 12:56:00 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
if res.ContentLength == -1 {
|
|
|
|
res.Body.Close()
|
|
|
|
return nil, 0, fmt.Errorf("unexpected chunking")
|
|
|
|
}
|
|
|
|
if res.StatusCode != 200 {
|
|
|
|
body, _ := ioutil.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
2021-03-31 11:55:21 -07:00
|
|
|
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
2021-03-30 12:56:00 -07:00
|
|
|
}
|
|
|
|
return res.Body, res.ContentLength, nil
|
|
|
|
}
|
2021-03-31 11:55:21 -07:00
|
|
|
|
2021-04-13 08:13:46 -07:00
|
|
|
func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
|
|
|
|
body, err := get200(ctx, "/localapi/v0/file-targets")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
var fts []apitype.FileTarget
|
|
|
|
if err := json.Unmarshal(body, &fts); err != nil {
|
|
|
|
return nil, fmt.Errorf("invalid JSON: %w", err)
|
|
|
|
}
|
|
|
|
return fts, nil
|
|
|
|
}
|
|
|
|
|
2021-11-30 08:47:52 -08:00
|
|
|
// PushFile sends Taildrop file r to target.
|
|
|
|
//
|
|
|
|
// 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 {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
if size != -1 {
|
|
|
|
req.ContentLength = size
|
|
|
|
}
|
|
|
|
res, err := doLocalRequestNiceError(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if res.StatusCode == 200 {
|
|
|
|
io.Copy(io.Discard, res.Body)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
all, _ := io.ReadAll(res.Body)
|
2022-01-25 09:58:21 -08:00
|
|
|
return bestError(fmt.Errorf("%s: %s", res.Status, all), all)
|
2021-11-30 08:47:52 -08:00
|
|
|
}
|
|
|
|
|
2021-03-31 11:55:21 -07:00
|
|
|
func CheckIPForwarding(ctx context.Context) error {
|
2021-04-07 08:19:36 -07:00
|
|
|
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
|
2021-03-31 11:55:21 -07:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
var jres struct {
|
|
|
|
Warning string
|
|
|
|
}
|
2021-04-07 08:19:36 -07:00
|
|
|
if err := json.Unmarshal(body, &jres); err != nil {
|
2021-03-31 11:55:21 -07:00
|
|
|
return fmt.Errorf("invalid JSON from check-ip-forwarding: %w", err)
|
|
|
|
}
|
|
|
|
if jres.Warning != "" {
|
|
|
|
return errors.New(jres.Warning)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2021-04-07 08:27:35 -07:00
|
|
|
|
|
|
|
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
|
|
|
|
body, err := get200(ctx, "/localapi/v0/prefs")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
var p ipn.Prefs
|
|
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
2021-04-11 16:10:31 -07:00
|
|
|
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
|
|
|
|
}
|
|
|
|
return &p, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
|
|
|
mpj, err := json.Marshal(mp)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-04-11 20:49:07 -07:00
|
|
|
body, err := send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, bytes.NewReader(mpj))
|
2021-04-11 16:10:31 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
var p ipn.Prefs
|
|
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
|
|
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
|
2021-04-07 08:27:35 -07:00
|
|
|
}
|
|
|
|
return &p, nil
|
|
|
|
}
|
2021-04-07 21:06:31 -07:00
|
|
|
|
|
|
|
func Logout(ctx context.Context) error {
|
|
|
|
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
|
|
|
|
return err
|
|
|
|
}
|
2021-06-08 14:49:13 -07:00
|
|
|
|
2021-06-08 15:24:53 -07:00
|
|
|
// SetDNS adds a DNS TXT record for the given domain name, containing
|
|
|
|
// the provided TXT value. The intended use case is answering
|
|
|
|
// LetsEncrypt/ACME dns-01 challenges.
|
|
|
|
//
|
|
|
|
// The control plane will only permit SetDNS requests with very
|
|
|
|
// specific names and values. The name should be
|
|
|
|
// "_acme-challenge." + your node's MagicDNS name. It's expected that
|
|
|
|
// clients cache the certs from LetsEncrypt (or whichever CA is
|
|
|
|
// providing them) and only request new ones as needed; the control plane
|
|
|
|
// rate limits SetDNS requests.
|
|
|
|
//
|
|
|
|
// This is a low-level interface; it's expected that most Tailscale
|
|
|
|
// users use a higher level interface to getting/using TLS
|
|
|
|
// certificates.
|
2021-06-08 14:49:13 -07:00
|
|
|
func 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)
|
|
|
|
return err
|
|
|
|
}
|
2021-06-25 11:44:40 -07:00
|
|
|
|
|
|
|
// 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) {
|
|
|
|
var derpMap tailcfg.DERPMap
|
|
|
|
res, err := send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err = json.Unmarshal(res, &derpMap); err != nil {
|
|
|
|
return nil, fmt.Errorf("invalid derp map json: %w", err)
|
|
|
|
}
|
|
|
|
return &derpMap, nil
|
|
|
|
}
|
2021-08-17 15:03:28 -07:00
|
|
|
|
|
|
|
// CertPair returns a cert and private key for the provided DNS domain.
|
|
|
|
//
|
|
|
|
// It returns a cached certificate from disk if it's still valid.
|
|
|
|
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)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
// with ?type=pair, the response PEM is first the one private
|
|
|
|
// key PEM block, then the cert PEM blocks.
|
|
|
|
i := mem.Index(mem.B(res), mem.S("--\n--"))
|
|
|
|
if i == -1 {
|
|
|
|
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
|
|
|
|
}
|
|
|
|
i += len("--\n")
|
|
|
|
keyPEM, certPEM = res[:i], res[i:]
|
|
|
|
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
|
|
|
|
return nil, nil, fmt.Errorf("unexpected output: key in cert")
|
|
|
|
}
|
|
|
|
return certPEM, keyPEM, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
|
|
if hi == nil || hi.ServerName == "" {
|
|
|
|
return nil, errors.New("no SNI ServerName")
|
|
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
|
|
defer cancel()
|
2021-08-18 10:05:05 -07:00
|
|
|
|
|
|
|
name := hi.ServerName
|
|
|
|
if !strings.Contains(name, ".") {
|
|
|
|
if v, ok := ExpandSNIName(ctx, name); ok {
|
|
|
|
name = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
certPEM, keyPEM, err := CertPair(ctx, name)
|
2021-08-17 15:03:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &cert, nil
|
|
|
|
}
|
2021-08-18 10:05:05 -07:00
|
|
|
|
|
|
|
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
|
|
|
|
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
|
|
|
st, err := StatusWithoutPeers(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
for _, d := range st.CertDomains {
|
|
|
|
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
|
|
|
|
return d, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "", false
|
|
|
|
}
|
2021-09-13 17:35:55 -07:00
|
|
|
|
|
|
|
// tailscaledConnectHint gives a little thing about why tailscaled (or
|
|
|
|
// platform equivalent) is not answering localapi connections.
|
|
|
|
//
|
|
|
|
// It ends in a punctuation. See caller.
|
|
|
|
func tailscaledConnectHint() string {
|
|
|
|
if runtime.GOOS != "linux" {
|
|
|
|
// TODO(bradfitz): flesh this out
|
|
|
|
return "not running?"
|
|
|
|
}
|
|
|
|
out, err := exec.Command("systemctl", "show", "tailscaled.service", "--no-page", "--property", "LoadState,ActiveState,SubState").Output()
|
|
|
|
if err != nil {
|
|
|
|
return "not running?"
|
|
|
|
}
|
|
|
|
// Parse:
|
|
|
|
// LoadState=loaded
|
|
|
|
// ActiveState=inactive
|
|
|
|
// SubState=dead
|
|
|
|
st := map[string]string{}
|
|
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
|
|
if i := strings.Index(line, "="); i != -1 {
|
|
|
|
st[line[:i]] = strings.TrimSpace(line[i+1:])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if st["LoadState"] == "loaded" &&
|
|
|
|
(st["SubState"] != "running" || st["ActiveState"] != "active") {
|
|
|
|
return "systemd tailscaled.service not running."
|
|
|
|
}
|
|
|
|
return "not running?"
|
|
|
|
}
|