50d7176333
Add a new control/tsp package providing a client for speaking the Tailscale protocol to a coordination server over Noise, along with a cmd/tsp binary exposing it as a low-level composable tool for generating keys, registering nodes, and issuing map requests. Previously developed out-of-tree at github.com/bradfitz/tsp; imported here without git history. Updates #12542 Change-Id: I6ad21143c4aefe8939d4a46ae65b2184173bf69f Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
117 lines
3.0 KiB
Go
117 lines
3.0 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package tsp
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"tailscale.com/control/ts2021"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
)
|
|
|
|
// RegisterOpts contains options for registering a node.
|
|
type RegisterOpts struct {
|
|
// NodeKey is the node's private key. Required.
|
|
NodeKey key.NodePrivate
|
|
|
|
// Hostinfo is the host information to send. Optional;
|
|
// if nil, a minimal default is used.
|
|
Hostinfo *tailcfg.Hostinfo
|
|
|
|
// Ephemeral marks the node as ephemeral.
|
|
Ephemeral bool
|
|
|
|
// AuthKey is a pre-authorized auth key.
|
|
AuthKey string
|
|
|
|
// Tags is a list of ACL tags to request.
|
|
Tags []string
|
|
|
|
// MaxResponseSize is the maximum size in bytes of the register
|
|
// response body. If zero, [DefaultMaxMessageSize] is used.
|
|
MaxResponseSize int64
|
|
}
|
|
|
|
// Register sends a registration request to the coordination server
|
|
// and returns the response.
|
|
func (c *Client) Register(ctx context.Context, opts RegisterOpts) (*tailcfg.RegisterResponse, error) {
|
|
hi := opts.Hostinfo
|
|
if hi == nil {
|
|
hi = defaultHostinfo()
|
|
}
|
|
if len(opts.Tags) > 0 {
|
|
hi.RequestTags = opts.Tags
|
|
}
|
|
|
|
regReq := tailcfg.RegisterRequest{
|
|
Version: tailcfg.CurrentCapabilityVersion,
|
|
NodeKey: opts.NodeKey.Public(),
|
|
Hostinfo: hi,
|
|
Ephemeral: opts.Ephemeral,
|
|
}
|
|
if opts.AuthKey != "" {
|
|
regReq.Auth = &tailcfg.RegisterResponseAuth{
|
|
AuthKey: opts.AuthKey,
|
|
}
|
|
}
|
|
|
|
body, err := json.Marshal(regReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encoding register request: %w", err)
|
|
}
|
|
|
|
nc, err := c.noiseClient(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("establishing noise connection: %w", err)
|
|
}
|
|
|
|
url := c.serverURL + "/machine/register"
|
|
url = strings.Replace(url, "http:", "https:", 1)
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating register request: %w", err)
|
|
}
|
|
ts2021.AddLBHeader(req, opts.NodeKey.Public())
|
|
|
|
res, err := nc.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("register request: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
maxResponseSize := cmp.Or(opts.MaxResponseSize, DefaultMaxMessageSize)
|
|
|
|
if res.StatusCode != 200 {
|
|
msg, _ := io.ReadAll(io.LimitReader(res.Body, maxResponseSize))
|
|
return nil, fmt.Errorf("register request: http %d: %.200s",
|
|
res.StatusCode, strings.TrimSpace(string(msg)))
|
|
}
|
|
|
|
// Read up to maxResponseSize+1 so we can distinguish "exactly at cap" from
|
|
// "over the cap" rather than relying on a truncated json parse error.
|
|
data, err := io.ReadAll(io.LimitReader(res.Body, maxResponseSize+1))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading register response: %w", err)
|
|
}
|
|
if int64(len(data)) > maxResponseSize {
|
|
return nil, fmt.Errorf("register response exceeds max %d", maxResponseSize)
|
|
}
|
|
var resp tailcfg.RegisterResponse
|
|
if err := json.Unmarshal(data, &resp); err != nil {
|
|
return nil, fmt.Errorf("decoding register response: %w", err)
|
|
}
|
|
if resp.Error != "" {
|
|
return nil, fmt.Errorf("register: %s", resp.Error)
|
|
}
|
|
return &resp, nil
|
|
}
|