mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
603 lines
18 KiB
Go
603 lines
18 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package tailcfg
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tailscale/wireguard-go/wgcfg"
|
|
"go4.org/mem"
|
|
"golang.org/x/oauth2"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/types/opt"
|
|
"tailscale.com/types/structs"
|
|
)
|
|
|
|
type ID int64
|
|
|
|
type UserID ID
|
|
|
|
type LoginID ID
|
|
|
|
type NodeID ID
|
|
|
|
type GroupID ID
|
|
|
|
type RoleID ID
|
|
|
|
type CapabilityID ID
|
|
|
|
// MachineKey is the curve25519 public key for a machine.
|
|
type MachineKey [32]byte
|
|
|
|
// NodeKey is the curve25519 public key for a node.
|
|
type NodeKey [32]byte
|
|
|
|
// DiscoKey is the curve25519 public key for path discovery key.
|
|
// It's never written to disk or reused between network start-ups.
|
|
type DiscoKey [32]byte
|
|
|
|
type Group struct {
|
|
ID GroupID
|
|
Name string
|
|
Members []ID
|
|
}
|
|
|
|
type Role struct {
|
|
ID RoleID
|
|
Name string
|
|
Capabilities []CapabilityID
|
|
}
|
|
|
|
type CapType string
|
|
|
|
const (
|
|
CapRead = CapType("read")
|
|
CapWrite = CapType("write")
|
|
)
|
|
|
|
type Capability struct {
|
|
ID CapabilityID
|
|
Type CapType
|
|
Val ID
|
|
}
|
|
|
|
// User is an IPN user.
|
|
//
|
|
// A user can have multiple logins associated with it (e.g. gmail and github oauth).
|
|
// (Note: none of our UIs support this yet.)
|
|
//
|
|
// Some properties are inhereted from the logins and can be overridden, such as
|
|
// display name and profile picture.
|
|
//
|
|
// Other properties must be the same for all logins associated with a user.
|
|
// In particular: domain. If a user has a "tailscale.io" domain login, they cannot
|
|
// have a general gmail address login associated with the user.
|
|
type User struct {
|
|
ID UserID
|
|
LoginName string `json:"-"` // not stored, filled from Login // TODO REMOVE
|
|
DisplayName string // if non-empty overrides Login field
|
|
ProfilePicURL string // if non-empty overrides Login field
|
|
Domain string
|
|
Logins []LoginID
|
|
Roles []RoleID
|
|
Created time.Time
|
|
|
|
// Note: be sure to update Clone when adding new fields
|
|
}
|
|
|
|
// Clone returns a copy of u that aliases no memory with the original.
|
|
func (u *User) Clone() *User {
|
|
if u == nil {
|
|
return nil
|
|
}
|
|
u2 := new(User)
|
|
*u2 = *u
|
|
u2.Logins = append([]LoginID(nil), u.Logins...)
|
|
u2.Roles = append([]RoleID(nil), u.Roles...)
|
|
return u2
|
|
}
|
|
|
|
type Login struct {
|
|
_ structs.Incomparable
|
|
ID LoginID
|
|
Provider string
|
|
LoginName string
|
|
DisplayName string
|
|
ProfilePicURL string
|
|
Domain string
|
|
}
|
|
|
|
// A UserProfile is display-friendly data for a user.
|
|
// It includes the LoginName for display purposes but *not* the Provider.
|
|
// It also includes derived data from one of the user's logins.
|
|
type UserProfile struct {
|
|
ID UserID
|
|
LoginName string // "alice@smith.com"; for display purposes only (provider is not listed)
|
|
DisplayName string // "Alice Smith"
|
|
ProfilePicURL string
|
|
Roles []RoleID
|
|
}
|
|
|
|
type Node struct {
|
|
ID NodeID
|
|
Name string // DNS
|
|
User UserID
|
|
Key NodeKey
|
|
KeyExpiry time.Time
|
|
Machine MachineKey
|
|
DiscoKey DiscoKey
|
|
Addresses []wgcfg.CIDR // IP addresses of this Node directly
|
|
AllowedIPs []wgcfg.CIDR // range of IP addresses to route to this node
|
|
Endpoints []string `json:",omitempty"` // IP+port (public via STUN, and local LANs)
|
|
DERP string `json:",omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
|
|
Hostinfo Hostinfo
|
|
Created time.Time
|
|
LastSeen *time.Time `json:",omitempty"`
|
|
|
|
KeepAlive bool // open and keep open a connection to this peer
|
|
|
|
MachineAuthorized bool // TODO(crawshaw): replace with MachineStatus
|
|
|
|
// NOTE: any new fields containing pointers in this type
|
|
// require changes to Node.Clone.
|
|
}
|
|
|
|
// Clone makes a deep copy of Node.
|
|
// The result aliases no memory with the original.
|
|
func (n *Node) Clone() (res *Node) {
|
|
res = new(Node)
|
|
*res = *n
|
|
|
|
res.Addresses = append([]wgcfg.CIDR{}, res.Addresses...)
|
|
res.AllowedIPs = append([]wgcfg.CIDR{}, res.AllowedIPs...)
|
|
res.Endpoints = append([]string{}, res.Endpoints...)
|
|
if res.LastSeen != nil {
|
|
lastSeen := *res.LastSeen
|
|
res.LastSeen = &lastSeen
|
|
}
|
|
res.Hostinfo = *res.Hostinfo.Clone()
|
|
return res
|
|
}
|
|
|
|
type MachineStatus int
|
|
|
|
const (
|
|
MachineUnknown = MachineStatus(iota)
|
|
MachineUnauthorized // server has yet to approve
|
|
MachineAuthorized // server has approved
|
|
MachineInvalid // server has explicitly rejected this machine key
|
|
)
|
|
|
|
func (m MachineStatus) MarshalText() ([]byte, error) {
|
|
return []byte(m.String()), nil
|
|
}
|
|
|
|
func (m *MachineStatus) UnmarshalText(b []byte) error {
|
|
switch string(b) {
|
|
case "machine-unknown":
|
|
*m = MachineUnknown
|
|
case "machine-unauthorized":
|
|
*m = MachineUnauthorized
|
|
case "machine-authorized":
|
|
*m = MachineAuthorized
|
|
case "machine-invalid":
|
|
*m = MachineInvalid
|
|
default:
|
|
var val int
|
|
if _, err := fmt.Sscanf(string(b), "machine-unknown(%d)", &val); err != nil {
|
|
*m = MachineStatus(val)
|
|
} else {
|
|
*m = MachineUnknown
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m MachineStatus) String() string {
|
|
switch m {
|
|
case MachineUnknown:
|
|
return "machine-unknown"
|
|
case MachineUnauthorized:
|
|
return "machine-unauthorized"
|
|
case MachineAuthorized:
|
|
return "machine-authorized"
|
|
case MachineInvalid:
|
|
return "machine-invalid"
|
|
default:
|
|
return fmt.Sprintf("machine-unknown(%d)", int(m))
|
|
}
|
|
}
|
|
|
|
func isNum(b byte) bool {
|
|
return b >= '0' && b <= '9'
|
|
}
|
|
|
|
func isAlpha(b byte) bool {
|
|
return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
|
|
}
|
|
|
|
// CheckTag valids whether a given string can be used as an ACL tag.
|
|
// For now we allow only ascii alphanumeric tags, and they need to start
|
|
// with a letter. No unicode shenanigans allowed, and we reserve punctuation
|
|
// marks other than '-' for a possible future URI scheme.
|
|
//
|
|
// Because we're ignoring unicode entirely, we can treat utf-8 as a series of
|
|
// bytes. Anything >= 128 is disqualified anyway.
|
|
//
|
|
// We might relax these rules later.
|
|
func CheckTag(tag string) error {
|
|
if !strings.HasPrefix(tag, "tag:") {
|
|
return errors.New("tags must start with 'tag:'")
|
|
}
|
|
tag = tag[4:]
|
|
if tag == "" {
|
|
return errors.New("tag names must not be empty")
|
|
}
|
|
if !isAlpha(tag[0]) {
|
|
return errors.New("tag names must start with a letter, after 'tag:'")
|
|
}
|
|
|
|
for _, b := range []byte(tag) {
|
|
if !isNum(b) && !isAlpha(b) && b != '-' {
|
|
return errors.New("tag names can only contain numbers, letters, or dashes")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type ServiceProto string
|
|
|
|
const (
|
|
TCP = ServiceProto("tcp")
|
|
UDP = ServiceProto("udp")
|
|
)
|
|
|
|
type Service struct {
|
|
_ structs.Incomparable
|
|
Proto ServiceProto // TCP or UDP
|
|
Port uint16 // port number service is listening on
|
|
Description string // text description of service
|
|
// TODO(apenwarr): allow advertising services on subnet IPs?
|
|
// TODO(apenwarr): add "tags" here for each service?
|
|
|
|
// NOTE: any new fields containing pointers in this type
|
|
// require changes to Hostinfo.Clone.
|
|
}
|
|
|
|
// Hostinfo contains a summary of a Tailscale host.
|
|
//
|
|
// Because it contains pointers (slices), this type should not be used
|
|
// as a value type.
|
|
type Hostinfo struct {
|
|
// TODO(crawshaw): mark all these fields ",omitempty" when all the
|
|
// iOS apps are updated with the latest swift version of this struct.
|
|
IPNVersion string // version of this code
|
|
FrontendLogID string // logtail ID of frontend instance
|
|
BackendLogID string // logtail ID of backend instance
|
|
OS string // operating system the client runs on (a version.OS value)
|
|
Hostname string // name of the host the client runs on
|
|
RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route
|
|
RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim
|
|
Services []Service `json:",omitempty"` // services advertised by this machine
|
|
NetInfo *NetInfo `json:",omitempty"`
|
|
|
|
// NOTE: any new fields containing pointers in this type
|
|
// require changes to Hostinfo.Clone and Hostinfo.Equal.
|
|
}
|
|
|
|
// NetInfo contains information about the host's network state.
|
|
type NetInfo struct {
|
|
// MappingVariesByDestIP says whether the host's NAT mappings
|
|
// vary based on the destination IP.
|
|
MappingVariesByDestIP opt.Bool
|
|
|
|
// HairPinning is their router does hairpinning.
|
|
// It reports true even if there's no NAT involved.
|
|
HairPinning opt.Bool
|
|
|
|
// WorkingIPv6 is whether IPv6 works.
|
|
WorkingIPv6 opt.Bool
|
|
|
|
// WorkingUDP is whether UDP works.
|
|
WorkingUDP opt.Bool
|
|
|
|
// PreferredDERP is this node's preferred DERP server
|
|
// for incoming traffic. The node might be be temporarily
|
|
// connected to multiple DERP servers (to send to other nodes)
|
|
// but PreferredDERP is the instance number that the node
|
|
// subscribes to traffic at.
|
|
// Zero means disconnected or unknown.
|
|
PreferredDERP int
|
|
|
|
// LinkType is the current link type, if known.
|
|
LinkType string // "wired", "wifi", "mobile" (LTE, 4G, 3G, etc)
|
|
|
|
// DERPLatency is the fastest recent time to reach various
|
|
// DERP STUN servers, in seconds. The map key is the
|
|
// "regionID-v4" or "-v6"; it was previously the DERP server's
|
|
// STUN host:port.
|
|
//
|
|
// This should only be updated rarely, or when there's a
|
|
// material change, as any change here also gets uploaded to
|
|
// the control plane.
|
|
DERPLatency map[string]float64 `json:",omitempty"`
|
|
|
|
// Update Clone and BasicallyEqual when adding fields.
|
|
}
|
|
|
|
func (ni *NetInfo) String() string {
|
|
if ni == nil {
|
|
return "NetInfo(nil)"
|
|
}
|
|
return fmt.Sprintf("NetInfo{varies=%v hairpin=%v ipv6=%v udp=%v derp=#%v link=%q}",
|
|
ni.MappingVariesByDestIP, ni.HairPinning, ni.WorkingIPv6,
|
|
ni.WorkingUDP, ni.PreferredDERP, ni.LinkType)
|
|
}
|
|
|
|
// BasicallyEqual reports whether ni and ni2 are basically equal, ignoring
|
|
// changes in DERP ServerLatency & RegionLatency.
|
|
func (ni *NetInfo) BasicallyEqual(ni2 *NetInfo) bool {
|
|
if (ni == nil) != (ni2 == nil) {
|
|
return false
|
|
}
|
|
if ni == nil {
|
|
return true
|
|
}
|
|
return ni.MappingVariesByDestIP == ni2.MappingVariesByDestIP &&
|
|
ni.HairPinning == ni2.HairPinning &&
|
|
ni.WorkingIPv6 == ni2.WorkingIPv6 &&
|
|
ni.WorkingUDP == ni2.WorkingUDP &&
|
|
ni.PreferredDERP == ni2.PreferredDERP &&
|
|
ni.LinkType == ni2.LinkType
|
|
}
|
|
|
|
func (ni *NetInfo) Clone() (res *NetInfo) {
|
|
if ni == nil {
|
|
return nil
|
|
}
|
|
res = new(NetInfo)
|
|
*res = *ni
|
|
if ni.DERPLatency != nil {
|
|
res.DERPLatency = map[string]float64{}
|
|
for k, v := range ni.DERPLatency {
|
|
res.DERPLatency[k] = v
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// Clone makes a deep copy of Hostinfo.
|
|
// The result aliases no memory with the original.
|
|
func (h *Hostinfo) Clone() (res *Hostinfo) {
|
|
res = new(Hostinfo)
|
|
*res = *h
|
|
|
|
res.RoutableIPs = append([]wgcfg.CIDR{}, h.RoutableIPs...)
|
|
res.Services = append([]Service{}, h.Services...)
|
|
res.NetInfo = h.NetInfo.Clone()
|
|
return res
|
|
}
|
|
|
|
// Equal reports whether h and h2 are equal.
|
|
func (h *Hostinfo) Equal(h2 *Hostinfo) bool {
|
|
return reflect.DeepEqual(h, h2)
|
|
}
|
|
|
|
// RegisterRequest is sent by a client to register the key for a node.
|
|
// It is encoded to JSON, encrypted with golang.org/x/crypto/nacl/box,
|
|
// using the local machine key, and sent to:
|
|
// https://login.tailscale.com/machine/<mkey hex>
|
|
type RegisterRequest struct {
|
|
_ structs.Incomparable
|
|
Version int // currently 1
|
|
NodeKey NodeKey
|
|
OldNodeKey NodeKey
|
|
Auth struct {
|
|
_ structs.Incomparable
|
|
// One of Provider/LoginName, Oauth2Token, or AuthKey is set.
|
|
Provider, LoginName string
|
|
Oauth2Token *oauth2.Token
|
|
AuthKey string
|
|
}
|
|
Expiry time.Time // requested key expiry, server policy may override
|
|
Followup string // response waits until AuthURL is visited
|
|
Hostinfo *Hostinfo
|
|
}
|
|
|
|
// Clone makes a deep copy of RegisterRequest.
|
|
// The result aliases no memory with the original.
|
|
func (req *RegisterRequest) Clone() *RegisterRequest {
|
|
res := new(RegisterRequest)
|
|
*res = *req
|
|
if res.Hostinfo != nil {
|
|
res.Hostinfo = res.Hostinfo.Clone()
|
|
}
|
|
if res.Auth.Oauth2Token != nil {
|
|
tok := *res.Auth.Oauth2Token
|
|
res.Auth.Oauth2Token = &tok
|
|
}
|
|
return res
|
|
}
|
|
|
|
// RegisterResponse is returned by the server in response to a RegisterRequest.
|
|
type RegisterResponse struct {
|
|
User User
|
|
Login Login
|
|
NodeKeyExpired bool // if true, the NodeKey needs to be replaced
|
|
MachineAuthorized bool // TODO(crawshaw): move to using MachineStatus
|
|
AuthURL string // if set, authorization pending
|
|
}
|
|
|
|
// MapRequest is sent by a client to start a long-poll network map updates.
|
|
// The request includes a copy of the client's current set of WireGuard
|
|
// endpoints and general host information.
|
|
//
|
|
// The request is encoded to JSON, encrypted with golang.org/x/crypto/nacl/box,
|
|
// using the local machine key, and sent to:
|
|
// https://login.tailscale.com/machine/<mkey hex>/map
|
|
type MapRequest struct {
|
|
Version int // current version is 4
|
|
Compress string // "zstd" or "" (no compression)
|
|
KeepAlive bool // server sends keep-alives
|
|
NodeKey NodeKey
|
|
DiscoKey DiscoKey
|
|
Endpoints []string // caller's endpoints (IPv4 or IPv6)
|
|
IncludeIPv6 bool // include IPv6 endpoints in returned Node Endpoints
|
|
Stream bool // if true, multiple MapResponse objects are returned
|
|
Hostinfo *Hostinfo
|
|
}
|
|
|
|
// PortRange represents a range of UDP or TCP port numbers.
|
|
type PortRange struct {
|
|
First uint16
|
|
Last uint16
|
|
}
|
|
|
|
var PortRangeAny = PortRange{0, 65535}
|
|
|
|
// NetPortRange represents a single subnet:portrange.
|
|
type NetPortRange struct {
|
|
_ structs.Incomparable
|
|
IP string
|
|
Bits *int // backward compatibility: if missing, means "all" bits
|
|
Ports PortRange
|
|
}
|
|
|
|
// FilterRule represents one rule in a packet filter.
|
|
type FilterRule struct {
|
|
SrcIPs []string
|
|
SrcBits []int
|
|
DstPorts []NetPortRange
|
|
}
|
|
|
|
var FilterAllowAll = []FilterRule{
|
|
FilterRule{
|
|
SrcIPs: []string{"*"},
|
|
SrcBits: nil,
|
|
DstPorts: []NetPortRange{NetPortRange{
|
|
IP: "*",
|
|
Bits: nil,
|
|
Ports: PortRange{0, 65535},
|
|
}},
|
|
},
|
|
}
|
|
|
|
type MapResponse struct {
|
|
KeepAlive bool // if set, all other fields are ignored
|
|
|
|
// Networking
|
|
Node *Node
|
|
Peers []*Node
|
|
DNS []wgcfg.IP
|
|
SearchPaths []string
|
|
DERPMap *DERPMap
|
|
|
|
// ACLs
|
|
Domain string
|
|
PacketFilter []FilterRule
|
|
UserProfiles []UserProfile
|
|
Roles []Role
|
|
// TODO: Groups []Group
|
|
// TODO: Capabilities []Capability
|
|
|
|
// Debug is normally nil, except for when the control server
|
|
// is setting debug settings on a node.
|
|
Debug *Debug `json:",omitempty"`
|
|
}
|
|
|
|
// Debug are instructions from the control server to the client
|
|
// to adjust debug settings.
|
|
type Debug struct {
|
|
// LogHeapPprof controls whether the client should log
|
|
// its heap pprof data. Each true value sent from the server
|
|
// means that client should do one more log.
|
|
LogHeapPprof bool `json:",omitempty"`
|
|
|
|
// LogHeapURL is the URL to POST its heap pprof to.
|
|
// Empty means to not log.
|
|
LogHeapURL string `json:",omitempty"`
|
|
|
|
// ForceBackgroundSTUN controls whether magicsock should
|
|
// always do its background STUN queries (see magicsock's
|
|
// periodicReSTUN), regardless of inactivity.
|
|
ForceBackgroundSTUN bool `json:",omitempty"`
|
|
}
|
|
|
|
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
|
|
func (k MachineKey) MarshalText() ([]byte, error) { return keyMarshalText("mkey:", k), nil }
|
|
func (k *MachineKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "mkey:", text) }
|
|
|
|
func keyMarshalText(prefix string, k [32]byte) []byte {
|
|
buf := bytes.NewBuffer(make([]byte, 0, len(prefix)+64))
|
|
fmt.Fprintf(buf, "%s%x", prefix, k[:])
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func keyUnmarshalText(dst []byte, prefix string, text []byte) error {
|
|
if len(text) < len(prefix) || string(text[:len(prefix)]) != prefix {
|
|
return fmt.Errorf("UnmarshalText: missing %q prefix", prefix)
|
|
}
|
|
pub, err := key.NewPublicFromHexMem(mem.B(text[len(prefix):]))
|
|
if err != nil {
|
|
return fmt.Errorf("UnmarshalText: after %q: %v", prefix, err)
|
|
}
|
|
copy(dst[:], pub[:])
|
|
return nil
|
|
}
|
|
|
|
func (k NodeKey) ShortString() string { return (key.Public(k)).ShortString() }
|
|
|
|
func (k NodeKey) String() string { return fmt.Sprintf("nodekey:%x", k[:]) }
|
|
func (k NodeKey) MarshalText() ([]byte, error) { return keyMarshalText("nodekey:", k), nil }
|
|
func (k *NodeKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "nodekey:", text) }
|
|
|
|
// IsZero reports whether k is the zero value.
|
|
func (k NodeKey) IsZero() bool { return k == NodeKey{} }
|
|
|
|
func (k DiscoKey) String() string { return fmt.Sprintf("discokey:%x", k[:]) }
|
|
func (k DiscoKey) MarshalText() ([]byte, error) { return keyMarshalText("discokey:", k), nil }
|
|
func (k *DiscoKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "discokey:", text) }
|
|
func (k DiscoKey) ShortString() string { return fmt.Sprintf("d:%x", k[:8]) }
|
|
|
|
// IsZero reports whether k is the zero value.
|
|
func (k DiscoKey) IsZero() bool { return k == DiscoKey{} }
|
|
|
|
func (id ID) String() string { return fmt.Sprintf("id:%x", int64(id)) }
|
|
func (id UserID) String() string { return fmt.Sprintf("userid:%x", int64(id)) }
|
|
func (id LoginID) String() string { return fmt.Sprintf("loginid:%x", int64(id)) }
|
|
func (id NodeID) String() string { return fmt.Sprintf("nodeid:%x", int64(id)) }
|
|
func (id GroupID) String() string { return fmt.Sprintf("groupid:%x", int64(id)) }
|
|
func (id RoleID) String() string { return fmt.Sprintf("roleid:%x", int64(id)) }
|
|
func (id CapabilityID) String() string { return fmt.Sprintf("capid:%x", int64(id)) }
|
|
|
|
// Equal reports whether n and n2 are equal.
|
|
func (n *Node) Equal(n2 *Node) bool {
|
|
if n == nil && n2 == nil {
|
|
return true
|
|
}
|
|
return n != nil && n2 != nil &&
|
|
n.ID == n2.ID &&
|
|
n.Name == n2.Name &&
|
|
n.User == n2.User &&
|
|
n.Key == n2.Key &&
|
|
n.KeyExpiry.Equal(n2.KeyExpiry) &&
|
|
n.Machine == n2.Machine &&
|
|
n.DiscoKey == n2.DiscoKey &&
|
|
reflect.DeepEqual(n.Addresses, n2.Addresses) &&
|
|
reflect.DeepEqual(n.AllowedIPs, n2.AllowedIPs) &&
|
|
reflect.DeepEqual(n.Endpoints, n2.Endpoints) &&
|
|
reflect.DeepEqual(n.Hostinfo, n2.Hostinfo) &&
|
|
n.Created.Equal(n2.Created) &&
|
|
reflect.DeepEqual(n.LastSeen, n2.LastSeen) &&
|
|
n.MachineAuthorized == n2.MachineAuthorized
|
|
}
|