Compare commits

..

1 Commits

Author SHA1 Message Date
Kristoffer Dalby
ca125330fc Remove all tests before generating new ones
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-02 17:02:27 +01:00
26 changed files with 785 additions and 826 deletions

View File

@@ -63,7 +63,7 @@ jobs:
- name: Prettify code
if: steps.changed-files.outputs.any_changed == 'true'
uses: creyD/prettier_action@v4.3
uses: creyD/prettier_action@v4.0
with:
prettier_options: >-
--check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestExpireNode
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestExpireNode$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -9,8 +9,6 @@
- Make OpenID Connect authenticated client expiry time configurable [#1191](https://github.com/juanfont/headscale/pull/1191)
- defaults to 180 days like Tailscale SaaS
- adds option to use the expiry time from the OpenID token for the node (see config-example.yaml)
- Set ControlTime in Map info sent to nodes [#1195](https://github.com/juanfont/headscale/pull/1195)
- Populate Tags field on Node updates sent [#1195](https://github.com/juanfont/headscale/pull/1195)
## 0.19.0 (2023-01-29)

View File

@@ -81,11 +81,6 @@ one of the maintainers.
Please have a look at the documentation under [`docs/`](docs/).
## Talks
- Fosdem 2023 (video): [Headscale: How we are using integration testing to reimplement Tailscale](https://fosdem.org/2023/schedule/event/goheadscale/)
- presented by Juan Font Alonso and Kristoffer Dalby
## Disclaimer
1. We have nothing to do with Tailscale, or Tailscale Inc.

View File

@@ -1,8 +1,6 @@
package headscale
import (
"time"
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
)
@@ -57,46 +55,16 @@ func (h *Headscale) generateMapResponse(
peers,
)
now := time.Now()
resp := tailcfg.MapResponse{
KeepAlive: false,
Node: node,
// TODO: Only send if updated
DERPMap: h.DERPMap,
// TODO: Only send if updated
Peers: nodePeers,
// TODO(kradalby): Implement:
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L1351-L1374
// PeersChanged
// PeersRemoved
// PeersChangedPatch
// PeerSeenChange
// OnlineChange
// TODO: Only send if updated
DNSConfig: dnsConfig,
// TODO: Only send if updated
Domain: h.cfg.BaseDomain,
// Do not instruct clients to collect services, we do not
// support or do anything with them
CollectServices: "false",
// TODO: Only send if updated
KeepAlive: false,
Node: node,
Peers: nodePeers,
DNSConfig: dnsConfig,
Domain: h.cfg.BaseDomain,
PacketFilter: h.aclRules,
SSHPolicy: h.sshPolicy,
DERPMap: h.DERPMap,
UserProfiles: profiles,
// TODO: Only send if updated
SSHPolicy: h.sshPolicy,
ControlTime: &now,
Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort,

View File

@@ -157,14 +157,14 @@ func (h *Headscale) DERPHandler(
if !fastStart {
pubKey := h.privateKey.Public()
pubKeyStr, _ := pubKey.MarshalText() //nolint
pubKeyStr := pubKey.UntypedHexString() //nolint
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
"Upgrade: DERP\r\n"+
"Connection: Upgrade\r\n"+
"Derp-Version: %v\r\n"+
"Derp-Public-Key: %s\r\n\r\n",
derp.ProtocolVersion,
string(pubKeyStr))
pubKeyStr)
}
h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String())

View File

@@ -138,7 +138,6 @@ NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
WorkingDirectory=/var/lib/headscale
ReadWritePaths=/var/lib/headscale /var/run/headscale
AmbientCapabilities=CAP_NET_BIND_SERVICE
RuntimeDirectory=headscale

View File

@@ -10,14 +10,17 @@ describing how to make `headscale` run properly in a server environment.
1. Install from ports (Not Recommend)
As of OpenBSD 7.2, there's a headscale in ports collection, however, it's severely outdated(v0.12.4).
As of OpenBSD 7.1, there's a headscale in ports collection, however, it's severely outdated(v0.12.4).
You can install it via `pkg_add headscale`.
2. Install from source on OpenBSD 7.2
2. Install from source on OpenBSD 7.1
```shell
# Install prerequistes
pkg_add go
# 1. go v1.19+: headscale newer than 0.17 needs go 1.19+ to compile
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
pkg_add -D snap go
pkg_add gmake
git clone https://github.com/juanfont/headscale.git
@@ -30,7 +33,7 @@ latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
git checkout $latestTag
go build -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$latestTag" github.com/juanfont/headscale
gmake build
# make it executable
chmod a+x headscale

View File

@@ -36,7 +36,7 @@
# When updating go.mod or go.sum, a new sha will need to be calculated,
# update this if you have a mismatch after doing a change to thos files.
vendorSha256 = "sha256-QkBBzyKthDnjPCYtGDeQtW46FyWfScRczBQEhAdKHbs=";
vendorSha256 = "sha256-8p5NFxXKaZPsW4B6NMzfi0pqfVroIahSgA0fukvB3JI=";
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
};

110
go.mod
View File

@@ -6,52 +6,51 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029
github.com/cenkalti/backoff/v4 v4.2.0
github.com/coreos/go-oidc/v3 v3.5.0
github.com/coreos/go-oidc/v3 v3.4.0
github.com/davecgh/go-spew v1.1.1
github.com/deckarep/golang-set/v2 v2.1.0
github.com/efekarakus/termcolor v1.0.1
github.com/glebarez/sqlite v1.7.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/glebarez/sqlite v1.5.0
github.com/gofrs/uuid v4.3.1+incompatible
github.com/gorilla/mux v1.8.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1
github.com/klauspost/compress v1.15.15
github.com/grpc-ecosystem/grpc-gateway/v2 v2.14.0
github.com/klauspost/compress v1.15.12
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282
github.com/ory/dockertest/v3 v3.9.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/philip-bui/grpc-zerolog v1.0.1
github.com/prometheus/client_golang v1.14.0
github.com/prometheus/common v0.40.0
github.com/pterm/pterm v0.12.54
github.com/prometheus/common v0.37.0
github.com/pterm/pterm v0.12.50
github.com/puzpuzpuz/xsync/v2 v2.4.0
github.com/rs/zerolog v1.29.0
github.com/samber/lo v1.37.0
github.com/rs/zerolog v1.28.0
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.2
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/spf13/viper v1.14.0
github.com/stretchr/testify v1.8.1
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f
golang.org/x/crypto v0.6.0
golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.5.0
go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab
golang.org/x/crypto v0.3.0
golang.org/x/net v0.2.0
golang.org/x/oauth2 v0.2.0
golang.org/x/sync v0.1.0
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923
google.golang.org/grpc v1.53.0
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd
google.golang.org/grpc v1.51.0
google.golang.org/protobuf v1.28.1
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.4.8
gorm.io/gorm v1.24.5
tailscale.com v1.36.2
gorm.io/driver/postgres v1.4.5
gorm.io/gorm v1.24.2
tailscale.com v1.34.0
)
require (
atomicgo.dev/cursor v0.1.1 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/keyboard v0.2.8 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/akutz/memconn v0.1.0 // indirect
@@ -60,18 +59,15 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/docker/cli v23.0.1+incompatible // indirect
github.com/docker/docker v23.0.1+incompatible // indirect
github.com/docker/cli v20.10.21+incompatible // indirect
github.com/docker/docker v20.10.21+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/glebarez/go-sqlite v1.20.3 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/glebarez/go-sqlite v1.19.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
@@ -85,69 +81,71 @@ require (
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.13.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/jsimonetti/rtnetlink v1.3.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/jsimonetti/rtnetlink v1.3.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/lithammer/fuzzysearch v1.1.5 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mdlayher/netlink v1.7.1 // indirect
github.com/mdlayher/netlink v1.7.0 // indirect
github.com/mdlayher/socket v0.4.0 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/opencontainers/runc v1.1.4 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/afero v1.9.4 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.2.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.org/x/tools v0.3.0 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/libc v1.21.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.20.3 // indirect
modernc.org/sqlite v1.20.0 // indirect
nhooyr.io/websocket v1.8.7 // indirect
)

665
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -9,16 +9,6 @@ Headscale's test framework and the current set of scenarios are defined in this
Tests are located in files ending with `_test.go` and the framework are located in the rest.
## Running integration tests locally
The easiest way to run tests locally is to use `[act](INSERT LINK)`, a local GitHub Actions runner:
```
act pull_request -W .github/workflows/test-integration-v2-TestPingAllByIP.yaml
```
Alternatively, the `docker run` command in each GitHub workflow file can be used.
## Running integration tests on GitHub Actions
Each test currently runs as a separate workflows in GitHub actions, to add new test, run

View File

@@ -19,7 +19,6 @@ import (
"github.com/juanfont/headscale/integration/hsic"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/samber/lo"
)
const (
@@ -92,11 +91,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
return x.String()
})
success := pingAllHelper(t, allClients, allAddrs)
success := pingAll(t, allClients, allIps)
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
err = scenario.Shutdown()
@@ -162,11 +157,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
return x.String()
})
success := pingAllHelper(t, allClients, allAddrs)
success := pingAll(t, allClients, allIps)
t.Logf("%d successful pings out of %d (before expiry)", success, len(allClients)*len(allIps))
// await all nodes being logged out after OIDC token expiry
@@ -368,6 +359,24 @@ func (s *AuthOIDCScenario) runTailscaleUp(
return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
}
func pingAll(t *testing.T, clients []TailscaleClient, ips []netip.Addr) int {
t.Helper()
success := 0
for _, client := range clients {
for _, ip := range ips {
err := client.Ping(ip.String())
if err != nil {
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
} else {
success++
}
}
}
return success
}
func (s *AuthOIDCScenario) Shutdown() error {
err := s.pool.Purge(s.mockOIDC)
if err != nil {

View File

@@ -13,7 +13,6 @@ import (
"testing"
"github.com/juanfont/headscale/integration/hsic"
"github.com/samber/lo"
)
var errParseAuthPage = errors.New("failed to parse auth page")
@@ -60,11 +59,18 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
return x.String()
})
success := 0
for _, client := range allClients {
for _, ip := range allIps {
err := client.Ping(ip.String())
if err != nil {
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
} else {
success++
}
}
}
success := pingAllHelper(t, allClients, allAddrs)
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
err = scenario.Shutdown()
@@ -111,11 +117,18 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
return x.String()
})
success := 0
for _, client := range allClients {
for _, ip := range allIps {
err := client.Ping(ip.String())
if err != nil {
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
} else {
success++
}
}
}
success := pingAllHelper(t, allClients, allAddrs)
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
clientIPs := make(map[TailscaleClient][]netip.Addr)
@@ -162,11 +175,18 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
t.Errorf("failed to get clients: %s", err)
}
allAddrs = lo.Map(allIps, func(x netip.Addr, index int) string {
return x.String()
})
success = 0
for _, client := range allClients {
for _, ip := range allIps {
err := client.Ping(ip.String())
if err != nil {
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
} else {
success++
}
}
}
success = pingAllHelper(t, allClients, allAddrs)
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
for _, client := range allClients {
@@ -191,12 +211,7 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
}
if !found {
t.Errorf(
"IPs changed for client %s. Used to be %v now %v",
client.Hostname(),
clientIPs[client],
ips,
)
t.Errorf("IPs changed for client %s. Used to be %v now %v", client.Hostname(), clientIPs[client], ips)
}
}
}

View File

@@ -1,19 +1,15 @@
package integration
import (
"encoding/json"
"fmt"
"net/netip"
"strings"
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
func TestPingAllByIP(t *testing.T) {
@@ -50,11 +46,19 @@ func TestPingAllByIP(t *testing.T) {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
return x.String()
})
success := 0
for _, client := range allClients {
for _, ip := range allIps {
err := client.Ping(ip.String())
if err != nil {
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
} else {
success++
}
}
}
success := pingAllHelper(t, allClients, allAddrs)
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
err = scenario.Shutdown()
@@ -144,11 +148,18 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) {
t.Errorf("failed to get clients: %s", err)
}
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
return x.String()
})
success := 0
for _, client := range allClients {
for _, ip := range allIps {
err := client.Ping(ip.String())
if err != nil {
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
} else {
success++
}
}
}
success := pingAllHelper(t, allClients, allAddrs)
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
for _, client := range allClients {
@@ -173,12 +184,7 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) {
}
if !found {
t.Errorf(
"IPs changed for client %s. Used to be %v now %v",
client.Hostname(),
clientIPs[client],
ips,
)
t.Errorf("IPs changed for client %s. Used to be %v now %v", client.Hostname(), clientIPs[client], ips)
}
}
}
@@ -247,11 +253,18 @@ func TestEphemeral(t *testing.T) {
t.Errorf("failed to get clients: %s", err)
}
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
return x.String()
})
success := 0
for _, client := range allClients {
for _, ip := range allIps {
err := client.Ping(ip.String())
if err != nil {
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
} else {
success++
}
}
}
success := pingAllHelper(t, allClients, allAddrs)
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
for _, client := range allClients {
@@ -322,7 +335,18 @@ func TestPingAllByHostname(t *testing.T) {
t.Errorf("failed to get FQDNs: %s", err)
}
success := pingAllHelper(t, allClients, allHostnames)
success := 0
for _, client := range allClients {
for _, hostname := range allHostnames {
err := client.Ping(hostname)
if err != nil {
t.Errorf("failed to ping %s from %s: %s", hostname, client.Hostname(), err)
} else {
success++
}
}
}
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allClients))
@@ -558,93 +582,3 @@ func TestResolveMagicDNS(t *testing.T) {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestExpireNode(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
scenario, err := NewScenario()
if err != nil {
t.Errorf("failed to create scenario: %s", err)
}
spec := map[string]int{
"user1": len(TailscaleVersions),
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("expirenode"))
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)
}
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
return x.String()
})
success := pingAllHelper(t, allClients, allAddrs)
t.Logf("before expire: %d successful pings out of %d", success, len(allClients)*len(allIps))
for _, client := range allClients {
status, err := client.Status()
assert.NoError(t, err)
// Assert that we have the original count - self
assert.Len(t, status.Peers(), len(TailscaleVersions)-1)
}
headscale, err := scenario.Headscale()
assert.NoError(t, err)
// TODO(kradalby): This is Headscale specific and would not play nicely
// with other implementations of the ControlServer interface
result, err := headscale.Execute([]string{
"headscale", "nodes", "expire", "--identifier", "0", "--output", "json",
})
assert.NoError(t, err)
var machine v1.Machine
err = json.Unmarshal([]byte(result), &machine)
assert.NoError(t, err)
time.Sleep(30 * time.Second)
// Verify that the expired not is no longer present in the Peer list
// of connected nodes.
for _, client := range allClients {
status, err := client.Status()
assert.NoError(t, err)
for _, peerKey := range status.Peers() {
peerStatus := status.Peer[peerKey]
peerPublicKey := strings.TrimPrefix(peerStatus.PublicKey.String(), "nodekey:")
assert.NotEqual(t, machine.NodeKey, peerPublicKey)
}
if client.Hostname() != machine.Name {
// Assert that we have the original count - self - expired node
assert.Len(t, status.Peers(), len(TailscaleVersions)-2)
}
}
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}

View File

@@ -41,8 +41,6 @@ type fileInContainer struct {
contents []byte
}
// HeadscaleInContainer is an implementation of ControlServer which
// sets up a Headscale instance inside a container.
type HeadscaleInContainer struct {
hostname string
@@ -59,12 +57,8 @@ type HeadscaleInContainer struct {
filesInContainer []fileInContainer
}
// Option represent optional settings that can be given to a
// Headscale instance.
type Option = func(c *HeadscaleInContainer)
// WithACLPolicy adds a headscale.ACLPolicy policy to the
// HeadscaleInContainer instance.
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
return func(hsic *HeadscaleInContainer) {
// TODO(kradalby): Move somewhere appropriate
@@ -74,7 +68,6 @@ func WithACLPolicy(acl *headscale.ACLPolicy) Option {
}
}
// WithTLS creates certificates and enables HTTPS.
func WithTLS() Option {
return func(hsic *HeadscaleInContainer) {
cert, key, err := createCertificate()
@@ -91,8 +84,6 @@ func WithTLS() Option {
}
}
// WithConfigEnv takes a map of environment variables that
// can be used to override Headscale configuration.
func WithConfigEnv(configEnv map[string]string) Option {
return func(hsic *HeadscaleInContainer) {
for key, value := range configEnv {
@@ -101,15 +92,12 @@ func WithConfigEnv(configEnv map[string]string) Option {
}
}
// WithPort sets the port on where to run Headscale.
func WithPort(port int) Option {
return func(hsic *HeadscaleInContainer) {
hsic.port = port
}
}
// WithTestName sets a a name for the test, this will be reflected
// in the Docker container name.
func WithTestName(testName string) Option {
return func(hsic *HeadscaleInContainer) {
hash, _ := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
@@ -119,8 +107,6 @@ func WithTestName(testName string) Option {
}
}
// WithHostnameAsServerURL sets the Headscale ServerURL based on
// the Hostname.
func WithHostnameAsServerURL() Option {
return func(hsic *HeadscaleInContainer) {
hsic.env["HEADSCALE_SERVER_URL"] = fmt.Sprintf("http://%s",
@@ -130,7 +116,6 @@ func WithHostnameAsServerURL() Option {
}
}
// WithFileInContainer adds a file to the container at the given path.
func WithFileInContainer(path string, contents []byte) Option {
return func(hsic *HeadscaleInContainer) {
hsic.filesInContainer = append(hsic.filesInContainer,
@@ -141,7 +126,6 @@ func WithFileInContainer(path string, contents []byte) Option {
}
}
// New returns a new HeadscaleInContainer instance.
func New(
pool *dockertest.Pool,
network *dockertest.Network,
@@ -260,19 +244,14 @@ func (t *HeadscaleInContainer) hasTLS() bool {
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
}
// Shutdown stops and cleans up the Headscale container.
func (t *HeadscaleInContainer) Shutdown() error {
return t.pool.Purge(t.container)
}
// SaveLog saves the current stdout log of the container to a path
// on the host system.
func (t *HeadscaleInContainer) SaveLog(path string) error {
return dockertestutil.SaveLog(t.pool, t.container, path)
}
// Execute runs a command inside the Headscale container and returns the
// result of stdout as a string.
func (t *HeadscaleInContainer) Execute(
command []string,
) (string, error) {
@@ -294,23 +273,18 @@ func (t *HeadscaleInContainer) Execute(
return stdout, nil
}
// GetIP returns the docker container IP as a string.
func (t *HeadscaleInContainer) GetIP() string {
return t.container.GetIPInNetwork(t.network)
}
// GetPort returns the docker container port as a string.
func (t *HeadscaleInContainer) GetPort() string {
return fmt.Sprintf("%d", t.port)
}
// GetHealthEndpoint returns a health endpoint for the HeadscaleInContainer
// instance.
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
return fmt.Sprintf("%s/health", t.GetEndpoint())
}
// GetEndpoint returns the Headscale endpoint for the HeadscaleInContainer.
func (t *HeadscaleInContainer) GetEndpoint() string {
hostEndpoint := fmt.Sprintf("%s:%d",
t.GetIP(),
@@ -323,18 +297,14 @@ func (t *HeadscaleInContainer) GetEndpoint() string {
return fmt.Sprintf("http://%s", hostEndpoint)
}
// GetCert returns the public certificate of the HeadscaleInContainer.
func (t *HeadscaleInContainer) GetCert() []byte {
return t.tlsCert
}
// GetHostname returns the hostname of the HeadscaleInContainer.
func (t *HeadscaleInContainer) GetHostname() string {
return t.hostname
}
// WaitForReady blocks until the Headscale instance is ready to
// serve clients.
func (t *HeadscaleInContainer) WaitForReady() error {
url := t.GetHealthEndpoint()
@@ -362,7 +332,6 @@ func (t *HeadscaleInContainer) WaitForReady() error {
})
}
// CreateUser adds a new user to the Headscale instance.
func (t *HeadscaleInContainer) CreateUser(
user string,
) error {
@@ -380,8 +349,6 @@ func (t *HeadscaleInContainer) CreateUser(
return nil
}
// CreateAuthKey creates a new "authorisation key" for a User that can be used
// to authorise a TailscaleClient with the Headscale instance.
func (t *HeadscaleInContainer) CreateAuthKey(
user string,
reusable bool,
@@ -425,8 +392,6 @@ func (t *HeadscaleInContainer) CreateAuthKey(
return &preAuthKey, nil
}
// ListMachinesInUser list the TailscaleClients (Machine, Headscale internal representation)
// associated with a user.
func (t *HeadscaleInContainer) ListMachinesInUser(
user string,
) ([]*v1.Machine, error) {
@@ -450,7 +415,6 @@ func (t *HeadscaleInContainer) ListMachinesInUser(
return nodes, nil
}
// WriteFile save file inside the Headscale container.
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
}

View File

@@ -26,7 +26,6 @@ const (
var (
errNoHeadscaleAvailable = errors.New("no headscale available")
errNoUserAvailable = errors.New("no user available")
errNoClientFound = errors.New("client not found")
// Tailscale started adding TS2021 support in CapabilityVersion>=28 (v1.24.0), but
// proper support in Headscale was only added for CapabilityVersion>=39 clients (v1.30.0).
@@ -57,23 +56,12 @@ var (
// "1.8.7",
// }.
// TailscaleVersions represents a list of Tailscale versions the suite
// uses to test compatibility with the ControlServer.
//
// The list contains two special cases, "head" and "unstable" which
// points to the current tip of Tailscale's main branch and the latest
// released unstable version.
//
// The rest of the version represents Tailscale versions that can be
// found in Tailscale's apt repository.
TailscaleVersions = append(
tailscaleVersions2021,
tailscaleVersions2019...,
)
)
// User represents a User in the ControlServer and a map of TailscaleClient's
// associated with the User.
type User struct {
Clients map[string]TailscaleClient
@@ -82,10 +70,6 @@ type User struct {
syncWaitGroup sync.WaitGroup
}
// Scenario is a representation of an environment with one ControlServer and
// one or more User's and its associated TailscaleClients.
// A Scenario is intended to simplify setting up a new testcase for testing
// a ControlServer with TailscaleClients.
// TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.
type Scenario struct {
// TODO(kradalby): support multiple headcales for later, currently only
@@ -100,8 +84,6 @@ type Scenario struct {
headscaleLock sync.Mutex
}
// NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with
// a set of Users and TailscaleClients.
func NewScenario() (*Scenario, error) {
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
if err != nil {
@@ -142,10 +124,6 @@ func NewScenario() (*Scenario, error) {
}, nil
}
// Shutdown shuts down and cleans up all the containers (ControlServer, TailscaleClient)
// and networks associated with it.
// In addition, it will save the logs of the ControlServer to `/tmp/control` in the
// environment running the tests.
func (s *Scenario) Shutdown() error {
s.controlServers.Range(func(_ string, control ControlServer) bool {
err := control.SaveLog("/tmp/control")
@@ -189,7 +167,6 @@ func (s *Scenario) Shutdown() error {
return nil
}
// Users returns the name of all users associated with the Scenario.
func (s *Scenario) Users() []string {
users := make([]string, 0)
for user := range s.users {
@@ -202,9 +179,6 @@ func (s *Scenario) Users() []string {
/// Headscale related stuff
// Note: These functions assume that there is a _single_ headscale instance for now
// Headscale returns a ControlServer instance based on hsic (HeadscaleInContainer)
// If the Scenario already has an instance, the pointer to the running container
// will be return, otherwise a new instance will be created.
// TODO(kradalby): make port and headscale configurable, multiple instances support?
func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
s.headscaleLock.Lock()
@@ -229,13 +203,7 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
return headscale, nil
}
// CreatePreAuthKey creates a "pre authentorised key" to be created in the
// Headscale instance on behalf of the Scenario.
func (s *Scenario) CreatePreAuthKey(
user string,
reusable bool,
ephemeral bool,
) (*v1.PreAuthKey, error) {
func (s *Scenario) CreatePreAuthKey(user string, reusable bool, ephemeral bool) (*v1.PreAuthKey, error) {
if headscale, err := s.Headscale(); err == nil {
key, err := headscale.CreateAuthKey(user, reusable, ephemeral)
if err != nil {
@@ -248,8 +216,6 @@ func (s *Scenario) CreatePreAuthKey(
return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable)
}
// CreateUser creates a User to be created in the
// Headscale instance on behalf of the Scenario.
func (s *Scenario) CreateUser(user string) error {
if headscale, err := s.Headscale(); err == nil {
err := headscale.CreateUser(user)
@@ -269,8 +235,6 @@ func (s *Scenario) CreateUser(user string) error {
/// Client related stuff
// CreateTailscaleNodesInUser creates and adds a new TailscaleClient to a
// User in the Scenario.
func (s *Scenario) CreateTailscaleNodesInUser(
userStr string,
requestedVersion string,
@@ -331,8 +295,6 @@ func (s *Scenario) CreateTailscaleNodesInUser(
return fmt.Errorf("failed to add tailscale node: %w", errNoUserAvailable)
}
// RunTailscaleUp will log in all of the TailscaleClients associated with a
// User to the given ControlServer (by URL).
func (s *Scenario) RunTailscaleUp(
userStr, loginServer, authKey string,
) error {
@@ -361,8 +323,6 @@ func (s *Scenario) RunTailscaleUp(
return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
}
// CountTailscale returns the total number of TailscaleClients in a Scenario.
// This is the sum of Users x TailscaleClients.
func (s *Scenario) CountTailscale() int {
count := 0
@@ -373,8 +333,6 @@ func (s *Scenario) CountTailscale() int {
return count
}
// WaitForTailscaleSync blocks execution until all the TailscaleClient reports
// to have all other TailscaleClients present in their netmap.NetworkMap.
func (s *Scenario) WaitForTailscaleSync() error {
tsCount := s.CountTailscale()
@@ -395,7 +353,7 @@ func (s *Scenario) WaitForTailscaleSync() error {
return nil
}
// CreateHeadscaleEnv is a conventient method returning a complete Headcale
// CreateHeadscaleEnv is a conventient method returning a set up Headcale
// test environment with nodes of all versions, joined to the server with X
// users.
func (s *Scenario) CreateHeadscaleEnv(
@@ -433,8 +391,6 @@ func (s *Scenario) CreateHeadscaleEnv(
return nil
}
// GetIPs returns all netip.Addr of TailscaleClients associated with a User
// in a Scenario.
func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
var ips []netip.Addr
if ns, ok := s.users[user]; ok {
@@ -452,7 +408,6 @@ func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
return ips, fmt.Errorf("failed to get ips: %w", errNoUserAvailable)
}
// GetIPs returns all TailscaleClients associated with a User in a Scenario.
func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
var clients []TailscaleClient
if ns, ok := s.users[user]; ok {
@@ -466,8 +421,6 @@ func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
return clients, fmt.Errorf("failed to get clients: %w", errNoUserAvailable)
}
// ListTailscaleClients returns a list of TailscaleClients given the Users
// passed as parameters.
func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, error) {
var allClients []TailscaleClient
@@ -487,28 +440,6 @@ func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, err
return allClients, nil
}
// FindTailscaleClientByIP returns a TailscaleClient associated with an IP address
// if it exists.
func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, error) {
clients, err := s.ListTailscaleClients()
if err != nil {
return nil, err
}
for _, client := range clients {
ips, _ := client.IPs()
for _, ip2 := range ips {
if ip == ip2 {
return client, nil
}
}
}
return nil, errNoClientFound
}
// ListTailscaleClientsIPs returns a list of netip.Addr based on Users
// passed as parameters.
func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error) {
var allIps []netip.Addr
@@ -528,8 +459,6 @@ func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error
return allIps, nil
}
// ListTailscaleClientsIPs returns a list of FQDN based on Users
// passed as parameters.
func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) {
allFQDNs := make([]string, 0)
@@ -550,8 +479,6 @@ func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error)
return allFQDNs, nil
}
// WaitForTailscaleLogout blocks execution until all TailscaleClients have
// logged out of the ControlServer.
func (s *Scenario) WaitForTailscaleLogout() {
for _, user := range s.users {
for _, client := range user.Clients {

View File

@@ -4,7 +4,6 @@ import (
"net/netip"
"net/url"
"github.com/juanfont/headscale/integration/tsic"
"tailscale.com/ipn/ipnstate"
)
@@ -23,6 +22,6 @@ type TailscaleClient interface {
WaitForReady() error
WaitForLogout() error
WaitForPeers(expected int) error
Ping(hostnameOrIP string, opts ...tsic.PingOption) error
Ping(hostnameOrIP string) error
ID() string
}

View File

@@ -7,9 +7,7 @@ import (
"log"
"net/netip"
"net/url"
"strconv"
"strings"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/juanfont/headscale"
@@ -22,7 +20,6 @@ import (
const (
tsicHashLength = 6
defaultPingCount = 10
dockerContextPath = "../."
headscaleCertPath = "/usr/local/share/ca-certificates/headscale.crt"
)
@@ -36,8 +33,6 @@ var (
errTailscaleNotLoggedOut = errors.New("tailscale not logged out")
)
// TailscaleInContainer is an implementation of TailscaleClient which
// sets up a Tailscale instance inside a container.
type TailscaleInContainer struct {
version string
hostname string
@@ -54,25 +49,16 @@ type TailscaleInContainer struct {
headscaleCert []byte
headscaleHostname string
withSSH bool
withTags []string
}
// Option represent optional settings that can be given to a
// Tailscale instance.
type Option = func(c *TailscaleInContainer)
// WithHeadscaleTLS takes the certificate of the Headscale instance
// and adds it to the trusted surtificate of the Tailscale container.
func WithHeadscaleTLS(cert []byte) Option {
return func(tsic *TailscaleInContainer) {
tsic.headscaleCert = cert
}
}
// WithOrCreateNetwork sets the Docker container network to use with
// the Tailscale instance, if the parameter is nil, a new network,
// isolating the TailscaleClient, will be created. If a network is
// passed, the Tailscale instance will join the given network.
func WithOrCreateNetwork(network *dockertest.Network) Option {
return func(tsic *TailscaleInContainer) {
if network != nil {
@@ -93,29 +79,18 @@ func WithOrCreateNetwork(network *dockertest.Network) Option {
}
}
// WithHeadscaleName set the name of the headscale instance,
// mostly useful in combination with TLS and WithHeadscaleTLS.
func WithHeadscaleName(hsName string) Option {
return func(tsic *TailscaleInContainer) {
tsic.headscaleHostname = hsName
}
}
// WithTags associates the given tags to the Tailscale instance.
func WithTags(tags []string) Option {
return func(tsic *TailscaleInContainer) {
tsic.withTags = tags
}
}
// WithSSH enables SSH for the Tailscale instance.
func WithSSH() Option {
return func(tsic *TailscaleInContainer) {
tsic.withSSH = true
}
}
// New returns a new TailscaleInContainer instance.
func New(
pool *dockertest.Pool,
version string,
@@ -197,29 +172,22 @@ func (t *TailscaleInContainer) hasTLS() bool {
return len(t.headscaleCert) != 0
}
// Shutdown stops and cleans up the Tailscale container.
func (t *TailscaleInContainer) Shutdown() error {
return t.pool.Purge(t.container)
}
// Hostname returns the hostname of the Tailscale instance.
func (t *TailscaleInContainer) Hostname() string {
return t.hostname
}
// Version returns the running Tailscale version of the instance.
func (t *TailscaleInContainer) Version() string {
return t.version
}
// ID returns the Docker container ID of the TailscaleInContainer
// instance.
func (t *TailscaleInContainer) ID() string {
return t.container.Container.ID
}
// Execute runs a command inside the Tailscale container and returns the
// result of stdout as a string.
func (t *TailscaleInContainer) Execute(
command []string,
) (string, string, error) {
@@ -245,8 +213,6 @@ func (t *TailscaleInContainer) Execute(
return stdout, stderr, nil
}
// Up runs the login routine on the given Tailscale instance.
// This login mechanism uses the authorised key for authentication.
func (t *TailscaleInContainer) Up(
loginServer, authKey string,
) error {
@@ -265,12 +231,6 @@ func (t *TailscaleInContainer) Up(
command = append(command, "--ssh")
}
if len(t.withTags) > 0 {
command = append(command,
fmt.Sprintf(`--advertise-tags=%s`, strings.Join(t.withTags, ",")),
)
}
if _, _, err := t.Execute(command); err != nil {
return fmt.Errorf("failed to join tailscale client: %w", err)
}
@@ -278,8 +238,6 @@ func (t *TailscaleInContainer) Up(
return nil
}
// Up runs the login routine on the given Tailscale instance.
// This login mechanism uses web + command line flow for authentication.
func (t *TailscaleInContainer) UpWithLoginURL(
loginServer string,
) (*url.URL, error) {
@@ -312,7 +270,6 @@ func (t *TailscaleInContainer) UpWithLoginURL(
return loginURL, nil
}
// Logout runs the logout routine on the given Tailscale instance.
func (t *TailscaleInContainer) Logout() error {
_, _, err := t.Execute([]string{"tailscale", "logout"})
if err != nil {
@@ -322,7 +279,6 @@ func (t *TailscaleInContainer) Logout() error {
return nil
}
// IPs returns the netip.Addr of the Tailscale instance.
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
if t.ips != nil && len(t.ips) != 0 {
return t.ips, nil
@@ -355,7 +311,6 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
return ips, nil
}
// Status returns the ipnstate.Status of the Tailscale instance.
func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
command := []string{
"tailscale",
@@ -377,7 +332,6 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
return &status, err
}
// FQDN returns the FQDN as a string of the Tailscale instance.
func (t *TailscaleInContainer) FQDN() (string, error) {
if t.fqdn != "" {
return t.fqdn, nil
@@ -391,8 +345,6 @@ func (t *TailscaleInContainer) FQDN() (string, error) {
return status.Self.DNSName, nil
}
// WaitForReady blocks until the Tailscale (tailscaled) instance is ready
// to login or be used.
func (t *TailscaleInContainer) WaitForReady() error {
return t.pool.Retry(func() error {
status, err := t.Status()
@@ -408,7 +360,6 @@ func (t *TailscaleInContainer) WaitForReady() error {
})
}
// WaitForLogout blocks until the Tailscale instance has logged out.
func (t *TailscaleInContainer) WaitForLogout() error {
return t.pool.Retry(func() error {
status, err := t.Status()
@@ -424,8 +375,6 @@ func (t *TailscaleInContainer) WaitForLogout() error {
})
}
// WaitForPeers blocks until N number of peers is present in the
// Peer list of the Tailscale instance.
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
return t.pool.Retry(func() error {
status, err := t.Status()
@@ -441,65 +390,17 @@ func (t *TailscaleInContainer) WaitForPeers(expected int) error {
})
}
type (
// PingOption repreent optional settings that can be given
// to ping another host.
PingOption = func(args *pingArgs)
pingArgs struct {
timeout time.Duration
count int
direct bool
}
)
// WithPingTimeout sets the timeout for the ping command.
func WithPingTimeout(timeout time.Duration) PingOption {
return func(args *pingArgs) {
args.timeout = timeout
}
}
// WithPingCount sets the count of pings to attempt.
func WithPingCount(count int) PingOption {
return func(args *pingArgs) {
args.count = count
}
}
// WithPingUntilDirect decides if the ping should only succeed
// if a direct connection is established or if successful
// DERP ping is sufficient.
func WithPingUntilDirect(direct bool) PingOption {
return func(args *pingArgs) {
args.direct = direct
}
}
// Ping executes the Tailscale ping command and pings a hostname
// or IP. It accepts a series of PingOption.
// TODO(kradalby): Make multiping, go routine magic.
func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) error {
args := pingArgs{
timeout: time.Second,
count: defaultPingCount,
direct: true,
}
for _, opt := range opts {
opt(&args)
}
command := []string{
"tailscale", "ping",
fmt.Sprintf("--timeout=%s", args.timeout),
fmt.Sprintf("--c=%d", args.count),
fmt.Sprintf("--until-direct=%s", strconv.FormatBool(args.direct)),
}
command = append(command, hostnameOrIP)
func (t *TailscaleInContainer) Ping(hostnameOrIP string) error {
return t.pool.Retry(func() error {
command := []string{
"tailscale", "ping",
"--timeout=1s",
"--c=10",
"--until-direct=true",
hostnameOrIP,
}
result, _, err := t.Execute(command)
if err != nil {
log.Printf(
@@ -520,7 +421,6 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
})
}
// WriteFile save file inside the Tailscale container.
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
}

View File

@@ -1,48 +0,0 @@
package integration
import (
"testing"
)
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
t.Helper()
success := 0
for _, client := range clients {
for _, addr := range addrs {
err := client.Ping(addr)
if err != nil {
t.Errorf("failed to ping %s from %s: %s", addr, client.Hostname(), err)
} else {
success++
}
}
}
return success
}
// pingAllNegativeHelper is intended to have 1 or more nodes timeing out from the ping,
// it counts failures instead of successes.
// func pingAllNegativeHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
// t.Helper()
// failures := 0
//
// timeout := 100
// count := 3
//
// for _, client := range clients {
// for _, addr := range addrs {
// err := client.Ping(
// addr,
// tsic.WithPingTimeout(time.Duration(timeout)*time.Millisecond),
// tsic.WithPingCount(count),
// )
// if err != nil {
// failures++
// }
// }
// }
//
// return failures
// }

View File

@@ -24,6 +24,7 @@ type IntegrationCLITestSuite struct {
pool dockertest.Pool
network dockertest.Network
headscale dockertest.Resource
env []string
}
func TestIntegrationCLITestSuite(t *testing.T) {

View File

@@ -9,6 +9,7 @@ import (
"net/netip"
"os"
"strconv"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
@@ -31,8 +32,7 @@ var (
tailscaleVersions = []string{
"head",
"unstable",
"1.36.2",
"1.34.2",
"1.34.0",
"1.32.3",
"1.30.2",
"1.28.0",
@@ -47,6 +47,11 @@ var (
}
)
type TestUser struct {
count int
tailscales map[string]dockertest.Resource
}
type ExecuteCommandConfig struct {
timeout time.Duration
}
@@ -196,6 +201,38 @@ func getDockerBuildOptions(version string) *dockertest.BuildOptions {
return tailscaleBuildOptions
}
func getIPs(
tailscales map[string]dockertest.Resource,
) (map[string][]netip.Addr, error) {
ips := make(map[string][]netip.Addr)
for hostname, tailscale := range tailscales {
command := []string{"tailscale", "ip"}
result, _, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
if err != nil {
return nil, 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[hostname] = append(ips[hostname], ip)
}
}
return ips, nil
}
func getDNSNames(
headscale *dockertest.Resource,
) ([]string, error) {
@@ -229,6 +266,43 @@ func getDNSNames(
return hostnames, nil
}
func getMagicFQDN(
headscale *dockertest.Resource,
) ([]string, error) {
listAllResult, _, err := ExecuteCommand(
headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
},
[]string{},
)
if err != nil {
return nil, err
}
var listAll []v1.Machine
err = json.Unmarshal([]byte(listAllResult), &listAll)
if err != nil {
return nil, err
}
hostnames := make([]string, len(listAll))
for index := range listAll {
hostnames[index] = fmt.Sprintf(
"%s.%s.headscale.net",
listAll[index].GetGivenName(),
listAll[index].GetUser().GetName(),
)
}
return hostnames, nil
}
func GetEnvStr(key string) (string, error) {
v := os.Getenv(key)
if v == "" {

View File

@@ -382,7 +382,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
err = os.WriteFile(
path.Join(basePath, resource.Container.Name+".stdout.log"),
stderr.Bytes(),
[]byte(stdout.String()),
0o644,
)
if err != nil {
@@ -391,7 +391,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
err = os.WriteFile(
path.Join(basePath, resource.Container.Name+".stderr.log"),
stderr.Bytes(),
[]byte(stdout.String()),
0o644,
)
if err != nil {

View File

@@ -12,7 +12,6 @@ import (
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"google.golang.org/protobuf/types/known/timestamppb"
"gorm.io/gorm"
"tailscale.com/tailcfg"
@@ -739,39 +738,30 @@ func (h *Headscale) toNode(
online := machine.isOnline()
tags, _ := getTags(h.aclPolicy, machine, h.cfg.OIDC.StripEmaildomain)
tags = lo.Uniq(append(tags, machine.ForcedTags...))
node := tailcfg.Node{
ID: tailcfg.NodeID(machine.ID), // this is the actual ID
StableID: tailcfg.StableNodeID(
strconv.FormatUint(machine.ID, Base10),
), // in headscale, unlike tailcontrol server, IDs are permanent
Name: hostname,
User: tailcfg.UserID(machine.UserID),
Key: nodeKey,
KeyExpiry: keyExpiry,
Machine: machineKey,
DiscoKey: discoKey,
Addresses: addrs,
AllowedIPs: allowedIPs,
Endpoints: machine.Endpoints,
DERP: derp,
Hostinfo: hostInfo.View(),
Created: machine.CreatedAt,
Tags: tags,
Name: hostname,
User: tailcfg.UserID(machine.UserID),
Key: nodeKey,
KeyExpiry: keyExpiry,
Machine: machineKey,
DiscoKey: discoKey,
Addresses: addrs,
AllowedIPs: allowedIPs,
PrimaryRoutes: primaryPrefixes,
Endpoints: machine.Endpoints,
DERP: derp,
Online: &online,
Hostinfo: hostInfo.View(),
Created: machine.CreatedAt,
LastSeen: machine.LastSeen,
LastSeen: machine.LastSeen,
Online: &online,
KeepAlive: true,
MachineAuthorized: !machine.isExpired(),
Capabilities: []string{
tailcfg.CapabilityFileSharing,
tailcfg.CapabilityAdmin,
@@ -1122,8 +1112,7 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error {
routes := []Route{}
err := h.db.
Preload("Machine").
Where("machine_id = ? AND advertised = true AND enabled = false", machine.ID).
Find(&routes).Error
Where("machine_id = ? AND advertised = true AND enabled = false", machine.ID).Find(&routes).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
log.Error().
Caller().
@@ -1137,9 +1126,7 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error {
approvedRoutes := []Route{}
for _, advertisedRoute := range routes {
routeApprovers, err := h.aclPolicy.AutoApprovers.GetRouteApprovers(
netip.Prefix(advertisedRoute.Prefix),
)
routeApprovers, err := h.aclPolicy.AutoApprovers.GetRouteApprovers(netip.Prefix(advertisedRoute.Prefix))
if err != nil {
log.Err(err).
Str("advertisedRoute", advertisedRoute.String()).

View File

@@ -4,27 +4,11 @@
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>headscale - Apple</title>
<style>
body {
margin: 40px auto;
max-width: 800px;
line-height: 1.5;
font-size: 16px;
color: #444;
padding: 0 10px;
font-family: Sans-serif;
}
h1,
h2,
h3 {
line-height: 1.2;
}
</style>
<title>Document</title>
</head>
<body>
<h1>headscale: Apple configuration</h1>
<h1>headscale</h1>
<h2>Recent Tailscale versions (1.34.0 and higher)</h2>
<p>
Tailscale added Fast User Switching in version 1.34 and you can now use

View File

@@ -4,27 +4,11 @@
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>headscale - Windows</title>
<style>
body {
margin: 40px auto;
max-width: 800px;
line-height: 1.5;
font-size: 16px;
color: #444;
padding: 0 10px;
font-family: Sans-serif;
}
h1,
h2,
h3 {
line-height: 1.2;
}
</style>
<title>Document</title>
</head>
<body>
<h1>headscale: Windows configuration</h1>
<h1>headscale</h1>
<h2>Recent Tailscale versions (1.34.0 and higher)</h2>
<p>
Tailscale added Fast User Switching in version 1.34 and you can now use