diff --git a/client/tailscale/tailscale.go b/client/tailscale/tailscale.go index 626532e72..0af754fe8 100644 --- a/client/tailscale/tailscale.go +++ b/client/tailscale/tailscale.go @@ -18,6 +18,9 @@ import ( "fmt" "io" "net/http" + "net/url" + + "tailscale.com/types/key" ) // I_Acknowledge_This_API_Is_Unstable must be set true to use this package @@ -90,6 +93,29 @@ func (c *Client) setAuth(r *http.Request) { } } +// nodeKeyAuth is an AuthMethod for NewClient that authenticates requests +// using a node key over the Noise protocol. +type nodeKeyAuth key.NodePublic + +func (k nodeKeyAuth) modifyRequest(req *http.Request) { + // QueryEscape the node key since it has a colon in it. + nk := url.QueryEscape(key.NodePublic(k).String()) + req.SetBasicAuth(nk, "") +} + +// NewNoiseClient is a convenience method for instantiating a new Client +// that uses the Noise protocol for authentication. +// +// tailnet is the globally unique identifier for a Tailscale network, such +// as "example.com" or "user@gmail.com". +func NewNoiseClient(tailnet string, noiseRoundTripper http.RoundTripper, nk key.NodePublic) *Client { + return &Client{ + tailnet: tailnet, + auth: nodeKeyAuth(nk), + HTTPClient: &http.Client{Transport: noiseRoundTripper}, + } +} + // NewClient is a convenience method for instantiating a new Client. // // tailnet is the globally unique identifier for a Tailscale network, such diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 6509a5bb7..407233eb7 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3736,6 +3736,20 @@ func (b *LocalBackend) magicConn() (*magicsock.Conn, error) { return mc, nil } +type noiseRoundTripper struct { + *LocalBackend +} + +func (n noiseRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return n.LocalBackend.DoNoiseRequest(req) +} + +// NoiseRoundTripper returns an http.RoundTripper that uses the LocalBackend's +// DoNoiseRequest method. +func (b *LocalBackend) NoiseRoundTripper() http.RoundTripper { + return noiseRoundTripper{b} +} + // DoNoiseRequest sends a request to URL over the control plane // Noise connection. func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error) { diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index bc063437c..373b088ce 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -9,6 +9,7 @@ package tsnet import ( "context" + "errors" "fmt" "io" "log" @@ -481,6 +482,25 @@ func getTSNetDir(logf logger.Logf, confDir, prog string) (string, error) { return newPath, nil } +// APIClient returns a tailscale.Client that can be used to make authenticated +// requests to the Tailscale control server. +// It requires the user to set tailscale.I_Acknowledge_This_API_Is_Unstable. +func (s *Server) APIClient() (*tailscale.Client, error) { + if !tailscale.I_Acknowledge_This_API_Is_Unstable { + return nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable") + } + if err := s.Start(); err != nil { + return nil, err + } + + nm := s.lb.NetMap() + if nm == nil { + return nil, errors.New("no netmap, not logged in?") + } + c := tailscale.NewNoiseClient(nm.Domain, s.lb.NoiseRoundTripper(), nm.NodeKey) + return c, nil +} + // Listen announces only on the Tailscale network. // It will start the server if it has not been started yet. func (s *Server) Listen(network, addr string) (net.Listener, error) {