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.
tailscale/tailcfg/tailcfg.go

360 lines
9.3 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"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/oauth2"
"tailscale.com/wgengine/filter"
)
type ID int64
type UserID ID
type LoginID ID
type NodeID ID
type GroupID ID
type RoleID ID
type CapabilityID ID
type MachineKey [32]byte
type NodeKey [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
}
type Login struct {
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 // for display purposes only (provider is not listed)
DisplayName string
ProfilePicURL string
Roles []RoleID
}
type Node struct {
ID NodeID
Name string // DNS
User UserID
Key NodeKey
KeyExpiry time.Time
Machine MachineKey
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)
Hostinfo Hostinfo
Created time.Time
LastSeen *time.Time `json:",omitempty"`
MachineAuthorized bool // TODO(crawshaw): replace with MachineStatus
// NOTE: any new fields containing pointers in this type
// require changes to Node.Copy.
}
// Copy makes a deep copy of Node.
// The result aliases no memory with the original.
func (n *Node) Copy() (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.Copy()
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))
}
}
type ServiceProto string
const (
TCP = ServiceProto("tcp")
UDP = ServiceProto("udp")
)
type Service struct {
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.Copy.
}
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 number 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
Hostname string // name of the host the client runs on
RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route
Services []Service `json:",omitempty"` // services advertised by this machine
// NOTE: any new fields containing pointers in this type
// require changes to Hostinfo.Copy.
}
// Copy makes a deep copy of Hostinfo.
// The result aliases no memory with the original.
func (hinfo *Hostinfo) Copy() (res *Hostinfo) {
res = new(Hostinfo)
*res = *hinfo
res.RoutableIPs = append([]wgcfg.CIDR{}, res.RoutableIPs...)
res.Services = append([]Service{}, res.Services...)
return res
}
type RegisterRequest struct {
Version int
NodeKey NodeKey
OldNodeKey NodeKey
Auth struct {
Provider string
LoginName string
// One of LoginName or Oauth2Token is set.
Oauth2Token *oauth2.Token
}
Expiry time.Time // requested key expiry, server policy may override
Followup string // response waits until AuthURL is visited
Hostinfo Hostinfo
}
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
}
type MapRequest struct {
Version int // current version is 4
Compress string // "zstd" or "" (no compression)
KeepAlive bool // server sends keep-alives
NodeKey NodeKey
Endpoints []string
Stream bool
Hostinfo Hostinfo
}
type MapResponse struct {
KeepAlive bool // if set, all other fields are ignored
// Networking
Node Node
Peers []Node
DNS []wgcfg.IP
SearchPaths []string
// ACLs
Domain string
PacketFilter filter.Matches
UserProfiles []UserProfile
Roles []Role
// TODO: Groups []Group
// TODO: Capabilities []Capability
}
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
func (k MachineKey) MarshalText() ([]byte, error) {
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "mkey:%x", k[:])
return buf.Bytes(), nil
}
func (k *MachineKey) UnmarshalText(text []byte) error {
s := string(text)
if !strings.HasPrefix(s, "mkey:") {
return errors.New(`MachineKey.UnmarshalText: missing prefix`)
}
s = strings.TrimPrefix(s, `mkey:`)
key, err := wgcfg.ParseHexKey(s)
if err != nil {
return fmt.Errorf("MachineKey.UnmarhsalText: %v", err)
}
copy(k[:], key[:])
return nil
}
func (k NodeKey) String() string { return fmt.Sprintf("nodekey:%x", k[:]) }
func (k NodeKey) AbbrevString() string {
pk := wgcfg.Key(k)
return pk.ShortString()
}
func (k NodeKey) MarshalText() ([]byte, error) {
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "nodekey:%x", k[:])
return buf.Bytes(), nil
}
func (k *NodeKey) UnmarshalText(text []byte) error {
s := string(text)
if !strings.HasPrefix(s, "nodekey:") {
return errors.New(`Nodekey.UnmarshalText: missing prefix`)
}
s = strings.TrimPrefix(s, "nodekey:")
key, err := wgcfg.ParseHexKey(s)
if err != nil {
return fmt.Errorf("tailcfg.Ukey.UnmarhsalText: %v", err)
}
copy(k[:], key[:])
return nil
}
func (k *NodeKey) IsZero() bool {
z := NodeKey{}
return bytes.Equal(k[:], z[:])
}
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)) }
func (n *Node) Equal(n2 *Node) bool {
// TODO(crawshaw): this is crude, but is an easy way to avoid bugs.
b, err := json.Marshal(n)
if err != nil {
panic(err)
}
b2, err := json.Marshal(n2)
if err != nil {
panic(err)
}
return bytes.Equal(b, b2)
}