Rename Machine to Node (#1553)

This commit is contained in:
Juan Font 2023-09-24 13:42:05 +02:00 committed by GitHub
parent 096ac31bb3
commit 0030af3fa4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 5222 additions and 5238 deletions

View file

@ -28,7 +28,7 @@ func (hi *HostInfo) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), hi)
default:
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
return fmt.Errorf("%w: unexpected data type %T", ErrNodeAddressesInvalid, destination)
}
}
@ -74,7 +74,7 @@ func (i *IPPrefixes) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), i)
default:
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
return fmt.Errorf("%w: unexpected data type %T", ErrNodeAddressesInvalid, destination)
}
}
@ -96,7 +96,7 @@ func (i *StringList) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), i)
default:
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
return fmt.Errorf("%w: unexpected data type %T", ErrNodeAddressesInvalid, destination)
}
}
@ -123,8 +123,8 @@ type StateUpdate struct {
Type StateUpdateType
// Changed must be set when Type is StatePeerChanged and
// contain the Machine IDs of machines that has changed.
Changed Machines
// contain the Node IDs of nodes that have changed.
Changed Nodes
// Removed must be set when Type is StatePeerRemoved and
// contain a list of the nodes that has been removed from

View file

@ -1,346 +0,0 @@
package types
import (
"database/sql/driver"
"errors"
"fmt"
"net/netip"
"sort"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/policy/matcher"
"github.com/juanfont/headscale/hscontrol/util"
"go4.org/netipx"
"google.golang.org/protobuf/types/known/timestamppb"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
var (
ErrMachineAddressesInvalid = errors.New("failed to parse machine addresses")
ErrHostnameTooLong = errors.New("hostname too long")
)
// Machine is a Headscale client.
type Machine struct {
ID uint64 `gorm:"primary_key"`
MachineKey string `gorm:"type:varchar(64);unique_index"`
NodeKey string
DiscoKey string
IPAddresses MachineAddresses
// Hostname represents the name given by the Tailscale
// client during registration
Hostname string
// Givenname represents either:
// a DNS normalized version of Hostname
// a valid name set by the User
//
// GivenName is the name used in all DNS related
// parts of headscale.
GivenName string `gorm:"type:varchar(63);unique_index"`
UserID uint
User User `gorm:"foreignKey:UserID"`
RegisterMethod string
ForcedTags StringList
// TODO(kradalby): This seems like irrelevant information?
AuthKeyID uint
AuthKey *PreAuthKey
LastSeen *time.Time
Expiry *time.Time
HostInfo HostInfo
Endpoints StringList
Routes []Route
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
type (
Machines []*Machine
)
type MachineAddresses []netip.Addr
func (ma MachineAddresses) Sort() {
sort.Slice(ma, func(index1, index2 int) bool {
if ma[index1].Is4() && ma[index2].Is6() {
return true
}
if ma[index1].Is6() && ma[index2].Is4() {
return false
}
return ma[index1].Compare(ma[index2]) < 0
})
}
func (ma MachineAddresses) StringSlice() []string {
ma.Sort()
strSlice := make([]string, 0, len(ma))
for _, addr := range ma {
strSlice = append(strSlice, addr.String())
}
return strSlice
}
func (ma MachineAddresses) Prefixes() []netip.Prefix {
addrs := []netip.Prefix{}
for _, machineAddress := range ma {
ip := netip.PrefixFrom(machineAddress, machineAddress.BitLen())
addrs = append(addrs, ip)
}
return addrs
}
func (ma MachineAddresses) InIPSet(set *netipx.IPSet) bool {
for _, machineAddr := range ma {
if set.Contains(machineAddr) {
return true
}
}
return false
}
// AppendToIPSet adds the individual ips in MachineAddresses to a
// given netipx.IPSetBuilder.
func (ma MachineAddresses) AppendToIPSet(build *netipx.IPSetBuilder) {
for _, ip := range ma {
build.Add(ip)
}
}
func (ma *MachineAddresses) Scan(destination interface{}) error {
switch value := destination.(type) {
case string:
addresses := strings.Split(value, ",")
*ma = (*ma)[:0]
for _, addr := range addresses {
if len(addr) < 1 {
continue
}
parsed, err := netip.ParseAddr(addr)
if err != nil {
return err
}
*ma = append(*ma, parsed)
}
return nil
default:
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (ma MachineAddresses) Value() (driver.Value, error) {
addresses := strings.Join(ma.StringSlice(), ",")
return addresses, nil
}
// IsExpired returns whether the machine registration has expired.
func (machine Machine) IsExpired() bool {
// If Expiry is not set, the client has not indicated that
// it wants an expiry time, it is therefor considered
// to mean "not expired"
if machine.Expiry == nil || machine.Expiry.IsZero() {
return false
}
return time.Now().UTC().After(*machine.Expiry)
}
// IsOnline returns if the machine is connected to Headscale.
// This is really a naive implementation, as we don't really see
// if there is a working connection between the client and the server.
func (machine *Machine) IsOnline() bool {
if machine.LastSeen == nil {
return false
}
if machine.IsExpired() {
return false
}
return machine.LastSeen.After(time.Now().Add(-KeepAliveInterval))
}
// IsEphemeral returns if the machine is registered as an Ephemeral node.
// https://tailscale.com/kb/1111/ephemeral-nodes/
func (machine *Machine) IsEphemeral() bool {
return machine.AuthKey != nil && machine.AuthKey.Ephemeral
}
func (machine *Machine) CanAccess(filter []tailcfg.FilterRule, machine2 *Machine) bool {
for _, rule := range filter {
// TODO(kradalby): Cache or pregen this
matcher := matcher.MatchFromFilterRule(rule)
if !matcher.SrcsContainsIPs([]netip.Addr(machine.IPAddresses)) {
continue
}
if matcher.DestsContainsIP([]netip.Addr(machine2.IPAddresses)) {
return true
}
}
return false
}
func (machines Machines) FilterByIP(ip netip.Addr) Machines {
found := make(Machines, 0)
for _, machine := range machines {
for _, mIP := range machine.IPAddresses {
if ip == mIP {
found = append(found, machine)
}
}
}
return found
}
func (machine *Machine) Proto() *v1.Machine {
machineProto := &v1.Machine{
Id: machine.ID,
MachineKey: machine.MachineKey,
NodeKey: machine.NodeKey,
DiscoKey: machine.DiscoKey,
IpAddresses: machine.IPAddresses.StringSlice(),
Name: machine.Hostname,
GivenName: machine.GivenName,
User: machine.User.Proto(),
ForcedTags: machine.ForcedTags,
Online: machine.IsOnline(),
// TODO(kradalby): Implement register method enum converter
// RegisterMethod: ,
CreatedAt: timestamppb.New(machine.CreatedAt),
}
if machine.AuthKey != nil {
machineProto.PreAuthKey = machine.AuthKey.Proto()
}
if machine.LastSeen != nil {
machineProto.LastSeen = timestamppb.New(*machine.LastSeen)
}
if machine.Expiry != nil {
machineProto.Expiry = timestamppb.New(*machine.Expiry)
}
return machineProto
}
// GetHostInfo returns a Hostinfo struct for the machine.
func (machine *Machine) GetHostInfo() tailcfg.Hostinfo {
return tailcfg.Hostinfo(machine.HostInfo)
}
func (machine *Machine) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (string, error) {
var hostname string
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS
hostname = fmt.Sprintf(
"%s.%s.%s",
machine.GivenName,
machine.User.Name,
baseDomain,
)
if len(hostname) > MaxHostnameLength {
return "", fmt.Errorf(
"hostname %q is too long it cannot except 255 ASCII chars: %w",
hostname,
ErrHostnameTooLong,
)
}
} else {
hostname = machine.GivenName
}
return hostname, nil
}
func (machine *Machine) MachinePublicKey() (key.MachinePublic, error) {
var machineKey key.MachinePublic
if machine.MachineKey != "" {
err := machineKey.UnmarshalText(
[]byte(util.MachinePublicKeyEnsurePrefix(machine.MachineKey)),
)
if err != nil {
return key.MachinePublic{}, fmt.Errorf("failed to parse machine public key: %w", err)
}
}
return machineKey, nil
}
func (machine *Machine) DiscoPublicKey() (key.DiscoPublic, error) {
var discoKey key.DiscoPublic
if machine.DiscoKey != "" {
err := discoKey.UnmarshalText(
[]byte(util.DiscoPublicKeyEnsurePrefix(machine.DiscoKey)),
)
if err != nil {
return key.DiscoPublic{}, fmt.Errorf("failed to parse disco public key: %w", err)
}
} else {
discoKey = key.DiscoPublic{}
}
return discoKey, nil
}
func (machine *Machine) NodePublicKey() (key.NodePublic, error) {
var nodeKey key.NodePublic
err := nodeKey.UnmarshalText([]byte(util.NodePublicKeyEnsurePrefix(machine.NodeKey)))
if err != nil {
return key.NodePublic{}, fmt.Errorf("failed to parse node public key: %w", err)
}
return nodeKey, nil
}
func (machine Machine) String() string {
return machine.Hostname
}
func (machines Machines) String() string {
temp := make([]string, len(machines))
for index, machine := range machines {
temp[index] = machine.Hostname
}
return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp))
}
func (machines Machines) IDMap() map[uint64]*Machine {
ret := map[uint64]*Machine{}
for _, machine := range machines {
ret[machine.ID] = machine
}
return ret
}

346
hscontrol/types/node.go Normal file
View file

@ -0,0 +1,346 @@
package types
import (
"database/sql/driver"
"errors"
"fmt"
"net/netip"
"sort"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/policy/matcher"
"github.com/juanfont/headscale/hscontrol/util"
"go4.org/netipx"
"google.golang.org/protobuf/types/known/timestamppb"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
var (
ErrNodeAddressesInvalid = errors.New("failed to parse node addresses")
ErrHostnameTooLong = errors.New("hostname too long")
)
// Node is a Headscale client.
type Node struct {
ID uint64 `gorm:"primary_key"`
MachineKey string `gorm:"type:varchar(64);unique_index"`
NodeKey string
DiscoKey string
IPAddresses NodeAddresses
// Hostname represents the name given by the Tailscale
// client during registration
Hostname string
// Givenname represents either:
// a DNS normalized version of Hostname
// a valid name set by the User
//
// GivenName is the name used in all DNS related
// parts of headscale.
GivenName string `gorm:"type:varchar(63);unique_index"`
UserID uint
User User `gorm:"foreignKey:UserID"`
RegisterMethod string
ForcedTags StringList
// TODO(kradalby): This seems like irrelevant information?
AuthKeyID uint
AuthKey *PreAuthKey
LastSeen *time.Time
Expiry *time.Time
HostInfo HostInfo
Endpoints StringList
Routes []Route
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
type (
Nodes []*Node
)
type NodeAddresses []netip.Addr
func (na NodeAddresses) Sort() {
sort.Slice(na, func(index1, index2 int) bool {
if na[index1].Is4() && na[index2].Is6() {
return true
}
if na[index1].Is6() && na[index2].Is4() {
return false
}
return na[index1].Compare(na[index2]) < 0
})
}
func (na NodeAddresses) StringSlice() []string {
na.Sort()
strSlice := make([]string, 0, len(na))
for _, addr := range na {
strSlice = append(strSlice, addr.String())
}
return strSlice
}
func (na NodeAddresses) Prefixes() []netip.Prefix {
addrs := []netip.Prefix{}
for _, nodeAddress := range na {
ip := netip.PrefixFrom(nodeAddress, nodeAddress.BitLen())
addrs = append(addrs, ip)
}
return addrs
}
func (na NodeAddresses) InIPSet(set *netipx.IPSet) bool {
for _, nodeAddr := range na {
if set.Contains(nodeAddr) {
return true
}
}
return false
}
// AppendToIPSet adds the individual ips in NodeAddresses to a
// given netipx.IPSetBuilder.
func (na NodeAddresses) AppendToIPSet(build *netipx.IPSetBuilder) {
for _, ip := range na {
build.Add(ip)
}
}
func (na *NodeAddresses) Scan(destination interface{}) error {
switch value := destination.(type) {
case string:
addresses := strings.Split(value, ",")
*na = (*na)[:0]
for _, addr := range addresses {
if len(addr) < 1 {
continue
}
parsed, err := netip.ParseAddr(addr)
if err != nil {
return err
}
*na = append(*na, parsed)
}
return nil
default:
return fmt.Errorf("%w: unexpected data type %T", ErrNodeAddressesInvalid, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (na NodeAddresses) Value() (driver.Value, error) {
addresses := strings.Join(na.StringSlice(), ",")
return addresses, nil
}
// IsExpired returns whether the node registration has expired.
func (node Node) IsExpired() bool {
// If Expiry is not set, the client has not indicated that
// it wants an expiry time, it is therefor considered
// to mean "not expired"
if node.Expiry == nil || node.Expiry.IsZero() {
return false
}
return time.Now().UTC().After(*node.Expiry)
}
// IsOnline returns if the node is connected to Headscale.
// This is really a naive implementation, as we don't really see
// if there is a working connection between the client and the server.
func (node *Node) IsOnline() bool {
if node.LastSeen == nil {
return false
}
if node.IsExpired() {
return false
}
return node.LastSeen.After(time.Now().Add(-KeepAliveInterval))
}
// IsEphemeral returns if the node is registered as an Ephemeral node.
// https://tailscale.com/kb/1111/ephemeral-nodes/
func (node *Node) IsEphemeral() bool {
return node.AuthKey != nil && node.AuthKey.Ephemeral
}
func (node *Node) CanAccess(filter []tailcfg.FilterRule, node2 *Node) bool {
for _, rule := range filter {
// TODO(kradalby): Cache or pregen this
matcher := matcher.MatchFromFilterRule(rule)
if !matcher.SrcsContainsIPs([]netip.Addr(node.IPAddresses)) {
continue
}
if matcher.DestsContainsIP([]netip.Addr(node2.IPAddresses)) {
return true
}
}
return false
}
func (nodes Nodes) FilterByIP(ip netip.Addr) Nodes {
found := make(Nodes, 0)
for _, node := range nodes {
for _, mIP := range node.IPAddresses {
if ip == mIP {
found = append(found, node)
}
}
}
return found
}
func (node *Node) Proto() *v1.Node {
nodeProto := &v1.Node{
Id: node.ID,
MachineKey: node.MachineKey,
NodeKey: node.NodeKey,
DiscoKey: node.DiscoKey,
IpAddresses: node.IPAddresses.StringSlice(),
Name: node.Hostname,
GivenName: node.GivenName,
User: node.User.Proto(),
ForcedTags: node.ForcedTags,
Online: node.IsOnline(),
// TODO(kradalby): Implement register method enum converter
// RegisterMethod: ,
CreatedAt: timestamppb.New(node.CreatedAt),
}
if node.AuthKey != nil {
nodeProto.PreAuthKey = node.AuthKey.Proto()
}
if node.LastSeen != nil {
nodeProto.LastSeen = timestamppb.New(*node.LastSeen)
}
if node.Expiry != nil {
nodeProto.Expiry = timestamppb.New(*node.Expiry)
}
return nodeProto
}
// GetHostInfo returns a Hostinfo struct for the node.
func (node *Node) GetHostInfo() tailcfg.Hostinfo {
return tailcfg.Hostinfo(node.HostInfo)
}
func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (string, error) {
var hostname string
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS
hostname = fmt.Sprintf(
"%s.%s.%s",
node.GivenName,
node.User.Name,
baseDomain,
)
if len(hostname) > MaxHostnameLength {
return "", fmt.Errorf(
"hostname %q is too long it cannot except 255 ASCII chars: %w",
hostname,
ErrHostnameTooLong,
)
}
} else {
hostname = node.GivenName
}
return hostname, nil
}
func (node *Node) MachinePublicKey() (key.MachinePublic, error) {
var machineKey key.MachinePublic
if node.MachineKey != "" {
err := machineKey.UnmarshalText(
[]byte(util.MachinePublicKeyEnsurePrefix(node.MachineKey)),
)
if err != nil {
return key.MachinePublic{}, fmt.Errorf("failed to parse machine public key: %w", err)
}
}
return machineKey, nil
}
func (node *Node) DiscoPublicKey() (key.DiscoPublic, error) {
var discoKey key.DiscoPublic
if node.DiscoKey != "" {
err := discoKey.UnmarshalText(
[]byte(util.DiscoPublicKeyEnsurePrefix(node.DiscoKey)),
)
if err != nil {
return key.DiscoPublic{}, fmt.Errorf("failed to parse disco public key: %w", err)
}
} else {
discoKey = key.DiscoPublic{}
}
return discoKey, nil
}
func (node *Node) NodePublicKey() (key.NodePublic, error) {
var nodeKey key.NodePublic
err := nodeKey.UnmarshalText([]byte(util.NodePublicKeyEnsurePrefix(node.NodeKey)))
if err != nil {
return key.NodePublic{}, fmt.Errorf("failed to parse node public key: %w", err)
}
return nodeKey, nil
}
func (node Node) String() string {
return node.Hostname
}
func (nodes Nodes) String() string {
temp := make([]string, len(nodes))
for index, node := range nodes {
temp[index] = node.Hostname
}
return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp))
}
func (nodes Nodes) IDMap() map[uint64]*Node {
ret := map[uint64]*Node{}
for _, node := range nodes {
ret[node.ID] = node
}
return ret
}

View file

@ -7,20 +7,20 @@ import (
"tailscale.com/tailcfg"
)
func Test_MachineCanAccess(t *testing.T) {
func Test_NodeCanAccess(t *testing.T) {
tests := []struct {
name string
machine1 Machine
machine2 Machine
rules []tailcfg.FilterRule
want bool
name string
node1 Node
node2 Node
rules []tailcfg.FilterRule
want bool
}{
{
name: "no-rules",
machine1: Machine{
node1: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("10.0.0.1")},
},
machine2: Machine{
node2: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("10.0.0.2")},
},
rules: []tailcfg.FilterRule{},
@ -28,10 +28,10 @@ func Test_MachineCanAccess(t *testing.T) {
},
{
name: "wildcard",
machine1: Machine{
node1: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("10.0.0.1")},
},
machine2: Machine{
node2: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("10.0.0.2")},
},
rules: []tailcfg.FilterRule{
@ -49,10 +49,10 @@ func Test_MachineCanAccess(t *testing.T) {
},
{
name: "other-cant-access-src",
machine1: Machine{
node1: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
},
machine2: Machine{
node2: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
},
rules: []tailcfg.FilterRule{
@ -67,10 +67,10 @@ func Test_MachineCanAccess(t *testing.T) {
},
{
name: "dest-cant-access-src",
machine1: Machine{
node1: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
},
machine2: Machine{
node2: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
},
rules: []tailcfg.FilterRule{
@ -85,10 +85,10 @@ func Test_MachineCanAccess(t *testing.T) {
},
{
name: "src-can-access-dest",
machine1: Machine{
node1: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
},
machine2: Machine{
node2: Node{
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
},
rules: []tailcfg.FilterRule{
@ -105,7 +105,7 @@ func Test_MachineCanAccess(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.machine1.CanAccess(tt.rules, &tt.machine2)
got := tt.node1.CanAccess(tt.rules, &tt.node2)
if got != tt.want {
t.Errorf("canAccess() failed: want (%t), got (%t)", tt.want, got)
@ -114,8 +114,8 @@ func Test_MachineCanAccess(t *testing.T) {
}
}
func TestMachineAddressesOrder(t *testing.T) {
machineAddresses := MachineAddresses{
func TestNodeAddressesOrder(t *testing.T) {
machineAddresses := NodeAddresses{
netip.MustParseAddr("2001:db8::2"),
netip.MustParseAddr("100.64.0.2"),
netip.MustParseAddr("2001:db8::1"),

View file

@ -17,9 +17,9 @@ var (
type Route struct {
gorm.Model
MachineID uint64
Machine Machine
Prefix IPPrefix
NodeID uint64
Node Node
Prefix IPPrefix
Advertised bool
Enabled bool
@ -29,7 +29,7 @@ type Route struct {
type Routes []Route
func (r *Route) String() string {
return fmt.Sprintf("%s:%s", r.Machine, netip.Prefix(r.Prefix).String())
return fmt.Sprintf("%s:%s", r.Node, netip.Prefix(r.Prefix).String())
}
func (r *Route) IsExitRoute() bool {
@ -51,7 +51,7 @@ func (rs Routes) Proto() []*v1.Route {
for _, route := range rs {
protoRoute := v1.Route{
Id: uint64(route.ID),
Machine: route.Machine.Proto(),
Node: route.Node.Proto(),
Prefix: netip.Prefix(route.Prefix).String(),
Advertised: route.Advertised,
Enabled: route.Enabled,