diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go new file mode 100644 index 0000000..40a0e69 --- /dev/null +++ b/integration/tsic/tsic.go @@ -0,0 +1,217 @@ +package tsic + +import ( + "errors" + "fmt" + "log" + "net/netip" + "strings" + + "github.com/juanfont/headscale" + "github.com/juanfont/headscale/integration/dockertestutil" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" +) + +const tsicHashLength = 6 +const dockerContextPath = "../." + +var errTailscalePingFailed = errors.New("ping failed") + +type TailscaleInContainer struct { + version string + Hostname string + + pool *dockertest.Pool + container *dockertest.Resource + network *dockertest.Network +} + +func New( + pool *dockertest.Pool, + version string, + network *dockertest.Network) (*TailscaleInContainer, error) { + hash, err := headscale.GenerateRandomStringDNSSafe(tsicHashLength) + if err != nil { + return nil, err + } + + hostname := fmt.Sprintf("ts-%s-%s", version, hash) + + // TODO(kradalby): figure out why we need to "refresh" the network here. + // network, err = dockertestutil.GetFirstOrCreateNetwork(pool, network.Network.Name) + // if err != nil { + // return nil, err + // } + + tailscaleOptions := &dockertest.RunOptions{ + Name: hostname, + Networks: []*dockertest.Network{network}, + Cmd: []string{ + "tailscaled", "--tun=tsdev", + }, + } + + // dockertest isnt very good at handling containers that has already + // been created, this is an attempt to make sure this container isnt + // present. + err = pool.RemoveContainerByName(hostname) + if err != nil { + return nil, err + } + + container, err := pool.BuildAndRunWithBuildOptions( + createTailscaleBuildOptions(version), + tailscaleOptions, + dockertestutil.DockerRestartPolicy, + dockertestutil.DockerAllowLocalIPv6, + dockertestutil.DockerAllowNetworkAdministration, + ) + if err != nil { + return nil, fmt.Errorf("could not start tailscale container: %w", err) + } + log.Printf("Created %s container\n", hostname) + + return &TailscaleInContainer{ + version: version, + Hostname: hostname, + + pool: pool, + container: container, + network: network, + }, nil +} + +func (t *TailscaleInContainer) Shutdown() error { + return t.pool.Purge(t.container) +} + +func (t *TailscaleInContainer) Up( + loginServer, authKey string, +) error { + command := []string{ + "tailscale", + "up", + "-login-server", + loginServer, + "--authkey", + authKey, + "--hostname", + t.Hostname, + } + + log.Println("Join command:", command) + log.Printf("Running join command for %s\n", t.Hostname) + _, _, err := dockertestutil.ExecuteCommand( + t.container, + command, + []string{}, + ) + if err != nil { + return err + } + log.Printf("%s joined\n", t.Hostname) + + return nil +} + +func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) { + ips := make([]netip.Addr, 0) + + command := []string{ + "tailscale", + "ip", + } + + result, _, err := dockertestutil.ExecuteCommand( + t.container, + command, + []string{}, + ) + if err != nil { + return []netip.Addr{}, err + } + + for _, address := range strings.Split(result, "\n") { + address = strings.TrimSuffix(address, "\n") + if len(address) < 1 { + continue + } + ip, err := netip.ParseAddr(address) + if err != nil { + return nil, err + } + ips = append(ips, ip) + } + + return ips, nil +} + +func (t *TailscaleInContainer) Ping(ip netip.Addr) error { + command := []string{ + "tailscale", "ping", + "--timeout=1s", + "--c=10", + "--until-direct=true", + ip.String(), + } + + result, _, err := dockertestutil.ExecuteCommand( + t.container, + command, + []string{}, + ) + if err != nil { + return err + } + + if !strings.Contains(result, "pong") { + return errTailscalePingFailed + } + + return nil +} + +func createTailscaleBuildOptions(version string) *dockertest.BuildOptions { + var tailscaleBuildOptions *dockertest.BuildOptions + switch version { + case "head": + tailscaleBuildOptions = &dockertest.BuildOptions{ + Dockerfile: "Dockerfile.tailscale-HEAD", + ContextDir: dockerContextPath, + BuildArgs: []docker.BuildArg{}, + } + case "unstable": + tailscaleBuildOptions = &dockertest.BuildOptions{ + Dockerfile: "Dockerfile.tailscale", + ContextDir: dockerContextPath, + BuildArgs: []docker.BuildArg{ + { + Name: "TAILSCALE_VERSION", + Value: "*", // Installs the latest version https://askubuntu.com/a/824926 + }, + { + Name: "TAILSCALE_CHANNEL", + Value: "unstable", + }, + }, + } + default: + tailscaleBuildOptions = &dockertest.BuildOptions{ + Dockerfile: "Dockerfile.tailscale", + ContextDir: dockerContextPath, + BuildArgs: []docker.BuildArg{ + { + Name: "TAILSCALE_VERSION", + Value: version, + }, + { + Name: "TAILSCALE_CHANNEL", + Value: "stable", + }, + }, + } + } + + return tailscaleBuildOptions +}