mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-11-04 10:01:05 +01:00 
			
		
		
		
	fix routes not being saved when new nodes registers (#2444)
* add test to validate exitnode propagation Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * save routes on register Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * update changelog Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * no nil Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * add missing integration tests Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
		
							parent
							
								
									bcff0eaae7
								
							
						
					
					
						commit
						da2ca054b1
					
				
							
								
								
									
										2
									
								
								.github/workflows/test-integration.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test-integration.yaml
									
									
									
									
										vendored
									
									
								
							@ -24,6 +24,7 @@ jobs:
 | 
				
			|||||||
          - TestPolicyUpdateWhileRunningWithCLIInDatabase
 | 
					          - TestPolicyUpdateWhileRunningWithCLIInDatabase
 | 
				
			||||||
          - TestAuthKeyLogoutAndReloginSameUser
 | 
					          - TestAuthKeyLogoutAndReloginSameUser
 | 
				
			||||||
          - TestAuthKeyLogoutAndReloginNewUser
 | 
					          - TestAuthKeyLogoutAndReloginNewUser
 | 
				
			||||||
 | 
					          - TestAuthKeyLogoutAndReloginSameUserExpiredKey
 | 
				
			||||||
          - TestOIDCAuthenticationPingAll
 | 
					          - TestOIDCAuthenticationPingAll
 | 
				
			||||||
          - TestOIDCExpireNodesBasedOnTokenExpiry
 | 
					          - TestOIDCExpireNodesBasedOnTokenExpiry
 | 
				
			||||||
          - TestOIDC024UserCreation
 | 
					          - TestOIDC024UserCreation
 | 
				
			||||||
@ -68,6 +69,7 @@ jobs:
 | 
				
			|||||||
          - TestEnableDisableAutoApprovedRoute
 | 
					          - TestEnableDisableAutoApprovedRoute
 | 
				
			||||||
          - TestAutoApprovedSubRoute2068
 | 
					          - TestAutoApprovedSubRoute2068
 | 
				
			||||||
          - TestSubnetRouteACL
 | 
					          - TestSubnetRouteACL
 | 
				
			||||||
 | 
					          - TestEnablingExitRoutes
 | 
				
			||||||
          - TestHeadscale
 | 
					          - TestHeadscale
 | 
				
			||||||
          - TestCreateTailscale
 | 
					          - TestCreateTailscale
 | 
				
			||||||
          - TestTailscaleNodesJoiningHeadcale
 | 
					          - TestTailscaleNodesJoiningHeadcale
 | 
				
			||||||
 | 
				
			|||||||
@ -14,12 +14,14 @@
 | 
				
			|||||||
  - View of config, policy, filter, ssh policy per node, connected nodes and
 | 
					  - View of config, policy, filter, ssh policy per node, connected nodes and
 | 
				
			||||||
    DERPmap
 | 
					    DERPmap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 0.25.1 (2025-02-18)
 | 
					## 0.25.1 (2025-02-24)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Changes
 | 
					### Changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Fix issue where registration errors are sent correctly
 | 
					- Fix issue where registration errors are sent correctly
 | 
				
			||||||
  [#2435](https://github.com/juanfont/headscale/pull/2435)
 | 
					  [#2435](https://github.com/juanfont/headscale/pull/2435)
 | 
				
			||||||
 | 
					- Fix issue where routes passed on registration were not saved
 | 
				
			||||||
 | 
					  [#2444](https://github.com/juanfont/headscale/pull/2444)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 0.25.0 (2025-02-11)
 | 
					## 0.25.0 (2025-02-11)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -453,6 +453,10 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
 | 
				
			|||||||
		return nil, fmt.Errorf("failed register(save) node in the database: %w", err)
 | 
							return nil, fmt.Errorf("failed register(save) node in the database: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := SaveNodeRoutes(tx, &node); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to save node routes: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Trace().
 | 
						log.Trace().
 | 
				
			||||||
		Caller().
 | 
							Caller().
 | 
				
			||||||
		Str("node", node.Hostname).
 | 
							Str("node", node.Hostname).
 | 
				
			||||||
 | 
				
			|||||||
@ -744,6 +744,7 @@ func TestRenameNode(t *testing.T) {
 | 
				
			|||||||
		Hostname:       "test",
 | 
							Hostname:       "test",
 | 
				
			||||||
		UserID:         user.ID,
 | 
							UserID:         user.ID,
 | 
				
			||||||
		RegisterMethod: util.RegisterMethodAuthKey,
 | 
							RegisterMethod: util.RegisterMethodAuthKey,
 | 
				
			||||||
 | 
							Hostinfo:       &tailcfg.Hostinfo{},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	node2 := types.Node{
 | 
						node2 := types.Node{
 | 
				
			||||||
@ -753,6 +754,7 @@ func TestRenameNode(t *testing.T) {
 | 
				
			|||||||
		Hostname:       "test",
 | 
							Hostname:       "test",
 | 
				
			||||||
		UserID:         user2.ID,
 | 
							UserID:         user2.ID,
 | 
				
			||||||
		RegisterMethod: util.RegisterMethodAuthKey,
 | 
							RegisterMethod: util.RegisterMethodAuthKey,
 | 
				
			||||||
 | 
							Hostinfo:       &tailcfg.Hostinfo{},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = db.DB.Save(&node).Error
 | 
						err = db.DB.Save(&node).Error
 | 
				
			||||||
 | 
				
			|||||||
@ -17,6 +17,8 @@ import (
 | 
				
			|||||||
	"github.com/juanfont/headscale/integration/hsic"
 | 
						"github.com/juanfont/headscale/integration/hsic"
 | 
				
			||||||
	"github.com/juanfont/headscale/integration/tsic"
 | 
						"github.com/juanfont/headscale/integration/tsic"
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
						"tailscale.com/net/tsaddr"
 | 
				
			||||||
	"tailscale.com/types/ipproto"
 | 
						"tailscale.com/types/ipproto"
 | 
				
			||||||
	"tailscale.com/types/views"
 | 
						"tailscale.com/types/views"
 | 
				
			||||||
	"tailscale.com/wgengine/filter"
 | 
						"tailscale.com/wgengine/filter"
 | 
				
			||||||
@ -1316,3 +1318,123 @@ func TestSubnetRouteACL(t *testing.T) {
 | 
				
			|||||||
		t.Errorf("Subnet (%s) filter, unexpected result (-want +got):\n%s", subRouter1.Hostname(), diff)
 | 
							t.Errorf("Subnet (%s) filter, unexpected result (-want +got):\n%s", subRouter1.Hostname(), diff)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestEnablingExitRoutes tests enabling exit routes for clients.
 | 
				
			||||||
 | 
					// Its more or less the same as TestEnablingRoutes, but with the --advertise-exit-node flag
 | 
				
			||||||
 | 
					// set during login instead of set.
 | 
				
			||||||
 | 
					func TestEnablingExitRoutes(t *testing.T) {
 | 
				
			||||||
 | 
						IntegrationSkip(t)
 | 
				
			||||||
 | 
						t.Parallel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user := "user2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						scenario, err := NewScenario(dockertestMaxWait())
 | 
				
			||||||
 | 
						assertNoErrf(t, "failed to create scenario: %s", err)
 | 
				
			||||||
 | 
						defer scenario.ShutdownAssertNoPanics(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						spec := map[string]int{
 | 
				
			||||||
 | 
							user: 2,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{
 | 
				
			||||||
 | 
							tsic.WithExtraLoginArgs([]string{"--advertise-exit-node"}),
 | 
				
			||||||
 | 
						}, hsic.WithTestName("clienableroute"))
 | 
				
			||||||
 | 
						assertNoErrHeadscaleEnv(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						allClients, err := scenario.ListTailscaleClients()
 | 
				
			||||||
 | 
						assertNoErrListClients(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = scenario.WaitForTailscaleSync()
 | 
				
			||||||
 | 
						assertNoErrSync(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						headscale, err := scenario.Headscale()
 | 
				
			||||||
 | 
						assertNoErrGetHeadscale(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = scenario.WaitForTailscaleSync()
 | 
				
			||||||
 | 
						assertNoErrSync(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var routes []*v1.Route
 | 
				
			||||||
 | 
						err = executeAndUnmarshal(
 | 
				
			||||||
 | 
							headscale,
 | 
				
			||||||
 | 
							[]string{
 | 
				
			||||||
 | 
								"headscale",
 | 
				
			||||||
 | 
								"routes",
 | 
				
			||||||
 | 
								"list",
 | 
				
			||||||
 | 
								"--output",
 | 
				
			||||||
 | 
								"json",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							&routes,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assertNoErr(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, routes, 4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, route := range routes {
 | 
				
			||||||
 | 
							assert.True(t, route.GetAdvertised())
 | 
				
			||||||
 | 
							assert.False(t, route.GetEnabled())
 | 
				
			||||||
 | 
							assert.False(t, route.GetIsPrimary())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify that no routes has been sent to the client,
 | 
				
			||||||
 | 
						// they are not yet enabled.
 | 
				
			||||||
 | 
						for _, client := range allClients {
 | 
				
			||||||
 | 
							status, err := client.Status()
 | 
				
			||||||
 | 
							assertNoErr(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, peerKey := range status.Peers() {
 | 
				
			||||||
 | 
								peerStatus := status.Peer[peerKey]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								assert.Nil(t, peerStatus.PrimaryRoutes)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Enable all routes
 | 
				
			||||||
 | 
						for _, route := range routes {
 | 
				
			||||||
 | 
							_, err = headscale.Execute(
 | 
				
			||||||
 | 
								[]string{
 | 
				
			||||||
 | 
									"headscale",
 | 
				
			||||||
 | 
									"routes",
 | 
				
			||||||
 | 
									"enable",
 | 
				
			||||||
 | 
									"--route",
 | 
				
			||||||
 | 
									strconv.Itoa(int(route.GetId())),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							assertNoErr(t, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var enablingRoutes []*v1.Route
 | 
				
			||||||
 | 
						err = executeAndUnmarshal(
 | 
				
			||||||
 | 
							headscale,
 | 
				
			||||||
 | 
							[]string{
 | 
				
			||||||
 | 
								"headscale",
 | 
				
			||||||
 | 
								"routes",
 | 
				
			||||||
 | 
								"list",
 | 
				
			||||||
 | 
								"--output",
 | 
				
			||||||
 | 
								"json",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							&enablingRoutes,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						assertNoErr(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, enablingRoutes, 4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, route := range enablingRoutes {
 | 
				
			||||||
 | 
							assert.True(t, route.GetAdvertised())
 | 
				
			||||||
 | 
							assert.True(t, route.GetEnabled())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						time.Sleep(5 * time.Second)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify that the clients can see the new routes
 | 
				
			||||||
 | 
						for _, client := range allClients {
 | 
				
			||||||
 | 
							status, err := client.Status()
 | 
				
			||||||
 | 
							assertNoErr(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, peerKey := range status.Peers() {
 | 
				
			||||||
 | 
								peerStatus := status.Peer[peerKey]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								require.NotNil(t, peerStatus.AllowedIPs)
 | 
				
			||||||
 | 
								assert.Len(t, peerStatus.AllowedIPs.AsSlice(), 4)
 | 
				
			||||||
 | 
								assert.Contains(t, peerStatus.AllowedIPs.AsSlice(), tsaddr.AllIPv4())
 | 
				
			||||||
 | 
								assert.Contains(t, peerStatus.AllowedIPs.AsSlice(), tsaddr.AllIPv6())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -80,6 +80,7 @@ type TailscaleInContainer struct {
 | 
				
			|||||||
	withExtraHosts    []string
 | 
						withExtraHosts    []string
 | 
				
			||||||
	workdir           string
 | 
						workdir           string
 | 
				
			||||||
	netfilter         string
 | 
						netfilter         string
 | 
				
			||||||
 | 
						extraLoginArgs    []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// build options, solely for HEAD
 | 
						// build options, solely for HEAD
 | 
				
			||||||
	buildConfig TailscaleInContainerBuildConfig
 | 
						buildConfig TailscaleInContainerBuildConfig
 | 
				
			||||||
@ -203,6 +204,14 @@ func WithBuildTag(tag string) Option {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// WithExtraLoginArgs adds additional arguments to the `tailscale up` command
 | 
				
			||||||
 | 
					// as part of the Login function.
 | 
				
			||||||
 | 
					func WithExtraLoginArgs(args []string) Option {
 | 
				
			||||||
 | 
						return func(tsic *TailscaleInContainer) {
 | 
				
			||||||
 | 
							tsic.extraLoginArgs = args
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// New returns a new TailscaleInContainer instance.
 | 
					// New returns a new TailscaleInContainer instance.
 | 
				
			||||||
func New(
 | 
					func New(
 | 
				
			||||||
	pool *dockertest.Pool,
 | 
						pool *dockertest.Pool,
 | 
				
			||||||
@ -436,6 +445,10 @@ func (t *TailscaleInContainer) Login(
 | 
				
			|||||||
		"--accept-routes=false",
 | 
							"--accept-routes=false",
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if t.extraLoginArgs != nil {
 | 
				
			||||||
 | 
							command = append(command, t.extraLoginArgs...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if t.withSSH {
 | 
						if t.withSSH {
 | 
				
			||||||
		command = append(command, "--ssh")
 | 
							command = append(command, "--ssh")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -475,6 +488,10 @@ func (t *TailscaleInContainer) LoginWithURL(
 | 
				
			|||||||
		"--accept-routes=false",
 | 
							"--accept-routes=false",
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if t.extraLoginArgs != nil {
 | 
				
			||||||
 | 
							command = append(command, t.extraLoginArgs...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	stdout, stderr, err := t.Execute(command)
 | 
						stdout, stderr, err := t.Execute(command)
 | 
				
			||||||
	if errors.Is(err, errTailscaleNotLoggedIn) {
 | 
						if errors.Is(err, errTailscaleNotLoggedIn) {
 | 
				
			||||||
		return nil, errTailscaleCannotUpWithoutAuthkey
 | 
							return nil, errTailscaleCannotUpWithoutAuthkey
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user