mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	This commit changes most of our (*)types.Node to types.NodeView, which is a readonly version of the underlying node ensuring that there is no mutations happening in the read path. Based on the migration, there didnt seem to be any, but the idea here is to prevent it in the future and simplify other new implementations. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
		
			
				
	
	
		
			312 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			312 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package mapper
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"net/netip"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| 	"github.com/google/go-cmp/cmp/cmpopts"
 | |
| 	"github.com/juanfont/headscale/hscontrol/policy"
 | |
| 	"github.com/juanfont/headscale/hscontrol/routes"
 | |
| 	"github.com/juanfont/headscale/hscontrol/types"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| 	"tailscale.com/net/tsaddr"
 | |
| 	"tailscale.com/tailcfg"
 | |
| 	"tailscale.com/types/key"
 | |
| )
 | |
| 
 | |
| func TestTailNode(t *testing.T) {
 | |
| 	mustNK := func(str string) key.NodePublic {
 | |
| 		var k key.NodePublic
 | |
| 		_ = k.UnmarshalText([]byte(str))
 | |
| 
 | |
| 		return k
 | |
| 	}
 | |
| 
 | |
| 	mustDK := func(str string) key.DiscoPublic {
 | |
| 		var k key.DiscoPublic
 | |
| 		_ = k.UnmarshalText([]byte(str))
 | |
| 
 | |
| 		return k
 | |
| 	}
 | |
| 
 | |
| 	mustMK := func(str string) key.MachinePublic {
 | |
| 		var k key.MachinePublic
 | |
| 		_ = k.UnmarshalText([]byte(str))
 | |
| 
 | |
| 		return k
 | |
| 	}
 | |
| 
 | |
| 	hiview := func(hoin tailcfg.Hostinfo) tailcfg.HostinfoView {
 | |
| 		return hoin.View()
 | |
| 	}
 | |
| 
 | |
| 	created := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
 | |
| 	lastSeen := time.Date(2009, time.November, 10, 23, 9, 0, 0, time.UTC)
 | |
| 	expire := time.Date(2500, time.November, 11, 23, 0, 0, 0, time.UTC)
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name       string
 | |
| 		node       *types.Node
 | |
| 		pol        []byte
 | |
| 		dnsConfig  *tailcfg.DNSConfig
 | |
| 		baseDomain string
 | |
| 		want       *tailcfg.Node
 | |
| 		wantErr    bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "empty-node",
 | |
| 			node: &types.Node{
 | |
| 				GivenName: "empty",
 | |
| 				Hostinfo:  &tailcfg.Hostinfo{},
 | |
| 			},
 | |
| 			dnsConfig:  &tailcfg.DNSConfig{},
 | |
| 			baseDomain: "",
 | |
| 			want: &tailcfg.Node{
 | |
| 				Name:              "empty",
 | |
| 				StableID:          "0",
 | |
| 				HomeDERP:          0,
 | |
| 				LegacyDERPString:  "127.3.3.40:0",
 | |
| 				Hostinfo:          hiview(tailcfg.Hostinfo{}),
 | |
| 				Tags:              []string{},
 | |
| 				MachineAuthorized: true,
 | |
| 
 | |
| 				CapMap: tailcfg.NodeCapMap{
 | |
| 					tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{},
 | |
| 					tailcfg.CapabilityAdmin:       []tailcfg.RawMessage{},
 | |
| 					tailcfg.CapabilitySSH:         []tailcfg.RawMessage{},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "minimal-node",
 | |
| 			node: &types.Node{
 | |
| 				ID: 0,
 | |
| 				MachineKey: mustMK(
 | |
| 					"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
 | |
| 				),
 | |
| 				NodeKey: mustNK(
 | |
| 					"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
 | |
| 				),
 | |
| 				DiscoKey: mustDK(
 | |
| 					"discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
 | |
| 				),
 | |
| 				IPv4:      iap("100.64.0.1"),
 | |
| 				Hostname:  "mini",
 | |
| 				GivenName: "mini",
 | |
| 				UserID:    0,
 | |
| 				User: types.User{
 | |
| 					Name: "mini",
 | |
| 				},
 | |
| 				ForcedTags: []string{},
 | |
| 				AuthKey:    &types.PreAuthKey{},
 | |
| 				LastSeen:   &lastSeen,
 | |
| 				Expiry:     &expire,
 | |
| 				Hostinfo: &tailcfg.Hostinfo{
 | |
| 					RoutableIPs: []netip.Prefix{
 | |
| 						tsaddr.AllIPv4(),
 | |
| 						netip.MustParsePrefix("192.168.0.0/24"),
 | |
| 						netip.MustParsePrefix("172.0.0.0/10"),
 | |
| 					},
 | |
| 				},
 | |
| 				ApprovedRoutes: []netip.Prefix{tsaddr.AllIPv4(), netip.MustParsePrefix("192.168.0.0/24")},
 | |
| 				CreatedAt:      created,
 | |
| 			},
 | |
| 			dnsConfig:  &tailcfg.DNSConfig{},
 | |
| 			baseDomain: "",
 | |
| 			want: &tailcfg.Node{
 | |
| 				ID:       0,
 | |
| 				StableID: "0",
 | |
| 				Name:     "mini",
 | |
| 
 | |
| 				User: 0,
 | |
| 
 | |
| 				Key: mustNK(
 | |
| 					"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
 | |
| 				),
 | |
| 				KeyExpiry: expire,
 | |
| 
 | |
| 				Machine: mustMK(
 | |
| 					"mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
 | |
| 				),
 | |
| 				DiscoKey: mustDK(
 | |
| 					"discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
 | |
| 				),
 | |
| 				Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
 | |
| 				AllowedIPs: []netip.Prefix{
 | |
| 					tsaddr.AllIPv4(),
 | |
| 					netip.MustParsePrefix("192.168.0.0/24"),
 | |
| 					netip.MustParsePrefix("100.64.0.1/32"),
 | |
| 					tsaddr.AllIPv6(),
 | |
| 				},
 | |
| 				PrimaryRoutes: []netip.Prefix{
 | |
| 					netip.MustParsePrefix("192.168.0.0/24"),
 | |
| 				},
 | |
| 				HomeDERP:         0,
 | |
| 				LegacyDERPString: "127.3.3.40:0",
 | |
| 				Hostinfo: hiview(tailcfg.Hostinfo{
 | |
| 					RoutableIPs: []netip.Prefix{
 | |
| 						tsaddr.AllIPv4(),
 | |
| 						netip.MustParsePrefix("192.168.0.0/24"),
 | |
| 						netip.MustParsePrefix("172.0.0.0/10"),
 | |
| 					},
 | |
| 				}),
 | |
| 				Created: created,
 | |
| 
 | |
| 				Tags: []string{},
 | |
| 
 | |
| 				LastSeen:          &lastSeen,
 | |
| 				MachineAuthorized: true,
 | |
| 
 | |
| 				CapMap: tailcfg.NodeCapMap{
 | |
| 					tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{},
 | |
| 					tailcfg.CapabilityAdmin:       []tailcfg.RawMessage{},
 | |
| 					tailcfg.CapabilitySSH:         []tailcfg.RawMessage{},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "check-dot-suffix-on-node-name",
 | |
| 			node: &types.Node{
 | |
| 				GivenName: "minimal",
 | |
| 				Hostinfo:  &tailcfg.Hostinfo{},
 | |
| 			},
 | |
| 			dnsConfig:  &tailcfg.DNSConfig{},
 | |
| 			baseDomain: "example.com",
 | |
| 			want: &tailcfg.Node{
 | |
| 				// a node name should have a dot appended
 | |
| 				Name:              "minimal.example.com.",
 | |
| 				StableID:          "0",
 | |
| 				HomeDERP:          0,
 | |
| 				LegacyDERPString:  "127.3.3.40:0",
 | |
| 				Hostinfo:          hiview(tailcfg.Hostinfo{}),
 | |
| 				Tags:              []string{},
 | |
| 				MachineAuthorized: true,
 | |
| 
 | |
| 				CapMap: tailcfg.NodeCapMap{
 | |
| 					tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{},
 | |
| 					tailcfg.CapabilityAdmin:       []tailcfg.RawMessage{},
 | |
| 					tailcfg.CapabilitySSH:         []tailcfg.RawMessage{},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		// TODO: Add tests to check other aspects of the node conversion:
 | |
| 		// - With tags and policy
 | |
| 		// - dnsconfig and basedomain
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			polMan, err := policy.NewPolicyManager(tt.pol, []types.User{}, types.Nodes{tt.node}.ViewSlice())
 | |
| 			require.NoError(t, err)
 | |
| 			primary := routes.New()
 | |
| 			cfg := &types.Config{
 | |
| 				BaseDomain:          tt.baseDomain,
 | |
| 				TailcfgDNSConfig:    tt.dnsConfig,
 | |
| 				RandomizeClientPort: false,
 | |
| 			}
 | |
| 			_ = primary.SetRoutes(tt.node.ID, tt.node.SubnetRoutes()...)
 | |
| 
 | |
| 			// This is a hack to avoid having a second node to test the primary route.
 | |
| 			// This should be baked into the test case proper if it is extended in the future.
 | |
| 			_ = primary.SetRoutes(2, netip.MustParsePrefix("192.168.0.0/24"))
 | |
| 			got, err := tailNode(
 | |
| 				tt.node.View(),
 | |
| 				0,
 | |
| 				polMan,
 | |
| 				func(id types.NodeID) []netip.Prefix {
 | |
| 					return primary.PrimaryRoutes(id)
 | |
| 				},
 | |
| 				cfg,
 | |
| 			)
 | |
| 
 | |
| 			if (err != nil) != tt.wantErr {
 | |
| 				t.Errorf("tailNode() error = %v, wantErr %v", err, tt.wantErr)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			if diff := cmp.Diff(tt.want, got, cmpopts.EquateEmpty()); diff != "" {
 | |
| 				t.Errorf("tailNode() unexpected result (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestNodeExpiry(t *testing.T) {
 | |
| 	tp := func(t time.Time) *time.Time {
 | |
| 		return &t
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name         string
 | |
| 		exp          *time.Time
 | |
| 		wantTime     time.Time
 | |
| 		wantTimeZero bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name:         "no-expiry",
 | |
| 			exp:          nil,
 | |
| 			wantTimeZero: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "zero-expiry",
 | |
| 			exp:          &time.Time{},
 | |
| 			wantTimeZero: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "localtime",
 | |
| 			exp:          tp(time.Time{}.Local()),
 | |
| 			wantTimeZero: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			node := &types.Node{
 | |
| 				ID:        0,
 | |
| 				GivenName: "test",
 | |
| 				Expiry:    tt.exp,
 | |
| 			}
 | |
| 			polMan, err := policy.NewPolicyManager(nil, nil, types.Nodes{}.ViewSlice())
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			tn, err := tailNode(
 | |
| 				node.View(),
 | |
| 				0,
 | |
| 				polMan,
 | |
| 				func(id types.NodeID) []netip.Prefix {
 | |
| 					return []netip.Prefix{}
 | |
| 				},
 | |
| 				&types.Config{},
 | |
| 			)
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("nodeExpiry() error = %v", err)
 | |
| 			}
 | |
| 
 | |
| 			// Round trip the node through JSON to ensure the time is serialized correctly
 | |
| 			seri, err := json.Marshal(tn)
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("nodeExpiry() error = %v", err)
 | |
| 			}
 | |
| 			var deseri tailcfg.Node
 | |
| 			err = json.Unmarshal(seri, &deseri)
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("nodeExpiry() error = %v", err)
 | |
| 			}
 | |
| 
 | |
| 			if tt.wantTimeZero {
 | |
| 				if !deseri.KeyExpiry.IsZero() {
 | |
| 					t.Errorf("nodeExpiry() = %v, want zero", deseri.KeyExpiry)
 | |
| 				}
 | |
| 			} else if deseri.KeyExpiry != tt.wantTime {
 | |
| 				t.Errorf("nodeExpiry() = %v, want %v", deseri.KeyExpiry, tt.wantTime)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |