mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-04 19:56:35 +02:00
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>
106 lines
3.2 KiB
Go
106 lines
3.2 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package tsp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
|
|
"tailscale.com/types/key"
|
|
)
|
|
|
|
// ServerInfo identifies a coordination server by its URL and Noise public key.
|
|
type ServerInfo struct {
|
|
// URL is the base URL of the coordination server, without any path
|
|
// (e.g. "https://controlplane.tailscale.com").
|
|
//
|
|
// There is no default value; a URL must always be supplied.
|
|
URL string `json:"server_url"`
|
|
|
|
// Key is the server's Noise public key, used to establish an encrypted
|
|
// channel between the client and the coordination server.
|
|
Key key.MachinePublic `json:"server_key"`
|
|
}
|
|
|
|
// NodeFile is the JSON structure for a node credentials file. It contains
|
|
// the private keys that authenticate a node to a coordination server.
|
|
//
|
|
// Example:
|
|
//
|
|
// {
|
|
// "node_key": "privkey:...",
|
|
// "machine_key": "privkey:...",
|
|
// "server_url": "https://controlplane.tailscale.com",
|
|
// "server_key": "mkey:..."
|
|
// }
|
|
//
|
|
// Note that node and machine private keys share the same "privkey:"
|
|
// textual form; they are disambiguated by the surrounding JSON field
|
|
// names rather than by any prefix in the key itself.
|
|
type NodeFile struct {
|
|
// NodeKey is the node's WireGuard private key. The corresponding
|
|
// public key identifies this node to other peers.
|
|
NodeKey key.NodePrivate `json:"node_key"`
|
|
|
|
// MachineKey is the machine's private key. It authenticates this
|
|
// machine to the coordination server over Noise.
|
|
MachineKey key.MachinePrivate `json:"machine_key"`
|
|
|
|
ServerInfo // server_url and server_key
|
|
}
|
|
|
|
// ReadNodeFile reads and parses a node JSON file.
|
|
func ReadNodeFile(path string) (NodeFile, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return NodeFile{}, err
|
|
}
|
|
var nf NodeFile
|
|
if err := json.Unmarshal(data, &nf); err != nil {
|
|
return NodeFile{}, fmt.Errorf("parsing node file %q: %w", path, err)
|
|
}
|
|
return nf, nil
|
|
}
|
|
|
|
// WriteNodeFile writes a node JSON file. The file is created with mode 0600.
|
|
func WriteNodeFile(path string, nf NodeFile) error {
|
|
if err := nf.Check(); err != nil {
|
|
return fmt.Errorf("invalid NodeFile: %w", err)
|
|
}
|
|
return os.WriteFile(path, nf.AsJSON(), 0600)
|
|
}
|
|
|
|
// AsJSON returns nf as a pretty-printed JSON object, terminated by a newline.
|
|
//
|
|
// It always succeeds and always returns a valid JSON object. It does not
|
|
// validate that the fields of nf are non-zero; it is the caller's
|
|
// responsibility to call [NodeFile.Check] first if they want to reject
|
|
// incomplete NodeFiles.
|
|
func (nf NodeFile) AsJSON() []byte {
|
|
out, err := json.MarshalIndent(nf, "", " ")
|
|
if err != nil {
|
|
panic(fmt.Sprintf("NodeFile.AsJSON: %v", err)) // unreachable: all fields marshal successfully
|
|
}
|
|
return append(out, '\n')
|
|
}
|
|
|
|
// Check reports whether nf has all required fields set.
|
|
// It returns an error describing the first zero-valued field, if any.
|
|
func (nf NodeFile) Check() error {
|
|
if nf.NodeKey.IsZero() {
|
|
return fmt.Errorf("node_key is missing")
|
|
}
|
|
if nf.MachineKey.IsZero() {
|
|
return fmt.Errorf("machine_key is missing")
|
|
}
|
|
if nf.URL == "" {
|
|
return fmt.Errorf("server_url is missing")
|
|
}
|
|
if nf.ServerInfo.Key.IsZero() {
|
|
return fmt.Errorf("server_key is missing")
|
|
}
|
|
return nil
|
|
}
|