control/tsp: add lite map update support

Updates #12542
Updates tailscale/corp#40088

Change-Id: Idb4526f1bf1f3f424d6fb3d7e34ebe89a474b57b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-17 04:21:29 +00:00
committed by Brad Fitzpatrick
parent c2da563fef
commit 00a08ea86d
2 changed files with 205 additions and 0 deletions
+76
View File
@@ -249,6 +249,82 @@ func (s *MapSession) Close() error {
return s.closeErr
}
// SendMapUpdateOpts contains options for [Client.SendMapUpdate].
type SendMapUpdateOpts struct {
// NodeKey is the node's private key. Required.
NodeKey key.NodePrivate
// DiscoKey, if non-zero, is the node's disco public key.
// Peers use it to verify disco pings from this node, which is
// what enables direct (non-DERP) paths.
DiscoKey key.DiscoPublic
// Hostinfo is the host information to send. Optional;
// if nil, a minimal default is used.
Hostinfo *tailcfg.Hostinfo
}
// SendMapUpdate sends a one-shot, non-streaming MapRequest to push small
// updates (such as the node's endpoints, hostinfo, or disco public key) to the
// coordination server without starting or disturbing a streaming map session.
func (c *Client) SendMapUpdate(ctx context.Context, opts SendMapUpdateOpts) error {
if opts.NodeKey.IsZero() {
return fmt.Errorf("NodeKey is required")
}
hi := opts.Hostinfo
if hi == nil {
hi = defaultHostinfo()
}
mapReq := tailcfg.MapRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: opts.NodeKey.Public(),
DiscoKey: opts.DiscoKey,
Hostinfo: hi,
Compress: "zstd",
// A lite update that lets the server persist our state without breaking
// any existing streaming map session. See the [tailcfg.MapResponse]
// OmitPeers docs.
OmitPeers: true,
Stream: false,
ReadOnly: false,
}
body, err := json.Marshal(mapReq)
if err != nil {
return fmt.Errorf("encoding map request: %w", err)
}
nc, err := c.noiseClient(ctx)
if err != nil {
return fmt.Errorf("establishing noise connection: %w", err)
}
url := c.serverURL + "/machine/map"
url = strings.Replace(url, "http:", "https:", 1)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("creating map request: %w", err)
}
ts2021.AddLBHeader(req, opts.NodeKey.Public())
res, err := nc.Do(req)
if err != nil {
return fmt.Errorf("map request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := io.ReadAll(res.Body)
return fmt.Errorf("map request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
io.Copy(io.Discard, res.Body)
return nil
}
// Map sends a map request to the coordination server and returns a MapSession
// for reading the framed, zstd-compressed response(s).
func (c *Client) Map(ctx context.Context, opts MapOpts) (*MapSession, error) {