Move Linux client & common packages into a public repo.
This commit is contained in:
359
tailcfg/tailcfg.go
Normal file
359
tailcfg/tailcfg.go
Normal file
@ -0,0 +1,359 @@
|
||||
// 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)
|
||||
}
|
Reference in New Issue
Block a user