Redo route code (#2422)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
16868190c8
commit
7891378f57
53 changed files with 2977 additions and 6251 deletions
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -25,8 +26,11 @@ var (
|
|||
)
|
||||
|
||||
type NodeID uint64
|
||||
type NodeIDs []NodeID
|
||||
|
||||
// type NodeConnectedMap *xsync.MapOf[NodeID, bool]
|
||||
func (n NodeIDs) Len() int { return len(n) }
|
||||
func (n NodeIDs) Less(i, j int) bool { return n[i] < n[j] }
|
||||
func (n NodeIDs) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||
|
||||
func (id NodeID) StableID() tailcfg.StableNodeID {
|
||||
return tailcfg.StableNodeID(strconv.FormatUint(uint64(id), util.Base10))
|
||||
|
@ -84,10 +88,21 @@ type Node struct {
|
|||
AuthKeyID *uint64 `sql:"DEFAULT:NULL"`
|
||||
AuthKey *PreAuthKey
|
||||
|
||||
LastSeen *time.Time
|
||||
Expiry *time.Time
|
||||
Expiry *time.Time
|
||||
|
||||
Routes []Route `gorm:"constraint:OnDelete:CASCADE;"`
|
||||
// LastSeen is when the node was last in contact with
|
||||
// headscale. It is best effort and not persisted.
|
||||
LastSeen *time.Time `gorm:"-"`
|
||||
|
||||
// DEPRECATED: Use the ApprovedRoutes field instead.
|
||||
// TODO(kradalby): remove when ApprovedRoutes is used all over the code.
|
||||
// Routes []Route `gorm:"constraint:OnDelete:CASCADE;"`
|
||||
|
||||
// ApprovedRoutes is a list of routes that the node is allowed to announce
|
||||
// as a subnet router. They are not necessarily the routes that the node
|
||||
// announces at the moment.
|
||||
// See [Node.Hostinfo]
|
||||
ApprovedRoutes []netip.Prefix `gorm:"column:approved_routes;serializer:json"`
|
||||
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
@ -96,9 +111,7 @@ type Node struct {
|
|||
IsOnline *bool `gorm:"-"`
|
||||
}
|
||||
|
||||
type (
|
||||
Nodes []*Node
|
||||
)
|
||||
type Nodes []*Node
|
||||
|
||||
// GivenNameHasBeenChanged returns whether the `givenName` can be automatically changed based on the `Hostname` of the node.
|
||||
func (node *Node) GivenNameHasBeenChanged() bool {
|
||||
|
@ -185,23 +198,22 @@ func (node *Node) CanAccess(filter []tailcfg.FilterRule, node2 *Node) bool {
|
|||
|
||||
// TODO(kradalby): Regenerate this every time the filter change, instead of
|
||||
// every time we use it.
|
||||
// Part of #2416
|
||||
matchers := make([]matcher.Match, len(filter))
|
||||
for i, rule := range filter {
|
||||
matchers[i] = matcher.MatchFromFilterRule(rule)
|
||||
}
|
||||
|
||||
for _, route := range node2.Routes {
|
||||
if route.Enabled {
|
||||
allowedIPs = append(allowedIPs, netip.Prefix(route.Prefix).Addr())
|
||||
}
|
||||
}
|
||||
|
||||
for _, matcher := range matchers {
|
||||
if !matcher.SrcsContainsIPs(src) {
|
||||
if !matcher.SrcsContainsIPs(src...) {
|
||||
continue
|
||||
}
|
||||
|
||||
if matcher.DestsContainsIP(allowedIPs) {
|
||||
if matcher.DestsContainsIP(allowedIPs...) {
|
||||
return true
|
||||
}
|
||||
|
||||
if matcher.DestsOverlapsPrefixes(node2.SubnetRoutes()...) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -245,11 +257,14 @@ func (node *Node) Proto() *v1.Node {
|
|||
DiscoKey: node.DiscoKey.String(),
|
||||
|
||||
// TODO(kradalby): replace list with v4, v6 field?
|
||||
IpAddresses: node.IPsAsString(),
|
||||
Name: node.Hostname,
|
||||
GivenName: node.GivenName,
|
||||
User: node.User.Proto(),
|
||||
ForcedTags: node.ForcedTags,
|
||||
IpAddresses: node.IPsAsString(),
|
||||
Name: node.Hostname,
|
||||
GivenName: node.GivenName,
|
||||
User: node.User.Proto(),
|
||||
ForcedTags: node.ForcedTags,
|
||||
ApprovedRoutes: util.PrefixesToString(node.ApprovedRoutes),
|
||||
AvailableRoutes: util.PrefixesToString(node.AnnouncedRoutes()),
|
||||
SubnetRoutes: util.PrefixesToString(node.SubnetRoutes()),
|
||||
|
||||
RegisterMethod: node.RegisterMethodToV1Enum(),
|
||||
|
||||
|
@ -297,6 +312,29 @@ func (node *Node) GetFQDN(baseDomain string) (string, error) {
|
|||
return hostname, nil
|
||||
}
|
||||
|
||||
// AnnouncedRoutes returns the list of routes that the node announces.
|
||||
// It should be used instead of checking Hostinfo.RoutableIPs directly.
|
||||
func (node *Node) AnnouncedRoutes() []netip.Prefix {
|
||||
if node.Hostinfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return node.Hostinfo.RoutableIPs
|
||||
}
|
||||
|
||||
// SubnetRoutes returns the list of routes that the node announces and are approved.
|
||||
func (node *Node) SubnetRoutes() []netip.Prefix {
|
||||
var routes []netip.Prefix
|
||||
|
||||
for _, route := range node.AnnouncedRoutes() {
|
||||
if slices.Contains(node.ApprovedRoutes, route) {
|
||||
routes = append(routes, route)
|
||||
}
|
||||
}
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
// func (node *Node) String() string {
|
||||
// return node.Hostname
|
||||
// }
|
||||
|
|
|
@ -1,102 +1,31 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
// Deprecated: Approval of routes is denormalised onto the relevant node.
|
||||
// Struct is kept for GORM migrations only.
|
||||
type Route struct {
|
||||
gorm.Model
|
||||
|
||||
NodeID uint64 `gorm:"not null"`
|
||||
Node *Node
|
||||
|
||||
// TODO(kradalby): change this custom type to netip.Prefix
|
||||
Prefix netip.Prefix `gorm:"serializer:text"`
|
||||
|
||||
// Advertised is now only stored as part of [Node.Hostinfo].
|
||||
Advertised bool
|
||||
Enabled bool
|
||||
IsPrimary bool
|
||||
|
||||
// Enabled is stored directly on the node as ApprovedRoutes.
|
||||
Enabled bool
|
||||
|
||||
// IsPrimary is only determined in memory as it is only relevant
|
||||
// when the server is up.
|
||||
IsPrimary bool
|
||||
}
|
||||
|
||||
// Deprecated: Approval of routes is denormalised onto the relevant node.
|
||||
type Routes []Route
|
||||
|
||||
func (r *Route) String() string {
|
||||
return fmt.Sprintf("%s:%s", r.Node.Hostname, netip.Prefix(r.Prefix).String())
|
||||
}
|
||||
|
||||
func (r *Route) IsExitRoute() bool {
|
||||
return tsaddr.IsExitRoute(r.Prefix)
|
||||
}
|
||||
|
||||
func (r *Route) IsAnnouncable() bool {
|
||||
return r.Advertised && r.Enabled
|
||||
}
|
||||
|
||||
func (rs Routes) Prefixes() []netip.Prefix {
|
||||
prefixes := make([]netip.Prefix, len(rs))
|
||||
for i, r := range rs {
|
||||
prefixes[i] = netip.Prefix(r.Prefix)
|
||||
}
|
||||
|
||||
return prefixes
|
||||
}
|
||||
|
||||
// Primaries returns Primary routes from a list of routes.
|
||||
func (rs Routes) Primaries() Routes {
|
||||
res := make(Routes, 0)
|
||||
for _, route := range rs {
|
||||
if route.IsPrimary {
|
||||
res = append(res, route)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (rs Routes) PrefixMap() map[netip.Prefix][]Route {
|
||||
res := map[netip.Prefix][]Route{}
|
||||
|
||||
for _, route := range rs {
|
||||
if _, ok := res[route.Prefix]; ok {
|
||||
res[route.Prefix] = append(res[route.Prefix], route)
|
||||
} else {
|
||||
res[route.Prefix] = []Route{route}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (rs Routes) Proto() []*v1.Route {
|
||||
protoRoutes := []*v1.Route{}
|
||||
|
||||
for _, route := range rs {
|
||||
protoRoute := v1.Route{
|
||||
Id: uint64(route.ID),
|
||||
Prefix: route.Prefix.String(),
|
||||
Advertised: route.Advertised,
|
||||
Enabled: route.Enabled,
|
||||
IsPrimary: route.IsPrimary,
|
||||
CreatedAt: timestamppb.New(route.CreatedAt),
|
||||
UpdatedAt: timestamppb.New(route.UpdatedAt),
|
||||
}
|
||||
|
||||
if route.Node != nil {
|
||||
protoRoute.Node = route.Node.Proto()
|
||||
}
|
||||
|
||||
if route.DeletedAt.Valid {
|
||||
protoRoute.DeletedAt = timestamppb.New(route.DeletedAt.Time)
|
||||
}
|
||||
|
||||
protoRoutes = append(protoRoutes, &protoRoute)
|
||||
}
|
||||
|
||||
return protoRoutes
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
)
|
||||
|
||||
func TestPrefixMap(t *testing.T) {
|
||||
ipp := func(s string) netip.Prefix { return netip.MustParsePrefix(s) }
|
||||
|
||||
tests := []struct {
|
||||
rs Routes
|
||||
want map[netip.Prefix][]Route
|
||||
}{
|
||||
{
|
||||
rs: Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
},
|
||||
},
|
||||
want: map[netip.Prefix][]Route{
|
||||
ipp("10.0.0.0/24"): Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rs: Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
},
|
||||
Route{
|
||||
Prefix: ipp("10.0.1.0/24"),
|
||||
},
|
||||
},
|
||||
want: map[netip.Prefix][]Route{
|
||||
ipp("10.0.0.0/24"): Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
},
|
||||
},
|
||||
ipp("10.0.1.0/24"): Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.1.0/24"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rs: Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
Enabled: true,
|
||||
},
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
want: map[netip.Prefix][]Route{
|
||||
ipp("10.0.0.0/24"): Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
Enabled: true,
|
||||
},
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tt := range tests {
|
||||
t.Run(fmt.Sprintf("test-%d", idx), func(t *testing.T) {
|
||||
got := tt.rs.PrefixMap()
|
||||
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
|
||||
t.Errorf("PrefixMap() unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -55,6 +55,13 @@ type User struct {
|
|||
ProfilePicURL string
|
||||
}
|
||||
|
||||
func (u *User) StringID() string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatUint(uint64(u.ID), 10)
|
||||
}
|
||||
|
||||
// Username is the main way to get the username of a user,
|
||||
// it will return the email if it exists, the name if it exists,
|
||||
// the OIDCIdentifier if it exists, and the ID if nothing else exists.
|
||||
|
@ -63,7 +70,11 @@ type User struct {
|
|||
// should be used throughout headscale, in information returned to the
|
||||
// user and the Policy engine.
|
||||
func (u *User) Username() string {
|
||||
return cmp.Or(u.Email, u.Name, u.ProviderIdentifier.String, strconv.FormatUint(uint64(u.ID), 10))
|
||||
return cmp.Or(
|
||||
u.Email,
|
||||
u.Name,
|
||||
u.ProviderIdentifier.String,
|
||||
u.StringID())
|
||||
}
|
||||
|
||||
// DisplayNameOrUsername returns the DisplayName if it exists, otherwise
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue