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