mirror of
https://github.com/juanfont/headscale.git
synced 2025-12-17 06:52:32 +00:00
Compare commits
23 Commits
v0.17.0-be
...
v0.17.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d79179141 | ||
|
|
275cc28193 | ||
|
|
c5ba7552c5 | ||
|
|
8909f801bb | ||
|
|
3d4af52b3a | ||
|
|
6391555dab | ||
|
|
8cc5b2174b | ||
|
|
9269dd01f5 | ||
|
|
ef68f17a96 | ||
|
|
f74266f8f8 | ||
|
|
46df219ed3 | ||
|
|
835288d864 | ||
|
|
93d56362af | ||
|
|
4799859be0 | ||
|
|
8e44596171 | ||
|
|
d479234058 | ||
|
|
3fc5866de0 | ||
|
|
f3c40086ac | ||
|
|
09ed21edd8 | ||
|
|
456479eaa1 | ||
|
|
cb87852825 | ||
|
|
69440058bb | ||
|
|
9bc6ac0f35 |
78
.github/workflows/release.yml
vendored
78
.github/workflows/release.yml
vendored
@@ -65,7 +65,6 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
type=raw,value=latest
|
|
||||||
type=sha
|
type=sha
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
@@ -125,13 +124,12 @@ jobs:
|
|||||||
${{ secrets.DOCKERHUB_USERNAME }}/headscale
|
${{ secrets.DOCKERHUB_USERNAME }}/headscale
|
||||||
ghcr.io/${{ github.repository_owner }}/headscale
|
ghcr.io/${{ github.repository_owner }}/headscale
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
suffix=-debug,onlatest=true
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}-debug
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}-debug
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}-debug
|
type=semver,pattern={{major}}
|
||||||
type=raw,value=latest-debug
|
type=sha
|
||||||
type=sha,suffix=-debug
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
@@ -161,69 +159,3 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
rm -rf /tmp/.buildx-cache-debug
|
rm -rf /tmp/.buildx-cache-debug
|
||||||
mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug
|
mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug
|
||||||
|
|
||||||
docker-alpine-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Set up QEMU for multiple platforms
|
|
||||||
uses: docker/setup-qemu-action@master
|
|
||||||
with:
|
|
||||||
platforms: arm64,amd64
|
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache-alpine
|
|
||||||
key: ${{ runner.os }}-buildx-alpine-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-alpine-
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta-alpine
|
|
||||||
uses: docker/metadata-action@v3
|
|
||||||
with:
|
|
||||||
# list of Docker images to use as base name for tags
|
|
||||||
images: |
|
|
||||||
${{ secrets.DOCKERHUB_USERNAME }}/headscale
|
|
||||||
ghcr.io/${{ github.repository_owner }}/headscale
|
|
||||||
flavor: |
|
|
||||||
latest=false
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}-alpine
|
|
||||||
type=semver,pattern={{major}}.{{minor}}-alpine
|
|
||||||
type=semver,pattern={{major}}-alpine
|
|
||||||
type=raw,value=latest-alpine
|
|
||||||
type=sha,suffix=-alpine
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
context: .
|
|
||||||
file: Dockerfile.alpine
|
|
||||||
tags: ${{ steps.meta-alpine.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta-alpine.outputs.labels }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache-alpine
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-alpine-new
|
|
||||||
build-args: |
|
|
||||||
VERSION=${{ steps.meta-alpine.outputs.version }}
|
|
||||||
- name: Prepare cache for next build
|
|
||||||
run: |
|
|
||||||
rm -rf /tmp/.buildx-cache-alpine
|
|
||||||
mv /tmp/.buildx-cache-alpine-new /tmp/.buildx-cache-alpine
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
### BREAKING
|
### BREAKING
|
||||||
|
|
||||||
- Log level option `log_level` was moved to a distinct `log` config section and renamed to `level` [#768](https://github.com/juanfont/headscale/pull/768)
|
- Log level option `log_level` was moved to a distinct `log` config section and renamed to `level` [#768](https://github.com/juanfont/headscale/pull/768)
|
||||||
|
- Removed Alpine Linux container image [#962](https://github.com/juanfont/headscale/pull/962)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
- Add `dns_config.override_local_dns` option [#905](https://github.com/juanfont/headscale/pull/905)
|
- Add `dns_config.override_local_dns` option [#905](https://github.com/juanfont/headscale/pull/905)
|
||||||
- Fix some DNS config issues [#660](https://github.com/juanfont/headscale/issues/660)
|
- Fix some DNS config issues [#660](https://github.com/juanfont/headscale/issues/660)
|
||||||
- Make it possible to disable TS2019 with build flag [#928](https://github.com/juanfont/headscale/pull/928)
|
- Make it possible to disable TS2019 with build flag [#928](https://github.com/juanfont/headscale/pull/928)
|
||||||
|
- Fix OIDC registration issues [#960](https://github.com/juanfont/headscale/pull/960) and [#971](https://github.com/juanfont/headscale/pull/971)
|
||||||
|
|
||||||
## 0.16.4 (2022-08-21)
|
## 0.16.4 (2022-08-21)
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# Builder image
|
|
||||||
FROM docker.io/golang:1.19.0-alpine AS build
|
|
||||||
ARG VERSION=dev
|
|
||||||
ENV GOPATH /go
|
|
||||||
WORKDIR /go/src/headscale
|
|
||||||
|
|
||||||
COPY go.mod go.sum /go/src/headscale/
|
|
||||||
RUN apk add gcc musl-dev
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
|
|
||||||
RUN strip /go/bin/headscale
|
|
||||||
RUN test -e /go/bin/headscale
|
|
||||||
|
|
||||||
# Production image
|
|
||||||
FROM docker.io/alpine:latest
|
|
||||||
|
|
||||||
COPY --from=build /go/bin/headscale /bin/headscale
|
|
||||||
ENV TZ UTC
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
|
||||||
CMD ["headscale"]
|
|
||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errParseAuthPage = errors.New("failed to parse auth page")
|
var errParseAuthPage = errors.New("failed to parse auth page")
|
||||||
@@ -35,7 +37,7 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
|
|||||||
"namespace2": len(TailscaleVersions),
|
"namespace2": len(TailscaleVersions),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("webauthping"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
t.Errorf("failed to create headscale environment: %s", err)
|
||||||
}
|
}
|
||||||
@@ -76,13 +78,16 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthWebFlowScenario) CreateHeadscaleEnv(namespaces map[string]int) error {
|
func (s *AuthWebFlowScenario) CreateHeadscaleEnv(
|
||||||
err := s.StartHeadscale()
|
namespaces map[string]int,
|
||||||
|
opts ...hsic.Option,
|
||||||
|
) error {
|
||||||
|
headscale, err := s.Headscale(opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.Headscale().WaitForReady()
|
err = headscale.WaitForReady()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -99,7 +104,7 @@ func (s *AuthWebFlowScenario) CreateHeadscaleEnv(namespaces map[string]int) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.runTailscaleUp(namespaceName, s.Headscale().GetEndpoint())
|
err = s.runTailscaleUp(namespaceName, headscale.GetEndpoint())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -145,8 +150,13 @@ func (s *AuthWebFlowScenario) runTailscaleUp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthWebFlowScenario) runHeadscaleRegister(namespaceStr string, loginURL *url.URL) error {
|
func (s *AuthWebFlowScenario) runHeadscaleRegister(namespaceStr string, loginURL *url.URL) error {
|
||||||
|
headscale, err := s.Headscale()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("loginURL: %s", loginURL)
|
log.Printf("loginURL: %s", loginURL)
|
||||||
loginURL.Host = fmt.Sprintf("%s:8080", s.Headscale().GetIP())
|
loginURL.Host = fmt.Sprintf("%s:8080", headscale.GetIP())
|
||||||
loginURL.Scheme = "http"
|
loginURL.Scheme = "http"
|
||||||
|
|
||||||
httpClient := &http.Client{}
|
httpClient := &http.Client{}
|
||||||
@@ -177,8 +187,10 @@ func (s *AuthWebFlowScenario) runHeadscaleRegister(namespaceStr string, loginURL
|
|||||||
key := keySep[1]
|
key := keySep[1]
|
||||||
log.Printf("registering node %s", key)
|
log.Printf("registering node %s", key)
|
||||||
|
|
||||||
if headscale, ok := s.controlServers["headscale"]; ok {
|
if headscale, err := s.Headscale(); err == nil {
|
||||||
_, err = headscale.Execute([]string{"headscale", "-n", namespaceStr, "nodes", "register", "--key", key})
|
_, err = headscale.Execute(
|
||||||
|
[]string{"headscale", "-n", namespaceStr, "nodes", "register", "--key", key},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to register node: %s", err)
|
log.Printf("failed to register node: %s", err)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,11 +37,14 @@ func TestNamespaceCommand(t *testing.T) {
|
|||||||
"namespace2": 0,
|
"namespace2": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("clins"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
headscale, err := scenario.Headscale()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var listNamespaces []v1.Namespace
|
var listNamespaces []v1.Namespace
|
||||||
err = executeAndUnmarshal(scenario.Headscale(),
|
err = executeAndUnmarshal(headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"namespaces",
|
"namespaces",
|
||||||
@@ -61,7 +65,7 @@ func TestNamespaceCommand(t *testing.T) {
|
|||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err = scenario.Headscale().Execute(
|
_, err = headscale.Execute(
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"namespaces",
|
"namespaces",
|
||||||
@@ -75,7 +79,7 @@ func TestNamespaceCommand(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var listAfterRenameNamespaces []v1.Namespace
|
var listAfterRenameNamespaces []v1.Namespace
|
||||||
err = executeAndUnmarshal(scenario.Headscale(),
|
err = executeAndUnmarshal(headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"namespaces",
|
"namespaces",
|
||||||
@@ -114,7 +118,10 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
|||||||
namespace: 0,
|
namespace: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("clipak"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
headscale, err := scenario.Headscale()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
keys := make([]*v1.PreAuthKey, count)
|
keys := make([]*v1.PreAuthKey, count)
|
||||||
@@ -123,7 +130,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
|||||||
for index := 0; index < count; index++ {
|
for index := 0; index < count; index++ {
|
||||||
var preAuthKey v1.PreAuthKey
|
var preAuthKey v1.PreAuthKey
|
||||||
err := executeAndUnmarshal(
|
err := executeAndUnmarshal(
|
||||||
scenario.Headscale(),
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
@@ -149,7 +156,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
|||||||
|
|
||||||
var listedPreAuthKeys []v1.PreAuthKey
|
var listedPreAuthKeys []v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
scenario.Headscale(),
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
@@ -202,7 +209,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test key expiry
|
// Test key expiry
|
||||||
_, err = scenario.Headscale().Execute(
|
_, err = headscale.Execute(
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
@@ -216,7 +223,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
|||||||
|
|
||||||
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
|
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
scenario.Headscale(),
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
@@ -251,12 +258,15 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
|||||||
namespace: 0,
|
namespace: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("clipaknaexp"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
headscale, err := scenario.Headscale()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var preAuthKey v1.PreAuthKey
|
var preAuthKey v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
scenario.Headscale(),
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
@@ -273,7 +283,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
|||||||
|
|
||||||
var listedPreAuthKeys []v1.PreAuthKey
|
var listedPreAuthKeys []v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
scenario.Headscale(),
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
@@ -313,12 +323,15 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
|||||||
namespace: 0,
|
namespace: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("clipakresueeph"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
headscale, err := scenario.Headscale()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var preAuthReusableKey v1.PreAuthKey
|
var preAuthReusableKey v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
scenario.Headscale(),
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
@@ -335,7 +348,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
|||||||
|
|
||||||
var preAuthEphemeralKey v1.PreAuthKey
|
var preAuthEphemeralKey v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
scenario.Headscale(),
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
@@ -355,7 +368,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
|||||||
|
|
||||||
var listedPreAuthKeys []v1.PreAuthKey
|
var listedPreAuthKeys []v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
scenario.Headscale(),
|
headscale,
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
"headscale",
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
|
|||||||
@@ -13,4 +13,7 @@ type ControlServer interface {
|
|||||||
CreateNamespace(namespace string) error
|
CreateNamespace(namespace string) error
|
||||||
CreateAuthKey(namespace string) (*v1.PreAuthKey, error)
|
CreateAuthKey(namespace string) (*v1.PreAuthKey, error)
|
||||||
ListMachinesInNamespace(namespace string) ([]*v1.Machine, error)
|
ListMachinesInNamespace(namespace string) ([]*v1.Machine, error)
|
||||||
|
GetCert() []byte
|
||||||
|
GetHostname() string
|
||||||
|
GetIP() string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ func TestPingAllByIP(t *testing.T) {
|
|||||||
"namespace2": len(TailscaleVersions),
|
"namespace2": len(TailscaleVersions),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("pingallbyip"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
t.Errorf("failed to create headscale environment: %s", err)
|
||||||
}
|
}
|
||||||
@@ -77,7 +78,7 @@ func TestPingAllByHostname(t *testing.T) {
|
|||||||
"namespace4": len(TailscaleVersions) - 1,
|
"namespace4": len(TailscaleVersions) - 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("pingallbyname"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
t.Errorf("failed to create headscale environment: %s", err)
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,7 @@ func TestTaildrop(t *testing.T) {
|
|||||||
"taildrop": len(TailscaleVersions) - 1,
|
"taildrop": len(TailscaleVersions) - 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("taildrop"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
t.Errorf("failed to create headscale environment: %s", err)
|
||||||
}
|
}
|
||||||
@@ -271,7 +272,7 @@ func TestResolveMagicDNS(t *testing.T) {
|
|||||||
"magicdns2": len(TailscaleVersions) - 1,
|
"magicdns2": len(TailscaleVersions) - 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec)
|
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("magicdns"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
t.Errorf("failed to create headscale environment: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,83 @@
|
|||||||
package hsic
|
package hsic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"time"
|
||||||
|
|
||||||
"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/juanfont/headscale/integration/integrationutil"
|
||||||
"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"
|
aclPolicyPath = "/etc/headscale/acl.hujson"
|
||||||
|
tlsCertPath = "/etc/headscale/tls.cert"
|
||||||
|
tlsKeyPath = "/etc/headscale/tls.key"
|
||||||
|
headscaleDefaultPort = 8080
|
||||||
)
|
)
|
||||||
|
|
||||||
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
||||||
|
|
||||||
type HeadscaleInContainer struct {
|
type HeadscaleInContainer struct {
|
||||||
hostname string
|
hostname string
|
||||||
port int
|
|
||||||
|
|
||||||
pool *dockertest.Pool
|
pool *dockertest.Pool
|
||||||
container *dockertest.Resource
|
container *dockertest.Resource
|
||||||
network *dockertest.Network
|
network *dockertest.Network
|
||||||
|
|
||||||
// optional config
|
// optional config
|
||||||
|
port int
|
||||||
aclPolicy *headscale.ACLPolicy
|
aclPolicy *headscale.ACLPolicy
|
||||||
env []string
|
env []string
|
||||||
|
tlsCert []byte
|
||||||
|
tlsKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option = func(c *HeadscaleInContainer)
|
type Option = func(c *HeadscaleInContainer)
|
||||||
|
|
||||||
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
// TODO(kradalby): Move somewhere appropriate
|
||||||
|
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_ACL_POLICY_PATH=%s", aclPolicyPath))
|
||||||
|
|
||||||
hsic.aclPolicy = acl
|
hsic.aclPolicy = acl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithTLS() Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
cert, key, err := createCertificate()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create certificates for headscale test: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(kradalby): Move somewhere appropriate
|
||||||
|
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_TLS_CERT_PATH=%s", tlsCertPath))
|
||||||
|
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_TLS_KEY_PATH=%s", tlsKeyPath))
|
||||||
|
hsic.env = append(hsic.env, "HEADSCALE_TLS_CLIENT_AUTH_MODE=disabled")
|
||||||
|
|
||||||
|
hsic.tlsCert = cert
|
||||||
|
hsic.tlsKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithConfigEnv(configEnv map[string]string) Option {
|
func WithConfigEnv(configEnv map[string]string) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
env := []string{}
|
env := []string{}
|
||||||
@@ -59,9 +90,23 @@ func WithConfigEnv(configEnv map[string]string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithPort(port int) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.port = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTestName(testName string) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hash, _ := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
|
||||||
|
|
||||||
|
hostname := fmt.Sprintf("hs-%s-%s", testName, hash)
|
||||||
|
hsic.hostname = hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
pool *dockertest.Pool,
|
pool *dockertest.Pool,
|
||||||
port int,
|
|
||||||
network *dockertest.Network,
|
network *dockertest.Network,
|
||||||
opts ...Option,
|
opts ...Option,
|
||||||
) (*HeadscaleInContainer, error) {
|
) (*HeadscaleInContainer, error) {
|
||||||
@@ -71,11 +116,10 @@ func New(
|
|||||||
}
|
}
|
||||||
|
|
||||||
hostname := fmt.Sprintf("hs-%s", hash)
|
hostname := fmt.Sprintf("hs-%s", hash)
|
||||||
portProto := fmt.Sprintf("%d/tcp", port)
|
|
||||||
|
|
||||||
hsic := &HeadscaleInContainer{
|
hsic := &HeadscaleInContainer{
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
port: port,
|
port: headscaleDefaultPort,
|
||||||
|
|
||||||
pool: pool,
|
pool: pool,
|
||||||
network: network,
|
network: network,
|
||||||
@@ -85,9 +129,9 @@ func New(
|
|||||||
opt(hsic)
|
opt(hsic)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hsic.aclPolicy != nil {
|
log.Println("NAME: ", hsic.hostname)
|
||||||
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_ACL_POLICY_PATH=%s", aclPolicyPath))
|
|
||||||
}
|
portProto := fmt.Sprintf("%d/tcp", hsic.port)
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
Dockerfile: "Dockerfile.debug",
|
Dockerfile: "Dockerfile.debug",
|
||||||
@@ -95,7 +139,7 @@ func New(
|
|||||||
}
|
}
|
||||||
|
|
||||||
runOptions := &dockertest.RunOptions{
|
runOptions := &dockertest.RunOptions{
|
||||||
Name: hostname,
|
Name: hsic.hostname,
|
||||||
ExposedPorts: []string{portProto},
|
ExposedPorts: []string{portProto},
|
||||||
Networks: []*dockertest.Network{network},
|
Networks: []*dockertest.Network{network},
|
||||||
// Cmd: []string{"headscale", "serve"},
|
// Cmd: []string{"headscale", "serve"},
|
||||||
@@ -108,7 +152,7 @@ func New(
|
|||||||
// dockertest isnt very good at handling containers that has already
|
// dockertest isnt very good at handling containers that has already
|
||||||
// been created, this is an attempt to make sure this container isnt
|
// been created, this is an attempt to make sure this container isnt
|
||||||
// present.
|
// present.
|
||||||
err = pool.RemoveContainerByName(hostname)
|
err = pool.RemoveContainerByName(hsic.hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -123,7 +167,7 @@ func New(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not start headscale container: %w", err)
|
return nil, fmt.Errorf("could not start headscale container: %w", err)
|
||||||
}
|
}
|
||||||
log.Printf("Created %s container\n", hostname)
|
log.Printf("Created %s container\n", hsic.hostname)
|
||||||
|
|
||||||
hsic.container = container
|
hsic.container = container
|
||||||
|
|
||||||
@@ -144,9 +188,25 @@ func New(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hsic.hasTLS() {
|
||||||
|
err = hsic.WriteFile(tlsCertPath, hsic.tlsCert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = hsic.WriteFile(tlsKeyPath, hsic.tlsKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS key to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return hsic, nil
|
return hsic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *HeadscaleInContainer) hasTLS() bool {
|
||||||
|
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
|
||||||
|
}
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) Shutdown() error {
|
func (t *HeadscaleInContainer) Shutdown() error {
|
||||||
return t.pool.Purge(t.container)
|
return t.pool.Purge(t.container)
|
||||||
}
|
}
|
||||||
@@ -154,8 +214,6 @@ func (t *HeadscaleInContainer) Shutdown() error {
|
|||||||
func (t *HeadscaleInContainer) Execute(
|
func (t *HeadscaleInContainer) Execute(
|
||||||
command []string,
|
command []string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
log.Println("command", command)
|
|
||||||
log.Printf("running command for %s\n", t.hostname)
|
|
||||||
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
||||||
t.container,
|
t.container,
|
||||||
command,
|
command,
|
||||||
@@ -164,13 +222,13 @@ func (t *HeadscaleInContainer) Execute(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("command stderr: %s\n", stderr)
|
log.Printf("command stderr: %s\n", stderr)
|
||||||
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if stdout != "" {
|
if stdout != "" {
|
||||||
log.Printf("command stdout: %s\n", stdout)
|
log.Printf("command stdout: %s\n", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
return stdout, nil
|
return stdout, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,11 +241,7 @@ func (t *HeadscaleInContainer) GetPort() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
||||||
hostEndpoint := fmt.Sprintf("%s:%d",
|
return fmt.Sprintf("%s/health", t.GetEndpoint())
|
||||||
t.GetIP(),
|
|
||||||
t.port)
|
|
||||||
|
|
||||||
return fmt.Sprintf("http://%s/health", hostEndpoint)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) GetEndpoint() string {
|
func (t *HeadscaleInContainer) GetEndpoint() string {
|
||||||
@@ -195,17 +249,39 @@ func (t *HeadscaleInContainer) GetEndpoint() string {
|
|||||||
t.GetIP(),
|
t.GetIP(),
|
||||||
t.port)
|
t.port)
|
||||||
|
|
||||||
|
if t.hasTLS() {
|
||||||
|
return fmt.Sprintf("https://%s", hostEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("http://%s", hostEndpoint)
|
return fmt.Sprintf("http://%s", hostEndpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *HeadscaleInContainer) GetCert() []byte {
|
||||||
|
return t.tlsCert
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HeadscaleInContainer) GetHostname() string {
|
||||||
|
return t.hostname
|
||||||
|
}
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) WaitForReady() error {
|
func (t *HeadscaleInContainer) WaitForReady() error {
|
||||||
url := t.GetHealthEndpoint()
|
url := t.GetHealthEndpoint()
|
||||||
|
|
||||||
log.Printf("waiting for headscale to be ready at %s", url)
|
log.Printf("waiting for headscale to be ready at %s", url)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
if t.hasTLS() {
|
||||||
|
insecureTransport := http.DefaultTransport.(*http.Transport).Clone() //nolint
|
||||||
|
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint
|
||||||
|
client = &http.Client{Transport: insecureTransport}
|
||||||
|
}
|
||||||
|
|
||||||
return t.pool.Retry(func() error {
|
return t.pool.Retry(func() error {
|
||||||
resp, err := http.Get(url) //nolint
|
resp, err := client.Get(url) //nolint
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("ready err: %s", err)
|
||||||
|
|
||||||
return fmt.Errorf("headscale is not ready: %w", err)
|
return fmt.Errorf("headscale is not ready: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,55 +368,87 @@ func (t *HeadscaleInContainer) ListMachinesInNamespace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
||||||
dirPath, fileName := filepath.Split(path)
|
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||||
|
|
||||||
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)
|
//nolint
|
||||||
if err != nil {
|
func createCertificate() ([]byte, []byte, error) {
|
||||||
return fmt.Errorf("failed write file header to tar: %w", err)
|
// From:
|
||||||
}
|
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
||||||
|
|
||||||
_, err = io.Copy(tarWriter, file)
|
ca := &x509.Certificate{
|
||||||
if err != nil {
|
SerialNumber: big.NewInt(2019),
|
||||||
return fmt.Errorf("failed to copy file to tar: %w", err)
|
Subject: pkix.Name{
|
||||||
}
|
Organization: []string{"Headscale testing INC"},
|
||||||
|
Country: []string{"NL"},
|
||||||
err = tarWriter.Close()
|
Locality: []string{"Leiden"},
|
||||||
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()),
|
|
||||||
},
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(30 * time.Minute),
|
||||||
|
IsCA: true,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||||
|
x509.ExtKeyUsageClientAuth,
|
||||||
|
x509.ExtKeyUsageServerAuth,
|
||||||
|
},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1658),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{"Headscale testing INC"},
|
||||||
|
Country: []string{"NL"},
|
||||||
|
Locality: []string{"Leiden"},
|
||||||
|
},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(30 * time.Minute),
|
||||||
|
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
}
|
||||||
|
|
||||||
|
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes, err := x509.CreateCertificate(
|
||||||
|
rand.Reader,
|
||||||
|
cert,
|
||||||
|
ca,
|
||||||
|
&certPrivKey.PublicKey,
|
||||||
|
caPrivKey,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
certPEM := new(bytes.Buffer)
|
||||||
|
|
||||||
|
err = pem.Encode(certPEM, &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: certBytes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certPrivKeyPEM := new(bytes.Buffer)
|
||||||
|
|
||||||
|
err = pem.Encode(certPrivKeyPEM, &pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return certPEM.Bytes(), certPrivKeyPEM.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
77
integration/integrationutil/util.go
Normal file
77
integration/integrationutil/util.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package integrationutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WriteFileToContainer(
|
||||||
|
pool *dockertest.Pool,
|
||||||
|
container *dockertest.Resource,
|
||||||
|
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 = dockertestutil.ExecuteCommand(
|
||||||
|
container,
|
||||||
|
[]string{"mkdir", "-p", dirPath},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pool.Client.UploadToContainer(
|
||||||
|
container.Container.ID,
|
||||||
|
docker.UploadToContainerOptions{
|
||||||
|
NoOverwriteDirNonDir: false,
|
||||||
|
Path: dirPath,
|
||||||
|
InputStream: bytes.NewReader(buf.Bytes()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -15,22 +15,28 @@ 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/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/puzpuzpuz/xsync/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
scenarioHashLength = 6
|
scenarioHashLength = 6
|
||||||
maxWait = 60 * time.Second
|
maxWait = 60 * time.Second
|
||||||
headscalePort = 8080
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errNoHeadscaleAvailable = errors.New("no headscale available")
|
errNoHeadscaleAvailable = errors.New("no headscale available")
|
||||||
errNoNamespaceAvailable = errors.New("no namespace available")
|
errNoNamespaceAvailable = errors.New("no namespace available")
|
||||||
TailscaleVersions = []string{
|
|
||||||
|
// 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).
|
||||||
|
tailscaleVersions2021 = []string{
|
||||||
"head",
|
"head",
|
||||||
"unstable",
|
"unstable",
|
||||||
"1.32.1",
|
"1.32.1",
|
||||||
"1.30.2",
|
"1.30.2",
|
||||||
|
}
|
||||||
|
|
||||||
|
tailscaleVersions2019 = []string{
|
||||||
"1.28.0",
|
"1.28.0",
|
||||||
"1.26.2",
|
"1.26.2",
|
||||||
"1.24.2",
|
"1.24.2",
|
||||||
@@ -38,13 +44,20 @@ var (
|
|||||||
"1.20.4",
|
"1.20.4",
|
||||||
"1.18.2",
|
"1.18.2",
|
||||||
"1.16.2",
|
"1.16.2",
|
||||||
|
}
|
||||||
|
|
||||||
// These versions seem to fail when fetching from apt.
|
// tailscaleVersionsUnavailable = []string{
|
||||||
|
// // These versions seem to fail when fetching from apt.
|
||||||
// "1.14.6",
|
// "1.14.6",
|
||||||
// "1.12.4",
|
// "1.12.4",
|
||||||
// "1.10.2",
|
// "1.10.2",
|
||||||
// "1.8.7",
|
// "1.8.7",
|
||||||
}
|
// }.
|
||||||
|
|
||||||
|
TailscaleVersions = append(
|
||||||
|
tailscaleVersions2021,
|
||||||
|
tailscaleVersions2019...,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
type Namespace struct {
|
type Namespace struct {
|
||||||
@@ -59,12 +72,14 @@ type Namespace struct {
|
|||||||
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.
|
||||||
controlServers map[string]ControlServer
|
controlServers *xsync.MapOf[string, ControlServer]
|
||||||
|
|
||||||
namespaces map[string]*Namespace
|
namespaces map[string]*Namespace
|
||||||
|
|
||||||
pool *dockertest.Pool
|
pool *dockertest.Pool
|
||||||
network *dockertest.Network
|
network *dockertest.Network
|
||||||
|
|
||||||
|
headscaleLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScenario() (*Scenario, error) {
|
func NewScenario() (*Scenario, error) {
|
||||||
@@ -99,7 +114,7 @@ func NewScenario() (*Scenario, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Scenario{
|
return &Scenario{
|
||||||
controlServers: make(map[string]ControlServer),
|
controlServers: xsync.NewMapOf[ControlServer](),
|
||||||
namespaces: make(map[string]*Namespace),
|
namespaces: make(map[string]*Namespace),
|
||||||
|
|
||||||
pool: pool,
|
pool: pool,
|
||||||
@@ -108,13 +123,18 @@ func NewScenario() (*Scenario, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scenario) Shutdown() error {
|
func (s *Scenario) Shutdown() error {
|
||||||
for _, control := range s.controlServers {
|
s.controlServers.Range(func(_ string, control ControlServer) bool {
|
||||||
err := control.Shutdown()
|
err := control.Shutdown()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to tear down control: %w", err)
|
log.Printf(
|
||||||
}
|
"Failed to shut down control: %s",
|
||||||
|
fmt.Errorf("failed to tear down control: %w", err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
for namespaceName, namespace := range s.namespaces {
|
for namespaceName, namespace := range s.namespaces {
|
||||||
for _, client := range namespace.Clients {
|
for _, client := range namespace.Clients {
|
||||||
log.Printf("removing client %s in namespace %s", client.Hostname(), namespaceName)
|
log.Printf("removing client %s in namespace %s", client.Hostname(), namespaceName)
|
||||||
@@ -150,36 +170,31 @@ func (s *Scenario) Namespaces() []string {
|
|||||||
// Note: These functions assume that there is a _single_ headscale instance for now
|
// Note: These functions assume that there is a _single_ headscale instance for now
|
||||||
|
|
||||||
// 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) Headscale(opts ...hsic.Option) (ControlServer, error) {
|
||||||
headscale, err := hsic.New(s.pool, headscalePort, s.network,
|
s.headscaleLock.Lock()
|
||||||
hsic.WithACLPolicy(
|
defer s.headscaleLock.Unlock()
|
||||||
&headscale.ACLPolicy{
|
|
||||||
ACLs: []headscale.ACL{
|
if headscale, ok := s.controlServers.Load("headscale"); ok {
|
||||||
{
|
return headscale, nil
|
||||||
Action: "accept",
|
}
|
||||||
Sources: []string{"*"},
|
|
||||||
Destinations: []string{"*:*"},
|
headscale, err := hsic.New(s.pool, s.network, opts...)
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create headscale container: %w", err)
|
return nil, fmt.Errorf("failed to create headscale container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.controlServers["headscale"] = headscale
|
err = headscale.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
return nil
|
return nil, fmt.Errorf("failed reach headscale container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scenario) Headscale() *hsic.HeadscaleInContainer {
|
s.controlServers.Store("headscale", headscale)
|
||||||
//nolint
|
|
||||||
return s.controlServers["headscale"].(*hsic.HeadscaleInContainer)
|
return headscale, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
|
func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
|
||||||
if headscale, ok := s.controlServers["headscale"]; ok {
|
if headscale, err := s.Headscale(); err == nil {
|
||||||
key, err := headscale.CreateAuthKey(namespace)
|
key, err := headscale.CreateAuthKey(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create namespace: %w", err)
|
return nil, fmt.Errorf("failed to create namespace: %w", err)
|
||||||
@@ -192,7 +207,7 @@ func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scenario) CreateNamespace(namespace string) error {
|
func (s *Scenario) CreateNamespace(namespace string) error {
|
||||||
if headscale, ok := s.controlServers["headscale"]; ok {
|
if headscale, err := s.Headscale(); err == nil {
|
||||||
err := headscale.CreateNamespace(namespace)
|
err := headscale.CreateNamespace(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create namespace: %w", err)
|
return fmt.Errorf("failed to create namespace: %w", err)
|
||||||
@@ -222,16 +237,36 @@ func (s *Scenario) CreateTailscaleNodesInNamespace(
|
|||||||
version = TailscaleVersions[i%len(TailscaleVersions)]
|
version = TailscaleVersions[i%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()
|
||||||
|
|
||||||
namespace.createWaitGroup.Add(1)
|
namespace.createWaitGroup.Add(1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer namespace.createWaitGroup.Done()
|
defer namespace.createWaitGroup.Done()
|
||||||
|
|
||||||
// TODO(kradalby): error handle this
|
// TODO(kradalby): error handle this
|
||||||
tsClient, err := tsic.New(s.pool, version, s.network)
|
tsClient, err := tsic.New(
|
||||||
|
s.pool,
|
||||||
|
version,
|
||||||
|
s.network,
|
||||||
|
tsic.WithHeadscaleTLS(cert),
|
||||||
|
tsic.WithHeadscaleName(hostname),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// return fmt.Errorf("failed to add tailscale node: %w", err)
|
// return fmt.Errorf("failed to add tailscale node: %w", err)
|
||||||
log.Printf("failed to add tailscale node: %s", 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace.Clients[tsClient.Hostname()] = tsClient
|
namespace.Clients[tsClient.Hostname()] = tsClient
|
||||||
@@ -306,13 +341,8 @@ func (s *Scenario) WaitForTailscaleSync() error {
|
|||||||
// CreateHeadscaleEnv is a conventient method returning a set up Headcale
|
// CreateHeadscaleEnv is a conventient method returning a set up Headcale
|
||||||
// test environment with nodes of all versions, joined to the server with X
|
// test environment with nodes of all versions, joined to the server with X
|
||||||
// namespaces.
|
// namespaces.
|
||||||
func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int) error {
|
func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int, opts ...hsic.Option) error {
|
||||||
err := s.StartHeadscale()
|
headscale, err := s.Headscale(opts...)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Headscale().WaitForReady()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -333,7 +363,7 @@ func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.RunTailscaleUp(namespaceName, s.Headscale().GetEndpoint(), key.GetKey())
|
err = s.RunTailscaleUp(namespaceName, headscale.GetEndpoint(), key.GetKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ func TestHeadscale(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("start-headscale", func(t *testing.T) {
|
t.Run("start-headscale", func(t *testing.T) {
|
||||||
err = scenario.StartHeadscale()
|
headscale, err := scenario.Headscale()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create start headcale: %s", err)
|
t.Errorf("failed to create start headcale: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.Headscale().WaitForReady()
|
err = headscale.WaitForReady()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("headscale failed to become ready: %s", err)
|
t.Errorf("headscale failed to become ready: %s", err)
|
||||||
}
|
}
|
||||||
@@ -117,12 +117,11 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("start-headscale", func(t *testing.T) {
|
t.Run("start-headscale", func(t *testing.T) {
|
||||||
err = scenario.StartHeadscale()
|
headscale, err := scenario.Headscale()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create start headcale: %s", err)
|
t.Errorf("failed to create start headcale: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
headscale := scenario.Headscale()
|
|
||||||
err = headscale.WaitForReady()
|
err = headscale.WaitForReady()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("headscale failed to become ready: %s", err)
|
t.Errorf("headscale failed to become ready: %s", err)
|
||||||
@@ -157,7 +156,16 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
|
|||||||
t.Errorf("failed to create preauthkey: %s", err)
|
t.Errorf("failed to create preauthkey: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.RunTailscaleUp(namespace, scenario.Headscale().GetEndpoint(), key.GetKey())
|
headscale, err := scenario.Headscale()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create start headcale: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scenario.RunTailscaleUp(
|
||||||
|
namespace,
|
||||||
|
headscale.GetEndpoint(),
|
||||||
|
key.GetKey(),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to login: %s", err)
|
t.Errorf("failed to login: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"github.com/juanfont/headscale"
|
"github.com/juanfont/headscale"
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
|
"github.com/juanfont/headscale/integration/integrationutil"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/ory/dockertest/v3/docker"
|
"github.com/ory/dockertest/v3/docker"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
tsicHashLength = 6
|
tsicHashLength = 6
|
||||||
dockerContextPath = "../."
|
dockerContextPath = "../."
|
||||||
|
headscaleCertPath = "/usr/local/share/ca-certificates/headscale.crt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -41,12 +43,51 @@ type TailscaleInContainer struct {
|
|||||||
// "cache"
|
// "cache"
|
||||||
ips []netip.Addr
|
ips []netip.Addr
|
||||||
fqdn string
|
fqdn string
|
||||||
|
|
||||||
|
// optional config
|
||||||
|
headscaleCert []byte
|
||||||
|
headscaleHostname string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option = func(c *TailscaleInContainer)
|
||||||
|
|
||||||
|
func WithHeadscaleTLS(cert []byte) Option {
|
||||||
|
return func(tsic *TailscaleInContainer) {
|
||||||
|
tsic.headscaleCert = cert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithOrCreateNetwork(network *dockertest.Network) Option {
|
||||||
|
return func(tsic *TailscaleInContainer) {
|
||||||
|
if network != nil {
|
||||||
|
tsic.network = network
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
network, err := dockertestutil.GetFirstOrCreateNetwork(
|
||||||
|
tsic.pool,
|
||||||
|
fmt.Sprintf("%s-network", tsic.hostname),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create network: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tsic.network = network
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHeadscaleName(hsName string) Option {
|
||||||
|
return func(tsic *TailscaleInContainer) {
|
||||||
|
tsic.headscaleHostname = hsName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
pool *dockertest.Pool,
|
pool *dockertest.Pool,
|
||||||
version string,
|
version string,
|
||||||
network *dockertest.Network,
|
network *dockertest.Network,
|
||||||
|
opts ...Option,
|
||||||
) (*TailscaleInContainer, error) {
|
) (*TailscaleInContainer, error) {
|
||||||
hash, err := headscale.GenerateRandomStringDNSSafe(tsicHashLength)
|
hash, err := headscale.GenerateRandomStringDNSSafe(tsicHashLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,20 +96,38 @@ func New(
|
|||||||
|
|
||||||
hostname := fmt.Sprintf("ts-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
|
hostname := fmt.Sprintf("ts-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
|
||||||
|
|
||||||
// TODO(kradalby): figure out why we need to "refresh" the network here.
|
tsic := &TailscaleInContainer{
|
||||||
// network, err = dockertestutil.GetFirstOrCreateNetwork(pool, network.Network.Name)
|
version: version,
|
||||||
// if err != nil {
|
hostname: hostname,
|
||||||
// return nil, err
|
|
||||||
// }
|
pool: pool,
|
||||||
|
network: network,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(tsic)
|
||||||
|
}
|
||||||
|
|
||||||
tailscaleOptions := &dockertest.RunOptions{
|
tailscaleOptions := &dockertest.RunOptions{
|
||||||
Name: hostname,
|
Name: hostname,
|
||||||
Networks: []*dockertest.Network{network},
|
Networks: []*dockertest.Network{network},
|
||||||
Cmd: []string{
|
// Cmd: []string{
|
||||||
"tailscaled", "--tun=tsdev",
|
// "tailscaled", "--tun=tsdev",
|
||||||
|
// },
|
||||||
|
Entrypoint: []string{
|
||||||
|
"/bin/bash",
|
||||||
|
"-c",
|
||||||
|
"/bin/sleep 3 ; update-ca-certificates ; tailscaled --tun=tsdev",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tsic.headscaleHostname != "" {
|
||||||
|
tailscaleOptions.ExtraHosts = []string{
|
||||||
|
"host.docker.internal:host-gateway",
|
||||||
|
fmt.Sprintf("%s:host-gateway", tsic.headscaleHostname),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dockertest isnt very good at handling containers that has already
|
// dockertest isnt very good at handling containers that has already
|
||||||
// been created, this is an attempt to make sure this container isnt
|
// been created, this is an attempt to make sure this container isnt
|
||||||
// present.
|
// present.
|
||||||
@@ -89,14 +148,20 @@ func New(
|
|||||||
}
|
}
|
||||||
log.Printf("Created %s container\n", hostname)
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
return &TailscaleInContainer{
|
tsic.container = container
|
||||||
version: version,
|
|
||||||
hostname: hostname,
|
|
||||||
|
|
||||||
pool: pool,
|
if tsic.hasTLS() {
|
||||||
container: container,
|
err = tsic.WriteFile(headscaleCertPath, tsic.headscaleCert)
|
||||||
network: network,
|
if err != nil {
|
||||||
}, nil
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tsic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TailscaleInContainer) hasTLS() bool {
|
||||||
|
return len(t.headscaleCert) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TailscaleInContainer) Shutdown() error {
|
func (t *TailscaleInContainer) Shutdown() error {
|
||||||
@@ -114,8 +179,6 @@ func (t *TailscaleInContainer) Version() string {
|
|||||||
func (t *TailscaleInContainer) Execute(
|
func (t *TailscaleInContainer) Execute(
|
||||||
command []string,
|
command []string,
|
||||||
) (string, string, error) {
|
) (string, string, error) {
|
||||||
log.Println("command", command)
|
|
||||||
log.Printf("running command for %s\n", t.hostname)
|
|
||||||
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
||||||
t.container,
|
t.container,
|
||||||
command,
|
command,
|
||||||
@@ -318,6 +381,10 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
|
||||||
|
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||||
|
}
|
||||||
|
|
||||||
func createTailscaleBuildOptions(version string) *dockertest.BuildOptions {
|
func createTailscaleBuildOptions(version string) *dockertest.BuildOptions {
|
||||||
var tailscaleBuildOptions *dockertest.BuildOptions
|
var tailscaleBuildOptions *dockertest.BuildOptions
|
||||||
switch version {
|
switch version {
|
||||||
|
|||||||
92
oidc.go
92
oidc.go
@@ -76,19 +76,54 @@ func (h *Headscale) RegisterOIDC(
|
|||||||
) {
|
) {
|
||||||
vars := mux.Vars(req)
|
vars := mux.Vars(req)
|
||||||
nodeKeyStr, ok := vars["nkey"]
|
nodeKeyStr, ok := vars["nkey"]
|
||||||
if !ok || nodeKeyStr == "" {
|
|
||||||
|
log.Debug().
|
||||||
|
Caller().
|
||||||
|
Str("node_key", nodeKeyStr).
|
||||||
|
Bool("ok", ok).
|
||||||
|
Msg("Received oidc register call")
|
||||||
|
|
||||||
|
if !NodePublicKeyRegex.Match([]byte(nodeKeyStr)) {
|
||||||
|
log.Warn().Str("node_key", nodeKeyStr).Msg("Invalid node key passed to registration url")
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusUnauthorized)
|
||||||
|
_, err := writer.Write([]byte("Unauthorized"))
|
||||||
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Caller().
|
Caller().
|
||||||
Msg("Missing node key in URL")
|
Err(err).
|
||||||
http.Error(writer, "Missing node key in URL", http.StatusBadRequest)
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().
|
// We need to make sure we dont open for XSS style injections, if the parameter that
|
||||||
|
// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
|
||||||
|
// the template and log an error.
|
||||||
|
var nodeKey key.NodePublic
|
||||||
|
err := nodeKey.UnmarshalText(
|
||||||
|
[]byte(NodePublicKeyEnsurePrefix(nodeKeyStr)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if !ok || nodeKeyStr == "" || err != nil {
|
||||||
|
log.Warn().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to parse incoming nodekey in OIDC registration")
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, err := writer.Write([]byte("Wrong params"))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
Caller().
|
Caller().
|
||||||
Str("node_key", nodeKeyStr).
|
Err(err).
|
||||||
Msg("Received oidc register call")
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
randomBlob := make([]byte, randomByteSize)
|
randomBlob := make([]byte, randomByteSize)
|
||||||
if _, err := rand.Read(randomBlob); err != nil {
|
if _, err := rand.Read(randomBlob); err != nil {
|
||||||
@@ -103,7 +138,7 @@ func (h *Headscale) RegisterOIDC(
|
|||||||
stateStr := hex.EncodeToString(randomBlob)[:32]
|
stateStr := hex.EncodeToString(randomBlob)[:32]
|
||||||
|
|
||||||
// place the node key into the state cache, so it can be retrieved later
|
// place the node key into the state cache, so it can be retrieved later
|
||||||
h.registrationCache.Set(stateStr, nodeKeyStr, registerCacheExpiration)
|
h.registrationCache.Set(stateStr, NodePublicKeyStripPrefix(nodeKey), registerCacheExpiration)
|
||||||
|
|
||||||
// Add any extra parameter provided in the configuration to the Authorize Endpoint request
|
// Add any extra parameter provided in the configuration to the Authorize Endpoint request
|
||||||
extras := make([]oauth2.AuthCodeOption, 0, len(h.cfg.OIDC.ExtraParams))
|
extras := make([]oauth2.AuthCodeOption, 0, len(h.cfg.OIDC.ExtraParams))
|
||||||
@@ -405,8 +440,8 @@ func (h *Headscale) validateMachineForOIDCCallback(
|
|||||||
claims *IDTokenClaims,
|
claims *IDTokenClaims,
|
||||||
) (*key.NodePublic, bool, error) {
|
) (*key.NodePublic, bool, error) {
|
||||||
// retrieve machinekey from state cache
|
// retrieve machinekey from state cache
|
||||||
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
|
nodeKeyIf, nodeKeyFound := h.registrationCache.Get(state)
|
||||||
if !machineKeyFound {
|
if !nodeKeyFound {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msg("requested machine state key expired before authorisation completed")
|
Msg("requested machine state key expired before authorisation completed")
|
||||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
@@ -419,20 +454,38 @@ func (h *Headscale) validateMachineForOIDCCallback(
|
|||||||
Msg("Failed to write response")
|
Msg("Failed to write response")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false, errOIDCInvalidMachineState
|
return nil, false, errOIDCNodeKeyMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeKey key.NodePublic
|
var nodeKey key.NodePublic
|
||||||
nodeKeyFromCache, nodeKeyOK := machineKeyIf.(string)
|
nodeKeyFromCache, nodeKeyOK := nodeKeyIf.(string)
|
||||||
|
if !nodeKeyOK {
|
||||||
|
log.Error().
|
||||||
|
Msg("requested machine state key is not a string")
|
||||||
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, err := writer.Write([]byte("state is invalid"))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false, errOIDCInvalidMachineState
|
||||||
|
}
|
||||||
|
|
||||||
err := nodeKey.UnmarshalText(
|
err := nodeKey.UnmarshalText(
|
||||||
[]byte(NodePublicKeyEnsurePrefix(nodeKeyFromCache)),
|
[]byte(NodePublicKeyEnsurePrefix(nodeKeyFromCache)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
|
Str("nodeKey", nodeKeyFromCache).
|
||||||
|
Bool("nodeKeyOK", nodeKeyOK).
|
||||||
Msg("could not parse node public key")
|
Msg("could not parse node public key")
|
||||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
writer.WriteHeader(http.StatusBadRequest)
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
_, werr := writer.Write([]byte("could not parse public key"))
|
_, werr := writer.Write([]byte("could not parse node public key"))
|
||||||
if werr != nil {
|
if werr != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Caller().
|
Caller().
|
||||||
@@ -443,21 +496,6 @@ func (h *Headscale) validateMachineForOIDCCallback(
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !nodeKeyOK {
|
|
||||||
log.Error().Msg("could not get node key from cache")
|
|
||||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
writer.WriteHeader(http.StatusInternalServerError)
|
|
||||||
_, err := writer.Write([]byte("could not get node key from cache"))
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to write response")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false, errOIDCNodeKeyMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieve machine information if it exist
|
// retrieve machine information if it exist
|
||||||
// The error is not important, because if it does not
|
// The error is not important, because if it does not
|
||||||
// exist, then this is a new machine and we will move
|
// exist, then this is a new machine and we will move
|
||||||
|
|||||||
@@ -490,6 +490,7 @@ func (h *Headscale) handleNewMachineCommon(
|
|||||||
Bool("noise", machineKey.IsZero()).
|
Bool("noise", machineKey.IsZero()).
|
||||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
Msg("The node seems to be new, sending auth url")
|
Msg("The node seems to be new, sending auth url")
|
||||||
|
|
||||||
if h.oauth2Config != nil {
|
if h.oauth2Config != nil {
|
||||||
resp.AuthURL = fmt.Sprintf(
|
resp.AuthURL = fmt.Sprintf(
|
||||||
"%s/oidc/register/%s",
|
"%s/oidc/register/%s",
|
||||||
@@ -528,6 +529,7 @@ func (h *Headscale) handleNewMachineCommon(
|
|||||||
log.Info().
|
log.Info().
|
||||||
Caller().
|
Caller().
|
||||||
Bool("noise", machineKey.IsZero()).
|
Bool("noise", machineKey.IsZero()).
|
||||||
|
Str("AuthURL", resp.AuthURL).
|
||||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||||
Msg("Successfully sent auth url")
|
Msg("Successfully sent auth url")
|
||||||
}
|
}
|
||||||
@@ -726,7 +728,7 @@ func (h *Headscale) handleMachineExpiredCommon(
|
|||||||
if h.oauth2Config != nil {
|
if h.oauth2Config != nil {
|
||||||
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
|
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
|
||||||
strings.TrimSuffix(h.cfg.ServerURL, "/"),
|
strings.TrimSuffix(h.cfg.ServerURL, "/"),
|
||||||
NodePublicKeyStripPrefix(registerRequest.NodeKey))
|
registerRequest.NodeKey)
|
||||||
} else {
|
} else {
|
||||||
resp.AuthURL = fmt.Sprintf("%s/register/%s",
|
resp.AuthURL = fmt.Sprintf("%s/register/%s",
|
||||||
strings.TrimSuffix(h.cfg.ServerURL, "/"),
|
strings.TrimSuffix(h.cfg.ServerURL, "/"),
|
||||||
|
|||||||
Reference in New Issue
Block a user