Files
tailscale/control/tsp/register.go
T
Brad Fitzpatrick 50d7176333 control/tsp, cmd/tsp: add low-level Tailscale protocol client and tool
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>
2026-04-16 20:00:25 -07:00

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
}