Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+343
-34
@@ -1,7 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package local contains a Go client for the Tailscale LocalAPI.
|
||||
//
|
||||
// The APIs in this package vary in maturity: some methods are considered
|
||||
// stable APIs and are documented as such, while others are not necessarily
|
||||
// stable and are subject to change between releases. Methods without an
|
||||
// explicit "API maturity" note in their documentation should be assumed
|
||||
// to be unstable.
|
||||
package local
|
||||
|
||||
import (
|
||||
@@ -14,6 +20,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"iter"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -123,7 +130,28 @@ func (lc *Client) defaultDialer(ctx context.Context, network, addr string) (net.
|
||||
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.ConnectContext(ctx, lc.socket())
|
||||
sock := lc.socket()
|
||||
c, err := safesocket.ConnectContext(ctx, sock)
|
||||
if runtime.GOOS == "windows" && err != nil && sock == paths.DefaultTailscaledSocket() && errors.Is(err, fs.ErrNotExist) {
|
||||
// Nothing is listening on the default pipe. A tailscaled the
|
||||
// current user started with --windows-mode=dev listens on a
|
||||
// per-user pipe instead; try that before giving up. That pipe
|
||||
// is not under the administrators-only prefix, so insist that
|
||||
// it is owned by the current user and not some other user's
|
||||
// process squatting on the name.
|
||||
if dev := paths.WindowsDevTailscaledSocket(); dev != "" {
|
||||
c, err2 := safesocket.ConnectCurrentUserContext(ctx, dev)
|
||||
if err2 == nil {
|
||||
return c, nil
|
||||
}
|
||||
if !errors.Is(err2, fs.ErrNotExist) {
|
||||
// Something is listening there but we won't use it,
|
||||
// which is worth more than the default pipe's absence.
|
||||
return nil, fmt.Errorf("connecting to %s: %w", dev, err2)
|
||||
}
|
||||
}
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
|
||||
@@ -135,6 +163,9 @@ func (lc *Client) defaultDialer(ctx context.Context, network, addr string) (net.
|
||||
// authenticating to the local Tailscale daemon vary by platform.
|
||||
//
|
||||
// DoLocalRequest may mutate the request to add Authorization headers.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DoLocalRequest(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("Tailscale-Cap", strconv.Itoa(int(tailcfg.CurrentCapabilityVersion)))
|
||||
lc.tsClientOnce.Do(func() {
|
||||
@@ -192,8 +223,8 @@ 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)
|
||||
_, ok := errors.AsType[*AccessDeniedError](err)
|
||||
return ok
|
||||
}
|
||||
|
||||
// PreconditionsFailedError is returned when the server responds
|
||||
@@ -210,8 +241,8 @@ func (e *PreconditionsFailedError) Unwrap() error { return e.err }
|
||||
|
||||
// IsPreconditionsFailedError reports whether err is or wraps an PreconditionsFailedError.
|
||||
func IsPreconditionsFailedError(err error) bool {
|
||||
var ae *PreconditionsFailedError
|
||||
return errors.As(err, &ae)
|
||||
_, ok := errors.AsType[*PreconditionsFailedError](err)
|
||||
return ok
|
||||
}
|
||||
|
||||
// bestError returns either err, or if body contains a valid JSON
|
||||
@@ -280,7 +311,7 @@ func (lc *Client) sendWithHeaders(
|
||||
}
|
||||
if res.StatusCode != wantStatus {
|
||||
err = fmt.Errorf("%v: %s", res.Status, bytes.TrimSpace(slurp))
|
||||
return nil, nil, httpStatusError{bestError(err, slurp), res.StatusCode}
|
||||
return nil, nil, httpStatusError{bestError(err, slurp), res.StatusCode, res.Header}
|
||||
}
|
||||
return slurp, res.Header, nil
|
||||
}
|
||||
@@ -288,6 +319,7 @@ func (lc *Client) sendWithHeaders(
|
||||
type httpStatusError struct {
|
||||
error
|
||||
HTTPStatus int
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
func (lc *Client) get200(ctx context.Context, path string) ([]byte, error) {
|
||||
@@ -316,6 +348,8 @@ func decodeJSON[T any](b []byte) (ret T, err error) {
|
||||
// For connections proxied by tailscaled, this looks up the owner of the given
|
||||
// address as TCP first, falling back to UDP; if you want to only check a
|
||||
// specific address family, use WhoIsProto.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
|
||||
if err != nil {
|
||||
@@ -327,6 +361,39 @@ func (lc *Client) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsR
|
||||
return decodeJSON[*apitype.WhoIsResponse](body)
|
||||
}
|
||||
|
||||
// WhoIsForService is like [Client.WhoIs] but scopes the returned CapMap to
|
||||
// capabilities that apply to the named VIP service. This enables per-service
|
||||
// capability resolution on hosts that advertise multiple VIP services.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) WhoIsForService(ctx context.Context, remoteAddr string, svcName tailcfg.ServiceName) (*apitype.WhoIsResponse, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr)+"&svc_name="+url.QueryEscape(string(svcName)))
|
||||
if err != nil {
|
||||
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
|
||||
return nil, ErrPeerNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*apitype.WhoIsResponse](body)
|
||||
}
|
||||
|
||||
// WhoIsForIP is like [Client.WhoIs] but scopes the returned CapMap to
|
||||
// capabilities that apply to the given destination IP. The IP may be a
|
||||
// VIP service address, the node's own tailnet address, or any other
|
||||
// routable IP the node handles.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) WhoIsForIP(ctx context.Context, remoteAddr string, dst netip.Addr) (*apitype.WhoIsResponse, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr)+"&dst_ip="+url.QueryEscape(dst.String()))
|
||||
if err != nil {
|
||||
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
|
||||
return nil, ErrPeerNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*apitype.WhoIsResponse](body)
|
||||
}
|
||||
|
||||
// ErrPeerNotFound is returned by [Client.WhoIs], [Client.WhoIsNodeKey] and
|
||||
// [Client.WhoIsProto] when a peer is not found.
|
||||
var ErrPeerNotFound = errors.New("peer not found")
|
||||
@@ -334,6 +401,8 @@ var ErrPeerNotFound = errors.New("peer not found")
|
||||
// WhoIsNodeKey returns the owner of the given wireguard public key.
|
||||
//
|
||||
// If not found, the error is ErrPeerNotFound.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*apitype.WhoIsResponse, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(key.String()))
|
||||
if err != nil {
|
||||
@@ -349,6 +418,8 @@ func (lc *Client) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*apityp
|
||||
// IP:port, for the given protocol (tcp or udp).
|
||||
//
|
||||
// If not found, the error is [ErrPeerNotFound].
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) WhoIsProto(ctx context.Context, proto, remoteAddr string) (*apitype.WhoIsResponse, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/whois?proto="+url.QueryEscape(proto)+"&addr="+url.QueryEscape(remoteAddr))
|
||||
if err != nil {
|
||||
@@ -425,6 +496,9 @@ func (lc *Client) SetGauge(ctx context.Context, name string, value int) error {
|
||||
|
||||
// TailDaemonLogs returns a stream the Tailscale daemon's logs as they arrive.
|
||||
// Close the context to stop the stream.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/logtap", nil)
|
||||
if err != nil {
|
||||
@@ -441,15 +515,29 @@ func (lc *Client) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
|
||||
}
|
||||
|
||||
// EventBusGraph returns a graph of active publishers and subscribers in the eventbus
|
||||
// as a [eventbus.DebugTopics]
|
||||
// as a [eventbus.DebugTopics].
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) EventBusGraph(ctx context.Context) ([]byte, error) {
|
||||
return lc.get200(ctx, "/localapi/v0/debug-bus-graph")
|
||||
}
|
||||
|
||||
// EventBusQueues returns a JSON snapshot of event bus queue depths per client.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) EventBusQueues(ctx context.Context) ([]byte, error) {
|
||||
return lc.get200(ctx, "/localapi/v0/debug-bus-queues")
|
||||
}
|
||||
|
||||
// StreamBusEvents returns an iterator of Tailscale bus events as they arrive.
|
||||
// Each pair is a valid event and a nil error, or a zero event a non-nil error.
|
||||
// In case of error, the iterator ends after the pair reporting the error.
|
||||
// Iteration stops if ctx ends.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) StreamBusEvents(ctx context.Context) iter.Seq2[eventbus.DebugEvent, error] {
|
||||
return func(yield func(eventbus.DebugEvent, error) bool) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET",
|
||||
@@ -518,6 +606,8 @@ type BugReportOpts struct {
|
||||
//
|
||||
// The opts type specifies options to pass to the Tailscale daemon when
|
||||
// generating this bug report.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
|
||||
qparams := make(url.Values)
|
||||
if opts.Note != "" {
|
||||
@@ -563,12 +653,17 @@ func (lc *Client) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (st
|
||||
//
|
||||
// This is the same as calling [Client.BugReportWithOpts] and only specifying the Note
|
||||
// field.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) BugReport(ctx context.Context, note string) (string, error) {
|
||||
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change or removal between releases.
|
||||
func (lc *Client) 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 {
|
||||
@@ -579,7 +674,10 @@ func (lc *Client) DebugAction(ctx context.Context, action string) error {
|
||||
|
||||
// DebugActionBody invokes a debug action with a body parameter, such as
|
||||
// "debug-force-prefer-derp".
|
||||
// These are development tools and subject to change or removal over time.
|
||||
// These are development tools.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change or removal between releases.
|
||||
func (lc *Client) DebugActionBody(ctx context.Context, action string, rbody io.Reader) error {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, rbody)
|
||||
if err != nil {
|
||||
@@ -589,7 +687,10 @@ func (lc *Client) DebugActionBody(ctx context.Context, action string, rbody io.R
|
||||
}
|
||||
|
||||
// DebugResultJSON invokes a debug action and returns its result as something JSON-able.
|
||||
// These are development tools and subject to change or removal over time.
|
||||
// These are development tools.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change or removal between releases.
|
||||
func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
|
||||
if err != nil {
|
||||
@@ -602,6 +703,27 @@ func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, erro
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// GetDebugResultJSON invokes a debug action and decodes the JSON response
|
||||
// into a value of type T. It avoids the marshal/unmarshal roundtrip that
|
||||
// callers of [Client.DebugResultJSON] otherwise need to do to get a typed
|
||||
// value.
|
||||
//
|
||||
// These are development tools.
|
||||
//
|
||||
// API maturity: this function is not considered a stable API and is
|
||||
// subject to change or removal between releases.
|
||||
func GetDebugResultJSON[T any](ctx context.Context, lc *Client, action string) (T, error) {
|
||||
var v T
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
if err := json.Unmarshal(body, &v); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// QueryOptionalFeatures queries the optional features supported by the Tailscale daemon.
|
||||
func (lc *Client) QueryOptionalFeatures(ctx context.Context) (*apitype.OptionalFeatures, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-optional-features", 200, nil)
|
||||
@@ -631,6 +753,9 @@ func (lc *Client) SetDevStoreKeyValue(ctx context.Context, key, value string) er
|
||||
// SetComponentDebugLogging sets component's debug logging enabled for
|
||||
// the provided duration. If the duration is in the past, the debug logging
|
||||
// is disabled.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
|
||||
if !buildfeatures.HasDebug {
|
||||
return feature.ErrUnavailable
|
||||
@@ -659,6 +784,8 @@ func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
}
|
||||
|
||||
// Status returns the Tailscale daemon's status.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return lc.status(ctx, "")
|
||||
}
|
||||
@@ -669,6 +796,8 @@ func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
}
|
||||
|
||||
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return lc.status(ctx, "?peers=false")
|
||||
}
|
||||
@@ -773,6 +902,9 @@ func (lc *Client) PushFile(ctx context.Context, target tailcfg.StableNodeID, siz
|
||||
// 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.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) CheckIPForwarding(ctx context.Context) error {
|
||||
if !buildfeatures.HasAdvertiseRoutes {
|
||||
return nil
|
||||
@@ -796,6 +928,9 @@ func (lc *Client) CheckIPForwarding(ctx context.Context) error {
|
||||
// CheckUDPGROForwarding asks the local Tailscale daemon whether it looks like
|
||||
// the machine is optimally configured to forward UDP packets as a subnet router
|
||||
// or exit node.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) CheckUDPGROForwarding(ctx context.Context) error {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/check-udp-gro-forwarding")
|
||||
if err != nil {
|
||||
@@ -813,25 +948,6 @@ func (lc *Client) CheckUDPGROForwarding(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckReversePathFiltering asks the local Tailscale daemon whether strict
|
||||
// reverse path filtering is enabled, which would break exit node usage on Linux.
|
||||
func (lc *Client) CheckReversePathFiltering(ctx context.Context) error {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/check-reverse-path-filtering")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var jres struct {
|
||||
Warning string
|
||||
}
|
||||
if err := json.Unmarshal(body, &jres); err != nil {
|
||||
return fmt.Errorf("invalid JSON from check-reverse-path-filtering: %w", err)
|
||||
}
|
||||
if jres.Warning != "" {
|
||||
return errors.New(jres.Warning)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUDPGROForwarding enables UDP GRO forwarding for the main interface of this
|
||||
// node. This can be done to improve performance of tailnet nodes acting as exit
|
||||
// nodes or subnet routers.
|
||||
@@ -864,6 +980,9 @@ func (lc *Client) CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPrefs returns the [ipn.Prefs] of the current Tailscale profile.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/prefs")
|
||||
if err != nil {
|
||||
@@ -881,6 +1000,8 @@ func (lc *Client) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
|
||||
// or a policy restriction. An optional reason or justification for the request can be
|
||||
// provided as a context value using [apitype.RequestReasonKey]. If permitted by policy,
|
||||
// access may be granted, and the reason will be logged for auditing purposes.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
body, err := lc.send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, jsonBody(mp))
|
||||
if err != nil {
|
||||
@@ -891,6 +1012,9 @@ func (lc *Client) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Pref
|
||||
|
||||
// GetDNSOSConfig returns the system DNS configuration for the current device.
|
||||
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return nil, feature.ErrUnavailable
|
||||
@@ -924,7 +1048,26 @@ func (lc *Client) QueryDNS(ctx context.Context, name string, queryType string) (
|
||||
return res.Bytes, res.Resolvers, nil
|
||||
}
|
||||
|
||||
// StartLoginInteractive starts an interactive login.
|
||||
// StartLoginInteractive starts an interactive login, requesting a new
|
||||
// auth URL from the control plane if a login flow is not already in
|
||||
// progress. If one is, the existing auth URL is re-sent.
|
||||
//
|
||||
// The auth URL is not returned by this method; it is delivered
|
||||
// asynchronously to IPN bus watchers (see [Client.WatchIPNBus]) as an
|
||||
// [ipn.Notify] with a non-empty BrowseToURL field. StartLoginInteractive
|
||||
// returns as soon as the login has been requested; it does not wait for
|
||||
// the login to complete.
|
||||
//
|
||||
// Calling StartLoginInteractive does not itself change the node's
|
||||
// desired run state, but successfully completing the login does: the
|
||||
// node's WantRunning pref is set to true, so a stopped node
|
||||
// ("tailscale down") starts once the login finishes. If the login is
|
||||
// completed as a different user or node identity than the current
|
||||
// profile's, the node switches to an existing profile matching the new
|
||||
// identity if one exists, or else updates the current profile to the
|
||||
// new identity.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) StartLoginInteractive(ctx context.Context) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/login-interactive", http.StatusNoContent, nil)
|
||||
return err
|
||||
@@ -949,6 +1092,8 @@ func (lc *Client) Logout(ctx context.Context) 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].
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
|
||||
return lc.UserDial(ctx, "tcp", host, port)
|
||||
}
|
||||
@@ -960,6 +1105,8 @@ func (lc *Client) DialTCP(ctx context.Context, host string, port uint16) (net.Co
|
||||
//
|
||||
// The ctx is only used for the duration of the call, not the lifetime of the
|
||||
// [net.Conn].
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) UserDial(ctx context.Context, network, host string, port uint16) (net.Conn, error) {
|
||||
connCh := make(chan net.Conn, 1)
|
||||
trace := httptrace.ClientTrace{
|
||||
@@ -986,6 +1133,19 @@ func (lc *Client) UserDial(ctx context.Context, network, host string, port uint1
|
||||
if res.StatusCode != http.StatusSwitchingProtocols {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if res.StatusCode == http.StatusOK && res.Header.Get("Dial-Self") == "true" {
|
||||
// Server told us to dial the address ourselves rather than
|
||||
// proxying through the daemon. This happens for non-Tailscale
|
||||
// addresses where the daemon shouldn't dial as root on the
|
||||
// client's behalf. The server provides the resolved address
|
||||
// to avoid a TOCTOU race with DNS re-resolution.
|
||||
addr := res.Header.Get("Dial-Addr")
|
||||
if addr == "" {
|
||||
return nil, errors.New("server returned Dial-Self without Dial-Addr")
|
||||
}
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, addr)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected HTTP response: %s, %s", res.Status, body)
|
||||
}
|
||||
// From here on, the underlying net.Conn is ours to use, but there
|
||||
@@ -1011,6 +1171,10 @@ func (lc *Client) UserDial(ctx context.Context, network, host string, port uint1
|
||||
|
||||
// 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.
|
||||
//
|
||||
// API maturity: this is considered a stable API, though the returned
|
||||
// [tailcfg.DERPMap] type is subject to minor changes over time; its
|
||||
// general shape is stable.
|
||||
func (lc *Client) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
var derpMap tailcfg.DERPMap
|
||||
res, err := lc.send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
|
||||
@@ -1023,6 +1187,66 @@ func (lc *Client) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error)
|
||||
return &derpMap, nil
|
||||
}
|
||||
|
||||
// CertDomains returns the list of domains for which the local tailscaled can
|
||||
// fetch TLS certificates, equivalent to the DNS.CertDomains field of the
|
||||
// current netmap. The returned list is sorted in ascending order, and is
|
||||
// empty if no netmap has been received yet.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) CertDomains(ctx context.Context) ([]string, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/cert-domains")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[[]string](body)
|
||||
}
|
||||
|
||||
// DNSConfig returns the [tailcfg.DNSConfig] from the current netmap.
|
||||
// It returns an error if no netmap has been received yet.
|
||||
// It is intended for callers that need fields like ExtraRecords or CertDomains
|
||||
// without pulling the rest of the netmap.
|
||||
func (lc *Client) DNSConfig(ctx context.Context) (*tailcfg.DNSConfig, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/dns-config")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*tailcfg.DNSConfig](body)
|
||||
}
|
||||
|
||||
// PeerByID returns a peer's current full [tailcfg.Node] looked up by its
|
||||
// [tailcfg.NodeID]. It returns an error if no peer with that NodeID is in the
|
||||
// current netmap.
|
||||
//
|
||||
// It is intended for callers that observed a peer-mutation signal (e.g.
|
||||
// [ipn.Notify.PeerChangedPatch] or [ipn.Notify.PeersChanged]) and want the
|
||||
// latest state of the affected node without having to apply the patch
|
||||
// themselves.
|
||||
func (lc *Client) PeerByID(ctx context.Context, id tailcfg.NodeID) (*tailcfg.Node, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/peer-by-id?id="+strconv.FormatInt(int64(id), 10))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*tailcfg.Node](body)
|
||||
}
|
||||
|
||||
// UserProfile returns the current [tailcfg.UserProfile] for the given
|
||||
// [tailcfg.UserID]. It returns an error if no user with that UserID is in the
|
||||
// current netmap.
|
||||
//
|
||||
// It is the LocalAPI fallback for IPN-bus consumers that see a UserID
|
||||
// referenced by a peer Node and want to resolve it to a UserProfile. Sessions
|
||||
// opted in to [ipn.NotifyPeerChanges] / [ipn.NotifyPeerPatches] also receive
|
||||
// UserProfiles automatically via [ipn.Notify.UserProfiles].
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) UserProfile(ctx context.Context, id tailcfg.UserID) (*tailcfg.UserProfile, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/user-profile?id="+strconv.FormatInt(int64(id), 10))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*tailcfg.UserProfile](body)
|
||||
}
|
||||
|
||||
// PingOpts contains options for the ping request.
|
||||
//
|
||||
// The zero value is valid, which means to use defaults.
|
||||
@@ -1059,6 +1283,8 @@ func (lc *Client) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.Ping
|
||||
// DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be
|
||||
// run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over
|
||||
// to another replica whilst there is still some grace period for the existing connections to terminate.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) DisconnectControl(ctx context.Context) error {
|
||||
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/disconnect-control", 200, nil, nil)
|
||||
if err != nil {
|
||||
@@ -1085,7 +1311,7 @@ func tailscaledConnectHint() string {
|
||||
// ActiveState=inactive
|
||||
// SubState=dead
|
||||
st := map[string]string{}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
for line := range strings.SplitSeq(string(out), "\n") {
|
||||
if k, v, ok := strings.Cut(line, "="); ok {
|
||||
st[k] = strings.TrimSpace(v)
|
||||
}
|
||||
@@ -1154,13 +1380,18 @@ func (lc *Client) ReloadConfig(ctx context.Context) (ok bool, err error) {
|
||||
|
||||
// SwitchToEmptyProfile creates and switches to a new unnamed profile. The new
|
||||
// profile is not assigned an ID until it is persisted after a successful login.
|
||||
// In order to login to the new profile, the user must call LoginInteractive.
|
||||
// In order to login to the new profile, the user must call
|
||||
// [Client.StartLoginInteractive].
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) SwitchToEmptyProfile(ctx context.Context) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/profiles/", http.StatusCreated, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SwitchProfile switches to the given profile.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) SwitchProfile(ctx context.Context, profile ipn.ProfileID) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/profiles/"+url.PathEscape(string(profile)), 204, nil)
|
||||
return err
|
||||
@@ -1195,6 +1426,11 @@ func (lc *Client) QueryFeature(ctx context.Context, feature string) (*tailcfg.Qu
|
||||
return decodeJSON[*tailcfg.QueryFeatureResponse](body)
|
||||
}
|
||||
|
||||
// DebugDERPRegion reports diagnostic information about the DERP region with
|
||||
// the given ID or code.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) {
|
||||
v := url.Values{"region": {regionIDOrCode}}
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-derp-region?"+v.Encode(), 200, nil)
|
||||
@@ -1205,6 +1441,9 @@ func (lc *Client) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*
|
||||
}
|
||||
|
||||
// DebugPacketFilterRules returns the packet filter rules for the current device.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.FilterRule, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-packet-filter-rules", 200, nil)
|
||||
if err != nil {
|
||||
@@ -1216,6 +1455,9 @@ func (lc *Client) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.FilterR
|
||||
// DebugSetExpireIn marks the current node key to expire in d.
|
||||
//
|
||||
// This is meant primarily for debug and testing.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
|
||||
v := url.Values{"expiry": {fmt.Sprint(time.Now().Add(d).Unix())}}
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/set-expiry-sooner?"+v.Encode(), 200, nil)
|
||||
@@ -1224,6 +1466,9 @@ func (lc *Client) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
|
||||
|
||||
// DebugPeerRelaySessions returns debug information about the current peer
|
||||
// relay sessions running through this node.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DebugPeerRelaySessions(ctx context.Context) (*status.ServerStatus, error) {
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/debug-peer-relay-sessions", 200, nil)
|
||||
if err != nil {
|
||||
@@ -1236,6 +1481,9 @@ func (lc *Client) DebugPeerRelaySessions(ctx context.Context) (*status.ServerSta
|
||||
//
|
||||
// The provided context does not determine the lifetime of the
|
||||
// returned [io.ReadCloser].
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) StreamDebugCapture(ctx context.Context) (io.ReadCloser, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-capture", nil)
|
||||
if err != nil {
|
||||
@@ -1262,9 +1510,16 @@ func (lc *Client) StreamDebugCapture(ctx context.Context) (io.ReadCloser, error)
|
||||
// resources.
|
||||
//
|
||||
// A default set of ipn.Notify messages are returned but the set can be modified by mask.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IPNBusWatcher, error) {
|
||||
m, err := mask.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET",
|
||||
"http://"+apitype.LocalAPIHost+"/localapi/v0/watch-ipn-bus?mask="+fmt.Sprint(mask),
|
||||
"http://"+apitype.LocalAPIHost+"/localapi/v0/watch-ipn-bus?mask="+string(m),
|
||||
nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1288,6 +1543,8 @@ func (lc *Client) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IP
|
||||
// CheckUpdate returns a [*tailcfg.ClientVersion] indicating whether or not an update is available
|
||||
// to be installed via the LocalAPI. In case the LocalAPI can't install updates, it returns a
|
||||
// ClientVersion that says that we are up to date.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/update/check")
|
||||
if err != nil {
|
||||
@@ -1304,6 +1561,8 @@ func (lc *Client) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion, erro
|
||||
// To turn it on, there must have been a previously used exit node.
|
||||
// The most previously used one is reused.
|
||||
// This is a convenience method for GUIs. To select an actual one, update the prefs.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) SetUseExitNode(ctx context.Context, on bool) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/set-use-exit-node-enabled?enabled="+strconv.FormatBool(on), http.StatusOK, nil)
|
||||
return err
|
||||
@@ -1312,6 +1571,9 @@ func (lc *Client) SetUseExitNode(ctx context.Context, on bool) error {
|
||||
// DriveSetServerAddr instructs Taildrive to use the server at addr to access
|
||||
// the filesystem. This is used on platforms like Windows and MacOS to let
|
||||
// Taildrive know to use the file server running in the GUI app.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DriveSetServerAddr(ctx context.Context, addr string) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/fileserver-address", http.StatusCreated, strings.NewReader(addr))
|
||||
return err
|
||||
@@ -1320,6 +1582,9 @@ func (lc *Client) DriveSetServerAddr(ctx context.Context, addr string) error {
|
||||
// DriveShareSet adds or updates the given share in the list of shares that
|
||||
// Taildrive will serve to remote nodes. If a share with the same name already
|
||||
// exists, the existing share is replaced/updated.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DriveShareSet(ctx context.Context, share *drive.Share) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/shares", http.StatusCreated, jsonBody(share))
|
||||
return err
|
||||
@@ -1327,6 +1592,9 @@ func (lc *Client) DriveShareSet(ctx context.Context, share *drive.Share) error {
|
||||
|
||||
// DriveShareRemove removes the share with the given name from the list of
|
||||
// shares that Taildrive will serve to remote nodes.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DriveShareRemove(ctx context.Context, name string) error {
|
||||
_, err := lc.send(
|
||||
ctx,
|
||||
@@ -1338,6 +1606,9 @@ func (lc *Client) DriveShareRemove(ctx context.Context, name string) error {
|
||||
}
|
||||
|
||||
// DriveShareRename renames the share from old to new name.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DriveShareRename(ctx context.Context, oldName, newName string) error {
|
||||
_, err := lc.send(
|
||||
ctx,
|
||||
@@ -1350,6 +1621,9 @@ func (lc *Client) DriveShareRename(ctx context.Context, oldName, newName string)
|
||||
|
||||
// DriveShareList returns the list of shares that drive is currently serving
|
||||
// to remote nodes.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
|
||||
result, err := lc.get200(ctx, "/localapi/v0/drive/shares")
|
||||
if err != nil {
|
||||
@@ -1406,8 +1680,25 @@ func (lc *Client) SuggestExitNode(ctx context.Context) (apitype.ExitNodeSuggesti
|
||||
return decodeJSON[apitype.ExitNodeSuggestionResponse](body)
|
||||
}
|
||||
|
||||
// SuggestExitNodeWithProbe requests an exit node suggestion based on an immediate routecheck probe,
|
||||
// waits for the probe to finish, and returns the exit node's details.
|
||||
func (lc *Client) SuggestExitNodeWithProbe(ctx context.Context) (apitype.ExitNodeSuggestionResponse, error) {
|
||||
if !buildfeatures.HasRouteCheck {
|
||||
return apitype.ExitNodeSuggestionResponse{}, feature.ErrUnavailable
|
||||
}
|
||||
v := url.Values{"probe": {"true"}}
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/suggest-exit-node?"+v.Encode(), 200, nil)
|
||||
if err != nil {
|
||||
return apitype.ExitNodeSuggestionResponse{}, err
|
||||
}
|
||||
return decodeJSON[apitype.ExitNodeSuggestionResponse](body)
|
||||
}
|
||||
|
||||
// CheckSOMarkInUse reports whether the socket mark option is in use. This will only
|
||||
// be true if tailscale is running on Linux and tailscaled uses SO_MARK.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) CheckSOMarkInUse(ctx context.Context) (bool, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/check-so-mark-in-use")
|
||||
if err != nil {
|
||||
@@ -1424,11 +1715,19 @@ func (lc *Client) CheckSOMarkInUse(ctx context.Context) (bool, error) {
|
||||
}
|
||||
|
||||
// ShutdownTailscaled requests a graceful shutdown of tailscaled.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) ShutdownTailscaled(ctx context.Context) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/shutdown", 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAppConnectorRouteInfo returns the current [appctype.RouteInfo] for this
|
||||
// node's app connector.
|
||||
//
|
||||
// API maturity: this method is not considered a stable API and is
|
||||
// subject to change between releases.
|
||||
func (lc *Client) GetAppConnectorRouteInfo(ctx context.Context) (appctype.RouteInfo, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/appc-route-info")
|
||||
if err != nil {
|
||||
@@ -1436,3 +1735,13 @@ func (lc *Client) GetAppConnectorRouteInfo(ctx context.Context) (appctype.RouteI
|
||||
}
|
||||
return decodeJSON[appctype.RouteInfo](body)
|
||||
}
|
||||
|
||||
// GetServices returns the Services visible to this node,
|
||||
// including their names, IP addresses, and ports, keyed by service name.
|
||||
func (lc *Client) GetServices(ctx context.Context) (map[tailcfg.ServiceName]tailcfg.ServiceDetails, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/services")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[map[tailcfg.ServiceName]tailcfg.ServiceDetails](body)
|
||||
}
|
||||
Reference in new issue
Block a user