diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go new file mode 100644 index 0000000..a166ba4 --- /dev/null +++ b/integration/embedded_derp_test.go @@ -0,0 +1,247 @@ +package integration + +import ( + "fmt" + "log" + "net/url" + "testing" + + "github.com/juanfont/headscale" + "github.com/juanfont/headscale/integration/dockertestutil" + "github.com/juanfont/headscale/integration/hsic" + "github.com/juanfont/headscale/integration/tsic" + "github.com/ory/dockertest/v3" +) + +type EmbeddedDERPServerScenario struct { + *Scenario + + tsicNetworks map[string]*dockertest.Network +} + +func TestDERPServerScenario(t *testing.T) { + IntegrationSkip(t) + // t.Parallel() + + baseScenario, err := NewScenario() + if err != nil { + t.Errorf("failed to create scenario: %s", err) + } + + scenario := EmbeddedDERPServerScenario{ + Scenario: baseScenario, + tsicNetworks: map[string]*dockertest.Network{}, + } + + spec := map[string]int{ + "user1": len(TailscaleVersions), + } + + headscaleConfig := hsic.DefaultConfigEnv() + headscaleConfig["HEADSCALE_LISTEN_ADDR"] = "0.0.0.0:8443" + headscaleConfig["HEADSCALE_DERP_URLS"] = "" + headscaleConfig["HEADSCALE_DERP_SERVER_ENABLED"] = "true" + headscaleConfig["HEADSCALE_DERP_SERVER_REGION_ID"] = "999" + headscaleConfig["HEADSCALE_DERP_SERVER_REGION_CODE"] = "headscale" + headscaleConfig["HEADSCALE_DERP_SERVER_REGION_NAME"] = "Headscale Embedded DERP" + headscaleConfig["HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR"] = "0.0.0.0:3478" + + err = scenario.CreateHeadscaleEnv( + spec, + hsic.WithConfigEnv(headscaleConfig), + hsic.WithPort(8443), + hsic.WithTestName("derpserver"), + hsic.WithHostPortBindings( + map[string][]string{ + "8443/tcp": {"8443"}, + "3478/udp": {"3478"}, + }, + ), + hsic.WithExtraPorts([]string{"3478/udp"}), + hsic.WithTLS(), + hsic.WithHostnameAsServerURL(), + ) + + if err != nil { + t.Errorf("failed to create headscale environment: %s", err) + } + + allClients, err := scenario.ListTailscaleClients() + if err != nil { + t.Errorf("failed to get clients: %s", err) + } + + allIps, err := scenario.ListTailscaleClientsIPs() + if err != nil { + t.Errorf("failed to get clients: %s", err) + } + + err = scenario.WaitForTailscaleSync() + if err != nil { + t.Errorf("failed wait for tailscale clients to be in sync: %s", err) + } + + allHostnames, err := scenario.ListTailscaleClientsFQDNs() + if err != nil { + t.Errorf("failed to get FQDNs: %s", err) + } + + success := pingDerpAllHelper(t, allClients, allHostnames) + + t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) + + err = scenario.Shutdown() + if err != nil { + t.Errorf("failed to tear down scenario: %s", err) + } +} + +func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv( + users map[string]int, + opts ...hsic.Option, +) error { + hsServer, err := s.Headscale(opts...) + if err != nil { + return err + } + + headscaleEndpoint := hsServer.GetEndpoint() + headscaleURL, err := url.Parse(headscaleEndpoint) + if err != nil { + return err + } + + headscaleURL.Host = fmt.Sprintf("%s:%s", hsServer.GetHostname(), headscaleURL.Port()) + + extraHosts := []string{ + "host.docker.internal:host-gateway", + fmt.Sprintf("%s:host-gateway", hsServer.GetHostname()), + } + + err = hsServer.WaitForReady() + if err != nil { + return err + } + + hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength) + if err != nil { + return err + } + + for userName, clientCount := range users { + err = s.CreateUser(userName) + if err != nil { + return err + } + + err = s.CreateTailscaleIsolatedNodesInUser( + hash, + userName, + "all", + clientCount, + tsic.WithExtraHosts(extraHosts), + ) + if err != nil { + return err + } + + key, err := s.CreatePreAuthKey(userName, true, false) + if err != nil { + return err + } + + err = s.RunTailscaleUp(userName, headscaleURL.String(), key.GetKey()) + if err != nil { + return err + } + } + + return nil +} + +func (s *EmbeddedDERPServerScenario) CreateTailscaleIsolatedNodesInUser( + hash string, + userStr string, + requestedVersion string, + count int, + opts ...tsic.Option, +) error { + if user, ok := s.users[userStr]; ok { + for clientN := 0; clientN < count; clientN++ { + networkName := fmt.Sprintf("tsnet-%s-%s-%d", + hash, + userStr, + clientN, + ) + network, err := dockertestutil.GetFirstOrCreateNetwork( + s.pool, + networkName, + ) + if err != nil { + return fmt.Errorf("failed to create or get %s network: %w", networkName, err) + } + + s.tsicNetworks[networkName] = network + + version := requestedVersion + if requestedVersion == "all" { + version = TailscaleVersions[clientN%len(TailscaleVersions)] + } + + headscale, err := s.Headscale() + if err != nil { + return fmt.Errorf("failed to create tailscale node: %w", err) + } + + cert := headscale.GetCert() + hostname := headscale.GetHostname() + + user.createWaitGroup.Add(1) + + opts = append(opts, + tsic.WithHeadscaleTLS(cert), + tsic.WithHeadscaleName(hostname), + ) + + go func() { + defer user.createWaitGroup.Done() + + // TODO(kradalby): error handle this + tsClient, err := tsic.New( + s.pool, + version, + network, + opts..., + ) + if err != nil { + // return fmt.Errorf("failed to add tailscale node: %w", err) + log.Printf("failed to create tailscale node: %s", err) + } + + err = tsClient.WaitForReady() + if err != nil { + // return fmt.Errorf("failed to add tailscale node: %w", err) + log.Printf("failed to wait for tailscaled: %s", err) + } + + user.Clients[tsClient.Hostname()] = tsClient + }() + } + user.createWaitGroup.Wait() + + return nil + } + + return fmt.Errorf("failed to add tailscale node: %w", errNoUserAvailable) +} + +func (s *EmbeddedDERPServerScenario) Shutdown() error { + for _, network := range s.tsicNetworks { + err := s.pool.RemoveNetwork(network) + if err != nil { + return err + } + } + + return s.Scenario.Shutdown() +} diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index e7dfd28..30a51c6 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -112,7 +112,7 @@ func WithPort(port int) Option { } } -// WithExtraPorts exposes additional ports on the container (e.g. 3478/udp for STUN) +// WithExtraPorts exposes additional ports on the container (e.g. 3478/udp for STUN). func WithExtraPorts(ports []string) Option { return func(hsic *HeadscaleInContainer) { hsic.extraPorts = ports @@ -190,14 +190,14 @@ func New( portProto := fmt.Sprintf("%d/tcp", hsic.port) - serverUrl, err := url.Parse(hsic.env["HEADSCALE_SERVER_URL"]) + serverURL, err := url.Parse(hsic.env["HEADSCALE_SERVER_URL"]) if err != nil { return nil, err } if len(hsic.tlsCert) != 0 && len(hsic.tlsKey) != 0 { - serverUrl.Scheme = "https" - hsic.env["HEADSCALE_SERVER_URL"] = serverUrl.String() + serverURL.Scheme = "https" + hsic.env["HEADSCALE_SERVER_URL"] = serverURL.String() } headscaleBuildOptions := &dockertest.BuildOptions{ diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index e5a469d..28d5d06 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -499,15 +499,6 @@ type ( } ) -type ( - DERPPingOption = func(args *derpPingArgs) - - derpPingArgs struct { - timeout time.Duration - count int - } -) - // WithPingTimeout sets the timeout for the ping command. func WithPingTimeout(timeout time.Duration) PingOption { return func(args *pingArgs) { @@ -605,7 +596,7 @@ func (t *TailscaleInContainer) PingViaDERP(hostnameOrIP string, opts ...PingOpti ), ) if err != nil { - fmt.Printf( + log.Printf( "failed to run ping command from %s to %s, err: %s", t.Hostname(), hostnameOrIP, diff --git a/integration/utils.go b/integration/utils.go index 43860b1..907c545 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -7,6 +7,11 @@ import ( "github.com/juanfont/headscale/integration/tsic" ) +const ( + derpPingTimeout = 2 * time.Second + derpPingCount = 10 +) + func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int { t.Helper() success := 0 @@ -37,8 +42,8 @@ func pingDerpAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) err := client.PingViaDERP( addr, - tsic.WithPingTimeout(2*time.Second), - tsic.WithPingCount(10), + tsic.WithPingTimeout(derpPingTimeout), + tsic.WithPingCount(derpPingCount), ) if err != nil { t.Errorf("failed to ping %s from %s: %s", addr, client.Hostname(), err)