mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-21 23:47:54 +00:00
Compare commits
12 Commits
v0.17.0-be
...
kradalby-g
Author | SHA1 | Date | |
---|---|---|---|
![]() |
94048a96e7 | ||
![]() |
a617edadf5 | ||
![]() |
6e83b7f06b | ||
![]() |
31d427b655 | ||
![]() |
d8c856e602 | ||
![]() |
aad4c90fe6 | ||
![]() |
4f9fe93146 | ||
![]() |
96fe6aa3a1 | ||
![]() |
947e961a3a | ||
![]() |
43731cad2e | ||
![]() |
ac15b21720 | ||
![]() |
dfc03a6124 |
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: CI
|
name: Lint
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
2
.github/workflows/test-integration-cli.yml
vendored
2
.github/workflows/test-integration-cli.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: CI
|
name: Integration Test CLI
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
2
.github/workflows/test-integration-derp.yml
vendored
2
.github/workflows/test-integration-derp.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: CI
|
name: Integration Test DERP
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
2
.github/workflows/test-integration-oidc.yml
vendored
2
.github/workflows/test-integration-oidc.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: CI
|
name: Integration Test OIDC
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
@@ -1,21 +1,16 @@
|
|||||||
name: CI
|
name: Integration Test v2 - kradalby
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
integration-test-v2-general:
|
integration-test-v2-kradalby:
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, linux, x64, nixos, docker]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Set Swap Space
|
|
||||||
uses: pierotofy/set-swap-space@master
|
|
||||||
with:
|
|
||||||
swap-size-gb: 10
|
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v14.1
|
uses: tj-actions/changed-files@v14.1
|
||||||
@@ -27,9 +22,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: nix develop --command -- make test_integration_v2_general
|
run: nix develop --command -- make test_integration_v2_general
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: CI
|
name: Tests
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/h
|
|||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
# Debug image
|
# Debug image
|
||||||
FROM gcr.io/distroless/base-debian11:debug
|
FROM docker.io/golang:1.19.0-bullseye
|
||||||
|
|
||||||
COPY --from=build /go/bin/headscale /bin/headscale
|
COPY --from=build /go/bin/headscale /bin/headscale
|
||||||
ENV TZ UTC
|
ENV TZ UTC
|
||||||
|
2
Makefile
2
Makefile
@@ -64,7 +64,7 @@ test_integration_v2_general:
|
|||||||
-v $$PWD:$$PWD -w $$PWD/integration \
|
-v $$PWD:$$PWD -w $$PWD/integration \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... -timeout 60m
|
go test ./... -timeout 60m -parallel 6
|
||||||
|
|
||||||
coverprofile_func:
|
coverprofile_func:
|
||||||
go tool cover -func=coverage.out
|
go tool cover -func=coverage.out
|
||||||
|
97
integration/hsic/config.go
Normal file
97
integration/hsic/config.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package hsic
|
||||||
|
|
||||||
|
// const (
|
||||||
|
// defaultEphemeralNodeInactivityTimeout = time.Second * 30
|
||||||
|
// defaultNodeUpdateCheckInterval = time.Second * 10
|
||||||
|
// )
|
||||||
|
|
||||||
|
// TODO(kradalby): This approach doesnt work because we cannot
|
||||||
|
// serialise our config object to YAML or JSON.
|
||||||
|
// func DefaultConfig() headscale.Config {
|
||||||
|
// derpMap, _ := url.Parse("https://controlplane.tailscale.com/derpmap/default")
|
||||||
|
//
|
||||||
|
// config := headscale.Config{
|
||||||
|
// Log: headscale.LogConfig{
|
||||||
|
// Level: zerolog.TraceLevel,
|
||||||
|
// },
|
||||||
|
// ACL: headscale.GetACLConfig(),
|
||||||
|
// DBtype: "sqlite3",
|
||||||
|
// EphemeralNodeInactivityTimeout: defaultEphemeralNodeInactivityTimeout,
|
||||||
|
// NodeUpdateCheckInterval: defaultNodeUpdateCheckInterval,
|
||||||
|
// IPPrefixes: []netip.Prefix{
|
||||||
|
// netip.MustParsePrefix("fd7a:115c:a1e0::/48"),
|
||||||
|
// netip.MustParsePrefix("100.64.0.0/10"),
|
||||||
|
// },
|
||||||
|
// DNSConfig: &tailcfg.DNSConfig{
|
||||||
|
// Proxied: true,
|
||||||
|
// Nameservers: []netip.Addr{
|
||||||
|
// netip.MustParseAddr("127.0.0.11"),
|
||||||
|
// netip.MustParseAddr("1.1.1.1"),
|
||||||
|
// },
|
||||||
|
// Resolvers: []*dnstype.Resolver{
|
||||||
|
// {
|
||||||
|
// Addr: "127.0.0.11",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Addr: "1.1.1.1",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// BaseDomain: "headscale.net",
|
||||||
|
//
|
||||||
|
// DBpath: "/tmp/integration_test_db.sqlite3",
|
||||||
|
//
|
||||||
|
// PrivateKeyPath: "/tmp/integration_private.key",
|
||||||
|
// NoisePrivateKeyPath: "/tmp/noise_integration_private.key",
|
||||||
|
// Addr: "0.0.0.0:8080",
|
||||||
|
// MetricsAddr: "127.0.0.1:9090",
|
||||||
|
// ServerURL: "http://headscale:8080",
|
||||||
|
//
|
||||||
|
// DERP: headscale.DERPConfig{
|
||||||
|
// URLs: []url.URL{
|
||||||
|
// *derpMap,
|
||||||
|
// },
|
||||||
|
// AutoUpdate: false,
|
||||||
|
// UpdateFrequency: 1 * time.Minute,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return config
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO: Reuse the actual configuration object above.
|
||||||
|
func DefaultConfigYAML() string {
|
||||||
|
yaml := `
|
||||||
|
log:
|
||||||
|
level: trace
|
||||||
|
acl_policy_path: ""
|
||||||
|
db_type: sqlite3
|
||||||
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
|
node_update_check_interval: 10s
|
||||||
|
ip_prefixes:
|
||||||
|
- fd7a:115c:a1e0::/48
|
||||||
|
- 100.64.0.0/10
|
||||||
|
dns_config:
|
||||||
|
base_domain: headscale.net
|
||||||
|
magic_dns: true
|
||||||
|
domains: []
|
||||||
|
nameservers:
|
||||||
|
- 127.0.0.11
|
||||||
|
- 1.1.1.1
|
||||||
|
private_key_path: /tmp/private.key
|
||||||
|
noise:
|
||||||
|
private_key_path: /tmp/noise_private.key
|
||||||
|
listen_addr: 0.0.0.0:8080
|
||||||
|
metrics_listen_addr: 127.0.0.1:9090
|
||||||
|
server_url: http://headscale:8080
|
||||||
|
|
||||||
|
derp:
|
||||||
|
urls:
|
||||||
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
|
auto_update_enabled: false
|
||||||
|
update_frequency: 1m
|
||||||
|
`
|
||||||
|
|
||||||
|
return yaml
|
||||||
|
}
|
@@ -1,23 +1,27 @@
|
|||||||
package hsic
|
package hsic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"path/filepath"
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/juanfont/headscale"
|
"github.com/juanfont/headscale"
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/ory/dockertest/v3/docker"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hsicHashLength = 6
|
hsicHashLength = 6
|
||||||
dockerContextPath = "../."
|
dockerContextPath = "../."
|
||||||
|
aclPolicyPath = "/etc/headscale/acl.hujson"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
||||||
@@ -29,44 +33,76 @@ type HeadscaleInContainer struct {
|
|||||||
pool *dockertest.Pool
|
pool *dockertest.Pool
|
||||||
container *dockertest.Resource
|
container *dockertest.Resource
|
||||||
network *dockertest.Network
|
network *dockertest.Network
|
||||||
|
|
||||||
|
// optional config
|
||||||
|
aclPolicy *headscale.ACLPolicy
|
||||||
|
env []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option = func(c *HeadscaleInContainer)
|
||||||
|
|
||||||
|
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.aclPolicy = acl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithConfigEnv(configEnv map[string]string) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
env := []string{}
|
||||||
|
|
||||||
|
for key, value := range configEnv {
|
||||||
|
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
hsic.env = env
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
pool *dockertest.Pool,
|
pool *dockertest.Pool,
|
||||||
port int,
|
port int,
|
||||||
network *dockertest.Network,
|
network *dockertest.Network,
|
||||||
|
opts ...Option,
|
||||||
) (*HeadscaleInContainer, error) {
|
) (*HeadscaleInContainer, error) {
|
||||||
hash, err := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
|
hash, err := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
|
||||||
Dockerfile: "Dockerfile",
|
|
||||||
ContextDir: dockerContextPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname := fmt.Sprintf("hs-%s", hash)
|
hostname := fmt.Sprintf("hs-%s", hash)
|
||||||
portProto := fmt.Sprintf("%d/tcp", port)
|
portProto := fmt.Sprintf("%d/tcp", port)
|
||||||
|
|
||||||
currentPath, err := os.Getwd()
|
hsic := &HeadscaleInContainer{
|
||||||
if err != nil {
|
hostname: hostname,
|
||||||
return nil, fmt.Errorf("could not determine current path: %w", err)
|
port: port,
|
||||||
|
|
||||||
|
pool: pool,
|
||||||
|
network: network,
|
||||||
}
|
}
|
||||||
|
|
||||||
integrationConfigPath := path.Join(currentPath, "..", "integration_test", "etc")
|
for _, opt := range opts {
|
||||||
|
opt(hsic)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hsic.aclPolicy != nil {
|
||||||
|
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_ACL_POLICY_PATH=%s", aclPolicyPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
|
Dockerfile: "Dockerfile.debug",
|
||||||
|
ContextDir: dockerContextPath,
|
||||||
|
}
|
||||||
|
|
||||||
runOptions := &dockertest.RunOptions{
|
runOptions := &dockertest.RunOptions{
|
||||||
Name: hostname,
|
Name: hostname,
|
||||||
// TODO(kradalby): Do something clever here, can we ditch the config repo?
|
|
||||||
// Always generate the config from code?
|
|
||||||
Mounts: []string{
|
|
||||||
fmt.Sprintf("%s:/etc/headscale", integrationConfigPath),
|
|
||||||
},
|
|
||||||
ExposedPorts: []string{portProto},
|
ExposedPorts: []string{portProto},
|
||||||
// TODO(kradalby): WHY do we need to bind these now that we run fully in docker?
|
Networks: []*dockertest.Network{network},
|
||||||
Networks: []*dockertest.Network{network},
|
// Cmd: []string{"headscale", "serve"},
|
||||||
Cmd: []string{"headscale", "serve"},
|
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
||||||
|
// to inject the headscale configuration further down.
|
||||||
|
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve"},
|
||||||
|
Env: hsic.env,
|
||||||
}
|
}
|
||||||
|
|
||||||
// dockertest isnt very good at handling containers that has already
|
// dockertest isnt very good at handling containers that has already
|
||||||
@@ -89,14 +125,26 @@ func New(
|
|||||||
}
|
}
|
||||||
log.Printf("Created %s container\n", hostname)
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
return &HeadscaleInContainer{
|
hsic.container = container
|
||||||
hostname: hostname,
|
|
||||||
port: port,
|
|
||||||
|
|
||||||
pool: pool,
|
err = hsic.WriteFile("/etc/headscale/config.yaml", []byte(DefaultConfigYAML()))
|
||||||
container: container,
|
if err != nil {
|
||||||
network: network,
|
return nil, fmt.Errorf("failed to write headscale config to container: %w", err)
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
if hsic.aclPolicy != nil {
|
||||||
|
data, err := json.Marshal(hsic.aclPolicy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal ACL Policy to JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = hsic.WriteFile(aclPolicyPath, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write ACL policy to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hsic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) Shutdown() error {
|
func (t *HeadscaleInContainer) Shutdown() error {
|
||||||
@@ -244,3 +292,57 @@ func (t *HeadscaleInContainer) ListMachinesInNamespace(
|
|||||||
|
|
||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
||||||
|
dirPath, fileName := filepath.Split(path)
|
||||||
|
|
||||||
|
file := bytes.NewReader(data)
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
|
||||||
|
tarWriter := tar.NewWriter(buf)
|
||||||
|
|
||||||
|
header := &tar.Header{
|
||||||
|
Name: fileName,
|
||||||
|
Size: file.Size(),
|
||||||
|
// Mode: int64(stat.Mode()),
|
||||||
|
// ModTime: stat.ModTime(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tarWriter.WriteHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed write file header to tar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(tarWriter, file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to copy file to tar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tarWriter.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close tar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("tar: %s", buf.String())
|
||||||
|
|
||||||
|
// Ensure the directory is present inside the container
|
||||||
|
_, err = t.Execute([]string{"mkdir", "-p", dirPath})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.pool.Client.UploadToContainer(
|
||||||
|
t.container.Container.ID,
|
||||||
|
docker.UploadToContainerOptions{
|
||||||
|
NoOverwriteDirNonDir: false,
|
||||||
|
Path: dirPath,
|
||||||
|
InputStream: bytes.NewReader(buf.Bytes()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -55,8 +55,7 @@ type Namespace struct {
|
|||||||
syncWaitGroup sync.WaitGroup
|
syncWaitGroup sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(kradalby): make control server configurable, test test correctness with
|
// TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.
|
||||||
// Tailscale SaaS.
|
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
// TODO(kradalby): support multiple headcales for later, currently only
|
// TODO(kradalby): support multiple headcales for later, currently only
|
||||||
// use one.
|
// use one.
|
||||||
@@ -152,7 +151,19 @@ func (s *Scenario) Namespaces() []string {
|
|||||||
|
|
||||||
// TODO(kradalby): make port and headscale configurable, multiple instances support?
|
// TODO(kradalby): make port and headscale configurable, multiple instances support?
|
||||||
func (s *Scenario) StartHeadscale() error {
|
func (s *Scenario) StartHeadscale() error {
|
||||||
headscale, err := hsic.New(s.pool, headscalePort, s.network)
|
headscale, err := hsic.New(s.pool, headscalePort, s.network,
|
||||||
|
hsic.WithACLPolicy(
|
||||||
|
&headscale.ACLPolicy{
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"*"},
|
||||||
|
Destinations: []string{"*:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create headscale container: %w", err)
|
return fmt.Errorf("failed to create headscale container: %w", err)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user