Compare commits

..

2 Commits

Author SHA1 Message Date
Juan Font Alonso
5539ef1f8f Reduce the number of containers 2022-08-04 21:36:38 +02:00
Juan Font Alonso
100f7190f3 Temporary fix integration tests with dedicated Dockerfile 2022-07-31 10:44:09 +02:00
134 changed files with 4931 additions and 11519 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
ko_fi: kradalby
github: [kradalby]

View File

@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
with: with:
fetch-depth: 2 fetch-depth: 2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v34 uses: tj-actions/changed-files@v14.1
with: with:
files: | files: |
*.nix *.nix

View File

@@ -9,7 +9,7 @@ jobs:
add-contributors: add-contributors:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Delete upstream contributor branch - name: Delete upstream contributor branch
# Allow continue on failure to account for when the # Allow continue on failure to account for when the
# upstream branch is deleted or does not exist. # upstream branch is deleted or does not exist.

View File

@@ -1,5 +1,5 @@
--- ---
name: Lint name: CI
on: [push, pull_request] on: [push, pull_request]
@@ -7,13 +7,13 @@ jobs:
golangci-lint: golangci-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
with: with:
fetch-depth: 2 fetch-depth: 2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v34 uses: tj-actions/changed-files@v14.1
with: with:
files: | files: |
*.nix *.nix
@@ -26,7 +26,7 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
with: with:
version: v1.49.0 version: v1.46.1
# Only block PRs on new problems. # Only block PRs on new problems.
# If this is not enabled, we will end up having PRs # If this is not enabled, we will end up having PRs
@@ -70,7 +70,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: bufbuild/buf-setup-action@v1.7.0 - uses: bufbuild/buf-setup-action@v0.7.0
- uses: bufbuild/buf-lint-action@v1 - uses: bufbuild/buf-lint-action@v1
with: with:
input: "proto" input: "proto"

View File

@@ -1,5 +1,5 @@
--- ---
name: Release name: release
on: on:
push: push:
@@ -12,13 +12,13 @@ jobs:
runs-on: ubuntu-18.04 # due to CGO we need to user an older version runs-on: ubuntu-18.04 # due to CGO we need to user an older version
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v2
with: with:
go-version: 1.19.0 go-version: 1.18.0
- name: Install dependencies - name: Install dependencies
run: | run: |
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -65,8 +65,8 @@ 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
type=raw,value=develop
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -125,13 +125,13 @@ jobs:
${{ secrets.DOCKERHUB_USERNAME }}/headscale ${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale ghcr.io/${{ github.repository_owner }}/headscale
flavor: | flavor: |
suffix=-debug,onlatest=true latest=false
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}-debug
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}-debug
type=semver,pattern={{major}} type=semver,pattern={{major}}-debug
type=sha type=raw,value=latest-debug
type=raw,value=develop 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,3 +161,69 @@ 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@v2
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

27
.github/workflows/renovatebot.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Renovate
on:
schedule:
- cron: "* * 5,20 * *" # Every 5th and 20th of the month
workflow_dispatch:
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- name: Get token
id: get_token
uses: machine-learning-apps/actions-app-token@master
with:
APP_PEM: ${{ secrets.RENOVATEBOT_SECRET }}
APP_ID: ${{ secrets.RENOVATEBOT_APP_ID }}
- name: Checkout
uses: actions/checkout@v2.0.0
- name: Self-hosted Renovate
uses: renovatebot/github-action@v31.81.3
with:
configurationFile: .github/renovate.json
token: "x-access-token:${{ steps.get_token.outputs.app_token }}"
# env:
# LOG_LEVEL: "debug"

View File

@@ -1,35 +0,0 @@
name: Integration Test DERP
on: [pull_request]
jobs:
integration-test-derp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- 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@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run Embedded DERP server integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_derp

View File

@@ -1,27 +0,0 @@
name: Integration Test v2 - kradalby
on: [pull_request]
jobs:
integration-test-v2-kradalby:
runs-on: [self-hosted, linux, x64, nixos, docker]
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
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_v2_general

View File

@@ -1,24 +1,19 @@
name: Integration Test CLI name: CI
on: [pull_request] on: [pull_request]
jobs: jobs:
integration-test-cli: integration-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
with: with:
fetch-depth: 2 fetch-depth: 2
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v34 uses: tj-actions/changed-files@v14.1
with: with:
files: | files: |
*.nix *.nix
@@ -30,6 +25,11 @@ jobs:
- uses: cachix/install-nix-action@v16 - uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
- name: Run CLI integration tests - name: Run Integration tests
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_cli uses: nick-fields/retry@v2
with:
timeout_minutes: 240
max_attempts: 5
retry_on: error
command: nix develop --command -- make test_integration

View File

@@ -1,4 +1,4 @@
name: Tests name: CI
on: [push, pull_request] on: [push, pull_request]
@@ -7,13 +7,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
with: with:
fetch-depth: 2 fetch-depth: 2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v34 uses: tj-actions/changed-files@v14.1
with: with:
files: | files: |
*.nix *.nix

View File

@@ -1,8 +1,6 @@
--- ---
run: run:
timeout: 10m timeout: 10m
build-tags:
- ts2019
issues: issues:
skip-dirs: skip-dirs:
@@ -28,7 +26,6 @@ linters:
- ireturn - ireturn
- execinquery - execinquery
- exhaustruct - exhaustruct
- nolintlint
# We should strive to enable these: # We should strive to enable these:
- wrapcheck - wrapcheck
@@ -36,9 +33,6 @@ linters:
- makezero - makezero
- maintidx - maintidx
# Limits the methods of an interface to 10. We have more in integration tests
- interfacebloat
# We might want to enable this, but it might be a lot of work # We might want to enable this, but it might be a lot of work
- cyclop - cyclop
- nestif - nestif

View File

@@ -1,7 +1,7 @@
--- ---
before: before:
hooks: hooks:
- go mod tidy -compat=1.19 - go mod tidy -compat=1.18
release: release:
prerelease: auto prerelease: auto
@@ -20,8 +20,6 @@ builds:
- -mod=readonly - -mod=readonly
ldflags: ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: darwin-arm64 - id: darwin-arm64
main: ./cmd/headscale/headscale.go main: ./cmd/headscale/headscale.go
@@ -36,8 +34,6 @@ builds:
- -mod=readonly - -mod=readonly
ldflags: ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-amd64 - id: linux-amd64
mod_timestamp: "{{ .CommitTimestamp }}" mod_timestamp: "{{ .CommitTimestamp }}"
@@ -50,8 +46,6 @@ builds:
main: ./cmd/headscale/headscale.go main: ./cmd/headscale/headscale.go
ldflags: ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-arm64 - id: linux-arm64
mod_timestamp: "{{ .CommitTimestamp }}" mod_timestamp: "{{ .CommitTimestamp }}"
@@ -64,8 +58,6 @@ builds:
main: ./cmd/headscale/headscale.go main: ./cmd/headscale/headscale.go
ldflags: ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
archives: archives:
- id: golang-cross - id: golang-cross

View File

@@ -1,73 +1,6 @@
# CHANGELOG # CHANGELOG
## 0.17.0 (2022-XX-XX) ## 0.17.0 (2022-xx-xx)
### 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)
- Removed Alpine Linux container image [#962](https://github.com/juanfont/headscale/pull/962)
### Important Changes
- Added support for Tailscale TS2021 protocol [#738](https://github.com/juanfont/headscale/pull/738)
- Add experimental support for [SSH ACL](https://tailscale.com/kb/1018/acls/#tailscale-ssh) (see docs for limitations) [#847](https://github.com/juanfont/headscale/pull/847)
- Please note that this support should be considered _partially_ implemented
- SSH ACLs status:
- Support `accept` and `check` (SSH can be enabled and used for connecting and authentication)
- Rejecting connections **are not supported**, meaning that if you enable SSH, then assume that _all_ `ssh` connections **will be allowed**.
- If you decied to try this feature, please carefully managed permissions by blocking port `22` with regular ACLs or do _not_ set `--ssh` on your clients.
- We are currently improving our testing of the SSH ACLs, help us get an overview by testing and giving feedback.
- This feature should be considered dangerous and it is disabled by default. Enable by setting `HEADSCALE_EXPERIMENTAL_FEATURE_SSH=1`.
### Changes
- Add ability to specify config location via env var `HEADSCALE_CONFIG` [#674](https://github.com/juanfont/headscale/issues/674)
- Target Go 1.19 for Headscale [#778](https://github.com/juanfont/headscale/pull/778)
- Target Tailscale v1.30.0 to build Headscale [#780](https://github.com/juanfont/headscale/pull/780)
- Give a warning when running Headscale with reverse proxy improperly configured for WebSockets [#788](https://github.com/juanfont/headscale/pull/788)
- Fix subnet routers with Primary Routes [#811](https://github.com/juanfont/headscale/pull/811)
- Added support for JSON logs [#653](https://github.com/juanfont/headscale/issues/653)
- Sanitise the node key passed to registration url [#823](https://github.com/juanfont/headscale/pull/823)
- Add support for generating pre-auth keys with tags [#767](https://github.com/juanfont/headscale/pull/767)
- Add support for evaluating `autoApprovers` ACL entries when a machine is registered [#763](https://github.com/juanfont/headscale/pull/763)
- Add config flag to allow Headscale to start if OIDC provider is down [#829](https://github.com/juanfont/headscale/pull/829)
- Fix prefix length comparison bug in AutoApprovers route evaluation [#862](https://github.com/juanfont/headscale/pull/862)
- Random node DNS suffix only applied if names collide in namespace. [#766](https://github.com/juanfont/headscale/issues/766)
- Remove `ip_prefix` configuration option and warning [#899](https://github.com/juanfont/headscale/pull/899)
- 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)
- 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)
- Add support for specifying NextDNS DNS-over-HTTPS resolver [#940](https://github.com/juanfont/headscale/pull/940)
- Make more sslmode available for postgresql connection [#927](https://github.com/juanfont/headscale/pull/927)
## 0.16.4 (2022-08-21)
### Changes
- Add ability to connect to PostgreSQL over TLS/SSL [#745](https://github.com/juanfont/headscale/pull/745)
- Fix CLI registration of expired machines [#754](https://github.com/juanfont/headscale/pull/754)
## 0.16.3 (2022-08-17)
### Changes
- Fix issue with OIDC authentication [#747](https://github.com/juanfont/headscale/pull/747)
## 0.16.2 (2022-08-14)
### Changes
- Fixed bugs in the client registration process after migration to NodeKey [#735](https://github.com/juanfont/headscale/pull/735)
## 0.16.1 (2022-08-12)
### Changes
- Updated dependencies (including the library that lacked armhf support) [#722](https://github.com/juanfont/headscale/pull/722)
- Fix missing group expansion in function `excludeCorretlyTaggedNodes` [#563](https://github.com/juanfont/headscale/issues/563)
- Improve registration protocol implementation and switch to NodeKey as main identifier [#725](https://github.com/juanfont/headscale/pull/725)
- Add ability to connect to PostgreSQL via unix socket [#734](https://github.com/juanfont/headscale/pull/734)
## 0.16.0 (2022-07-25) ## 0.16.0 (2022-07-25)
@@ -177,7 +110,7 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
- OpenID Connect users will be mapped per namespaces - OpenID Connect users will be mapped per namespaces
- Each user will get its own namespace, created if it does not exist - Each user will get its own namespace, created if it does not exist
- `oidc.domain_map` option has been removed - `oidc.domain_map` option has been removed
- `strip_email_domain` option has been added (see [config-example.yaml](./config-example.yaml)) - `strip_email_domain` option has been added (see [config-example.yaml](./config_example.yaml))
### Changes ### Changes

View File

@@ -1,134 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation
in our community a harassment-free experience for everyone, regardless
of age, body size, visible or invisible disability, ethnicity, sex
characteristics, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open,
welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for
our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our
mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or
political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in
a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our
standards of acceptable behavior and will take appropriate and fair
corrective action in response to any behavior that they deem
inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit,
or reject comments, commits, code, wiki edits, issues, and other
contributions that are not aligned to this Code of Conduct, and will
communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also
applies when an individual is officially representing the community in
public spaces. Examples of representing our community include using an
official e-mail address, posting via an official social media account,
or acting as an appointed representative at an online or offline
event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported to the community leaders responsible for enforcement
at our Discord channel. All complaints
will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and
security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in
determining the consequences for any action they deem in violation of
this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior
deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders,
providing clarity around the nature of the violation and an
explanation of why the behavior was inappropriate. A public apology
may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued
behavior. No interaction with the people involved, including
unsolicited interaction with those enforcing the Code of Conduct, for
a specified period of time. This includes avoiding interactions in
community spaces as well as external channels like social
media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards,
including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or
public communication with the community for a specified period of
time. No public or private interaction with the people involved,
including unsolicited interaction with those enforcing the Code of
Conduct, is allowed during this period. Violating these terms may lead
to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of
community standards, including sustained inappropriate behavior,
harassment of an individual, or aggression toward or disparagement of
classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction
within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor
Covenant][homepage], version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of
conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the
FAQ at https://www.contributor-covenant.org/faq. Translations are
available at https://www.contributor-covenant.org/translations.

View File

@@ -1,5 +1,5 @@
# Builder image # Builder image
FROM docker.io/golang:1.19.0-bullseye AS build FROM --platform=$BUILDPLATFORM docker.io/golang:1.18.0-bullseye AS build
ARG VERSION=dev ARG VERSION=dev
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale
@@ -8,9 +8,8 @@ COPY go.mod go.sum /go/src/headscale/
RUN go mod download RUN go mod download
COPY . . COPY . .
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /go/bin/headscale -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 RUN test -e /go/bin/headscale
# Production image # Production image

24
Dockerfile.alpine Normal file
View File

@@ -0,0 +1,24 @@
# Builder image
FROM --platform=$BUILDPLATFORM docker.io/golang:1.18.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 . .
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /go/bin/headscale -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/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"]

View File

@@ -1,5 +1,5 @@
# Builder image # Builder image
FROM docker.io/golang:1.19.0-bullseye AS build FROM --platform=$BUILDPLATFORM docker.io/golang:1.18.0-bullseye AS build
ARG VERSION=dev ARG VERSION=dev
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale
@@ -9,11 +9,12 @@ RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale ARG TARGETOS TARGETARCH
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 test -e /go/bin/headscale RUN test -e /go/bin/headscale
# Debug image # Debug image
FROM docker.io/golang:1.19.0-bullseye FROM gcr.io/distroless/base-debian11:debug
COPY --from=build /go/bin/headscale /bin/headscale COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC ENV TZ UTC

View File

@@ -4,16 +4,14 @@ ARG TAILSCALE_VERSION=*
ARG TAILSCALE_CHANNEL=stable ARG TAILSCALE_CHANNEL=stable
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y gnupg curl ssh \ && apt-get install -y gnupg curl \
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \ && curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \ && curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
&& apt-get update \ && apt-get update \
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \ && apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN adduser --shell=/bin/bash ssh-it-user
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/ ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
RUN chmod 644 /usr/local/share/ca-certificates/server.crt RUN chmod 644 /usr/local/share/ca-certificates/server.crt
RUN update-ca-certificates RUN update-ca-certificates

View File

@@ -1,16 +1,13 @@
FROM golang:latest FROM golang:latest
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y ca-certificates dnsutils git iptables ssh \ && apt-get install -y ca-certificates dnsutils git iptables \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN useradd --shell=/bin/bash --create-home ssh-it-user
RUN git clone https://github.com/tailscale/tailscale.git RUN git clone https://github.com/tailscale/tailscale.git
WORKDIR /go/tailscale WORKDIR tailscale
RUN git checkout main
RUN sh build_dist.sh tailscale.com/cmd/tailscale RUN sh build_dist.sh tailscale.com/cmd/tailscale
RUN sh build_dist.sh tailscale.com/cmd/tailscaled RUN sh build_dist.sh tailscale.com/cmd/tailscaled
@@ -19,6 +16,6 @@ RUN cp tailscale /usr/local/bin/
RUN cp tailscaled /usr/local/bin/ RUN cp tailscaled /usr/local/bin/
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/ ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
RUN chmod 644 /usr/local/share/ca-certificates/server.crt RUN chmod 644 /usr/local/share/ca-certificates/server.crt
RUN update-ca-certificates RUN update-ca-certificates

View File

@@ -0,0 +1,21 @@
# Builder image
FROM docker.io/golang:1.18.0-bullseye AS build
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale
COPY go.mod go.sum /go/src/headscale/
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /go/bin/headscale -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN test -e /go/bin/headscale
# Production image
FROM gcr.io/distroless/base-debian11
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
EXPOSE 8080/tcp
CMD ["headscale"]

View File

@@ -10,8 +10,6 @@ ifeq ($(filter $(GOOS), openbsd netbsd soloaris plan9), )
else else
endif endif
TAGS = -tags ts2019
# GO_SOURCES = $(wildcard *.go) # GO_SOURCES = $(wildcard *.go)
# PROTO_SOURCES = $(wildcard **/*.proto) # PROTO_SOURCES = $(wildcard **/*.proto)
GO_SOURCES = $(call rwildcard,,*.go) GO_SOURCES = $(call rwildcard,,*.go)
@@ -19,44 +17,21 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
build: build:
nix build GOOS=$(GOOS) CGO_ENABLED=0 go build -trimpath $(pieflags) -mod=readonly -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
dev: lint test build dev: lint test build
test: test:
@go test $(TAGS) -short -coverprofile=coverage.out ./... @go test -coverprofile=coverage.out ./...
test_integration: test_integration_cli test_integration_derp test_integration_oidc test_integration_v2_general test_integration:
go test -failfast -tags integration -timeout 30m -count=1 ./...
test_integration_cli: test_integration_cli:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true go test -tags integration -v integration_cli_test.go integration_common_test.go
docker network create headscale-test || true
docker run -t --rm \
--network headscale-test \
-v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
test_integration_derp: test_integration_derp:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true go test -tags integration -v integration_embedded_derp_test.go integration_common_test.go
docker network create headscale-test || true
docker run -t --rm \
--network headscale-test \
-v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
test_integration_v2_general:
docker run \
-t --rm \
-v ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
-v $$PWD:$$PWD -w $$PWD/integration \
-v /var/run/docker.sock:/var/run/docker.sock \
golang:1 \
go test $(TAGS) -failfast ./... -timeout 120m -parallel 8
coverprofile_func: coverprofile_func:
go tool cover -func=coverage.out go tool cover -func=coverage.out

262
README.md
View File

@@ -1,4 +1,4 @@
![headscale logo](./docs/logo/headscale3_header_stacked_left.png) # headscale
![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg) ![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)
@@ -67,15 +67,15 @@ one of the maintainers.
## Client OS support ## Client OS support
| OS | Supports headscale | | OS | Supports headscale |
| ------- | --------------------------------------------------------- | | ------- | ----------------------------------------------------------------------------------------------------------------- |
| Linux | Yes | | Linux | Yes |
| OpenBSD | Yes | | OpenBSD | Yes |
| FreeBSD | Yes | | FreeBSD | Yes |
| macOS | Yes (see `/apple` on your headscale for more information) | | macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes [docs](./docs/windows-client.md) | | Windows | Yes [docs](./docs/windows-client.md) |
| Android | Yes [docs](./docs/android-client.md) | | Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
| iOS | Not yet | | iOS | Not yet |
## Running headscale ## Running headscale
@@ -195,15 +195,6 @@ make build
<sub style="font-size:14px"><b>Jiang Zhu</b></sub> <sub style="font-size:14px"><b>Jiang Zhu</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/tsujamin>
<img src=https://avatars.githubusercontent.com/u/2435619?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Benjamin Roberts/>
<br />
<sub style="font-size:14px"><b>Benjamin Roberts</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/reynico> <a href=https://github.com/reynico>
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/> <img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
@@ -211,6 +202,8 @@ make build
<sub style="font-size:14px"><b>Nico</b></sub> <sub style="font-size:14px"><b>Nico</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/e-zk> <a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/> <img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
@@ -239,6 +232,13 @@ make build
<sub style="font-size:14px"><b>unreality</b></sub> <sub style="font-size:14px"><b>unreality</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mpldr>
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
<br />
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ohdearaugustin> <a href=https://github.com/ohdearaugustin>
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/> <img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
@@ -248,34 +248,6 @@ make build
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mpldr>
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
<br />
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/GrigoriyMikhalkin>
<img src=https://avatars.githubusercontent.com/u/3637857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=GrigoriyMikhalkin/>
<br />
<sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mike-lloyd03>
<img src=https://avatars.githubusercontent.com/u/49411532?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mike Lloyd/>
<br />
<sub style="font-size:14px"><b>Mike Lloyd</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/iSchluff>
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
<br />
<sub style="font-size:14px"><b>Anton Schubert</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Niek> <a href=https://github.com/Niek>
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/> <img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
@@ -290,15 +262,6 @@ make build
<sub style="font-size:14px"><b>Eugen Biegler</b></sub> <sub style="font-size:14px"><b>Eugen Biegler</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/617a7a>
<img src=https://avatars.githubusercontent.com/u/67651251?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Azz/>
<br />
<sub style="font-size:14px"><b>Azz</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/qbit> <a href=https://github.com/qbit>
<img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/> <img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/>
@@ -307,17 +270,10 @@ make build
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kazauwa> <a href=https://github.com/iSchluff>
<img src=https://avatars.githubusercontent.com/u/12330159?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Igor Perepilitsyn/> <img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
<br /> <br />
<sub style="font-size:14px"><b>Igor Perepilitsyn</b></sub> <sub style="font-size:14px"><b>Anton Schubert</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Aluxima>
<img src=https://avatars.githubusercontent.com/u/16262531?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Laurent Marchaud/>
<br />
<sub style="font-size:14px"><b>Laurent Marchaud</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -328,19 +284,19 @@ make build
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/OrvilleQ> <a href=https://github.com/GrigoriyMikhalkin>
<img src=https://avatars.githubusercontent.com/u/21377465?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Orville Q. Song/> <img src=https://avatars.githubusercontent.com/u/3637857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=GrigoriyMikhalkin/>
<br /> <br />
<sub style="font-size:14px"><b>Orville Q. Song</b></sub> <sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
</a> </a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/hdhoang> <a href=https://github.com/hdhoang>
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hdhoang/> <img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Hoàng Đức Hiếu/>
<br /> <br />
<sub style="font-size:14px"><b>hdhoang</b></sub> <sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -357,13 +313,6 @@ make build
<sub style="font-size:14px"><b>Deon Thomas</b></sub> <sub style="font-size:14px"><b>Deon Thomas</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/madjam002>
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
<br />
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ChibangLW> <a href=https://github.com/ChibangLW>
<img src=https://avatars.githubusercontent.com/u/22293464?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ChibangLW/> <img src=https://avatars.githubusercontent.com/u/22293464?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ChibangLW/>
@@ -378,8 +327,6 @@ make build
<sub style="font-size:14px"><b>Mevan Samaratunga</b></sub> <sub style="font-size:14px"><b>Mevan Samaratunga</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dragetd> <a href=https://github.com/dragetd>
<img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./> <img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./>
@@ -387,6 +334,8 @@ make build
<sub style="font-size:14px"><b>Michael G.</b></sub> <sub style="font-size:14px"><b>Michael G.</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ptman> <a href=https://github.com/ptman>
<img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/> <img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/>
@@ -408,13 +357,6 @@ make build
<sub style="font-size:14px"><b>Stefan Majer</b></sub> <sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kevin1sMe>
<img src=https://avatars.githubusercontent.com/u/6886076?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kevinlin/>
<br />
<sub style="font-size:14px"><b>kevinlin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/artemklevtsov> <a href=https://github.com/artemklevtsov>
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/> <img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
@@ -422,8 +364,6 @@ make build
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub> <sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cmars> <a href=https://github.com/cmars>
<img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/> <img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/>
@@ -431,13 +371,6 @@ make build
<sub style="font-size:14px"><b>Casey Marshall</b></sub> <sub style="font-size:14px"><b>Casey Marshall</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/CNLHC>
<img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/>
<br />
<sub style="font-size:14px"><b>LiuHanCheng</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pvinis> <a href=https://github.com/pvinis>
<img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/> <img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/>
@@ -445,6 +378,8 @@ make build
<sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub> <sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/SilverBut> <a href=https://github.com/SilverBut>
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/> <img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
@@ -452,22 +387,6 @@ make build
<sub style="font-size:14px"><b>Silver Bullet</b></sub> <sub style="font-size:14px"><b>Silver Bullet</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/snh>
<img src=https://avatars.githubusercontent.com/u/2051768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Steven Honson/>
<br />
<sub style="font-size:14px"><b>Steven Honson</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ratsclub>
<img src=https://avatars.githubusercontent.com/u/25647735?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Victor Freire/>
<br />
<sub style="font-size:14px"><b>Victor Freire</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lachy2849> <a href=https://github.com/lachy2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/> <img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
@@ -489,13 +408,6 @@ make build
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub> <sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/puzpuzpuz>
<img src=https://avatars.githubusercontent.com/u/37772591?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andrei Pechkurov/>
<br />
<sub style="font-size:14px"><b>Andrei Pechkurov</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/apognu> <a href=https://github.com/apognu>
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/> <img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
@@ -512,13 +424,6 @@ make build
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/arnarg>
<img src=https://avatars.githubusercontent.com/u/1291396?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arnar/>
<br />
<sub style="font-size:14px"><b>Arnar</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/awoimbee> <a href=https://github.com/awoimbee>
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/> <img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
@@ -554,8 +459,6 @@ make build
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub> <sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/felixonmars> <a href=https://github.com/felixonmars>
<img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/> <img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/>
@@ -563,6 +466,8 @@ make build
<sub style="font-size:14px"><b>Felix Yan</b></sub> <sub style="font-size:14px"><b>Felix Yan</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/JJGadgets> <a href=https://github.com/JJGadgets>
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/> <img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
@@ -570,6 +475,13 @@ make build
<sub style="font-size:14px"><b>JJGadgets</b></sub> <sub style="font-size:14px"><b>JJGadgets</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/madjam002>
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
<br />
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jimt> <a href=https://github.com/jimt>
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/> <img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
@@ -577,13 +489,6 @@ make build
<sub style="font-size:14px"><b>Jim Tittsler</b></sub> <sub style="font-size:14px"><b>Jim Tittsler</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ShadowJonathan>
<img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/>
<br />
<sub style="font-size:14px"><b>Jonathan de Jong</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/piec> <a href=https://github.com/piec>
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/> <img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
@@ -591,15 +496,6 @@ make build
<sub style="font-size:14px"><b>Pierre Carru</b></sub> <sub style="font-size:14px"><b>Pierre Carru</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nnsee>
<img src=https://avatars.githubusercontent.com/u/36747857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Rasmus Moorats/>
<br />
<sub style="font-size:14px"><b>Rasmus Moorats</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/rcursaru> <a href=https://github.com/rcursaru>
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/> <img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
@@ -609,11 +505,13 @@ make build
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/renovate-bot> <a href=https://github.com/renovate-bot>
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mend Renovate/> <img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
<br /> <br />
<sub style="font-size:14px"><b>Mend Renovate</b></sub> <sub style="font-size:14px"><b>WhiteSource Renovate</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ryanfowler> <a href=https://github.com/ryanfowler>
<img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/> <img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/>
@@ -628,22 +526,6 @@ make build
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub> <sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/stefanvanburen>
<img src=https://avatars.githubusercontent.com/u/622527?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan VanBuren/>
<br />
<sub style="font-size:14px"><b>Stefan VanBuren</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/sophware>
<img src=https://avatars.githubusercontent.com/u/41669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sophware/>
<br />
<sub style="font-size:14px"><b>sophware</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/m-tanner-dev0> <a href=https://github.com/m-tanner-dev0>
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/> <img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
@@ -672,13 +554,8 @@ make build
<sub style="font-size:14px"><b>Tianon Gravi</b></sub> <sub style="font-size:14px"><b>Tianon Gravi</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> </tr>
<a href=https://github.com/thetillhoff> <tr>
<img src=https://avatars.githubusercontent.com/u/25052289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Till Hoffmann/>
<br />
<sub style="font-size:14px"><b>Till Hoffmann</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/woudsma> <a href=https://github.com/woudsma>
<img src=https://avatars.githubusercontent.com/u/6162978?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tjerk Woudsma/> <img src=https://avatars.githubusercontent.com/u/6162978?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tjerk Woudsma/>
@@ -686,8 +563,6 @@ make build
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub> <sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/y0ngb1n> <a href=https://github.com/y0ngb1n>
<img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/> <img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/>
@@ -695,13 +570,6 @@ make build
<sub style="font-size:14px"><b>Yang Bin</b></sub> <sub style="font-size:14px"><b>Yang Bin</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/gozssky>
<img src=https://avatars.githubusercontent.com/u/17199941?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yujie Xia/>
<br />
<sub style="font-size:14px"><b>Yujie Xia</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/zekker6> <a href=https://github.com/zekker6>
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/> <img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
@@ -709,13 +577,6 @@ make build
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub> <sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/zhzy0077>
<img src=https://avatars.githubusercontent.com/u/8717471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zhiyuan Zheng/>
<br />
<sub style="font-size:14px"><b>Zhiyuan Zheng</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Bpazy> <a href=https://github.com/Bpazy>
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ziyuan Han/> <img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ziyuan Han/>
@@ -730,8 +591,6 @@ make build
<sub style="font-size:14px"><b>derelm</b></sub> <sub style="font-size:14px"><b>derelm</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nning> <a href=https://github.com/nning>
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/> <img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
@@ -739,6 +598,8 @@ make build
<sub style="font-size:14px"><b>henning mueller</b></sub> <sub style="font-size:14px"><b>henning mueller</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ignoramous> <a href=https://github.com/ignoramous>
<img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/> <img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
@@ -746,25 +607,11 @@ make build
<sub style="font-size:14px"><b>ignoramous</b></sub> <sub style="font-size:14px"><b>ignoramous</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/magichuihui>
<img src=https://avatars.githubusercontent.com/u/10866198?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=suhelen/>
<br />
<sub style="font-size:14px"><b>suhelen</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lion24> <a href=https://github.com/lion24>
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sharkonet/> <img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lion24/>
<br /> <br />
<sub style="font-size:14px"><b>sharkonet</b></sub> <sub style="font-size:14px"><b>lion24</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/manju-rn>
<img src=https://avatars.githubusercontent.com/u/26291847?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=manju-rn/>
<br />
<sub style="font-size:14px"><b>manju-rn</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -774,15 +621,6 @@ make build
<sub style="font-size:14px"><b>pernila</b></sub> <sub style="font-size:14px"><b>pernila</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/phpmalik>
<img src=https://avatars.githubusercontent.com/u/26834645?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=phpmalik/>
<br />
<sub style="font-size:14px"><b>phpmalik</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Wakeful-Cloud> <a href=https://github.com/Wakeful-Cloud>
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/> <img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>

154
acls.go
View File

@@ -5,17 +5,15 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net/netip"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/tailscale/hujson" "github.com/tailscale/hujson"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"tailscale.com/envknob" "inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@@ -56,8 +54,6 @@ const (
ProtocolFC = 133 // Fibre Channel ProtocolFC = 133 // Fibre Channel
) )
var featureEnableSSH = envknob.RegisterBool("HEADSCALE_EXPERIMENTAL_FEATURE_SSH")
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules. // LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
func (h *Headscale) LoadACLPolicy(path string) error { func (h *Headscale) LoadACLPolicy(path string) error {
log.Debug(). log.Debug().
@@ -124,20 +120,6 @@ func (h *Headscale) UpdateACLRules() error {
log.Trace().Interface("ACL", rules).Msg("ACL rules generated") log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
h.aclRules = rules h.aclRules = rules
if featureEnableSSH() {
sshRules, err := h.generateSSHRules()
if err != nil {
return err
}
log.Trace().Interface("SSH", sshRules).Msg("SSH rules generated")
if h.sshPolicy == nil {
h.sshPolicy = &tailcfg.SSHPolicy{}
}
h.sshPolicy.Rules = sshRules
} else if h.aclPolicy != nil && len(h.aclPolicy.SSHs) > 0 {
log.Info().Msg("SSH ACLs has been defined, but HEADSCALE_EXPERIMENTAL_FEATURE_SSH is not enabled, this is a unstable feature, check docs before activating")
}
return nil return nil
} }
@@ -180,12 +162,7 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
destPorts := []tailcfg.NetPortRange{} destPorts := []tailcfg.NetPortRange{}
for innerIndex, dest := range acl.Destinations { for innerIndex, dest := range acl.Destinations {
dests, err := h.generateACLPolicyDest( dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
machines,
*h.aclPolicy,
dest,
needsWildcard,
)
if err != nil { if err != nil {
log.Error(). log.Error().
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex) Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
@@ -205,111 +182,6 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
return rules, nil return rules, nil
} }
func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) {
rules := []*tailcfg.SSHRule{}
if h.aclPolicy == nil {
return nil, errEmptyPolicy
}
machines, err := h.ListMachines()
if err != nil {
return nil, err
}
acceptAction := tailcfg.SSHAction{
Message: "",
Reject: false,
Accept: true,
SessionDuration: 0,
AllowAgentForwarding: false,
HoldAndDelegate: "",
AllowLocalPortForwarding: true,
}
rejectAction := tailcfg.SSHAction{
Message: "",
Reject: true,
Accept: false,
SessionDuration: 0,
AllowAgentForwarding: false,
HoldAndDelegate: "",
AllowLocalPortForwarding: false,
}
for index, sshACL := range h.aclPolicy.SSHs {
action := rejectAction
switch sshACL.Action {
case "accept":
action = acceptAction
case "check":
checkAction, err := sshCheckAction(sshACL.CheckPeriod)
if err != nil {
log.Error().
Msgf("Error parsing SSH %d, check action with unparsable duration '%s'", index, sshACL.CheckPeriod)
} else {
action = *checkAction
}
default:
log.Error().
Msgf("Error parsing SSH %d, unknown action '%s'", index, sshACL.Action)
return nil, err
}
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
for innerIndex, rawSrc := range sshACL.Sources {
expandedSrcs, err := expandAlias(
machines,
*h.aclPolicy,
rawSrc,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
log.Error().
Msgf("Error parsing SSH %d, Source %d", index, innerIndex)
return nil, err
}
for _, expandedSrc := range expandedSrcs {
principals = append(principals, &tailcfg.SSHPrincipal{
NodeIP: expandedSrc,
})
}
}
userMap := make(map[string]string, len(sshACL.Users))
for _, user := range sshACL.Users {
userMap[user] = "="
}
rules = append(rules, &tailcfg.SSHRule{
RuleExpires: nil,
Principals: principals,
SSHUsers: userMap,
Action: &action,
})
}
return rules, nil
}
func sshCheckAction(duration string) (*tailcfg.SSHAction, error) {
sessionLength, err := time.ParseDuration(duration)
if err != nil {
return nil, err
}
return &tailcfg.SSHAction{
Message: "",
Reject: false,
Accept: true,
SessionDuration: sessionLength,
AllowAgentForwarding: false,
HoldAndDelegate: "",
AllowLocalPortForwarding: true,
}, nil
}
func (h *Headscale) generateACLPolicySrcIP( func (h *Headscale) generateACLPolicySrcIP(
machines []Machine, machines []Machine,
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
@@ -383,12 +255,7 @@ func (h *Headscale) generateACLPolicyDest(
func parseProtocol(protocol string) ([]int, bool, error) { func parseProtocol(protocol string) ([]int, bool, error) {
switch protocol { switch protocol {
case "": case "":
return []int{ return []int{protocolICMP, protocolIPv6ICMP, protocolTCP, protocolUDP}, false, nil
protocolICMP,
protocolIPv6ICMP,
protocolTCP,
protocolUDP,
}, false, nil
case "igmp": case "igmp":
return []int{protocolIGMP}, true, nil return []int{protocolIGMP}, true, nil
case "ipv4", "ip-in-ip": case "ipv4", "ip-in-ip":
@@ -417,9 +284,7 @@ func parseProtocol(protocol string) ([]int, bool, error) {
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
needsWildcard := protocolNumber != protocolTCP && needsWildcard := protocolNumber != protocolTCP && protocolNumber != protocolUDP && protocolNumber != protocolSCTP
protocolNumber != protocolUDP &&
protocolNumber != protocolSCTP
return []int{protocolNumber}, needsWildcard, nil return []int{protocolNumber}, needsWildcard, nil
} }
@@ -502,7 +367,7 @@ func expandAlias(
// if alias is a namespace // if alias is a namespace
nodes := filterMachinesByNamespace(machines, alias) nodes := filterMachinesByNamespace(machines, alias)
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain) nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
for _, n := range nodes { for _, n := range nodes {
ips = append(ips, n.IPAddresses.ToStringSlice()...) ips = append(ips, n.IPAddresses.ToStringSlice()...)
@@ -517,13 +382,13 @@ func expandAlias(
} }
// if alias is an IP // if alias is an IP
ip, err := netip.ParseAddr(alias) ip, err := netaddr.ParseIP(alias)
if err == nil { if err == nil {
return []string{ip.String()}, nil return []string{ip.String()}, nil
} }
// if alias is an CIDR // if alias is an CIDR
cidr, err := netip.ParsePrefix(alias) cidr, err := netaddr.ParseIPPrefix(alias)
if err == nil { if err == nil {
return []string{cidr.String()}, nil return []string{cidr.String()}, nil
} }
@@ -540,13 +405,10 @@ func excludeCorrectlyTaggedNodes(
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
nodes []Machine, nodes []Machine,
namespace string, namespace string,
stripEmailDomain bool,
) []Machine { ) []Machine {
out := []Machine{} out := []Machine{}
tags := []string{} tags := []string{}
for tag := range aclPolicy.TagOwners { for tag, ns := range aclPolicy.TagOwners {
owners, _ := expandTagOwners(aclPolicy, namespace, stripEmailDomain)
ns := append(owners, namespace)
if contains(ns, namespace) { if contains(ns, namespace) {
tags = append(tags, tag) tags = append(tags, tag)
} }

View File

@@ -2,12 +2,11 @@ package headscale
import ( import (
"errors" "errors"
"net/netip"
"reflect" "reflect"
"testing" "testing"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"tailscale.com/envknob" "inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@@ -63,92 +62,13 @@ func (s *Suite) TestBasicRule(c *check.C) {
func (s *Suite) TestInvalidAction(c *check.C) { func (s *Suite) TestInvalidAction(c *check.C) {
app.aclPolicy = &ACLPolicy{ app.aclPolicy = &ACLPolicy{
ACLs: []ACL{ ACLs: []ACL{
{ {Action: "invalidAction", Sources: []string{"*"}, Destinations: []string{"*:*"}},
Action: "invalidAction",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
}, },
} }
err := app.UpdateACLRules() err := app.UpdateACLRules()
c.Assert(errors.Is(err, errInvalidAction), check.Equals, true) c.Assert(errors.Is(err, errInvalidAction), check.Equals, true)
} }
func (s *Suite) TestSshRules(c *check.C) {
envknob.Setenv("HEADSCALE_EXPERIMENTAL_FEATURE_SSH", "1")
namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := tailcfg.Hostinfo{
OS: "centos",
Hostname: "testmachine",
RequestTags: []string{"tag:test"},
}
machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Hostname: "testmachine",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo),
}
app.db.Save(&machine)
app.aclPolicy = &ACLPolicy{
Groups: Groups{
"group:test": []string{"user1"},
},
Hosts: Hosts{
"client": netip.PrefixFrom(netip.MustParseAddr("100.64.99.42"), 32),
},
ACLs: []ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
SSHs: []SSH{
{
Action: "accept",
Sources: []string{"group:test"},
Destinations: []string{"client"},
Users: []string{"autogroup:nonroot"},
},
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"client"},
Users: []string{"autogroup:nonroot"},
},
},
}
err = app.UpdateACLRules()
c.Assert(err, check.IsNil)
c.Assert(app.sshPolicy, check.NotNil)
c.Assert(app.sshPolicy.Rules, check.HasLen, 2)
c.Assert(app.sshPolicy.Rules[0].SSHUsers, check.HasLen, 1)
c.Assert(app.sshPolicy.Rules[0].Principals, check.HasLen, 1)
c.Assert(app.sshPolicy.Rules[0].Principals[0].NodeIP, check.Matches, "100.64.0.1")
c.Assert(app.sshPolicy.Rules[1].SSHUsers, check.HasLen, 1)
c.Assert(app.sshPolicy.Rules[1].Principals, check.HasLen, 1)
c.Assert(app.sshPolicy.Rules[1].Principals[0].NodeIP, check.Matches, "*")
}
func (s *Suite) TestInvalidGroupInGroup(c *check.C) { func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
// this ACL is wrong because the group in Sources sections doesn't exist // this ACL is wrong because the group in Sources sections doesn't exist
app.aclPolicy = &ACLPolicy{ app.aclPolicy = &ACLPolicy{
@@ -157,11 +77,7 @@ func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
"group:error": []string{"foo", "group:test"}, "group:error": []string{"foo", "group:test"},
}, },
ACLs: []ACL{ ACLs: []ACL{
{ {Action: "accept", Sources: []string{"group:error"}, Destinations: []string{"*:*"}},
Action: "accept",
Sources: []string{"group:error"},
Destinations: []string{"*:*"},
},
}, },
} }
err := app.UpdateACLRules() err := app.UpdateACLRules()
@@ -172,11 +88,7 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
// this ACL is wrong because no tagOwners own the requested tag for the server // this ACL is wrong because no tagOwners own the requested tag for the server
app.aclPolicy = &ACLPolicy{ app.aclPolicy = &ACLPolicy{
ACLs: []ACL{ ACLs: []ACL{
{ {Action: "accept", Sources: []string{"tag:foo"}, Destinations: []string{"*:*"}},
Action: "accept",
Sources: []string{"tag:foo"},
Destinations: []string{"*:*"},
},
}, },
} }
err := app.UpdateACLRules() err := app.UpdateACLRules()
@@ -190,7 +102,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@@ -207,7 +119,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -219,11 +131,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
Groups: Groups{"group:test": []string{"user1", "user2"}}, Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}}, TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{ ACLs: []ACL{
{ {Action: "accept", Sources: []string{"tag:test"}, Destinations: []string{"*:*"}},
Action: "accept",
Sources: []string{"tag:test"},
Destinations: []string{"*:*"},
},
}, },
} }
err = app.UpdateACLRules() err = app.UpdateACLRules()
@@ -240,7 +148,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@@ -257,7 +165,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -269,11 +177,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
Groups: Groups{"group:test": []string{"user1", "user2"}}, Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}}, TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{ ACLs: []ACL{
{ {Action: "accept", Sources: []string{"*"}, Destinations: []string{"tag:test:*"}},
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"tag:test:*"},
},
}, },
} }
err = app.UpdateACLRules() err = app.UpdateACLRules()
@@ -290,7 +194,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@@ -307,7 +211,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -318,11 +222,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
app.aclPolicy = &ACLPolicy{ app.aclPolicy = &ACLPolicy{
TagOwners: TagOwners{"tag:test": []string{"user1"}}, TagOwners: TagOwners{"tag:test": []string{"user1"}},
ACLs: []ACL{ ACLs: []ACL{
{ {Action: "accept", Sources: []string{"user1"}, Destinations: []string{"*:*"}},
Action: "accept",
Sources: []string{"user1"},
Destinations: []string{"*:*"},
},
}, },
} }
err = app.UpdateACLRules() err = app.UpdateACLRules()
@@ -339,7 +239,7 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "webserver") _, err = app.GetMachine("user1", "webserver")
@@ -356,7 +256,7 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "webserver", Hostname: "webserver",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -375,7 +275,7 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
NodeKey: "bar2", NodeKey: "bar2",
DiscoKey: "faab", DiscoKey: "faab",
Hostname: "user", Hostname: "user",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -471,7 +371,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
namespace, err := app.CreateNamespace("testnamespace") namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("testnamespace", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")
@@ -513,7 +413,7 @@ func (s *Suite) TestPortGroup(c *check.C) {
namespace, err := app.CreateNamespace("testnamespace") namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("testnamespace", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")
@@ -901,6 +801,7 @@ func Test_listMachinesInNamespace(t *testing.T) {
} }
} }
// nolint
func Test_expandAlias(t *testing.T) { func Test_expandAlias(t *testing.T) {
type args struct { type args struct {
machines []Machine machines []Machine
@@ -919,10 +820,10 @@ func Test_expandAlias(t *testing.T) {
args: args{ args: args{
alias: "*", alias: "*",
machines: []Machine{ machines: []Machine{
{IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}}, {IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}},
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.78.84.227"), netaddr.MustParseIP("100.78.84.227"),
}, },
}, },
}, },
@@ -939,25 +840,25 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{ machines: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netaddr.MustParseIP("100.64.0.3"),
}, },
Namespace: Namespace{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
@@ -977,25 +878,25 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{ machines: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netaddr.MustParseIP("100.64.0.3"),
}, },
Namespace: Namespace{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
@@ -1026,7 +927,7 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{}, machines: []Machine{},
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
Hosts: Hosts{ Hosts: Hosts{
"homeNetwork": netip.MustParsePrefix("192.168.1.0/24"), "homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"),
}, },
}, },
stripEmailDomain: true, stripEmailDomain: true,
@@ -1063,7 +964,7 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{ machines: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1074,7 +975,7 @@ func Test_expandAlias(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1085,13 +986,13 @@ func Test_expandAlias(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netaddr.MustParseIP("100.64.0.3"),
}, },
Namespace: Namespace{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
@@ -1111,25 +1012,25 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{ machines: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netaddr.MustParseIP("100.64.0.3"),
}, },
Namespace: Namespace{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
@@ -1152,27 +1053,27 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{ machines: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"}, ForcedTags: []string{"tag:hr-webserver"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"}, ForcedTags: []string{"tag:hr-webserver"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netaddr.MustParseIP("100.64.0.3"),
}, },
Namespace: Namespace{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
@@ -1190,14 +1091,14 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{ machines: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"}, ForcedTags: []string{"tag:hr-webserver"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1208,13 +1109,13 @@ func Test_expandAlias(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netaddr.MustParseIP("100.64.0.3"),
}, },
Namespace: Namespace{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
@@ -1236,7 +1137,7 @@ func Test_expandAlias(t *testing.T) {
machines: []Machine{ machines: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1247,7 +1148,7 @@ func Test_expandAlias(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1258,13 +1159,13 @@ func Test_expandAlias(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netaddr.MustParseIP("100.64.0.3"),
}, },
Namespace: Namespace{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
@@ -1300,10 +1201,9 @@ func Test_expandAlias(t *testing.T) {
func Test_excludeCorrectlyTaggedNodes(t *testing.T) { func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
type args struct { type args struct {
aclPolicy ACLPolicy aclPolicy ACLPolicy
nodes []Machine nodes []Machine
namespace string namespace string
stripEmailDomain bool
} }
tests := []struct { tests := []struct {
name string name string
@@ -1320,7 +1220,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
nodes: []Machine{ nodes: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1331,7 +1231,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1342,68 +1242,16 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
namespace: "joe", namespace: "joe",
stripEmailDomain: true,
}, },
want: []Machine{ want: []Machine{
{ {
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
Namespace: Namespace{Name: "joe"},
},
},
},
{
name: "exclude nodes with valid tags, and owner is in a group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:accountant": []string{"joe", "bar"},
},
TagOwners: TagOwners{
"tag:accountant-webserver": []string{"group:accountant"},
},
},
nodes: []Machine{
{
IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
namespace: "joe",
stripEmailDomain: true,
},
want: []Machine{
{
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
@@ -1417,7 +1265,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
nodes: []Machine{ nodes: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1428,24 +1276,23 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:accountant-webserver"}, ForcedTags: []string{"tag:accountant-webserver"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
namespace: "joe", namespace: "joe",
stripEmailDomain: true,
}, },
want: []Machine{ want: []Machine{
{ {
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
@@ -1459,7 +1306,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
nodes: []Machine{ nodes: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1470,7 +1317,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1481,18 +1328,17 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
namespace: "joe", namespace: "joe",
stripEmailDomain: true,
}, },
want: []Machine{ want: []Machine{
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netaddr.MustParseIP("100.64.0.1"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1503,7 +1349,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netaddr.MustParseIP("100.64.0.2"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
@@ -1514,7 +1360,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netaddr.MustParseIP("100.64.0.4"),
}, },
Namespace: Namespace{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
@@ -1527,7 +1373,6 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
test.args.aclPolicy, test.args.aclPolicy,
test.args.nodes, test.args.nodes,
test.args.namespace, test.args.namespace,
test.args.stripEmailDomain,
) )
if !reflect.DeepEqual(got, test.want) { if !reflect.DeepEqual(got, test.want) {
t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want) t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)

View File

@@ -2,65 +2,47 @@ package headscale
import ( import (
"encoding/json" "encoding/json"
"net/netip"
"strings" "strings"
"github.com/tailscale/hujson" "github.com/tailscale/hujson"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"inet.af/netaddr"
) )
// ACLPolicy represents a Tailscale ACL Policy. // ACLPolicy represents a Tailscale ACL Policy.
type ACLPolicy struct { type ACLPolicy struct {
Groups Groups `json:"groups" yaml:"groups"` Groups Groups `json:"groups" yaml:"groups"`
Hosts Hosts `json:"hosts" yaml:"hosts"` Hosts Hosts `json:"hosts" yaml:"hosts"`
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"` TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
ACLs []ACL `json:"acls" yaml:"acls"` ACLs []ACL `json:"acls" yaml:"acls"`
Tests []ACLTest `json:"tests" yaml:"tests"` Tests []ACLTest `json:"tests" yaml:"tests"`
AutoApprovers AutoApprovers `json:"autoApprovers" yaml:"autoApprovers"`
SSHs []SSH `json:"ssh" yaml:"ssh"`
} }
// ACL is a basic rule for the ACL Policy. // ACL is a basic rule for the ACL Policy.
type ACL struct { type ACL struct {
Action string `json:"action" yaml:"action"` Action string `json:"action" yaml:"action"`
Protocol string `json:"proto" yaml:"proto"` Protocol string `json:"proto" yaml:"proto"`
Sources []string `json:"src" yaml:"src"` Sources []string `json:"src" yaml:"src"`
Destinations []string `json:"dst" yaml:"dst"` Destinations []string `json:"dst" yaml:"dst"`
} }
// Groups references a series of alias in the ACL rules. // Groups references a series of alias in the ACL rules.
type Groups map[string][]string type Groups map[string][]string
// Hosts are alias for IP addresses or subnets. // Hosts are alias for IP addresses or subnets.
type Hosts map[string]netip.Prefix type Hosts map[string]netaddr.IPPrefix
// TagOwners specify what users (namespaces?) are allow to use certain tags. // TagOwners specify what users (namespaces?) are allow to use certain tags.
type TagOwners map[string][]string type TagOwners map[string][]string
// ACLTest is not implemented, but should be use to check if a certain rule is allowed. // ACLTest is not implemented, but should be use to check if a certain rule is allowed.
type ACLTest struct { type ACLTest struct {
Source string `json:"src" yaml:"src"` Source string `json:"src" yaml:"src"`
Accept []string `json:"accept" yaml:"accept"` Accept []string `json:"accept" yaml:"accept"`
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"` Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
} }
// AutoApprovers specify which users (namespaces?), groups or tags have their advertised routes // UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
// or exit node status automatically enabled.
type AutoApprovers struct {
Routes map[string][]string `json:"routes" yaml:"routes"`
ExitNode []string `json:"exitNode" yaml:"exitNode"`
}
// SSH controls who can ssh into which machines.
type SSH struct {
Action string `json:"action" yaml:"action"`
Sources []string `json:"src" yaml:"src"`
Destinations []string `json:"dst" yaml:"dst"`
Users []string `json:"users" yaml:"users"`
CheckPeriod string `json:"checkPeriod,omitempty" yaml:"checkPeriod,omitempty"`
}
// UnmarshalJSON allows to parse the Hosts directly into netip objects.
func (hosts *Hosts) UnmarshalJSON(data []byte) error { func (hosts *Hosts) UnmarshalJSON(data []byte) error {
newHosts := Hosts{} newHosts := Hosts{}
hostIPPrefixMap := make(map[string]string) hostIPPrefixMap := make(map[string]string)
@@ -78,7 +60,7 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
if !strings.Contains(prefixStr, "/") { if !strings.Contains(prefixStr, "/") {
prefixStr += "/32" prefixStr += "/32"
} }
prefix, err := netip.ParsePrefix(prefixStr) prefix, err := netaddr.ParseIPPrefix(prefixStr)
if err != nil { if err != nil {
return err return err
} }
@@ -89,7 +71,7 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// UnmarshalYAML allows to parse the Hosts directly into netip objects. // UnmarshalYAML allows to parse the Hosts directly into netaddr objects.
func (hosts *Hosts) UnmarshalYAML(data []byte) error { func (hosts *Hosts) UnmarshalYAML(data []byte) error {
newHosts := Hosts{} newHosts := Hosts{}
hostIPPrefixMap := make(map[string]string) hostIPPrefixMap := make(map[string]string)
@@ -99,7 +81,7 @@ func (hosts *Hosts) UnmarshalYAML(data []byte) error {
return err return err
} }
for host, prefixStr := range hostIPPrefixMap { for host, prefixStr := range hostIPPrefixMap {
prefix, err := netip.ParsePrefix(prefixStr) prefix, err := netaddr.ParseIPPrefix(prefixStr)
if err != nil { if err != nil {
return err return err
} }
@@ -118,28 +100,3 @@ func (policy ACLPolicy) IsZero() bool {
return false return false
} }
// Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix.
func (autoApprovers *AutoApprovers) GetRouteApprovers(
prefix netip.Prefix,
) ([]string, error) {
if prefix.Bits() == 0 {
return autoApprovers.ExitNode, nil // 0.0.0.0/0, ::/0 or equivalent
}
approverAliases := []string{}
for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes {
autoApprovedPrefix, err := netip.ParsePrefix(autoApprovedPrefix)
if err != nil {
return nil, err
}
if prefix.Bits() >= autoApprovedPrefix.Bits() &&
autoApprovedPrefix.Contains(prefix.Masked().Addr()) {
approverAliases = append(approverAliases, autoApproverAliases...)
}
}
return approverAliases, nil
}

775
api.go
View File

@@ -2,19 +2,25 @@ package headscale
import ( import (
"bytes" "bytes"
"encoding/binary"
"encoding/json" "encoding/json"
"errors"
"fmt"
"html/template" "html/template"
"io"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/klauspost/compress/zstd"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
) )
const ( const (
// TODO(juan): remove this once https://github.com/juanfont/headscale/issues/727 is fixed.
registrationHoldoff = time.Second * 5
reservedResponseHeaderSize = 4 reservedResponseHeaderSize = 4
RegisterMethodAuthKey = "authkey" RegisterMethodAuthKey = "authkey"
RegisterMethodOIDC = "oidc" RegisterMethodOIDC = "oidc"
@@ -53,7 +59,7 @@ func (h *Headscale) HealthHandler(
} }
} }
if err := h.pingDB(req.Context()); err != nil { if err := h.pingDB(); err != nil {
respond(err) respond(err)
return return
@@ -62,6 +68,23 @@ func (h *Headscale) HealthHandler(
respond(nil) respond(nil)
} }
// KeyHandler provides the Headscale pub key
// Listens in /key.
func (h *Headscale) KeyHandler(
writer http.ResponseWriter,
req *http.Request,
) {
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
type registerWebAPITemplateConfig struct { type registerWebAPITemplateConfig struct {
Key string Key string
} }
@@ -84,44 +107,13 @@ var registerWebAPITemplate = template.Must(
`)) `))
// RegisterWebAPI shows a simple message in the browser to point to the CLI // RegisterWebAPI shows a simple message in the browser to point to the CLI
// Listens in /register/:nkey. // Listens in /register.
//
// This is not part of the Tailscale control API, as we could send whatever URL
// in the RegisterResponse.AuthURL field.
func (h *Headscale) RegisterWebAPI( func (h *Headscale) RegisterWebAPI(
writer http.ResponseWriter, writer http.ResponseWriter,
req *http.Request, req *http.Request,
) { ) {
vars := mux.Vars(req) machineKeyStr := req.URL.Query().Get("key")
nodeKeyStr, ok := vars["nkey"] if machineKeyStr == "" {
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().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
// 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")
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)
_, err := writer.Write([]byte("Wrong params")) _, err := writer.Write([]byte("Wrong params"))
@@ -137,7 +129,7 @@ func (h *Headscale) RegisterWebAPI(
var content bytes.Buffer var content bytes.Buffer
if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{ if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
Key: nodeKeyStr, Key: machineKeyStr,
}); err != nil { }); err != nil {
log.Error(). log.Error().
Str("func", "RegisterWebAPI"). Str("func", "RegisterWebAPI").
@@ -158,7 +150,7 @@ func (h *Headscale) RegisterWebAPI(
writer.Header().Set("Content-Type", "text/html; charset=utf-8") writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK) writer.WriteHeader(http.StatusOK)
_, err = writer.Write(content.Bytes()) _, err := writer.Write(content.Bytes())
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@@ -166,3 +158,708 @@ func (h *Headscale) RegisterWebAPI(
Msg("Failed to write response") Msg("Failed to write response")
} }
} }
// RegistrationHandler handles the actual registration process of a machine
// Endpoint /machine/:mkey.
func (h *Headscale) RegistrationHandler(
writer http.ResponseWriter,
req *http.Request,
) {
vars := mux.Vars(req)
machineKeyStr, ok := vars["mkey"]
if !ok || machineKeyStr == "" {
log.Error().
Str("handler", "RegistrationHandler").
Msg("No machine ID in request")
http.Error(writer, "No machine ID in request", http.StatusBadRequest)
return
}
body, _ := io.ReadAll(req.Body)
var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse machine key")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
http.Error(writer, "Cannot parse machine key", http.StatusBadRequest)
return
}
registerRequest := tailcfg.RegisterRequest{}
err = decode(body, &registerRequest, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot decode message")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
http.Error(writer, "Cannot decode message", http.StatusBadRequest)
return
}
now := time.Now().UTC()
machine, err := h.GetMachineByMachineKey(machineKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Info().Str("machine", registerRequest.Hostinfo.Hostname).Msg("New machine")
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
// If the machine has AuthKey set, handle registration via PreAuthKeys
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(writer, req, machineKey, registerRequest)
return
}
givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
if err != nil {
log.Error().
Caller().
Str("func", "RegistrationHandler").
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
Err(err)
return
}
// The machine did not have a key to authenticate, which means
// that we rely on a method that calls back some how (OpenID or CLI)
// We create the machine and then keep it around until a callback
// happens
newMachine := Machine{
MachineKey: machineKeyStr,
Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName,
NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey),
LastSeen: &now,
Expiry: &time.Time{},
}
if !registerRequest.Expiry.IsZero() {
log.Trace().
Caller().
Str("machine", registerRequest.Hostinfo.Hostname).
Time("expiry", registerRequest.Expiry).
Msg("Non-zero expiry time requested")
newMachine.Expiry = &registerRequest.Expiry
}
h.registrationCache.Set(
machineKeyStr,
newMachine,
registerCacheExpiration,
)
h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
return
}
// The machine is already registered, so we need to pass through reauth or key update.
if machine != nil {
// If the NodeKey stored in headscale is the same as the key presented in a registration
// request, then we have a node that is either:
// - Trying to log out (sending a expiry in the past)
// - A valid, registered machine, looking for the node map
// - Expired machine wanting to reauthenticate
if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) {
// The client sends an Expiry in the past if the client is requesting to expire the key (aka logout)
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
if !registerRequest.Expiry.IsZero() && registerRequest.Expiry.UTC().Before(now) {
h.handleMachineLogOut(writer, req, machineKey, *machine)
return
}
// If machine is not expired, and is register, we have a already accepted this machine,
// let it proceed with a valid registration
if !machine.isExpired() {
h.handleMachineValidRegistration(writer, req, machineKey, *machine)
return
}
}
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
!machine.isExpired() {
h.handleMachineRefreshKey(writer, req, machineKey, registerRequest, *machine)
return
}
// The machine has expired
h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine)
return
}
}
func (h *Headscale) getMapResponse(
machineKey key.MachinePublic,
mapRequest tailcfg.MapRequest,
machine *Machine,
) ([]byte, error) {
log.Trace().
Str("func", "getMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname).
Msg("Creating Map response")
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
log.Error().
Caller().
Str("func", "getMapResponse").
Err(err).
Msg("Cannot convert to node")
return nil, err
}
peers, err := h.getValidPeers(machine)
if err != nil {
log.Error().
Caller().
Str("func", "getMapResponse").
Err(err).
Msg("Cannot fetch peers")
return nil, err
}
profiles := getMapResponseUserProfiles(*machine, peers)
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
log.Error().
Caller().
Str("func", "getMapResponse").
Err(err).
Msg("Failed to convert peers to Tailscale nodes")
return nil, err
}
dnsConfig := getMapResponseDNSConfig(
h.cfg.DNSConfig,
h.cfg.BaseDomain,
*machine,
peers,
)
resp := tailcfg.MapResponse{
KeepAlive: false,
Node: node,
Peers: nodePeers,
DNSConfig: dnsConfig,
Domain: h.cfg.BaseDomain,
PacketFilter: h.aclRules,
DERPMap: h.DERPMap,
UserProfiles: profiles,
Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort,
},
}
log.Trace().
Str("func", "getMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname).
// Interface("payload", resp).
Msgf("Generated map response: %s", tailMapResponseToString(resp))
var respBody []byte
if mapRequest.Compress == "zstd" {
src, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Str("func", "getMapResponse").
Err(err).
Msg("Failed to marshal response for the client")
return nil, err
}
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
} else {
respBody, err = encode(resp, &machineKey, h.privateKey)
if err != nil {
return nil, err
}
}
// declare the incoming size on the first 4 bytes
data := make([]byte, reservedResponseHeaderSize)
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
data = append(data, respBody...)
return data, nil
}
func (h *Headscale) getMapKeepAliveResponse(
machineKey key.MachinePublic,
mapRequest tailcfg.MapRequest,
) ([]byte, error) {
mapResponse := tailcfg.MapResponse{
KeepAlive: true,
}
var respBody []byte
var err error
if mapRequest.Compress == "zstd" {
src, err := json.Marshal(mapResponse)
if err != nil {
log.Error().
Caller().
Str("func", "getMapKeepAliveResponse").
Err(err).
Msg("Failed to marshal keepalive response for the client")
return nil, err
}
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
} else {
respBody, err = encode(mapResponse, &machineKey, h.privateKey)
if err != nil {
return nil, err
}
}
data := make([]byte, reservedResponseHeaderSize)
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
data = append(data, respBody...)
return data, nil
}
func (h *Headscale) handleMachineLogOut(
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
log.Info().
Str("machine", machine.Hostname).
Msg("Client requested logout")
err := h.ExpireMachine(&machine)
if err != nil {
log.Error().
Caller().
Str("func", "handleMachineLogOut").
Err(err).
Msg("Failed to expire machine")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
resp.AuthURL = ""
resp.MachineAuthorized = false
resp.User = *machine.Namespace.toUser()
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
func (h *Headscale) handleMachineValidRegistration(
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is valid, respond with redirect to /map
log.Debug().
Str("machine", machine.Hostname).
Msg("Client is registered and we have the current NodeKey. All clear to /map")
resp.AuthURL = ""
resp.MachineAuthorized = true
resp.User = *machine.Namespace.toUser()
resp.Login = *machine.Namespace.toLogin()
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
Inc()
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
func (h *Headscale) handleMachineExpired(
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
// The client has registered before, but has expired
log.Debug().
Str("machine", machine.Hostname).
Msg("Machine registration has expired. Sending a authurl to register")
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(writer, req, machineKey, registerRequest)
return
}
if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
} else {
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
}
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
Inc()
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
func (h *Headscale) handleMachineRefreshKey(
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
log.Debug().
Str("machine", machine.Hostname).
Msg("We have the OldNodeKey in the database. This is a key refresh")
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
if err := h.db.Save(&machine).Error; err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to update machine key in the database")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
resp.AuthURL = ""
resp.User = *machine.Namespace.toUser()
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
func (h *Headscale) handleMachineRegistrationNew(
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is new, redirect the client to the registration URL
log.Debug().
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("The node is sending us a new NodeKey, sending auth url")
if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf(
"%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
machineKey.String(),
)
} else {
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey))
}
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
// TODO: check if any locks are needed around IP allocation.
func (h *Headscale) handleAuthKey(
writer http.ResponseWriter,
req *http.Request,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
) {
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
log.Debug().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{}
pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusUnauthorized)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Failed authentication via AuthKey")
if pak != nil {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
} else {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
}
return
}
log.Debug().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
// retrieve machine information if it exist
// The error is not important, because if it does not
// exist, then this is a new machine and we will move
// on to registration.
machine, _ := h.GetMachineByMachineKey(machineKey)
if machine != nil {
log.Trace().
Caller().
Str("machine", machine.Hostname).
Msg("machine already registered, refreshing with new auth key")
machine.NodeKey = nodeKey
machine.AuthKeyID = uint(pak.ID)
err := h.RefreshMachine(machine, registerRequest.Expiry)
if err != nil {
log.Error().
Caller().
Str("machine", machine.Hostname).
Err(err).
Msg("Failed to refresh machine")
return
}
} else {
now := time.Now().UTC()
givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
if err != nil {
log.Error().
Caller().
Str("func", "RegistrationHandler").
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
Err(err)
return
}
machineToRegister := Machine{
Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName,
NamespaceID: pak.Namespace.ID,
MachineKey: machineKeyStr,
RegisterMethod: RegisterMethodAuthKey,
Expiry: &registerRequest.Expiry,
NodeKey: nodeKey,
LastSeen: &now,
AuthKeyID: uint(pak.ID),
}
machine, err = h.RegisterMachine(
machineToRegister,
)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("could not register machine")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
}
err = h.UsePreAuthKey(pak)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to use pre-auth key")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser()
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
Inc()
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
log.Info().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey")
}

View File

@@ -1,81 +0,0 @@
package headscale
import (
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
)
func (h *Headscale) generateMapResponse(
mapRequest tailcfg.MapRequest,
machine *Machine,
) (*tailcfg.MapResponse, error) {
log.Trace().
Str("func", "generateMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname).
Msg("Creating Map response")
node, err := h.toNode(*machine, h.cfg.BaseDomain, h.cfg.DNSConfig)
if err != nil {
log.Error().
Caller().
Str("func", "generateMapResponse").
Err(err).
Msg("Cannot convert to node")
return nil, err
}
peers, err := h.getValidPeers(machine)
if err != nil {
log.Error().
Caller().
Str("func", "generateMapResponse").
Err(err).
Msg("Cannot fetch peers")
return nil, err
}
profiles := h.getMapResponseUserProfiles(*machine, peers)
nodePeers, err := h.toNodes(peers, h.cfg.BaseDomain, h.cfg.DNSConfig)
if err != nil {
log.Error().
Caller().
Str("func", "generateMapResponse").
Err(err).
Msg("Failed to convert peers to Tailscale nodes")
return nil, err
}
dnsConfig := getMapResponseDNSConfig(
h.cfg.DNSConfig,
h.cfg.BaseDomain,
*machine,
peers,
)
resp := tailcfg.MapResponse{
KeepAlive: false,
Node: node,
Peers: nodePeers,
DNSConfig: dnsConfig,
Domain: h.cfg.BaseDomain,
PacketFilter: h.aclRules,
SSHPolicy: h.sshPolicy,
DERPMap: h.DERPMap,
UserProfiles: profiles,
Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort,
},
}
log.Trace().
Str("func", "generateMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname).
// Interface("payload", resp).
Msgf("Generated map response: %s", tailMapResponseToString(resp))
return &resp, nil
}

View File

@@ -14,7 +14,7 @@ const (
apiPrefixLength = 7 apiPrefixLength = 7
apiKeyLength = 32 apiKeyLength = 32
ErrAPIKeyFailedToParse = Error("Failed to parse ApiKey") errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
) )
// APIKey describes the datamodel for API keys used to remotely authenticate with // APIKey describes the datamodel for API keys used to remotely authenticate with
@@ -116,7 +116,7 @@ func (h *Headscale) ExpireAPIKey(key *APIKey) error {
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) { func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
prefix, hash, found := strings.Cut(keyStr, ".") prefix, hash, found := strings.Cut(keyStr, ".")
if !found { if !found {
return false, ErrAPIKeyFailedToParse return false, errAPIKeyFailedToParse
} }
key, err := h.GetAPIKey(prefix) key, err := h.GetAPIKey(prefix)

180
app.go
View File

@@ -11,7 +11,6 @@ import (
"os" "os"
"os/signal" "os/signal"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@@ -19,13 +18,13 @@ import (
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/mux" "github.com/gorilla/mux"
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
zerolog "github.com/philip-bui/grpc-zerolog" zerolog "github.com/philip-bui/grpc-zerolog"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/puzpuzpuz/xsync/v2" "github.com/puzpuzpuz/xsync"
zl "github.com/rs/zerolog" zl "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/crypto/acme" "golang.org/x/crypto/acme"
@@ -73,24 +72,20 @@ const (
// Headscale represents the base app of the service. // Headscale represents the base app of the service.
type Headscale struct { type Headscale struct {
cfg *Config cfg *Config
db *gorm.DB db *gorm.DB
dbString string dbString string
dbType string dbType string
dbDebug bool dbDebug bool
privateKey *key.MachinePrivate privateKey *key.MachinePrivate
noisePrivateKey *key.MachinePrivate
noiseMux *mux.Router
DERPMap *tailcfg.DERPMap DERPMap *tailcfg.DERPMap
DERPServer *DERPServer DERPServer *DERPServer
aclPolicy *ACLPolicy aclPolicy *ACLPolicy
aclRules []tailcfg.FilterRule aclRules []tailcfg.FilterRule
sshPolicy *tailcfg.SSHPolicy
lastStateChange *xsync.MapOf[string, time.Time] lastStateChange *xsync.MapOf[time.Time]
oidcProvider *oidc.Provider oidcProvider *oidc.Provider
oauth2Config *oauth2.Config oauth2Config *oauth2.Config
@@ -103,47 +98,44 @@ type Headscale struct {
pollNetMapStreamWG sync.WaitGroup pollNetMapStreamWG sync.WaitGroup
} }
// Look up the TLS constant relative to user-supplied TLS client
// authentication mode. If an unknown mode is supplied, the default
// value, tls.RequireAnyClientCert, is returned. The returned boolean
// indicates if the supplied mode was valid.
func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
switch mode {
case DisabledClientAuth:
// Client cert is _not_ required.
return tls.NoClientCert, true
case RelaxedClientAuth:
// Client cert required, but _not verified_.
return tls.RequireAnyClientCert, true
case EnforcedClientAuth:
// Client cert is _required and verified_.
return tls.RequireAndVerifyClientCert, true
default:
// Return the default when an unknown value is supplied.
return tls.RequireAnyClientCert, false
}
}
func NewHeadscale(cfg *Config) (*Headscale, error) { func NewHeadscale(cfg *Config) (*Headscale, error) {
privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath) privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read or create private key: %w", err) return nil, fmt.Errorf("failed to read or create private key: %w", err)
} }
// TS2021 requires to have a different key from the legacy protocol.
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create Noise protocol private key: %w", err)
}
if privateKey.Equal(*noisePrivateKey) {
return nil, fmt.Errorf("private key and noise private key are the same: %w", err)
}
var dbString string var dbString string
switch cfg.DBtype { switch cfg.DBtype {
case Postgres: case Postgres:
dbString = fmt.Sprintf( dbString = fmt.Sprintf(
"host=%s dbname=%s user=%s", "host=%s port=%d dbname=%s user=%s password=%s sslmode=disable",
cfg.DBhost, cfg.DBhost,
cfg.DBport,
cfg.DBname, cfg.DBname,
cfg.DBuser, cfg.DBuser,
cfg.DBpass,
) )
if sslEnabled, err := strconv.ParseBool(cfg.DBssl); err == nil {
if !sslEnabled {
dbString += " sslmode=disable"
}
} else {
dbString += fmt.Sprintf(" sslmode=%s", cfg.DBssl)
}
if cfg.DBport != 0 {
dbString += fmt.Sprintf(" port=%d", cfg.DBport)
}
if cfg.DBpass != "" {
dbString += fmt.Sprintf(" password=%s", cfg.DBpass)
}
case Sqlite: case Sqlite:
dbString = cfg.DBpath dbString = cfg.DBpath
default: default:
@@ -159,8 +151,7 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
cfg: cfg, cfg: cfg,
dbType: cfg.DBtype, dbType: cfg.DBtype,
dbString: dbString, dbString: dbString,
privateKey: privateKey, privateKey: privKey,
noisePrivateKey: noisePrivateKey,
aclRules: tailcfg.FilterAllowAll, // default allowall aclRules: tailcfg.FilterAllowAll, // default allowall
registrationCache: registrationCache, registrationCache: registrationCache,
pollNetMapStreamWG: sync.WaitGroup{}, pollNetMapStreamWG: sync.WaitGroup{},
@@ -174,11 +165,7 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
if cfg.OIDC.Issuer != "" { if cfg.OIDC.Issuer != "" {
err = app.initOIDC() err = app.initOIDC()
if err != nil { if err != nil {
if cfg.OIDC.OnlyStartIfOIDCIsAvailable { return nil, err
return nil, err
} else {
log.Warn().Err(err).Msg("failed to set up OIDC provider, falling back to CLI based authentication")
}
} }
} }
@@ -219,16 +206,6 @@ func (h *Headscale) expireEphemeralNodes(milliSeconds int64) {
} }
} }
func (h *Headscale) failoverSubnetRoutes(milliSeconds int64) {
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
for range ticker.C {
err := h.handlePrimarySubnetFailover()
if err != nil {
log.Error().Err(err).Msg("failed to handle primary subnet failover")
}
}
}
func (h *Headscale) expireEphemeralNodesWorker() { func (h *Headscale) expireEphemeralNodesWorker() {
namespaces, err := h.ListNamespaces() namespaces, err := h.ListNamespaces()
if err != nil { if err != nil {
@@ -270,7 +247,7 @@ func (h *Headscale) expireEphemeralNodesWorker() {
} }
if expiredFound { if expiredFound {
h.setLastStateChangeToNow() h.setLastStateChangeToNow(namespace.Name)
} }
} }
} }
@@ -438,24 +415,19 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router { func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
router := mux.NewRouter() router := mux.NewRouter()
router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost)
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet) router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet) router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.HandleFunc("/register/{nkey}", h.RegisterWebAPI).Methods(http.MethodGet) router.HandleFunc("/register", h.RegisterWebAPI).Methods(http.MethodGet)
h.addLegacyHandlers(router) router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).Methods(http.MethodPost)
router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
router.HandleFunc("/oidc/register/{nkey}", h.RegisterOIDC).Methods(http.MethodGet) router.HandleFunc("/oidc/register/{mkey}", h.RegisterOIDC).Methods(http.MethodGet)
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet) router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet) router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig). router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig).Methods(http.MethodGet)
Methods(http.MethodGet)
router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet) router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig). router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig).Methods(http.MethodGet)
Methods(http.MethodGet)
router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet) router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet)
router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1). router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1).Methods(http.MethodGet)
Methods(http.MethodGet)
if h.cfg.DERP.ServerEnabled { if h.cfg.DERP.ServerEnabled {
router.HandleFunc("/derp", h.DERPHandler) router.HandleFunc("/derp", h.DERPHandler)
@@ -472,16 +444,6 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
return router return router
} }
func (h *Headscale) createNoiseMux() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/map", h.NoisePollNetMapHandler)
return router
}
// Serve launches a GIN server with the Headscale API. // Serve launches a GIN server with the Headscale API.
func (h *Headscale) Serve() error { func (h *Headscale) Serve() error {
var err error var err error
@@ -507,8 +469,6 @@ func (h *Headscale) Serve() error {
go h.expireEphemeralNodes(updateInterval) go h.expireEphemeralNodes(updateInterval)
go h.failoverSubnetRoutes(updateInterval)
if zl.GlobalLevel() == zl.TraceLevel { if zl.GlobalLevel() == zl.TraceLevel {
zerolog.RespLog = true zerolog.RespLog = true
} else { } else {
@@ -602,7 +562,7 @@ func (h *Headscale) Serve() error {
grpcOptions := []grpc.ServerOption{ grpcOptions := []grpc.ServerOption{
grpc.UnaryInterceptor( grpc.UnaryInterceptor(
grpcMiddleware.ChainUnaryServer( grpc_middleware.ChainUnaryServer(
h.grpcAuthenticationInterceptor, h.grpcAuthenticationInterceptor,
zerolog.NewUnaryServerInterceptor(), zerolog.NewUnaryServerInterceptor(),
), ),
@@ -637,15 +597,8 @@ func (h *Headscale) Serve() error {
// //
// HTTP setup // HTTP setup
// //
// This is the regular router that we expose
// over our main Addr. It also serves the legacy Tailcale API
router := h.createRouter(grpcGatewayMux)
// This router is served only over the Noise connection, and exposes only the new API. router := h.createRouter(grpcGatewayMux)
//
// The HTTP2 server that exposes this router is created for
// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
h.noiseMux = h.createNoiseMux()
httpServer := &http.Server{ httpServer := &http.Server{
Addr: h.cfg.Addr, Addr: h.cfg.Addr,
@@ -739,10 +692,7 @@ func (h *Headscale) Serve() error {
h.pollNetMapStreamWG.Wait() h.pollNetMapStreamWG.Wait()
// Gracefully shut down servers // Gracefully shut down servers
ctx, cancel := context.WithTimeout( ctx, cancel := context.WithTimeout(context.Background(), HTTPShutdownTimeout)
context.Background(),
HTTPShutdownTimeout,
)
if err := promHTTPServer.Shutdown(ctx); err != nil { if err := promHTTPServer.Shutdown(ctx); err != nil {
log.Error().Err(err).Msg("Failed to shutdown prometheus http") log.Error().Err(err).Msg("Failed to shutdown prometheus http")
} }
@@ -821,18 +771,10 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
// Configuration via autocert with HTTP-01. This requires listening on // Configuration via autocert with HTTP-01. This requires listening on
// port 80 for the certificate validation in addition to the headscale // port 80 for the certificate validation in addition to the headscale
// service, which can be configured to run on any other port. // service, which can be configured to run on any other port.
server := &http.Server{
Addr: h.cfg.TLS.LetsEncrypt.Listen,
Handler: certManager.HTTPHandler(http.HandlerFunc(h.redirect)),
ReadTimeout: HTTPReadTimeout,
}
go func() { go func() {
err := server.ListenAndServe()
log.Fatal(). log.Fatal().
Caller(). Caller().
Err(err). Err(http.ListenAndServe(h.cfg.TLS.LetsEncrypt.Listen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
Msg("failed to set up a HTTP server") Msg("failed to set up a HTTP server")
}() }()
@@ -852,7 +794,12 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://") log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
} }
log.Info().Msg(fmt.Sprintf(
"Client authentication (mTLS) is \"%s\". See the docs to learn about configuring this setting.",
h.cfg.TLS.ClientAuthMode))
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
ClientAuth: h.cfg.TLS.ClientAuthMode,
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1), Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
@@ -864,36 +811,35 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
} }
} }
func (h *Headscale) setLastStateChangeToNow() { func (h *Headscale) setLastStateChangeToNow(namespaces ...string) {
var err error var err error
now := time.Now().UTC() now := time.Now().UTC()
namespaces, err := h.ListNamespaces() if len(namespaces) == 0 {
if err != nil { namespaces, err = h.ListNamespacesStr()
log.Error(). if err != nil {
Caller(). log.Error().Caller().Err(err).Msg("failed to fetch all namespaces, failing to update last changed state.")
Err(err). }
Msg("failed to fetch all namespaces, failing to update last changed state.")
} }
for _, namespace := range namespaces { for _, namespace := range namespaces {
lastStateUpdate.WithLabelValues(namespace.Name, "headscale").Set(float64(now.Unix())) lastStateUpdate.WithLabelValues(namespace, "headscale").Set(float64(now.Unix()))
if h.lastStateChange == nil { if h.lastStateChange == nil {
h.lastStateChange = xsync.NewMapOf[time.Time]() h.lastStateChange = xsync.NewMapOf[time.Time]()
} }
h.lastStateChange.Store(namespace.Name, now) h.lastStateChange.Store(namespace, now)
} }
} }
func (h *Headscale) getLastStateChange(namespaces ...Namespace) time.Time { func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
times := []time.Time{} times := []time.Time{}
// getLastStateChange takes a list of namespaces as a "filter", if no namespaces // getLastStateChange takes a list of namespaces as a "filter", if no namespaces
// are past, then use the entier list of namespaces and look for the last update // are past, then use the entier list of namespaces and look for the last update
if len(namespaces) > 0 { if len(namespaces) > 0 {
for _, namespace := range namespaces { for _, namespace := range namespaces {
if lastChange, ok := h.lastStateChange.Load(namespace.Name); ok { if lastChange, ok := h.lastStateChange.Load(namespace); ok {
times = append(times, lastChange) times = append(times, lastChange)
} }
} }

View File

@@ -1,11 +1,12 @@
package headscale package headscale
import ( import (
"net/netip" "io/ioutil"
"os" "os"
"testing" "testing"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"inet.af/netaddr"
) )
func Test(t *testing.T) { func Test(t *testing.T) {
@@ -34,13 +35,13 @@ func (s *Suite) ResetDB(c *check.C) {
os.RemoveAll(tmpDir) os.RemoveAll(tmpDir)
} }
var err error var err error
tmpDir, err = os.MkdirTemp("", "autoygg-client-test") tmpDir, err = ioutil.TempDir("", "autoygg-client-test")
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
cfg := Config{ cfg := Config{
IPPrefixes: []netip.Prefix{ IPPrefixes: []netaddr.IPPrefix{
netip.MustParsePrefix("10.27.0.0/23"), netaddr.MustParseIPPrefix("10.27.0.0/23"),
}, },
} }
@@ -59,3 +60,20 @@ func (s *Suite) ResetDB(c *check.C) {
} }
app.db = db app.db = db
} }
// Enusre an error is returned when an invalid auth mode
// is supplied.
func (s *Suite) TestInvalidClientAuthMode(c *check.C) {
_, isValid := LookupTLSClientAuthMode("invalid")
c.Assert(isValid, check.Equals, false)
}
// Ensure that all client auth modes return a nil error.
func (s *Suite) TestAuthModes(c *check.C) {
modes := []string{"disabled", "relaxed", "enforced"}
for _, v := range modes {
_, isValid := LookupTLSClientAuthMode(v)
c.Assert(isValid, check.Equals, true)
}
}

View File

@@ -134,9 +134,7 @@ If you loose a key, create a new one and revoke (expire) the old one.`,
expiration := time.Now().UTC().Add(time.Duration(duration)) expiration := time.Now().UTC().Add(time.Duration(duration))
log.Trace(). log.Trace().Dur("expiration", time.Duration(duration)).Msg("expiration has been set")
Dur("expiration", time.Duration(duration)).
Msg("expiration has been set")
request.Expiration = timestamppb.New(expiration) request.Expiration = timestamppb.New(expiration)

View File

@@ -3,7 +3,6 @@ package cli
import ( import (
"fmt" "fmt"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -11,7 +10,8 @@ import (
) )
const ( const (
errPreAuthKeyMalformed = Error("key is malformed. expected 64 hex characters with `nodekey` prefix") keyLength = 64
errPreAuthKeyTooShort = Error("key too short, must be 64 hexadecimal characters")
) )
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors // Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
@@ -87,8 +87,8 @@ var createNodeCmd = &cobra.Command{
return return
} }
if !headscale.NodePublicKeyRegex.Match([]byte(machineKey)) { if len(machineKey) != keyLength {
err = errPreAuthKeyMalformed err = errPreAuthKeyTooShort
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Error: %s", err), fmt.Sprintf("Error: %s", err),

View File

@@ -1,104 +0,0 @@
package cli
import (
"fmt"
"net"
"os"
"strconv"
"time"
"github.com/oauth2-proxy/mockoidc"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
const (
errMockOidcClientIDNotDefined = Error("MOCKOIDC_CLIENT_ID not defined")
errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined")
errMockOidcPortNotDefined = Error("MOCKOIDC_PORT not defined")
accessTTL = 10 * time.Minute
refreshTTL = 60 * time.Minute
)
func init() {
rootCmd.AddCommand(mockOidcCmd)
}
var mockOidcCmd = &cobra.Command{
Use: "mockoidc",
Short: "Runs a mock OIDC server for testing",
Long: "This internal command runs a OpenID Connect for testing purposes",
Run: func(cmd *cobra.Command, args []string) {
err := mockOIDC()
if err != nil {
log.Error().Err(err).Msgf("Error running mock OIDC server")
os.Exit(1)
}
},
}
func mockOIDC() error {
clientID := os.Getenv("MOCKOIDC_CLIENT_ID")
if clientID == "" {
return errMockOidcClientIDNotDefined
}
clientSecret := os.Getenv("MOCKOIDC_CLIENT_SECRET")
if clientSecret == "" {
return errMockOidcClientSecretNotDefined
}
addrStr := os.Getenv("MOCKOIDC_ADDR")
if addrStr == "" {
return errMockOidcPortNotDefined
}
portStr := os.Getenv("MOCKOIDC_PORT")
if portStr == "" {
return errMockOidcPortNotDefined
}
port, err := strconv.Atoi(portStr)
if err != nil {
return err
}
mock, err := getMockOIDC(clientID, clientSecret)
if err != nil {
return err
}
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addrStr, port))
if err != nil {
return err
}
err = mock.Start(listener, nil)
if err != nil {
return err
}
log.Info().Msgf("Mock OIDC server listening on %s", listener.Addr().String())
log.Info().Msgf("Issuer: %s", mock.Issuer())
c := make(chan struct{})
<-c
return nil
}
func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, error) {
keypair, err := mockoidc.NewKeypair(nil)
if err != nil {
return nil, err
}
mock := mockoidc.MockOIDC{
ClientID: clientID,
ClientSecret: clientSecret,
AccessTTL: accessTTL,
RefreshTTL: refreshTTL,
CodeChallengeMethodsSupported: []string{"plain", "S256"},
Keypair: keypair,
SessionStore: mockoidc.NewSessionStore(),
UserQueue: &mockoidc.UserQueue{},
ErrorQueue: &mockoidc.ErrorQueue{},
}
return &mock, nil
}

View File

@@ -3,7 +3,6 @@ package cli
import ( import (
"fmt" "fmt"
"log" "log"
"net/netip"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -14,6 +13,7 @@ import (
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"inet.af/netaddr"
"tailscale.com/types/key" "tailscale.com/types/key"
) )
@@ -108,7 +108,7 @@ var registerNodeCmd = &cobra.Command{
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Error getting node key from flag: %s", err), fmt.Sprintf("Error getting machine key from flag: %s", err),
output, output,
) )
@@ -134,9 +134,7 @@ var registerNodeCmd = &cobra.Command{
return return
} }
SuccessOutput( SuccessOutput(response.Machine, "Machine register", output)
response.Machine,
fmt.Sprintf("Machine %s registered", response.Machine.GivenName), output)
}, },
} }
@@ -559,7 +557,7 @@ func nodesToPtables(
var IPV4Address string var IPV4Address string
var IPV6Address string var IPV6Address string
for _, addr := range machine.IpAddresses { for _, addr := range machine.IpAddresses {
if netip.MustParseAddr(addr).Is4() { if netaddr.MustParseIP(addr).Is4() {
IPV4Address = addr IPV4Address = addr
} else { } else {
IPV6Address = addr IPV6Address = addr

View File

@@ -3,7 +3,6 @@ package cli
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"time" "time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
@@ -34,8 +33,6 @@ func init() {
Bool("ephemeral", false, "Preauthkey for ephemeral nodes") Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
createPreAuthKeyCmd.Flags(). createPreAuthKeyCmd.Flags().
StringP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)") StringP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
createPreAuthKeyCmd.Flags().
StringSlice("tags", []string{}, "Tags to automatically assign to node")
} }
var preauthkeysCmd = &cobra.Command{ var preauthkeysCmd = &cobra.Command{
@@ -84,16 +81,7 @@ var listPreAuthKeys = &cobra.Command{
} }
tableData := pterm.TableData{ tableData := pterm.TableData{
{ {"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"},
"ID",
"Key",
"Reusable",
"Ephemeral",
"Used",
"Expiration",
"Created",
"Tags",
},
} }
for _, key := range response.PreAuthKeys { for _, key := range response.PreAuthKeys {
expiration := "-" expiration := "-"
@@ -108,14 +96,6 @@ var listPreAuthKeys = &cobra.Command{
reusable = fmt.Sprintf("%v", key.GetReusable()) reusable = fmt.Sprintf("%v", key.GetReusable())
} }
aclTags := ""
for _, tag := range key.AclTags {
aclTags += "," + tag
}
aclTags = strings.TrimLeft(aclTags, ",")
tableData = append(tableData, []string{ tableData = append(tableData, []string{
key.GetId(), key.GetId(),
key.GetKey(), key.GetKey(),
@@ -124,7 +104,6 @@ var listPreAuthKeys = &cobra.Command{
strconv.FormatBool(key.GetUsed()), strconv.FormatBool(key.GetUsed()),
expiration, expiration,
key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"), key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
aclTags,
}) })
} }
@@ -157,7 +136,6 @@ var createPreAuthKeyCmd = &cobra.Command{
reusable, _ := cmd.Flags().GetBool("reusable") reusable, _ := cmd.Flags().GetBool("reusable")
ephemeral, _ := cmd.Flags().GetBool("ephemeral") ephemeral, _ := cmd.Flags().GetBool("ephemeral")
tags, _ := cmd.Flags().GetStringSlice("tags")
log.Trace(). log.Trace().
Bool("reusable", reusable). Bool("reusable", reusable).
@@ -169,7 +147,6 @@ var createPreAuthKeyCmd = &cobra.Command{
Namespace: namespace, Namespace: namespace,
Reusable: reusable, Reusable: reusable,
Ephemeral: ephemeral, Ephemeral: ephemeral,
AclTags: tags,
} }
durationStr, _ := cmd.Flags().GetString("expiration") durationStr, _ := cmd.Flags().GetString("expiration")
@@ -187,9 +164,7 @@ var createPreAuthKeyCmd = &cobra.Command{
expiration := time.Now().UTC().Add(time.Duration(duration)) expiration := time.Now().UTC().Add(time.Duration(duration))
log.Trace(). log.Trace().Dur("expiration", time.Duration(duration)).Msg("expiration has been set")
Dur("expiration", time.Duration(duration)).
Msg("expiration has been set")
request.Expiration = timestamppb.New(expiration) request.Expiration = timestamppb.New(expiration)

View File

@@ -15,10 +15,6 @@ import (
var cfgFile string = "" var cfgFile string = ""
func init() { func init() {
if len(os.Args) > 1 && (os.Args[1] == "version" || os.Args[1] == "mockoidc" || os.Args[1] == "completion") {
return
}
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags(). rootCmd.PersistentFlags().
StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)") StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
@@ -29,18 +25,15 @@ func init() {
} }
func initConfig() { func initConfig() {
if cfgFile == "" {
cfgFile = os.Getenv("HEADSCALE_CONFIG")
}
if cfgFile != "" { if cfgFile != "" {
err := headscale.LoadConfig(cfgFile, true) err := headscale.LoadConfig(cfgFile, true)
if err != nil { if err != nil {
log.Fatal().Caller().Err(err).Msgf("Error loading config file %s", cfgFile) log.Fatal().Caller().Err(err)
} }
} else { } else {
err := headscale.LoadConfig("", false) err := headscale.LoadConfig("", false)
if err != nil { if err != nil {
log.Fatal().Caller().Err(err).Msgf("Error loading config") log.Fatal().Caller().Err(err)
} }
} }
@@ -51,7 +44,7 @@ func initConfig() {
machineOutput := HasMachineOutputFlag() machineOutput := HasMachineOutputFlag()
zerolog.SetGlobalLevel(cfg.Log.Level) zerolog.SetGlobalLevel(cfg.LogLevel)
// If the user has requested a "machine" readable format, // If the user has requested a "machine" readable format,
// then disable login so the output remains valid. // then disable login so the output remains valid.
@@ -59,10 +52,6 @@ func initConfig() {
zerolog.SetGlobalLevel(zerolog.Disabled) zerolog.SetGlobalLevel(zerolog.Disabled)
} }
if cfg.Log.Format == headscale.JSONLogFormat {
log.Logger = log.Output(os.Stdout)
}
if !cfg.DisableUpdateCheck && !machineOutput { if !cfg.DisableUpdateCheck && !machineOutput {
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
Version != "dev" { Version != "dev" {

View File

@@ -11,28 +11,29 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
const (
Base10 = 10
)
func init() { func init() {
rootCmd.AddCommand(routesCmd) rootCmd.AddCommand(routesCmd)
listRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)") listRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err := listRoutesCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
routesCmd.AddCommand(listRoutesCmd) routesCmd.AddCommand(listRoutesCmd)
enableRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)") enableRouteCmd.Flags().
err := enableRouteCmd.MarkFlagRequired("route") StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to enable")
enableRouteCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
enableRouteCmd.Flags().BoolP("all", "a", false, "All routes from host")
err = enableRouteCmd.MarkFlagRequired("identifier")
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
routesCmd.AddCommand(enableRouteCmd) routesCmd.AddCommand(enableRouteCmd)
disableRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)") nodeCmd.AddCommand(routesCmd)
err = disableRouteCmd.MarkFlagRequired("route")
if err != nil {
log.Fatalf(err.Error())
}
routesCmd.AddCommand(disableRouteCmd)
} }
var routesCmd = &cobra.Command{ var routesCmd = &cobra.Command{
@@ -43,7 +44,7 @@ var routesCmd = &cobra.Command{
var listRoutesCmd = &cobra.Command{ var listRoutesCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List all routes", Short: "List routes advertised and enabled by a given node",
Aliases: []string{"ls", "show"}, Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
@@ -63,51 +64,28 @@ var listRoutesCmd = &cobra.Command{
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
var routes []*v1.Route request := &v1.GetMachineRouteRequest{
MachineId: machineID,
if machineID == 0 {
response, err := client.GetRoutes(ctx, &v1.GetRoutesRequest{})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
return
}
routes = response.Routes
} else {
response, err := client.GetMachineRoutes(ctx, &v1.GetMachineRoutesRequest{
MachineId: machineID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get routes for machine %d: %s", machineID, status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
return
}
routes = response.Routes
} }
tableData := routesToPtables(routes) response, err := client.GetMachineRoute(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
return
}
tableData := routesToPtables(response.Routes)
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
@@ -129,12 +107,16 @@ var listRoutesCmd = &cobra.Command{
var enableRouteCmd = &cobra.Command{ var enableRouteCmd = &cobra.Command{
Use: "enable", Use: "enable",
Short: "Set a route as enabled", Short: "Set the enabled routes for a given node",
Long: `This command will make as enabled a given route.`, Long: `This command will take a list of routes that will _replace_
the current set of routes on a given node.
If you would like to disable a route, simply run the command again, but
omit the route you do not want to enable.
`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
routeID, err := cmd.Flags().GetUint64("route") machineID, err := cmd.Flags().GetUint64("identifier")
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
@@ -149,13 +131,52 @@ var enableRouteCmd = &cobra.Command{
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
response, err := client.EnableRoute(ctx, &v1.EnableRouteRequest{ var routes []string
RouteId: routeID,
}) isAll, _ := cmd.Flags().GetBool("all")
if isAll {
response, err := client.GetMachineRoute(ctx, &v1.GetMachineRouteRequest{
MachineId: machineID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot get machine routes: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
routes = response.GetRoutes().GetAdvertisedRoutes()
} else {
routes, err = cmd.Flags().GetStringSlice("route")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting routes from flag: %s", err),
output,
)
return
}
}
request := &v1.EnableMachineRoutesRequest{
MachineId: machineID,
Routes: routes,
}
response, err := client.EnableMachineRoutes(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Cannot enable route %d: %s", routeID, status.Convert(err).Message()), fmt.Sprintf(
"Cannot register machine: %s\n",
status.Convert(err).Message(),
),
output, output,
) )
@@ -163,71 +184,50 @@ var enableRouteCmd = &cobra.Command{
} }
if output != "" { if output != "" {
SuccessOutput(response, "", output) SuccessOutput(response.Routes, "", output)
return return
} }
},
}
var disableRouteCmd = &cobra.Command{ tableData := routesToPtables(response.Routes)
Use: "disable", if err != nil {
Short: "Set as disabled a given route", ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
Long: `This command will make as disabled a given route.`,
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
routeID, err := cmd.Flags().GetUint64("route") return
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Error getting machine id from flag: %s", err), fmt.Sprintf("Failed to render pterm table: %s", err),
output, output,
) )
return return
} }
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
response, err := client.DisableRoute(ctx, &v1.DisableRouteRequest{
RouteId: routeID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot enable route %d: %s", routeID, status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response, "", output)
return
}
}, },
} }
// routesToPtables converts the list of routes to a nice table. // routesToPtables converts the list of routes to a nice table.
func routesToPtables(routes []*v1.Route) pterm.TableData { func routesToPtables(routes *v1.Routes) pterm.TableData {
tableData := pterm.TableData{{"ID", "Machine", "Prefix", "Advertised", "Enabled", "Primary"}} tableData := pterm.TableData{{"Route", "Enabled"}}
for _, route := range routes { for _, route := range routes.GetAdvertisedRoutes() {
tableData = append(tableData, enabled := isStringInSlice(routes.EnabledRoutes, route)
[]string{
strconv.FormatUint(route.Id, Base10), tableData = append(tableData, []string{route, strconv.FormatBool(enabled)})
route.Machine.GivenName,
route.Prefix,
strconv.FormatBool(route.Advertised),
strconv.FormatBool(route.Enabled),
strconv.FormatBool(route.IsPrimary),
})
} }
return tableData return tableData
} }
func isStringInSlice(strs []string, s string) bool {
for _, s2 := range strs {
if s == s2 {
return true
}
}
return false
}

View File

@@ -19,16 +19,12 @@ import (
const ( const (
HeadscaleDateTimeFormat = "2006-01-02 15:04:05" HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
SocketWritePermissions = 0o666
) )
func getHeadscaleApp() (*headscale.Headscale, error) { func getHeadscaleApp() (*headscale.Headscale, error) {
cfg, err := headscale.GetHeadscaleConfig() cfg, err := headscale.GetHeadscaleConfig()
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf("failed to load configuration while creating headscale instance: %w", err)
"failed to load configuration while creating headscale instance: %w",
err,
)
} }
app, err := headscale.NewHeadscale(cfg) app, err := headscale.NewHeadscale(cfg)
@@ -82,19 +78,6 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
address = cfg.UnixSocket address = cfg.UnixSocket
// Try to give the user better feedback if we cannot write to the headscale
// socket.
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
if err != nil {
if os.IsPermission(err) {
log.Fatal().
Err(err).
Str("socket", cfg.UnixSocket).
Msgf("Unable to read/write to headscale socket, do you have the correct permissions?")
}
}
socket.Close()
grpcOptions = append( grpcOptions = append(
grpcOptions, grpcOptions,
grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTransportCredentials(insecure.NewCredentials()),

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"io/fs" "io/fs"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -27,7 +28,7 @@ func (s *Suite) TearDownSuite(c *check.C) {
} }
func (*Suite) TestConfigFileLoading(c *check.C) { func (*Suite) TestConfigFileLoading(c *check.C) {
tmpDir, err := os.MkdirTemp("", "headscale") tmpDir, err := ioutil.TempDir("", "headscale")
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
@@ -55,10 +56,10 @@ func (*Suite) TestConfigFileLoading(c *check.C) {
// Test that config file was interpreted correctly // Test that config file was interpreted correctly
c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080") c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080")
c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080") c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080")
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090") c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3") c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
c.Assert(viper.GetString("db_path"), check.Equals, "./db.sqlite") c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
@@ -72,7 +73,7 @@ func (*Suite) TestConfigFileLoading(c *check.C) {
} }
func (*Suite) TestConfigLoading(c *check.C) { func (*Suite) TestConfigLoading(c *check.C) {
tmpDir, err := os.MkdirTemp("", "headscale") tmpDir, err := ioutil.TempDir("", "headscale")
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
@@ -98,10 +99,10 @@ func (*Suite) TestConfigLoading(c *check.C) {
// Test that config file was interpreted correctly // Test that config file was interpreted correctly
c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080") c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080")
c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080") c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080")
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090") c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3") c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
c.Assert(viper.GetString("db_path"), check.Equals, "./db.sqlite") c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
@@ -116,7 +117,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
} }
func (*Suite) TestDNSConfigLoading(c *check.C) { func (*Suite) TestDNSConfigLoading(c *check.C) {
tmpDir, err := os.MkdirTemp("", "headscale") tmpDir, err := ioutil.TempDir("", "headscale")
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
@@ -151,24 +152,22 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
func writeConfig(c *check.C, tmpDir string, configYaml []byte) { func writeConfig(c *check.C, tmpDir string, configYaml []byte) {
// Populate a custom config file // Populate a custom config file
configFile := filepath.Join(tmpDir, "config.yaml") configFile := filepath.Join(tmpDir, "config.yaml")
err := os.WriteFile(configFile, configYaml, 0o600) err := ioutil.WriteFile(configFile, configYaml, 0o600)
if err != nil { if err != nil {
c.Fatalf("Couldn't write file %s", configFile) c.Fatalf("Couldn't write file %s", configFile)
} }
} }
func (*Suite) TestTLSConfigValidation(c *check.C) { func (*Suite) TestTLSConfigValidation(c *check.C) {
tmpDir, err := os.MkdirTemp("", "headscale") tmpDir, err := ioutil.TempDir("", "headscale")
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
// defer os.RemoveAll(tmpDir) // defer os.RemoveAll(tmpDir)
configYaml := []byte(`---
tls_letsencrypt_hostname: example.com configYaml := []byte(
tls_letsencrypt_challenge_type: "" "---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"",
tls_cert_path: abc.pem )
noise:
private_key_path: noise_private.key`)
writeConfig(c, tmpDir, configYaml) writeConfig(c, tmpDir, configYaml)
// Check configuration validation errors (1) // Check configuration validation errors (1)
@@ -193,13 +192,9 @@ noise:
) )
// Check configuration validation errors (2) // Check configuration validation errors (2)
configYaml = []byte(`--- configYaml = []byte(
noise: "---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
private_key_path: noise_private.key )
server_url: http://127.0.0.1:8080
tls_letsencrypt_hostname: example.com
tls_letsencrypt_challenge_type: TLS-ALPN-01
`)
writeConfig(c, tmpDir, configYaml) writeConfig(c, tmpDir, configYaml)
err = headscale.LoadConfig(tmpDir, false) err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

View File

@@ -14,9 +14,7 @@ server_url: http://127.0.0.1:8080
# Address to listen to / bind to on the server # Address to listen to / bind to on the server
# #
# For production: listen_addr: 0.0.0.0:8080
# listen_addr: 0.0.0.0:8080
listen_addr: 127.0.0.1:8080
# Address to listen to /metrics, you may want # Address to listen to /metrics, you may want
# to keep this endpoint private to your internal # to keep this endpoint private to your internal
@@ -29,10 +27,7 @@ metrics_listen_addr: 127.0.0.1:9090
# remotely with the CLI # remotely with the CLI
# Note: Remote access _only_ works if you have # Note: Remote access _only_ works if you have
# valid certificates. # valid certificates.
# grpc_listen_addr: 0.0.0.0:50443
# For production:
# grpc_listen_addr: 0.0.0.0:50443
grpc_listen_addr: 127.0.0.1:50443
# Allow the gRPC admin interface to run in INSECURE # Allow the gRPC admin interface to run in INSECURE
# mode. This is not recommended as the traffic will # mode. This is not recommended as the traffic will
@@ -40,25 +35,11 @@ grpc_listen_addr: 127.0.0.1:50443
# are doing. # are doing.
grpc_allow_insecure: false grpc_allow_insecure: false
# Private key used to encrypt the traffic between headscale # Private key used encrypt the traffic between headscale
# and Tailscale clients. # and Tailscale clients.
# The private key file will be autogenerated if it's missing. # The private key file which will be
# # autogenerated if it's missing
# For production: private_key_path: /var/lib/headscale/private.key
# /var/lib/headscale/private.key
private_key_path: ./private.key
# The Noise section includes specific configuration for the
# TS2021 Noise protocol
noise:
# The Noise private key is used to encrypt the
# traffic between headscale and Tailscale clients when
# using the new Noise-based protocol. It must be different
# from the legacy private key.
#
# For production:
# private_key_path: /var/lib/headscale/noise_private.key
private_key_path: ./noise_private.key
# List of IP prefixes to allocate tailaddresses from. # List of IP prefixes to allocate tailaddresses from.
# Each prefix consists of either an IPv4 or IPv6 address, # Each prefix consists of either an IPv4 or IPv6 address,
@@ -88,7 +69,7 @@ derp:
region_code: "headscale" region_code: "headscale"
region_name: "Headscale Embedded DERP" region_name: "Headscale Embedded DERP"
# Listens over UDP at the configured address for STUN connections - to help with NAT traversal. # Listens in UDP at the configured address for STUN connections to help on NAT traversal.
# When the embedded DERP server is enabled stun_listen_addr MUST be defined. # When the embedded DERP server is enabled stun_listen_addr MUST be defined.
# #
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/ # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
@@ -122,21 +103,17 @@ disable_check_updates: false
# Time before an inactive ephemeral node is deleted? # Time before an inactive ephemeral node is deleted?
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
# Period to check for node updates within the tailnet. A value too low will severely affect # Period to check for node updates in the tailnet. A value too low will severily affect
# CPU consumption of Headscale. A value too high (over 60s) will cause problems # CPU consumption of Headscale. A value too high (over 60s) will cause problems
# for the nodes, as they won't get updates or keep alive messages frequently enough. # to the nodes, as they won't get updates or keep alive messages in time.
# In case of doubts, do not touch the default 10s. # In case of doubts, do not touch the default 10s.
node_update_check_interval: 10s node_update_check_interval: 10s
# SQLite config # SQLite config
db_type: sqlite3 db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite
# For production:
# db_path: /var/lib/headscale/db.sqlite
db_path: ./db.sqlite
# # Postgres config # # Postgres config
# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
# db_type: postgres # db_type: postgres
# db_host: localhost # db_host: localhost
# db_port: 5432 # db_port: 5432
@@ -144,10 +121,6 @@ db_path: ./db.sqlite
# db_user: foo # db_user: foo
# db_pass: bar # db_pass: bar
# If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need
# in the 'db_ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1.
# db_ssl: false
### TLS configuration ### TLS configuration
# #
## Let's encrypt / ACME ## Let's encrypt / ACME
@@ -164,18 +137,23 @@ acme_email: ""
# Domain name to request a TLS certificate for: # Domain name to request a TLS certificate for:
tls_letsencrypt_hostname: "" tls_letsencrypt_hostname: ""
# Client (Tailscale/Browser) authentication mode (mTLS)
# Acceptable values:
# - disabled: client authentication disabled
# - relaxed: client certificate is required but not verified
# - enforced: client certificate is required and verified
tls_client_auth_mode: relaxed
# Path to store certificates and metadata needed by # Path to store certificates and metadata needed by
# letsencrypt # letsencrypt
# For production: tls_letsencrypt_cache_dir: /var/lib/headscale/cache
# tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_cache_dir: ./cache
# Type of ACME challenge to use, currently supported types: # Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS-ALPN-01 # HTTP-01 or TLS-ALPN-01
# See [docs/tls.md](docs/tls.md) for more information # See [docs/tls.md](docs/tls.md) for more information
tls_letsencrypt_challenge_type: HTTP-01 tls_letsencrypt_challenge_type: HTTP-01
# When HTTP-01 challenge is chosen, letsencrypt must set up a # When HTTP-01 challenge is chosen, letsencrypt must set up a
# verification endpoint, and it will be listening on: # verification endpoint, and it will be listning on:
# :http = port 80 # :http = port 80
tls_letsencrypt_listen: ":http" tls_letsencrypt_listen: ":http"
@@ -183,10 +161,7 @@ tls_letsencrypt_listen: ":http"
tls_cert_path: "" tls_cert_path: ""
tls_key_path: "" tls_key_path: ""
log: log_level: info
# Output formatting for logs: text or json
format: text
level: info
# Path to a file containg ACL policies. # Path to a file containg ACL policies.
# ACLs can be defined as YAML or HUJSON. # ACLs can be defined as YAML or HUJSON.
@@ -203,25 +178,10 @@ acl_policy_path: ""
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ # - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
# #
dns_config: dns_config:
# Whether to prefer using Headscale provided DNS or use local.
override_local_dns: true
# List of DNS servers to expose to clients. # List of DNS servers to expose to clients.
nameservers: nameservers:
- 1.1.1.1 - 1.1.1.1
# NextDNS (see https://tailscale.com/kb/1218/nextdns/).
# "abc123" is example NextDNS ID, replace with yours.
#
# With metadata sharing:
# nameservers:
# - https://dns.nextdns.io/abc123
#
# Without metadata sharing:
# nameservers:
# - 2a07:a8c0::ab:c123
# - 2a07:a8c1::ab:c123
# Split DNS (see https://tailscale.com/kb/1054/dns/), # Split DNS (see https://tailscale.com/kb/1054/dns/),
# list of search domains and the DNS to query for each one. # list of search domains and the DNS to query for each one.
# #
@@ -246,9 +206,9 @@ dns_config:
base_domain: example.com base_domain: example.com
# Unix socket used for the CLI to connect without authentication # Unix socket used for the CLI to connect without authentication
# Note: for production you will want to set this to something like: # Note: for local development, you probably want to change this to:
# unix_socket: /var/run/headscale.sock # unix_socket: ./headscale.sock
unix_socket: ./headscale.sock unix_socket: /var/run/headscale.sock
unix_socket_permission: "0770" unix_socket_permission: "0770"
# #
# headscale supports experimental OpenID connect support, # headscale supports experimental OpenID connect support,
@@ -256,7 +216,6 @@ unix_socket_permission: "0770"
# help us test it. # help us test it.
# OpenID Connect # OpenID Connect
# oidc: # oidc:
# only_start_if_oidc_is_available: true
# issuer: "https://your-oidc.issuer.com/path" # issuer: "https://your-oidc.issuer.com/path"
# client_id: "your-oidc-client-id" # client_id: "your-oidc-client-id"
# client_secret: "your-oidc-client-secret" # client_secret: "your-oidc-client-secret"

211
config.go
View File

@@ -1,10 +1,10 @@
package headscale package headscale
import ( import (
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"net/netip"
"net/url" "net/url"
"strings" "strings"
"time" "time"
@@ -13,7 +13,7 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/viper" "github.com/spf13/viper"
"go4.org/netipx" "inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
) )
@@ -21,9 +21,6 @@ import (
const ( const (
tlsALPN01ChallengeType = "TLS-ALPN-01" tlsALPN01ChallengeType = "TLS-ALPN-01"
http01ChallengeType = "HTTP-01" http01ChallengeType = "HTTP-01"
JSONLogFormat = "json"
TextLogFormat = "text"
) )
// Config contains the initial Headscale configuration. // Config contains the initial Headscale configuration.
@@ -35,11 +32,10 @@ type Config struct {
GRPCAllowInsecure bool GRPCAllowInsecure bool
EphemeralNodeInactivityTimeout time.Duration EphemeralNodeInactivityTimeout time.Duration
NodeUpdateCheckInterval time.Duration NodeUpdateCheckInterval time.Duration
IPPrefixes []netip.Prefix IPPrefixes []netaddr.IPPrefix
PrivateKeyPath string PrivateKeyPath string
NoisePrivateKeyPath string
BaseDomain string BaseDomain string
Log LogConfig LogLevel zerolog.Level
DisableUpdateCheck bool DisableUpdateCheck bool
DERP DERPConfig DERP DERPConfig
@@ -51,7 +47,6 @@ type Config struct {
DBname string DBname string
DBuser string DBuser string
DBpass string DBpass string
DBssl string
TLS TLSConfig TLS TLSConfig
@@ -74,8 +69,9 @@ type Config struct {
} }
type TLSConfig struct { type TLSConfig struct {
CertPath string CertPath string
KeyPath string KeyPath string
ClientAuthMode tls.ClientAuthType
LetsEncrypt LetsEncryptConfig LetsEncrypt LetsEncryptConfig
} }
@@ -88,15 +84,14 @@ type LetsEncryptConfig struct {
} }
type OIDCConfig struct { type OIDCConfig struct {
OnlyStartIfOIDCIsAvailable bool Issuer string
Issuer string ClientID string
ClientID string ClientSecret string
ClientSecret string Scope []string
Scope []string ExtraParams map[string]string
ExtraParams map[string]string AllowedDomains []string
AllowedDomains []string AllowedUsers []string
AllowedUsers []string StripEmaildomain bool
StripEmaildomain bool
} }
type DERPConfig struct { type DERPConfig struct {
@@ -126,11 +121,6 @@ type ACLConfig struct {
PolicyPath string PolicyPath string
} }
type LogConfig struct {
Format string
Level zerolog.Level
}
func LoadConfig(path string, isFile bool) error { func LoadConfig(path string, isFile bool) error {
if isFile { if isFile {
viper.SetConfigFile(path) viper.SetConfigFile(path)
@@ -152,12 +142,11 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache") viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls_letsencrypt_challenge_type", http01ChallengeType) viper.SetDefault("tls_letsencrypt_challenge_type", http01ChallengeType)
viper.SetDefault("tls_client_auth_mode", "relaxed")
viper.SetDefault("log.level", "info") viper.SetDefault("log_level", "info")
viper.SetDefault("log.format", TextLogFormat)
viper.SetDefault("dns_config", nil) viper.SetDefault("dns_config", nil)
viper.SetDefault("dns_config.override_local_dns", true)
viper.SetDefault("derp.server.enabled", false) viper.SetDefault("derp.server.enabled", false)
viper.SetDefault("derp.server.stun.enabled", true) viper.SetDefault("derp.server.stun.enabled", true)
@@ -173,7 +162,6 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"}) viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
viper.SetDefault("oidc.strip_email_domain", true) viper.SetDefault("oidc.strip_email_domain", true)
viper.SetDefault("oidc.only_start_if_oidc_is_available", true)
viper.SetDefault("logtail.enabled", false) viper.SetDefault("logtail.enabled", false)
viper.SetDefault("randomize_client_port", false) viper.SetDefault("randomize_client_port", false)
@@ -182,10 +170,6 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("node_update_check_interval", "10s") viper.SetDefault("node_update_check_interval", "10s")
if IsCLIConfigured() {
return nil
}
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
log.Warn().Err(err).Msg("Failed to read configuration from disk") log.Warn().Err(err).Msg("Failed to read configuration from disk")
@@ -199,10 +183,6 @@ func LoadConfig(path string, isFile bool) error {
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n" errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
} }
if !viper.IsSet("noise") || viper.GetString("noise.private_key_path") == "" {
errorText += "Fatal config error: headscale now requires a new `noise.private_key_path` field in the config file for the Tailscale v2 protocol\n"
}
if (viper.GetString("tls_letsencrypt_hostname") != "") && if (viper.GetString("tls_letsencrypt_hostname") != "") &&
(viper.GetString("tls_letsencrypt_challenge_type") == tlsALPN01ChallengeType) && (viper.GetString("tls_letsencrypt_challenge_type") == tlsALPN01ChallengeType) &&
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) { (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
@@ -221,6 +201,19 @@ func LoadConfig(path string, isFile bool) error {
errorText += "Fatal config error: server_url must start with https:// or http://\n" errorText += "Fatal config error: server_url must start with https:// or http://\n"
} }
_, authModeValid := LookupTLSClientAuthMode(
viper.GetString("tls_client_auth_mode"),
)
if !authModeValid {
errorText += fmt.Sprintf(
"Invalid tls_client_auth_mode supplied: %s. Accepted values: %s, %s, %s.",
viper.GetString("tls_client_auth_mode"),
DisabledClientAuth,
RelaxedClientAuth,
EnforcedClientAuth)
}
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds // Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
// to avoid races // to avoid races
minInactivityTimeout, _ := time.ParseDuration("65s") minInactivityTimeout, _ := time.ParseDuration("65s")
@@ -250,6 +243,10 @@ func LoadConfig(path string, isFile bool) error {
} }
func GetTLSConfig() TLSConfig { func GetTLSConfig() TLSConfig {
tlsClientAuthMode, _ := LookupTLSClientAuthMode(
viper.GetString("tls_client_auth_mode"),
)
return TLSConfig{ return TLSConfig{
LetsEncrypt: LetsEncryptConfig{ LetsEncrypt: LetsEncryptConfig{
Hostname: viper.GetString("tls_letsencrypt_hostname"), Hostname: viper.GetString("tls_letsencrypt_hostname"),
@@ -265,6 +262,7 @@ func GetTLSConfig() TLSConfig {
KeyPath: AbsolutePathFromConfigPath( KeyPath: AbsolutePathFromConfigPath(
viper.GetString("tls_key_path"), viper.GetString("tls_key_path"),
), ),
ClientAuthMode: tlsClientAuthMode,
} }
} }
@@ -329,59 +327,18 @@ func GetACLConfig() ACLConfig {
} }
} }
func GetLogConfig() LogConfig {
logLevelStr := viper.GetString("log.level")
logLevel, err := zerolog.ParseLevel(logLevelStr)
if err != nil {
logLevel = zerolog.DebugLevel
}
logFormatOpt := viper.GetString("log.format")
var logFormat string
switch logFormatOpt {
case "json":
logFormat = JSONLogFormat
case "text":
logFormat = TextLogFormat
case "":
logFormat = TextLogFormat
default:
log.Error().
Str("func", "GetLogConfig").
Msgf("Could not parse log format: %s. Valid choices are 'json' or 'text'", logFormatOpt)
}
return LogConfig{
Format: logFormat,
Level: logLevel,
}
}
func GetDNSConfig() (*tailcfg.DNSConfig, string) { func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config") { if viper.IsSet("dns_config") {
dnsConfig := &tailcfg.DNSConfig{} dnsConfig := &tailcfg.DNSConfig{}
overrideLocalDNS := viper.GetBool("dns_config.override_local_dns")
if viper.IsSet("dns_config.nameservers") { if viper.IsSet("dns_config.nameservers") {
nameserversStr := viper.GetStringSlice("dns_config.nameservers") nameserversStr := viper.GetStringSlice("dns_config.nameservers")
nameservers := []netip.Addr{} nameservers := make([]netaddr.IP, len(nameserversStr))
resolvers := []*dnstype.Resolver{} resolvers := make([]*dnstype.Resolver, len(nameserversStr))
for _, nameserverStr := range nameserversStr { for index, nameserverStr := range nameserversStr {
// Search for explicit DNS-over-HTTPS resolvers nameserver, err := netaddr.ParseIP(nameserverStr)
if strings.HasPrefix(nameserverStr, "https://") {
resolvers = append(resolvers, &dnstype.Resolver{
Addr: nameserverStr,
})
// This nameserver can not be parsed as an IP address
continue
}
// Parse nameserver as a regular IP
nameserver, err := netip.ParseAddr(nameserverStr)
if err != nil { if err != nil {
log.Error(). log.Error().
Str("func", "getDNSConfig"). Str("func", "getDNSConfig").
@@ -389,19 +346,14 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
Msgf("Could not parse nameserver IP: %s", nameserverStr) Msgf("Could not parse nameserver IP: %s", nameserverStr)
} }
nameservers = append(nameservers, nameserver) nameservers[index] = nameserver
resolvers = append(resolvers, &dnstype.Resolver{ resolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(), Addr: nameserver.String(),
}) }
} }
dnsConfig.Nameservers = nameservers dnsConfig.Nameservers = nameservers
dnsConfig.Resolvers = resolvers
if overrideLocalDNS {
dnsConfig.Resolvers = resolvers
} else {
dnsConfig.FallbackResolvers = resolvers
}
} }
if viper.IsSet("dns_config.restricted_nameservers") { if viper.IsSet("dns_config.restricted_nameservers") {
@@ -416,7 +368,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
len(restrictedNameservers), len(restrictedNameservers),
) )
for index, nameserverStr := range restrictedNameservers { for index, nameserverStr := range restrictedNameservers {
nameserver, err := netip.ParseAddr(nameserverStr) nameserver, err := netaddr.ParseIP(nameserverStr)
if err != nil { if err != nil {
log.Error(). log.Error().
Str("func", "getDNSConfig"). Str("func", "getDNSConfig").
@@ -436,17 +388,17 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
} }
if viper.IsSet("dns_config.domains") { if viper.IsSet("dns_config.domains") {
domains := viper.GetStringSlice("dns_config.domains") dnsConfig.Domains = viper.GetStringSlice("dns_config.domains")
if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Domains = domains
} else if domains != nil {
log.Warn().
Msg("Warning: dns_config.domains is set, but no nameservers are configured. Ignoring domains.")
}
} }
if viper.IsSet("dns_config.magic_dns") { if viper.IsSet("dns_config.magic_dns") {
dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") magicDNS := viper.GetBool("dns_config.magic_dns")
if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Proxied = magicDNS
} else if magicDNS {
log.Warn().
Msg("Warning: dns_config.magic_dns is set, but no nameservers are configured. Ignoring magic_dns.")
}
} }
var baseDomain string var baseDomain string
@@ -463,39 +415,50 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
} }
func GetHeadscaleConfig() (*Config, error) { func GetHeadscaleConfig() (*Config, error) {
if IsCLIConfigured() {
return &Config{
CLI: CLIConfig{
Address: viper.GetString("cli.address"),
APIKey: viper.GetString("cli.api_key"),
Timeout: viper.GetDuration("cli.timeout"),
Insecure: viper.GetBool("cli.insecure"),
},
}, nil
}
dnsConfig, baseDomain := GetDNSConfig() dnsConfig, baseDomain := GetDNSConfig()
derpConfig := GetDERPConfig() derpConfig := GetDERPConfig()
logConfig := GetLogTailConfig() logConfig := GetLogTailConfig()
randomizeClientPort := viper.GetBool("randomize_client_port") randomizeClientPort := viper.GetBool("randomize_client_port")
configuredPrefixes := viper.GetStringSlice("ip_prefixes") configuredPrefixes := viper.GetStringSlice("ip_prefixes")
parsedPrefixes := make([]netip.Prefix, 0, len(configuredPrefixes)+1) parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1)
logLevelStr := viper.GetString("log_level")
logLevel, err := zerolog.ParseLevel(logLevelStr)
if err != nil {
logLevel = zerolog.DebugLevel
}
legacyPrefixField := viper.GetString("ip_prefix")
if len(legacyPrefixField) > 0 {
log.
Warn().
Msgf(
"%s, %s",
"use of 'ip_prefix' for configuration is deprecated",
"please see 'ip_prefixes' in the shipped example.",
)
legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField)
if err != nil {
panic(fmt.Errorf("failed to parse ip_prefix: %w", err))
}
parsedPrefixes = append(parsedPrefixes, legacyPrefix)
}
for i, prefixInConfig := range configuredPrefixes { for i, prefixInConfig := range configuredPrefixes {
prefix, err := netip.ParsePrefix(prefixInConfig) prefix, err := netaddr.ParseIPPrefix(prefixInConfig)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err)) panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
} }
parsedPrefixes = append(parsedPrefixes, prefix) parsedPrefixes = append(parsedPrefixes, prefix)
} }
prefixes := make([]netip.Prefix, 0, len(parsedPrefixes)) prefixes := make([]netaddr.IPPrefix, 0, len(parsedPrefixes))
{ {
// dedup // dedup
normalizedPrefixes := make(map[string]int, len(parsedPrefixes)) normalizedPrefixes := make(map[string]int, len(parsedPrefixes))
for i, p := range parsedPrefixes { for i, p := range parsedPrefixes {
normalized, _ := netipx.RangeOfPrefix(p).Prefix() normalized, _ := p.Range().Prefix()
normalizedPrefixes[normalized.String()] = i normalizedPrefixes[normalized.String()] = i
} }
@@ -506,7 +469,7 @@ func GetHeadscaleConfig() (*Config, error) {
} }
if len(prefixes) < 1 { if len(prefixes) < 1 {
prefixes = append(prefixes, netip.MustParsePrefix("100.64.0.0/10")) prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10"))
log.Warn(). log.Warn().
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes) Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
} }
@@ -518,14 +481,12 @@ func GetHeadscaleConfig() (*Config, error) {
GRPCAddr: viper.GetString("grpc_listen_addr"), GRPCAddr: viper.GetString("grpc_listen_addr"),
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"), GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
DisableUpdateCheck: viper.GetBool("disable_check_updates"), DisableUpdateCheck: viper.GetBool("disable_check_updates"),
LogLevel: logLevel,
IPPrefixes: prefixes, IPPrefixes: prefixes,
PrivateKeyPath: AbsolutePathFromConfigPath( PrivateKeyPath: AbsolutePathFromConfigPath(
viper.GetString("private_key_path"), viper.GetString("private_key_path"),
), ),
NoisePrivateKeyPath: AbsolutePathFromConfigPath(
viper.GetString("noise.private_key_path"),
),
BaseDomain: baseDomain, BaseDomain: baseDomain,
DERP: derpConfig, DERP: derpConfig,
@@ -545,7 +506,6 @@ func GetHeadscaleConfig() (*Config, error) {
DBname: viper.GetString("db_name"), DBname: viper.GetString("db_name"),
DBuser: viper.GetString("db_user"), DBuser: viper.GetString("db_user"),
DBpass: viper.GetString("db_pass"), DBpass: viper.GetString("db_pass"),
DBssl: viper.GetString("db_ssl"),
TLS: GetTLSConfig(), TLS: GetTLSConfig(),
@@ -558,9 +518,6 @@ func GetHeadscaleConfig() (*Config, error) {
UnixSocketPermission: GetFileMode("unix_socket_permission"), UnixSocketPermission: GetFileMode("unix_socket_permission"),
OIDC: OIDCConfig{ OIDC: OIDCConfig{
OnlyStartIfOIDCIsAvailable: viper.GetBool(
"oidc.only_start_if_oidc_is_available",
),
Issuer: viper.GetString("oidc.issuer"), Issuer: viper.GetString("oidc.issuer"),
ClientID: viper.GetString("oidc.client_id"), ClientID: viper.GetString("oidc.client_id"),
ClientSecret: viper.GetString("oidc.client_secret"), ClientSecret: viper.GetString("oidc.client_secret"),
@@ -574,8 +531,6 @@ func GetHeadscaleConfig() (*Config, error) {
LogTail: logConfig, LogTail: logConfig,
RandomizeClientPort: randomizeClientPort, RandomizeClientPort: randomizeClientPort,
ACL: GetACLConfig(),
CLI: CLIConfig{ CLI: CLIConfig{
Address: viper.GetString("cli.address"), Address: viper.GetString("cli.address"),
APIKey: viper.GetString("cli.api_key"), APIKey: viper.GetString("cli.api_key"),
@@ -583,10 +538,6 @@ func GetHeadscaleConfig() (*Config, error) {
Insecure: viper.GetBool("cli.insecure"), Insecure: viper.GetBool("cli.insecure"),
}, },
Log: GetLogConfig(), ACL: GetACLConfig(),
}, nil }, nil
} }
func IsCLIConfigured() bool {
return viper.GetString("cli.address") != "" && viper.GetString("cli.api_key") != ""
}

110
db.go
View File

@@ -6,7 +6,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"time" "time"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
@@ -14,14 +13,13 @@ import (
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
const ( const (
dbVersion = "1" dbVersion = "1"
errValueNotFound = Error("not found")
errValueNotFound = Error("not found")
ErrCannotParsePrefix = Error("cannot parse prefix")
) )
// KV is a key-value store in a psql table. For future use... // KV is a key-value store in a psql table. For future use...
@@ -81,67 +79,6 @@ func (h *Headscale) initDB() error {
} }
} }
err = db.AutoMigrate(&Route{})
if err != nil {
return err
}
if db.Migrator().HasColumn(&Machine{}, "enabled_routes") {
log.Info().Msgf("Database has legacy enabled_routes column in machine, migrating...")
type MachineAux struct {
ID uint64
EnabledRoutes IPPrefixes
}
machinesAux := []MachineAux{}
err := db.Table("machines").Select("id, enabled_routes").Scan(&machinesAux).Error
if err != nil {
log.Fatal().Err(err).Msg("Error accessing db")
}
for _, machine := range machinesAux {
for _, prefix := range machine.EnabledRoutes {
if err != nil {
log.Error().
Err(err).
Str("enabled_route", prefix.String()).
Msg("Error parsing enabled_route")
continue
}
err = db.Preload("Machine").Where("machine_id = ? AND prefix = ?", machine.ID, IPPrefix(prefix)).First(&Route{}).Error
if err == nil {
log.Info().
Str("enabled_route", prefix.String()).
Msg("Route already migrated to new table, skipping")
continue
}
route := Route{
MachineID: machine.ID,
Advertised: true,
Enabled: true,
Prefix: IPPrefix(prefix),
}
if err := h.db.Create(&route).Error; err != nil {
log.Error().Err(err).Msg("Error creating route")
} else {
log.Info().
Uint64("machine_id", route.MachineID).
Str("prefix", prefix.String()).
Msg("Route migrated")
}
}
}
err = db.Migrator().DropColumn(&Machine{}, "enabled_routes")
if err != nil {
log.Error().Err(err).Msg("Error dropping enabled_routes column")
}
}
err = db.AutoMigrate(&Machine{}) err = db.AutoMigrate(&Machine{})
if err != nil { if err != nil {
return err return err
@@ -194,11 +131,6 @@ func (h *Headscale) initDB() error {
return err return err
} }
err = db.AutoMigrate(&PreAuthKeyACLTag{})
if err != nil {
return err
}
_ = db.Migrator().DropTable("shared_machines") _ = db.Migrator().DropTable("shared_machines")
err = db.AutoMigrate(&APIKey{}) err = db.AutoMigrate(&APIKey{})
@@ -289,8 +221,8 @@ func (h *Headscale) setValue(key string, value string) error {
return nil return nil
} }
func (h *Headscale) pingDB(ctx context.Context) error { func (h *Headscale) pingDB() error {
ctx, cancel := context.WithTimeout(ctx, time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() defer cancel()
db, err := h.db.DB() db, err := h.db.DB()
if err != nil { if err != nil {
@@ -316,7 +248,7 @@ func (hi *HostInfo) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), hi) return json.Unmarshal([]byte(value), hi)
default: default:
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination) return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
} }
} }
@@ -327,31 +259,7 @@ func (hi HostInfo) Value() (driver.Value, error) {
return string(bytes), err return string(bytes), err
} }
type IPPrefix netip.Prefix type IPPrefixes []netaddr.IPPrefix
func (i *IPPrefix) Scan(destination interface{}) error {
switch value := destination.(type) {
case string:
prefix, err := netip.ParsePrefix(value)
if err != nil {
return err
}
*i = IPPrefix(prefix)
return nil
default:
return fmt.Errorf("%w: unexpected data type %T", ErrCannotParsePrefix, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (i IPPrefix) Value() (driver.Value, error) {
prefixStr := netip.Prefix(i).String()
return prefixStr, nil
}
type IPPrefixes []netip.Prefix
func (i *IPPrefixes) Scan(destination interface{}) error { func (i *IPPrefixes) Scan(destination interface{}) error {
switch value := destination.(type) { switch value := destination.(type) {
@@ -362,7 +270,7 @@ func (i *IPPrefixes) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), i) return json.Unmarshal([]byte(value), i)
default: default:
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination) return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
} }
} }
@@ -384,7 +292,7 @@ func (i *StringList) Scan(destination interface{}) error {
return json.Unmarshal([]byte(value), i) return json.Unmarshal([]byte(value), i)
default: default:
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination) return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@@ -34,7 +35,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout) ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout)
defer cancel() defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr.String(), nil) req, err := http.NewRequestWithContext(ctx, "GET", addr.String(), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -49,7 +50,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/netip"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@@ -99,13 +98,10 @@ func (h *Headscale) DERPHandler(
req *http.Request, req *http.Request,
) { ) {
log.Trace().Caller().Msgf("/derp request from %v", req.RemoteAddr) log.Trace().Caller().Msgf("/derp request from %v", req.RemoteAddr)
upgrade := strings.ToLower(req.Header.Get("Upgrade")) up := strings.ToLower(req.Header.Get("Upgrade"))
if up != "websocket" && up != "derp" {
if upgrade != "websocket" && upgrade != "derp" { if up != "" {
if upgrade != "" { log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
log.Warn().
Caller().
Msg("No Upgrade header in DERP server request. If headscale is behind a reverse proxy, make sure it is configured to pass WebSockets through.")
} }
writer.Header().Set("Content-Type", "text/plain") writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusUpgradeRequired) writer.WriteHeader(http.StatusUpgradeRequired)
@@ -157,7 +153,7 @@ func (h *Headscale) DERPHandler(
if !fastStart { if !fastStart {
pubKey := h.privateKey.Public() pubKey := h.privateKey.Public()
pubKeyStr := pubKey.UntypedHexString() //nolint pubKeyStr := pubKey.UntypedHexString() // nolint
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+ fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
"Upgrade: DERP\r\n"+ "Upgrade: DERP\r\n"+
"Connection: Upgrade\r\n"+ "Connection: Upgrade\r\n"+
@@ -167,7 +163,7 @@ func (h *Headscale) DERPHandler(
pubKeyStr) pubKeyStr)
} }
h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String()) h.DERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
} }
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure // DERPProbeHandler is the endpoint that js/wasm clients hit to measure
@@ -177,7 +173,7 @@ func (h *Headscale) DERPProbeHandler(
req *http.Request, req *http.Request,
) { ) {
switch req.Method { switch req.Method {
case http.MethodHead, http.MethodGet: case "HEAD", "GET":
writer.Header().Set("Access-Control-Allow-Origin", "*") writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.WriteHeader(http.StatusOK) writer.WriteHeader(http.StatusOK)
default: default:
@@ -205,7 +201,7 @@ func (h *Headscale) DERPBootstrapDNSHandler(
) { ) {
dnsEntries := make(map[string][]net.IP) dnsEntries := make(map[string][]net.IP)
resolvCtx, cancel := context.WithTimeout(req.Context(), time.Minute) resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel() defer cancel()
var resolver net.Resolver var resolver net.Resolver
for _, region := range h.DERPMap.Regions { for _, region := range h.DERPMap.Regions {
@@ -280,8 +276,7 @@ func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
continue continue
} }
addr, _ := netip.AddrFromSlice(udpAddr.IP) res := stun.Response(txid, udpAddr.IP, uint16(udpAddr.Port))
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(udpAddr.Port)))
_, err = packetConn.WriteTo(res, udpAddr) _, err = packetConn.WriteTo(res, udpAddr)
if err != nil { if err != nil {
log.Trace().Caller().Err(err).Msgf("Issue writing to UDP") log.Trace().Caller().Err(err).Msgf("Issue writing to UDP")

56
dns.go
View File

@@ -2,14 +2,11 @@ package headscale
import ( import (
"fmt" "fmt"
"net/netip"
"net/url"
"strings" "strings"
mapset "github.com/deckarep/golang-set/v2" mapset "github.com/deckarep/golang-set/v2"
"go4.org/netipx" "inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
) )
@@ -22,10 +19,6 @@ const (
ipv6AddressLength = 128 ipv6AddressLength = 128
) )
const (
nextDNSDoHPrefix = "https://dns.nextdns.io"
)
// generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`. // generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`.
// This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS // This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS
// server (listening in 100.100.100.100 udp/53) should be used for. // server (listening in 100.100.100.100 udp/53) should be used for.
@@ -46,11 +39,11 @@ const (
// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask). // From the netmask we can find out the wildcard bits (the bits that are not set in the netmask).
// This allows us to then calculate the subnets included in the subsequent class block and generate the entries. // This allows us to then calculate the subnets included in the subsequent class block and generate the entries.
func generateMagicDNSRootDomains(ipPrefixes []netip.Prefix) []dnsname.FQDN { func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN {
fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes)) fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes))
for _, ipPrefix := range ipPrefixes { for _, ipPrefix := range ipPrefixes {
var generateDNSRoot func(netip.Prefix) []dnsname.FQDN var generateDNSRoot func(netaddr.IPPrefix) []dnsname.FQDN
switch ipPrefix.Addr().BitLen() { switch ipPrefix.IP().BitLen() {
case ipv4AddressLength: case ipv4AddressLength:
generateDNSRoot = generateIPv4DNSRootDomain generateDNSRoot = generateIPv4DNSRootDomain
@@ -61,7 +54,7 @@ func generateMagicDNSRootDomains(ipPrefixes []netip.Prefix) []dnsname.FQDN {
panic( panic(
fmt.Sprintf( fmt.Sprintf(
"unsupported IP version with address length %d", "unsupported IP version with address length %d",
ipPrefix.Addr().BitLen(), ipPrefix.IP().BitLen(),
), ),
) )
} }
@@ -72,9 +65,9 @@ func generateMagicDNSRootDomains(ipPrefixes []netip.Prefix) []dnsname.FQDN {
return fqdns return fqdns
} }
func generateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN { func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN {
// Conversion to the std lib net.IPnet, a bit easier to operate // Conversion to the std lib net.IPnet, a bit easier to operate
netRange := netipx.PrefixIPNet(ipPrefix) netRange := ipPrefix.IPNet()
maskBits, _ := netRange.Mask.Size() maskBits, _ := netRange.Mask.Size()
// lastOctet is the last IP byte covered by the mask // lastOctet is the last IP byte covered by the mask
@@ -108,11 +101,11 @@ func generateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
return fqdns return fqdns
} }
func generateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN { func generateIPv6DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN {
const nibbleLen = 4 const nibbleLen = 4
maskBits, _ := netipx.PrefixIPNet(ipPrefix).Mask.Size() maskBits, _ := ipPrefix.IPNet().Mask.Size()
expanded := ipPrefix.Addr().StringExpanded() expanded := ipPrefix.IP().StringExpanded()
nibbleStr := strings.Map(func(r rune) rune { nibbleStr := strings.Map(func(r rune) rune {
if r == ':' { if r == ':' {
return -1 return -1
@@ -158,39 +151,16 @@ func generateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
return fqdns return fqdns
} }
// If any nextdns DoH resolvers are present in the list of resolvers it will
// take metadata from the machine metadata and instruct tailscale to add it
// to the requests. This makes it possible to identify from which device the
// requests come in the NextDNS dashboard.
//
// This will produce a resolver like:
// `https://dns.nextdns.io/<nextdns-id>?device_name=node-name&device_model=linux&device_ip=100.64.0.1`
func addNextDNSMetadata(resolvers []*dnstype.Resolver, machine Machine) {
for _, resolver := range resolvers {
if strings.HasPrefix(resolver.Addr, nextDNSDoHPrefix) {
attrs := url.Values{
"device_name": []string{machine.Hostname},
"device_model": []string{machine.HostInfo.OS},
}
if len(machine.IPAddresses) > 0 {
attrs.Add("device_ip", machine.IPAddresses[0].String())
}
resolver.Addr = fmt.Sprintf("%s?%s", resolver.Addr, attrs.Encode())
}
}
}
func getMapResponseDNSConfig( func getMapResponseDNSConfig(
dnsConfigOrig *tailcfg.DNSConfig, dnsConfigOrig *tailcfg.DNSConfig,
baseDomain string, baseDomain string,
machine Machine, machine Machine,
peers Machines, peers Machines,
) *tailcfg.DNSConfig { ) *tailcfg.DNSConfig {
var dnsConfig *tailcfg.DNSConfig = dnsConfigOrig.Clone() var dnsConfig *tailcfg.DNSConfig
if dnsConfigOrig != nil && dnsConfigOrig.Proxied { // if MagicDNS is enabled if dnsConfigOrig != nil && dnsConfigOrig.Proxied { // if MagicDNS is enabled
// Only inject the Search Domain of the current namespace - shared nodes should use their full FQDN // Only inject the Search Domain of the current namespace - shared nodes should use their full FQDN
dnsConfig = dnsConfigOrig.Clone()
dnsConfig.Domains = append( dnsConfig.Domains = append(
dnsConfig.Domains, dnsConfig.Domains,
fmt.Sprintf( fmt.Sprintf(
@@ -213,7 +183,5 @@ func getMapResponseDNSConfig(
dnsConfig = dnsConfigOrig dnsConfig = dnsConfigOrig
} }
addNextDNSMetadata(dnsConfig.Resolvers, machine)
return dnsConfig return dnsConfig
} }

View File

@@ -2,16 +2,16 @@ package headscale
import ( import (
"fmt" "fmt"
"net/netip"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
) )
func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
prefixes := []netip.Prefix{ prefixes := []netaddr.IPPrefix{
netip.MustParsePrefix("100.64.0.0/10"), netaddr.MustParseIPPrefix("100.64.0.0/10"),
} }
domains := generateMagicDNSRootDomains(prefixes) domains := generateMagicDNSRootDomains(prefixes)
@@ -47,8 +47,8 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
} }
func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
prefixes := []netip.Prefix{ prefixes := []netaddr.IPPrefix{
netip.MustParsePrefix("172.16.0.0/16"), netaddr.MustParseIPPrefix("172.16.0.0/16"),
} }
domains := generateMagicDNSRootDomains(prefixes) domains := generateMagicDNSRootDomains(prefixes)
@@ -75,8 +75,8 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
// Happens when netmask is a multiple of 4 bits (sounds likely). // Happens when netmask is a multiple of 4 bits (sounds likely).
func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) { func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) {
prefixes := []netip.Prefix{ prefixes := []netaddr.IPPrefix{
netip.MustParsePrefix("fd7a:115c:a1e0::/48"), netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48"),
} }
domains := generateMagicDNSRootDomains(prefixes) domains := generateMagicDNSRootDomains(prefixes)
@@ -89,8 +89,8 @@ func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) {
} }
func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) { func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) {
prefixes := []netip.Prefix{ prefixes := []netaddr.IPPrefix{
netip.MustParsePrefix("fd7a:115c:a1e0::/50"), netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/50"),
} }
domains := generateMagicDNSRootDomains(prefixes) domains := generateMagicDNSRootDomains(prefixes)
@@ -126,7 +126,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -135,7 +134,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -144,7 +142,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -153,7 +150,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -169,7 +165,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
app.db.Save(machineInShared1) app.db.Save(machineInShared1)
@@ -186,7 +182,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NamespaceID: namespaceShared2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
app.db.Save(machineInShared2) app.db.Save(machineInShared2)
@@ -203,7 +199,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NamespaceID: namespaceShared3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
app.db.Save(machineInShared3) app.db.Save(machineInShared3)
@@ -220,7 +216,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(PreAuthKey2InShared1.ID), AuthKeyID: uint(PreAuthKey2InShared1.ID),
} }
app.db.Save(machine2InShared1) app.db.Save(machine2InShared1)
@@ -273,7 +269,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -282,7 +277,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -291,7 +285,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -300,7 +293,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -316,7 +308,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
app.db.Save(machineInShared1) app.db.Save(machineInShared1)
@@ -333,7 +325,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NamespaceID: namespaceShared2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
app.db.Save(machineInShared2) app.db.Save(machineInShared2)
@@ -350,7 +342,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NamespaceID: namespaceShared3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
app.db.Save(machineInShared3) app.db.Save(machineInShared3)
@@ -367,7 +359,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(preAuthKey2InShared1.ID), AuthKeyID: uint(preAuthKey2InShared1.ID),
} }
app.db.Save(machine2InShared1) app.db.Save(machine2InShared1)

View File

@@ -28,7 +28,6 @@ written by community members. It is _not_ verified by `headscale` developers.
- [Running headscale in a container](running-headscale-container.md) - [Running headscale in a container](running-headscale-container.md)
- [Running headscale on OpenBSD](running-headscale-openbsd.md) - [Running headscale on OpenBSD](running-headscale-openbsd.md)
- [Running headscale behind a reverse proxy](reverse-proxy.md)
## Misc ## Misc

View File

@@ -36,7 +36,7 @@ ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
or YAML. Check the [test ACLs](../tests/acls) for further information. or YAML. Check the [test ACLs](../tests/acls) for further information.
When registering the servers we will need to add the flag When registering the servers we will need to add the flag
`--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is `--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
registering the server should be allowed to do it. Since anyone can add tags to registering the server should be allowed to do it. Since anyone can add tags to
a server they can register, the check of the tags is done on headscale server a server they can register, the check of the tags is done on headscale server
and only valid tags are applied. A tag is valid if the namespace that is and only valid tags are applied. A tag is valid if the namespace that is

View File

@@ -1,19 +0,0 @@
# Connecting an Android client
## Goal
This documentation has the goal of showing how a user can use the official Android [Tailscale](https://tailscale.com) client with `headscale`.
## Installation
Install the official Tailscale Android client from the [Google Play Store](https://play.google.com/store/apps/details?id=com.tailscale.ipn) or [F-Droid](https://f-droid.org/packages/com.tailscale.ipn/).
Ensure that the installed version is at least 1.30.0, as that is the first release to support custom URLs.
## Configuring the headscale URL
After opening the app, the kebab menu icon (three dots) on the top bar on the right must be repeatedly opened and closed until the _Change server_ option appears in the menu. This is where you can enter your headscale URL.
A screen recording of this process can be seen in the `tailscale-android` PR which implemented this functionality: <https://github.com/tailscale/tailscale-android/pull/55>
After saving and restarting the app, selecting the regular _Sign in_ option (non-SSO) should open up the headscale authentication page.

View File

@@ -0,0 +1,32 @@
# Build docker from scratch
The Dockerfiles included in the repository are using the [buildx plugin](https://docs.docker.com/buildx/working-with-buildx/). This plugin is includes in docker newer than Docker-ce CLI 19.03.2. The plugin is used to be able to build different container arches. Building the Dockerfiles without buildx is not possible.
# Build native
To build the container on the native arch you can just use:
```
$ sudo docker buildx build -t headscale:custom-arch .
```
For example: This will build a amd64(x86_64) container if your hostsystem is amd64(x86_64). Or a arm64 container on a arm64 hostsystem (raspberry pi4).
# Build cross platform
To build a arm64 container on a amd64 hostsystem you could use:
```
$ sudo docker buildx build --platform linux/arm64 -t headscale:custom-arm64 .
```
**Import: Currently arm32 build are not supported as there is a problem with a library used by headscale. Hopefully this will be fixed soon.**
# Build multiple arches
To build multiple archres you could use:
```
$ sudo docker buildx create --use
$ sudo docker buildx build --platform linux/amd64,linux/arm64 .
```

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,48 +0,0 @@
# Better route management
As of today, route management in Headscale is very basic and does not allow for much flexibility, including implementing subnet HA, 4via6 or more advanced features. We also have a number of bugs (e.g., routes exposed by ephemeral nodes)
This proposal aims to improve the route management.
## Current situation
Routes advertised by the nodes are read from the Hostinfo struct. If approved from the the CLI or via autoApprovers, the route is added to the EnabledRoutes field in `Machine`.
This means that the advertised routes are not persisted in the database, as Hostinfo is always replaced. In the same way, EnabledRoutes can get out of sync with the actual routes in the node.
In case of colliding routes (i.e., subnets that are exposed from multiple nodes), we are currently just sending all of them in `PrimaryRoutes`... and hope for the best. (`PrimaryRoutes` is the field in `Node` used for subnet failover).
## Proposal
The core part is to create a new `Route` struct (and DB table), with the following fields:
```go
type Route struct {
ID uint64 `gorm:"primary_key"`
Machine *Machine
Prefix IPPrefix
Advertised bool
Enabled bool
IsPrimary bool
CreatedAt *time.Time
UpdatedAt *time.Time
DeletedAt *time.Time
}
```
- The `Advertised` field is set to true if the route is being advertised by the node. It is set to false if the route is removed. This way we can indicate if a later enabled route has stopped being advertised. A similar behaviour happens in the Tailscale.com control panel.
- The `Enabled` field is set to true if the route is enabled - via CLI or autoApprovers.
- `IsPrimary` indicates if Headscale has selected this route as the primary route for that particular subnet. This allows us to implement subnet failover. This would be fully automatic if there is more than subnet routers advertising the same network - which is the behaviour of Tailscale.com.
## Stuff to bear in mind
- We need to make sure to migrate the current `EnabledRoutes` of `Machine` into the new table.
- When a node stops sharing a subnet, I reckon we should mark it both as not `Advertised` and not `Enabled`. Users should re-enable it if the node advertises it again.
- If only one subnet router is advertising a subnet, we should mark it as primary.
- Regarding subnet failover, the current behaviour of Tailscale.com is to perform the failover after 15 seconds from the node disconnecting from their control panel. I reckon we cannot do the same currently. Our maximum granularity is the keep alive period.

View File

@@ -1,100 +0,0 @@
# Running headscale behind a reverse proxy
Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.
### WebSockets
The reverse proxy MUST be configured to support WebSockets, as it is needed for clients running Tailscale v1.30+.
WebSockets support is required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml).
### TLS
Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file.
```yaml
server_url: https://<YOUR_SERVER_NAME> # This should be the FQDN at which headscale will be served
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
tls_cert_path: ""
tls_key_path: ""
```
## nginx
The following example configuration can be used in your nginx setup, substituting values as necessary. `<IP:PORT>` should be the IP address and port where headscale is running. In most cases, this will be `http://localhost:8080`.
```Nginx
map $http_upgrade $connection_upgrade {
default keep-alive;
'websocket' upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name <YOUR_SERVER_NAME>;
ssl_certificate <PATH_TO_CERT>;
ssl_certificate_key <PATH_CERT_KEY>;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://<IP:PORT>;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
}
```
## istio/envoy
If you using [Istio](https://istio.io/) ingressgateway or [Envoy](https://www.envoyproxy.io/) as reverse proxy, there are some tips for you. If not set, you may see some debug log in proxy as below:
```log
Sending local reply with details upgrade_failed
```
### Envoy
You need add a new upgrade_type named `tailscale-control-protocol`. [see detail](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig)
### Istio
Same as envoy, we can use `EnvoyFilter` to add upgrade_type.
```yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: headscale-behind-istio-ingress
namespace: istio-system
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
patch:
operation: MERGE
value:
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
upgrade_configs:
- upgrade_type: tailscale-control-protocol
```

View File

@@ -48,17 +48,12 @@ Modify the config file to your preferences before launching Docker container.
Here are some settings that you likely want: Here are some settings that you likely want:
```yaml ```yaml
# Change to your hostname or host IP server_url: http://your-host-name:8080 # Change to your hostname or host IP
server_url: http://your-host-name:8080
# Listen to 0.0.0.0 so it's accessible outside the container # Listen to 0.0.0.0 so it's accessible outside the container
metrics_listen_addr: 0.0.0.0:9090 metrics_listen_addr: 0.0.0.0:9090
# The default /var/lib/headscale path is not writable in the container # The default /var/lib/headscale path is not writable in the container
private_key_path: /etc/headscale/private.key private_key_path: /etc/headscale/private.key
# The default /var/lib/headscale path is not writable in the container
noise:
private_key_path: /etc/headscale/noise_private.key
# The default /var/lib/headscale path is not writable in the container # The default /var/lib/headscale path is not writable in the container
db_type: sqlite3
db_path: /etc/headscale/db.sqlite db_path: /etc/headscale/db.sqlite
``` ```
@@ -68,6 +63,7 @@ db_path: /etc/headscale/db.sqlite
docker run \ docker run \
--name headscale \ --name headscale \
--detach \ --detach \
--rm \
--volume $(pwd)/config:/etc/headscale/ \ --volume $(pwd)/config:/etc/headscale/ \
--publish 127.0.0.1:8080:8080 \ --publish 127.0.0.1:8080:8080 \
--publish 127.0.0.1:9090:9090 \ --publish 127.0.0.1:9090:9090 \

View File

@@ -17,7 +17,7 @@ describing how to make `headscale` run properly in a server environment.
```shell ```shell
# Install prerequistes # Install prerequistes
# 1. go v1.19+: headscale newer than 0.17 needs go 1.19+ to compile # 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax # 2. gmake: Makefile in the headscale repo is written in GNU make syntax
pkg_add -D snap go pkg_add -D snap go
pkg_add gmake pkg_add gmake
@@ -46,7 +46,7 @@ cp headscale /usr/local/sbin
```shell ```shell
# Install prerequistes # Install prerequistes
# 1. go v1.19+: headscale newer than 0.17 needs go 1.19+ to compile # 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax # 2. gmake: Makefile in the headscale repo is written in GNU make syntax
git clone https://github.com/juanfont/headscale.git git clone https://github.com/juanfont/headscale.git

View File

@@ -29,3 +29,17 @@ headscale can also be configured to expose its web service via TLS. To configure
tls_cert_path: "" tls_cert_path: ""
tls_key_path: "" tls_key_path: ""
``` ```
### Configuring Mutual TLS Authentication (mTLS)
mTLS is a method by which an HTTPS server authenticates clients, e.g. Tailscale, using TLS certificates. This can be configured by applying one of the following values to the `tls_client_auth_mode` setting in the configuration file.
| Value | Behavior |
| ------------------- | ---------------------------------------------------------- |
| `disabled` | Disable mTLS. |
| `relaxed` (default) | A client certificate is required, but it is not verified. |
| `enforced` | Requires clients to supply a certificate that is verified. |
```yaml
tls_client_auth_mode: ""
```

14
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1659877975, "lastModified": 1653893745,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -17,16 +17,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1666869603, "lastModified": 1654847188,
"narHash": "sha256-3V53or4Vpu4+LrGfGSh3T2V8+qf5RP6nRuex9GywkwE=", "narHash": "sha256-MC+eP7XOGE1LAswOPqdcGoUqY9mEQ3ZaaxamVTbc0hM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2001e2b31c565bcdf7bc13062b8d7cfccaca05b8", "rev": "8b66e3f2ebcc644b78cec9d6f152192f4e7d322f",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-unstable", "ref": "nixos-22.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

301
flake.nix
View File

@@ -2,178 +2,167 @@
description = "headscale - Open Source Tailscale Control server"; description = "headscale - Open Source Tailscale Control server";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };
outputs = { outputs = { self, nixpkgs, flake-utils, ... }:
self, let
nixpkgs, headscaleVersion = if (self ? shortRev) then self.shortRev else "dev";
flake-utils, in
...
}: let
headscaleVersion =
if (self ? shortRev)
then self.shortRev
else "dev";
in
{ {
overlay = _: prev: let overlay = final: prev:
pkgs = nixpkgs.legacyPackages.${prev.system}; let
in rec { pkgs = nixpkgs.legacyPackages.${prev.system};
headscale = pkgs.buildGo119Module rec { in
pname = "headscale"; rec {
version = headscaleVersion; headscale =
src = pkgs.lib.cleanSource self; pkgs.buildGo118Module rec {
pname = "headscale";
version = headscaleVersion;
src = pkgs.lib.cleanSource self;
tags = ["ts2019"]; # 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-2o78hsi0B9U5NOcYXRqkBmg34p71J/R8FibXsgwEcSo=";
# Only run unit tests when testing a build ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
checkFlags = ["-short"]; };
# When updating go.mod or go.sum, a new sha will need to be calculated, golines =
# update this if you have a mismatch after doing a change to thos files. pkgs.buildGoModule rec {
vendorSha256 = "sha256-Cq0WipTQ+kGcvnfP0kjyvjyonl2OC9W7Tj0MCuB1lDU="; pname = "golines";
version = "0.9.0";
ldflags = ["-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}"]; src = pkgs.fetchFromGitHub {
}; owner = "segmentio";
repo = "golines";
rev = "v${version}";
sha256 = "sha256-BUXEg+4r9L/gqe4DhTlhN55P3jWt7ZyWFQycO6QePrw=";
};
golines = pkgs.buildGoModule rec { vendorSha256 = "sha256-sEzWUeVk5GB0H41wrp12P8sBWRjg0FHUX6ABDEEBqK8=";
pname = "golines";
version = "0.11.0";
src = pkgs.fetchFromGitHub { nativeBuildInputs = [ pkgs.installShellFiles ];
owner = "segmentio"; };
repo = "golines";
rev = "v${version}"; golangci-lint = prev.golangci-lint.override {
sha256 = "sha256-2K9KAg8iSubiTbujyFGN3yggrL+EDyeUCs9OOta/19A="; # Override https://github.com/NixOS/nixpkgs/pull/166801 which changed this
# to buildGo118Module because it does not build on Darwin.
inherit (prev) buildGoModule;
}; };
vendorSha256 = "sha256-rxYuzn4ezAxaeDhxd8qdOzt+CKYIh03A9zKNdzILq18="; # golangci-lint =
# pkgs.buildGo117Module rec {
# pname = "golangci-lint";
# version = "1.46.2";
#
# src = pkgs.fetchFromGitHub {
# owner = "golangci";
# repo = "golangci-lint";
# rev = "v${version}";
# sha256 = "sha256-7sDAwWz+qoB/ngeH35tsJ5FZUfAQvQsU6kU9rUHIHMk=";
# };
#
# vendorSha256 = "sha256-w38OKN6HPoz37utG/2QSPMai55IRDXCIIymeMe6ogIU=";
#
# nativeBuildInputs = [ pkgs.installShellFiles ];
# };
nativeBuildInputs = [pkgs.installShellFiles]; protoc-gen-grpc-gateway =
pkgs.buildGoModule rec {
pname = "grpc-gateway";
version = "2.8.0";
src = pkgs.fetchFromGitHub {
owner = "grpc-ecosystem";
repo = "grpc-gateway";
rev = "v${version}";
sha256 = "sha256-8eBBBYJ+tBjB2fgPMX/ZlbN3eeS75e8TAZYOKXs6hcg=";
};
vendorSha256 = "sha256-AW2Gn/mlZyLMwF+NpK59eiOmQrYWW/9HPjbunYc9Ij4=";
nativeBuildInputs = [ pkgs.installShellFiles ];
subPackages = [ "protoc-gen-grpc-gateway" "protoc-gen-openapiv2" ];
};
}; };
} // flake-utils.lib.eachDefaultSystem
golangci-lint = prev.golangci-lint.override { (system:
# Override https://github.com/NixOS/nixpkgs/pull/166801 which changed this let
# to buildGo118Module because it does not build on Darwin. pkgs = import nixpkgs {
inherit (prev) buildGoModule; overlays = [ self.overlay ];
}; inherit system;
# golangci-lint =
# pkgs.buildGo117Module rec {
# pname = "golangci-lint";
# version = "1.46.2";
#
# src = pkgs.fetchFromGitHub {
# owner = "golangci";
# repo = "golangci-lint";
# rev = "v${version}";
# sha256 = "sha256-7sDAwWz+qoB/ngeH35tsJ5FZUfAQvQsU6kU9rUHIHMk=";
# };
#
# vendorSha256 = "sha256-w38OKN6HPoz37utG/2QSPMai55IRDXCIIymeMe6ogIU=";
#
# nativeBuildInputs = [ pkgs.installShellFiles ];
# };
protoc-gen-grpc-gateway = pkgs.buildGoModule rec {
pname = "grpc-gateway";
version = "2.8.0";
src = pkgs.fetchFromGitHub {
owner = "grpc-ecosystem";
repo = "grpc-gateway";
rev = "v${version}";
sha256 = "sha256-8eBBBYJ+tBjB2fgPMX/ZlbN3eeS75e8TAZYOKXs6hcg=";
}; };
buildDeps = with pkgs; [ git go_1_18 gnumake ];
vendorSha256 = "sha256-AW2Gn/mlZyLMwF+NpK59eiOmQrYWW/9HPjbunYc9Ij4="; devDeps = with pkgs;
buildDeps ++ [
nativeBuildInputs = [pkgs.installShellFiles];
subPackages = ["protoc-gen-grpc-gateway" "protoc-gen-openapiv2"];
};
};
}
// flake-utils.lib.eachDefaultSystem
(system: let
pkgs = import nixpkgs {
overlays = [self.overlay];
inherit system;
};
buildDeps = with pkgs; [git go_1_19 gnumake];
devDeps = with pkgs;
buildDeps
++ [
golangci-lint
golines
nodePackages.prettier
# Protobuf dependencies
protobuf
protoc-gen-go
protoc-gen-go-grpc
protoc-gen-grpc-gateway
buf
clang-tools # clang-format
];
# Add entry to build a docker image with headscale
# caveat: only works on Linux
#
# Usage:
# nix build .#headscale-docker
# docker load < result
headscale-docker = pkgs.dockerTools.buildLayeredImage {
name = "headscale";
tag = headscaleVersion;
contents = [pkgs.headscale];
config.Entrypoint = [(pkgs.headscale + "/bin/headscale")];
};
in rec {
# `nix develop`
devShell = pkgs.mkShell {
buildInputs = devDeps;
shellHook = ''
export GOFLAGS=-tags="ts2019"
'';
};
# `nix build`
packages = with pkgs; {
inherit headscale;
inherit headscale-docker;
};
defaultPackage = pkgs.headscale;
# `nix run`
apps.headscale = flake-utils.lib.mkApp {
drv = packages.headscale;
};
defaultApp = apps.headscale;
checks = {
format =
pkgs.runCommand "check-format"
{
buildInputs = with pkgs; [
gnumake
nixpkgs-fmt
golangci-lint golangci-lint
nodePackages.prettier
golines golines
clang-tools nodePackages.prettier
# Protobuf dependencies
protobuf
protoc-gen-go
protoc-gen-go-grpc
protoc-gen-grpc-gateway
buf
clang-tools # clang-format
]; ];
} ''
${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt ${./.}
${pkgs.golangci-lint}/bin/golangci-lint run --fix --timeout 10m # Add entry to build a docker image with headscale
${pkgs.nodePackages.prettier}/bin/prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}' # caveat: only works on Linux
${pkgs.golines}/bin/golines --max-len=88 --base-formatter=gofumpt -w ${./.} #
${pkgs.clang-tools}/bin/clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i ${./.} # Usage:
''; # nix build .#headscale-docker
}; # docker load < result
}); headscale-docker = pkgs.dockerTools.buildLayeredImage {
name = "headscale";
tag = headscaleVersion;
contents = [ pkgs.headscale ];
config.Entrypoint = [ (pkgs.headscale + "/bin/headscale") ];
};
in
rec {
# `nix develop`
devShell = pkgs.mkShell { buildInputs = devDeps; };
# `nix build`
packages = with pkgs; {
inherit headscale;
inherit headscale-docker;
};
defaultPackage = pkgs.headscale;
# `nix run`
apps.headscale = flake-utils.lib.mkApp {
drv = packages.headscale;
};
defaultApp = apps.headscale;
checks = {
format = pkgs.runCommand "check-format"
{
buildInputs = with pkgs; [
gnumake
nixpkgs-fmt
golangci-lint
nodePackages.prettier
golines
clang-tools
];
} ''
${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt ${./.}
${pkgs.golangci-lint}/bin/golangci-lint run --fix --timeout 10m
${pkgs.nodePackages.prettier}/bin/prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}'
${pkgs.golines}/bin/golines --max-len=88 --base-formatter=gofumpt -w ${./.}
${pkgs.clang-tools}/bin/clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i ${./.}
'';
};
});
} }

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.27.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/apikey.proto // source: headscale/v1/apikey.proto

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.27.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/device.proto // source: headscale/v1/device.proto

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.27.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/headscale.proto // source: headscale/v1/headscale.proto
@@ -36,7 +36,7 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76,
0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69,
0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0x81, 0x18, 0x0a, 0x10, 0x48, 0x65, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xb1, 0x16, 0x0a, 0x10, 0x48, 0x65,
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x77, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x77,
0x0a, 0x0c, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x21,
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65,
@@ -51,9 +51,9 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x3a, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x22,
0x01, 0x2a, 0x22, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x96, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x63, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x96, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65,
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
@@ -85,17 +85,17 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75,
0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70,
0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x12, 0x87, 0x01, 0x0a, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x3a, 0x01, 0x2a, 0x12, 0x87, 0x01, 0x0a,
0x10, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x10, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
0x79, 0x12, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72,
0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x3a, 0x01, 0x2a, 0x22, 0x19, 0x2f, 0x61, 0x70, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x22, 0x19, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76,
0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2f, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70,
0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x7a, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x69, 0x72, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x7a, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72,
0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65,
0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
@@ -110,8 +110,8 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x73, 0x74, 0x1a, 0x28, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x31, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3,
0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x65,
0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x75, 0x62, 0x75, 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x75,
0x0a, 0x0a, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x1f, 0x2e, 0x68, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x1f, 0x2e, 0x68,
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e,
@@ -124,9 +124,9 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d,
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65,
0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x82, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x26, 0x3a, 0x01, 0x2a, 0x22, 0x21, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x22, 0x21, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d,
0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f,
0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x74, 0x61, 0x67, 0x73, 0x12, 0x80, 0x01, 0x0a, 0x0f, 0x69, 0x64, 0x7d, 0x2f, 0x74, 0x61, 0x67, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x80, 0x01, 0x0a, 0x0f,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12,
0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52,
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
@@ -175,37 +175,24 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2e, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2e,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x22, 0x26, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x22, 0x26, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x64, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x8b,
0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75,
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x74, 0x65, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, 0x65, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74,
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69,
0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x75, 0x74, 0x65, 0x73, 0x12, 0x7c, 0x0a, 0x0b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x12, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x97, 0x01, 0x0a,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x13, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f,
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x75, 0x74, 0x65, 0x73, 0x12, 0x28, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x22, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29,
0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x65, 0x6e, 0x61, 0x62, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e,
0x6c, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65,
0x75, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75,
0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0x82, 0xd3, 0xe4, 0x93,
0x02, 0x23, 0x22, 0x21, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74,
0x65, 0x73, 0x2f, 0x7b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x69,
0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x8e, 0x01, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x25, 0x2e, 0x68, 0x65, 0x61,
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02,
0x25, 0x12, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x25, 0x22, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f,
0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
@@ -213,16 +200,16 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41,
0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x22, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61,
0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x69, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x3a, 0x01, 0x2a, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x69,
0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70,
0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65,
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72,
0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x3a, 0x01,
0x65, 0x12, 0x6a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x2a, 0x12, 0x6a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73,
0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
@@ -235,54 +222,50 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
} }
var file_headscale_v1_headscale_proto_goTypes = []interface{}{ var file_headscale_v1_headscale_proto_goTypes = []interface{}{
(*GetNamespaceRequest)(nil), // 0: headscale.v1.GetNamespaceRequest (*GetNamespaceRequest)(nil), // 0: headscale.v1.GetNamespaceRequest
(*CreateNamespaceRequest)(nil), // 1: headscale.v1.CreateNamespaceRequest (*CreateNamespaceRequest)(nil), // 1: headscale.v1.CreateNamespaceRequest
(*RenameNamespaceRequest)(nil), // 2: headscale.v1.RenameNamespaceRequest (*RenameNamespaceRequest)(nil), // 2: headscale.v1.RenameNamespaceRequest
(*DeleteNamespaceRequest)(nil), // 3: headscale.v1.DeleteNamespaceRequest (*DeleteNamespaceRequest)(nil), // 3: headscale.v1.DeleteNamespaceRequest
(*ListNamespacesRequest)(nil), // 4: headscale.v1.ListNamespacesRequest (*ListNamespacesRequest)(nil), // 4: headscale.v1.ListNamespacesRequest
(*CreatePreAuthKeyRequest)(nil), // 5: headscale.v1.CreatePreAuthKeyRequest (*CreatePreAuthKeyRequest)(nil), // 5: headscale.v1.CreatePreAuthKeyRequest
(*ExpirePreAuthKeyRequest)(nil), // 6: headscale.v1.ExpirePreAuthKeyRequest (*ExpirePreAuthKeyRequest)(nil), // 6: headscale.v1.ExpirePreAuthKeyRequest
(*ListPreAuthKeysRequest)(nil), // 7: headscale.v1.ListPreAuthKeysRequest (*ListPreAuthKeysRequest)(nil), // 7: headscale.v1.ListPreAuthKeysRequest
(*DebugCreateMachineRequest)(nil), // 8: headscale.v1.DebugCreateMachineRequest (*DebugCreateMachineRequest)(nil), // 8: headscale.v1.DebugCreateMachineRequest
(*GetMachineRequest)(nil), // 9: headscale.v1.GetMachineRequest (*GetMachineRequest)(nil), // 9: headscale.v1.GetMachineRequest
(*SetTagsRequest)(nil), // 10: headscale.v1.SetTagsRequest (*SetTagsRequest)(nil), // 10: headscale.v1.SetTagsRequest
(*RegisterMachineRequest)(nil), // 11: headscale.v1.RegisterMachineRequest (*RegisterMachineRequest)(nil), // 11: headscale.v1.RegisterMachineRequest
(*DeleteMachineRequest)(nil), // 12: headscale.v1.DeleteMachineRequest (*DeleteMachineRequest)(nil), // 12: headscale.v1.DeleteMachineRequest
(*ExpireMachineRequest)(nil), // 13: headscale.v1.ExpireMachineRequest (*ExpireMachineRequest)(nil), // 13: headscale.v1.ExpireMachineRequest
(*RenameMachineRequest)(nil), // 14: headscale.v1.RenameMachineRequest (*RenameMachineRequest)(nil), // 14: headscale.v1.RenameMachineRequest
(*ListMachinesRequest)(nil), // 15: headscale.v1.ListMachinesRequest (*ListMachinesRequest)(nil), // 15: headscale.v1.ListMachinesRequest
(*MoveMachineRequest)(nil), // 16: headscale.v1.MoveMachineRequest (*MoveMachineRequest)(nil), // 16: headscale.v1.MoveMachineRequest
(*GetRoutesRequest)(nil), // 17: headscale.v1.GetRoutesRequest (*GetMachineRouteRequest)(nil), // 17: headscale.v1.GetMachineRouteRequest
(*EnableRouteRequest)(nil), // 18: headscale.v1.EnableRouteRequest (*EnableMachineRoutesRequest)(nil), // 18: headscale.v1.EnableMachineRoutesRequest
(*DisableRouteRequest)(nil), // 19: headscale.v1.DisableRouteRequest (*CreateApiKeyRequest)(nil), // 19: headscale.v1.CreateApiKeyRequest
(*GetMachineRoutesRequest)(nil), // 20: headscale.v1.GetMachineRoutesRequest (*ExpireApiKeyRequest)(nil), // 20: headscale.v1.ExpireApiKeyRequest
(*CreateApiKeyRequest)(nil), // 21: headscale.v1.CreateApiKeyRequest (*ListApiKeysRequest)(nil), // 21: headscale.v1.ListApiKeysRequest
(*ExpireApiKeyRequest)(nil), // 22: headscale.v1.ExpireApiKeyRequest (*GetNamespaceResponse)(nil), // 22: headscale.v1.GetNamespaceResponse
(*ListApiKeysRequest)(nil), // 23: headscale.v1.ListApiKeysRequest (*CreateNamespaceResponse)(nil), // 23: headscale.v1.CreateNamespaceResponse
(*GetNamespaceResponse)(nil), // 24: headscale.v1.GetNamespaceResponse (*RenameNamespaceResponse)(nil), // 24: headscale.v1.RenameNamespaceResponse
(*CreateNamespaceResponse)(nil), // 25: headscale.v1.CreateNamespaceResponse (*DeleteNamespaceResponse)(nil), // 25: headscale.v1.DeleteNamespaceResponse
(*RenameNamespaceResponse)(nil), // 26: headscale.v1.RenameNamespaceResponse (*ListNamespacesResponse)(nil), // 26: headscale.v1.ListNamespacesResponse
(*DeleteNamespaceResponse)(nil), // 27: headscale.v1.DeleteNamespaceResponse (*CreatePreAuthKeyResponse)(nil), // 27: headscale.v1.CreatePreAuthKeyResponse
(*ListNamespacesResponse)(nil), // 28: headscale.v1.ListNamespacesResponse (*ExpirePreAuthKeyResponse)(nil), // 28: headscale.v1.ExpirePreAuthKeyResponse
(*CreatePreAuthKeyResponse)(nil), // 29: headscale.v1.CreatePreAuthKeyResponse (*ListPreAuthKeysResponse)(nil), // 29: headscale.v1.ListPreAuthKeysResponse
(*ExpirePreAuthKeyResponse)(nil), // 30: headscale.v1.ExpirePreAuthKeyResponse (*DebugCreateMachineResponse)(nil), // 30: headscale.v1.DebugCreateMachineResponse
(*ListPreAuthKeysResponse)(nil), // 31: headscale.v1.ListPreAuthKeysResponse (*GetMachineResponse)(nil), // 31: headscale.v1.GetMachineResponse
(*DebugCreateMachineResponse)(nil), // 32: headscale.v1.DebugCreateMachineResponse (*SetTagsResponse)(nil), // 32: headscale.v1.SetTagsResponse
(*GetMachineResponse)(nil), // 33: headscale.v1.GetMachineResponse (*RegisterMachineResponse)(nil), // 33: headscale.v1.RegisterMachineResponse
(*SetTagsResponse)(nil), // 34: headscale.v1.SetTagsResponse (*DeleteMachineResponse)(nil), // 34: headscale.v1.DeleteMachineResponse
(*RegisterMachineResponse)(nil), // 35: headscale.v1.RegisterMachineResponse (*ExpireMachineResponse)(nil), // 35: headscale.v1.ExpireMachineResponse
(*DeleteMachineResponse)(nil), // 36: headscale.v1.DeleteMachineResponse (*RenameMachineResponse)(nil), // 36: headscale.v1.RenameMachineResponse
(*ExpireMachineResponse)(nil), // 37: headscale.v1.ExpireMachineResponse (*ListMachinesResponse)(nil), // 37: headscale.v1.ListMachinesResponse
(*RenameMachineResponse)(nil), // 38: headscale.v1.RenameMachineResponse (*MoveMachineResponse)(nil), // 38: headscale.v1.MoveMachineResponse
(*ListMachinesResponse)(nil), // 39: headscale.v1.ListMachinesResponse (*GetMachineRouteResponse)(nil), // 39: headscale.v1.GetMachineRouteResponse
(*MoveMachineResponse)(nil), // 40: headscale.v1.MoveMachineResponse (*EnableMachineRoutesResponse)(nil), // 40: headscale.v1.EnableMachineRoutesResponse
(*GetRoutesResponse)(nil), // 41: headscale.v1.GetRoutesResponse (*CreateApiKeyResponse)(nil), // 41: headscale.v1.CreateApiKeyResponse
(*EnableRouteResponse)(nil), // 42: headscale.v1.EnableRouteResponse (*ExpireApiKeyResponse)(nil), // 42: headscale.v1.ExpireApiKeyResponse
(*DisableRouteResponse)(nil), // 43: headscale.v1.DisableRouteResponse (*ListApiKeysResponse)(nil), // 43: headscale.v1.ListApiKeysResponse
(*GetMachineRoutesResponse)(nil), // 44: headscale.v1.GetMachineRoutesResponse
(*CreateApiKeyResponse)(nil), // 45: headscale.v1.CreateApiKeyResponse
(*ExpireApiKeyResponse)(nil), // 46: headscale.v1.ExpireApiKeyResponse
(*ListApiKeysResponse)(nil), // 47: headscale.v1.ListApiKeysResponse
} }
var file_headscale_v1_headscale_proto_depIdxs = []int32{ var file_headscale_v1_headscale_proto_depIdxs = []int32{
0, // 0: headscale.v1.HeadscaleService.GetNamespace:input_type -> headscale.v1.GetNamespaceRequest 0, // 0: headscale.v1.HeadscaleService.GetNamespace:input_type -> headscale.v1.GetNamespaceRequest
@@ -302,39 +285,35 @@ var file_headscale_v1_headscale_proto_depIdxs = []int32{
14, // 14: headscale.v1.HeadscaleService.RenameMachine:input_type -> headscale.v1.RenameMachineRequest 14, // 14: headscale.v1.HeadscaleService.RenameMachine:input_type -> headscale.v1.RenameMachineRequest
15, // 15: headscale.v1.HeadscaleService.ListMachines:input_type -> headscale.v1.ListMachinesRequest 15, // 15: headscale.v1.HeadscaleService.ListMachines:input_type -> headscale.v1.ListMachinesRequest
16, // 16: headscale.v1.HeadscaleService.MoveMachine:input_type -> headscale.v1.MoveMachineRequest 16, // 16: headscale.v1.HeadscaleService.MoveMachine:input_type -> headscale.v1.MoveMachineRequest
17, // 17: headscale.v1.HeadscaleService.GetRoutes:input_type -> headscale.v1.GetRoutesRequest 17, // 17: headscale.v1.HeadscaleService.GetMachineRoute:input_type -> headscale.v1.GetMachineRouteRequest
18, // 18: headscale.v1.HeadscaleService.EnableRoute:input_type -> headscale.v1.EnableRouteRequest 18, // 18: headscale.v1.HeadscaleService.EnableMachineRoutes:input_type -> headscale.v1.EnableMachineRoutesRequest
19, // 19: headscale.v1.HeadscaleService.DisableRoute:input_type -> headscale.v1.DisableRouteRequest 19, // 19: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest
20, // 20: headscale.v1.HeadscaleService.GetMachineRoutes:input_type -> headscale.v1.GetMachineRoutesRequest 20, // 20: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest
21, // 21: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest 21, // 21: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest
22, // 22: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest 22, // 22: headscale.v1.HeadscaleService.GetNamespace:output_type -> headscale.v1.GetNamespaceResponse
23, // 23: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest 23, // 23: headscale.v1.HeadscaleService.CreateNamespace:output_type -> headscale.v1.CreateNamespaceResponse
24, // 24: headscale.v1.HeadscaleService.GetNamespace:output_type -> headscale.v1.GetNamespaceResponse 24, // 24: headscale.v1.HeadscaleService.RenameNamespace:output_type -> headscale.v1.RenameNamespaceResponse
25, // 25: headscale.v1.HeadscaleService.CreateNamespace:output_type -> headscale.v1.CreateNamespaceResponse 25, // 25: headscale.v1.HeadscaleService.DeleteNamespace:output_type -> headscale.v1.DeleteNamespaceResponse
26, // 26: headscale.v1.HeadscaleService.RenameNamespace:output_type -> headscale.v1.RenameNamespaceResponse 26, // 26: headscale.v1.HeadscaleService.ListNamespaces:output_type -> headscale.v1.ListNamespacesResponse
27, // 27: headscale.v1.HeadscaleService.DeleteNamespace:output_type -> headscale.v1.DeleteNamespaceResponse 27, // 27: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
28, // 28: headscale.v1.HeadscaleService.ListNamespaces:output_type -> headscale.v1.ListNamespacesResponse 28, // 28: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
29, // 29: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse 29, // 29: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
30, // 30: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse 30, // 30: headscale.v1.HeadscaleService.DebugCreateMachine:output_type -> headscale.v1.DebugCreateMachineResponse
31, // 31: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse 31, // 31: headscale.v1.HeadscaleService.GetMachine:output_type -> headscale.v1.GetMachineResponse
32, // 32: headscale.v1.HeadscaleService.DebugCreateMachine:output_type -> headscale.v1.DebugCreateMachineResponse 32, // 32: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse
33, // 33: headscale.v1.HeadscaleService.GetMachine:output_type -> headscale.v1.GetMachineResponse 33, // 33: headscale.v1.HeadscaleService.RegisterMachine:output_type -> headscale.v1.RegisterMachineResponse
34, // 34: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse 34, // 34: headscale.v1.HeadscaleService.DeleteMachine:output_type -> headscale.v1.DeleteMachineResponse
35, // 35: headscale.v1.HeadscaleService.RegisterMachine:output_type -> headscale.v1.RegisterMachineResponse 35, // 35: headscale.v1.HeadscaleService.ExpireMachine:output_type -> headscale.v1.ExpireMachineResponse
36, // 36: headscale.v1.HeadscaleService.DeleteMachine:output_type -> headscale.v1.DeleteMachineResponse 36, // 36: headscale.v1.HeadscaleService.RenameMachine:output_type -> headscale.v1.RenameMachineResponse
37, // 37: headscale.v1.HeadscaleService.ExpireMachine:output_type -> headscale.v1.ExpireMachineResponse 37, // 37: headscale.v1.HeadscaleService.ListMachines:output_type -> headscale.v1.ListMachinesResponse
38, // 38: headscale.v1.HeadscaleService.RenameMachine:output_type -> headscale.v1.RenameMachineResponse 38, // 38: headscale.v1.HeadscaleService.MoveMachine:output_type -> headscale.v1.MoveMachineResponse
39, // 39: headscale.v1.HeadscaleService.ListMachines:output_type -> headscale.v1.ListMachinesResponse 39, // 39: headscale.v1.HeadscaleService.GetMachineRoute:output_type -> headscale.v1.GetMachineRouteResponse
40, // 40: headscale.v1.HeadscaleService.MoveMachine:output_type -> headscale.v1.MoveMachineResponse 40, // 40: headscale.v1.HeadscaleService.EnableMachineRoutes:output_type -> headscale.v1.EnableMachineRoutesResponse
41, // 41: headscale.v1.HeadscaleService.GetRoutes:output_type -> headscale.v1.GetRoutesResponse 41, // 41: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse
42, // 42: headscale.v1.HeadscaleService.EnableRoute:output_type -> headscale.v1.EnableRouteResponse 42, // 42: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
43, // 43: headscale.v1.HeadscaleService.DisableRoute:output_type -> headscale.v1.DisableRouteResponse 43, // 43: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
44, // 44: headscale.v1.HeadscaleService.GetMachineRoutes:output_type -> headscale.v1.GetMachineRoutesResponse 22, // [22:44] is the sub-list for method output_type
45, // 45: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse 0, // [0:22] is the sub-list for method input_type
46, // 46: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
47, // 47: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
24, // [24:48] is the sub-list for method output_type
0, // [0:24] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name 0, // [0:0] is the sub-list for field type_name

View File

@@ -835,130 +835,8 @@ func local_request_HeadscaleService_MoveMachine_0(ctx context.Context, marshaler
} }
func request_HeadscaleService_GetRoutes_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { func request_HeadscaleService_GetMachineRoute_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetRoutesRequest var protoReq GetMachineRouteRequest
var metadata runtime.ServerMetadata
msg, err := client.GetRoutes(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_HeadscaleService_GetRoutes_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetRoutesRequest
var metadata runtime.ServerMetadata
msg, err := server.GetRoutes(ctx, &protoReq)
return msg, metadata, err
}
func request_HeadscaleService_EnableRoute_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq EnableRouteRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["route_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "route_id")
}
protoReq.RouteId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "route_id", err)
}
msg, err := client.EnableRoute(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_HeadscaleService_EnableRoute_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq EnableRouteRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["route_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "route_id")
}
protoReq.RouteId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "route_id", err)
}
msg, err := server.EnableRoute(ctx, &protoReq)
return msg, metadata, err
}
func request_HeadscaleService_DisableRoute_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq DisableRouteRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["route_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "route_id")
}
protoReq.RouteId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "route_id", err)
}
msg, err := client.DisableRoute(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_HeadscaleService_DisableRoute_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq DisableRouteRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["route_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "route_id")
}
protoReq.RouteId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "route_id", err)
}
msg, err := server.DisableRoute(ctx, &protoReq)
return msg, metadata, err
}
func request_HeadscaleService_GetMachineRoutes_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetMachineRoutesRequest
var metadata runtime.ServerMetadata var metadata runtime.ServerMetadata
var ( var (
@@ -978,13 +856,13 @@ func request_HeadscaleService_GetMachineRoutes_0(ctx context.Context, marshaler
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err) return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
} }
msg, err := client.GetMachineRoutes(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) msg, err := client.GetMachineRoute(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err return msg, metadata, err
} }
func local_request_HeadscaleService_GetMachineRoutes_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { func local_request_HeadscaleService_GetMachineRoute_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetMachineRoutesRequest var protoReq GetMachineRouteRequest
var metadata runtime.ServerMetadata var metadata runtime.ServerMetadata
var ( var (
@@ -1004,7 +882,77 @@ func local_request_HeadscaleService_GetMachineRoutes_0(ctx context.Context, mars
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err) return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
} }
msg, err := server.GetMachineRoutes(ctx, &protoReq) msg, err := server.GetMachineRoute(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_HeadscaleService_EnableMachineRoutes_0 = &utilities.DoubleArray{Encoding: map[string]int{"machine_id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
func request_HeadscaleService_EnableMachineRoutes_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq EnableMachineRoutesRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["machine_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
}
protoReq.MachineId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_HeadscaleService_EnableMachineRoutes_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.EnableMachineRoutes(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_HeadscaleService_EnableMachineRoutes_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq EnableMachineRoutesRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["machine_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
}
protoReq.MachineId, err = runtime.Uint64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_HeadscaleService_EnableMachineRoutes_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.EnableMachineRoutes(ctx, &protoReq)
return msg, metadata, err return msg, metadata, err
} }
@@ -1492,18 +1440,18 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser
}) })
mux.Handle("GET", pattern_HeadscaleService_GetRoutes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("GET", pattern_HeadscaleService_GetMachineRoute_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
var stream runtime.ServerTransportStream var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/GetRoutes", runtime.WithHTTPPathPattern("/api/v1/routes")) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/GetMachineRoute", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/routes"))
if err != nil { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return return
} }
resp, md, err := local_request_HeadscaleService_GetRoutes_0(rctx, inboundMarshaler, server, req, pathParams) resp, md, err := local_request_HeadscaleService_GetMachineRoute_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md) ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil { if err != nil {
@@ -1511,22 +1459,22 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser
return return
} }
forward_HeadscaleService_GetRoutes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_HeadscaleService_GetMachineRoute_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle("POST", pattern_HeadscaleService_EnableRoute_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("POST", pattern_HeadscaleService_EnableMachineRoutes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
var stream runtime.ServerTransportStream var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/EnableRoute", runtime.WithHTTPPathPattern("/api/v1/routes/{route_id}/enable")) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/EnableMachineRoutes", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/routes"))
if err != nil { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return return
} }
resp, md, err := local_request_HeadscaleService_EnableRoute_0(rctx, inboundMarshaler, server, req, pathParams) resp, md, err := local_request_HeadscaleService_EnableMachineRoutes_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md) ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil { if err != nil {
@@ -1534,53 +1482,7 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser
return return
} }
forward_HeadscaleService_EnableRoute_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_HeadscaleService_EnableMachineRoutes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_HeadscaleService_DisableRoute_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/DisableRoute", runtime.WithHTTPPathPattern("/api/v1/routes/{route_id}/disable"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_HeadscaleService_DisableRoute_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_DisableRoute_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_HeadscaleService_GetMachineRoutes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/GetMachineRoutes", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/routes"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_HeadscaleService_GetMachineRoutes_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_GetMachineRoutes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
@@ -2034,83 +1936,43 @@ func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *runtime.Ser
}) })
mux.Handle("GET", pattern_HeadscaleService_GetRoutes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("GET", pattern_HeadscaleService_GetMachineRoute_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/GetRoutes", runtime.WithHTTPPathPattern("/api/v1/routes")) rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/GetMachineRoute", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/routes"))
if err != nil { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return return
} }
resp, md, err := request_HeadscaleService_GetRoutes_0(rctx, inboundMarshaler, client, req, pathParams) resp, md, err := request_HeadscaleService_GetMachineRoute_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md) ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return return
} }
forward_HeadscaleService_GetRoutes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_HeadscaleService_GetMachineRoute_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle("POST", pattern_HeadscaleService_EnableRoute_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("POST", pattern_HeadscaleService_EnableMachineRoutes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/EnableRoute", runtime.WithHTTPPathPattern("/api/v1/routes/{route_id}/enable")) rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/EnableMachineRoutes", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/routes"))
if err != nil { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return return
} }
resp, md, err := request_HeadscaleService_EnableRoute_0(rctx, inboundMarshaler, client, req, pathParams) resp, md, err := request_HeadscaleService_EnableMachineRoutes_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md) ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return return
} }
forward_HeadscaleService_EnableRoute_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_HeadscaleService_EnableMachineRoutes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_HeadscaleService_DisableRoute_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/DisableRoute", runtime.WithHTTPPathPattern("/api/v1/routes/{route_id}/disable"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_HeadscaleService_DisableRoute_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_DisableRoute_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_HeadscaleService_GetMachineRoutes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/GetMachineRoutes", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/routes"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_HeadscaleService_GetMachineRoutes_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_HeadscaleService_GetMachineRoutes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
@@ -2212,13 +2074,9 @@ var (
pattern_HeadscaleService_MoveMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "namespace"}, "")) pattern_HeadscaleService_MoveMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "namespace"}, ""))
pattern_HeadscaleService_GetRoutes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "routes"}, "")) pattern_HeadscaleService_GetMachineRoute_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "routes"}, ""))
pattern_HeadscaleService_EnableRoute_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "routes", "route_id", "enable"}, "")) pattern_HeadscaleService_EnableMachineRoutes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "routes"}, ""))
pattern_HeadscaleService_DisableRoute_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "routes", "route_id", "disable"}, ""))
pattern_HeadscaleService_GetMachineRoutes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "routes"}, ""))
pattern_HeadscaleService_CreateApiKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "apikey"}, "")) pattern_HeadscaleService_CreateApiKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "apikey"}, ""))
@@ -2262,13 +2120,9 @@ var (
forward_HeadscaleService_MoveMachine_0 = runtime.ForwardResponseMessage forward_HeadscaleService_MoveMachine_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_GetRoutes_0 = runtime.ForwardResponseMessage forward_HeadscaleService_GetMachineRoute_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_EnableRoute_0 = runtime.ForwardResponseMessage forward_HeadscaleService_EnableMachineRoutes_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_DisableRoute_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_GetMachineRoutes_0 = runtime.ForwardResponseMessage
forward_HeadscaleService_CreateApiKey_0 = runtime.ForwardResponseMessage forward_HeadscaleService_CreateApiKey_0 = runtime.ForwardResponseMessage

View File

@@ -1,8 +1,4 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc (unknown)
// source: headscale/v1/headscale.proto
package v1 package v1
@@ -43,10 +39,8 @@ type HeadscaleServiceClient interface {
ListMachines(ctx context.Context, in *ListMachinesRequest, opts ...grpc.CallOption) (*ListMachinesResponse, error) ListMachines(ctx context.Context, in *ListMachinesRequest, opts ...grpc.CallOption) (*ListMachinesResponse, error)
MoveMachine(ctx context.Context, in *MoveMachineRequest, opts ...grpc.CallOption) (*MoveMachineResponse, error) MoveMachine(ctx context.Context, in *MoveMachineRequest, opts ...grpc.CallOption) (*MoveMachineResponse, error)
// --- Route start --- // --- Route start ---
GetRoutes(ctx context.Context, in *GetRoutesRequest, opts ...grpc.CallOption) (*GetRoutesResponse, error) GetMachineRoute(ctx context.Context, in *GetMachineRouteRequest, opts ...grpc.CallOption) (*GetMachineRouteResponse, error)
EnableRoute(ctx context.Context, in *EnableRouteRequest, opts ...grpc.CallOption) (*EnableRouteResponse, error) EnableMachineRoutes(ctx context.Context, in *EnableMachineRoutesRequest, opts ...grpc.CallOption) (*EnableMachineRoutesResponse, error)
DisableRoute(ctx context.Context, in *DisableRouteRequest, opts ...grpc.CallOption) (*DisableRouteResponse, error)
GetMachineRoutes(ctx context.Context, in *GetMachineRoutesRequest, opts ...grpc.CallOption) (*GetMachineRoutesResponse, error)
// --- ApiKeys start --- // --- ApiKeys start ---
CreateApiKey(ctx context.Context, in *CreateApiKeyRequest, opts ...grpc.CallOption) (*CreateApiKeyResponse, error) CreateApiKey(ctx context.Context, in *CreateApiKeyRequest, opts ...grpc.CallOption) (*CreateApiKeyResponse, error)
ExpireApiKey(ctx context.Context, in *ExpireApiKeyRequest, opts ...grpc.CallOption) (*ExpireApiKeyResponse, error) ExpireApiKey(ctx context.Context, in *ExpireApiKeyRequest, opts ...grpc.CallOption) (*ExpireApiKeyResponse, error)
@@ -214,36 +208,18 @@ func (c *headscaleServiceClient) MoveMachine(ctx context.Context, in *MoveMachin
return out, nil return out, nil
} }
func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesRequest, opts ...grpc.CallOption) (*GetRoutesResponse, error) { func (c *headscaleServiceClient) GetMachineRoute(ctx context.Context, in *GetMachineRouteRequest, opts ...grpc.CallOption) (*GetMachineRouteResponse, error) {
out := new(GetRoutesResponse) out := new(GetMachineRouteResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetRoutes", in, out, opts...) err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetMachineRoute", in, out, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return out, nil return out, nil
} }
func (c *headscaleServiceClient) EnableRoute(ctx context.Context, in *EnableRouteRequest, opts ...grpc.CallOption) (*EnableRouteResponse, error) { func (c *headscaleServiceClient) EnableMachineRoutes(ctx context.Context, in *EnableMachineRoutesRequest, opts ...grpc.CallOption) (*EnableMachineRoutesResponse, error) {
out := new(EnableRouteResponse) out := new(EnableMachineRoutesResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/EnableRoute", in, out, opts...) err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/EnableMachineRoutes", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) DisableRoute(ctx context.Context, in *DisableRouteRequest, opts ...grpc.CallOption) (*DisableRouteResponse, error) {
out := new(DisableRouteResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DisableRoute", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) GetMachineRoutes(ctx context.Context, in *GetMachineRoutesRequest, opts ...grpc.CallOption) (*GetMachineRoutesResponse, error) {
out := new(GetMachineRoutesResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetMachineRoutes", in, out, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -302,10 +278,8 @@ type HeadscaleServiceServer interface {
ListMachines(context.Context, *ListMachinesRequest) (*ListMachinesResponse, error) ListMachines(context.Context, *ListMachinesRequest) (*ListMachinesResponse, error)
MoveMachine(context.Context, *MoveMachineRequest) (*MoveMachineResponse, error) MoveMachine(context.Context, *MoveMachineRequest) (*MoveMachineResponse, error)
// --- Route start --- // --- Route start ---
GetRoutes(context.Context, *GetRoutesRequest) (*GetRoutesResponse, error) GetMachineRoute(context.Context, *GetMachineRouteRequest) (*GetMachineRouteResponse, error)
EnableRoute(context.Context, *EnableRouteRequest) (*EnableRouteResponse, error) EnableMachineRoutes(context.Context, *EnableMachineRoutesRequest) (*EnableMachineRoutesResponse, error)
DisableRoute(context.Context, *DisableRouteRequest) (*DisableRouteResponse, error)
GetMachineRoutes(context.Context, *GetMachineRoutesRequest) (*GetMachineRoutesResponse, error)
// --- ApiKeys start --- // --- ApiKeys start ---
CreateApiKey(context.Context, *CreateApiKeyRequest) (*CreateApiKeyResponse, error) CreateApiKey(context.Context, *CreateApiKeyRequest) (*CreateApiKeyResponse, error)
ExpireApiKey(context.Context, *ExpireApiKeyRequest) (*ExpireApiKeyResponse, error) ExpireApiKey(context.Context, *ExpireApiKeyRequest) (*ExpireApiKeyResponse, error)
@@ -368,17 +342,11 @@ func (UnimplementedHeadscaleServiceServer) ListMachines(context.Context, *ListMa
func (UnimplementedHeadscaleServiceServer) MoveMachine(context.Context, *MoveMachineRequest) (*MoveMachineResponse, error) { func (UnimplementedHeadscaleServiceServer) MoveMachine(context.Context, *MoveMachineRequest) (*MoveMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method MoveMachine not implemented") return nil, status.Errorf(codes.Unimplemented, "method MoveMachine not implemented")
} }
func (UnimplementedHeadscaleServiceServer) GetRoutes(context.Context, *GetRoutesRequest) (*GetRoutesResponse, error) { func (UnimplementedHeadscaleServiceServer) GetMachineRoute(context.Context, *GetMachineRouteRequest) (*GetMachineRouteResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRoutes not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetMachineRoute not implemented")
} }
func (UnimplementedHeadscaleServiceServer) EnableRoute(context.Context, *EnableRouteRequest) (*EnableRouteResponse, error) { func (UnimplementedHeadscaleServiceServer) EnableMachineRoutes(context.Context, *EnableMachineRoutesRequest) (*EnableMachineRoutesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method EnableRoute not implemented") return nil, status.Errorf(codes.Unimplemented, "method EnableMachineRoutes not implemented")
}
func (UnimplementedHeadscaleServiceServer) DisableRoute(context.Context, *DisableRouteRequest) (*DisableRouteResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DisableRoute not implemented")
}
func (UnimplementedHeadscaleServiceServer) GetMachineRoutes(context.Context, *GetMachineRoutesRequest) (*GetMachineRoutesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMachineRoutes not implemented")
} }
func (UnimplementedHeadscaleServiceServer) CreateApiKey(context.Context, *CreateApiKeyRequest) (*CreateApiKeyResponse, error) { func (UnimplementedHeadscaleServiceServer) CreateApiKey(context.Context, *CreateApiKeyRequest) (*CreateApiKeyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateApiKey not implemented") return nil, status.Errorf(codes.Unimplemented, "method CreateApiKey not implemented")
@@ -708,74 +676,38 @@ func _HeadscaleService_MoveMachine_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _HeadscaleService_GetRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _HeadscaleService_GetMachineRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetRoutesRequest) in := new(GetMachineRouteRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(HeadscaleServiceServer).GetRoutes(ctx, in) return srv.(HeadscaleServiceServer).GetMachineRoute(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/GetRoutes", FullMethod: "/headscale.v1.HeadscaleService/GetMachineRoute",
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).GetRoutes(ctx, req.(*GetRoutesRequest)) return srv.(HeadscaleServiceServer).GetMachineRoute(ctx, req.(*GetMachineRouteRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _HeadscaleService_EnableRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _HeadscaleService_EnableMachineRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EnableRouteRequest) in := new(EnableMachineRoutesRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(HeadscaleServiceServer).EnableRoute(ctx, in) return srv.(HeadscaleServiceServer).EnableMachineRoutes(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/EnableRoute", FullMethod: "/headscale.v1.HeadscaleService/EnableMachineRoutes",
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).EnableRoute(ctx, req.(*EnableRouteRequest)) return srv.(HeadscaleServiceServer).EnableMachineRoutes(ctx, req.(*EnableMachineRoutesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_DisableRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DisableRouteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).DisableRoute(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/DisableRoute",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).DisableRoute(ctx, req.(*DisableRouteRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_GetMachineRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMachineRoutesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).GetMachineRoutes(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/GetMachineRoutes",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).GetMachineRoutes(ctx, req.(*GetMachineRoutesRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
@@ -910,20 +842,12 @@ var HeadscaleService_ServiceDesc = grpc.ServiceDesc{
Handler: _HeadscaleService_MoveMachine_Handler, Handler: _HeadscaleService_MoveMachine_Handler,
}, },
{ {
MethodName: "GetRoutes", MethodName: "GetMachineRoute",
Handler: _HeadscaleService_GetRoutes_Handler, Handler: _HeadscaleService_GetMachineRoute_Handler,
}, },
{ {
MethodName: "EnableRoute", MethodName: "EnableMachineRoutes",
Handler: _HeadscaleService_EnableRoute_Handler, Handler: _HeadscaleService_EnableMachineRoutes_Handler,
},
{
MethodName: "DisableRoute",
Handler: _HeadscaleService_DisableRoute_Handler,
},
{
MethodName: "GetMachineRoutes",
Handler: _HeadscaleService_GetMachineRoutes_Handler,
}, },
{ {
MethodName: "CreateApiKey", MethodName: "CreateApiKey",

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.27.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/machine.proto // source: headscale/v1/machine.proto

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.27.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/namespace.proto // source: headscale/v1/namespace.proto

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.27.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/preauthkey.proto // source: headscale/v1/preauthkey.proto
@@ -34,7 +34,6 @@ type PreAuthKey struct {
Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"` Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"`
Expiration *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiration,proto3" json:"expiration,omitempty"` Expiration *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiration,proto3" json:"expiration,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
AclTags []string `protobuf:"bytes,9,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
} }
func (x *PreAuthKey) Reset() { func (x *PreAuthKey) Reset() {
@@ -125,13 +124,6 @@ func (x *PreAuthKey) GetCreatedAt() *timestamppb.Timestamp {
return nil return nil
} }
func (x *PreAuthKey) GetAclTags() []string {
if x != nil {
return x.AclTags
}
return nil
}
type CreatePreAuthKeyRequest struct { type CreatePreAuthKeyRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -141,7 +133,6 @@ type CreatePreAuthKeyRequest struct {
Reusable bool `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"` Reusable bool `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"`
Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"` Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"` Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
AclTags []string `protobuf:"bytes,5,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
} }
func (x *CreatePreAuthKeyRequest) Reset() { func (x *CreatePreAuthKeyRequest) Reset() {
@@ -204,13 +195,6 @@ func (x *CreatePreAuthKeyRequest) GetExpiration() *timestamppb.Timestamp {
return nil return nil
} }
func (x *CreatePreAuthKeyRequest) GetAclTags() []string {
if x != nil {
return x.AclTags
}
return nil
}
type CreatePreAuthKeyResponse struct { type CreatePreAuthKeyResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -452,7 +436,7 @@ var file_headscale_v1_preauthkey_proto_rawDesc = []byte{
0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x0c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x0c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xac, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x91,
0x02, 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x02, 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a,
0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69,
@@ -470,45 +454,42 @@ var file_headscale_v1_preauthkey_proto_rawDesc = []byte{
0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
0x41, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x63, 0x6c, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x09, 0x41, 0x74, 0x22, 0xad, 0x01, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65,
0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x6c, 0x54, 0x61, 0x67, 0x73, 0x22, 0xc8, 0x01, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c,
0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08,
0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61, 0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65,
0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68,
0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61,
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
0x6c, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x6f, 0x6e, 0x22, 0x56, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41,
0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a,
0x08, 0x61, 0x63, 0x6c, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01,
0x07, 0x61, 0x63, 0x6c, 0x54, 0x61, 0x67, 0x73, 0x22, 0x56, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x22, 0x49, 0x0a, 0x17, 0x45, 0x78,
0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65,
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
0x22, 0x49, 0x0a, 0x17, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x1a, 0x0a, 0x18, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50,
0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x36, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68,
0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x57, 0x0a, 0x17, 0x4c, 0x69, 0x73,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x1a, 0x0a, 0x18, 0x45, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70,
0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65,
0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75,
0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x79, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x57, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70,
0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x65,
0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f,
0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.27.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/routes.proto // source: headscale/v1/routes.proto
@@ -9,7 +9,6 @@ package v1
import ( import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
) )
@@ -21,24 +20,17 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
type Route struct { type Routes struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` AdvertisedRoutes []string `protobuf:"bytes,1,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
Machine *Machine `protobuf:"bytes,2,opt,name=machine,proto3" json:"machine,omitempty"` EnabledRoutes []string `protobuf:"bytes,2,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
Prefix string `protobuf:"bytes,3,opt,name=prefix,proto3" json:"prefix,omitempty"`
Advertised bool `protobuf:"varint,4,opt,name=advertised,proto3" json:"advertised,omitempty"`
Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"`
IsPrimary bool `protobuf:"varint,6,opt,name=is_primary,json=isPrimary,proto3" json:"is_primary,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
DeletedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=deleted_at,json=deletedAt,proto3" json:"deleted_at,omitempty"`
} }
func (x *Route) Reset() { func (x *Routes) Reset() {
*x = Route{} *x = Routes{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[0] mi := &file_headscale_v1_routes_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -46,13 +38,13 @@ func (x *Route) Reset() {
} }
} }
func (x *Route) String() string { func (x *Routes) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*Route) ProtoMessage() {} func (*Routes) ProtoMessage() {}
func (x *Route) ProtoReflect() protoreflect.Message { func (x *Routes) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[0] mi := &file_headscale_v1_routes_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -64,82 +56,35 @@ func (x *Route) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use Route.ProtoReflect.Descriptor instead. // Deprecated: Use Routes.ProtoReflect.Descriptor instead.
func (*Route) Descriptor() ([]byte, []int) { func (*Routes) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{0} return file_headscale_v1_routes_proto_rawDescGZIP(), []int{0}
} }
func (x *Route) GetId() uint64 { func (x *Routes) GetAdvertisedRoutes() []string {
if x != nil { if x != nil {
return x.Id return x.AdvertisedRoutes
}
return 0
}
func (x *Route) GetMachine() *Machine {
if x != nil {
return x.Machine
} }
return nil return nil
} }
func (x *Route) GetPrefix() string { func (x *Routes) GetEnabledRoutes() []string {
if x != nil { if x != nil {
return x.Prefix return x.EnabledRoutes
}
return ""
}
func (x *Route) GetAdvertised() bool {
if x != nil {
return x.Advertised
}
return false
}
func (x *Route) GetEnabled() bool {
if x != nil {
return x.Enabled
}
return false
}
func (x *Route) GetIsPrimary() bool {
if x != nil {
return x.IsPrimary
}
return false
}
func (x *Route) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
} }
return nil return nil
} }
func (x *Route) GetUpdatedAt() *timestamppb.Timestamp { type GetMachineRouteRequest struct {
if x != nil {
return x.UpdatedAt
}
return nil
}
func (x *Route) GetDeletedAt() *timestamppb.Timestamp {
if x != nil {
return x.DeletedAt
}
return nil
}
type GetRoutesRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
MachineId uint64 `protobuf:"varint,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
} }
func (x *GetRoutesRequest) Reset() { func (x *GetMachineRouteRequest) Reset() {
*x = GetRoutesRequest{} *x = GetMachineRouteRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[1] mi := &file_headscale_v1_routes_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -147,13 +92,13 @@ func (x *GetRoutesRequest) Reset() {
} }
} }
func (x *GetRoutesRequest) String() string { func (x *GetMachineRouteRequest) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*GetRoutesRequest) ProtoMessage() {} func (*GetMachineRouteRequest) ProtoMessage() {}
func (x *GetRoutesRequest) ProtoReflect() protoreflect.Message { func (x *GetMachineRouteRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[1] mi := &file_headscale_v1_routes_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -165,21 +110,28 @@ func (x *GetRoutesRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use GetRoutesRequest.ProtoReflect.Descriptor instead. // Deprecated: Use GetMachineRouteRequest.ProtoReflect.Descriptor instead.
func (*GetRoutesRequest) Descriptor() ([]byte, []int) { func (*GetMachineRouteRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{1} return file_headscale_v1_routes_proto_rawDescGZIP(), []int{1}
} }
type GetRoutesResponse struct { func (x *GetMachineRouteRequest) GetMachineId() uint64 {
if x != nil {
return x.MachineId
}
return 0
}
type GetMachineRouteResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
Routes []*Route `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` Routes *Routes `protobuf:"bytes,1,opt,name=routes,proto3" json:"routes,omitempty"`
} }
func (x *GetRoutesResponse) Reset() { func (x *GetMachineRouteResponse) Reset() {
*x = GetRoutesResponse{} *x = GetMachineRouteResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[2] mi := &file_headscale_v1_routes_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -187,13 +139,13 @@ func (x *GetRoutesResponse) Reset() {
} }
} }
func (x *GetRoutesResponse) String() string { func (x *GetMachineRouteResponse) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*GetRoutesResponse) ProtoMessage() {} func (*GetMachineRouteResponse) ProtoMessage() {}
func (x *GetRoutesResponse) ProtoReflect() protoreflect.Message { func (x *GetMachineRouteResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[2] mi := &file_headscale_v1_routes_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -205,28 +157,29 @@ func (x *GetRoutesResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use GetRoutesResponse.ProtoReflect.Descriptor instead. // Deprecated: Use GetMachineRouteResponse.ProtoReflect.Descriptor instead.
func (*GetRoutesResponse) Descriptor() ([]byte, []int) { func (*GetMachineRouteResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{2} return file_headscale_v1_routes_proto_rawDescGZIP(), []int{2}
} }
func (x *GetRoutesResponse) GetRoutes() []*Route { func (x *GetMachineRouteResponse) GetRoutes() *Routes {
if x != nil { if x != nil {
return x.Routes return x.Routes
} }
return nil return nil
} }
type EnableRouteRequest struct { type EnableMachineRoutesRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
RouteId uint64 `protobuf:"varint,1,opt,name=route_id,json=routeId,proto3" json:"route_id,omitempty"` MachineId uint64 `protobuf:"varint,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
Routes []string `protobuf:"bytes,2,rep,name=routes,proto3" json:"routes,omitempty"`
} }
func (x *EnableRouteRequest) Reset() { func (x *EnableMachineRoutesRequest) Reset() {
*x = EnableRouteRequest{} *x = EnableMachineRoutesRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[3] mi := &file_headscale_v1_routes_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -234,13 +187,13 @@ func (x *EnableRouteRequest) Reset() {
} }
} }
func (x *EnableRouteRequest) String() string { func (x *EnableMachineRoutesRequest) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*EnableRouteRequest) ProtoMessage() {} func (*EnableMachineRoutesRequest) ProtoMessage() {}
func (x *EnableRouteRequest) ProtoReflect() protoreflect.Message { func (x *EnableMachineRoutesRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[3] mi := &file_headscale_v1_routes_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -252,26 +205,35 @@ func (x *EnableRouteRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use EnableRouteRequest.ProtoReflect.Descriptor instead. // Deprecated: Use EnableMachineRoutesRequest.ProtoReflect.Descriptor instead.
func (*EnableRouteRequest) Descriptor() ([]byte, []int) { func (*EnableMachineRoutesRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{3} return file_headscale_v1_routes_proto_rawDescGZIP(), []int{3}
} }
func (x *EnableRouteRequest) GetRouteId() uint64 { func (x *EnableMachineRoutesRequest) GetMachineId() uint64 {
if x != nil { if x != nil {
return x.RouteId return x.MachineId
} }
return 0 return 0
} }
type EnableRouteResponse struct { func (x *EnableMachineRoutesRequest) GetRoutes() []string {
if x != nil {
return x.Routes
}
return nil
}
type EnableMachineRoutesResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
Routes *Routes `protobuf:"bytes,1,opt,name=routes,proto3" json:"routes,omitempty"`
} }
func (x *EnableRouteResponse) Reset() { func (x *EnableMachineRoutesResponse) Reset() {
*x = EnableRouteResponse{} *x = EnableMachineRoutesResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[4] mi := &file_headscale_v1_routes_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -279,13 +241,13 @@ func (x *EnableRouteResponse) Reset() {
} }
} }
func (x *EnableRouteResponse) String() string { func (x *EnableMachineRoutesResponse) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*EnableRouteResponse) ProtoMessage() {} func (*EnableMachineRoutesResponse) ProtoMessage() {}
func (x *EnableRouteResponse) ProtoReflect() protoreflect.Message { func (x *EnableMachineRoutesResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[4] mi := &file_headscale_v1_routes_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -297,184 +259,12 @@ func (x *EnableRouteResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use EnableRouteResponse.ProtoReflect.Descriptor instead. // Deprecated: Use EnableMachineRoutesResponse.ProtoReflect.Descriptor instead.
func (*EnableRouteResponse) Descriptor() ([]byte, []int) { func (*EnableMachineRoutesResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{4} return file_headscale_v1_routes_proto_rawDescGZIP(), []int{4}
} }
type DisableRouteRequest struct { func (x *EnableMachineRoutesResponse) GetRoutes() *Routes {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RouteId uint64 `protobuf:"varint,1,opt,name=route_id,json=routeId,proto3" json:"route_id,omitempty"`
}
func (x *DisableRouteRequest) Reset() {
*x = DisableRouteRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DisableRouteRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DisableRouteRequest) ProtoMessage() {}
func (x *DisableRouteRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DisableRouteRequest.ProtoReflect.Descriptor instead.
func (*DisableRouteRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{5}
}
func (x *DisableRouteRequest) GetRouteId() uint64 {
if x != nil {
return x.RouteId
}
return 0
}
type DisableRouteResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *DisableRouteResponse) Reset() {
*x = DisableRouteResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DisableRouteResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DisableRouteResponse) ProtoMessage() {}
func (x *DisableRouteResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DisableRouteResponse.ProtoReflect.Descriptor instead.
func (*DisableRouteResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{6}
}
type GetMachineRoutesRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
MachineId uint64 `protobuf:"varint,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
}
func (x *GetMachineRoutesRequest) Reset() {
*x = GetMachineRoutesRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetMachineRoutesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetMachineRoutesRequest) ProtoMessage() {}
func (x *GetMachineRoutesRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetMachineRoutesRequest.ProtoReflect.Descriptor instead.
func (*GetMachineRoutesRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{7}
}
func (x *GetMachineRoutesRequest) GetMachineId() uint64 {
if x != nil {
return x.MachineId
}
return 0
}
type GetMachineRoutesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Routes []*Route `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"`
}
func (x *GetMachineRoutesResponse) Reset() {
*x = GetMachineRoutesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetMachineRoutesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetMachineRoutesResponse) ProtoMessage() {}
func (x *GetMachineRoutesResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetMachineRoutesResponse.ProtoReflect.Descriptor instead.
func (*GetMachineRoutesResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{8}
}
func (x *GetMachineRoutesResponse) GetRoutes() []*Route {
if x != nil { if x != nil {
return x.Routes return x.Routes
} }
@@ -486,60 +276,34 @@ var File_headscale_v1_routes_proto protoreflect.FileDescriptor
var file_headscale_v1_routes_proto_rawDesc = []byte{ var file_headscale_v1_routes_proto_rawDesc = []byte{
0x0a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x0a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x72,
0x6f, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x68, 0x65, 0x61, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x68, 0x65, 0x61,
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x5c, 0x0a, 0x06, 0x52, 0x6f, 0x75,
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65,
0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1a, 0x68, 0x65, 0x61, 0x64, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xea, 0x02, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74,
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65,
0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x37, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4d, 0x61,
0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18,
0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64,
0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x64, 0x76, 0x22, 0x47, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f,
0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x72,
0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x68, 0x65,
0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65,
0x6c, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x73, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x53, 0x0a, 0x1a, 0x45, 0x6e, 0x61,
0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73,
0x72, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73,
0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x4b,
0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0a, 0x1b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52,
0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a,
0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e,
0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x64, 0x65, 0x6c, 0x65, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x75,
0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x74, 0x65, 0x73, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e,
0x64, 0x41, 0x74, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x52, 0x6f,
0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06,
0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x68,
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x75, 0x74,
0x65, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x2f, 0x0a, 0x12, 0x45, 0x6e, 0x61,
0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x19, 0x0a, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x04, 0x52, 0x07, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x22, 0x15, 0x0a, 0x13, 0x45, 0x6e,
0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x30, 0x0a, 0x13, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x6f, 0x75, 0x74,
0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x72, 0x6f, 0x75, 0x74,
0x65, 0x49, 0x64, 0x22, 0x16, 0x0a, 0x14, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f,
0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, 0x0a, 0x17, 0x47,
0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x47, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x2b, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x13, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x29,
0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61,
0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f,
0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
} }
var ( var (
@@ -554,32 +318,22 @@ func file_headscale_v1_routes_proto_rawDescGZIP() []byte {
return file_headscale_v1_routes_proto_rawDescData return file_headscale_v1_routes_proto_rawDescData
} }
var file_headscale_v1_routes_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_headscale_v1_routes_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_headscale_v1_routes_proto_goTypes = []interface{}{ var file_headscale_v1_routes_proto_goTypes = []interface{}{
(*Route)(nil), // 0: headscale.v1.Route (*Routes)(nil), // 0: headscale.v1.Routes
(*GetRoutesRequest)(nil), // 1: headscale.v1.GetRoutesRequest (*GetMachineRouteRequest)(nil), // 1: headscale.v1.GetMachineRouteRequest
(*GetRoutesResponse)(nil), // 2: headscale.v1.GetRoutesResponse (*GetMachineRouteResponse)(nil), // 2: headscale.v1.GetMachineRouteResponse
(*EnableRouteRequest)(nil), // 3: headscale.v1.EnableRouteRequest (*EnableMachineRoutesRequest)(nil), // 3: headscale.v1.EnableMachineRoutesRequest
(*EnableRouteResponse)(nil), // 4: headscale.v1.EnableRouteResponse (*EnableMachineRoutesResponse)(nil), // 4: headscale.v1.EnableMachineRoutesResponse
(*DisableRouteRequest)(nil), // 5: headscale.v1.DisableRouteRequest
(*DisableRouteResponse)(nil), // 6: headscale.v1.DisableRouteResponse
(*GetMachineRoutesRequest)(nil), // 7: headscale.v1.GetMachineRoutesRequest
(*GetMachineRoutesResponse)(nil), // 8: headscale.v1.GetMachineRoutesResponse
(*Machine)(nil), // 9: headscale.v1.Machine
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
} }
var file_headscale_v1_routes_proto_depIdxs = []int32{ var file_headscale_v1_routes_proto_depIdxs = []int32{
9, // 0: headscale.v1.Route.machine:type_name -> headscale.v1.Machine 0, // 0: headscale.v1.GetMachineRouteResponse.routes:type_name -> headscale.v1.Routes
10, // 1: headscale.v1.Route.created_at:type_name -> google.protobuf.Timestamp 0, // 1: headscale.v1.EnableMachineRoutesResponse.routes:type_name -> headscale.v1.Routes
10, // 2: headscale.v1.Route.updated_at:type_name -> google.protobuf.Timestamp 2, // [2:2] is the sub-list for method output_type
10, // 3: headscale.v1.Route.deleted_at:type_name -> google.protobuf.Timestamp 2, // [2:2] is the sub-list for method input_type
0, // 4: headscale.v1.GetRoutesResponse.routes:type_name -> headscale.v1.Route 2, // [2:2] is the sub-list for extension type_name
0, // 5: headscale.v1.GetMachineRoutesResponse.routes:type_name -> headscale.v1.Route 2, // [2:2] is the sub-list for extension extendee
6, // [6:6] is the sub-list for method output_type 0, // [0:2] is the sub-list for field type_name
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
} }
func init() { file_headscale_v1_routes_proto_init() } func init() { file_headscale_v1_routes_proto_init() }
@@ -587,10 +341,9 @@ func file_headscale_v1_routes_proto_init() {
if File_headscale_v1_routes_proto != nil { if File_headscale_v1_routes_proto != nil {
return return
} }
file_headscale_v1_machine_proto_init()
if !protoimpl.UnsafeEnabled { if !protoimpl.UnsafeEnabled {
file_headscale_v1_routes_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { file_headscale_v1_routes_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Route); i { switch v := v.(*Routes); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -602,7 +355,7 @@ func file_headscale_v1_routes_proto_init() {
} }
} }
file_headscale_v1_routes_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { file_headscale_v1_routes_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetRoutesRequest); i { switch v := v.(*GetMachineRouteRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -614,7 +367,7 @@ func file_headscale_v1_routes_proto_init() {
} }
} }
file_headscale_v1_routes_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { file_headscale_v1_routes_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetRoutesResponse); i { switch v := v.(*GetMachineRouteResponse); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -626,7 +379,7 @@ func file_headscale_v1_routes_proto_init() {
} }
} }
file_headscale_v1_routes_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { file_headscale_v1_routes_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*EnableRouteRequest); i { switch v := v.(*EnableMachineRoutesRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -638,55 +391,7 @@ func file_headscale_v1_routes_proto_init() {
} }
} }
file_headscale_v1_routes_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { file_headscale_v1_routes_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*EnableRouteResponse); i { switch v := v.(*EnableMachineRoutesResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_routes_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DisableRouteRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_routes_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DisableRouteResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_routes_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetMachineRoutesRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_routes_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetMachineRoutesResponse); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -704,7 +409,7 @@ func file_headscale_v1_routes_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_headscale_v1_routes_proto_rawDesc, RawDescriptor: file_headscale_v1_routes_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 9, NumMessages: 5,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@@ -367,12 +367,13 @@
}, },
"/api/v1/machine/{machineId}/routes": { "/api/v1/machine/{machineId}/routes": {
"get": { "get": {
"operationId": "HeadscaleService_GetMachineRoutes", "summary": "--- Route start ---",
"operationId": "HeadscaleService_GetMachineRoute",
"responses": { "responses": {
"200": { "200": {
"description": "A successful response.", "description": "A successful response.",
"schema": { "schema": {
"$ref": "#/definitions/v1GetMachineRoutesResponse" "$ref": "#/definitions/v1GetMachineRouteResponse"
} }
}, },
"default": { "default": {
@@ -394,6 +395,45 @@
"tags": [ "tags": [
"HeadscaleService" "HeadscaleService"
] ]
},
"post": {
"operationId": "HeadscaleService_EnableMachineRoutes",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1EnableMachineRoutesResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
},
{
"name": "routes",
"in": "query",
"required": false,
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi"
}
],
"tags": [
"HeadscaleService"
]
} }
}, },
"/api/v1/machine/{machineId}/tags": { "/api/v1/machine/{machineId}/tags": {
@@ -682,91 +722,6 @@
"HeadscaleService" "HeadscaleService"
] ]
} }
},
"/api/v1/routes": {
"get": {
"summary": "--- Route start ---",
"operationId": "HeadscaleService_GetRoutes",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1GetRoutesResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/routes/{routeId}/disable": {
"post": {
"operationId": "HeadscaleService_DisableRoute",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1DisableRouteResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "routeId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/routes/{routeId}/enable": {
"post": {
"operationId": "HeadscaleService_EnableRoute",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1EnableRouteResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "routeId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
}
],
"tags": [
"HeadscaleService"
]
}
} }
}, },
"definitions": { "definitions": {
@@ -869,12 +824,6 @@
"expiration": { "expiration": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
},
"aclTags": {
"type": "array",
"items": {
"type": "string"
}
} }
} }
}, },
@@ -920,11 +869,13 @@
"v1DeleteNamespaceResponse": { "v1DeleteNamespaceResponse": {
"type": "object" "type": "object"
}, },
"v1DisableRouteResponse": { "v1EnableMachineRoutesResponse": {
"type": "object" "type": "object",
}, "properties": {
"v1EnableRouteResponse": { "routes": {
"type": "object" "$ref": "#/definitions/v1Routes"
}
}
}, },
"v1ExpireApiKeyRequest": { "v1ExpireApiKeyRequest": {
"type": "object", "type": "object",
@@ -967,14 +918,11 @@
} }
} }
}, },
"v1GetMachineRoutesResponse": { "v1GetMachineRouteResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"routes": { "routes": {
"type": "array", "$ref": "#/definitions/v1Routes"
"items": {
"$ref": "#/definitions/v1Route"
}
} }
} }
}, },
@@ -986,17 +934,6 @@
} }
} }
}, },
"v1GetRoutesResponse": {
"type": "object",
"properties": {
"routes": {
"type": "array",
"items": {
"$ref": "#/definitions/v1Route"
}
}
}
},
"v1ListApiKeysResponse": { "v1ListApiKeysResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1165,12 +1102,6 @@
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
},
"aclTags": {
"type": "array",
"items": {
"type": "string"
}
} }
} }
}, },
@@ -1208,39 +1139,20 @@
} }
} }
}, },
"v1Route": { "v1Routes": {
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "advertisedRoutes": {
"type": "string", "type": "array",
"format": "uint64" "items": {
"type": "string"
}
}, },
"machine": { "enabledRoutes": {
"$ref": "#/definitions/v1Machine" "type": "array",
}, "items": {
"prefix": { "type": "string"
"type": "string" }
},
"advertised": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
},
"isPrimary": {
"type": "boolean"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"deletedAt": {
"type": "string",
"format": "date-time"
} }
} }
}, },

179
go.mod
View File

@@ -1,150 +1,139 @@
module github.com/juanfont/headscale module github.com/juanfont/headscale
go 1.19 go 1.18
require ( require (
github.com/AlecAivazis/survey/v2 v2.3.6 github.com/AlecAivazis/survey/v2 v2.3.4
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029
github.com/cenkalti/backoff/v4 v4.1.3 github.com/coreos/go-oidc/v3 v3.1.0
github.com/coreos/go-oidc/v3 v3.4.0
github.com/deckarep/golang-set/v2 v2.1.0 github.com/deckarep/golang-set/v2 v2.1.0
github.com/efekarakus/termcolor v1.0.1 github.com/efekarakus/termcolor v1.0.1
github.com/glebarez/sqlite v1.5.0 github.com/glebarez/sqlite v1.4.3
github.com/gofrs/uuid v4.3.0+incompatible github.com/gofrs/uuid v4.2.0+incompatible
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.12.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
github.com/klauspost/compress v1.15.12 github.com/klauspost/compress v1.15.4
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282
github.com/ory/dockertest/v3 v3.9.1 github.com/ory/dockertest/v3 v3.9.1
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/philip-bui/grpc-zerolog v1.0.1 github.com/philip-bui/grpc-zerolog v1.0.1
github.com/prometheus/client_golang v1.13.0 github.com/prometheus/client_golang v1.12.1
github.com/prometheus/common v0.37.0 github.com/prometheus/common v0.32.1
github.com/pterm/pterm v0.12.49 github.com/pterm/pterm v0.12.41
github.com/puzpuzpuz/xsync/v2 v2.0.2 github.com/puzpuzpuz/xsync v1.2.1
github.com/rs/zerolog v1.28.0 github.com/rs/zerolog v1.26.1
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.13.0 github.com/spf13/viper v1.11.0
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.7.1
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
golang.org/x/crypto v0.1.0 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
golang.org/x/net v0.1.0 golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
golang.org/x/oauth2 v0.1.0 google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
golang.org/x/sync v0.1.0 google.golang.org/grpc v1.46.0
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c google.golang.org/protobuf v1.28.0
google.golang.org/grpc v1.50.1
google.golang.org/protobuf v1.28.1
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gorm.io/driver/postgres v1.4.5 gorm.io/driver/postgres v1.3.5
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755 gorm.io/gorm v1.23.4
tailscale.com v1.32.2 inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
tailscale.com v1.26.0
) )
require ( require (
atomicgo.dev/cursor v0.1.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
atomicgo.dev/keyboard v0.2.8 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect
filippo.io/edwards25519 v1.0.0 // 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/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/akutz/memconn v0.1.0 // indirect github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
github.com/atomicgo/cursor v0.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/continuity v0.3.0 // indirect github.com/containerd/continuity v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.21+incompatible // indirect github.com/docker/cli v20.10.16+incompatible // indirect
github.com/docker/docker v20.10.21+incompatible // indirect github.com/docker/docker v20.10.16+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/glebarez/go-sqlite v1.16.0 // indirect
github.com/glebarez/go-sqlite v1.19.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.5.8 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gookit/color v1.5.2 // indirect github.com/gookit/color v1.5.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/go-version v1.4.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/imdario/mergo v0.3.12 // indirect
github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect github.com/jackc/pgconn v1.12.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect github.com/jackc/pgproto3/v2 v2.3.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.12.0 // indirect github.com/jackc/pgtype v1.11.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect github.com/jackc/pgx/v4 v4.16.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.4 // indirect
github.com/josharian/native v1.0.0 // indirect github.com/josharian/native v1.0.0 // indirect
github.com/jsimonetti/rtnetlink v1.2.3 // indirect github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.3.0 // indirect github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/lithammer/fuzzysearch v1.1.5 // indirect
github.com/magiconair/properties v1.8.6 // indirect github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mdlayher/netlink v1.6.2 // indirect github.com/mdlayher/netlink v1.6.0 // indirect
github.com/mdlayher/socket v0.2.3 // indirect github.com/mdlayher/socket v0.2.3 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opencontainers/runc v1.1.4 // indirect github.com/opencontainers/runc v1.1.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.9.2 // indirect github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.1 // indirect github.com/subosito/gotenv v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
golang.org/x/mod v0.6.0 // indirect go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
golang.org/x/sys v0.1.0 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
golang.org/x/term v0.1.0 // indirect golang.org/x/net v0.0.0-20220516155154-20f960328961 // indirect
golang.org/x/text v0.4.0 // indirect golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect
golang.org/x/time v0.1.0 // indirect golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
golang.org/x/tools v0.2.0 // indirect golang.org/x/text v0.3.7 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
golang.zx2c4.com/wireguard/windows v0.4.10 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect
modernc.org/libc v1.21.4 // indirect modernc.org/libc v1.14.12 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.4.0 // indirect modernc.org/memory v1.0.7 // indirect
modernc.org/sqlite v1.19.3 // indirect modernc.org/sqlite v1.16.0 // indirect
nhooyr.io/websocket v1.8.7 // indirect
) )

1174
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// nolint //nolint
package headscale package headscale
import ( import (
@@ -12,7 +12,6 @@ import (
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key"
) )
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
@@ -107,21 +106,11 @@ func (api headscaleV1APIServer) CreatePreAuthKey(
expiration = request.GetExpiration().AsTime() expiration = request.GetExpiration().AsTime()
} }
for _, tag := range request.AclTags {
err := validateTag(tag)
if err != nil {
return &v1.CreatePreAuthKeyResponse{
PreAuthKey: nil,
}, status.Error(codes.InvalidArgument, err.Error())
}
}
preAuthKey, err := api.h.CreatePreAuthKey( preAuthKey, err := api.h.CreatePreAuthKey(
request.GetNamespace(), request.GetNamespace(),
request.GetReusable(), request.GetReusable(),
request.GetEphemeral(), request.GetEphemeral(),
&expiration, &expiration,
request.AclTags,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -170,7 +159,7 @@ func (api headscaleV1APIServer) RegisterMachine(
) (*v1.RegisterMachineResponse, error) { ) (*v1.RegisterMachineResponse, error) {
log.Trace(). log.Trace().
Str("namespace", request.GetNamespace()). Str("namespace", request.GetNamespace()).
Str("node_key", request.GetKey()). Str("machine_key", request.GetKey()).
Msg("Registering machine") Msg("Registering machine")
machine, err := api.h.RegisterMachineFromAuthCallback( machine, err := api.h.RegisterMachineFromAuthCallback(
@@ -210,7 +199,7 @@ func (api headscaleV1APIServer) SetTags(
err := validateTag(tag) err := validateTag(tag)
if err != nil { if err != nil {
return &v1.SetTagsResponse{ return &v1.SetTagsResponse{
Machine: nil, Machine: nil,
}, status.Error(codes.InvalidArgument, err.Error()) }, status.Error(codes.InvalidArgument, err.Error())
} }
} }
@@ -364,60 +353,36 @@ func (api headscaleV1APIServer) MoveMachine(
return &v1.MoveMachineResponse{Machine: machine.toProto()}, nil return &v1.MoveMachineResponse{Machine: machine.toProto()}, nil
} }
func (api headscaleV1APIServer) GetRoutes( func (api headscaleV1APIServer) GetMachineRoute(
ctx context.Context, ctx context.Context,
request *v1.GetRoutesRequest, request *v1.GetMachineRouteRequest,
) (*v1.GetRoutesResponse, error) { ) (*v1.GetMachineRouteResponse, error) {
routes, err := api.h.GetRoutes()
if err != nil {
return nil, err
}
return &v1.GetRoutesResponse{
Routes: Routes(routes).toProto(),
}, nil
}
func (api headscaleV1APIServer) EnableRoute(
ctx context.Context,
request *v1.EnableRouteRequest,
) (*v1.EnableRouteResponse, error) {
err := api.h.EnableRoute(request.GetRouteId())
if err != nil {
return nil, err
}
return &v1.EnableRouteResponse{}, nil
}
func (api headscaleV1APIServer) DisableRoute(
ctx context.Context,
request *v1.DisableRouteRequest,
) (*v1.DisableRouteResponse, error) {
err := api.h.DisableRoute(request.GetRouteId())
if err != nil {
return nil, err
}
return &v1.DisableRouteResponse{}, nil
}
func (api headscaleV1APIServer) GetMachineRoutes(
ctx context.Context,
request *v1.GetMachineRoutesRequest,
) (*v1.GetMachineRoutesResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId()) machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil { if err != nil {
return nil, err return nil, err
} }
routes, err := api.h.GetMachineRoutes(machine) return &v1.GetMachineRouteResponse{
Routes: machine.RoutesToProto(),
}, nil
}
func (api headscaleV1APIServer) EnableMachineRoutes(
ctx context.Context,
request *v1.EnableMachineRoutesRequest,
) (*v1.EnableMachineRoutesResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &v1.GetMachineRoutesResponse{ err = api.h.EnableRoutes(machine, request.GetRoutes()...)
Routes: Routes(routes).toProto(), if err != nil {
return nil, err
}
return &v1.EnableMachineRoutesResponse{
Routes: machine.RoutesToProto(),
}, nil }, nil
} }
@@ -504,7 +469,7 @@ func (api headscaleV1APIServer) DebugCreateMachine(
Hostname: "DebugTestMachine", Hostname: "DebugTestMachine",
} }
givenName, err := api.h.GenerateGivenName(request.GetKey(), request.GetName()) givenName, err := api.h.GenerateGivenName(request.GetName())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -522,14 +487,8 @@ func (api headscaleV1APIServer) DebugCreateMachine(
HostInfo: HostInfo(hostinfo), HostInfo: HostInfo(hostinfo),
} }
nodeKey := key.NodePublic{}
err = nodeKey.UnmarshalText([]byte(request.GetKey()))
if err != nil {
log.Panic().Msg("can not add machine for debug. invalid node key")
}
api.h.registrationCache.Set( api.h.registrationCache.Set(
NodePublicKeyStripPrefix(nodeKey), request.GetKey(),
newMachine, newMachine,
registerCacheExpiration, registerCacheExpiration,
) )

View File

@@ -1,15 +0,0 @@
//go:build ts2019
package headscale
import (
"net/http"
"github.com/gorilla/mux"
)
func (h *Headscale) addLegacyHandlers(router *mux.Router) {
router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
}

View File

@@ -1,8 +0,0 @@
//go:build !ts2019
package headscale
import "github.com/gorilla/mux"
func (h *Headscale) addLegacyHandlers(router *mux.Router) {
}

View File

@@ -1,302 +0,0 @@
package integration
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"strconv"
"testing"
"github.com/juanfont/headscale"
"github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/hsic"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
const (
dockerContextPath = "../."
hsicOIDCMockHashLength = 6
oidcServerPort = 10000
)
var errStatusCodeNotOK = errors.New("status code not OK")
type AuthOIDCScenario struct {
*Scenario
mockOIDC *dockertest.Resource
}
func TestOIDCAuthenticationPingAll(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
baseScenario, err := NewScenario()
if err != nil {
t.Errorf("failed to create scenario: %s", err)
}
scenario := AuthOIDCScenario{
Scenario: baseScenario,
}
spec := map[string]int{
"namespace1": len(TailscaleVersions),
}
oidcConfig, err := scenario.runMockOIDC()
if err != nil {
t.Errorf("failed to run mock OIDC server: %s", err)
}
oidcMap := map[string]string{
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
"HEADSCALE_OIDC_CLIENT_ID": oidcConfig.ClientID,
"HEADSCALE_OIDC_CLIENT_SECRET": oidcConfig.ClientSecret,
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": fmt.Sprintf("%t", oidcConfig.StripEmaildomain),
}
err = scenario.CreateHeadscaleEnv(
spec,
hsic.WithTestName("oidcauthping"),
hsic.WithConfigEnv(oidcMap),
hsic.WithHostnameAsServerURL(),
)
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
allClients, err := scenario.ListTailscaleClients()
if err != nil {
t.Errorf("failed to get clients: %s", err)
}
allIps, err := scenario.ListTailscaleClientsIPs()
if err != nil {
t.Errorf("failed to get clients: %s", err)
}
err = scenario.WaitForTailscaleSync()
if err != nil {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
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++
}
}
}
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func (s *AuthOIDCScenario) CreateHeadscaleEnv(
namespaces map[string]int,
opts ...hsic.Option,
) error {
headscale, err := s.Headscale(opts...)
if err != nil {
return err
}
err = headscale.WaitForReady()
if err != nil {
return err
}
for namespaceName, clientCount := range namespaces {
log.Printf("creating namespace %s with %d clients", namespaceName, clientCount)
err = s.CreateNamespace(namespaceName)
if err != nil {
return err
}
err = s.CreateTailscaleNodesInNamespace(namespaceName, "all", clientCount)
if err != nil {
return err
}
err = s.runTailscaleUp(namespaceName, headscale.GetEndpoint())
if err != nil {
return err
}
}
return nil
}
func (s *AuthOIDCScenario) runMockOIDC() (*headscale.OIDCConfig, error) {
hash, _ := headscale.GenerateRandomStringDNSSafe(hsicOIDCMockHashLength)
hostname := fmt.Sprintf("hs-oidcmock-%s", hash)
mockOidcOptions := &dockertest.RunOptions{
Name: hostname,
Cmd: []string{"headscale", "mockoidc"},
ExposedPorts: []string{"10000/tcp"},
PortBindings: map[docker.Port][]docker.PortBinding{
"10000/tcp": {{HostPort: "10000"}},
},
Networks: []*dockertest.Network{s.Scenario.network},
Env: []string{
fmt.Sprintf("MOCKOIDC_ADDR=%s", hostname),
"MOCKOIDC_PORT=10000",
"MOCKOIDC_CLIENT_ID=superclient",
"MOCKOIDC_CLIENT_SECRET=supersecret",
},
}
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.debug",
ContextDir: dockerContextPath,
}
err := s.pool.RemoveContainerByName(hostname)
if err != nil {
return nil, err
}
if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions(
headscaleBuildOptions,
mockOidcOptions,
dockertestutil.DockerRestartPolicy); err == nil {
s.mockOIDC = pmockoidc
} else {
return nil, err
}
log.Println("Waiting for headscale mock oidc to be ready for tests")
hostEndpoint := fmt.Sprintf(
"%s:%s",
s.mockOIDC.GetIPInNetwork(s.network),
s.mockOIDC.GetPort(fmt.Sprintf("%d/tcp", oidcServerPort)),
)
if err := s.pool.Retry(func() error {
oidcConfigURL := fmt.Sprintf("http://%s/oidc/.well-known/openid-configuration", hostEndpoint)
httpClient := &http.Client{}
ctx := context.Background()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, oidcConfigURL, nil)
resp, err := httpClient.Do(req)
if err != nil {
log.Printf("headscale mock OIDC tests is not ready: %s\n", err)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errStatusCodeNotOK
}
return nil
}); err != nil {
return nil, err
}
log.Printf("headscale mock oidc is ready for tests at %s", hostEndpoint)
return &headscale.OIDCConfig{
Issuer: fmt.Sprintf("http://%s/oidc",
net.JoinHostPort(s.mockOIDC.GetIPInNetwork(s.network), strconv.Itoa(oidcServerPort))),
ClientID: "superclient",
ClientSecret: "supersecret",
StripEmaildomain: true,
}, nil
}
func (s *AuthOIDCScenario) runTailscaleUp(
namespaceStr, loginServer string,
) error {
headscale, err := s.Headscale()
if err != nil {
return err
}
log.Printf("running tailscale up for namespace %s", namespaceStr)
if namespace, ok := s.namespaces[namespaceStr]; ok {
for _, client := range namespace.Clients {
namespace.joinWaitGroup.Add(1)
go func(c TailscaleClient) {
defer namespace.joinWaitGroup.Done()
// TODO(juanfont): error handle this
loginURL, err := c.UpWithLoginURL(loginServer)
if err != nil {
log.Printf("failed to run tailscale up: %s", err)
}
loginURL.Host = fmt.Sprintf("%s:8080", headscale.GetIP())
loginURL.Scheme = "http"
insecureTransport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
}
log.Printf("%s login url: %s\n", c.Hostname(), loginURL.String())
httpClient := &http.Client{Transport: insecureTransport}
ctx := context.Background()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
resp, err := httpClient.Do(req)
if err != nil {
log.Printf("%s failed to get login url %s: %s", c.Hostname(), loginURL, err)
return
}
defer resp.Body.Close()
_, err = io.ReadAll(resp.Body)
if err != nil {
log.Printf("%s failed to read response body: %s", c.Hostname(), err)
return
}
log.Printf("Finished request for %s to join tailnet", c.Hostname())
}(client)
err = client.WaitForReady()
if err != nil {
log.Printf("error waiting for client %s to be ready: %s", client.Hostname(), err)
}
log.Printf("client %s is ready", client.Hostname())
}
namespace.joinWaitGroup.Wait()
return nil
}
return fmt.Errorf("failed to up tailscale node: %w", errNoNamespaceAvailable)
}
func (s *AuthOIDCScenario) Shutdown() error {
err := s.pool.Purge(s.mockOIDC)
if err != nil {
return err
}
return s.Scenario.Shutdown()
}

View File

@@ -1,205 +0,0 @@
package integration
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"testing"
"github.com/juanfont/headscale/integration/hsic"
)
var errParseAuthPage = errors.New("failed to parse auth page")
type AuthWebFlowScenario struct {
*Scenario
}
func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
baseScenario, err := NewScenario()
if err != nil {
t.Errorf("failed to create scenario: %s", err)
}
scenario := AuthWebFlowScenario{
Scenario: baseScenario,
}
spec := map[string]int{
"namespace1": len(TailscaleVersions),
"namespace2": len(TailscaleVersions),
}
err = scenario.CreateHeadscaleEnv(spec, hsic.WithTestName("webauthping"))
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)
}
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++
}
}
}
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func (s *AuthWebFlowScenario) CreateHeadscaleEnv(
namespaces map[string]int,
opts ...hsic.Option,
) error {
headscale, err := s.Headscale(opts...)
if err != nil {
return err
}
err = headscale.WaitForReady()
if err != nil {
return err
}
for namespaceName, clientCount := range namespaces {
log.Printf("creating namespace %s with %d clients", namespaceName, clientCount)
err = s.CreateNamespace(namespaceName)
if err != nil {
return err
}
err = s.CreateTailscaleNodesInNamespace(namespaceName, "all", clientCount)
if err != nil {
return err
}
err = s.runTailscaleUp(namespaceName, headscale.GetEndpoint())
if err != nil {
return err
}
}
return nil
}
func (s *AuthWebFlowScenario) runTailscaleUp(
namespaceStr, loginServer string,
) error {
log.Printf("running tailscale up for namespace %s", namespaceStr)
if namespace, ok := s.namespaces[namespaceStr]; ok {
for _, client := range namespace.Clients {
namespace.joinWaitGroup.Add(1)
go func(c TailscaleClient) {
defer namespace.joinWaitGroup.Done()
// TODO(juanfont): error handle this
loginURL, err := c.UpWithLoginURL(loginServer)
if err != nil {
log.Printf("failed to run tailscale up: %s", err)
}
err = s.runHeadscaleRegister(namespaceStr, loginURL)
if err != nil {
log.Printf("failed to register client: %s", err)
}
}(client)
err := client.WaitForReady()
if err != nil {
log.Printf("error waiting for client %s to be ready: %s", client.Hostname(), err)
}
}
namespace.joinWaitGroup.Wait()
return nil
}
return fmt.Errorf("failed to up tailscale node: %w", errNoNamespaceAvailable)
}
func (s *AuthWebFlowScenario) runHeadscaleRegister(namespaceStr string, loginURL *url.URL) error {
headscale, err := s.Headscale()
if err != nil {
return err
}
log.Printf("loginURL: %s", loginURL)
loginURL.Host = fmt.Sprintf("%s:8080", headscale.GetIP())
loginURL.Scheme = "http"
httpClient := &http.Client{}
ctx := context.Background()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
resp, err := httpClient.Do(req)
if err != nil {
return err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
// see api.go HTML template
codeSep := strings.Split(string(body), "</code>")
if len(codeSep) != 2 {
return errParseAuthPage
}
keySep := strings.Split(codeSep[0], "key ")
if len(keySep) != 2 {
return errParseAuthPage
}
key := keySep[1]
log.Printf("registering node %s", key)
if headscale, err := s.Headscale(); err == nil {
_, err = headscale.Execute(
[]string{"headscale", "-n", namespaceStr, "nodes", "register", "--key", key},
)
if err != nil {
log.Printf("failed to register node: %s", err)
return err
}
return nil
}
return fmt.Errorf("failed to find headscale: %w", errNoHeadscaleAvailable)
}

View File

@@ -1,534 +0,0 @@
package integration
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"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/stretchr/testify/assert"
)
func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
str, err := headscale.Execute(command)
if err != nil {
return err
}
err = json.Unmarshal([]byte(str), result)
if err != nil {
return err
}
return nil
}
func TestNamespaceCommand(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
scenario, err := NewScenario()
assert.NoError(t, err)
spec := map[string]int{
"namespace1": 0,
"namespace2": 0,
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
assert.NoError(t, err)
headscale, err := scenario.Headscale()
assert.NoError(t, err)
var listNamespaces []v1.Namespace
err = executeAndUnmarshal(headscale,
[]string{
"headscale",
"namespaces",
"list",
"--output",
"json",
},
&listNamespaces,
)
assert.NoError(t, err)
result := []string{listNamespaces[0].Name, listNamespaces[1].Name}
sort.Strings(result)
assert.Equal(
t,
[]string{"namespace1", "namespace2"},
result,
)
_, err = headscale.Execute(
[]string{
"headscale",
"namespaces",
"rename",
"--output",
"json",
"namespace2",
"newname",
},
)
assert.NoError(t, err)
var listAfterRenameNamespaces []v1.Namespace
err = executeAndUnmarshal(headscale,
[]string{
"headscale",
"namespaces",
"list",
"--output",
"json",
},
&listAfterRenameNamespaces,
)
assert.NoError(t, err)
result = []string{listAfterRenameNamespaces[0].Name, listAfterRenameNamespaces[1].Name}
sort.Strings(result)
assert.Equal(
t,
[]string{"namespace1", "newname"},
result,
)
err = scenario.Shutdown()
assert.NoError(t, err)
}
func TestPreAuthKeyCommand(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
namespace := "preauthkeyspace"
count := 3
scenario, err := NewScenario()
assert.NoError(t, err)
spec := map[string]int{
namespace: 0,
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipak"))
assert.NoError(t, err)
headscale, err := scenario.Headscale()
assert.NoError(t, err)
keys := make([]*v1.PreAuthKey, count)
assert.NoError(t, err)
for index := 0; index < count; index++ {
var preAuthKey v1.PreAuthKey
err := executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--namespace",
namespace,
"create",
"--reusable",
"--expiration",
"24h",
"--output",
"json",
"--tags",
"tag:test1,tag:test2",
},
&preAuthKey,
)
assert.NoError(t, err)
keys[index] = &preAuthKey
}
assert.Len(t, keys, 3)
var listedPreAuthKeys []v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--namespace",
namespace,
"list",
"--output",
"json",
},
&listedPreAuthKeys,
)
assert.NoError(t, err)
// There is one key created by "scenario.CreateHeadscaleEnv"
assert.Len(t, listedPreAuthKeys, 4)
assert.Equal(
t,
[]string{keys[0].Id, keys[1].Id, keys[2].Id},
[]string{listedPreAuthKeys[1].Id, listedPreAuthKeys[2].Id, listedPreAuthKeys[3].Id},
)
assert.NotEmpty(t, listedPreAuthKeys[1].Key)
assert.NotEmpty(t, listedPreAuthKeys[2].Key)
assert.NotEmpty(t, listedPreAuthKeys[3].Key)
assert.True(t, listedPreAuthKeys[1].Expiration.AsTime().After(time.Now()))
assert.True(t, listedPreAuthKeys[2].Expiration.AsTime().After(time.Now()))
assert.True(t, listedPreAuthKeys[3].Expiration.AsTime().After(time.Now()))
assert.True(
t,
listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedPreAuthKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedPreAuthKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
)
for index := range listedPreAuthKeys {
if index == 0 {
continue
}
assert.Equal(t, listedPreAuthKeys[index].AclTags, []string{"tag:test1", "tag:test2"})
}
// Test key expiry
_, err = headscale.Execute(
[]string{
"headscale",
"preauthkeys",
"--namespace",
namespace,
"expire",
listedPreAuthKeys[1].Key,
},
)
assert.NoError(t, err)
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--namespace",
namespace,
"list",
"--output",
"json",
},
&listedPreAuthKeysAfterExpire,
)
assert.NoError(t, err)
assert.True(t, listedPreAuthKeysAfterExpire[1].Expiration.AsTime().Before(time.Now()))
assert.True(t, listedPreAuthKeysAfterExpire[2].Expiration.AsTime().After(time.Now()))
assert.True(t, listedPreAuthKeysAfterExpire[3].Expiration.AsTime().After(time.Now()))
err = scenario.Shutdown()
assert.NoError(t, err)
}
func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
namespace := "pre-auth-key-without-exp-namespace"
scenario, err := NewScenario()
assert.NoError(t, err)
spec := map[string]int{
namespace: 0,
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipaknaexp"))
assert.NoError(t, err)
headscale, err := scenario.Headscale()
assert.NoError(t, err)
var preAuthKey v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--namespace",
namespace,
"create",
"--reusable",
"--output",
"json",
},
&preAuthKey,
)
assert.NoError(t, err)
var listedPreAuthKeys []v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--namespace",
namespace,
"list",
"--output",
"json",
},
&listedPreAuthKeys,
)
assert.NoError(t, err)
// There is one key created by "scenario.CreateHeadscaleEnv"
assert.Len(t, listedPreAuthKeys, 2)
assert.True(t, listedPreAuthKeys[1].Expiration.AsTime().After(time.Now()))
assert.True(
t,
listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Minute*70)),
)
err = scenario.Shutdown()
assert.NoError(t, err)
}
func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
namespace := "pre-auth-key-reus-ephm-namespace"
scenario, err := NewScenario()
assert.NoError(t, err)
spec := map[string]int{
namespace: 0,
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipakresueeph"))
assert.NoError(t, err)
headscale, err := scenario.Headscale()
assert.NoError(t, err)
var preAuthReusableKey v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--namespace",
namespace,
"create",
"--reusable=true",
"--output",
"json",
},
&preAuthReusableKey,
)
assert.NoError(t, err)
var preAuthEphemeralKey v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--namespace",
namespace,
"create",
"--ephemeral=true",
"--output",
"json",
},
&preAuthEphemeralKey,
)
assert.NoError(t, err)
assert.True(t, preAuthEphemeralKey.GetEphemeral())
assert.False(t, preAuthEphemeralKey.GetReusable())
var listedPreAuthKeys []v1.PreAuthKey
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--namespace",
namespace,
"list",
"--output",
"json",
},
&listedPreAuthKeys,
)
assert.NoError(t, err)
// There is one key created by "scenario.CreateHeadscaleEnv"
assert.Len(t, listedPreAuthKeys, 3)
err = scenario.Shutdown()
assert.NoError(t, err)
}
func TestEnablingRoutes(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
namespace := "enable-routing"
scenario, err := NewScenario()
assert.NoError(t, err)
spec := map[string]int{
namespace: 3,
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"))
assert.NoError(t, err)
allClients, err := scenario.ListTailscaleClients()
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)
}
headscale, err := scenario.Headscale()
assert.NoError(t, err)
// advertise routes using the up command
for i, client := range allClients {
routeStr := fmt.Sprintf("10.0.%d.0/24", i)
hostname, _ := client.FQDN()
_, _, err = client.Execute([]string{
"tailscale",
"up",
fmt.Sprintf("--advertise-routes=%s", routeStr),
"-login-server", headscale.GetEndpoint(),
"--hostname", hostname,
})
assert.NoError(t, err)
}
err = scenario.WaitForTailscaleSync()
if err != nil {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
var routes []*v1.Route
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"routes",
"list",
"--output",
"json",
},
&routes,
)
assert.NoError(t, err)
assert.Len(t, routes, 3)
for _, route := range routes {
assert.Equal(t, route.Advertised, true)
assert.Equal(t, route.Enabled, false)
assert.Equal(t, route.IsPrimary, false)
}
for _, route := range routes {
_, err = headscale.Execute(
[]string{
"headscale",
"routes",
"enable",
"--route",
strconv.Itoa(int(route.Id)),
})
assert.NoError(t, err)
}
var enablingRoutes []*v1.Route
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"routes",
"list",
"--output",
"json",
},
&enablingRoutes,
)
assert.NoError(t, err)
for _, route := range enablingRoutes {
assert.Equal(t, route.Advertised, true)
assert.Equal(t, route.Enabled, true)
assert.Equal(t, route.IsPrimary, true)
}
routeIDToBeDisabled := enablingRoutes[0].Id
_, err = headscale.Execute(
[]string{
"headscale",
"routes",
"disable",
"--route",
strconv.Itoa(int(routeIDToBeDisabled)),
})
assert.NoError(t, err)
var disablingRoutes []*v1.Route
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"routes",
"list",
"--output",
"json",
},
&disablingRoutes,
)
assert.NoError(t, err)
for _, route := range disablingRoutes {
assert.Equal(t, true, route.Advertised)
if route.Id == routeIDToBeDisabled {
assert.Equal(t, route.Enabled, false)
assert.Equal(t, route.IsPrimary, false)
} else {
assert.Equal(t, route.Enabled, true)
assert.Equal(t, route.IsPrimary, true)
}
}
}

View File

@@ -1,19 +0,0 @@
package integration
import (
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
)
type ControlServer interface {
Shutdown() error
Execute(command []string) (string, error)
GetHealthEndpoint() string
GetEndpoint() string
WaitForReady() error
CreateNamespace(namespace string) error
CreateAuthKey(namespace string) (*v1.PreAuthKey, error)
ListMachinesInNamespace(namespace string) ([]*v1.Machine, error)
GetCert() []byte
GetHostname() string
GetIP() string
}

View File

@@ -1,40 +0,0 @@
package dockertestutil
import (
"os"
"github.com/ory/dockertest/v3/docker"
)
func IsRunningInContainer() bool {
if _, err := os.Stat("/.dockerenv"); err != nil {
return false
}
return true
}
func DockerRestartPolicy(config *docker.HostConfig) {
// set AutoRemove to true so that stopped container goes away by itself on error *immediately*.
// when set to false, containers remain until the end of the integration test.
config.AutoRemove = false
config.RestartPolicy = docker.RestartPolicy{
Name: "no",
}
}
func DockerAllowLocalIPv6(config *docker.HostConfig) {
if config.Sysctls == nil {
config.Sysctls = make(map[string]string, 1)
}
config.Sysctls["net.ipv6.conf.all.disable_ipv6"] = "0"
}
func DockerAllowNetworkAdministration(config *docker.HostConfig) {
config.CapAdd = append(config.CapAdd, "NET_ADMIN")
config.Mounts = append(config.Mounts, docker.HostMount{
Type: "bind",
Source: "/dev/net/tun",
Target: "/dev/net/tun",
})
}

View File

@@ -1,94 +0,0 @@
package dockertestutil
import (
"bytes"
"errors"
"fmt"
"time"
"github.com/ory/dockertest/v3"
)
const dockerExecuteTimeout = time.Second * 10
var (
ErrDockertestCommandFailed = errors.New("dockertest command failed")
ErrDockertestCommandTimeout = errors.New("dockertest command timed out")
)
type ExecuteCommandConfig struct {
timeout time.Duration
}
type ExecuteCommandOption func(*ExecuteCommandConfig) error
func ExecuteCommandTimeout(timeout time.Duration) ExecuteCommandOption {
return ExecuteCommandOption(func(conf *ExecuteCommandConfig) error {
conf.timeout = timeout
return nil
})
}
func ExecuteCommand(
resource *dockertest.Resource,
cmd []string,
env []string,
options ...ExecuteCommandOption,
) (string, string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
execConfig := ExecuteCommandConfig{
timeout: dockerExecuteTimeout,
}
for _, opt := range options {
if err := opt(&execConfig); err != nil {
return "", "", fmt.Errorf("execute-command/options: %w", err)
}
}
type result struct {
exitCode int
err error
}
resultChan := make(chan result, 1)
// Run your long running function in it's own goroutine and pass back it's
// response into our channel.
go func() {
exitCode, err := resource.Exec(
cmd,
dockertest.ExecOptions{
Env: append(env, "HEADSCALE_LOG_LEVEL=disabled"),
StdOut: &stdout,
StdErr: &stderr,
},
)
resultChan <- result{exitCode, err}
}()
// Listen on our channel AND a timeout channel - which ever happens first.
select {
case res := <-resultChan:
if res.err != nil {
return stdout.String(), stderr.String(), res.err
}
if res.exitCode != 0 {
// Uncomment for debugging
// log.Println("Command: ", cmd)
// log.Println("stdout: ", stdout.String())
// log.Println("stderr: ", stderr.String())
return stdout.String(), stderr.String(), ErrDockertestCommandFailed
}
return stdout.String(), stderr.String(), nil
case <-time.After(execConfig.timeout):
return stdout.String(), stderr.String(), ErrDockertestCommandTimeout
}
}

View File

@@ -1,62 +0,0 @@
package dockertestutil
import (
"errors"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
var ErrContainerNotFound = errors.New("container not found")
func GetFirstOrCreateNetwork(pool *dockertest.Pool, name string) (*dockertest.Network, error) {
networks, err := pool.NetworksByName(name)
if err != nil || len(networks) == 0 {
if _, err := pool.CreateNetwork(name); err == nil {
// Create does not give us an updated version of the resource, so we need to
// get it again.
networks, err := pool.NetworksByName(name)
if err != nil {
return nil, err
}
return &networks[0], nil
}
}
return &networks[0], nil
}
func AddContainerToNetwork(
pool *dockertest.Pool,
network *dockertest.Network,
testContainer string,
) error {
containers, err := pool.Client.ListContainers(docker.ListContainersOptions{
All: true,
Filters: map[string][]string{
"name": {testContainer},
},
})
if err != nil {
return err
}
err = pool.Client.ConnectNetwork(network.Network.ID, docker.NetworkConnectionOptions{
Container: containers[0].ID,
})
if err != nil {
return err
}
// TODO(kradalby): This doesnt work reliably, but calling the exact same functions
// seem to work fine...
// if container, ok := pool.ContainerByName("/" + testContainer); ok {
// err := container.ConnectToNetwork(network)
// if err != nil {
// return err
// }
// }
return nil
}

View File

@@ -1,346 +0,0 @@
package integration
import (
"fmt"
"strings"
"testing"
"time"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/rs/zerolog/log"
)
func TestPingAllByIP(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{
"namespace1": len(TailscaleVersions),
"namespace2": len(TailscaleVersions),
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("pingallbyip"))
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)
}
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++
}
}
}
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestPingAllByHostname(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{
// Omit 1.16.2 (-1) because it does not have the FQDN field
"namespace3": len(TailscaleVersions) - 1,
"namespace4": len(TailscaleVersions) - 1,
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("pingallbyname"))
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)
}
err = scenario.WaitForTailscaleSync()
if err != nil {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
allHostnames, err := scenario.ListTailscaleClientsFQDNs()
if err != nil {
t.Errorf("failed to get FQDNs: %s", err)
}
success := 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))
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestTaildrop(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
retry := func(times int, sleepInverval time.Duration, doWork func() error) error {
var err error
for attempts := 0; attempts < times; attempts++ {
err = doWork()
if err == nil {
return nil
}
time.Sleep(sleepInverval)
}
return err
}
scenario, err := NewScenario()
if err != nil {
t.Errorf("failed to create scenario: %s", err)
}
spec := map[string]int{
// Omit 1.16.2 (-1) because it does not have the FQDN field
"taildrop": len(TailscaleVersions) - 1,
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("taildrop"))
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)
}
err = scenario.WaitForTailscaleSync()
if err != nil {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
// This will essentially fetch and cache all the FQDNs
_, err = scenario.ListTailscaleClientsFQDNs()
if err != nil {
t.Errorf("failed to get FQDNs: %s", err)
}
for _, client := range allClients {
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", client.Hostname())}
if _, _, err := client.Execute(command); err != nil {
t.Errorf("failed to create taildrop file on %s, err: %s", client.Hostname(), err)
}
for _, peer := range allClients {
if client.Hostname() == peer.Hostname() {
continue
}
// It is safe to ignore this error as we handled it when caching it
peerFQDN, _ := peer.FQDN()
t.Run(fmt.Sprintf("%s-%s", client.Hostname(), peer.Hostname()), func(t *testing.T) {
command := []string{
"tailscale", "file", "cp",
fmt.Sprintf("/tmp/file_from_%s", client.Hostname()),
fmt.Sprintf("%s:", peerFQDN),
}
err := retry(10, 1*time.Second, func() error {
t.Logf(
"Sending file from %s to %s\n",
client.Hostname(),
peer.Hostname(),
)
_, _, err := client.Execute(command)
return err
})
if err != nil {
t.Errorf(
"failed to send taildrop file on %s, err: %s",
client.Hostname(),
err,
)
}
})
}
}
for _, client := range allClients {
command := []string{
"tailscale", "file",
"get",
"/tmp/",
}
if _, _, err := client.Execute(command); err != nil {
t.Errorf("failed to get taildrop file on %s, err: %s", client.Hostname(), err)
}
for _, peer := range allClients {
if client.Hostname() == peer.Hostname() {
continue
}
t.Run(fmt.Sprintf("%s-%s", client.Hostname(), peer.Hostname()), func(t *testing.T) {
command := []string{
"ls",
fmt.Sprintf("/tmp/file_from_%s", peer.Hostname()),
}
log.Printf(
"Checking file in %s from %s\n",
client.Hostname(),
peer.Hostname(),
)
result, _, err := client.Execute(command)
if err != nil {
t.Errorf("failed to execute command to ls taildrop: %s", err)
}
log.Printf("Result for %s: %s\n", peer.Hostname(), result)
if fmt.Sprintf("/tmp/file_from_%s\n", peer.Hostname()) != result {
t.Errorf(
"taildrop result is not correct %s, wanted %s",
result,
fmt.Sprintf("/tmp/file_from_%s\n", peer.Hostname()),
)
}
})
}
}
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestResolveMagicDNS(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{
// Omit 1.16.2 (-1) because it does not have the FQDN field
"magicdns1": len(TailscaleVersions) - 1,
"magicdns2": len(TailscaleVersions) - 1,
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("magicdns"))
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)
}
err = scenario.WaitForTailscaleSync()
if err != nil {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
// Poor mans cache
_, err = scenario.ListTailscaleClientsFQDNs()
if err != nil {
t.Errorf("failed to get FQDNs: %s", err)
}
_, err = scenario.ListTailscaleClientsIPs()
if err != nil {
t.Errorf("failed to get IPs: %s", err)
}
for _, client := range allClients {
for _, peer := range allClients {
// It is safe to ignore this error as we handled it when caching it
peerFQDN, _ := peer.FQDN()
command := []string{
"tailscale",
"ip", peerFQDN,
}
result, _, err := client.Execute(command)
if err != nil {
t.Errorf(
"failed to execute resolve/ip command %s from %s: %s",
peerFQDN,
client.Hostname(),
err,
)
}
ips, err := peer.IPs()
if err != nil {
t.Errorf(
"failed to get ips for %s: %s",
peer.Hostname(),
err,
)
}
for _, ip := range ips {
if !strings.Contains(result, ip.String()) {
t.Errorf("ip %s is not found in \n%s\n", ip.String(), result)
}
}
}
}
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}

View File

@@ -1,97 +0,0 @@
package hsic
// const (
// defaultEphemeralNodeInactivityTimeout = time.Second * 30
// defaultNodeUpdateCheckInterval = time.Second * 10
// )
// TODO(kradalby): This approach doesnt work because we cannot
// serialise our config object to YAML or JSON.
// func DefaultConfig() headscale.Config {
// derpMap, _ := url.Parse("https://controlplane.tailscale.com/derpmap/default")
//
// config := headscale.Config{
// Log: headscale.LogConfig{
// Level: zerolog.TraceLevel,
// },
// ACL: headscale.GetACLConfig(),
// DBtype: "sqlite3",
// EphemeralNodeInactivityTimeout: defaultEphemeralNodeInactivityTimeout,
// NodeUpdateCheckInterval: defaultNodeUpdateCheckInterval,
// IPPrefixes: []netip.Prefix{
// netip.MustParsePrefix("fd7a:115c:a1e0::/48"),
// netip.MustParsePrefix("100.64.0.0/10"),
// },
// DNSConfig: &tailcfg.DNSConfig{
// Proxied: true,
// Nameservers: []netip.Addr{
// netip.MustParseAddr("127.0.0.11"),
// netip.MustParseAddr("1.1.1.1"),
// },
// Resolvers: []*dnstype.Resolver{
// {
// Addr: "127.0.0.11",
// },
// {
// Addr: "1.1.1.1",
// },
// },
// },
// BaseDomain: "headscale.net",
//
// DBpath: "/tmp/integration_test_db.sqlite3",
//
// PrivateKeyPath: "/tmp/integration_private.key",
// NoisePrivateKeyPath: "/tmp/noise_integration_private.key",
// Addr: "0.0.0.0:8080",
// MetricsAddr: "127.0.0.1:9090",
// ServerURL: "http://headscale:8080",
//
// DERP: headscale.DERPConfig{
// URLs: []url.URL{
// *derpMap,
// },
// AutoUpdate: false,
// UpdateFrequency: 1 * time.Minute,
// },
// }
//
// return config
// }
// TODO: Reuse the actual configuration object above.
func DefaultConfigYAML() string {
yaml := `
log:
level: trace
acl_policy_path: ""
db_type: sqlite3
db_path: /tmp/integration_test_db.sqlite3
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
dns_config:
base_domain: headscale.net
magic_dns: true
domains: []
nameservers:
- 127.0.0.11
- 1.1.1.1
private_key_path: /tmp/private.key
noise:
private_key_path: /tmp/noise_private.key
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
server_url: http://headscale:8080
derp:
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: false
update_frequency: 1m
`
return yaml
}

View File

@@ -1,458 +0,0 @@
package hsic
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"log"
"math/big"
"net"
"net/http"
"time"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/ory/dockertest/v3"
)
const (
hsicHashLength = 6
dockerContextPath = "../."
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")
type HeadscaleInContainer struct {
hostname string
pool *dockertest.Pool
container *dockertest.Resource
network *dockertest.Network
// optional config
port int
aclPolicy *headscale.ACLPolicy
env []string
tlsCert []byte
tlsKey []byte
}
type Option = func(c *HeadscaleInContainer)
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
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
}
}
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.tlsCert = cert
hsic.tlsKey = key
}
}
func WithConfigEnv(configEnv map[string]string) Option {
return func(hsic *HeadscaleInContainer) {
for key, value := range configEnv {
hsic.env = append(hsic.env, fmt.Sprintf("%s=%s", key, value))
}
}
}
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 WithHostnameAsServerURL() Option {
return func(hsic *HeadscaleInContainer) {
hsic.env = append(
hsic.env,
fmt.Sprintf("HEADSCALE_SERVER_URL=http://%s:%d",
hsic.GetHostname(),
hsic.port,
))
}
}
func New(
pool *dockertest.Pool,
network *dockertest.Network,
opts ...Option,
) (*HeadscaleInContainer, error) {
hash, err := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
if err != nil {
return nil, err
}
hostname := fmt.Sprintf("hs-%s", hash)
hsic := &HeadscaleInContainer{
hostname: hostname,
port: headscaleDefaultPort,
pool: pool,
network: network,
}
for _, opt := range opts {
opt(hsic)
}
log.Println("NAME: ", hsic.hostname)
portProto := fmt.Sprintf("%d/tcp", hsic.port)
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.debug",
ContextDir: dockerContextPath,
}
runOptions := &dockertest.RunOptions{
Name: hsic.hostname,
ExposedPorts: []string{portProto},
Networks: []*dockertest.Network{network},
// Cmd: []string{"headscale", "serve"},
// TODO(kradalby): Get rid of this hack, we currently need to give us some
// to inject the headscale configuration further down.
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve"},
Env: hsic.env,
}
// dockertest isnt very good at handling containers that has already
// been created, this is an attempt to make sure this container isnt
// present.
err = pool.RemoveContainerByName(hsic.hostname)
if err != nil {
return nil, err
}
container, err := pool.BuildAndRunWithBuildOptions(
headscaleBuildOptions,
runOptions,
dockertestutil.DockerRestartPolicy,
dockertestutil.DockerAllowLocalIPv6,
dockertestutil.DockerAllowNetworkAdministration,
)
if err != nil {
return nil, fmt.Errorf("could not start headscale container: %w", err)
}
log.Printf("Created %s container\n", hsic.hostname)
hsic.container = container
err = hsic.WriteFile("/etc/headscale/config.yaml", []byte(DefaultConfigYAML()))
if err != nil {
return nil, fmt.Errorf("failed to write headscale config to container: %w", err)
}
if hsic.aclPolicy != nil {
data, err := json.Marshal(hsic.aclPolicy)
if err != nil {
return nil, fmt.Errorf("failed to marshal ACL Policy to JSON: %w", err)
}
err = hsic.WriteFile(aclPolicyPath, data)
if err != nil {
return nil, fmt.Errorf("failed to write ACL policy to container: %w", err)
}
}
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
}
func (t *HeadscaleInContainer) hasTLS() bool {
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
}
func (t *HeadscaleInContainer) Shutdown() error {
return t.pool.Purge(t.container)
}
func (t *HeadscaleInContainer) Execute(
command []string,
) (string, error) {
stdout, stderr, err := dockertestutil.ExecuteCommand(
t.container,
command,
[]string{},
)
if err != nil {
log.Printf("command stderr: %s\n", stderr)
if stdout != "" {
log.Printf("command stdout: %s\n", stdout)
}
return "", err
}
return stdout, nil
}
func (t *HeadscaleInContainer) GetIP() string {
return t.container.GetIPInNetwork(t.network)
}
func (t *HeadscaleInContainer) GetPort() string {
return fmt.Sprintf("%d", t.port)
}
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
return fmt.Sprintf("%s/health", t.GetEndpoint())
}
func (t *HeadscaleInContainer) GetEndpoint() string {
hostEndpoint := fmt.Sprintf("%s:%d",
t.GetIP(),
t.port)
if t.hasTLS() {
return fmt.Sprintf("https://%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 {
url := t.GetHealthEndpoint()
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 {
resp, err := client.Get(url) //nolint
if err != nil {
return fmt.Errorf("headscale is not ready: %w", err)
}
if resp.StatusCode != http.StatusOK {
return errHeadscaleStatusCodeNotOk
}
return nil
})
}
func (t *HeadscaleInContainer) CreateNamespace(
namespace string,
) error {
command := []string{"headscale", "namespaces", "create", namespace}
_, _, err := dockertestutil.ExecuteCommand(
t.container,
command,
[]string{},
)
if err != nil {
return err
}
return nil
}
func (t *HeadscaleInContainer) CreateAuthKey(
namespace string,
) (*v1.PreAuthKey, error) {
command := []string{
"headscale",
"--namespace",
namespace,
"preauthkeys",
"create",
"--reusable",
"--expiration",
"24h",
"--output",
"json",
}
result, _, err := dockertestutil.ExecuteCommand(
t.container,
command,
[]string{},
)
if err != nil {
return nil, fmt.Errorf("failed to execute create auth key command: %w", err)
}
var preAuthKey v1.PreAuthKey
err = json.Unmarshal([]byte(result), &preAuthKey)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal auth key: %w", err)
}
return &preAuthKey, nil
}
func (t *HeadscaleInContainer) ListMachinesInNamespace(
namespace string,
) ([]*v1.Machine, error) {
command := []string{"headscale", "--namespace", namespace, "nodes", "list", "--output", "json"}
result, _, err := dockertestutil.ExecuteCommand(
t.container,
command,
[]string{},
)
if err != nil {
return nil, fmt.Errorf("failed to execute list node command: %w", err)
}
var nodes []*v1.Machine
err = json.Unmarshal([]byte(result), &nodes)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal nodes: %w", err)
}
return nodes, nil
}
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
}
// nolint
func createCertificate() ([]byte, []byte, error) {
// From:
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
ca := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Headscale testing INC"},
Country: []string{"NL"},
Locality: []string{"Leiden"},
},
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 {
return nil, nil, err
}
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
}

View File

@@ -1,74 +0,0 @@
package integrationutil
import (
"archive/tar"
"bytes"
"fmt"
"io"
"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)
}
// 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
}

View File

@@ -1,470 +0,0 @@
package integration
import (
"errors"
"fmt"
"log"
"net/netip"
"os"
"sync"
"time"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/ory/dockertest/v3"
"github.com/puzpuzpuz/xsync/v2"
)
const (
scenarioHashLength = 6
maxWait = 60 * time.Second
)
var (
errNoHeadscaleAvailable = errors.New("no headscale available")
errNoNamespaceAvailable = errors.New("no namespace available")
// 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",
"unstable",
"1.32.1",
"1.30.2",
}
tailscaleVersions2019 = []string{
"1.28.0",
"1.26.2",
"1.24.2",
"1.22.2",
"1.20.4",
"1.18.2",
"1.16.2",
}
// tailscaleVersionsUnavailable = []string{
// // These versions seem to fail when fetching from apt.
// "1.14.6",
// "1.12.4",
// "1.10.2",
// "1.8.7",
// }.
TailscaleVersions = append(
tailscaleVersions2021,
tailscaleVersions2019...,
)
)
type Namespace struct {
Clients map[string]TailscaleClient
createWaitGroup sync.WaitGroup
joinWaitGroup sync.WaitGroup
syncWaitGroup sync.WaitGroup
}
// TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.
type Scenario struct {
// TODO(kradalby): support multiple headcales for later, currently only
// use one.
controlServers *xsync.MapOf[string, ControlServer]
namespaces map[string]*Namespace
pool *dockertest.Pool
network *dockertest.Network
headscaleLock sync.Mutex
}
func NewScenario() (*Scenario, error) {
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
if err != nil {
return nil, err
}
pool, err := dockertest.NewPool("")
if err != nil {
return nil, fmt.Errorf("could not connect to docker: %w", err)
}
pool.MaxWait = maxWait
networkName := fmt.Sprintf("hs-%s", hash)
if overrideNetworkName := os.Getenv("HEADSCALE_TEST_NETWORK_NAME"); overrideNetworkName != "" {
networkName = overrideNetworkName
}
network, err := dockertestutil.GetFirstOrCreateNetwork(pool, networkName)
if err != nil {
return nil, fmt.Errorf("failed to create or get network: %w", err)
}
// We run the test suite in a docker container that calls a couple of endpoints for
// readiness checks, this ensures that we can run the tests with individual networks
// and have the client reach the different containers
err = dockertestutil.AddContainerToNetwork(pool, network, "headscale-test-suite")
if err != nil {
return nil, fmt.Errorf("failed to add test suite container to network: %w", err)
}
return &Scenario{
controlServers: xsync.NewMapOf[ControlServer](),
namespaces: make(map[string]*Namespace),
pool: pool,
network: network,
}, nil
}
func (s *Scenario) Shutdown() error {
s.controlServers.Range(func(_ string, control ControlServer) bool {
err := control.Shutdown()
if err != nil {
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 _, client := range namespace.Clients {
log.Printf("removing client %s in namespace %s", client.Hostname(), namespaceName)
err := client.Shutdown()
if err != nil {
return fmt.Errorf("failed to tear down client: %w", err)
}
}
}
if err := s.pool.RemoveNetwork(s.network); err != nil {
return fmt.Errorf("failed to remove network: %w", err)
}
// TODO(kradalby): This seem redundant to the previous call
// if err := s.network.Close(); err != nil {
// return fmt.Errorf("failed to tear down network: %w", err)
// }
return nil
}
func (s *Scenario) Namespaces() []string {
namespaces := make([]string, 0)
for namespace := range s.namespaces {
namespaces = append(namespaces, namespace)
}
return namespaces
}
/// Headscale related stuff
// Note: These functions assume that there is a _single_ headscale instance for now
// TODO(kradalby): make port and headscale configurable, multiple instances support?
func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
s.headscaleLock.Lock()
defer s.headscaleLock.Unlock()
if headscale, ok := s.controlServers.Load("headscale"); ok {
return headscale, nil
}
headscale, err := hsic.New(s.pool, s.network, opts...)
if err != nil {
return nil, fmt.Errorf("failed to create headscale container: %w", err)
}
err = headscale.WaitForReady()
if err != nil {
return nil, fmt.Errorf("failed reach headscale container: %w", err)
}
s.controlServers.Store("headscale", headscale)
return headscale, nil
}
func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
if headscale, err := s.Headscale(); err == nil {
key, err := headscale.CreateAuthKey(namespace)
if err != nil {
return nil, fmt.Errorf("failed to create namespace: %w", err)
}
return key, nil
}
return nil, fmt.Errorf("failed to create namespace: %w", errNoHeadscaleAvailable)
}
func (s *Scenario) CreateNamespace(namespace string) error {
if headscale, err := s.Headscale(); err == nil {
err := headscale.CreateNamespace(namespace)
if err != nil {
return fmt.Errorf("failed to create namespace: %w", err)
}
s.namespaces[namespace] = &Namespace{
Clients: make(map[string]TailscaleClient),
}
return nil
}
return fmt.Errorf("failed to create namespace: %w", errNoHeadscaleAvailable)
}
/// Client related stuff
func (s *Scenario) CreateTailscaleNodesInNamespace(
namespaceStr string,
requestedVersion string,
count int,
opts ...tsic.Option,
) error {
if namespace, ok := s.namespaces[namespaceStr]; ok {
for i := 0; i < count; i++ {
version := requestedVersion
if requestedVersion == "all" {
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)
opts = append(opts,
tsic.WithHeadscaleTLS(cert),
tsic.WithHeadscaleName(hostname),
)
go func() {
defer namespace.createWaitGroup.Done()
// TODO(kradalby): error handle this
tsClient, err := tsic.New(
s.pool,
version,
s.network,
opts...,
)
if err != nil {
// return fmt.Errorf("failed to add tailscale node: %w", err)
log.Printf("failed to create tailscale node: %s", err)
}
err = tsClient.WaitForReady()
if err != nil {
// return fmt.Errorf("failed to add tailscale node: %w", err)
log.Printf("failed to wait for tailscaled: %s", err)
}
namespace.Clients[tsClient.Hostname()] = tsClient
}()
}
namespace.createWaitGroup.Wait()
return nil
}
return fmt.Errorf("failed to add tailscale node: %w", errNoNamespaceAvailable)
}
func (s *Scenario) RunTailscaleUp(
namespaceStr, loginServer, authKey string,
) error {
if namespace, ok := s.namespaces[namespaceStr]; ok {
for _, client := range namespace.Clients {
namespace.joinWaitGroup.Add(1)
go func(c TailscaleClient) {
defer namespace.joinWaitGroup.Done()
// TODO(kradalby): error handle this
_ = c.Up(loginServer, authKey)
}(client)
err := client.WaitForReady()
if err != nil {
log.Printf("error waiting for client %s to be ready: %s", client.Hostname(), err)
}
}
namespace.joinWaitGroup.Wait()
return nil
}
return fmt.Errorf("failed to up tailscale node: %w", errNoNamespaceAvailable)
}
func (s *Scenario) CountTailscale() int {
count := 0
for _, namespace := range s.namespaces {
count += len(namespace.Clients)
}
return count
}
func (s *Scenario) WaitForTailscaleSync() error {
tsCount := s.CountTailscale()
for _, namespace := range s.namespaces {
for _, client := range namespace.Clients {
namespace.syncWaitGroup.Add(1)
go func(c TailscaleClient) {
defer namespace.syncWaitGroup.Done()
// TODO(kradalby): error handle this
_ = c.WaitForPeers(tsCount)
}(client)
}
namespace.syncWaitGroup.Wait()
}
return nil
}
// CreateHeadscaleEnv is a conventient method returning a set up Headcale
// test environment with nodes of all versions, joined to the server with X
// namespaces.
func (s *Scenario) CreateHeadscaleEnv(
namespaces map[string]int,
tsOpts []tsic.Option,
opts ...hsic.Option,
) error {
headscale, err := s.Headscale(opts...)
if err != nil {
return err
}
for namespaceName, clientCount := range namespaces {
err = s.CreateNamespace(namespaceName)
if err != nil {
return err
}
err = s.CreateTailscaleNodesInNamespace(namespaceName, "all", clientCount, tsOpts...)
if err != nil {
return err
}
key, err := s.CreatePreAuthKey(namespaceName)
if err != nil {
return err
}
err = s.RunTailscaleUp(namespaceName, headscale.GetEndpoint(), key.GetKey())
if err != nil {
return err
}
}
return nil
}
func (s *Scenario) GetIPs(namespace string) ([]netip.Addr, error) {
var ips []netip.Addr
if ns, ok := s.namespaces[namespace]; ok {
for _, client := range ns.Clients {
clientIps, err := client.IPs()
if err != nil {
return ips, fmt.Errorf("failed to get ips: %w", err)
}
ips = append(ips, clientIps...)
}
return ips, nil
}
return ips, fmt.Errorf("failed to get ips: %w", errNoNamespaceAvailable)
}
func (s *Scenario) GetClients(namespace string) ([]TailscaleClient, error) {
var clients []TailscaleClient
if ns, ok := s.namespaces[namespace]; ok {
for _, client := range ns.Clients {
clients = append(clients, client)
}
return clients, nil
}
return clients, fmt.Errorf("failed to get clients: %w", errNoNamespaceAvailable)
}
func (s *Scenario) ListTailscaleClients(namespaces ...string) ([]TailscaleClient, error) {
var allClients []TailscaleClient
if len(namespaces) == 0 {
namespaces = s.Namespaces()
}
for _, namespace := range namespaces {
clients, err := s.GetClients(namespace)
if err != nil {
return nil, err
}
allClients = append(allClients, clients...)
}
return allClients, nil
}
func (s *Scenario) ListTailscaleClientsIPs(namespaces ...string) ([]netip.Addr, error) {
var allIps []netip.Addr
if len(namespaces) == 0 {
namespaces = s.Namespaces()
}
for _, namespace := range namespaces {
ips, err := s.GetIPs(namespace)
if err != nil {
return nil, err
}
allIps = append(allIps, ips...)
}
return allIps, nil
}
func (s *Scenario) ListTailscaleClientsFQDNs(namespaces ...string) ([]string, error) {
allFQDNs := make([]string, 0)
clients, err := s.ListTailscaleClients(namespaces...)
if err != nil {
return nil, err
}
for _, client := range clients {
fqdn, err := client.FQDN()
if err != nil {
return nil, err
}
allFQDNs = append(allFQDNs, fqdn)
}
return allFQDNs, nil
}

View File

@@ -1,192 +0,0 @@
package integration
import (
"testing"
"github.com/juanfont/headscale/integration/dockertestutil"
)
// This file is intended to "test the test framework", by proxy it will also test
// some Headcsale/Tailscale stuff, but mostly in very simple ways.
func IntegrationSkip(t *testing.T) {
t.Helper()
if !dockertestutil.IsRunningInContainer() {
t.Skip("not running in docker, skipping")
}
if testing.Short() {
t.Skip("skipping integration tests due to short flag")
}
}
func TestHeadscale(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
var err error
namespace := "test-space"
scenario, err := NewScenario()
if err != nil {
t.Errorf("failed to create scenario: %s", err)
}
t.Run("start-headscale", func(t *testing.T) {
headscale, err := scenario.Headscale()
if err != nil {
t.Errorf("failed to create start headcale: %s", err)
}
err = headscale.WaitForReady()
if err != nil {
t.Errorf("headscale failed to become ready: %s", err)
}
})
t.Run("create-namespace", func(t *testing.T) {
err := scenario.CreateNamespace(namespace)
if err != nil {
t.Errorf("failed to create namespace: %s", err)
}
if _, ok := scenario.namespaces[namespace]; !ok {
t.Errorf("namespace is not in scenario")
}
})
t.Run("create-auth-key", func(t *testing.T) {
_, err := scenario.CreatePreAuthKey(namespace)
if err != nil {
t.Errorf("failed to create preauthkey: %s", err)
}
})
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestCreateTailscale(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
namespace := "only-create-containers"
scenario, err := NewScenario()
if err != nil {
t.Errorf("failed to create scenario: %s", err)
}
scenario.namespaces[namespace] = &Namespace{
Clients: make(map[string]TailscaleClient),
}
t.Run("create-tailscale", func(t *testing.T) {
err := scenario.CreateTailscaleNodesInNamespace(namespace, "all", 3)
if err != nil {
t.Errorf("failed to add tailscale nodes: %s", err)
}
if clients := len(scenario.namespaces[namespace].Clients); clients != 3 {
t.Errorf("wrong number of tailscale clients: %d != %d", clients, 3)
}
// TODO(kradalby): Test "all" version logic
})
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
var err error
namespace := "join-node-test"
count := 1
scenario, err := NewScenario()
if err != nil {
t.Errorf("failed to create scenario: %s", err)
}
t.Run("start-headscale", func(t *testing.T) {
headscale, err := scenario.Headscale()
if err != nil {
t.Errorf("failed to create start headcale: %s", err)
}
err = headscale.WaitForReady()
if err != nil {
t.Errorf("headscale failed to become ready: %s", err)
}
})
t.Run("create-namespace", func(t *testing.T) {
err := scenario.CreateNamespace(namespace)
if err != nil {
t.Errorf("failed to create namespace: %s", err)
}
if _, ok := scenario.namespaces[namespace]; !ok {
t.Errorf("namespace is not in scenario")
}
})
t.Run("create-tailscale", func(t *testing.T) {
err := scenario.CreateTailscaleNodesInNamespace(namespace, "1.30.2", count)
if err != nil {
t.Errorf("failed to add tailscale nodes: %s", err)
}
if clients := len(scenario.namespaces[namespace].Clients); clients != count {
t.Errorf("wrong number of tailscale clients: %d != %d", clients, count)
}
})
t.Run("join-headscale", func(t *testing.T) {
key, err := scenario.CreatePreAuthKey(namespace)
if err != nil {
t.Errorf("failed to create preauthkey: %s", err)
}
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 {
t.Errorf("failed to login: %s", err)
}
})
t.Run("get-ips", func(t *testing.T) {
ips, err := scenario.GetIPs(namespace)
if err != nil {
t.Errorf("failed to get tailscale ips: %s", err)
}
if len(ips) != count*2 {
t.Errorf("got the wrong amount of tailscale ips, %d != %d", len(ips), count*2)
}
})
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}

View File

@@ -1,519 +0,0 @@
package integration
import (
"fmt"
"strings"
"testing"
"time"
"github.com/juanfont/headscale"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/stretchr/testify/assert"
)
var retry = func(times int, sleepInterval time.Duration,
doWork func() (string, string, error),
) (string, string, error) {
var result string
var stderr string
var err error
for attempts := 0; attempts < times; attempts++ {
tempResult, tempStderr, err := doWork()
result += tempResult
stderr += tempStderr
if err == nil {
return result, stderr, nil
}
// If we get a permission denied error, we can fail immediately
// since that is something we wont recover from by retrying.
if err != nil && strings.Contains(stderr, "Permission denied (tailscale)") {
return result, stderr, err
}
time.Sleep(sleepInterval)
}
return result, stderr, err
}
func TestSSHOneNamespaceAllToAll(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{
"namespace1": len(TailscaleVersions) - 5,
}
err = scenario.CreateHeadscaleEnv(spec,
[]tsic.Option{tsic.WithSSH()},
hsic.WithACLPolicy(
&headscale.ACLPolicy{
Groups: map[string][]string{
"group:integration-test": {"namespace1"},
},
ACLs: []headscale.ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
SSHs: []headscale.SSH{
{
Action: "accept",
Sources: []string{"group:integration-test"},
Destinations: []string{"group:integration-test"},
Users: []string{"ssh-it-user"},
},
},
},
),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1",
}),
)
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)
}
err = scenario.WaitForTailscaleSync()
if err != nil {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
_, err = scenario.ListTailscaleClientsFQDNs()
if err != nil {
t.Errorf("failed to get FQDNs: %s", err)
}
for _, client := range allClients {
for _, peer := range allClients {
if client.Hostname() == peer.Hostname() {
continue
}
assertSSHHostname(t, client, peer)
}
}
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestSSHMultipleNamespacesAllToAll(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{
"namespace1": len(TailscaleVersions) - 5,
"namespace2": len(TailscaleVersions) - 5,
}
err = scenario.CreateHeadscaleEnv(spec,
[]tsic.Option{tsic.WithSSH()},
hsic.WithACLPolicy(
&headscale.ACLPolicy{
Groups: map[string][]string{
"group:integration-test": {"namespace1", "namespace2"},
},
ACLs: []headscale.ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
SSHs: []headscale.SSH{
{
Action: "accept",
Sources: []string{"group:integration-test"},
Destinations: []string{"group:integration-test"},
Users: []string{"ssh-it-user"},
},
},
},
),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1",
}),
)
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
nsOneClients, err := scenario.ListTailscaleClients("namespace1")
if err != nil {
t.Errorf("failed to get clients: %s", err)
}
nsTwoClients, err := scenario.ListTailscaleClients("namespace2")
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)
}
_, err = scenario.ListTailscaleClientsFQDNs()
if err != nil {
t.Errorf("failed to get FQDNs: %s", err)
}
testInterNamespaceSSH := func(sourceClients []TailscaleClient, targetClients []TailscaleClient) {
for _, client := range sourceClients {
for _, peer := range targetClients {
assertSSHHostname(t, client, peer)
}
}
}
testInterNamespaceSSH(nsOneClients, nsTwoClients)
testInterNamespaceSSH(nsTwoClients, nsOneClients)
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestSSHNoSSHConfigured(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{
"namespace1": len(TailscaleVersions) - 5,
}
err = scenario.CreateHeadscaleEnv(spec,
[]tsic.Option{tsic.WithSSH()},
hsic.WithACLPolicy(
&headscale.ACLPolicy{
Groups: map[string][]string{
"group:integration-test": {"namespace1"},
},
ACLs: []headscale.ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
SSHs: []headscale.SSH{},
},
),
hsic.WithTestName("sshnoneconfigured"),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1",
}),
)
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)
}
err = scenario.WaitForTailscaleSync()
if err != nil {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
_, err = scenario.ListTailscaleClientsFQDNs()
if err != nil {
t.Errorf("failed to get FQDNs: %s", err)
}
for _, client := range allClients {
for _, peer := range allClients {
if client.Hostname() == peer.Hostname() {
continue
}
assertSSHPermissionDenied(t, client, peer)
}
}
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestSSHIsBlockedInACL(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{
"namespace1": len(TailscaleVersions) - 5,
}
err = scenario.CreateHeadscaleEnv(spec,
[]tsic.Option{tsic.WithSSH()},
hsic.WithACLPolicy(
&headscale.ACLPolicy{
Groups: map[string][]string{
"group:integration-test": {"namespace1"},
},
ACLs: []headscale.ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:80"},
},
},
SSHs: []headscale.SSH{
{
Action: "accept",
Sources: []string{"group:integration-test"},
Destinations: []string{"group:integration-test"},
Users: []string{"ssh-it-user"},
},
},
},
),
hsic.WithTestName("sshisblockedinacl"),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1",
}),
)
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)
}
err = scenario.WaitForTailscaleSync()
if err != nil {
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
}
_, err = scenario.ListTailscaleClientsFQDNs()
if err != nil {
t.Errorf("failed to get FQDNs: %s", err)
}
for _, client := range allClients {
for _, peer := range allClients {
if client.Hostname() == peer.Hostname() {
continue
}
assertSSHTimeout(t, client, peer)
}
}
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func TestSSNamespaceOnlyIsolation(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{
"namespaceacl1": len(TailscaleVersions) - 5,
"namespaceacl2": len(TailscaleVersions) - 5,
}
err = scenario.CreateHeadscaleEnv(spec,
[]tsic.Option{tsic.WithSSH()},
hsic.WithACLPolicy(
&headscale.ACLPolicy{
Groups: map[string][]string{
"group:ssh1": {"namespaceacl1"},
"group:ssh2": {"namespaceacl2"},
},
ACLs: []headscale.ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
SSHs: []headscale.SSH{
{
Action: "accept",
Sources: []string{"group:ssh1"},
Destinations: []string{"group:ssh1"},
Users: []string{"ssh-it-user"},
},
{
Action: "accept",
Sources: []string{"group:ssh2"},
Destinations: []string{"group:ssh2"},
Users: []string{"ssh-it-user"},
},
},
},
),
hsic.WithTestName("sshtwonamespaceaclblock"),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1",
}),
)
if err != nil {
t.Errorf("failed to create headscale environment: %s", err)
}
ssh1Clients, err := scenario.ListTailscaleClients("namespaceacl1")
if err != nil {
t.Errorf("failed to get clients: %s", err)
}
ssh2Clients, err := scenario.ListTailscaleClients("namespaceacl2")
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)
}
_, err = scenario.ListTailscaleClientsFQDNs()
if err != nil {
t.Errorf("failed to get FQDNs: %s", err)
}
// TODO(kradalby,evenh): ACLs do currently not cover reject
// cases properly, and currently will accept all incomming connections
// as long as a rule is present.
//
// for _, client := range ssh1Clients {
// for _, peer := range ssh2Clients {
// if client.Hostname() == peer.Hostname() {
// continue
// }
//
// assertSSHPermissionDenied(t, client, peer)
// }
// }
//
// for _, client := range ssh2Clients {
// for _, peer := range ssh1Clients {
// if client.Hostname() == peer.Hostname() {
// continue
// }
//
// assertSSHPermissionDenied(t, client, peer)
// }
// }
for _, client := range ssh1Clients {
for _, peer := range ssh1Clients {
if client.Hostname() == peer.Hostname() {
continue
}
assertSSHHostname(t, client, peer)
}
}
for _, client := range ssh2Clients {
for _, peer := range ssh2Clients {
if client.Hostname() == peer.Hostname() {
continue
}
assertSSHHostname(t, client, peer)
}
}
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func doSSH(t *testing.T, client TailscaleClient, peer TailscaleClient) (string, string, error) {
t.Helper()
peerFQDN, _ := peer.FQDN()
command := []string{
"ssh", "-o StrictHostKeyChecking=no", "-o ConnectTimeout=1",
fmt.Sprintf("%s@%s", "ssh-it-user", peerFQDN),
"'hostname'",
}
return retry(10, 1*time.Second, func() (string, string, error) {
return client.Execute(command)
})
}
func assertSSHHostname(t *testing.T, client TailscaleClient, peer TailscaleClient) {
t.Helper()
result, _, err := doSSH(t, client, peer)
assert.NoError(t, err)
assert.Contains(t, peer.ID(), strings.ReplaceAll(result, "\n", ""))
}
func assertSSHPermissionDenied(t *testing.T, client TailscaleClient, peer TailscaleClient) {
t.Helper()
result, stderr, err := doSSH(t, client, peer)
assert.Error(t, err)
assert.Empty(t, result)
assert.Contains(t, stderr, "Permission denied (tailscale)")
}
func assertSSHTimeout(t *testing.T, client TailscaleClient, peer TailscaleClient) {
t.Helper()
result, stderr, err := doSSH(t, client, peer)
assert.NoError(t, err)
assert.Empty(t, result)
assert.Contains(t, stderr, "Connection timed out")
}

View File

@@ -1,25 +0,0 @@
package integration
import (
"net/netip"
"net/url"
"tailscale.com/ipn/ipnstate"
)
//nolint
type TailscaleClient interface {
Hostname() string
Shutdown() error
Version() string
Execute(command []string) (string, string, error)
Up(loginServer, authKey string) error
UpWithLoginURL(loginServer string) (*url.URL, error)
IPs() ([]netip.Addr, error)
FQDN() (string, error)
Status() (*ipnstate.Status, error)
WaitForReady() error
WaitForPeers(expected int) error
Ping(hostnameOrIP string) error
ID() string
}

View File

@@ -1,445 +0,0 @@
package tsic
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/netip"
"net/url"
"strings"
"github.com/cenkalti/backoff/v4"
"github.com/juanfont/headscale"
"github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"tailscale.com/ipn/ipnstate"
)
const (
tsicHashLength = 6
dockerContextPath = "../."
headscaleCertPath = "/usr/local/share/ca-certificates/headscale.crt"
)
var (
errTailscalePingFailed = errors.New("ping failed")
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
errTailscaleWrongPeerCount = errors.New("wrong peer count")
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
errTailscaleNotConnected = errors.New("tailscale not connected")
)
type TailscaleInContainer struct {
version string
hostname string
pool *dockertest.Pool
container *dockertest.Resource
network *dockertest.Network
// "cache"
ips []netip.Addr
fqdn string
// optional config
headscaleCert []byte
headscaleHostname string
withSSH bool
}
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 WithSSH() Option {
return func(tsic *TailscaleInContainer) {
tsic.withSSH = true
}
}
func New(
pool *dockertest.Pool,
version string,
network *dockertest.Network,
opts ...Option,
) (*TailscaleInContainer, error) {
hash, err := headscale.GenerateRandomStringDNSSafe(tsicHashLength)
if err != nil {
return nil, err
}
hostname := fmt.Sprintf("ts-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
tsic := &TailscaleInContainer{
version: version,
hostname: hostname,
pool: pool,
network: network,
}
for _, opt := range opts {
opt(tsic)
}
tailscaleOptions := &dockertest.RunOptions{
Name: hostname,
Networks: []*dockertest.Network{network},
// Cmd: []string{
// "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
// been created, this is an attempt to make sure this container isnt
// present.
err = pool.RemoveContainerByName(hostname)
if err != nil {
return nil, err
}
container, err := pool.BuildAndRunWithBuildOptions(
createTailscaleBuildOptions(version),
tailscaleOptions,
dockertestutil.DockerRestartPolicy,
dockertestutil.DockerAllowLocalIPv6,
dockertestutil.DockerAllowNetworkAdministration,
)
if err != nil {
return nil, fmt.Errorf("could not start tailscale container: %w", err)
}
log.Printf("Created %s container\n", hostname)
tsic.container = container
if tsic.hasTLS() {
err = tsic.WriteFile(headscaleCertPath, tsic.headscaleCert)
if err != 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 {
return t.pool.Purge(t.container)
}
func (t *TailscaleInContainer) Hostname() string {
return t.hostname
}
func (t *TailscaleInContainer) Version() string {
return t.version
}
func (t *TailscaleInContainer) ID() string {
return t.container.Container.ID
}
func (t *TailscaleInContainer) Execute(
command []string,
) (string, string, error) {
stdout, stderr, err := dockertestutil.ExecuteCommand(
t.container,
command,
[]string{},
)
if err != nil {
log.Printf("command stderr: %s\n", stderr)
if stdout != "" {
log.Printf("command stdout: %s\n", stdout)
}
if strings.Contains(stderr, "NeedsLogin") {
return stdout, stderr, errTailscaleNotLoggedIn
}
return stdout, stderr, err
}
return stdout, stderr, nil
}
func (t *TailscaleInContainer) Up(
loginServer, authKey string,
) error {
command := []string{
"tailscale",
"up",
"-login-server",
loginServer,
"--authkey",
authKey,
"--hostname",
t.hostname,
}
if t.withSSH {
command = append(command, "--ssh")
}
if _, _, err := t.Execute(command); err != nil {
return fmt.Errorf("failed to join tailscale client: %w", err)
}
return nil
}
func (t *TailscaleInContainer) UpWithLoginURL(
loginServer string,
) (*url.URL, error) {
command := []string{
"tailscale",
"up",
"-login-server",
loginServer,
"--hostname",
t.hostname,
}
_, stderr, err := t.Execute(command)
if errors.Is(err, errTailscaleNotLoggedIn) {
return nil, errTailscaleCannotUpWithoutAuthkey
}
urlStr := strings.ReplaceAll(stderr, "\nTo authenticate, visit:\n\n\t", "")
urlStr = strings.TrimSpace(urlStr)
// parse URL
loginURL, err := url.Parse(urlStr)
if err != nil {
log.Printf("Could not parse login URL: %s", err)
log.Printf("Original join command result: %s", stderr)
return nil, err
}
return loginURL, nil
}
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
if t.ips != nil && len(t.ips) != 0 {
return t.ips, nil
}
ips := make([]netip.Addr, 0)
command := []string{
"tailscale",
"ip",
}
result, _, err := t.Execute(command)
if err != nil {
return []netip.Addr{}, fmt.Errorf("failed to join tailscale client: %w", err)
}
for _, address := range strings.Split(result, "\n") {
address = strings.TrimSuffix(address, "\n")
if len(address) < 1 {
continue
}
ip, err := netip.ParseAddr(address)
if err != nil {
return nil, err
}
ips = append(ips, ip)
}
return ips, nil
}
func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
command := []string{
"tailscale",
"status",
"--json",
}
result, _, err := t.Execute(command)
if err != nil {
return nil, fmt.Errorf("failed to execute tailscale status command: %w", err)
}
var status ipnstate.Status
err = json.Unmarshal([]byte(result), &status)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal tailscale status: %w", err)
}
return &status, err
}
func (t *TailscaleInContainer) FQDN() (string, error) {
if t.fqdn != "" {
return t.fqdn, nil
}
status, err := t.Status()
if err != nil {
return "", fmt.Errorf("failed to get FQDN: %w", err)
}
return status.Self.DNSName, nil
}
func (t *TailscaleInContainer) WaitForReady() error {
return t.pool.Retry(func() error {
status, err := t.Status()
if err != nil {
return fmt.Errorf("failed to fetch tailscale status: %w", err)
}
if status.CurrentTailnet != nil {
return nil
}
return errTailscaleNotConnected
})
}
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
return t.pool.Retry(func() error {
status, err := t.Status()
if err != nil {
return fmt.Errorf("failed to fetch tailscale status: %w", err)
}
if peers := status.Peers(); len(peers) != expected {
return errTailscaleWrongPeerCount
}
return nil
})
}
// TODO(kradalby): Make multiping, go routine magic.
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(
"failed to run ping command from %s to %s, err: %s",
t.Hostname(),
hostnameOrIP,
err,
)
return err
}
if !strings.Contains(result, "pong") && !strings.Contains(result, "is local") {
return backoff.Permanent(errTailscalePingFailed)
}
return nil
})
}
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
}
func createTailscaleBuildOptions(version string) *dockertest.BuildOptions {
var tailscaleBuildOptions *dockertest.BuildOptions
switch version {
case "head":
tailscaleBuildOptions = &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tailscale-HEAD",
ContextDir: dockerContextPath,
BuildArgs: []docker.BuildArg{},
}
case "unstable":
tailscaleBuildOptions = &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tailscale",
ContextDir: dockerContextPath,
BuildArgs: []docker.BuildArg{
{
Name: "TAILSCALE_VERSION",
Value: "*", // Installs the latest version https://askubuntu.com/a/824926
},
{
Name: "TAILSCALE_CHANNEL",
Value: "unstable",
},
},
}
default:
tailscaleBuildOptions = &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tailscale",
ContextDir: dockerContextPath,
BuildArgs: []docker.BuildArg{
{
Name: "TAILSCALE_VERSION",
Value: version,
},
{
Name: "TAILSCALE_CHANNEL",
Value: "stable",
},
},
}
}
return tailscaleBuildOptions
}

View File

@@ -1,4 +1,6 @@
// nolint //go:build integration
// +build integration
package headscale package headscale
import ( import (
@@ -12,7 +14,6 @@ import (
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@@ -27,11 +28,7 @@ type IntegrationCLITestSuite struct {
env []string env []string
} }
func TestIntegrationCLITestSuite(t *testing.T) { func TestCLIIntegrationTestSuite(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration tests due to short flag")
}
s := new(IntegrationCLITestSuite) s := new(IntegrationCLITestSuite)
suite.Run(t, s) suite.Run(t, s)
@@ -46,14 +43,14 @@ func (s *IntegrationCLITestSuite) SetupTest() {
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "") s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
} }
network, err := GetFirstOrCreateNetwork(&s.pool, headscaleNetwork) if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil {
if err != nil { s.network = *pnetwork
s.FailNow(fmt.Sprintf("Failed to create or get network: %s", err), "") } else {
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
} }
s.network = network
headscaleBuildOptions := &dockertest.BuildOptions{ headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile", Dockerfile: "Dockerfile.tmp-integration",
ContextDir: ".", ContextDir: ".",
} }
@@ -67,43 +64,30 @@ func (s *IntegrationCLITestSuite) SetupTest() {
Mounts: []string{ Mounts: []string{
fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath), fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath),
}, },
Cmd: []string{"headscale", "serve"}, Networks: []*dockertest.Network{&s.network},
Networks: []*dockertest.Network{&s.network}, Cmd: []string{"headscale", "serve"},
ExposedPorts: []string{"8080/tcp"},
PortBindings: map[docker.Port][]docker.PortBinding{
"8080/tcp": {{HostPort: "8080"}},
},
} }
err = s.pool.RemoveContainerByName(headscaleHostname) err = s.pool.RemoveContainerByName(headscaleHostname)
if err != nil { if err != nil {
s.FailNow( s.FailNow(fmt.Sprintf("Could not remove existing container before building test: %s", err), "")
fmt.Sprintf(
"Could not remove existing container before building test: %s",
err,
),
"",
)
} }
fmt.Println("Creating headscale container for CLI tests") fmt.Println("Creating headscale container")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil { if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale s.headscale = *pheadscale
} else { } else {
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "") s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
} }
fmt.Println("Created headscale container for CLI tests") fmt.Println("Created headscale container")
fmt.Println("Waiting for headscale to be ready for CLI tests") fmt.Println("Waiting for headscale to be ready")
hostEndpoint := fmt.Sprintf("%s:%s", hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8080/tcp"))
s.headscale.GetIPInNetwork(&s.network),
s.headscale.GetPort("8080/tcp"))
if err := s.pool.Retry(func() error { if err := s.pool.Retry(func() error {
url := fmt.Sprintf("http://%s/health", hostEndpoint) url := fmt.Sprintf("http://%s/health", hostEndpoint)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
fmt.Printf("headscale for CLI test is not ready: %s\n", err)
return err return err
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -118,7 +102,7 @@ func (s *IntegrationCLITestSuite) SetupTest() {
// https://github.com/stretchr/testify/issues/849 // https://github.com/stretchr/testify/issues/849
return // fmt.Errorf("Could not connect to headscale: %s", err) return // fmt.Errorf("Could not connect to headscale: %s", err)
} }
fmt.Println("headscale container is ready for CLI tests") fmt.Println("headscale container is ready")
} }
func (s *IntegrationCLITestSuite) TearDownTest() { func (s *IntegrationCLITestSuite) TearDownTest() {
@@ -139,7 +123,7 @@ func (s *IntegrationCLITestSuite) HandleStats(
} }
func (s *IntegrationCLITestSuite) createNamespace(name string) (*v1.Namespace, error) { func (s *IntegrationCLITestSuite) createNamespace(name string) (*v1.Namespace, error) {
result, _, err := ExecuteCommand( result, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -182,7 +166,7 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
assert.Equal(s.T(), names[2], namespaces[2].Name) assert.Equal(s.T(), names[2], namespaces[2].Name)
// Test list namespaces // Test list namespaces
listResult, _, err := ExecuteCommand( listResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -204,7 +188,7 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
assert.Equal(s.T(), names[2], listedNamespaces[2].Name) assert.Equal(s.T(), names[2], listedNamespaces[2].Name)
// Test rename namespace // Test rename namespace
renameResult, _, err := ExecuteCommand( renameResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -226,7 +210,7 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
assert.Equal(s.T(), renamedNamespace.Name, "newname") assert.Equal(s.T(), renamedNamespace.Name, "newname")
// Test list after rename namespaces // Test list after rename namespaces
listAfterRenameResult, _, err := ExecuteCommand( listAfterRenameResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -257,7 +241,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
preAuthResult, _, err := ExecuteCommand( preAuthResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -270,8 +254,6 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
"24h", "24h",
"--output", "--output",
"json", "json",
"--tags",
"tag:test1,tag:test2",
}, },
[]string{}, []string{},
) )
@@ -287,7 +269,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
assert.Len(s.T(), keys, 5) assert.Len(s.T(), keys, 5)
// Test list of keys // Test list of keys
listResult, _, err := ExecuteCommand( listResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -345,14 +327,9 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
listedPreAuthKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), listedPreAuthKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
) )
// Test that tags are present
for i := 0; i < count; i++ {
assert.Equal(s.T(), listedPreAuthKeys[i].AclTags, []string{"tag:test1", "tag:test2"})
}
// Expire three keys // Expire three keys
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -368,7 +345,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
} }
// Test list pre auth keys after expire // Test list pre auth keys after expire
listAfterExpireResult, _, err := ExecuteCommand( listAfterExpireResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -413,7 +390,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandWithoutExpiry() {
namespace, err := s.createNamespace("pre-auth-key-without-exp-namespace") namespace, err := s.createNamespace("pre-auth-key-without-exp-namespace")
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
preAuthResult, _, err := ExecuteCommand( preAuthResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -434,7 +411,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandWithoutExpiry() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
// Test list of keys // Test list of keys
listResult, _, err := ExecuteCommand( listResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -466,7 +443,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
namespace, err := s.createNamespace("pre-auth-key-reus-ephm-namespace") namespace, err := s.createNamespace("pre-auth-key-reus-ephm-namespace")
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
preAuthReusableResult, _, err := ExecuteCommand( preAuthReusableResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -489,7 +466,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
assert.True(s.T(), preAuthReusableKey.GetReusable()) assert.True(s.T(), preAuthReusableKey.GetReusable())
assert.False(s.T(), preAuthReusableKey.GetEphemeral()) assert.False(s.T(), preAuthReusableKey.GetEphemeral())
preAuthEphemeralResult, _, err := ExecuteCommand( preAuthEphemeralResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -531,7 +508,7 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() {
// assert.NotNil(s.T(), err) // assert.NotNil(s.T(), err)
// Test list of keys // Test list of keys
listResult, _, err := ExecuteCommand( listResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -558,14 +535,14 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
machineKeys := []string{ machineKeys := []string{
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", "9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", "6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
} }
machines := make([]*v1.Machine, len(machineKeys)) machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for index, machineKey := range machineKeys { for index, machineKey := range machineKeys {
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -584,7 +561,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
machineResult, _, err := ExecuteCommand( machineResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -609,7 +586,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
} }
assert.Len(s.T(), machines, len(machineKeys)) assert.Len(s.T(), machines, len(machineKeys))
addTagResult, _, err := ExecuteCommand( addTagResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -629,7 +606,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
assert.Equal(s.T(), []string{"tag:test"}, machine.ForcedTags) assert.Equal(s.T(), []string{"tag:test"}, machine.ForcedTags)
// try to set a wrong tag and retrieve the error // try to set a wrong tag and retrieve the error
wrongTagResult, _, err := ExecuteCommand( wrongTagResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -651,7 +628,7 @@ func (s *IntegrationCLITestSuite) TestNodeTagCommand() {
assert.Contains(s.T(), errorOutput.Error, "tag must start with the string 'tag:'") assert.Contains(s.T(), errorOutput.Error, "tag must start with the string 'tag:'")
// Test list all nodes after added seconds // Test list all nodes after added seconds
listAllResult, _, err := ExecuteCommand( listAllResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -691,17 +668,17 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
// Randomly generated machine keys // Randomly generated machine keys
machineKeys := []string{ machineKeys := []string{
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", "9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", "6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", "f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", "8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", "cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
} }
machines := make([]*v1.Machine, len(machineKeys)) machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for index, machineKey := range machineKeys { for index, machineKey := range machineKeys {
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -720,7 +697,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
machineResult, _, err := ExecuteCommand( machineResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -747,7 +724,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Len(s.T(), machines, len(machineKeys)) assert.Len(s.T(), machines, len(machineKeys))
// Test list all nodes after added seconds // Test list all nodes after added seconds
listAllResult, _, err := ExecuteCommand( listAllResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -779,14 +756,14 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Equal(s.T(), "machine-5", listAll[4].Name) assert.Equal(s.T(), "machine-5", listAll[4].Name)
otherNamespaceMachineKeys := []string{ otherNamespaceMachineKeys := []string{
"nodekey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e", "b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
"nodekey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584", "dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
} }
otherNamespaceMachines := make([]*v1.Machine, len(otherNamespaceMachineKeys)) otherNamespaceMachines := make([]*v1.Machine, len(otherNamespaceMachineKeys))
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for index, machineKey := range otherNamespaceMachineKeys { for index, machineKey := range otherNamespaceMachineKeys {
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -805,7 +782,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
machineResult, _, err := ExecuteCommand( machineResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -832,7 +809,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Len(s.T(), otherNamespaceMachines, len(otherNamespaceMachineKeys)) assert.Len(s.T(), otherNamespaceMachines, len(otherNamespaceMachineKeys))
// Test list all nodes after added otherNamespace // Test list all nodes after added otherNamespace
listAllWithotherNamespaceResult, _, err := ExecuteCommand( listAllWithotherNamespaceResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -862,7 +839,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name) assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name)
// Test list all nodes after added otherNamespace // Test list all nodes after added otherNamespace
listOnlyotherNamespaceMachineNamespaceResult, _, err := ExecuteCommand( listOnlyotherNamespaceMachineNamespaceResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -901,7 +878,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
) )
// Delete a machines // Delete a machines
_, _, err = ExecuteCommand( _, err = ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -919,7 +896,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
// Test: list main namespace after machine is deleted // Test: list main namespace after machine is deleted
listOnlyMachineNamespaceAfterDeleteResult, _, err := ExecuteCommand( listOnlyMachineNamespaceAfterDeleteResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -950,17 +927,17 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
// Randomly generated machine keys // Randomly generated machine keys
machineKeys := []string{ machineKeys := []string{
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", "9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", "6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", "f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", "8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", "cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
} }
machines := make([]*v1.Machine, len(machineKeys)) machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for index, machineKey := range machineKeys { for index, machineKey := range machineKeys {
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -979,7 +956,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
machineResult, _, err := ExecuteCommand( machineResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1005,7 +982,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
assert.Len(s.T(), machines, len(machineKeys)) assert.Len(s.T(), machines, len(machineKeys))
listAllResult, _, err := ExecuteCommand( listAllResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1031,7 +1008,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
assert.True(s.T(), listAll[4].Expiry.AsTime().IsZero()) assert.True(s.T(), listAll[4].Expiry.AsTime().IsZero())
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1045,7 +1022,7 @@ func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
} }
listAllAfterExpiryResult, _, err := ExecuteCommand( listAllAfterExpiryResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1077,17 +1054,17 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
// Randomly generated machine keys // Randomly generated machine keys
machineKeys := []string{ machineKeys := []string{
"nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", "cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
"nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", "8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1",
"nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", "f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507",
"nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", "6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
"nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", "9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
} }
machines := make([]*v1.Machine, len(machineKeys)) machines := make([]*v1.Machine, len(machineKeys))
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for index, machineKey := range machineKeys { for index, machineKey := range machineKeys {
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1106,7 +1083,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
machineResult, _, err := ExecuteCommand( machineResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1132,7 +1109,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
assert.Len(s.T(), machines, len(machineKeys)) assert.Len(s.T(), machines, len(machineKeys))
listAllResult, _, err := ExecuteCommand( listAllResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1158,7 +1135,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
assert.Contains(s.T(), listAll[4].GetGivenName(), "machine-5") assert.Contains(s.T(), listAll[4].GetGivenName(), "machine-5")
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1173,7 +1150,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
} }
listAllAfterRenameResult, _, err := ExecuteCommand( listAllAfterRenameResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1199,7 +1176,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
assert.Contains(s.T(), listAllAfterRename[4].GetGivenName(), "machine-5") assert.Contains(s.T(), listAllAfterRename[4].GetGivenName(), "machine-5")
// Test failure for too long names // Test failure for too long names
result, _, err := ExecuteCommand( result, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1214,7 +1191,7 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
assert.Contains(s.T(), result, "not be over 63 chars") assert.Contains(s.T(), result, "not be over 63 chars")
listAllAfterRenameAttemptResult, _, err := ExecuteCommand( listAllAfterRenameAttemptResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1243,13 +1220,206 @@ func (s *IntegrationCLITestSuite) TestNodeRenameCommand() {
assert.Contains(s.T(), listAllAfterRenameAttempt[4].GetGivenName(), "machine-5") assert.Contains(s.T(), listAllAfterRenameAttempt[4].GetGivenName(), "machine-5")
} }
func (s *IntegrationCLITestSuite) TestRouteCommand() {
namespace, err := s.createNamespace("routes-namespace")
assert.Nil(s.T(), err)
// Randomly generated machine keys
machineKey := "9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe"
_, err = ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"debug",
"create-node",
"--name",
"route-machine",
"--namespace",
namespace.Name,
"--key",
machineKey,
"--route",
"10.0.0.0/8",
"--route",
"192.168.1.0/24",
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
machineResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"nodes",
"--namespace",
namespace.Name,
"register",
"--key",
machineKey,
"--output",
"json",
},
[]string{},
)
assert.Nil(s.T(), err)
var machine v1.Machine
err = json.Unmarshal([]byte(machineResult), &machine)
assert.Nil(s.T(), err)
assert.Equal(s.T(), uint64(1), machine.Id)
assert.Equal(s.T(), "route-machine", machine.Name)
listAllResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"routes",
"list",
"--output",
"json",
"--identifier",
"0",
},
[]string{},
)
assert.Nil(s.T(), err)
var listAll v1.Routes
err = json.Unmarshal([]byte(listAllResult), &listAll)
assert.Nil(s.T(), err)
assert.Len(s.T(), listAll.AdvertisedRoutes, 2)
assert.Contains(s.T(), listAll.AdvertisedRoutes, "10.0.0.0/8")
assert.Contains(s.T(), listAll.AdvertisedRoutes, "192.168.1.0/24")
assert.Empty(s.T(), listAll.EnabledRoutes)
enableTwoRoutesResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"routes",
"enable",
"--output",
"json",
"--identifier",
"0",
"--route",
"10.0.0.0/8",
"--route",
"192.168.1.0/24",
},
[]string{},
)
assert.Nil(s.T(), err)
var enableTwoRoutes v1.Routes
err = json.Unmarshal([]byte(enableTwoRoutesResult), &enableTwoRoutes)
assert.Nil(s.T(), err)
assert.Len(s.T(), enableTwoRoutes.AdvertisedRoutes, 2)
assert.Contains(s.T(), enableTwoRoutes.AdvertisedRoutes, "10.0.0.0/8")
assert.Contains(s.T(), enableTwoRoutes.AdvertisedRoutes, "192.168.1.0/24")
assert.Len(s.T(), enableTwoRoutes.EnabledRoutes, 2)
assert.Contains(s.T(), enableTwoRoutes.EnabledRoutes, "10.0.0.0/8")
assert.Contains(s.T(), enableTwoRoutes.EnabledRoutes, "192.168.1.0/24")
// Enable only one route, effectively disabling one of the routes
enableOneRouteResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"routes",
"enable",
"--output",
"json",
"--identifier",
"0",
"--route",
"10.0.0.0/8",
},
[]string{},
)
assert.Nil(s.T(), err)
var enableOneRoute v1.Routes
err = json.Unmarshal([]byte(enableOneRouteResult), &enableOneRoute)
assert.Nil(s.T(), err)
assert.Len(s.T(), enableOneRoute.AdvertisedRoutes, 2)
assert.Contains(s.T(), enableOneRoute.AdvertisedRoutes, "10.0.0.0/8")
assert.Contains(s.T(), enableOneRoute.AdvertisedRoutes, "192.168.1.0/24")
assert.Len(s.T(), enableOneRoute.EnabledRoutes, 1)
assert.Contains(s.T(), enableOneRoute.EnabledRoutes, "10.0.0.0/8")
// Enable only one route, effectively disabling one of the routes
failEnableNonAdvertisedRoute, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"routes",
"enable",
"--output",
"json",
"--identifier",
"0",
"--route",
"11.0.0.0/8",
},
[]string{},
)
assert.Nil(s.T(), err)
assert.Contains(
s.T(),
string(failEnableNonAdvertisedRoute),
"route (route-machine) is not available on node",
)
// Enable all routes on host
enableAllRouteResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"routes",
"enable",
"--output",
"json",
"--identifier",
"0",
"--all",
},
[]string{},
)
assert.Nil(s.T(), err)
var enableAllRoute v1.Routes
err = json.Unmarshal([]byte(enableAllRouteResult), &enableAllRoute)
assert.Nil(s.T(), err)
assert.Len(s.T(), enableAllRoute.AdvertisedRoutes, 2)
assert.Contains(s.T(), enableAllRoute.AdvertisedRoutes, "10.0.0.0/8")
assert.Contains(s.T(), enableAllRoute.AdvertisedRoutes, "192.168.1.0/24")
assert.Len(s.T(), enableAllRoute.EnabledRoutes, 2)
assert.Contains(s.T(), enableAllRoute.EnabledRoutes, "10.0.0.0/8")
assert.Contains(s.T(), enableAllRoute.EnabledRoutes, "192.168.1.0/24")
}
func (s *IntegrationCLITestSuite) TestApiKeyCommand() { func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
count := 5 count := 5
keys := make([]string, count) keys := make([]string, count)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
apiResult, _, err := ExecuteCommand( apiResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1275,7 +1445,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
assert.Len(s.T(), keys, 5) assert.Len(s.T(), keys, 5)
// Test list of keys // Test list of keys
listResult, _, err := ExecuteCommand( listResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1337,7 +1507,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
// Expire three keys // Expire three keys
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1354,7 +1524,7 @@ func (s *IntegrationCLITestSuite) TestApiKeyCommand() {
} }
// Test list pre auth keys after expire // Test list pre auth keys after expire
listAfterExpireResult, _, err := ExecuteCommand( listAfterExpireResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1395,9 +1565,9 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
// Randomly generated machine key // Randomly generated machine key
machineKey := "nodekey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa" machineKey := "688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
_, _, err = ExecuteCommand( _, err = ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1416,7 +1586,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
machineResult, _, err := ExecuteCommand( machineResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1443,7 +1613,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
machineId := fmt.Sprintf("%d", machine.Id) machineId := fmt.Sprintf("%d", machine.Id)
moveToNewNSResult, _, err := ExecuteCommand( moveToNewNSResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1465,7 +1635,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
assert.Equal(s.T(), machine.Namespace, newNamespace) assert.Equal(s.T(), machine.Namespace, newNamespace)
listAllNodesResult, _, err := ExecuteCommand( listAllNodesResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1488,7 +1658,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
assert.Equal(s.T(), allNodes[0].Namespace, machine.Namespace) assert.Equal(s.T(), allNodes[0].Namespace, machine.Namespace)
assert.Equal(s.T(), allNodes[0].Namespace, newNamespace) assert.Equal(s.T(), allNodes[0].Namespace, newNamespace)
moveToNonExistingNSResult, _, err := ExecuteCommand( moveToNonExistingNSResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1512,7 +1682,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
) )
assert.Equal(s.T(), machine.Namespace, newNamespace) assert.Equal(s.T(), machine.Namespace, newNamespace)
moveToOldNSResult, _, err := ExecuteCommand( moveToOldNSResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1534,7 +1704,7 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
assert.Equal(s.T(), machine.Namespace, oldNamespace) assert.Equal(s.T(), machine.Namespace, oldNamespace)
moveToSameNSResult, _, err := ExecuteCommand( moveToSameNSResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1563,10 +1733,8 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml") altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml")
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
altEnvConfig, err := os.ReadFile("integration_test/etc/alt-env-config.dump.gold.yaml")
assert.Nil(s.T(), err)
_, _, err = ExecuteCommand( _, err = ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1581,7 +1749,7 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig)) assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig))
_, _, err = ExecuteCommand( _, err = ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -1597,40 +1765,4 @@ func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig)) assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
_, _, err = ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"dumpConfig",
},
[]string{
"HEADSCALE_CONFIG=/etc/headscale/alt-env-config.yaml",
},
)
assert.Nil(s.T(), err)
altEnvDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml")
assert.Nil(s.T(), err)
assert.YAMLEq(s.T(), string(altEnvConfig), string(altEnvDumpConfig))
_, _, err = ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"-c",
"/etc/headscale/alt-config.yaml",
"dumpConfig",
},
[]string{
"HEADSCALE_CONFIG=/etc/headscale/alt-env-config.yaml",
},
)
assert.Nil(s.T(), err)
altDumpConfig, err = os.ReadFile("integration_test/etc/config.dump.yaml")
assert.Nil(s.T(), err)
assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
} }

View File

@@ -1,4 +1,6 @@
//nolint //go:build integration
// +build integration
package headscale package headscale
import ( import (
@@ -6,7 +8,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@@ -15,27 +16,23 @@ import (
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker" "github.com/ory/dockertest/v3/docker"
"inet.af/netaddr"
) )
const ( const (
headscaleNetwork = "headscale-test"
headscaleHostname = "headscale"
DOCKER_EXECUTE_TIMEOUT = 10 * time.Second DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
) )
var ( var (
errEnvVarEmpty = errors.New("getenv: environment variable empty") errEnvVarEmpty = errors.New("getenv: environment variable empty")
IpPrefix4 = netip.MustParsePrefix("100.64.0.0/10") IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
IpPrefix6 = netip.MustParsePrefix("fd7a:115c:a1e0::/48") IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
tailscaleVersions = []string{ tailscaleVersions = []string{
"head", "head",
"unstable", "unstable",
"1.32.0", "1.26.0",
"1.30.2",
"1.28.0",
"1.26.2",
"1.24.2", "1.24.2",
"1.22.2", "1.22.2",
"1.20.4", "1.20.4",
@@ -69,7 +66,7 @@ func ExecuteCommand(
cmd []string, cmd []string,
env []string, env []string,
options ...ExecuteCommandOption, options ...ExecuteCommandOption,
) (string, string, error) { ) (string, error) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
@@ -79,7 +76,7 @@ func ExecuteCommand(
for _, opt := range options { for _, opt := range options {
if err := opt(&execConfig); err != nil { if err := opt(&execConfig); err != nil {
return "", "", fmt.Errorf("execute-command/options: %w", err) return "", fmt.Errorf("execute-command/options: %w", err)
} }
} }
@@ -108,7 +105,7 @@ func ExecuteCommand(
select { select {
case res := <-resultChan: case res := <-resultChan:
if res.err != nil { if res.err != nil {
return stdout.String(), stderr.String(), res.err return "", res.err
} }
if res.exitCode != 0 { if res.exitCode != 0 {
@@ -116,19 +113,13 @@ func ExecuteCommand(
fmt.Println("stdout: ", stdout.String()) fmt.Println("stdout: ", stdout.String())
fmt.Println("stderr: ", stderr.String()) fmt.Println("stderr: ", stderr.String())
return stdout.String(), stderr.String(), fmt.Errorf( return "", fmt.Errorf("command failed with: %s", stderr.String())
"command failed with: %s",
stderr.String(),
)
} }
return stdout.String(), stderr.String(), nil return stdout.String(), nil
case <-time.After(execConfig.timeout): case <-time.After(execConfig.timeout):
return stdout.String(), stderr.String(), fmt.Errorf( return "", fmt.Errorf("command timed out after %s", execConfig.timeout)
"command timed out after %s",
execConfig.timeout,
)
} }
} }
@@ -202,12 +193,12 @@ func getDockerBuildOptions(version string) *dockertest.BuildOptions {
func getIPs( func getIPs(
tailscales map[string]dockertest.Resource, tailscales map[string]dockertest.Resource,
) (map[string][]netip.Addr, error) { ) (map[string][]netaddr.IP, error) {
ips := make(map[string][]netip.Addr) ips := make(map[string][]netaddr.IP)
for hostname, tailscale := range tailscales { for hostname, tailscale := range tailscales {
command := []string{"tailscale", "ip"} command := []string{"tailscale", "ip"}
result, _, err := ExecuteCommand( result, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@@ -221,7 +212,7 @@ func getIPs(
if len(address) < 1 { if len(address) < 1 {
continue continue
} }
ip, err := netip.ParseAddr(address) ip, err := netaddr.ParseIP(address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -235,7 +226,8 @@ func getIPs(
func getDNSNames( func getDNSNames(
headscale *dockertest.Resource, headscale *dockertest.Resource,
) ([]string, error) { ) ([]string, error) {
listAllResult, _, err := ExecuteCommand(
listAllResult, err := ExecuteCommand(
headscale, headscale,
[]string{ []string{
"headscale", "headscale",
@@ -268,7 +260,8 @@ func getDNSNames(
func getMagicFQDN( func getMagicFQDN(
headscale *dockertest.Resource, headscale *dockertest.Resource,
) ([]string, error) { ) ([]string, error) {
listAllResult, _, err := ExecuteCommand(
listAllResult, err := ExecuteCommand(
headscale, headscale,
[]string{ []string{
"headscale", "headscale",
@@ -292,11 +285,7 @@ func getMagicFQDN(
hostnames := make([]string, len(listAll)) hostnames := make([]string, len(listAll))
for index := range listAll { for index := range listAll {
hostnames[index] = fmt.Sprintf( hostnames[index] = fmt.Sprintf("%s.%s.headscale.net", listAll[index].GetGivenName(), listAll[index].GetNamespace().GetName())
"%s.%s.headscale.net",
listAll[index].GetGivenName(),
listAll[index].GetNamespace().GetName(),
)
} }
return hostnames, nil return hostnames, nil
@@ -323,21 +312,3 @@ func GetEnvBool(key string) (bool, error) {
return v, nil return v, nil
} }
func GetFirstOrCreateNetwork(pool *dockertest.Pool, name string) (dockertest.Network, error) {
networks, err := pool.NetworksByName(name)
if err != nil || len(networks) == 0 {
if _, err := pool.CreateNetwork(name); err == nil {
// Create does not give us an updated version of the resource, so we need to
// get it again.
networks, err := pool.NetworksByName(name)
if err != nil {
return dockertest.Network{}, err
}
return networks[0], nil
}
}
return networks[0], nil
}

View File

@@ -1,4 +1,5 @@
//nolint //go:build integration
package headscale package headscale
import ( import (
@@ -7,6 +8,7 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -16,39 +18,35 @@ import (
"testing" "testing"
"time" "time"
"github.com/ccding/go-stun/stun"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker" "github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/ccding/go-stun/stun"
) )
const ( const (
headscaleDerpHostname = "headscale-derp" headscaleHostname = "headscale-derp"
namespaceName = "derpnamespace" namespaceName = "derpnamespace"
totalContainers = 3 totalContainers = 3
) )
type IntegrationDERPTestSuite struct { type IntegrationDERPTestSuite struct {
suite.Suite suite.Suite
stats *suite.SuiteInformation stats *suite.SuiteInformation
pool dockertest.Pool pool dockertest.Pool
network dockertest.Network networks map[int]dockertest.Network // so we keep the containers isolated
containerNetworks map[int]dockertest.Network // so we keep the containers isolated headscale dockertest.Resource
headscale dockertest.Resource saveLogs bool
saveLogs bool
tailscales map[string]dockertest.Resource tailscales map[string]dockertest.Resource
joinWaitGroup sync.WaitGroup joinWaitGroup sync.WaitGroup
} }
func TestIntegrationDERPTestSuite(t *testing.T) { func TestDERPIntegrationTestSuite(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration tests due to short flag")
}
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG") saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
if err != nil { if err != nil {
saveLogs = false saveLogs = false
@@ -57,7 +55,7 @@ func TestIntegrationDERPTestSuite(t *testing.T) {
s := new(IntegrationDERPTestSuite) s := new(IntegrationDERPTestSuite)
s.tailscales = make(map[string]dockertest.Resource) s.tailscales = make(map[string]dockertest.Resource)
s.containerNetworks = make(map[int]dockertest.Network) s.networks = make(map[int]dockertest.Network)
s.saveLogs = saveLogs s.saveLogs = saveLogs
suite.Run(t, s) suite.Run(t, s)
@@ -82,7 +80,7 @@ func TestIntegrationDERPTestSuite(t *testing.T) {
log.Printf("Could not purge resource: %s\n", err) log.Printf("Could not purge resource: %s\n", err)
} }
for _, network := range s.containerNetworks { for _, network := range s.networks {
if err := network.Close(); err != nil { if err := network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err) log.Printf("Could not close network: %s\n", err)
} }
@@ -97,22 +95,16 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "") s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
} }
network, err := GetFirstOrCreateNetwork(&s.pool, headscaleNetwork)
if err != nil {
s.FailNow(fmt.Sprintf("Failed to create or get network: %s", err), "")
}
s.network = network
for i := 0; i < totalContainers; i++ { for i := 0; i < totalContainers; i++ {
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil { if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
s.containerNetworks[i] = *pnetwork s.networks[i] = *pnetwork
} else { } else {
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "") s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
} }
} }
headscaleBuildOptions := &dockertest.BuildOptions{ headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile", Dockerfile: "Dockerfile.tmp-integration",
ContextDir: ".", ContextDir: ".",
} }
@@ -122,7 +114,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
} }
headscaleOptions := &dockertest.RunOptions{ headscaleOptions := &dockertest.RunOptions{
Name: headscaleDerpHostname, Name: headscaleHostname,
Mounts: []string{ Mounts: []string{
fmt.Sprintf( fmt.Sprintf(
"%s/integration_test/etc_embedded_derp:/etc/headscale", "%s/integration_test/etc_embedded_derp:/etc/headscale",
@@ -130,7 +122,6 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
), ),
}, },
Cmd: []string{"headscale", "serve"}, Cmd: []string{"headscale", "serve"},
Networks: []*dockertest.Network{&s.network},
ExposedPorts: []string{"8443/tcp", "3478/udp"}, ExposedPorts: []string{"8443/tcp", "3478/udp"},
PortBindings: map[docker.Port][]docker.PortBinding{ PortBindings: map[docker.Port][]docker.PortBinding{
"8443/tcp": {{HostPort: "8443"}}, "8443/tcp": {{HostPort: "8443"}},
@@ -138,41 +129,33 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
}, },
} }
err = s.pool.RemoveContainerByName(headscaleDerpHostname) err = s.pool.RemoveContainerByName(headscaleHostname)
if err != nil { if err != nil {
s.FailNow( s.FailNow(fmt.Sprintf("Could not remove existing container before building test: %s", err), "")
fmt.Sprintf(
"Could not remove existing container before building test: %s",
err,
),
"",
)
} }
log.Println("Creating headscale container for DERP integration tests") log.Println("Creating headscale container")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil { if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale s.headscale = *pheadscale
} else { } else {
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "") s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
} }
log.Println("Created headscale container for embedded DERP tests") log.Println("Created headscale container to test DERP")
log.Println("Creating tailscale containers for embedded DERP tests") log.Println("Creating tailscale containers")
for i := 0; i < totalContainers; i++ { for i := 0; i < totalContainers; i++ {
version := tailscaleVersions[i%len(tailscaleVersions)] version := tailscaleVersions[i%len(tailscaleVersions)]
hostname, container := s.tailscaleContainer( hostname, container := s.tailscaleContainer(
fmt.Sprint(i), fmt.Sprint(i),
version, version,
s.containerNetworks[i], s.networks[i],
) )
s.tailscales[hostname] = *container s.tailscales[hostname] = *container
} }
log.Println("Waiting for headscale to be ready for embedded DERP tests") log.Println("Waiting for headscale to be ready")
hostEndpoint := fmt.Sprintf("%s:%s", hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8443/tcp"))
s.headscale.GetIPInNetwork(&s.network),
s.headscale.GetPort("8443/tcp"))
if err := s.pool.Retry(func() error { if err := s.pool.Retry(func() error {
url := fmt.Sprintf("https://%s/health", hostEndpoint) url := fmt.Sprintf("https://%s/health", hostEndpoint)
@@ -181,7 +164,6 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
client := &http.Client{Transport: insecureTransport} client := &http.Client{Transport: insecureTransport}
resp, err := client.Get(url) resp, err := client.Get(url)
if err != nil { if err != nil {
fmt.Printf("headscale for embedded DERP tests is not ready: %s\n", err)
return err return err
} }
@@ -197,10 +179,10 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
// https://github.com/stretchr/testify/issues/849 // https://github.com/stretchr/testify/issues/849
return // fmt.Errorf("Could not connect to headscale: %s", err) return // fmt.Errorf("Could not connect to headscale: %s", err)
} }
log.Println("headscale container is ready for embedded DERP tests") log.Println("headscale container is ready")
log.Printf("Creating headscale namespace: %s\n", namespaceName) log.Printf("Creating headscale namespace: %s\n", namespaceName)
result, _, err := ExecuteCommand( result, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{"headscale", "namespaces", "create", namespaceName}, []string{"headscale", "namespaces", "create", namespaceName},
[]string{}, []string{},
@@ -209,7 +191,7 @@ func (s *IntegrationDERPTestSuite) SetupSuite() {
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
log.Printf("Creating pre auth key for %s\n", namespaceName) log.Printf("Creating pre auth key for %s\n", namespaceName)
preAuthResult, _, err := ExecuteCommand( preAuthResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
@@ -272,7 +254,7 @@ func (s *IntegrationDERPTestSuite) Join(
log.Println("Join command:", command) log.Println("Join command:", command)
log.Printf("Running join command for %s\n", hostname) log.Printf("Running join command for %s\n", hostname)
_, _, err := ExecuteCommand( _, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@@ -333,7 +315,7 @@ func (s *IntegrationDERPTestSuite) TearDownSuite() {
log.Printf("Could not purge resource: %s\n", err) log.Printf("Could not purge resource: %s\n", err)
} }
for _, network := range s.containerNetworks { for _, network := range s.networks {
if err := network.Close(); err != nil { if err := network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err) log.Printf("Could not close network: %s\n", err)
} }
@@ -380,7 +362,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath) log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
err = os.WriteFile( err = ioutil.WriteFile(
path.Join(basePath, resource.Container.Name+".stdout.log"), path.Join(basePath, resource.Container.Name+".stdout.log"),
[]byte(stdout.String()), []byte(stdout.String()),
0o644, 0o644,
@@ -389,7 +371,7 @@ func (s *IntegrationDERPTestSuite) saveLog(
return err return err
} }
err = os.WriteFile( err = ioutil.WriteFile(
path.Join(basePath, resource.Container.Name+".stderr.log"), path.Join(basePath, resource.Container.Name+".stderr.log"),
[]byte(stdout.String()), []byte(stdout.String()),
0o644, 0o644,
@@ -427,7 +409,7 @@ func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
peername, peername,
) )
log.Println(command) log.Println(command)
result, _, err := ExecuteCommand( result, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@@ -441,9 +423,7 @@ func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
} }
func (s *IntegrationDERPTestSuite) TestDERPSTUN() { func (s *IntegrationDERPTestSuite) TestDERPSTUN() {
headscaleSTUNAddr := fmt.Sprintf("%s:%s", headscaleSTUNAddr := fmt.Sprintf("localhost:%s", s.headscale.GetPort("3478/udp"))
s.headscale.GetIPInNetwork(&s.network),
s.headscale.GetPort("3478/udp"))
client := stun.NewClient() client := stun.NewClient()
client.SetVerbose(true) client.SetVerbose(true)
client.SetVVerbose(true) client.SetVVerbose(true)

754
integration_test.go Normal file
View File

@@ -0,0 +1,754 @@
//go:build integration
// +build integration
package headscale
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"strings"
"sync"
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn/ipnstate"
)
type IntegrationTestSuite struct {
suite.Suite
stats *suite.SuiteInformation
pool dockertest.Pool
network dockertest.Network
headscale dockertest.Resource
saveLogs bool
namespaces map[string]TestNamespace
joinWaitGroup sync.WaitGroup
}
func TestIntegrationTestSuite(t *testing.T) {
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
if err != nil {
saveLogs = false
}
s := new(IntegrationTestSuite)
s.namespaces = map[string]TestNamespace{
"thisspace": {
count: 5,
tailscales: make(map[string]dockertest.Resource),
},
"otherspace": {
count: 2,
tailscales: make(map[string]dockertest.Resource),
},
}
s.saveLogs = saveLogs
suite.Run(t, s)
// HandleStats, which allows us to check if we passed and save logs
// is called after TearDown, so we cannot tear down containers before
// we have potentially saved the logs.
if s.saveLogs {
for _, scales := range s.namespaces {
for _, tailscale := range scales.tailscales {
if err := s.pool.Purge(&tailscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
}
}
if !s.stats.Passed() {
err := s.saveLog(&s.headscale, "test_output")
if err != nil {
log.Printf("Could not save log: %s\n", err)
}
}
if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
if err := s.network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err)
}
}
}
func (s *IntegrationTestSuite) saveLog(
resource *dockertest.Resource,
basePath string,
) error {
err := os.MkdirAll(basePath, os.ModePerm)
if err != nil {
return err
}
var stdout bytes.Buffer
var stderr bytes.Buffer
err = s.pool.Client.Logs(
docker.LogsOptions{
Context: context.TODO(),
Container: resource.Container.ID,
OutputStream: &stdout,
ErrorStream: &stderr,
Tail: "all",
RawTerminal: false,
Stdout: true,
Stderr: true,
Follow: false,
Timestamps: false,
},
)
if err != nil {
return err
}
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
err = ioutil.WriteFile(
path.Join(basePath, resource.Container.Name+".stdout.log"),
[]byte(stdout.String()),
0o644,
)
if err != nil {
return err
}
err = ioutil.WriteFile(
path.Join(basePath, resource.Container.Name+".stderr.log"),
[]byte(stdout.String()),
0o644,
)
if err != nil {
return err
}
return nil
}
func (s *IntegrationTestSuite) Join(
endpoint, key, hostname string,
tailscale dockertest.Resource,
) {
defer s.joinWaitGroup.Done()
command := []string{
"tailscale",
"up",
"-login-server",
endpoint,
"--authkey",
key,
"--hostname",
hostname,
}
log.Println("Join command:", command)
log.Printf("Running join command for %s\n", hostname)
_, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(s.T(), err)
log.Printf("%s joined\n", hostname)
}
func (s *IntegrationTestSuite) tailscaleContainer(
namespace, identifier, version string,
) (string, *dockertest.Resource) {
tailscaleBuildOptions := getDockerBuildOptions(version)
hostname := fmt.Sprintf(
"%s-tailscale-%s-%s",
namespace,
strings.Replace(version, ".", "-", -1),
identifier,
)
tailscaleOptions := &dockertest.RunOptions{
Name: hostname,
Networks: []*dockertest.Network{&s.network},
Cmd: []string{
"tailscaled", "--tun=tsdev",
},
}
pts, err := s.pool.BuildAndRunWithBuildOptions(
tailscaleBuildOptions,
tailscaleOptions,
DockerRestartPolicy,
DockerAllowLocalIPv6,
DockerAllowNetworkAdministration,
)
if err != nil {
log.Fatalf("Could not start tailscale container version %s: %s", version, err)
}
log.Printf("Created %s container\n", hostname)
return hostname, pts
}
func (s *IntegrationTestSuite) SetupSuite() {
var err error
app = Headscale{
dbType: "sqlite3",
dbString: "integration_test_db.sqlite3",
}
if ppool, err := dockertest.NewPool(""); err == nil {
s.pool = *ppool
} else {
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
}
if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil {
s.network = *pnetwork
} else {
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
}
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tmp-integration",
ContextDir: ".",
}
currentPath, err := os.Getwd()
if err != nil {
s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "")
}
headscaleOptions := &dockertest.RunOptions{
Name: "headscale",
Mounts: []string{
fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath),
},
Networks: []*dockertest.Network{&s.network},
Cmd: []string{"headscale", "serve"},
}
err = s.pool.RemoveContainerByName(headscaleHostname)
if err != nil {
s.FailNow(fmt.Sprintf("Could not remove existing container before building test: %s", err), "")
}
log.Println("Creating headscale container")
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
s.headscale = *pheadscale
} else {
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
}
log.Println("Created headscale container")
log.Println("Creating tailscale containers")
for namespace, scales := range s.namespaces {
for i := 0; i < scales.count; i++ {
version := tailscaleVersions[i%len(tailscaleVersions)]
hostname, container := s.tailscaleContainer(
namespace,
fmt.Sprint(i),
version,
)
scales.tailscales[hostname] = *container
}
}
log.Println("Waiting for headscale to be ready")
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8080/tcp"))
if err := s.pool.Retry(func() error {
url := fmt.Sprintf("http://%s/health", hostEndpoint)
resp, err := http.Get(url)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status code not OK")
}
return nil
}); err != nil {
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
// test setup, we need to abort and tear down. However, testify does not seem to
// support that at the moment:
// https://github.com/stretchr/testify/issues/849
return // fmt.Errorf("Could not connect to headscale: %s", err)
}
log.Println("headscale container is ready")
for namespace, scales := range s.namespaces {
log.Printf("Creating headscale namespace: %s\n", namespace)
result, err := ExecuteCommand(
&s.headscale,
[]string{"headscale", "namespaces", "create", namespace},
[]string{},
)
log.Println("headscale create namespace result: ", result)
assert.Nil(s.T(), err)
log.Printf("Creating pre auth key for %s\n", namespace)
preAuthResult, err := ExecuteCommand(
&s.headscale,
[]string{
"headscale",
"--namespace",
namespace,
"preauthkeys",
"create",
"--reusable",
"--expiration",
"24h",
"--output",
"json",
},
[]string{"LOG_LEVEL=error"},
)
assert.Nil(s.T(), err)
var preAuthKey v1.PreAuthKey
err = json.Unmarshal([]byte(preAuthResult), &preAuthKey)
assert.Nil(s.T(), err)
assert.True(s.T(), preAuthKey.Reusable)
headscaleEndpoint := "http://headscale:8080"
log.Printf(
"Joining tailscale containers to headscale at %s\n",
headscaleEndpoint,
)
for hostname, tailscale := range scales.tailscales {
s.joinWaitGroup.Add(1)
go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
}
s.joinWaitGroup.Wait()
}
// The nodes need a bit of time to get their updated maps from headscale
// TODO: See if we can have a more deterministic wait here.
time.Sleep(60 * time.Second)
}
func (s *IntegrationTestSuite) TearDownSuite() {
if !s.saveLogs {
for _, scales := range s.namespaces {
for _, tailscale := range scales.tailscales {
if err := s.pool.Purge(&tailscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
}
}
if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err)
}
if err := s.network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err)
}
}
}
func (s *IntegrationTestSuite) HandleStats(
suiteName string,
stats *suite.SuiteInformation,
) {
s.stats = stats
}
func (s *IntegrationTestSuite) TestListNodes() {
for namespace, scales := range s.namespaces {
log.Println("Listing nodes")
result, err := ExecuteCommand(
&s.headscale,
[]string{"headscale", "--namespace", namespace, "nodes", "list"},
[]string{},
)
assert.Nil(s.T(), err)
log.Printf("List nodes: \n%s\n", result)
// Chck that the correct count of host is present in node list
lines := strings.Split(result, "\n")
assert.Equal(s.T(), len(scales.tailscales), len(lines)-2)
for hostname := range scales.tailscales {
assert.Contains(s.T(), result, hostname)
}
}
}
func (s *IntegrationTestSuite) TestGetIpAddresses() {
for _, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err)
for hostname := range scales.tailscales {
ips := ips[hostname]
for _, ip := range ips {
s.T().Run(hostname, func(t *testing.T) {
assert.NotNil(t, ip)
log.Printf("IP for %s: %s\n", hostname, ip)
// c.Assert(ip.Valid(), check.IsTrue)
assert.True(t, ip.Is4() || ip.Is6())
switch {
case ip.Is4():
assert.True(t, IpPrefix4.Contains(ip))
case ip.Is6():
assert.True(t, IpPrefix6.Contains(ip))
}
})
}
}
}
}
// TODO(kradalby): fix this test
// We need some way to import ipnstate.Status from multiple go packages.
// Currently it will only work with 1.18.x since that is the last
// version we have in go.mod
// func (s *IntegrationTestSuite) TestStatus() {
// for _, scales := range s.namespaces {
// ips, err := getIPs(scales.tailscales)
// assert.Nil(s.T(), err)
//
// for hostname, tailscale := range scales.tailscales {
// s.T().Run(hostname, func(t *testing.T) {
// command := []string{"tailscale", "status", "--json"}
//
// log.Printf("Getting status for %s\n", hostname)
// result, err := ExecuteCommand(
// &tailscale,
// command,
// []string{},
// )
// assert.Nil(t, err)
//
// var status ipnstate.Status
// err = json.Unmarshal([]byte(result), &status)
// assert.Nil(s.T(), err)
//
// // TODO(kradalby): Replace this check with peer length of SAME namespace
// // Check if we have as many nodes in status
// // as we have IPs/tailscales
// // lines := strings.Split(result, "\n")
// // assert.Equal(t, len(ips), len(lines)-1)
// // assert.Equal(t, len(scales.tailscales), len(lines)-1)
//
// peerIps := getIPsfromIPNstate(status)
//
// // Check that all hosts is present in all hosts status
// for ipHostname, ip := range ips {
// if hostname != ipHostname {
// assert.Contains(t, peerIps, ip)
// }
// }
// })
// }
// }
// }
func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP {
ips := make([]netaddr.IP, 0)
for _, peer := range status.Peer {
ips = append(ips, peer.TailscaleIPs...)
}
return ips
}
// TODO: Adopt test for cross communication between namespaces
func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
for _, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err)
for hostname, tailscale := range scales.tailscales {
for peername, peerIPs := range ips {
for i, ip := range peerIPs {
// We currently cant ping ourselves, so skip that.
if peername == hostname {
continue
}
s.T().
Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) {
// We are only interested in "direct ping" which means what we
// might need a couple of more attempts before reaching the node.
command := []string{
"tailscale", "ping",
"--timeout=1s",
"--c=10",
"--until-direct=true",
ip.String(),
}
log.Printf(
"Pinging from %s to %s (%s)\n",
hostname,
peername,
ip,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
log.Printf("Result for %s: %s\n", hostname, result)
assert.Contains(t, result, "pong")
})
}
}
}
}
}
func (s *IntegrationTestSuite) TestTailDrop() {
for _, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err)
retry := func(times int, sleepInverval time.Duration, doWork func() error) (err error) {
for attempts := 0; attempts < times; attempts++ {
err = doWork()
if err == nil {
return
}
time.Sleep(sleepInverval)
}
return
}
for hostname, tailscale := range scales.tailscales {
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", hostname)}
_, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(s.T(), err)
for peername := range ips {
if peername == hostname {
continue
}
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
command := []string{
"tailscale", "file", "cp",
fmt.Sprintf("/tmp/file_from_%s", hostname),
fmt.Sprintf("%s:", ips[peername][1]),
}
retry(10, 1*time.Second, func() error {
log.Printf(
"Sending file from %s to %s\n",
hostname,
peername,
)
_, err := ExecuteCommand(
&tailscale,
command,
[]string{},
ExecuteCommandTimeout(60*time.Second),
)
return err
})
assert.Nil(t, err)
})
}
}
for hostname, tailscale := range scales.tailscales {
command := []string{
"tailscale", "file",
"get",
"/tmp/",
}
_, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(s.T(), err)
for peername, ip := range ips {
if peername == hostname {
continue
}
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
command := []string{
"ls",
fmt.Sprintf("/tmp/file_from_%s", peername),
}
log.Printf(
"Checking file in %s (%s) from %s (%s)\n",
hostname,
ips[hostname][1],
peername,
ip,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
log.Printf("Result for %s: %s\n", peername, result)
assert.Equal(
t,
fmt.Sprintf("/tmp/file_from_%s\n", peername),
result,
)
})
}
}
}
}
func (s *IntegrationTestSuite) TestPingAllPeersByHostname() {
hostnames, err := getMagicFQDN(&s.headscale)
assert.Nil(s.T(), err)
log.Printf("Resolved hostnames: %#v", hostnames)
for _, scales := range s.namespaces {
for hostname, tailscale := range scales.tailscales {
for _, peername := range hostnames {
if strings.Contains(peername, hostname) {
continue
}
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
command := []string{
"tailscale", "ping",
"--timeout=10s",
"--c=20",
"--until-direct=true",
peername,
}
log.Printf(
"Pinging using hostname from %s to %s\n",
hostname,
peername,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
log.Printf("Result for %s: %s\n", hostname, result)
assert.Contains(t, result, "pong")
})
}
}
}
}
func (s *IntegrationTestSuite) TestMagicDNS() {
hostnames, err := getMagicFQDN(&s.headscale)
assert.Nil(s.T(), err)
log.Printf("Resolved hostnames: %#v", hostnames)
for _, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err)
for hostname, tailscale := range scales.tailscales {
for _, peername := range hostnames {
if strings.Contains(peername, hostname) {
continue
}
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
command := []string{
"tailscale", "ip", peername,
}
log.Printf(
"Resolving name %s from %s\n",
peername,
hostname,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
log.Printf("Result for %s: %s\n", hostname, result)
peerBaseName := peername[:len(peername)-MachineGivenNameHashLength-1]
expectedAddresses := ips[peerBaseName]
for _, ip := range expectedAddresses {
assert.Contains(t, result, ip.String())
}
})
}
}
}
}
func getAPIURLs(
tailscales map[string]dockertest.Resource,
) (map[netaddr.IP]string, error) {
fts := make(map[netaddr.IP]string)
for _, tailscale := range tailscales {
command := []string{
"curl",
"--unix-socket",
"/run/tailscale/tailscaled.sock",
"http://localhost/localapi/v0/file-targets",
}
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
if err != nil {
return nil, err
}
var pft []apitype.FileTarget
if err := json.Unmarshal([]byte(result), &pft); err != nil {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
for _, ft := range pft {
n := ft.Node
for _, a := range n.Addresses { // just add all the addresses
if _, ok := fts[a.IP()]; !ok {
if ft.PeerAPIURL == "" {
return nil, errors.New("api url is empty")
}
fts[a.IP()] = ft.PeerAPIURL
}
}
}
}
return fts, nil
}

View File

@@ -14,12 +14,10 @@ derp:
urls: urls:
- https://controlplane.tailscale.com/derpmap/default - https://controlplane.tailscale.com/derpmap/default
dns_config: dns_config:
override_local_dns: true
base_domain: headscale.net base_domain: headscale.net
domains: [] domains: []
magic_dns: true magic_dns: true
nameservers: nameservers:
- 127.0.0.11
- 1.1.1.1 - 1.1.1.1
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 10s node_update_check_interval: 10s
@@ -29,23 +27,19 @@ ip_prefixes:
- fd7a:115c:a1e0::/48 - fd7a:115c:a1e0::/48
- 100.64.0.0/10 - 100.64.0.0/10
listen_addr: 0.0.0.0:18080 listen_addr: 0.0.0.0:18080
log: log_level: disabled
level: disabled
format: text
logtail: logtail:
enabled: false enabled: false
metrics_listen_addr: 127.0.0.1:19090 metrics_listen_addr: 127.0.0.1:19090
oidc: oidc:
only_start_if_oidc_is_available: true
scope: scope:
- openid - openid
- profile - profile
- email - email
strip_email_domain: true strip_email_domain: true
private_key_path: private.key private_key_path: private.key
noise:
private_key_path: noise_private.key
server_url: http://headscale:18080 server_url: http://headscale:18080
tls_client_auth_mode: relaxed
tls_letsencrypt_cache_dir: /var/www/.cache tls_letsencrypt_cache_dir: /var/www/.cache
tls_letsencrypt_challenge_type: HTTP-01 tls_letsencrypt_challenge_type: HTTP-01
unix_socket: /var/run/headscale.sock unix_socket: /var/run/headscale.sock

View File

@@ -1,5 +1,4 @@
log: log_level: trace
level: trace
acl_policy_path: "" acl_policy_path: ""
db_type: sqlite3 db_type: sqlite3
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
@@ -8,17 +7,13 @@ ip_prefixes:
- fd7a:115c:a1e0::/48 - fd7a:115c:a1e0::/48
- 100.64.0.0/10 - 100.64.0.0/10
dns_config: dns_config:
override_local_dns: true
base_domain: headscale.net base_domain: headscale.net
magic_dns: true magic_dns: true
domains: [] domains: []
nameservers: nameservers:
- 127.0.0.11
- 1.1.1.1 - 1.1.1.1
db_path: /tmp/integration_test_db.sqlite3 db_path: /tmp/integration_test_db.sqlite3
private_key_path: private.key private_key_path: private.key
noise:
private_key_path: noise_private.key
listen_addr: 0.0.0.0:18080 listen_addr: 0.0.0.0:18080
metrics_listen_addr: 127.0.0.1:19090 metrics_listen_addr: 127.0.0.1:19090
server_url: http://headscale:18080 server_url: http://headscale:18080

View File

@@ -1,52 +0,0 @@
acl_policy_path: ""
cli:
insecure: false
timeout: 5s
db_path: /tmp/integration_test_db.sqlite3
db_type: sqlite3
derp:
auto_update_enabled: false
server:
enabled: false
stun:
enabled: true
update_frequency: 1m
urls:
- https://controlplane.tailscale.com/derpmap/default
dns_config:
override_local_dns: true
base_domain: headscale.net
domains: []
magic_dns: true
nameservers:
- 1.1.1.1
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 30s
grpc_allow_insecure: false
grpc_listen_addr: :50443
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
listen_addr: 0.0.0.0:18080
log:
level: disabled
format: text
logtail:
enabled: false
metrics_listen_addr: 127.0.0.1:19090
oidc:
only_start_if_oidc_is_available: true
scope:
- openid
- profile
- email
strip_email_domain: true
private_key_path: private.key
noise:
private_key_path: noise_private.key
server_url: http://headscale:18080
tls_letsencrypt_cache_dir: /var/www/.cache
tls_letsencrypt_challenge_type: HTTP-01
unix_socket: /var/run/headscale.sock
unix_socket_permission: "0o770"
randomize_client_port: false

View File

@@ -1,29 +0,0 @@
log:
level: trace
acl_policy_path: ""
db_type: sqlite3
ephemeral_node_inactivity_timeout: 30m
node_update_check_interval: 30s
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
dns_config:
override_local_dns: true
base_domain: headscale.net
magic_dns: true
domains: []
nameservers:
- 1.1.1.1
db_path: /tmp/integration_test_db.sqlite3
private_key_path: private.key
noise:
private_key_path: noise_private.key
listen_addr: 0.0.0.0:18080
metrics_listen_addr: 127.0.0.1:19090
server_url: http://headscale:18080
derp:
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: false
update_frequency: 1m

Some files were not shown because too many files have changed in this diff Show More