// 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) }