Merge branch 'main' into patch-1

This commit is contained in:
Kristoffer Dalby 2022-01-30 10:09:28 +00:00 committed by GitHub
commit df10dd9c21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
132 changed files with 17702 additions and 3218 deletions

View File

@ -15,3 +15,4 @@ README.md
LICENSE LICENSE
.vscode .vscode
*.sock

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,28 @@
---
name: "Bug report"
about: "Create a bug report to help us improve"
title: ""
labels: ["bug"]
assignees: ""
---
**Bug description**
<!-- A clear and concise description of what the bug is. Describe the expected bahavior
and how it is currently different. If you are unsure if it is a bug, consider discussing
it on our Discord server first. -->
**To Reproduce**
<!-- Steps to reproduce the behavior. -->
**Context info**
<!-- Please add relevant information about your system. For example:
- Version of headscale used
- Version of tailscale client
- OS (e.g. Linux, Mac, Cygwin, WSL, etc.) and version
- Kernel version
- The relevant config parameters you used
- Log output
-->

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# Issues must have some content
blank_issues_enabled: false
# Contact links
contact_links:
- name: "headscale usage documentation"
url: "https://github.com/juanfont/headscale/blob/main/docs"
about: "Find documentation about how to configure and run headscale."
- name: "headscale Discord community"
url: "https://discord.com/invite/XcQxk2VHjx"
about: "Please ask and answer questions about usage of headscale here."

View File

@ -0,0 +1,15 @@
---
name: "Feature request"
about: "Suggest an idea for headscale"
title: ""
labels: ["enhancement"]
assignees: ""
---
**Feature request**
<!-- A clear and precise description of what new or changed feature you want. -->
<!-- Please include the reason, why you would need the feature. E.g. what problem
does it solve? Or which workflow is currently frustrating and will be improved by
this? -->

28
.github/ISSUE_TEMPLATE/other_issue.md vendored Normal file
View File

@ -0,0 +1,28 @@
---
name: "Other issue"
about: "Report a different issue"
title: ""
labels: ["bug"]
assignees: ""
---
<!-- If you have a question, please consider using our Discord for asking questions -->
**Issue description**
<!-- Please add your issue description. -->
**To Reproduce**
<!-- Steps to reproduce the behavior. -->
**Context info**
<!-- Please add relevant information about your system. For example:
- Version of headscale used
- Version of tailscale client
- OS (e.g. Linux, Mac, Cygwin, WSL, etc.) and version
- Kernel version
- The relevant config parameters you used
- Log output
-->

10
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,10 @@
<!-- Please tick if the following things apply. You… -->
- [] read the [CONTRIBUTING guidelines](README.md#user-content-contributing)
- [] raised a GitHub issue or discussed it on the projects chat beforehand
- [] added unit tests
- [] added integration tests
- [] updated documentation if needed
- [] updated CHANGELOG.md
<!-- If applicable, please reference the issue using `Fixes #XXX` and add tests to cover your new code. -->

View File

@ -14,23 +14,38 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v14.1
with:
files: |
go.*
**/*.go
integration_test/
config-example.yaml
- name: Setup Go - name: Setup Go
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: "1.16.3" go-version: "1.17"
- name: Install dependencies - name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: | run: |
go version go version
go install golang.org/x/lint/golint@latest
sudo apt update sudo apt update
sudo apt install -y make sudo apt install -y make
- name: Run lint - name: Run build
if: steps.changed-files.outputs.any_changed == 'true'
run: make build run: make build
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: steps.changed-files.outputs.any_changed == 'true'
with: with:
name: headscale-linux name: headscale-linux
path: headscale path: headscale

View File

@ -1,39 +1,74 @@
---
name: CI name: CI
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
# The "build" workflow golangci-lint:
lint:
# The type of runner that the job will run on
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2 - uses: actions/checkout@v2
# Install and run golangci-lint as a separate step, it's much faster this
# way because this action has caching. It'll get run again in `make lint`
# below, but it's still much faster in the end than installing
# golangci-lint manually in the `Run lint` step.
- uses: golangci/golangci-lint-action@v2
with: with:
args: --timeout 5m fetch-depth: 2
# Setup Go - name: Get changed files
- name: Setup Go id: changed-files
uses: actions/setup-go@v2 uses: tj-actions/changed-files@v14.1
with: with:
go-version: "1.16.3" # The Go version to download (if necessary) and use. files: |
go.*
**/*.go
integration_test/
config-example.yaml
# Install all the dependencies - name: golangci-lint
- name: Install dependencies if: steps.changed-files.outputs.any_changed == 'true'
run: | uses: golangci/golangci-lint-action@v2
go version with:
go install golang.org/x/lint/golint@latest version: latest
sudo apt update
sudo apt install -y make
- name: Run lint # Only block PRs on new problems.
run: make lint # If this is not enabled, we will end up having PRs
# blocked because new linters has appared and other
# parts of the code is affected.
only-new-issues: true
prettier-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v14.1
with:
files: |
**/*.md
**/*.yml
**/*.yaml
**/*.ts
**/*.js
**/*.sass
**/*.css
**/*.scss
**/*.html
- name: Prettify code
if: steps.changed-files.outputs.any_changed == 'true'
uses: creyD/prettier_action@v4.0
with:
prettier_options: >-
--check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}
only_changed: false
dry: true
proto-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: bufbuild/buf-setup-action@v0.7.0
- uses: bufbuild/buf-lint-action@v1
with:
input: "proto"

View File

@ -18,7 +18,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.16 go-version: 1.17
- name: Install dependencies - name: Install dependencies
run: | run: |
@ -40,6 +40,19 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
fetch-depth: 0 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
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v3
@ -52,6 +65,7 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=raw,value=latest
type=sha type=sha
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1
@ -72,3 +86,138 @@ jobs:
context: . context: .
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
docker-debug-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-debug
key: ${{ runner.os }}-buildx-debug-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-debug-
- name: Docker meta
id: meta-debug
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}}-debug
type=semver,pattern={{major}}.{{minor}}-debug
type=semver,pattern={{major}}-debug
type=raw,value=latest-debug
type=sha,suffix=-debug
- 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.debug
tags: ${{ steps.meta-debug.outputs.tags }}
labels: ${{ steps.meta-debug.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-debug
cache-to: type=local,dest=/tmp/.buildx-cache-debug-new
- name: Prepare cache for next build
run: |
rm -rf /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
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache-alpine
mv /tmp/.buildx-cache-alpine-new /tmp/.buildx-cache-alpine

View File

@ -3,21 +3,30 @@ name: CI
on: [pull_request] on: [pull_request]
jobs: jobs:
# The "build" workflow
integration-test: integration-test:
# The type of runner that the job will run on
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v14.1
with:
files: |
go.*
**/*.go
integration_test/
config-example.yaml
# Setup Go
- name: Setup Go - name: Setup Go
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: "1.16.3" go-version: "1.17"
- name: Run Integration tests - name: Run Integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: go test -tags integration -timeout 30m run: go test -tags integration -timeout 30m

View File

@ -3,31 +3,41 @@ name: CI
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
# The "build" workflow
test: test:
# The type of runner that the job will run on
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v14.1
with:
files: |
go.*
**/*.go
integration_test/
config-example.yaml
# Setup Go
- name: Setup Go - name: Setup Go
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: "1.16.3" # The Go version to download (if necessary) and use. go-version: "1.17"
# Install all the dependencies
- name: Install dependencies - name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: | run: |
go version go version
sudo apt update sudo apt update
sudo apt install -y make sudo apt install -y make
- name: Run tests - name: Run tests
if: steps.changed-files.outputs.any_changed == 'true'
run: make test run: make test
- name: Run build - name: Run build
if: steps.changed-files.outputs.any_changed == 'true'
run: make run: make

2
.gitignore vendored
View File

@ -17,6 +17,8 @@
/headscale /headscale
config.json config.json
config.yaml config.yaml
derp.yaml
*.hujson
*.key *.key
/db.sqlite /db.sqlite
*.sqlite3 *.sqlite3

56
.golangci.yaml Normal file
View File

@ -0,0 +1,56 @@
---
run:
timeout: 10m
issues:
skip-dirs:
- gen
linters:
enable-all: true
disable:
- exhaustivestruct
- revive
- lll
- interfacer
- scopelint
- maligned
- golint
- gofmt
- gochecknoglobals
- gochecknoinits
- gocognit
- funlen
- exhaustivestruct
- tagliatelle
- godox
- ireturn
# We should strive to enable these:
- wrapcheck
- dupl
- makezero
# We might want to enable this, but it might be a lot of work
- cyclop
- nestif
- wsl # might be incompatible with gofumpt
- testpackage
- paralleltest
linters-settings:
varnamelen:
ignore-type-assert-ok: true
ignore-map-index-ok: true
ignore-names:
- err
- db
- id
- ip
- ok
- c
gocritic:
disabled-checks:
- appendAssign
# TODO(kradalby): Remove this
- ifElseChain

View File

@ -1,8 +1,11 @@
# This is an example .goreleaser.yml file with some sane defaults. ---
# Make sure to check the documentation at http://goreleaser.com
before: before:
hooks: hooks:
- go mod tidy - go mod tidy -compat=1.17
release:
prerelease: auto
builds: builds:
- id: darwin-amd64 - id: darwin-amd64
main: ./cmd/headscale/headscale.go main: ./cmd/headscale/headscale.go
@ -29,7 +32,7 @@ builds:
goarch: goarch:
- arm - arm
goarm: goarm:
- 7 - "7"
env: env:
- CC=arm-linux-gnueabihf-gcc - CC=arm-linux-gnueabihf-gcc
- CXX=arm-linux-gnueabihf-g++ - CXX=arm-linux-gnueabihf-g++

60
CHANGELOG.md Normal file
View File

@ -0,0 +1,60 @@
# CHANGELOG
**TBD (TBD):**
**Changes**:
**0.12.4 (2022-01-29):**
**Changes**:
- Make gRPC Unix Socket permissions configurable [#292](https://github.com/juanfont/headscale/pull/292)
- Trim whitespace before reading Private Key from file [#289](https://github.com/juanfont/headscale/pull/289)
- Add new command to generate a private key for `headscale` [#290](https://github.com/juanfont/headscale/pull/290)
- Fixed issue where hosts deleted from control server may be written back to the database, as long as they are connected to the control server [#278](https://github.com/juanfont/headscale/pull/278)
**0.12.3 (2022-01-13):**
**Changes**:
- Added Alpine container [#270](https://github.com/juanfont/headscale/pull/270)
- Minor updates in dependencies [#271](https://github.com/juanfont/headscale/pull/271)
**0.12.2 (2022-01-11):**
Happy New Year!
**Changes**:
- Fix Docker release [#258](https://github.com/juanfont/headscale/pull/258)
- Rewrite main docs [#262](https://github.com/juanfont/headscale/pull/262)
- Improve Docker docs [#263](https://github.com/juanfont/headscale/pull/263)
**0.12.1 (2021-12-24):**
(We are skipping 0.12.0 to correct a mishap done weeks ago with the version tagging)
**BREAKING**:
- Upgrade to Tailscale 1.18 [#229](https://github.com/juanfont/headscale/pull/229)
- This change requires a new format for private key, private keys are now generated automatically:
1. Delete your current key
2. Restart `headscale`, a new key will be generated.
3. Restart all Tailscale clients to fetch the new key
**Changes**:
- Unify configuration example [#197](https://github.com/juanfont/headscale/pull/197)
- Add stricter linting and formatting [#223](https://github.com/juanfont/headscale/pull/223)
**Features**:
- Add gRPC and HTTP API (HTTP API is currently disabled) [#204](https://github.com/juanfont/headscale/pull/204)
- Use gRPC between the CLI and the server [#206](https://github.com/juanfont/headscale/pull/206), [#212](https://github.com/juanfont/headscale/pull/212)
- Beta OpenID Connect support [#126](https://github.com/juanfont/headscale/pull/126), [#227](https://github.com/juanfont/headscale/pull/227)
**0.11.0 (2021-10-25):**
**BREAKING**:
- Make headscale fetch DERP map from URL and file [#196](https://github.com/juanfont/headscale/pull/196)

View File

@ -1,23 +1,21 @@
FROM golang:1.17.1-bullseye AS build # Builder image
FROM golang:1.17.6-bullseye AS build
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale
COPY go.mod go.sum /go/src/headscale/ COPY go.mod go.sum /go/src/headscale/
WORKDIR /go/src/headscale
RUN go mod download RUN go mod download
COPY . /go/src/headscale COPY . .
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale RUN test -e /go/bin/headscale
FROM ubuntu:20.04 # Production image
FROM gcr.io/distroless/base-debian11
RUN apt-get update \ COPY --from=build /go/bin/headscale /bin/headscale
&& apt-get install -y ca-certificates \
&& update-ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /go/bin/headscale /usr/local/bin/headscale
ENV TZ UTC ENV TZ UTC
EXPOSE 8080/tcp EXPOSE 8080/tcp

23
Dockerfile.alpine Normal file
View File

@ -0,0 +1,23 @@
# Builder image
FROM golang:1.17.6-alpine AS build
ENV GOPATH /go
WORKDIR /go/src/headscale
COPY go.mod go.sum /go/src/headscale/
RUN apk add gcc musl-dev
RUN go mod download
COPY . .
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale
# Production image
FROM alpine:latest
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
EXPOSE 8080/tcp
CMD ["headscale"]

23
Dockerfile.debug Normal file
View File

@ -0,0 +1,23 @@
# Builder image
FROM golang:1.17.1-bullseye AS build
ENV GOPATH /go
WORKDIR /go/src/headscale
COPY go.mod go.sum /go/src/headscale/
RUN go mod download
COPY . .
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
RUN test -e /go/bin/headscale
# Debug image
FROM gcr.io/distroless/base-debian11:debug
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
# Need to reset the entrypoint or everything will run as a busybox script
ENTRYPOINT []
EXPOSE 8080/tcp
CMD ["headscale"]

View File

@ -1,6 +1,14 @@
# Calculate version # Calculate version
version = $(shell ./scripts/version-at-commit.sh) version = $(shell ./scripts/version-at-commit.sh)
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
# GO_SOURCES = $(wildcard *.go)
# PROTO_SOURCES = $(wildcard **/*.proto)
GO_SOURCES = $(call rwildcard,,*.go)
PROTO_SOURCES = $(call rwildcard,,*.proto)
build: build:
go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
@ -12,6 +20,9 @@ test:
test_integration: test_integration:
go test -tags integration -timeout 30m ./... go test -tags integration -timeout 30m ./...
test_integration_cli:
go test -tags integration -v integration_cli_test.go integration_common_test.go
coverprofile_func: coverprofile_func:
go tool cover -func=coverage.out go tool cover -func=coverage.out
@ -19,9 +30,26 @@ coverprofile_html:
go tool cover -html=coverage.out go tool cover -html=coverage.out
lint: lint:
golint golangci-lint run --fix --timeout 10m
golangci-lint run --timeout 5m
fmt:
prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}'
golines --max-len=88 --base-formatter=gofumpt -w $(GO_SOURCES)
clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i $(PROTO_SOURCES)
proto-lint:
cd proto/ && buf lint
compress: build compress: build
upx --brute headscale upx --brute headscale
generate:
rm -rf gen
buf generate proto
install-protobuf-plugins:
go install \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc

View File

@ -6,6 +6,8 @@ An open source, self-hosted implementation of the Tailscale coordination server.
Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat. Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat.
**Note:** Always select the same GitHub tag as the released version you use to ensure you have the correct example configuration and documentation. The `main` branch might contain unreleased changes.
## Overview ## Overview
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using all kinds of [NAT traversal sorcery](https://tailscale.com/blog/how-nat-traversal-works/). Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using all kinds of [NAT traversal sorcery](https://tailscale.com/blog/how-nat-traversal-works/).
@ -29,6 +31,7 @@ headscale implements this coordination server.
- [x] Taildrop (File Sharing) - [x] Taildrop (File Sharing)
- [x] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) - [x] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10)
- [x] DNS (passing DNS servers to nodes) - [x] DNS (passing DNS servers to nodes)
- [x] Single-Sign-On (via Open ID Connect)
- [x] Share nodes between namespaces - [x] Share nodes between namespaces
- [x] MagicDNS (see `docs/`) - [x] MagicDNS (see `docs/`)
@ -47,17 +50,67 @@ headscale implements this coordination server.
Suggestions/PRs welcomed! Suggestions/PRs welcomed!
## Running headscale ## Running headscale
Please have a look at the documentation under [`docs/`](docs/). Please have a look at the documentation under [`docs/`](docs/).
## Disclaimer ## Disclaimer
1. We have nothing to do with Tailscale, or Tailscale Inc. 1. We have nothing to do with Tailscale, or Tailscale Inc.
2. The purpose of writing this was to learn how Tailscale works. 2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel.
## Contributing
To contribute to Headscale you would need the lastest version of [Go](https://golang.org) and [Buf](https://buf.build)(Protobuf generator).
### Code style
To ensure we have some consistency with a growing number of contributions, this project has adopted linting and style/formatting rules:
The **Go** code is linted with [`golangci-lint`](https://golangci-lint.run) and
formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and
[`gofumpt`](https://github.com/mvdan/gofumpt).
Please configure your editor to run the tools while developing and make sure to
run `make lint` and `make fmt` before committing any code.
The **Proto** code is linted with [`buf`](https://docs.buf.build/lint/overview) and
formatted with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html).
The **rest** (Markdown, YAML, etc) is formatted with [`prettier`](https://prettier.io).
Check out the `.golangci.yaml` and `Makefile` to see the specific configuration.
### Install development tools
- Go
- Buf
- Protobuf tools:
```shell
make install-protobuf-plugins
```
### Testing and building
Some parts of the project require the generation of Go code from Protobuf (if changes are made in `proto/`) and it must be (re-)generated with:
```shell
make generate
```
**Note**: Please check in changes from `gen/` in a separate commit to make it easier to review.
To run the tests:
```shell
make test
```
To build the program:
```shell
make build
```
## Contributors ## Contributors
@ -91,6 +144,13 @@ Please have a look at the documentation under [`docs/`](docs/).
<sub style="font-size:14px"><b>ohdearaugustin</b></sub> <sub style="font-size:14px"><b>ohdearaugustin</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/unreality>
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
<br />
<sub style="font-size:14px"><b>unreality</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/>
@ -98,6 +158,8 @@ Please have a look at the documentation under [`docs/`](docs/).
<sub style="font-size:14px"><b>Aaron Bieber</b></sub> <sub style="font-size:14px"><b>Aaron Bieber</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/>
@ -105,8 +167,6 @@ Please have a look at the documentation under [`docs/`](docs/).
<sub style="font-size:14px"><b>Paul Tötterman</b></sub> <sub style="font-size:14px"><b>Paul Tötterman</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/>
@ -142,6 +202,8 @@ Please have a look at the documentation under [`docs/`](docs/).
<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/>
@ -149,8 +211,6 @@ Please have a look at the documentation under [`docs/`](docs/).
<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/shaananc> <a href=https://github.com/shaananc>
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/> <img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
@ -186,6 +246,8 @@ Please have a look at the documentation under [`docs/`](docs/).
<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/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/>
@ -193,8 +255,6 @@ Please have a look at the documentation under [`docs/`](docs/).
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub> <sub style="font-size:14px"><b>Zakhar Bessarab</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/derelm> <a href=https://github.com/derelm>
<img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/> <img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/>
@ -218,5 +278,3 @@ Please have a look at the documentation under [`docs/`](docs/).
</td> </td>
</tr> </tr>
</table> </table>

196
acls.go
View File

@ -9,22 +9,39 @@ import (
"strings" "strings"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/tailscale/hujson" "github.com/tailscale/hujson"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
const errorEmptyPolicy = Error("empty policy") const (
const errorInvalidAction = Error("invalid action") errEmptyPolicy = Error("empty policy")
const errorInvalidUserSection = Error("invalid user section") errInvalidAction = Error("invalid action")
const errorInvalidGroup = Error("invalid group") errInvalidUserSection = Error("invalid user section")
const errorInvalidTag = Error("invalid tag") errInvalidGroup = Error("invalid group")
const errorInvalidNamespace = Error("invalid namespace") errInvalidTag = Error("invalid tag")
const errorInvalidPortFormat = Error("invalid port format") errInvalidNamespace = Error("invalid namespace")
errInvalidPortFormat = Error("invalid port format")
)
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules const (
Base8 = 8
Base10 = 10
BitSize16 = 16
BitSize32 = 32
BitSize64 = 64
portRangeBegin = 0
portRangeEnd = 65535
expectedTokenItems = 2
)
// 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().
Str("func", "LoadACLPolicy").
Str("path", path).
Msg("Loading ACL policy from path")
policyFile, err := os.Open(path) policyFile, err := os.Open(path)
if err != nil { if err != nil {
return err return err
@ -32,16 +49,23 @@ func (h *Headscale) LoadACLPolicy(path string) error {
defer policyFile.Close() defer policyFile.Close()
var policy ACLPolicy var policy ACLPolicy
b, err := io.ReadAll(policyFile) policyBytes, err := io.ReadAll(policyFile)
if err != nil { if err != nil {
return err return err
} }
err = hujson.Unmarshal(b, &policy)
ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}
ast.Standardize()
policyBytes = ast.Pack()
err = json.Unmarshal(policyBytes, &policy)
if err != nil { if err != nil {
return err return err
} }
if policy.IsZero() { if policy.IsZero() {
return errorEmptyPolicy return errEmptyPolicy
} }
h.aclPolicy = &policy h.aclPolicy = &policy
@ -50,40 +74,45 @@ func (h *Headscale) LoadACLPolicy(path string) error {
return err return err
} }
h.aclRules = rules h.aclRules = rules
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
return nil return nil
} }
func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
rules := []tailcfg.FilterRule{} rules := []tailcfg.FilterRule{}
for i, a := range h.aclPolicy.ACLs { for index, acl := range h.aclPolicy.ACLs {
if a.Action != "accept" { if acl.Action != "accept" {
return nil, errorInvalidAction return nil, errInvalidAction
} }
r := tailcfg.FilterRule{} filterRule := tailcfg.FilterRule{}
srcIPs := []string{} srcIPs := []string{}
for j, u := range a.Users { for innerIndex, user := range acl.Users {
srcs, err := h.generateACLPolicySrcIP(u) srcs, err := h.generateACLPolicySrcIP(user)
if err != nil { if err != nil {
log.Error(). log.Error().
Msgf("Error parsing ACL %d, User %d", i, j) Msgf("Error parsing ACL %d, User %d", index, innerIndex)
return nil, err return nil, err
} }
srcIPs = append(srcIPs, *srcs...) srcIPs = append(srcIPs, srcs...)
} }
r.SrcIPs = srcIPs filterRule.SrcIPs = srcIPs
destPorts := []tailcfg.NetPortRange{} destPorts := []tailcfg.NetPortRange{}
for j, d := range a.Ports { for innerIndex, ports := range acl.Ports {
dests, err := h.generateACLPolicyDestPorts(d) dests, err := h.generateACLPolicyDestPorts(ports)
if err != nil { if err != nil {
log.Error(). log.Error().
Msgf("Error parsing ACL %d, Port %d", i, j) Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
return nil, err return nil, err
} }
destPorts = append(destPorts, *dests...) destPorts = append(destPorts, dests...)
} }
rules = append(rules, tailcfg.FilterRule{ rules = append(rules, tailcfg.FilterRule{
@ -92,17 +121,19 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) {
}) })
} }
return &rules, nil return rules, nil
} }
func (h *Headscale) generateACLPolicySrcIP(u string) (*[]string, error) { func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) {
return h.expandAlias(u) return h.expandAlias(u)
} }
func (h *Headscale) generateACLPolicyDestPorts(d string) (*[]tailcfg.NetPortRange, error) { func (h *Headscale) generateACLPolicyDestPorts(
d string,
) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(d, ":") tokens := strings.Split(d, ":")
if len(tokens) < 2 || len(tokens) > 3 { if len(tokens) < expectedTokenItems || len(tokens) > 3 {
return nil, errorInvalidPortFormat return nil, errInvalidPortFormat
} }
var alias string var alias string
@ -112,7 +143,7 @@ func (h *Headscale) generateACLPolicyDestPorts(d string) (*[]tailcfg.NetPortRang
// tag:montreal-webserver:80,443 // tag:montreal-webserver:80,443
// tag:api-server:443 // tag:api-server:443
// example-host-1:* // example-host-1:*
if len(tokens) == 2 { if len(tokens) == expectedTokenItems {
alias = tokens[0] alias = tokens[0]
} else { } else {
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1]) alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
@ -128,7 +159,7 @@ func (h *Headscale) generateACLPolicyDestPorts(d string) (*[]tailcfg.NetPortRang
} }
dests := []tailcfg.NetPortRange{} dests := []tailcfg.NetPortRange{}
for _, d := range *expanded { for _, d := range expanded {
for _, p := range *ports { for _, p := range *ports {
pr := tailcfg.NetPortRange{ pr := tailcfg.NetPortRange{
IP: d, IP: d,
@ -137,34 +168,36 @@ func (h *Headscale) generateACLPolicyDestPorts(d string) (*[]tailcfg.NetPortRang
dests = append(dests, pr) dests = append(dests, pr)
} }
} }
return &dests, nil
return dests, nil
} }
func (h *Headscale) expandAlias(s string) (*[]string, error) { func (h *Headscale) expandAlias(alias string) ([]string, error) {
if s == "*" { if alias == "*" {
return &[]string{"*"}, nil return []string{"*"}, nil
} }
if strings.HasPrefix(s, "group:") { if strings.HasPrefix(alias, "group:") {
if _, ok := h.aclPolicy.Groups[s]; !ok { if _, ok := h.aclPolicy.Groups[alias]; !ok {
return nil, errorInvalidGroup return nil, errInvalidGroup
} }
ips := []string{} ips := []string{}
for _, n := range h.aclPolicy.Groups[s] { for _, n := range h.aclPolicy.Groups[alias] {
nodes, err := h.ListMachinesInNamespace(n) nodes, err := h.ListMachinesInNamespace(n)
if err != nil { if err != nil {
return nil, errorInvalidNamespace return nil, errInvalidNamespace
} }
for _, node := range *nodes { for _, node := range nodes {
ips = append(ips, node.IPAddress) ips = append(ips, node.IPAddress)
} }
} }
return &ips, nil
return ips, nil
} }
if strings.HasPrefix(s, "tag:") { if strings.HasPrefix(alias, "tag:") {
if _, ok := h.aclPolicy.TagOwners[s]; !ok { if _, ok := h.aclPolicy.TagOwners[alias]; !ok {
return nil, errorInvalidTag return nil, errInvalidTag
} }
// This will have HORRIBLE performance. // This will have HORRIBLE performance.
@ -174,10 +207,10 @@ func (h *Headscale) expandAlias(s string) (*[]string, error) {
return nil, err return nil, err
} }
ips := []string{} ips := []string{}
for _, m := range machines { for _, machine := range machines {
hostinfo := tailcfg.Hostinfo{} hostinfo := tailcfg.Hostinfo{}
if len(m.HostInfo) != 0 { if len(machine.HostInfo) != 0 {
hi, err := m.HostInfo.MarshalJSON() hi, err := machine.HostInfo.MarshalJSON()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -188,69 +221,76 @@ func (h *Headscale) expandAlias(s string) (*[]string, error) {
// FIXME: Check TagOwners allows this // FIXME: Check TagOwners allows this
for _, t := range hostinfo.RequestTags { for _, t := range hostinfo.RequestTags {
if s[4:] == t { if alias[4:] == t {
ips = append(ips, m.IPAddress) ips = append(ips, machine.IPAddress)
break break
} }
} }
} }
} }
return &ips, nil
return ips, nil
} }
n, err := h.GetNamespace(s) n, err := h.GetNamespace(alias)
if err == nil { if err == nil {
nodes, err := h.ListMachinesInNamespace(n.Name) nodes, err := h.ListMachinesInNamespace(n.Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ips := []string{} ips := []string{}
for _, n := range *nodes { for _, n := range nodes {
ips = append(ips, n.IPAddress) ips = append(ips, n.IPAddress)
} }
return &ips, nil
return ips, nil
} }
if h, ok := h.aclPolicy.Hosts[s]; ok { if h, ok := h.aclPolicy.Hosts[alias]; ok {
return &[]string{h.String()}, nil return []string{h.String()}, nil
} }
ip, err := netaddr.ParseIP(s) ip, err := netaddr.ParseIP(alias)
if err == nil { if err == nil {
return &[]string{ip.String()}, nil return []string{ip.String()}, nil
} }
cidr, err := netaddr.ParseIPPrefix(s) cidr, err := netaddr.ParseIPPrefix(alias)
if err == nil { if err == nil {
return &[]string{cidr.String()}, nil return []string{cidr.String()}, nil
} }
return nil, errorInvalidUserSection return nil, errInvalidUserSection
} }
func (h *Headscale) expandPorts(s string) (*[]tailcfg.PortRange, error) { func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
if s == "*" { if portsStr == "*" {
return &[]tailcfg.PortRange{{First: 0, Last: 65535}}, nil return &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
}, nil
} }
ports := []tailcfg.PortRange{} ports := []tailcfg.PortRange{}
for _, p := range strings.Split(s, ",") { for _, portStr := range strings.Split(portsStr, ",") {
rang := strings.Split(p, "-") rang := strings.Split(portStr, "-")
if len(rang) == 1 { switch len(rang) {
pi, err := strconv.ParseUint(rang[0], 10, 16) case 1:
port, err := strconv.ParseUint(rang[0], Base10, BitSize16)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ports = append(ports, tailcfg.PortRange{ ports = append(ports, tailcfg.PortRange{
First: uint16(pi), First: uint16(port),
Last: uint16(pi), Last: uint16(port),
}) })
} else if len(rang) == 2 {
start, err := strconv.ParseUint(rang[0], 10, 16) case expectedTokenItems:
start, err := strconv.ParseUint(rang[0], Base10, BitSize16)
if err != nil { if err != nil {
return nil, err return nil, err
} }
last, err := strconv.ParseUint(rang[1], 10, 16) last, err := strconv.ParseUint(rang[1], Base10, BitSize16)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -258,9 +298,11 @@ func (h *Headscale) expandPorts(s string) (*[]tailcfg.PortRange, error) {
First: uint16(start), First: uint16(start),
Last: uint16(last), Last: uint16(last),
}) })
} else {
return nil, errorInvalidPortFormat default:
return nil, errInvalidPortFormat
} }
} }
return &ports, nil return &ports, nil
} }

View File

@ -5,156 +5,161 @@ import (
) )
func (s *Suite) TestWrongPath(c *check.C) { func (s *Suite) TestWrongPath(c *check.C) {
err := h.LoadACLPolicy("asdfg") err := app.LoadACLPolicy("asdfg")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
} }
func (s *Suite) TestBrokenHuJson(c *check.C) { func (s *Suite) TestBrokenHuJson(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/broken.hujson") err := app.LoadACLPolicy("./tests/acls/broken.hujson")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
} }
func (s *Suite) TestInvalidPolicyHuson(c *check.C) { func (s *Suite) TestInvalidPolicyHuson(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/invalid.hujson") err := app.LoadACLPolicy("./tests/acls/invalid.hujson")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
c.Assert(err, check.Equals, errorEmptyPolicy) c.Assert(err, check.Equals, errEmptyPolicy)
} }
func (s *Suite) TestParseHosts(c *check.C) { func (s *Suite) TestParseHosts(c *check.C) {
var hs Hosts var hosts Hosts
err := hs.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100","example-host-2": "100.100.101.100/24"}`)) err := hosts.UnmarshalJSON(
c.Assert(hs, check.NotNil) []byte(
`{"example-host-1": "100.100.100.100","example-host-2": "100.100.101.100/24"}`,
),
)
c.Assert(hosts, check.NotNil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
} }
func (s *Suite) TestParseInvalidCIDR(c *check.C) { func (s *Suite) TestParseInvalidCIDR(c *check.C) {
var hs Hosts var hosts Hosts
err := hs.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100/42"}`)) err := hosts.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100/42"}`))
c.Assert(hs, check.IsNil) c.Assert(hosts, check.IsNil)
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
} }
func (s *Suite) TestRuleInvalidGeneration(c *check.C) { func (s *Suite) TestRuleInvalidGeneration(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson") err := app.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
} }
func (s *Suite) TestBasicRule(c *check.C) { func (s *Suite) TestBasicRule(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson") err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := h.generateACLRules() rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
} }
func (s *Suite) TestPortRange(c *check.C) { func (s *Suite) TestPortRange(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson") err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := h.generateACLRules() rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
c.Assert(*rules, check.HasLen, 1) c.Assert(rules, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts, check.HasLen, 1) c.Assert((rules)[0].DstPorts, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(5400)) c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(5400))
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500)) c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500))
} }
func (s *Suite) TestPortWildcard(c *check.C) { func (s *Suite) TestPortWildcard(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := h.generateACLRules() rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
c.Assert(*rules, check.HasLen, 1) c.Assert(rules, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts, check.HasLen, 1) c.Assert((rules)[0].DstPorts, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert((*rules)[0].SrcIPs, check.HasLen, 1) c.Assert((rules)[0].SrcIPs, check.HasLen, 1)
c.Assert((*rules)[0].SrcIPs[0], check.Equals, "*") c.Assert((rules)[0].SrcIPs[0], check.Equals, "*")
} }
func (s *Suite) TestPortNamespace(c *check.C) { func (s *Suite) TestPortNamespace(c *check.C) {
n, err := h.CreateNamespace("testnamespace") namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = h.GetMachine("testnamespace", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
ip, _ := h.getAvailableIP() ip, _ := app.getAvailableIP()
m := Machine{ machine := Machine{
ID: 0, ID: 0,
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: n.ID, NamespaceID: namespace.ID,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: ip.String(), IPAddress: ip.String(),
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
h.db.Save(&m) app.db.Save(&machine)
err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson") err = app.LoadACLPolicy(
"./tests/acls/acl_policy_basic_namespace_as_user.hujson",
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := h.generateACLRules() rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
c.Assert(*rules, check.HasLen, 1) c.Assert(rules, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts, check.HasLen, 1) c.Assert((rules)[0].DstPorts, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert((*rules)[0].SrcIPs, check.HasLen, 1) c.Assert((rules)[0].SrcIPs, check.HasLen, 1)
c.Assert((*rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip") c.Assert((rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip")
c.Assert((*rules)[0].SrcIPs[0], check.Equals, ip.String()) c.Assert((rules)[0].SrcIPs[0], check.Equals, ip.String())
} }
func (s *Suite) TestPortGroup(c *check.C) { func (s *Suite) TestPortGroup(c *check.C) {
n, err := h.CreateNamespace("testnamespace") namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = h.GetMachine("testnamespace", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
ip, _ := h.getAvailableIP() ip, _ := app.getAvailableIP()
m := Machine{ machine := Machine{
ID: 0, ID: 0,
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: n.ID, NamespaceID: namespace.ID,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: ip.String(), IPAddress: ip.String(),
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
h.db.Save(&m) app.db.Save(&machine)
err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson") err = app.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := h.generateACLRules() rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
c.Assert(*rules, check.HasLen, 1) c.Assert(rules, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts, check.HasLen, 1) c.Assert((rules)[0].DstPorts, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert((*rules)[0].SrcIPs, check.HasLen, 1) c.Assert((rules)[0].SrcIPs, check.HasLen, 1)
c.Assert((*rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip") c.Assert((rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip")
c.Assert((*rules)[0].SrcIPs[0], check.Equals, ip.String()) c.Assert((rules)[0].SrcIPs[0], check.Equals, ip.String())
} }

View File

@ -1,13 +1,14 @@
package headscale package headscale
import ( import (
"encoding/json"
"strings" "strings"
"github.com/tailscale/hujson" "github.com/tailscale/hujson"
"inet.af/netaddr" "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"` Groups Groups `json:"Groups"`
Hosts Hosts `json:"Hosts"` Hosts Hosts `json:"Hosts"`
@ -16,55 +17,63 @@ type ACLPolicy struct {
Tests []ACLTest `json:"Tests"` Tests []ACLTest `json:"Tests"`
} }
// 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"` Action string `json:"Action"`
Users []string `json:"Users"` Users []string `json:"Users"`
Ports []string `json:"Ports"` Ports []string `json:"Ports"`
} }
// 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]netaddr.IPPrefix 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 {
User string `json:"User"` User string `json:"User"`
Allow []string `json:"Allow"` Allow []string `json:"Allow"`
Deny []string `json:"Deny,omitempty"` Deny []string `json:"Deny,omitempty"`
} }
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects // UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
func (h *Hosts) UnmarshalJSON(data []byte) error { func (hosts *Hosts) UnmarshalJSON(data []byte) error {
hosts := Hosts{} newHosts := Hosts{}
hs := make(map[string]string) hostIPPrefixMap := make(map[string]string)
err := hujson.Unmarshal(data, &hs) ast, err := hujson.Parse(data)
if err != nil { if err != nil {
return err return err
} }
for k, v := range hs { ast.Standardize()
if !strings.Contains(v, "/") { data = ast.Pack()
v = v + "/32" err = json.Unmarshal(data, &hostIPPrefixMap)
if err != nil {
return err
}
for host, prefixStr := range hostIPPrefixMap {
if !strings.Contains(prefixStr, "/") {
prefixStr += "/32"
} }
prefix, err := netaddr.ParseIPPrefix(v) prefix, err := netaddr.ParseIPPrefix(prefixStr)
if err != nil { if err != nil {
return err return err
} }
hosts[k] = prefix newHosts[host] = prefix
} }
*h = hosts *hosts = newHosts
return nil return nil
} }
// IsZero is perhaps a bit naive here // IsZero is perhaps a bit naive here.
func (p ACLPolicy) IsZero() bool { func (policy ACLPolicy) IsZero() bool {
if len(p.Groups) == 0 && len(p.Hosts) == 0 && len(p.ACLs) == 0 { if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 {
return true return true
} }
return false return false
} }

644
api.go
View File

@ -1,40 +1,51 @@
package headscale package headscale
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"html/template"
"io" "io"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
"github.com/rs/zerolog/log"
"gorm.io/gorm" "gorm.io/gorm"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/wgkey" "tailscale.com/types/key"
)
const (
reservedResponseHeaderSize = 4
RegisterMethodAuthKey = "authKey"
RegisterMethodOIDC = "oidc"
RegisterMethodCLI = "cli"
ErrRegisterMethodCLIDoesNotSupportExpire = Error(
"machines registered with CLI does not support expire",
)
) )
// KeyHandler provides the Headscale pub key // KeyHandler provides the Headscale pub key
// Listens in /key // Listens in /key.
func (h *Headscale) KeyHandler(c *gin.Context) { func (h *Headscale) KeyHandler(ctx *gin.Context) {
c.Data(200, "text/plain; charset=utf-8", []byte(h.publicKey.HexString())) ctx.Data(
http.StatusOK,
"text/plain; charset=utf-8",
[]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())),
)
} }
// RegisterWebAPI shows a simple message in the browser to point to the CLI type registerWebAPITemplateConfig struct {
// Listens in /register Key string
func (h *Headscale) RegisterWebAPI(c *gin.Context) { }
mKeyStr := c.Query("key")
if mKeyStr == "" {
c.String(http.StatusBadRequest, "Wrong params")
return
}
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(` var registerWebAPITemplate = template.Must(
<html> template.New("registerweb").Parse(`<html>
<body> <body>
<h1>headscale</h1> <h1>headscale</h1>
<p> <p>
@ -43,228 +54,193 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) {
<p> <p>
<code> <code>
<b>headscale -n NAMESPACE nodes register %s</b> <b>headscale -n NAMESPACE nodes register --key {{.Key}}</b>
</code> </code>
</p> </p>
</body> </body>
</html> </html>`),
)
`, mKeyStr))) // RegisterWebAPI shows a simple message in the browser to point to the CLI
// Listens in /register.
func (h *Headscale) RegisterWebAPI(ctx *gin.Context) {
machineKeyStr := ctx.Query("key")
if machineKeyStr == "" {
ctx.String(http.StatusBadRequest, "Wrong params")
return
}
var content bytes.Buffer
if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
Key: machineKeyStr,
}); err != nil {
log.Error().
Str("func", "RegisterWebAPI").
Err(err).
Msg("Could not render register web API template")
ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render register web API template"),
)
}
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
} }
// RegistrationHandler handles the actual registration process of a machine // RegistrationHandler handles the actual registration process of a machine
// Endpoint /machine/:id // Endpoint /machine/:id.
func (h *Headscale) RegistrationHandler(c *gin.Context) { func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
body, _ := io.ReadAll(c.Request.Body) body, _ := io.ReadAll(ctx.Request.Body)
mKeyStr := c.Param("id") machineKeyStr := ctx.Param("id")
mKey, err := wgkey.ParseHex(mKeyStr)
var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
if err != nil { if err != nil {
log.Error(). log.Error().
Str("handler", "Registration"). Caller().
Err(err). Err(err).
Msg("Cannot parse machine key") Msg("Cannot parse machine key")
machineRegistrations.WithLabelValues("unkown", "web", "error", "unknown").Inc() machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
c.String(http.StatusInternalServerError, "Sad!") ctx.String(http.StatusInternalServerError, "Sad!")
return return
} }
req := tailcfg.RegisterRequest{} req := tailcfg.RegisterRequest{}
err = decode(body, &req, &mKey, h.privateKey) err = decode(body, &req, &machineKey, h.privateKey)
if err != nil { if err != nil {
log.Error(). log.Error().
Str("handler", "Registration"). Caller().
Err(err). Err(err).
Msg("Cannot decode message") Msg("Cannot decode message")
machineRegistrations.WithLabelValues("unkown", "web", "error", "unknown").Inc() machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
c.String(http.StatusInternalServerError, "Very sad!") ctx.String(http.StatusInternalServerError, "Very sad!")
return return
} }
now := time.Now().UTC() now := time.Now().UTC()
var m Machine machine, err := h.GetMachineByMachineKey(machineKey)
if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is( if errors.Is(err, gorm.ErrRecordNotFound) {
result.Error,
gorm.ErrRecordNotFound,
) {
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine") log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine")
m = Machine{ newMachine := Machine{
Expiry: &req.Expiry, Expiry: &time.Time{},
MachineKey: mKey.HexString(), MachineKey: MachinePublicKeyStripPrefix(machineKey),
Name: req.Hostinfo.Hostname, Name: req.Hostinfo.Hostname,
NodeKey: wgkey.Key(req.NodeKey).HexString(),
LastSuccessfulUpdate: &now,
} }
if err := h.db.Create(&m).Error; err != nil { if err := h.db.Create(&newMachine).Error; err != nil {
log.Error(). log.Error().
Str("handler", "Registration"). Caller().
Err(err). Err(err).
Msg("Could not create row") Msg("Could not create row")
machineRegistrations.WithLabelValues("unkown", "web", "error", m.Namespace.Name).Inc() machineRegistrations.WithLabelValues("unknown", "web", "error", machine.Namespace.Name).
Inc()
return return
} }
machine = &newMachine
} }
if !m.Registered && req.Auth.AuthKey != "" { if machine.Registered {
h.handleAuthKey(c, h.db, mKey, req, m) // If the NodeKey stored in headscale is the same as the key presented in a registration
return // 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(req.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 !req.Expiry.IsZero() && req.Expiry.UTC().Before(now) {
h.handleMachineLogOut(ctx, machineKey, *machine)
resp := tailcfg.RegisterResponse{}
// We have the updated key!
if m.NodeKey == wgkey.Key(req.NodeKey).HexString() {
if m.Registered {
log.Debug().
Str("handler", "Registration").
Str("machine", m.Name).
Msg("Client is registered and we have the current NodeKey. All clear to /map")
resp.AuthURL = ""
resp.MachineAuthorized = true
resp.User = *m.Namespace.toUser()
respBody, err := encode(resp, &mKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("update", "web", "error", m.Namespace.Name).Inc()
c.String(http.StatusInternalServerError, "")
return return
} }
machineRegistrations.WithLabelValues("update", "web", "success", m.Namespace.Name).Inc()
c.Data(200, "application/json; charset=utf-8", respBody) // 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(ctx, machineKey, *machine)
return
}
}
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
if machine.NodeKey == NodePublicKeyStripPrefix(req.OldNodeKey) &&
!machine.isExpired() {
h.handleMachineRefreshKey(ctx, machineKey, req, *machine)
return return
} }
log.Debug(). // The machine has expired
Str("handler", "Registration"). h.handleMachineExpired(ctx, machineKey, req, *machine)
Str("machine", m.Name).
Msg("Not registered and not NodeKey rotation. Sending a authurl to register")
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
h.cfg.ServerURL, mKey.HexString())
respBody, err := encode(resp, &mKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
machineRegistrations.WithLabelValues("new", "web", "error", m.Namespace.Name).Inc()
c.String(http.StatusInternalServerError, "")
return
}
machineRegistrations.WithLabelValues("new", "web", "success", m.Namespace.Name).Inc()
c.Data(200, "application/json; charset=utf-8", respBody)
return return
} }
// The NodeKey we have matches OldNodeKey, which means this is a refresh after an key expiration // If the machine has AuthKey set, handle registration via PreAuthKeys
if m.NodeKey == wgkey.Key(req.OldNodeKey).HexString() { if req.Auth.AuthKey != "" {
log.Debug(). h.handleAuthKey(ctx, machineKey, req, *machine)
Str("handler", "Registration").
Str("machine", m.Name).
Msg("We have the OldNodeKey in the database. This is a key refresh")
m.NodeKey = wgkey.Key(req.NodeKey).HexString()
h.db.Save(&m)
resp.AuthURL = ""
resp.User = *m.Namespace.toUser()
respBody, err := encode(resp, &mKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
c.String(http.StatusInternalServerError, "Extremely sad!")
return
}
c.Data(200, "application/json; charset=utf-8", respBody)
return return
} }
// We arrive here after a client is restarted without finalizing the authentication flow or h.handleMachineRegistrationNew(ctx, machineKey, req, *machine)
// when headscale is stopped in the middle of the auth process.
if m.Registered {
log.Debug().
Str("handler", "Registration").
Str("machine", m.Name).
Msg("The node is sending us a new NodeKey, but machine is registered. All clear for /map")
resp.AuthURL = ""
resp.MachineAuthorized = true
resp.User = *m.Namespace.toUser()
respBody, err := encode(resp, &mKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
c.String(http.StatusInternalServerError, "")
return
}
c.Data(200, "application/json; charset=utf-8", respBody)
return
}
log.Debug().
Str("handler", "Registration").
Str("machine", m.Name).
Msg("The node is sending us a new NodeKey, sending auth url")
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
h.cfg.ServerURL, mKey.HexString())
respBody, err := encode(resp, &mKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
c.String(http.StatusInternalServerError, "")
return
}
c.Data(200, "application/json; charset=utf-8", respBody)
} }
func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Machine) ([]byte, error) { func (h *Headscale) getMapResponse(
machineKey key.MachinePublic,
req tailcfg.MapRequest,
machine *Machine,
) ([]byte, error) {
log.Trace(). log.Trace().
Str("func", "getMapResponse"). Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname). Str("machine", req.Hostinfo.Hostname).
Msg("Creating Map response") Msg("Creating Map response")
node, err := m.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true) node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller().
Str("func", "getMapResponse"). Str("func", "getMapResponse").
Err(err). Err(err).
Msg("Cannot convert to node") Msg("Cannot convert to node")
return nil, err return nil, err
} }
peers, err := h.getPeers(m) peers, err := h.getValidPeers(machine)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller().
Str("func", "getMapResponse"). Str("func", "getMapResponse").
Err(err). Err(err).
Msg("Cannot fetch peers") Msg("Cannot fetch peers")
return nil, err return nil, err
} }
profiles := getMapResponseUserProfiles(*m, peers) profiles := getMapResponseUserProfiles(*machine, peers)
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true) nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller().
Str("func", "getMapResponse"). Str("func", "getMapResponse").
Err(err). Err(err).
Msg("Failed to convert peers to Tailscale nodes") Msg("Failed to convert peers to Tailscale nodes")
return nil, err return nil, err
} }
dnsConfig, err := getMapResponseDNSConfig(h.cfg.DNSConfig, h.cfg.BaseDomain, *m, peers) dnsConfig := getMapResponseDNSConfig(
if err != nil { h.cfg.DNSConfig,
log.Error(). h.cfg.BaseDomain,
Str("func", "getMapResponse"). *machine,
Err(err). peers,
Msg("Failed generate the DNSConfig") )
return nil, err
}
resp := tailcfg.MapResponse{ resp := tailcfg.MapResponse{
KeepAlive: false, KeepAlive: false,
@ -272,7 +248,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
Peers: nodePeers, Peers: nodePeers,
DNSConfig: dnsConfig, DNSConfig: dnsConfig,
Domain: h.cfg.BaseDomain, Domain: h.cfg.BaseDomain,
PacketFilter: *h.aclRules, PacketFilter: h.aclRules,
DERPMap: h.DERPMap, DERPMap: h.DERPMap,
UserProfiles: profiles, UserProfiles: profiles,
} }
@ -289,137 +265,351 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
encoder, _ := zstd.NewWriter(nil) encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil) srcCompressed := encoder.EncodeAll(src, nil)
respBody, err = encodeMsg(srcCompressed, &mKey, h.privateKey) respBody = h.privateKey.SealTo(machineKey, srcCompressed)
if err != nil {
return nil, err
}
} else { } else {
respBody, err = encode(resp, &mKey, h.privateKey) respBody, err = encode(resp, &machineKey, h.privateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
// declare the incoming size on the first 4 bytes // declare the incoming size on the first 4 bytes
data := make([]byte, 4) data := make([]byte, reservedResponseHeaderSize)
binary.LittleEndian.PutUint32(data, uint32(len(respBody))) binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
data = append(data, respBody...) data = append(data, respBody...)
return data, nil return data, nil
} }
func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Machine) ([]byte, error) { func (h *Headscale) getMapKeepAliveResponse(
resp := tailcfg.MapResponse{ machineKey key.MachinePublic,
mapRequest tailcfg.MapRequest,
) ([]byte, error) {
mapResponse := tailcfg.MapResponse{
KeepAlive: true, KeepAlive: true,
} }
var respBody []byte var respBody []byte
var err error var err error
if req.Compress == "zstd" { if mapRequest.Compress == "zstd" {
src, _ := json.Marshal(resp) src, _ := json.Marshal(mapResponse)
encoder, _ := zstd.NewWriter(nil) encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil) srcCompressed := encoder.EncodeAll(src, nil)
respBody, err = encodeMsg(srcCompressed, &mKey, h.privateKey) respBody = h.privateKey.SealTo(machineKey, srcCompressed)
if err != nil {
return nil, err
}
} else { } else {
respBody, err = encode(resp, &mKey, h.privateKey) respBody, err = encode(mapResponse, &machineKey, h.privateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
data := make([]byte, 4) data := make([]byte, reservedResponseHeaderSize)
binary.LittleEndian.PutUint32(data, uint32(len(respBody))) binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
data = append(data, respBody...) data = append(data, respBody...)
return data, nil return data, nil
} }
func (h *Headscale) handleMachineLogOut(
ctx *gin.Context,
machineKey key.MachinePublic,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
log.Info().
Str("machine", machine.Name).
Msg("Client requested logout")
h.ExpireMachine(&machine)
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")
ctx.String(http.StatusInternalServerError, "")
return
}
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
func (h *Headscale) handleMachineValidRegistration(
ctx *gin.Context,
machineKey key.MachinePublic,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is valid, respond with redirect to /map
log.Debug().
Str("machine", machine.Name).
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()
ctx.String(http.StatusInternalServerError, "")
return
}
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
func (h *Headscale) handleMachineExpired(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
// The client has registered before, but has expired
log.Debug().
Str("machine", machine.Name).
Msg("Machine registration has expired. Sending a authurl to register")
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, registerRequest, machine)
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()
ctx.String(http.StatusInternalServerError, "")
return
}
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
func (h *Headscale) handleMachineRefreshKey(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
log.Debug().
Str("machine", machine.Name).
Msg("We have the OldNodeKey in the database. This is a key refresh")
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
h.db.Save(&machine)
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")
ctx.String(http.StatusInternalServerError, "Extremely sad!")
return
}
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
func (h *Headscale) handleMachineRegistrationNew(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
machine Machine,
) {
resp := tailcfg.RegisterResponse{}
// The machine registration is new, redirect the client to the registration URL
log.Debug().
Str("machine", machine.Name).
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))
}
if !registerRequest.Expiry.IsZero() {
log.Trace().
Caller().
Str("machine", machine.Name).
Time("expiry", registerRequest.Expiry).
Msg("Non-zero expiry time requested, adding to cache")
h.requestedExpiryCache.Set(
machineKey.String(),
registerRequest.Expiry,
requestedExpiryCacheExpiration,
)
}
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
// save the NodeKey
h.db.Save(&machine)
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "")
return
}
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
func (h *Headscale) handleAuthKey( func (h *Headscale) handleAuthKey(
c *gin.Context, ctx *gin.Context,
db *gorm.DB, machineKey key.MachinePublic,
idKey wgkey.Key, registerRequest tailcfg.RegisterRequest,
req tailcfg.RegisterRequest, machine Machine,
m Machine,
) { ) {
log.Debug(). log.Debug().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", req.Hostinfo.Hostname). Str("machine", registerRequest.Hostinfo.Hostname).
Msgf("Processing auth key for %s", req.Hostinfo.Hostname) Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{} resp := tailcfg.RegisterResponse{}
pak, err := h.checkKeyValidity(req.Auth.AuthKey) pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", m.Name). Str("machine", machine.Name).
Err(err). Err(err).
Msg("Failed authentication via AuthKey") Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false resp.MachineAuthorized = false
respBody, err := encode(resp, &idKey, h.privateKey) respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", m.Name). Str("machine", machine.Name).
Err(err). Err(err).
Msg("Cannot encode message") Msg("Cannot encode message")
c.String(http.StatusInternalServerError, "") ctx.String(http.StatusInternalServerError, "")
machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc() machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
Inc()
return return
} }
c.Data(401, "application/json; charset=utf-8", respBody) ctx.Data(http.StatusUnauthorized, "application/json; charset=utf-8", respBody)
log.Error(). log.Error().
Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", m.Name). Str("machine", machine.Name).
Msg("Failed authentication via AuthKey") Msg("Failed authentication via AuthKey")
machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc() machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
Inc()
return return
} }
log.Debug(). if machine.isRegistered() {
Str("func", "handleAuthKey"). log.Trace().
Str("machine", m.Name). Caller().
Msg("Authentication key was valid, proceeding to acquire an IP address") Str("machine", machine.Name).
ip, err := h.getAvailableIP() Msg("machine already registered, reauthenticating")
if err != nil {
log.Error(). h.RefreshMachine(&machine, registerRequest.Expiry)
} else {
log.Debug().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", m.Name). Str("machine", machine.Name).
Msg("Failed to find an available IP") Msg("Authentication key was valid, proceeding to acquire an IP address")
machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc() ip, err := h.getAvailableIP()
return if err != nil {
} log.Error().
log.Info(). Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", m.Name). Str("machine", machine.Name).
Str("ip", ip.String()). Msg("Failed to find an available IP")
Msgf("Assigning %s to %s", ip, m.Name) machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
Inc()
m.AuthKeyID = uint(pak.ID) return
m.IPAddress = ip.String() }
m.NamespaceID = pak.NamespaceID log.Info().
m.NodeKey = wgkey.Key(req.NodeKey).HexString() // we update it just in case Str("func", "handleAuthKey").
m.Registered = true Str("machine", machine.Name).
m.RegisterMethod = "authKey" Str("ip", ip.String()).
db.Save(&m) Msgf("Assigning %s to %s", ip, machine.Name)
machine.Expiry = &registerRequest.Expiry
machine.AuthKeyID = uint(pak.ID)
machine.IPAddress = ip.String()
machine.NamespaceID = pak.NamespaceID
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
// we update it just in case
machine.Registered = true
machine.RegisterMethod = RegisterMethodAuthKey
h.db.Save(&machine)
}
pak.Used = true pak.Used = true
db.Save(&pak) h.db.Save(&pak)
resp.MachineAuthorized = true resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser() resp.User = *pak.Namespace.toUser()
respBody, err := encode(resp, &idKey, h.privateKey) respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", m.Name). Str("machine", machine.Name).
Err(err). Err(err).
Msg("Cannot encode message") Msg("Cannot encode message")
machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc() machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
c.String(http.StatusInternalServerError, "Extremely sad!") Inc()
ctx.String(http.StatusInternalServerError, "Extremely sad!")
return return
} }
machineRegistrations.WithLabelValues("new", "authkey", "success", m.Namespace.Name).Inc() machineRegistrations.WithLabelValues("new", "authkey", "success", machine.Namespace.Name).
c.Data(200, "application/json; charset=utf-8", respBody) Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
log.Info(). log.Info().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", m.Name). Str("machine", machine.Name).
Str("ip", ip.String()). Str("ip", machine.IPAddress).
Msg("Successfully authenticated via AuthKey") Msg("Successfully authenticated via AuthKey")
} }

598
app.go
View File

@ -1,36 +1,76 @@
package headscale package headscale
import ( import (
"context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io"
"io/fs"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"os/signal"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/rs/zerolog/log" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/patrickmn/go-cache"
zerolog "github.com/philip-bui/grpc-zerolog"
zl "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/soheilhy/cmux"
ginprometheus "github.com/zsais/go-gin-prometheus" ginprometheus "github.com/zsais/go-gin-prometheus"
"golang.org/x/crypto/acme" "golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
"golang.org/x/oauth2"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"gorm.io/gorm" "gorm.io/gorm"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
"tailscale.com/types/wgkey" "tailscale.com/types/key"
) )
// Config contains the initial Headscale configuration const (
AuthPrefix = "Bearer "
Postgres = "postgres"
Sqlite = "sqlite3"
updateInterval = 5000
HTTPReadTimeout = 30 * time.Second
privateKeyFileMode = 0o600
requestedExpiryCacheExpiration = time.Minute * 5
requestedExpiryCacheCleanupInterval = time.Minute * 10
errUnsupportedDatabase = Error("unsupported DB")
errUnsupportedLetsEncryptChallengeType = Error(
"unknown value for Lets Encrypt challenge type",
)
)
// Config contains the initial Headscale configuration.
type Config struct { type Config struct {
ServerURL string ServerURL string
Addr string Addr string
PrivateKeyPath string
EphemeralNodeInactivityTimeout time.Duration EphemeralNodeInactivityTimeout time.Duration
IPPrefix netaddr.IPPrefix IPPrefix netaddr.IPPrefix
PrivateKeyPath string
BaseDomain string BaseDomain string
DERP DERPConfig DERP DERPConfig
@ -55,6 +95,20 @@ type Config struct {
ACMEEmail string ACMEEmail string
DNSConfig *tailcfg.DNSConfig DNSConfig *tailcfg.DNSConfig
UnixSocket string
UnixSocketPermission fs.FileMode
OIDC OIDCConfig
CLI CLIConfig
}
type OIDCConfig struct {
Issuer string
ClientID string
ClientSecret string
MatchMap map[string]string
} }
type DERPConfig struct { type DERPConfig struct {
@ -64,86 +118,110 @@ type DERPConfig struct {
UpdateFrequency time.Duration UpdateFrequency time.Duration
} }
// Headscale represents the base app of the service type CLIConfig struct {
Address string
APIKey string
Insecure bool
Timeout time.Duration
}
// 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
publicKey *wgkey.Key privateKey *key.MachinePrivate
privateKey *wgkey.Private
DERPMap *tailcfg.DERPMap DERPMap *tailcfg.DERPMap
aclPolicy *ACLPolicy aclPolicy *ACLPolicy
aclRules *[]tailcfg.FilterRule aclRules []tailcfg.FilterRule
lastStateChange sync.Map lastStateChange sync.Map
oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
oidcStateCache *cache.Cache
requestedExpiryCache *cache.Cache
} }
// NewHeadscale returns the Headscale app // NewHeadscale returns the Headscale app.
func NewHeadscale(cfg Config) (*Headscale, error) { func NewHeadscale(cfg Config) (*Headscale, error) {
content, err := os.ReadFile(cfg.PrivateKeyPath) privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to read or create private key: %w", err)
} }
privKey, err := wgkey.ParsePrivate(string(content))
if err != nil {
return nil, err
}
pubKey := privKey.Public()
var dbString string var dbString string
switch cfg.DBtype { switch cfg.DBtype {
case "postgres": case Postgres:
dbString = fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=disable", cfg.DBhost, dbString = fmt.Sprintf(
cfg.DBport, cfg.DBname, cfg.DBuser, cfg.DBpass) "host=%s port=%d dbname=%s user=%s password=%s sslmode=disable",
case "sqlite3": cfg.DBhost,
cfg.DBport,
cfg.DBname,
cfg.DBuser,
cfg.DBpass,
)
case Sqlite:
dbString = cfg.DBpath dbString = cfg.DBpath
default: default:
return nil, errors.New("unsupported DB") return nil, errUnsupportedDatabase
} }
h := Headscale{ requestedExpiryCache := cache.New(
cfg: cfg, requestedExpiryCacheExpiration,
dbType: cfg.DBtype, requestedExpiryCacheCleanupInterval,
dbString: dbString, )
privateKey: privKey,
publicKey: &pubKey, app := Headscale{
aclRules: &tailcfg.FilterAllowAll, // default allowall cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey: privKey,
aclRules: tailcfg.FilterAllowAll, // default allowall
requestedExpiryCache: requestedExpiryCache,
} }
err = h.initDB() err = app.initDB()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS if cfg.OIDC.Issuer != "" {
magicDNSDomains, err := generateMagicDNSRootDomains(h.cfg.IPPrefix, h.cfg.BaseDomain) err = app.initOIDC()
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS
magicDNSDomains := generateMagicDNSRootDomains(
app.cfg.IPPrefix,
)
// we might have routes already from Split DNS // we might have routes already from Split DNS
if h.cfg.DNSConfig.Routes == nil { if app.cfg.DNSConfig.Routes == nil {
h.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver) app.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver)
} }
for _, d := range magicDNSDomains { for _, d := range magicDNSDomains {
h.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
} }
} }
return &h, nil return &app, nil
} }
// Redirect to our TLS url // Redirect to our TLS url.
func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) { func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
target := h.cfg.ServerURL + req.URL.RequestURI() target := h.cfg.ServerURL + req.URL.RequestURI()
http.Redirect(w, req, target, http.StatusFound) http.Redirect(w, req, target, http.StatusFound)
} }
// expireEphemeralNodes deletes ephemeral machine records that have not been // expireEphemeralNodes deletes ephemeral machine records that have not been
// seen for longer than h.cfg.EphemeralNodeInactivityTimeout // seen for longer than h.cfg.EphemeralNodeInactivityTimeout.
func (h *Headscale) expireEphemeralNodes(milliSeconds int64) { func (h *Headscale) expireEphemeralNodes(milliSeconds int64) {
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond) ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
for range ticker.C { for range ticker.C {
@ -155,33 +233,46 @@ func (h *Headscale) expireEphemeralNodesWorker() {
namespaces, err := h.ListNamespaces() namespaces, err := h.ListNamespaces()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error listing namespaces") log.Error().Err(err).Msg("Error listing namespaces")
return return
} }
for _, ns := range *namespaces {
machines, err := h.ListMachinesInNamespace(ns.Name) for _, namespace := range namespaces {
machines, err := h.ListMachinesInNamespace(namespace.Name)
if err != nil { if err != nil {
log.Error().Err(err).Str("namespace", ns.Name).Msg("Error listing machines in namespace") log.Error().
Err(err).
Str("namespace", namespace.Name).
Msg("Error listing machines in namespace")
return return
} }
for _, m := range *machines {
if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral && for _, machine := range machines {
time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) { if machine.AuthKey != nil && machine.LastSeen != nil &&
log.Info().Str("machine", m.Name).Msg("Ephemeral client removed from database") machine.AuthKey.Ephemeral &&
err = h.db.Unscoped().Delete(m).Error time.Now().
After(machine.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
log.Info().
Str("machine", machine.Name).
Msg("Ephemeral client removed from database")
err = h.db.Unscoped().Delete(machine).Error
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
Str("machine", m.Name). Str("machine", machine.Name).
Msg("🤮 Cannot delete ephemeral machine from the database") Msg("🤮 Cannot delete ephemeral machine from the database")
} }
} }
} }
h.setLastStateChangeToNow(ns.Name)
h.setLastStateChangeToNow(namespace.Name)
} }
} }
// WatchForKVUpdates checks the KV DB table for requests to perform tailnet upgrades // WatchForKVUpdates checks the KV DB table for requests to perform tailnet upgrades
// This is a way to communitate the CLI with the headscale server // This is a way to communitate the CLI with the headscale server.
func (h *Headscale) watchForKVUpdates(milliSeconds int64) { func (h *Headscale) watchForKVUpdates(milliSeconds int64) {
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond) ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
for range ticker.C { for range ticker.C {
@ -194,24 +285,234 @@ func (h *Headscale) watchForKVUpdatesWorker() {
// more functions will come here in the future // more functions will come here in the future
} }
// Serve launches a GIN server with the Headscale API func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {
// Check if the request is coming from the on-server client.
// This is not secure, but it is to maintain maintainability
// with the "legacy" database-based client
// It is also neede for grpc-gateway to be able to connect to
// the server
client, _ := peer.FromContext(ctx)
log.Trace().
Caller().
Str("client_address", client.Addr.String()).
Msg("Client is trying to authenticate")
meta, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Error().
Caller().
Str("client_address", client.Addr.String()).
Msg("Retrieving metadata is failed")
return ctx, status.Errorf(
codes.InvalidArgument,
"Retrieving metadata is failed",
)
}
authHeader, ok := meta["authorization"]
if !ok {
log.Error().
Caller().
Str("client_address", client.Addr.String()).
Msg("Authorization token is not supplied")
return ctx, status.Errorf(
codes.Unauthenticated,
"Authorization token is not supplied",
)
}
token := authHeader[0]
if !strings.HasPrefix(token, AuthPrefix) {
log.Error().
Caller().
Str("client_address", client.Addr.String()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
return ctx, status.Error(
codes.Unauthenticated,
`missing "Bearer " prefix in "Authorization" header`,
)
}
// TODO(kradalby): Implement API key backend:
// - Table in the DB
// - Key name
// - Encrypted
// - Expiry
//
// Currently all other than localhost traffic is unauthorized, this is intentional to allow
// us to make use of gRPC for our CLI, but not having to implement any of the remote capabilities
// and API key auth
return ctx, status.Error(
codes.Unauthenticated,
"Authentication is not implemented yet",
)
// if strings.TrimPrefix(token, AUTH_PREFIX) != a.Token {
// log.Error().Caller().Str("client_address", p.Addr.String()).Msg("invalid token")
// return ctx, status.Error(codes.Unauthenticated, "invalid token")
// }
// return handler(ctx, req)
}
func (h *Headscale) httpAuthenticationMiddleware(ctx *gin.Context) {
log.Trace().
Caller().
Str("client_address", ctx.ClientIP()).
Msg("HTTP authentication invoked")
authHeader := ctx.GetHeader("authorization")
if !strings.HasPrefix(authHeader, AuthPrefix) {
log.Error().
Caller().
Str("client_address", ctx.ClientIP()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
ctx.AbortWithStatus(http.StatusUnauthorized)
// TODO(kradalby): Implement API key backend
// Currently all traffic is unauthorized, this is intentional to allow
// us to make use of gRPC for our CLI, but not having to implement any of the remote capabilities
// and API key auth
//
// if strings.TrimPrefix(authHeader, AUTH_PREFIX) != a.Token {
// log.Error().Caller().Str("client_address", c.ClientIP()).Msg("invalid token")
// c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error", "unauthorized"})
// return
// }
// c.Next()
}
// ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear
// and will remove it if it is not.
func (h *Headscale) ensureUnixSocketIsAbsent() error {
// File does not exist, all fine
if _, err := os.Stat(h.cfg.UnixSocket); errors.Is(err, os.ErrNotExist) {
return nil
}
return os.Remove(h.cfg.UnixSocket)
}
// Serve launches a GIN server with the Headscale API.
func (h *Headscale) Serve() error { func (h *Headscale) Serve() error {
r := gin.Default()
p := ginprometheus.NewPrometheus("gin")
p.Use(r)
r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"healthy": "ok"}) })
r.GET("/key", h.KeyHandler)
r.GET("/register", h.RegisterWebAPI)
r.POST("/machine/:id/map", h.PollNetMapHandler)
r.POST("/machine/:id", h.RegistrationHandler)
r.GET("/apple", h.AppleMobileConfig)
r.GET("/apple/:platform", h.ApplePlatformConfig)
var err error var err error
go h.watchForKVUpdates(5000) ctx := context.Background()
go h.expireEphemeralNodes(5000) ctx, cancel := context.WithCancel(ctx)
defer cancel()
err = h.ensureUnixSocketIsAbsent()
if err != nil {
return fmt.Errorf("unable to remove old socket file: %w", err)
}
socketListener, err := net.Listen("unix", h.cfg.UnixSocket)
if err != nil {
return fmt.Errorf("failed to set up gRPC socket: %w", err)
}
// Change socket permissions
if err := os.Chmod(h.cfg.UnixSocket, h.cfg.UnixSocketPermission); err != nil {
return fmt.Errorf("failed change permission of gRPC socket: %w", err)
}
// Handle common process-killing signals so we can gracefully shut down:
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go func(c chan os.Signal) {
// Wait for a SIGINT or SIGKILL:
sig := <-c
log.Printf("Caught signal %s: shutting down.", sig)
// Stop listening (and unlink the socket if unix type):
socketListener.Close()
// And we're done:
os.Exit(0)
}(sigc)
networkListener, err := net.Listen("tcp", h.cfg.Addr)
if err != nil {
return fmt.Errorf("failed to bind to TCP address: %w", err)
}
// Create the cmux object that will multiplex 2 protocols on the same port.
// The two following listeners will be served on the same port below gracefully.
networkMutex := cmux.New(networkListener)
// Match gRPC requests here
grpcListener := networkMutex.MatchWithWriters(
cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"),
cmux.HTTP2MatchHeaderFieldSendSettings(
"content-type",
"application/grpc+proto",
),
)
// Otherwise match regular http requests.
httpListener := networkMutex.Match(cmux.Any())
grpcGatewayMux := runtime.NewServeMux()
// Make the grpc-gateway connect to grpc over socket
grpcGatewayConn, err := grpc.Dial(
h.cfg.UnixSocket,
[]grpc.DialOption{
grpc.WithInsecure(),
grpc.WithContextDialer(GrpcSocketDialer),
}...,
)
if err != nil {
return err
}
// Connect to the gRPC server over localhost to skip
// the authentication.
err = v1.RegisterHeadscaleServiceHandler(ctx, grpcGatewayMux, grpcGatewayConn)
if err != nil {
return err
}
router := gin.Default()
prometheus := ginprometheus.NewPrometheus("gin")
prometheus.Use(router)
router.GET(
"/health",
func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) },
)
router.GET("/key", h.KeyHandler)
router.GET("/register", h.RegisterWebAPI)
router.POST("/machine/:id/map", h.PollNetMapHandler)
router.POST("/machine/:id", h.RegistrationHandler)
router.GET("/oidc/register/:mkey", h.RegisterOIDC)
router.GET("/oidc/callback", h.OIDCCallback)
router.GET("/apple", h.AppleMobileConfig)
router.GET("/apple/:platform", h.ApplePlatformConfig)
router.GET("/swagger", SwaggerUI)
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
api := router.Group("/api")
api.Use(h.httpAuthenticationMiddleware)
{
api.Any("/v1/*any", gin.WrapF(grpcGatewayMux.ServeHTTP))
}
router.NoRoute(stdoutHandler)
// Fetch an initial DERP Map before we start serving // Fetch an initial DERP Map before we start serving
h.DERPMap = GetDERPMap(h.cfg.DERP) h.DERPMap = GetDERPMap(h.cfg.DERP)
@ -222,10 +523,14 @@ func (h *Headscale) Serve() error {
go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel) go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
} }
s := &http.Server{ // I HATE THIS
go h.watchForKVUpdates(updateInterval)
go h.expireEphemeralNodes(updateInterval)
httpServer := &http.Server{
Addr: h.cfg.Addr, Addr: h.cfg.Addr,
Handler: r, Handler: router,
ReadTimeout: 30 * time.Second, ReadTimeout: HTTPReadTimeout,
// Go does not handle timeouts in HTTP very well, and there is // Go does not handle timeouts in HTTP very well, and there is
// no good way to handle streaming timeouts, therefore we need to // no good way to handle streaming timeouts, therefore we need to
// keep this at unlimited and be careful to clean up connections // keep this at unlimited and be careful to clean up connections
@ -233,12 +538,78 @@ func (h *Headscale) Serve() error {
WriteTimeout: 0, WriteTimeout: 0,
} }
if zl.GlobalLevel() == zl.TraceLevel {
zerolog.RespLog = true
} else {
zerolog.RespLog = false
}
grpcOptions := []grpc.ServerOption{
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
h.grpcAuthenticationInterceptor,
zerolog.NewUnaryServerInterceptor(),
),
),
}
tlsConfig, err := h.getTLSSettings()
if err != nil {
log.Error().Err(err).Msg("Failed to set up TLS configuration")
return err
}
if tlsConfig != nil {
httpServer.TLSConfig = tlsConfig
grpcOptions = append(grpcOptions, grpc.Creds(credentials.NewTLS(tlsConfig)))
}
grpcServer := grpc.NewServer(grpcOptions...)
// Start the local gRPC server without TLS and without authentication
grpcSocket := grpc.NewServer(zerolog.UnaryInterceptor())
v1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h))
v1.RegisterHeadscaleServiceServer(grpcSocket, newHeadscaleV1APIServer(h))
reflection.Register(grpcServer)
reflection.Register(grpcSocket)
errorGroup := new(errgroup.Group)
errorGroup.Go(func() error { return grpcSocket.Serve(socketListener) })
// TODO(kradalby): Verify if we need the same TLS setup for gRPC as HTTP
errorGroup.Go(func() error { return grpcServer.Serve(grpcListener) })
if tlsConfig != nil {
errorGroup.Go(func() error {
tlsl := tls.NewListener(httpListener, tlsConfig)
return httpServer.Serve(tlsl)
})
} else {
errorGroup.Go(func() error { return httpServer.Serve(httpListener) })
}
errorGroup.Go(func() error { return networkMutex.Serve() })
log.Info().
Msgf("listening and serving (multiplexed HTTP and gRPC) on: %s", h.cfg.Addr)
return errorGroup.Wait()
}
func (h *Headscale) getTLSSettings() (*tls.Config, error) {
var err error
if h.cfg.TLSLetsEncryptHostname != "" { if h.cfg.TLSLetsEncryptHostname != "" {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") { if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
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://")
} }
m := autocert.Manager{ certManager := autocert.Manager{
Prompt: autocert.AcceptTOS, Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname), HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir), Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
@ -248,38 +619,48 @@ func (h *Headscale) Serve() error {
Email: h.cfg.ACMEEmail, Email: h.cfg.ACMEEmail,
} }
s.TLSConfig = m.TLSConfig() switch h.cfg.TLSLetsEncryptChallengeType {
case "TLS-ALPN-01":
if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" {
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737) // Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
// The RFC requires that the validation is done on port 443; in other words, headscale // The RFC requires that the validation is done on port 443; in other words, headscale
// must be reachable on port 443. // must be reachable on port 443.
err = s.ListenAndServeTLS("", "") return certManager.TLSConfig(), nil
} else if h.cfg.TLSLetsEncryptChallengeType == "HTTP-01" {
case "HTTP-01":
// 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.
go func() { go func() {
log.Fatal(). log.Fatal().
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, m.HTTPHandler(http.HandlerFunc(h.redirect)))). Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
Msg("failed to set up a HTTP server") Msg("failed to set up a HTTP server")
}() }()
err = s.ListenAndServeTLS("", "")
} else { return certManager.TLSConfig(), nil
return errors.New("unknown value for TLSLetsEncryptChallengeType")
default:
return nil, errUnsupportedLetsEncryptChallengeType
} }
} else if h.cfg.TLSCertPath == "" { } else if h.cfg.TLSCertPath == "" {
if !strings.HasPrefix(h.cfg.ServerURL, "http://") { if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://") log.Warn().Msg("Listening without TLS but ServerURL does not start with http://")
} }
err = s.ListenAndServe()
return nil, err
} else { } else {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") { if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
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://")
} }
err = s.ListenAndServeTLS(h.cfg.TLSCertPath, h.cfg.TLSKeyPath) tlsConfig := &tls.Config{
ClientAuth: tls.RequireAnyClientCert,
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12,
}
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
return tlsConfig, err
} }
return err
} }
func (h *Headscale) setLastStateChangeToNow(namespace string) { func (h *Headscale) setLastStateChangeToNow(namespace string) {
@ -311,3 +692,58 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
return times[0] return times[0]
} }
} }
func stdoutHandler(ctx *gin.Context) {
body, _ := io.ReadAll(ctx.Request.Body)
log.Trace().
Interface("header", ctx.Request.Header).
Interface("proto", ctx.Request.Proto).
Interface("url", ctx.Request.URL).
Bytes("body", body).
Msg("Request did not match")
}
func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
privateKey, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
log.Info().Str("path", path).Msg("No private key file at path, creating...")
machineKey := key.NewMachine()
machineKeyStr, err := machineKey.MarshalText()
if err != nil {
return nil, fmt.Errorf(
"failed to convert private key to string for saving: %w",
err,
)
}
err = os.WriteFile(path, machineKeyStr, privateKeyFileMode)
if err != nil {
return nil, fmt.Errorf(
"failed to save private key to disk: %w",
err,
)
}
return &machineKey, nil
} else if err != nil {
return nil, fmt.Errorf("failed to read private key file: %w", err)
}
trimmedPrivateKey := strings.TrimSpace(string(privateKey))
privateKeyEnsurePrefix := PrivateKeyEnsurePrefix(trimmedPrivateKey)
var machineKey key.MachinePrivate
if err = machineKey.UnmarshalText([]byte(privateKeyEnsurePrefix)); err != nil {
log.Info().
Str("path", path).
Msg("This might be due to a legacy (headscale pre-0.12) private key. " +
"If the key is in WireGuard format, delete the key and restart headscale. " +
"A new key will automatically be generated. All Tailscale clients will have to be restarted")
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
return &machineKey, nil
}

View File

@ -5,6 +5,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/patrickmn/go-cache"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"inet.af/netaddr" "inet.af/netaddr"
) )
@ -17,8 +18,10 @@ var _ = check.Suite(&Suite{})
type Suite struct{} type Suite struct{}
var tmpDir string var (
var h Headscale tmpDir string
app Headscale
)
func (s *Suite) SetUpTest(c *check.C) { func (s *Suite) SetUpTest(c *check.C) {
s.ResetDB(c) s.ResetDB(c)
@ -41,18 +44,22 @@ func (s *Suite) ResetDB(c *check.C) {
IPPrefix: netaddr.MustParseIPPrefix("10.27.0.0/23"), IPPrefix: netaddr.MustParseIPPrefix("10.27.0.0/23"),
} }
h = Headscale{ app = Headscale{
cfg: cfg, cfg: cfg,
dbType: "sqlite3", dbType: "sqlite3",
dbString: tmpDir + "/headscale_test.db", dbString: tmpDir + "/headscale_test.db",
requestedExpiryCache: cache.New(
requestedExpiryCacheExpiration,
requestedExpiryCacheCleanupInterval,
),
} }
err = h.initDB() err = app.initDB()
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
db, err := h.openDB() db, err := app.openDB()
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
h.db = db app.db = db
} }

View File

@ -2,19 +2,18 @@ package headscale
import ( import (
"bytes" "bytes"
"html/template"
"net/http" "net/http"
"text/template"
"github.com/rs/zerolog/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/rs/zerolog/log"
) )
// AppleMobileConfig shows a simple message in the browser to point to the CLI // AppleMobileConfig shows a simple message in the browser to point to the CLI
// Listens in /register // Listens in /register.
func (h *Headscale) AppleMobileConfig(c *gin.Context) { func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
t := template.Must(template.New("apple").Parse(` appleTemplate := template.Must(template.New("apple").Parse(`
<html> <html>
<body> <body>
<h1>Apple configuration profiles</h1> <h1>Apple configuration profiles</h1>
@ -56,7 +55,7 @@ func (h *Headscale) AppleMobileConfig(c *gin.Context) {
<p>Or</p> <p>Or</p>
<p>Use your terminal to configure the default setting for Tailscale by issuing:</p> <p>Use your terminal to configure the default setting for Tailscale by issuing:</p>
<code>defaults write io.tailscale.ipn.macos ControlURL {{.Url}}</code> <code>defaults write io.tailscale.ipn.macos ControlURL {{.URL}}</code>
<p>Restart Tailscale.app and log in.</p> <p>Restart Tailscale.app and log in.</p>
@ -64,24 +63,29 @@ func (h *Headscale) AppleMobileConfig(c *gin.Context) {
</html>`)) </html>`))
config := map[string]interface{}{ config := map[string]interface{}{
"Url": h.cfg.ServerURL, "URL": h.cfg.ServerURL,
} }
var payload bytes.Buffer var payload bytes.Buffer
if err := t.Execute(&payload, config); err != nil { if err := appleTemplate.Execute(&payload, config); err != nil {
log.Error(). log.Error().
Str("handler", "AppleMobileConfig"). Str("handler", "AppleMobileConfig").
Err(err). Err(err).
Msg("Could not render Apple index template") Msg("Could not render Apple index template")
c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple index template")) ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Apple index template"),
)
return return
} }
c.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes()) ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes())
} }
func (h *Headscale) ApplePlatformConfig(c *gin.Context) { func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
platform := c.Param("platform") platform := ctx.Param("platform")
id, err := uuid.NewV4() id, err := uuid.NewV4()
if err != nil { if err != nil {
@ -89,23 +93,33 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) {
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Failed not create UUID") Msg("Failed not create UUID")
c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Failed to create UUID")) ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Failed to create UUID"),
)
return return
} }
contentId, err := uuid.NewV4() contentID, err := uuid.NewV4()
if err != nil { if err != nil {
log.Error(). log.Error().
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Failed not create UUID") Msg("Failed not create UUID")
c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Failed to create UUID")) ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Failed to create UUID"),
)
return return
} }
platformConfig := AppleMobilePlatformConfig{ platformConfig := AppleMobilePlatformConfig{
UUID: contentId, UUID: contentID,
Url: h.cfg.ServerURL, URL: h.cfg.ServerURL,
} }
var payload bytes.Buffer var payload bytes.Buffer
@ -117,7 +131,12 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) {
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Could not render Apple macOS template") Msg("Could not render Apple macOS template")
c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple macOS template")) ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Apple macOS template"),
)
return return
} }
case "ios": case "ios":
@ -126,17 +145,27 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) {
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Could not render Apple iOS template") Msg("Could not render Apple iOS template")
c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple iOS template")) ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Apple iOS template"),
)
return return
} }
default: default:
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte("Invalid platform, only ios and macos is supported")) ctx.Data(
http.StatusOK,
"text/html; charset=utf-8",
[]byte("Invalid platform, only ios and macos is supported"),
)
return return
} }
config := AppleMobileConfig{ config := AppleMobileConfig{
UUID: id, UUID: id,
Url: h.cfg.ServerURL, URL: h.cfg.ServerURL,
Payload: payload.String(), Payload: payload.String(),
} }
@ -146,25 +175,35 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) {
Str("handler", "ApplePlatformConfig"). Str("handler", "ApplePlatformConfig").
Err(err). Err(err).
Msg("Could not render Apple platform template") Msg("Could not render Apple platform template")
c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple platform template")) ctx.Data(
http.StatusInternalServerError,
"text/html; charset=utf-8",
[]byte("Could not render Apple platform template"),
)
return return
} }
c.Data(http.StatusOK, "application/x-apple-aspen-config; charset=utf-8", content.Bytes()) ctx.Data(
http.StatusOK,
"application/x-apple-aspen-config; charset=utf-8",
content.Bytes(),
)
} }
type AppleMobileConfig struct { type AppleMobileConfig struct {
UUID uuid.UUID UUID uuid.UUID
Url string URL string
Payload string Payload string
} }
type AppleMobilePlatformConfig struct { type AppleMobilePlatformConfig struct {
UUID uuid.UUID UUID uuid.UUID
Url string URL string
} }
var commonTemplate = template.Must(template.New("mobileconfig").Parse(`<?xml version="1.0" encoding="UTF-8"?> var commonTemplate = template.Must(
template.New("mobileconfig").Parse(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
@ -173,7 +212,7 @@ var commonTemplate = template.Must(template.New("mobileconfig").Parse(`<?xml ver
<key>PayloadDisplayName</key> <key>PayloadDisplayName</key>
<string>Headscale</string> <string>Headscale</string>
<key>PayloadDescription</key> <key>PayloadDescription</key>
<string>Configure Tailscale login server to: {{.Url}}</string> <string>Configure Tailscale login server to: {{.URL}}</string>
<key>PayloadIdentifier</key> <key>PayloadIdentifier</key>
<string>com.github.juanfont.headscale</string> <string>com.github.juanfont.headscale</string>
<key>PayloadRemovalDisallowed</key> <key>PayloadRemovalDisallowed</key>
@ -187,7 +226,8 @@ var commonTemplate = template.Must(template.New("mobileconfig").Parse(`<?xml ver
{{.Payload}} {{.Payload}}
</array> </array>
</dict> </dict>
</plist>`)) </plist>`),
)
var iosTemplate = template.Must(template.New("iosTemplate").Parse(` var iosTemplate = template.Must(template.New("iosTemplate").Parse(`
<dict> <dict>
@ -203,7 +243,7 @@ var iosTemplate = template.Must(template.New("iosTemplate").Parse(`
<true/> <true/>
<key>ControlURL</key> <key>ControlURL</key>
<string>{{.Url}}</string> <string>{{.URL}}</string>
</dict> </dict>
`)) `))
@ -221,6 +261,6 @@ var macosTemplate = template.Must(template.New("macosTemplate").Parse(`
<true/> <true/>
<key>ControlURL</key> <key>ControlURL</key>
<string>{{.Url}}</string> <string>{{.URL}}</string>
</dict> </dict>
`)) `))

21
buf.gen.yaml Normal file
View File

@ -0,0 +1,21 @@
version: v1
plugins:
- name: go
out: gen/go
opt:
- paths=source_relative
- name: go-grpc
out: gen/go
opt:
- paths=source_relative
- name: grpc-gateway
out: gen/go
opt:
- paths=source_relative
- generate_unbound_methods=true
# - name: gorm
# out: gen/go
# opt:
# - paths=source_relative,enums=string,gateway=true
- name: openapiv2
out: gen/openapiv2

40
cli.go
View File

@ -1,40 +0,0 @@
package headscale
import (
"errors"
"gorm.io/gorm"
"tailscale.com/types/wgkey"
)
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey
func (h *Headscale) RegisterMachine(key string, namespace string) (*Machine, error) {
ns, err := h.GetNamespace(namespace)
if err != nil {
return nil, err
}
mKey, err := wgkey.ParseHex(key)
if err != nil {
return nil, err
}
m := Machine{}
if result := h.db.First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, errors.New("Machine not found")
}
if m.isAlreadyRegistered() {
return nil, errors.New("Machine already registered")
}
ip, err := h.getAvailableIP()
if err != nil {
return nil, err
}
m.IPAddress = ip.String()
m.NamespaceID = ns.ID
m.Registered = true
m.RegisterMethod = "cli"
h.db.Save(&m)
return &m, nil
}

View File

@ -1,31 +1,39 @@
package headscale package headscale
import ( import (
"time"
"gopkg.in/check.v1" "gopkg.in/check.v1"
) )
func (s *Suite) TestRegisterMachine(c *check.C) { func (s *Suite) TestRegisterMachine(c *check.C) {
n, err := h.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
m := Machine{ now := time.Now().UTC()
machine := Machine{
ID: 0, ID: 0,
MachineKey: "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e", MachineKey: "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: n.ID, NamespaceID: namespace.ID,
IPAddress: "10.0.0.1", IPAddress: "10.0.0.1",
Expiry: &now,
} }
h.db.Save(&m) app.db.Save(&machine)
_, err = h.GetMachine("test", "testmachine") _, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
m2, err := h.RegisterMachine("8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e", n.Name) machineAfterRegistering, err := app.RegisterMachine(
"8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e",
namespace.Name,
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(m2.Registered, check.Equals, true) c.Assert(machineAfterRegistering.Registered, check.Equals, true)
_, err = m2.GetHostInfo() _, err = machineAfterRegistering.GetHostInfo()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
} }

132
cmd/headscale/cli/debug.go Normal file
View File

@ -0,0 +1,132 @@
package cli
import (
"fmt"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
)
const (
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
type Error string
func (e Error) Error() string { return string(e) }
func init() {
rootCmd.AddCommand(debugCmd)
createNodeCmd.Flags().StringP("name", "", "", "Name")
err := createNodeCmd.MarkFlagRequired("name")
if err != nil {
log.Fatal().Err(err).Msg("")
}
createNodeCmd.Flags().StringP("namespace", "n", "", "Namespace")
err = createNodeCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatal().Err(err).Msg("")
}
createNodeCmd.Flags().StringP("key", "k", "", "Key")
err = createNodeCmd.MarkFlagRequired("key")
if err != nil {
log.Fatal().Err(err).Msg("")
}
createNodeCmd.Flags().
StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to advertise")
debugCmd.AddCommand(createNodeCmd)
}
var debugCmd = &cobra.Command{
Use: "debug",
Short: "debug and testing commands",
Long: "debug contains extra commands used for debugging and testing headscale",
}
var createNodeCmd = &cobra.Command{
Use: "create-node",
Short: "Create a node (machine) that can be registered with `nodes register <>` command",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
name, err := cmd.Flags().GetString("name")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting node from flag: %s", err),
output,
)
return
}
machineKey, err := cmd.Flags().GetString("key")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting key from flag: %s", err),
output,
)
return
}
if len(machineKey) != keyLength {
err = errPreAuthKeyTooShort
ErrorOutput(
err,
fmt.Sprintf("Error: %s", err),
output,
)
return
}
routes, err := cmd.Flags().GetStringSlice("route")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting routes from flag: %s", err),
output,
)
return
}
request := &v1.DebugCreateMachineRequest{
Key: machineKey,
Name: name,
Namespace: namespace,
Routes: routes,
}
response, err := client.DebugCreateMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot create machine: %s", status.Convert(err).Message()),
output,
)
return
}
SuccessOutput(response.Machine, "Machine created", output)
},
}

View File

@ -0,0 +1,41 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"tailscale.com/types/key"
)
func init() {
rootCmd.AddCommand(generateCmd)
generateCmd.AddCommand(generatePrivateKeyCmd)
}
var generateCmd = &cobra.Command{
Use: "generate",
Short: "Generate commands",
}
var generatePrivateKeyCmd = &cobra.Command{
Use: "private-key",
Short: "Generate a private key for the headscale server",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
machineKey := key.NewMachine()
machineKeyStr, err := machineKey.MarshalText()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting machine key from flag: %s", err),
output,
)
}
SuccessOutput(map[string]string{
"private_key": string(machineKeyStr),
},
string(machineKeyStr), output)
},
}

View File

@ -2,12 +2,14 @@ package cli
import ( import (
"fmt" "fmt"
"log"
"strconv"
"strings"
survey "github.com/AlecAivazis/survey/v2"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/status"
) )
func init() { func init() {
@ -18,6 +20,10 @@ func init() {
namespaceCmd.AddCommand(renameNamespaceCmd) namespaceCmd.AddCommand(renameNamespaceCmd)
} }
const (
errMissingParameter = headscale.Error("missing parameters")
)
var namespaceCmd = &cobra.Command{ var namespaceCmd = &cobra.Command{
Use: "namespaces", Use: "namespaces",
Short: "Manage the namespaces of Headscale", Short: "Manage the namespaces of Headscale",
@ -28,26 +34,40 @@ var createNamespaceCmd = &cobra.Command{
Short: "Creates a new namespace", Short: "Creates a new namespace",
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 { if len(args) < 1 {
return fmt.Errorf("Missing parameters") return errMissingParameter
} }
return nil return nil
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
o, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp()
namespaceName := args[0]
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
log.Trace().Interface("client", client).Msg("Obtained gRPC client")
request := &v1.CreateNamespaceRequest{Name: namespaceName}
log.Trace().Interface("request", request).Msg("Sending CreateNamespace request")
response, err := client.CreateNamespace(ctx, request)
if err != nil { if err != nil {
log.Fatalf("Error initializing: %s", err) ErrorOutput(
} err,
namespace, err := h.CreateNamespace(args[0]) fmt.Sprintf(
if strings.HasPrefix(o, "json") { "Cannot create namespace: %s",
JsonOutput(namespace, err, o) status.Convert(err).Message(),
),
output,
)
return return
} }
if err != nil {
fmt.Printf("Error creating namespace: %s\n", err) SuccessOutput(response.Namespace, "Namespace created", output)
return
}
fmt.Printf("Namespace created\n")
}, },
} }
@ -56,26 +76,70 @@ var destroyNamespaceCmd = &cobra.Command{
Short: "Destroys a namespace", Short: "Destroys a namespace",
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 { if len(args) < 1 {
return fmt.Errorf("Missing parameters") return errMissingParameter
} }
return nil return nil
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
o, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp()
if err != nil { namespaceName := args[0]
log.Fatalf("Error initializing: %s", err)
request := &v1.GetNamespaceRequest{
Name: namespaceName,
} }
err = h.DestroyNamespace(args[0])
if strings.HasPrefix(o, "json") { ctx, client, conn, cancel := getHeadscaleCLIClient()
JsonOutput(map[string]string{"Result": "Namespace destroyed"}, err, o) defer cancel()
defer conn.Close()
_, err := client.GetNamespace(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error: %s", status.Convert(err).Message()),
output,
)
return return
} }
if err != nil {
fmt.Printf("Error destroying namespace: %s\n", err) confirm := false
return force, _ := cmd.Flags().GetBool("force")
if !force {
prompt := &survey.Confirm{
Message: fmt.Sprintf(
"Do you want to remove the namespace '%s' and any associated preauthkeys?",
namespaceName,
),
}
err := survey.AskOne(prompt, &confirm)
if err != nil {
return
}
}
if confirm || force {
request := &v1.DeleteNamespaceRequest{Name: namespaceName}
response, err := client.DeleteNamespace(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot destroy namespace: %s",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response, "Namespace destroyed", output)
} else {
SuccessOutput(map[string]string{"Result": "Namespace not destroyed"}, "Namespace not destroyed", output)
} }
fmt.Printf("Namespace destroyed\n")
}, },
} }
@ -83,28 +147,51 @@ var listNamespacesCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List all the namespaces", Short: "List all the namespaces",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
o, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp()
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ListNamespacesRequest{}
response, err := client.ListNamespaces(ctx, request)
if err != nil { if err != nil {
log.Fatalf("Error initializing: %s", err) ErrorOutput(
} err,
namespaces, err := h.ListNamespaces() fmt.Sprintf("Cannot get namespaces: %s", status.Convert(err).Message()),
if strings.HasPrefix(o, "json") { output,
JsonOutput(namespaces, err, o) )
return
}
if err != nil {
fmt.Println(err)
return return
} }
d := pterm.TableData{{"ID", "Name", "Created"}} if output != "" {
for _, n := range *namespaces { SuccessOutput(response.Namespaces, "", output)
d = append(d, []string{strconv.FormatUint(uint64(n.ID), 10), n.Name, n.CreatedAt.Format("2006-01-02 15:04:05")})
return
} }
err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
tableData := pterm.TableData{{"ID", "Name", "Created"}}
for _, namespace := range response.GetNamespaces() {
tableData = append(
tableData,
[]string{
namespace.GetId(),
namespace.GetName(),
namespace.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
},
)
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil { if err != nil {
log.Fatal(err) ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
} }
}, },
} }
@ -113,26 +200,39 @@ var renameNamespaceCmd = &cobra.Command{
Use: "rename OLD_NAME NEW_NAME", Use: "rename OLD_NAME NEW_NAME",
Short: "Renames a namespace", Short: "Renames a namespace",
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 2 { expectedArguments := 2
return fmt.Errorf("Missing parameters") if len(args) < expectedArguments {
return errMissingParameter
} }
return nil return nil
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
o, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp()
if err != nil { ctx, client, conn, cancel := getHeadscaleCLIClient()
log.Fatalf("Error initializing: %s", err) defer cancel()
defer conn.Close()
request := &v1.RenameNamespaceRequest{
OldName: args[0],
NewName: args[1],
} }
err = h.RenameNamespace(args[0], args[1])
if strings.HasPrefix(o, "json") { response, err := client.RenameNamespace(ctx, request)
JsonOutput(map[string]string{"Result": "Namespace renamed"}, err, o) if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot rename namespace: %s",
status.Convert(err).Message(),
),
output,
)
return return
} }
if err != nil {
fmt.Printf("Error renaming namespace: %s\n", err) SuccessOutput(response.Namespace, "Namespace renamed", output)
return
}
fmt.Printf("Namespace renamed\n")
}, },
} }

View File

@ -4,28 +4,70 @@ import (
"fmt" "fmt"
"log" "log"
"strconv" "strconv"
"strings"
"time" "time"
survey "github.com/AlecAivazis/survey/v2" survey "github.com/AlecAivazis/survey/v2"
"github.com/juanfont/headscale" "github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"tailscale.com/tailcfg" "google.golang.org/grpc/status"
"tailscale.com/types/wgkey" "tailscale.com/types/key"
) )
func init() { func init() {
rootCmd.AddCommand(nodeCmd) rootCmd.AddCommand(nodeCmd)
nodeCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace") listNodesCmd.Flags().StringP("namespace", "n", "", "Filter by namespace")
err := nodeCmd.MarkPersistentFlagRequired("namespace") nodeCmd.AddCommand(listNodesCmd)
registerNodeCmd.Flags().StringP("namespace", "n", "", "Namespace")
err := registerNodeCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
registerNodeCmd.Flags().StringP("key", "k", "", "Key")
err = registerNodeCmd.MarkFlagRequired("key")
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
nodeCmd.AddCommand(listNodesCmd)
nodeCmd.AddCommand(registerNodeCmd) nodeCmd.AddCommand(registerNodeCmd)
expireNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = expireNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(expireNodeCmd)
deleteNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = deleteNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(deleteNodeCmd) nodeCmd.AddCommand(deleteNodeCmd)
shareMachineCmd.Flags().StringP("namespace", "n", "", "Namespace")
err = shareMachineCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
shareMachineCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = shareMachineCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(shareMachineCmd) nodeCmd.AddCommand(shareMachineCmd)
unshareMachineCmd.Flags().StringP("namespace", "n", "", "Namespace")
err = unshareMachineCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
unshareMachineCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = unshareMachineCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(unshareMachineCmd) nodeCmd.AddCommand(unshareMachineCmd)
} }
@ -35,120 +77,208 @@ var nodeCmd = &cobra.Command{
} }
var registerNodeCmd = &cobra.Command{ var registerNodeCmd = &cobra.Command{
Use: "register machineID", Use: "register",
Short: "Registers a machine to your network", Short: "Registers a machine to your network",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("missing parameters")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
n, err := cmd.Flags().GetString("namespace") output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
log.Fatalf("Error getting namespace: %s", err) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
}
o, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp()
if err != nil {
log.Fatalf("Error initializing: %s", err)
}
m, err := h.RegisterMachine(args[0], n)
if strings.HasPrefix(o, "json") {
JsonOutput(m, err, o)
return return
} }
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
machineKey, err := cmd.Flags().GetString("key")
if err != nil { if err != nil {
fmt.Printf("Cannot register machine: %s\n", err) ErrorOutput(
err,
fmt.Sprintf("Error getting machine key from flag: %s", err),
output,
)
return return
} }
fmt.Printf("Machine registered\n")
request := &v1.RegisterMachineRequest{
Key: machineKey,
Namespace: namespace,
}
response, err := client.RegisterMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot register machine: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response.Machine, "Machine register", output)
}, },
} }
var listNodesCmd = &cobra.Command{ var listNodesCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List the nodes in a given namespace", Short: "List nodes",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
n, err := cmd.Flags().GetString("namespace") output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
log.Fatalf("Error getting namespace: %s", err) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
}
o, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp()
if err != nil {
log.Fatalf("Error initializing: %s", err)
}
namespace, err := h.GetNamespace(n)
if err != nil {
log.Fatalf("Error fetching namespace: %s", err)
}
machines, err := h.ListMachinesInNamespace(n)
if err != nil {
log.Fatalf("Error fetching machines: %s", err)
}
sharedMachines, err := h.ListSharedMachinesInNamespace(n)
if err != nil {
log.Fatalf("Error fetching shared machines: %s", err)
}
allMachines := append(*machines, *sharedMachines...)
if strings.HasPrefix(o, "json") {
JsonOutput(allMachines, err, o)
return return
} }
if err != nil { ctx, client, conn, cancel := getHeadscaleCLIClient()
log.Fatalf("Error getting nodes: %s", err) defer cancel()
defer conn.Close()
request := &v1.ListMachinesRequest{
Namespace: namespace,
} }
d, err := nodesToPtables(*namespace, allMachines) response, err := client.ListMachines(ctx, request)
if err != nil { if err != nil {
log.Fatalf("Error converting to table: %s", err) ErrorOutput(
err,
fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
output,
)
return
} }
err = pterm.DefaultTable.WithHasHeader().WithData(d).Render() if output != "" {
SuccessOutput(response.Machines, "", output)
return
}
tableData, err := nodesToPtables(namespace, response.Machines)
if err != nil { if err != nil {
log.Fatal(err) ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
return
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
} }
}, },
} }
var deleteNodeCmd = &cobra.Command{ var expireNodeCmd = &cobra.Command{
Use: "delete ID", Use: "expire",
Short: "Delete a node", Short: "Expire (log out) a machine in your network",
Args: func(cmd *cobra.Command, args []string) error { Long: "Expiring a node will keep the node in the database and force it to reauthenticate.",
if len(args) < 1 { Aliases: []string{"logout"},
return fmt.Errorf("missing parameters")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp()
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil { if err != nil {
log.Fatalf("Error initializing: %s", err) ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
} }
id, err := strconv.Atoi(args[0])
if err != nil { ctx, client, conn, cancel := getHeadscaleCLIClient()
log.Fatalf("Error converting ID to integer: %s", err) defer cancel()
defer conn.Close()
request := &v1.ExpireMachineRequest{
MachineId: identifier,
} }
m, err := h.GetMachineByID(uint64(id))
response, err := client.ExpireMachine(ctx, request)
if err != nil { if err != nil {
log.Fatalf("Error getting node: %s", err) ErrorOutput(
err,
fmt.Sprintf(
"Cannot expire machine: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response.Machine, "Machine expired", output)
},
}
var deleteNodeCmd = &cobra.Command{
Use: "delete",
Short: "Delete a node",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
getRequest := &v1.GetMachineRequest{
MachineId: identifier,
}
getResponse, err := client.GetMachine(ctx, getRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Error getting node node: %s",
status.Convert(err).Message(),
),
output,
)
return
}
deleteRequest := &v1.DeleteMachineRequest{
MachineId: identifier,
} }
confirm := false confirm := false
force, _ := cmd.Flags().GetBool("force") force, _ := cmd.Flags().GetBool("force")
if !force { if !force {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: fmt.Sprintf("Do you want to remove the node %s?", m.Name), Message: fmt.Sprintf(
"Do you want to remove the node %s?",
getResponse.GetMachine().Name,
),
} }
err = survey.AskOne(prompt, &confirm) err = survey.AskOne(prompt, &confirm)
if err != nil { if err != nil {
@ -157,162 +287,250 @@ var deleteNodeCmd = &cobra.Command{
} }
if confirm || force { if confirm || force {
err = h.DeleteMachine(m) response, err := client.DeleteMachine(ctx, deleteRequest)
if strings.HasPrefix(output, "json") { if output != "" {
JsonOutput(map[string]string{"Result": "Node deleted"}, err, output) SuccessOutput(response, "", output)
return return
} }
if err != nil { if err != nil {
log.Fatalf("Error deleting node: %s", err) ErrorOutput(
} err,
fmt.Printf("Node deleted\n") fmt.Sprintf(
} else { "Error deleting node: %s",
if strings.HasPrefix(output, "json") { status.Convert(err).Message(),
JsonOutput(map[string]string{"Result": "Node not deleted"}, err, output) ),
output,
)
return return
} }
fmt.Printf("Node not deleted\n") SuccessOutput(
map[string]string{"Result": "Node deleted"},
"Node deleted",
output,
)
} else {
SuccessOutput(map[string]string{"Result": "Node not deleted"}, "Node not deleted", output)
} }
}, },
} }
func sharingWorker(
cmd *cobra.Command,
) (string, *v1.Machine, *v1.Namespace, error) {
output, _ := cmd.Flags().GetString("output")
namespaceStr, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return "", nil, nil, err
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting ID to integer: %s", err), output)
return "", nil, nil, err
}
machineRequest := &v1.GetMachineRequest{
MachineId: identifier,
}
machineResponse, err := client.GetMachine(ctx, machineRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()),
output,
)
return "", nil, nil, err
}
namespaceRequest := &v1.GetNamespaceRequest{
Name: namespaceStr,
}
namespaceResponse, err := client.GetNamespace(ctx, namespaceRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()),
output,
)
return "", nil, nil, err
}
return output, machineResponse.GetMachine(), namespaceResponse.GetNamespace(), nil
}
var shareMachineCmd = &cobra.Command{ var shareMachineCmd = &cobra.Command{
Use: "share ID namespace", Use: "share",
Short: "Shares a node from the current namespace to the specified one", Short: "Shares a node from the current namespace to the specified one",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return fmt.Errorf("missing parameters")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
namespace, err := cmd.Flags().GetString("namespace") output, machine, namespace, err := sharingWorker(cmd)
if err != nil { if err != nil {
log.Fatalf("Error getting namespace: %s", err) ErrorOutput(
} err,
output, _ := cmd.Flags().GetString("output") fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
output,
)
h, err := getHeadscaleApp()
if err != nil {
log.Fatalf("Error initializing: %s", err)
}
_, err = h.GetNamespace(namespace)
if err != nil {
log.Fatalf("Error fetching origin namespace: %s", err)
}
destinationNamespace, err := h.GetNamespace(args[1])
if err != nil {
log.Fatalf("Error fetching destination namespace: %s", err)
}
id, err := strconv.Atoi(args[0])
if err != nil {
log.Fatalf("Error converting ID to integer: %s", err)
}
machine, err := h.GetMachineByID(uint64(id))
if err != nil {
log.Fatalf("Error getting node: %s", err)
}
err = h.AddSharedMachineToNamespace(machine, destinationNamespace)
if strings.HasPrefix(output, "json") {
JsonOutput(map[string]string{"Result": "Node shared"}, err, output)
return
}
if err != nil {
fmt.Printf("Error sharing node: %s\n", err)
return return
} }
fmt.Println("Node shared!") ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ShareMachineRequest{
MachineId: machine.Id,
Namespace: namespace.Name,
}
response, err := client.ShareMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error sharing node: %s", status.Convert(err).Message()),
output,
)
return
}
SuccessOutput(response.Machine, "Node shared", output)
}, },
} }
var unshareMachineCmd = &cobra.Command{ var unshareMachineCmd = &cobra.Command{
Use: "unshare ID", Use: "unshare",
Short: "Unshares a node from the specified namespace", Short: "Unshares a node from the specified namespace",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("missing parameters")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
namespace, err := cmd.Flags().GetString("namespace") output, machine, namespace, err := sharingWorker(cmd)
if err != nil { if err != nil {
log.Fatalf("Error getting namespace: %s", err) ErrorOutput(
} err,
output, _ := cmd.Flags().GetString("output") fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
output,
)
h, err := getHeadscaleApp()
if err != nil {
log.Fatalf("Error initializing: %s", err)
}
n, err := h.GetNamespace(namespace)
if err != nil {
log.Fatalf("Error fetching namespace: %s", err)
}
id, err := strconv.Atoi(args[0])
if err != nil {
log.Fatalf("Error converting ID to integer: %s", err)
}
machine, err := h.GetMachineByID(uint64(id))
if err != nil {
log.Fatalf("Error getting node: %s", err)
}
err = h.RemoveSharedMachineFromNamespace(machine, n)
if strings.HasPrefix(output, "json") {
JsonOutput(map[string]string{"Result": "Node unshared"}, err, output)
return
}
if err != nil {
fmt.Printf("Error unsharing node: %s\n", err)
return return
} }
fmt.Println("Node unshared!") ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.UnshareMachineRequest{
MachineId: machine.Id,
Namespace: namespace.Name,
}
response, err := client.UnshareMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error unsharing node: %s", status.Convert(err).Message()),
output,
)
return
}
SuccessOutput(response.Machine, "Node unshared", output)
}, },
} }
func nodesToPtables(currentNamespace headscale.Namespace, machines []headscale.Machine) (pterm.TableData, error) { func nodesToPtables(
d := pterm.TableData{{"ID", "Name", "NodeKey", "Namespace", "IP address", "Ephemeral", "Last seen", "Online"}} currentNamespace string,
machines []*v1.Machine,
) (pterm.TableData, error) {
tableData := pterm.TableData{
{
"ID",
"Name",
"NodeKey",
"Namespace",
"IP address",
"Ephemeral",
"Last seen",
"Online",
"Expired",
},
}
for _, machine := range machines { for _, machine := range machines {
var ephemeral bool var ephemeral bool
if machine.AuthKey != nil && machine.AuthKey.Ephemeral { if machine.PreAuthKey != nil && machine.PreAuthKey.Ephemeral {
ephemeral = true ephemeral = true
} }
var lastSeen time.Time var lastSeen time.Time
var lastSeenTime string var lastSeenTime string
if machine.LastSeen != nil { if machine.LastSeen != nil {
lastSeen = *machine.LastSeen lastSeen = machine.LastSeen.AsTime()
lastSeenTime = lastSeen.Format("2006-01-02 15:04:05") lastSeenTime = lastSeen.Format("2006-01-02 15:04:05")
} }
nKey, err := wgkey.ParseHex(machine.NodeKey)
var expiry time.Time
if machine.Expiry != nil {
expiry = machine.Expiry.AsTime()
}
var nodeKey key.NodePublic
err := nodeKey.UnmarshalText(
[]byte(headscale.NodePublicKeyEnsurePrefix(machine.NodeKey)),
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
nodeKey := tailcfg.NodeKey(nKey)
var online string var online string
if lastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online if lastSeen.After(
online = pterm.LightGreen("true") time.Now().Add(-5 * time.Minute),
) { // TODO: Find a better way to reliably show if online
online = pterm.LightGreen("online")
} else { } else {
online = pterm.LightRed("false") online = pterm.LightRed("offline")
}
var expired string
if expiry.IsZero() || expiry.After(time.Now()) {
expired = pterm.LightGreen("no")
} else {
expired = pterm.LightRed("yes")
} }
var namespace string var namespace string
if currentNamespace.ID == machine.NamespaceID { if currentNamespace == "" || (currentNamespace == machine.Namespace.Name) {
namespace = pterm.LightMagenta(machine.Namespace.Name) namespace = pterm.LightMagenta(machine.Namespace.Name)
} else { } else {
// Shared into this namespace
namespace = pterm.LightYellow(machine.Namespace.Name) namespace = pterm.LightYellow(machine.Namespace.Name)
} }
d = append(d, []string{strconv.FormatUint(machine.ID, 10), machine.Name, nodeKey.ShortString(), namespace, machine.IPAddress, strconv.FormatBool(ephemeral), lastSeenTime, online}) tableData = append(
tableData,
[]string{
strconv.FormatUint(machine.Id, headscale.Base10),
machine.Name,
nodeKey.ShortString(),
namespace,
machine.IpAddress,
strconv.FormatBool(ephemeral),
lastSeenTime,
online,
expired,
},
)
} }
return d, nil
return tableData, nil
} }

View File

@ -2,14 +2,18 @@ package cli
import ( import (
"fmt" "fmt"
"log"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/hako/durafmt" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
DefaultPreAuthKeyExpiry = 1 * time.Hour
) )
func init() { func init() {
@ -17,14 +21,17 @@ func init() {
preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace") preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace")
err := preauthkeysCmd.MarkPersistentFlagRequired("namespace") err := preauthkeysCmd.MarkPersistentFlagRequired("namespace")
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatal().Err(err).Msg("")
} }
preauthkeysCmd.AddCommand(listPreAuthKeys) preauthkeysCmd.AddCommand(listPreAuthKeys)
preauthkeysCmd.AddCommand(createPreAuthKeyCmd) preauthkeysCmd.AddCommand(createPreAuthKeyCmd)
preauthkeysCmd.AddCommand(expirePreAuthKeyCmd) preauthkeysCmd.AddCommand(expirePreAuthKeyCmd)
createPreAuthKeyCmd.PersistentFlags().Bool("reusable", false, "Make the preauthkey reusable") createPreAuthKeyCmd.PersistentFlags().
createPreAuthKeyCmd.PersistentFlags().Bool("ephemeral", false, "Preauthkey for ephemeral nodes") Bool("reusable", false, "Make the preauthkey reusable")
createPreAuthKeyCmd.Flags().StringP("expiration", "e", "", "Human-readable expiration of the key (30m, 24h, 365d...)") createPreAuthKeyCmd.PersistentFlags().
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
createPreAuthKeyCmd.Flags().
DurationP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (30m, 24h, 365d...)")
} }
var preauthkeysCmd = &cobra.Command{ var preauthkeysCmd = &cobra.Command{
@ -36,55 +43,76 @@ var listPreAuthKeys = &cobra.Command{
Use: "list", Use: "list",
Short: "List the preauthkeys for this namespace", Short: "List the preauthkeys for this namespace",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
n, err := cmd.Flags().GetString("namespace") output, _ := cmd.Flags().GetString("output")
if err != nil {
log.Fatalf("Error getting namespace: %s", err)
}
o, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp() namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
log.Fatalf("Error initializing: %s", err) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
}
keys, err := h.GetPreAuthKeys(n)
if strings.HasPrefix(o, "json") {
JsonOutput(keys, err, o)
return return
} }
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ListPreAuthKeysRequest{
Namespace: namespace,
}
response, err := client.ListPreAuthKeys(ctx, request)
if err != nil { if err != nil {
fmt.Printf("Error getting the list of keys: %s\n", err) ErrorOutput(
err,
fmt.Sprintf("Error getting the list of keys: %s", err),
output,
)
return return
} }
d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"}} if output != "" {
for _, k := range *keys { SuccessOutput(response.PreAuthKeys, "", output)
return
}
tableData := pterm.TableData{
{"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"},
}
for _, key := range response.PreAuthKeys {
expiration := "-" expiration := "-"
if k.Expiration != nil { if key.GetExpiration() != nil {
expiration = k.Expiration.Format("2006-01-02 15:04:05") expiration = key.Expiration.AsTime().Format("2006-01-02 15:04:05")
} }
var reusable string var reusable string
if k.Ephemeral { if key.GetEphemeral() {
reusable = "N/A" reusable = "N/A"
} else { } else {
reusable = fmt.Sprintf("%v", k.Reusable) reusable = fmt.Sprintf("%v", key.GetReusable())
} }
d = append(d, []string{ tableData = append(tableData, []string{
strconv.FormatUint(k.ID, 10), key.GetId(),
k.Key, key.GetKey(),
reusable, reusable,
strconv.FormatBool(k.Ephemeral), strconv.FormatBool(key.GetEphemeral()),
fmt.Sprintf("%v", k.Used), strconv.FormatBool(key.GetUsed()),
expiration, expiration,
k.CreatedAt.Format("2006-01-02 15:04:05"), key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
}) })
} }
err = pterm.DefaultTable.WithHasHeader().WithData(d).Render() err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil { if err != nil {
log.Fatal(err) ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
} }
}, },
} }
@ -93,40 +121,53 @@ var createPreAuthKeyCmd = &cobra.Command{
Use: "create", Use: "create",
Short: "Creates a new preauthkey in the specified namespace", Short: "Creates a new preauthkey in the specified namespace",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
n, err := cmd.Flags().GetString("namespace") output, _ := cmd.Flags().GetString("output")
if err != nil {
log.Fatalf("Error getting namespace: %s", err)
}
o, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp() namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
log.Fatalf("Error initializing: %s", err) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return
} }
reusable, _ := cmd.Flags().GetBool("reusable") reusable, _ := cmd.Flags().GetBool("reusable")
ephemeral, _ := cmd.Flags().GetBool("ephemeral") ephemeral, _ := cmd.Flags().GetBool("ephemeral")
e, _ := cmd.Flags().GetString("expiration") log.Trace().
var expiration *time.Time Bool("reusable", reusable).
if e != "" { Bool("ephemeral", ephemeral).
duration, err := durafmt.ParseStringShort(e) Str("namespace", namespace).
if err != nil { Msg("Preparing to create preauthkey")
log.Fatalf("Error parsing expiration: %s", err)
} request := &v1.CreatePreAuthKeyRequest{
exp := time.Now().UTC().Add(duration.Duration()) Namespace: namespace,
expiration = &exp Reusable: reusable,
Ephemeral: ephemeral,
} }
k, err := h.CreatePreAuthKey(n, reusable, ephemeral, expiration) duration, _ := cmd.Flags().GetDuration("expiration")
if strings.HasPrefix(o, "json") { expiration := time.Now().UTC().Add(duration)
JsonOutput(k, err, o)
return log.Trace().Dur("expiration", duration).Msg("expiration has been set")
}
request.Expiration = timestamppb.New(expiration)
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
response, err := client.CreatePreAuthKey(ctx, request)
if err != nil { if err != nil {
fmt.Println(err) ErrorOutput(
err,
fmt.Sprintf("Cannot create Pre Auth Key: %s\n", err),
output,
)
return return
} }
fmt.Printf("%s\n", k.Key)
SuccessOutput(response.PreAuthKey, response.PreAuthKey.Key, output)
}, },
} }
@ -135,40 +176,40 @@ var expirePreAuthKeyCmd = &cobra.Command{
Short: "Expire a preauthkey", Short: "Expire a preauthkey",
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 { if len(args) < 1 {
return fmt.Errorf("missing parameters") return errMissingParameter
} }
return nil return nil
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
n, err := cmd.Flags().GetString("namespace") output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
log.Fatalf("Error getting namespace: %s", err) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
}
o, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp()
if err != nil {
log.Fatalf("Error initializing: %s", err)
}
k, err := h.GetPreAuthKey(n, args[0])
if err != nil {
if strings.HasPrefix(o, "json") {
JsonOutput(k, err, o)
return
}
log.Fatalf("Error getting the key: %s", err)
}
err = h.MarkExpirePreAuthKey(k)
if strings.HasPrefix(o, "json") {
JsonOutput(k, err, o)
return return
} }
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ExpirePreAuthKeyRequest{
Namespace: namespace,
Key: args[0],
}
response, err := client.ExpirePreAuthKey(ctx, request)
if err != nil { if err != nil {
fmt.Println(err) ErrorOutput(
err,
fmt.Sprintf("Cannot expire Pre Auth Key: %s\n", err),
output,
)
return return
} }
fmt.Println("Expired")
SuccessOutput(response, "Key expired", output)
}, },
} }

View File

@ -8,8 +8,10 @@ import (
) )
func init() { func init() {
rootCmd.PersistentFlags().StringP("output", "o", "", "Output format. Empty for human-readable, 'json' or 'json-line'") rootCmd.PersistentFlags().
rootCmd.PersistentFlags().Bool("force", false, "Disable prompts and forces the execution") StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'")
rootCmd.PersistentFlags().
Bool("force", false, "Disable prompts and forces the execution")
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{

View File

@ -3,24 +3,35 @@ package cli
import ( import (
"fmt" "fmt"
"log" "log"
"strings" "strconv"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/status"
) )
func init() { func init() {
rootCmd.AddCommand(routesCmd) rootCmd.AddCommand(routesCmd)
routesCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace")
err := routesCmd.MarkPersistentFlagRequired("namespace") listRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err := listRoutesCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
routesCmd.AddCommand(listRoutesCmd)
enableRouteCmd.Flags().
StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to enable")
enableRouteCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = enableRouteCmd.MarkFlagRequired("identifier")
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
enableRouteCmd.Flags().BoolP("all", "a", false, "Enable all routes advertised by the node")
routesCmd.AddCommand(listRoutesCmd)
routesCmd.AddCommand(enableRouteCmd) routesCmd.AddCommand(enableRouteCmd)
nodeCmd.AddCommand(routesCmd)
} }
var routesCmd = &cobra.Command{ var routesCmd = &cobra.Command{
@ -29,119 +40,168 @@ var routesCmd = &cobra.Command{
} }
var listRoutesCmd = &cobra.Command{ var listRoutesCmd = &cobra.Command{
Use: "list NODE", Use: "list",
Short: "List the routes exposed by this node", Short: "List routes advertised and enabled by a given node",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("Missing parameters")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
n, err := cmd.Flags().GetString("namespace") output, _ := cmd.Flags().GetString("output")
if err != nil {
log.Fatalf("Error getting namespace: %s", err)
}
o, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp() machineID, err := cmd.Flags().GetUint64("identifier")
if err != nil { if err != nil {
log.Fatalf("Error initializing: %s", err) ErrorOutput(
} err,
fmt.Sprintf("Error getting machine id from flag: %s", err),
output,
)
availableRoutes, err := h.GetAdvertisedNodeRoutes(n, args[0])
if err != nil {
fmt.Println(err)
return return
} }
if strings.HasPrefix(o, "json") { ctx, client, conn, cancel := getHeadscaleCLIClient()
// TODO: Add enable/disabled information to this interface defer cancel()
JsonOutput(availableRoutes, err, o) defer conn.Close()
request := &v1.GetMachineRouteRequest{
MachineId: machineID,
}
response, err := client.GetMachineRoute(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
output,
)
return return
} }
d := h.RoutesToPtables(n, args[0], *availableRoutes) if output != "" {
SuccessOutput(response.Routes, "", output)
err = pterm.DefaultTable.WithHasHeader().WithData(d).Render() return
}
tableData := routesToPtables(response.Routes)
if err != nil { if err != nil {
log.Fatal(err) ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
return
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
} }
}, },
} }
var enableRouteCmd = &cobra.Command{ var enableRouteCmd = &cobra.Command{
Use: "enable node-name route", Use: "enable",
Short: "Allows exposing a route declared by this node to the rest of the nodes", Short: "Set the enabled routes for a given node",
Args: func(cmd *cobra.Command, args []string) error { Long: `This command will take a list of routes that will _replace_
all, err := cmd.Flags().GetBool("all") the current set of routes on a given node.
if err != nil { If you would like to disable a route, simply run the command again, but
log.Fatalf("Error getting namespace: %s", err) omit the route you do not want to enable.
} `,
if all {
if len(args) < 1 {
return fmt.Errorf("Missing parameters")
}
return nil
} else {
if len(args) < 2 {
return fmt.Errorf("Missing parameters")
}
return nil
}
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
n, err := cmd.Flags().GetString("namespace") output, _ := cmd.Flags().GetString("output")
machineID, err := cmd.Flags().GetUint64("identifier")
if err != nil { if err != nil {
log.Fatalf("Error getting namespace: %s", err) ErrorOutput(
err,
fmt.Sprintf("Error getting machine id from flag: %s", err),
output,
)
return
} }
o, _ := cmd.Flags().GetString("output") routes, err := cmd.Flags().GetStringSlice("route")
all, err := cmd.Flags().GetBool("all")
if err != nil { if err != nil {
log.Fatalf("Error getting namespace: %s", err) ErrorOutput(
err,
fmt.Sprintf("Error getting routes from flag: %s", err),
output,
)
return
} }
h, err := getHeadscaleApp() ctx, client, conn, cancel := getHeadscaleCLIClient()
if err != nil { defer cancel()
log.Fatalf("Error initializing: %s", err) defer conn.Close()
request := &v1.EnableMachineRoutesRequest{
MachineId: machineID,
Routes: routes,
} }
if all { response, err := client.EnableMachineRoutes(ctx, request)
availableRoutes, err := h.GetAdvertisedNodeRoutes(n, args[0]) if err != nil {
if err != nil { ErrorOutput(
fmt.Println(err) err,
return fmt.Sprintf(
} "Cannot register machine: %s\n",
status.Convert(err).Message(),
),
output,
)
for _, availableRoute := range *availableRoutes { return
err = h.EnableNodeRoute(n, args[0], availableRoute.String()) }
if err != nil {
fmt.Println(err)
return
}
if strings.HasPrefix(o, "json") { if output != "" {
JsonOutput(availableRoute, err, o) SuccessOutput(response.Routes, "", output)
} else {
fmt.Printf("Enabled route %s\n", availableRoute)
}
}
} else {
err = h.EnableNodeRoute(n, args[0], args[1])
if strings.HasPrefix(o, "json") { return
JsonOutput(args[1], err, o) }
return
}
if err != nil { tableData := routesToPtables(response.Routes)
fmt.Println(err) if err != nil {
return ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
}
fmt.Printf("Enabled route %s\n", args[1]) return
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
} }
}, },
} }
// routesToPtables converts the list of routes to a nice table.
func routesToPtables(routes *v1.Routes) pterm.TableData {
tableData := pterm.TableData{{"Route", "Enabled"}}
for _, route := range routes.GetAdvertisedRoutes() {
enabled := isStringInSlice(routes.EnabledRoutes, route)
tableData = append(tableData, []string{route, strconv.FormatBool(enabled)})
}
return tableData
}
func isStringInSlice(strs []string, s string) bool {
for _, s2 := range strs {
if s == s2 {
return true
}
}
return false
}

View File

@ -1,26 +1,33 @@
package cli package cli
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strconv"
"strings" "strings"
"time" "time"
"github.com/juanfont/headscale" "github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/viper" "github.com/spf13/viper"
"google.golang.org/grpc"
"gopkg.in/yaml.v2"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
) )
type ErrorOutput struct { const (
Error string PermissionFallback = 0o700
} )
func LoadConfig(path string) error { func LoadConfig(path string) error {
viper.SetConfigName("config") viper.SetConfigName("config")
@ -32,6 +39,9 @@ func LoadConfig(path string) error {
// For testing // For testing
viper.AddConfigPath(path) viper.AddConfigPath(path)
} }
viper.SetEnvPrefix("headscale")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv() viper.AutomaticEnv()
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache") viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
@ -43,9 +53,14 @@ func LoadConfig(path string) error {
viper.SetDefault("dns_config", nil) viper.SetDefault("dns_config", nil)
err := viper.ReadInConfig() viper.SetDefault("unix_socket", "/var/run/headscale.sock")
if err != nil { viper.SetDefault("unix_socket_permission", "0o770")
return fmt.Errorf("Fatal error reading config file: %s \n", err)
viper.SetDefault("cli.insecure", false)
viper.SetDefault("cli.timeout", "5s")
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("fatal error reading config file: %w", err)
} }
// Collect any validation errors and return them all at once // Collect any validation errors and return them all at once
@ -73,6 +88,7 @@ func LoadConfig(path string) 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"
} }
if errorText != "" { if errorText != "" {
//nolint
return errors.New(strings.TrimSuffix(errorText, "\n")) return errors.New(strings.TrimSuffix(errorText, "\n"))
} else { } else {
return nil return nil
@ -140,9 +156,14 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config.restricted_nameservers") { if viper.IsSet("dns_config.restricted_nameservers") {
if len(dnsConfig.Nameservers) > 0 { if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Routes = make(map[string][]dnstype.Resolver) dnsConfig.Routes = make(map[string][]dnstype.Resolver)
restrictedDNS := viper.GetStringMapStringSlice("dns_config.restricted_nameservers") restrictedDNS := viper.GetStringMapStringSlice(
"dns_config.restricted_nameservers",
)
for domain, restrictedNameservers := range restrictedDNS { for domain, restrictedNameservers := range restrictedDNS {
restrictedResolvers := make([]dnstype.Resolver, len(restrictedNameservers)) restrictedResolvers := make(
[]dnstype.Resolver,
len(restrictedNameservers),
)
for index, nameserverStr := range restrictedNameservers { for index, nameserverStr := range restrictedNameservers {
nameserver, err := netaddr.ParseIP(nameserverStr) nameserver, err := netaddr.ParseIP(nameserverStr)
if err != nil { if err != nil {
@ -199,35 +220,26 @@ func absPath(path string) string {
path = filepath.Join(dir, path) path = filepath.Join(dir, path)
} }
} }
return path return path
} }
func getHeadscaleApp() (*headscale.Headscale, error) { func getHeadscaleConfig() headscale.Config {
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
// to avoid races
minInactivityTimeout, _ := time.ParseDuration("65s")
if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout {
err := fmt.Errorf(
"ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s\n",
viper.GetString("ephemeral_node_inactivity_timeout"),
minInactivityTimeout,
)
return nil, err
}
dnsConfig, baseDomain := GetDNSConfig() dnsConfig, baseDomain := GetDNSConfig()
derpConfig := GetDERPConfig() derpConfig := GetDERPConfig()
cfg := headscale.Config{ return headscale.Config{
ServerURL: viper.GetString("server_url"), ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"), Addr: viper.GetString("listen_addr"),
PrivateKeyPath: absPath(viper.GetString("private_key_path")),
IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")), IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")),
PrivateKeyPath: absPath(viper.GetString("private_key_path")),
BaseDomain: baseDomain, BaseDomain: baseDomain,
DERP: derpConfig, DERP: derpConfig,
EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"), EphemeralNodeInactivityTimeout: viper.GetDuration(
"ephemeral_node_inactivity_timeout",
),
DBtype: viper.GetString("db_type"), DBtype: viper.GetString("db_type"),
DBpath: absPath(viper.GetString("db_path")), DBpath: absPath(viper.GetString("db_path")),
@ -237,9 +249,11 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
DBuser: viper.GetString("db_user"), DBuser: viper.GetString("db_user"),
DBpass: viper.GetString("db_pass"), DBpass: viper.GetString("db_pass"),
TLSLetsEncryptHostname: viper.GetString("tls_letsencrypt_hostname"), TLSLetsEncryptHostname: viper.GetString("tls_letsencrypt_hostname"),
TLSLetsEncryptListen: viper.GetString("tls_letsencrypt_listen"), TLSLetsEncryptListen: viper.GetString("tls_letsencrypt_listen"),
TLSLetsEncryptCacheDir: absPath(viper.GetString("tls_letsencrypt_cache_dir")), TLSLetsEncryptCacheDir: absPath(
viper.GetString("tls_letsencrypt_cache_dir"),
),
TLSLetsEncryptChallengeType: viper.GetString("tls_letsencrypt_challenge_type"), TLSLetsEncryptChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
TLSCertPath: absPath(viper.GetString("tls_cert_path")), TLSCertPath: absPath(viper.GetString("tls_cert_path")),
@ -249,9 +263,46 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
ACMEEmail: viper.GetString("acme_email"), ACMEEmail: viper.GetString("acme_email"),
ACMEURL: viper.GetString("acme_url"), ACMEURL: viper.GetString("acme_url"),
UnixSocket: viper.GetString("unix_socket"),
UnixSocketPermission: GetFileMode("unix_socket_permission"),
OIDC: headscale.OIDCConfig{
Issuer: viper.GetString("oidc.issuer"),
ClientID: viper.GetString("oidc.client_id"),
ClientSecret: viper.GetString("oidc.client_secret"),
},
CLI: headscale.CLIConfig{
Address: viper.GetString("cli.address"),
APIKey: viper.GetString("cli.api_key"),
Insecure: viper.GetBool("cli.insecure"),
Timeout: viper.GetDuration("cli.timeout"),
},
}
}
func getHeadscaleApp() (*headscale.Headscale, error) {
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
// to avoid races
minInactivityTimeout, _ := time.ParseDuration("65s")
if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout {
// TODO: Find a better way to return this text
//nolint
err := fmt.Errorf(
"ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s",
viper.GetString("ephemeral_node_inactivity_timeout"),
minInactivityTimeout,
)
return nil, err
} }
h, err := headscale.NewHeadscale(cfg) cfg := getHeadscaleConfig()
cfg.OIDC.MatchMap = loadOIDCMatchMap()
app, err := headscale.NewHeadscale(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -260,7 +311,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
if viper.GetString("acl_policy_path") != "" { if viper.GetString("acl_policy_path") != "" {
aclPath := absPath(viper.GetString("acl_policy_path")) aclPath := absPath(viper.GetString("acl_policy_path"))
err = h.LoadACLPolicy(aclPath) err = app.LoadACLPolicy(aclPath)
if err != nil { if err != nil {
log.Error(). log.Error().
Str("path", aclPath). Str("path", aclPath).
@ -269,46 +320,150 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
} }
} }
return h, nil return app, nil
} }
func JsonOutput(result interface{}, errResult error, outputFormat string) { func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
cfg := getHeadscaleConfig()
log.Debug().
Dur("timeout", cfg.CLI.Timeout).
Msgf("Setting timeout")
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
grpcOptions := []grpc.DialOption{
grpc.WithBlock(),
}
address := cfg.CLI.Address
// If the address is not set, we assume that we are on the server hosting headscale.
if address == "" {
log.Debug().
Str("socket", cfg.UnixSocket).
Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
address = cfg.UnixSocket
grpcOptions = append(
grpcOptions,
grpc.WithInsecure(),
grpc.WithContextDialer(headscale.GrpcSocketDialer),
)
} else {
// If we are not connecting to a local server, require an API key for authentication
apiKey := cfg.CLI.APIKey
if apiKey == "" {
log.Fatal().Msgf("HEADSCALE_CLI_API_KEY environment variable needs to be set.")
}
grpcOptions = append(grpcOptions,
grpc.WithPerRPCCredentials(tokenAuth{
token: apiKey,
}),
)
if cfg.CLI.Insecure {
grpcOptions = append(grpcOptions, grpc.WithInsecure())
}
}
log.Trace().Caller().Str("address", address).Msg("Connecting via gRPC")
conn, err := grpc.DialContext(ctx, address, grpcOptions...)
if err != nil {
log.Fatal().Err(err).Msgf("Could not connect: %v", err)
}
client := v1.NewHeadscaleServiceClient(conn)
return ctx, client, conn, cancel
}
func SuccessOutput(result interface{}, override string, outputFormat string) {
var j []byte var j []byte
var err error var err error
switch outputFormat { switch outputFormat {
case "json": case "json":
if errResult != nil { j, err = json.MarshalIndent(result, "", "\t")
j, err = json.MarshalIndent(ErrorOutput{errResult.Error()}, "", "\t") if err != nil {
if err != nil { log.Fatal().Err(err)
log.Fatal().Err(err)
}
} else {
j, err = json.MarshalIndent(result, "", "\t")
if err != nil {
log.Fatal().Err(err)
}
} }
case "json-line": case "json-line":
if errResult != nil { j, err = json.Marshal(result)
j, err = json.Marshal(ErrorOutput{errResult.Error()}) if err != nil {
if err != nil { log.Fatal().Err(err)
log.Fatal().Err(err)
}
} else {
j, err = json.Marshal(result)
if err != nil {
log.Fatal().Err(err)
}
} }
case "yaml":
j, err = yaml.Marshal(result)
if err != nil {
log.Fatal().Err(err)
}
default:
//nolint
fmt.Println(override)
return
} }
//nolint
fmt.Println(string(j)) fmt.Println(string(j))
} }
func HasJsonOutputFlag() bool { func ErrorOutput(errResult error, override string, outputFormat string) {
type errOutput struct {
Error string `json:"error"`
}
SuccessOutput(errOutput{errResult.Error()}, override, outputFormat)
}
func HasMachineOutputFlag() bool {
for _, arg := range os.Args { for _, arg := range os.Args {
if arg == "json" || arg == "json-line" { if arg == "json" || arg == "json-line" || arg == "yaml" {
return true return true
} }
} }
return false return false
} }
type tokenAuth struct {
token string
}
// Return value is mapped to request headers.
func (t tokenAuth) GetRequestMetadata(
ctx context.Context,
in ...string,
) (map[string]string, error) {
return map[string]string{
"authorization": "Bearer " + t.token,
}, nil
}
func (tokenAuth) RequireTransportSecurity() bool {
return true
}
// loadOIDCMatchMap is a wrapper around viper to verifies that the keys in
// the match map is valid regex strings.
func loadOIDCMatchMap() map[string]string {
strMap := viper.GetStringMapString("oidc.domain_map")
for oidcMatcher := range strMap {
_ = regexp.MustCompile(oidcMatcher)
}
return strMap
}
func GetFileMode(key string) fs.FileMode {
modeStr := viper.GetString(key)
mode, err := strconv.ParseUint(modeStr, headscale.Base8, headscale.BitSize64)
if err != nil {
return PermissionFallback
}
return fs.FileMode(mode)
}

View File

@ -1,9 +1,6 @@
package cli package cli
import ( import (
"fmt"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -18,11 +15,7 @@ var versionCmd = &cobra.Command{
Short: "Print the version.", Short: "Print the version.",
Long: "The version of headscale.", Long: "The version of headscale.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
o, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
if strings.HasPrefix(o, "json") { SuccessOutput(map[string]string{"version": Version}, Version, output)
JsonOutput(map[string]string{"version": Version}, nil, o)
return
}
fmt.Println(Version)
}, },
} }

View File

@ -23,6 +23,8 @@ func main() {
colors = true colors = true
case termcolor.LevelBasic: case termcolor.LevelBasic:
colors = true colors = true
case termcolor.LevelNone:
colors = false
default: default:
// no color, return text as is. // no color, return text as is.
colors = false colors = false
@ -41,38 +43,41 @@ func main() {
NoColor: !colors, NoColor: !colors,
}) })
err := cli.LoadConfig("") if err := cli.LoadConfig(""); err != nil {
if err != nil {
log.Fatal().Err(err) log.Fatal().Err(err)
} }
machineOutput := cli.HasMachineOutputFlag()
logLevel := viper.GetString("log_level") logLevel := viper.GetString("log_level")
switch logLevel { level, err := zerolog.ParseLevel(logLevel)
case "trace": if err != nil {
zerolog.SetGlobalLevel(zerolog.TraceLevel)
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
default:
zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.SetGlobalLevel(zerolog.DebugLevel)
} else {
zerolog.SetGlobalLevel(level)
} }
jsonOutput := cli.HasJsonOutputFlag() // If the user has requested a "machine" readable format,
if !viper.GetBool("disable_check_updates") && !jsonOutput { // then disable login so the output remains valid.
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && cli.Version != "dev" { if machineOutput {
zerolog.SetGlobalLevel(zerolog.Disabled)
}
if !viper.GetBool("disable_check_updates") && !machineOutput {
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
cli.Version != "dev" {
githubTag := &latest.GithubTag{ githubTag := &latest.GithubTag{
Owner: "juanfont", Owner: "juanfont",
Repository: "headscale", Repository: "headscale",
} }
res, err := latest.Check(githubTag, cli.Version) res, err := latest.Check(githubTag, cli.Version)
if err == nil && res.Outdated { if err == nil && res.Outdated {
fmt.Printf("An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n", //nolint
res.Current, cli.Version) fmt.Printf(
"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n",
res.Current,
cli.Version,
)
} }
} }
} }

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
"fmt" "io/fs"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -40,7 +40,10 @@ func (*Suite) TestConfigLoading(c *check.C) {
} }
// Symlink the example config file // Symlink the example config file
err = os.Symlink(filepath.Clean(path+"/../../config-example.yaml"), filepath.Join(tmpDir, "config.yaml")) err = os.Symlink(
filepath.Clean(path+"/../../config-example.yaml"),
filepath.Join(tmpDir, "config.yaml"),
)
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
@ -52,13 +55,13 @@ 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, "0.0.0.0:8080") c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080")
c.Assert(viper.GetStringSlice("derp.paths")[0], check.Equals, "derp-example.yaml")
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")
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
c.Assert(cli.GetFileMode("unix_socket_permission"), check.Equals, fs.FileMode(0o770))
} }
func (*Suite) TestDNSConfigLoading(c *check.C) { func (*Suite) TestDNSConfigLoading(c *check.C) {
@ -74,7 +77,10 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
} }
// Symlink the example config file // Symlink the example config file
err = os.Symlink(filepath.Clean(path+"/../../config-example.yaml"), filepath.Join(tmpDir, "config.yaml")) err = os.Symlink(
filepath.Clean(path+"/../../config-example.yaml"),
filepath.Join(tmpDir, "config.yaml"),
)
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
@ -94,7 +100,7 @@ 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 := ioutil.WriteFile(configFile, configYaml, 0o644) 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)
} }
@ -106,7 +112,6 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
c.Fatal(err) c.Fatal(err)
} }
// defer os.RemoveAll(tmpDir) // defer os.RemoveAll(tmpDir)
fmt.Println(tmpDir)
configYaml := []byte( configYaml := []byte(
"---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"", "---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"",
@ -128,8 +133,11 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
check.Matches, check.Matches,
".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*", ".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*",
) )
c.Assert(tmp, check.Matches, ".*Fatal config error: server_url must start with https:// or http://.*") c.Assert(
fmt.Println(tmp) tmp,
check.Matches,
".*Fatal config error: server_url must start with https:// or http://.*",
)
// Check configuration validation errors (2) // Check configuration validation errors (2)
configYaml = []byte( configYaml = []byte(

View File

@ -1,38 +1,65 @@
--- ---
# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order:
#
# - `/etc/headscale`
# - `~/.headscale`
# - current working directory
# The url clients will connect to. # The url clients will connect to.
# Typically this will be a domain. # Typically this will be a domain like:
#
# https://myheadscale.example.com:443
#
server_url: http://127.0.0.1:8080 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
#
listen_addr: 0.0.0.0:8080 listen_addr: 0.0.0.0:8080
# Path to WireGuard private key file # Private key used encrypt the traffic between headscale
private_key_path: private.key # and Tailscale clients.
# The private key file which will be
# autogenerated if it's missing
private_key_path: /var/lib/headscale/private.key
# DERP is a relay system that Tailscale uses when a direct
# connection cannot be established.
# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp
#
# headscale needs a list of DERP servers that can be presented
# to the clients.
derp: derp:
# List of externally available DERP maps encoded in JSON # List of externally available DERP maps encoded in JSON
urls: urls:
- https://controlplane.tailscale.com/derpmap/default - https://controlplane.tailscale.com/derpmap/default
# Locally available DERP map files encoded in YAML # Locally available DERP map files encoded in YAML
paths: #
- derp-example.yaml # This option is mostly interesting for people hosting
# their own DERP servers:
# https://tailscale.com/kb/1118/custom-derp-servers/
#
# paths:
# - /etc/headscale/derp-example.yaml
paths: []
# If enabled, a worker will be set up to periodically # If enabled, a worker will be set up to periodically
# refresh the given sources and update the derpmap # refresh the given sources and update the derpmap
# will be set up. # will be set up.
auto_update_enabled: true auto_update_enabled: true
# How often should we check for updates? # How often should we check for DERP updates?
update_frequency: 24h update_frequency: 24h
# Disables the automatic check for updates on startup # Disables the automatic check for headscale updates on startup
disable_check_updates: false disable_check_updates: false
# Time before an inactive ephemeral node is deleted?
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
# SQLite config # SQLite config
db_type: sqlite3 db_type: sqlite3
db_path: db.sqlite db_path: /var/lib/headscale/db.sqlite
# # Postgres config # # Postgres config
# db_type: postgres # db_type: postgres
@ -42,25 +69,98 @@ db_path: db.sqlite
# db_user: foo # db_user: foo
# db_pass: bar # db_pass: bar
### TLS configuration
#
## Let's encrypt / ACME
#
# headscale supports automatically requesting and setting up
# TLS for a domain with Let's Encrypt.
#
# URL to ACME directory
acme_url: https://acme-v02.api.letsencrypt.org/directory acme_url: https://acme-v02.api.letsencrypt.org/directory
# Email to register with ACME provider
acme_email: "" acme_email: ""
# Domain name to request a TLS certificate for:
tls_letsencrypt_hostname: "" tls_letsencrypt_hostname: ""
tls_letsencrypt_listen: ":http"
tls_letsencrypt_cache_dir: ".cache"
tls_letsencrypt_challenge_type: HTTP-01
# Path to store certificates and metadata needed by
# letsencrypt
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
# Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS_ALPN-01
# See [docs/tls.md](docs/tls.md) for more information
tls_letsencrypt_challenge_type: HTTP-01
# When HTTP-01 challenge is chosen, letsencrypt must set up a
# verification endpoint, and it will be listning on:
# :http = port 80
tls_letsencrypt_listen: ":http"
## Use already defined certificates:
tls_cert_path: "" tls_cert_path: ""
tls_key_path: "" tls_key_path: ""
log_level: info
# Path to a file containg ACL policies. # Path to a file containg ACL policies.
# Recommended path: /etc/headscale/acl.hujson
acl_policy_path: "" acl_policy_path: ""
## DNS
#
# headscale supports Tailscale's DNS configuration and MagicDNS.
# Please have a look to their KB to better understand the concepts:
#
# - https://tailscale.com/kb/1054/dns/
# - https://tailscale.com/kb/1081/magicdns/
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
#
dns_config: dns_config:
# Upstream DNS servers # List of DNS servers to expose to clients.
nameservers: nameservers:
- 1.1.1.1 - 1.1.1.1
# Split DNS (see https://tailscale.com/kb/1054/dns/),
# list of search domains and the DNS to query for each one.
#
# restricted_nameservers:
# foo.bar.com:
# - 1.1.1.1
# darp.headscale.net:
# - 1.1.1.1
# - 8.8.8.8
# Search domains to inject.
domains: [] domains: []
# Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
# Only works if there is at least a nameserver defined.
magic_dns: true magic_dns: true
# Defines the base domain to create the hostnames for MagicDNS.
# `base_domain` must be a FQDNs, without the trailing dot.
# The FQDN of the hosts will be
# `hostname.namespace.base_domain` (e.g., _myhost.mynamespace.example.com_).
base_domain: example.com base_domain: example.com
# Unix socket used for the CLI to connect without authentication
# Note: for local development, you probably want to change this to:
# unix_socket: ./headscale.sock
unix_socket: /var/run/headscale.sock
unix_socket_permission: "0770"
#
# headscale supports experimental OpenID connect support,
# it is still being tested and might have some bugs, please
# help us test it.
# OpenID Connect
# oidc:
# issuer: "https://your-oidc.issuer.com/path"
# client_id: "your-oidc-client-id"
# client_secret: "your-oidc-client-secret"
#
# # Domain map is used to map incomming users (by their email) to
# # a namespace. The key can be a string, or regex.
# domain_map:
# ".*": default-namespace

35
db.go
View File

@ -9,7 +9,10 @@ import (
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
) )
const dbVersion = "1" const (
dbVersion = "1"
errValueNotFound = Error("not found")
)
// 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...
type KV struct { type KV struct {
@ -24,7 +27,7 @@ func (h *Headscale) initDB() error {
} }
h.db = db h.db = db
if h.dbType == "postgres" { if h.dbType == Postgres {
db.Exec("create extension if not exists \"uuid-ossp\";") db.Exec("create extension if not exists \"uuid-ossp\";")
} }
err = db.AutoMigrate(&Machine{}) err = db.AutoMigrate(&Machine{})
@ -50,6 +53,7 @@ func (h *Headscale) initDB() error {
} }
err = h.setValue("db_version", dbVersion) err = h.setValue("db_version", dbVersion)
return err return err
} }
@ -65,12 +69,12 @@ func (h *Headscale) openDB() (*gorm.DB, error) {
} }
switch h.dbType { switch h.dbType {
case "sqlite3": case Sqlite:
db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{ db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
Logger: log, Logger: log,
}) })
case "postgres": case Postgres:
db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{ db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
Logger: log, Logger: log,
@ -84,28 +88,33 @@ func (h *Headscale) openDB() (*gorm.DB, error) {
return db, nil return db, nil
} }
// getValue returns the value for the given key in KV // getValue returns the value for the given key in KV.
func (h *Headscale) getValue(key string) (string, error) { func (h *Headscale) getValue(key string) (string, error) {
var row KV var row KV
if result := h.db.First(&row, "key = ?", key); errors.Is(result.Error, gorm.ErrRecordNotFound) { if result := h.db.First(&row, "key = ?", key); errors.Is(
return "", errors.New("not found") result.Error,
gorm.ErrRecordNotFound,
) {
return "", errValueNotFound
} }
return row.Value, nil return row.Value, nil
} }
// setValue sets value for the given key in KV // setValue sets value for the given key in KV.
func (h *Headscale) setValue(key string, value string) error { func (h *Headscale) setValue(key string, value string) error {
kv := KV{ keyValue := KV{
Key: key, Key: key,
Value: value, Value: value,
} }
_, err := h.getValue(key) if _, err := h.getValue(key); err == nil {
if err == nil { h.db.Model(&keyValue).Where("key = ?", key).Update("value", value)
h.db.Model(&kv).Where("key = ?", key).Update("value", value)
return nil return nil
} }
h.db.Create(kv) h.db.Create(keyValue)
return nil return nil
} }

View File

@ -1,15 +1,15 @@
# If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/ # If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/
regions: regions:
900: 900:
regionid: 900 regionid: 900
regioncode: custom regioncode: custom
regionname: My Region regionname: My Region
nodes: nodes:
- name: 1a - name: 900a
regionid: 1 regionid: 900
hostname: myderp.mydomain.no hostname: myderp.mydomain.no
ipv4: 123.123.123.123 ipv4: 123.123.123.123
ipv6: "2604:a880:400:d1::828:b001" ipv6: "2604:a880:400:d1::828:b001"
stunport: 0 stunport: 0
stunonly: false stunonly: false
derptestport: 0 derptestport: 0

26
derp.go
View File

@ -1,6 +1,7 @@
package headscale package headscale
import ( import (
"context"
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil" "io/ioutil"
@ -10,9 +11,7 @@ import (
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@ -28,14 +27,24 @@ func loadDERPMapFromPath(path string) (*tailcfg.DERPMap, error) {
return nil, err return nil, err
} }
err = yaml.Unmarshal(b, &derpMap) err = yaml.Unmarshal(b, &derpMap)
return &derpMap, err return &derpMap, err
} }
func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) { func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
client := http.Client{ ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout)
Timeout: 10 * time.Second, defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", addr.String(), nil)
if err != nil {
return nil, err
} }
resp, err := client.Get(addr.String())
client := http.Client{
Timeout: HTTPReadTimeout,
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -48,6 +57,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap var derpMap tailcfg.DERPMap
err = json.Unmarshal(body, &derpMap) err = json.Unmarshal(body, &derpMap)
return &derpMap, err return &derpMap, err
} }
@ -55,7 +65,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
// DERPMap, it will _only_ look at the Regions, an integer. // DERPMap, it will _only_ look at the Regions, an integer.
// If a region exists in two of the given DERPMaps, the region // If a region exists in two of the given DERPMaps, the region
// form the _last_ DERPMap will be preserved. // form the _last_ DERPMap will be preserved.
// An empty DERPMap list will result in a DERPMap with no regions // An empty DERPMap list will result in a DERPMap with no regions.
func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap { func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
result := tailcfg.DERPMap{ result := tailcfg.DERPMap{
OmitDefaultRegions: false, OmitDefaultRegions: false,
@ -86,6 +96,7 @@ func GetDERPMap(cfg DERPConfig) *tailcfg.DERPMap {
Str("path", path). Str("path", path).
Err(err). Err(err).
Msg("Could not load DERP map from path") Msg("Could not load DERP map from path")
break break
} }
@ -104,6 +115,7 @@ func GetDERPMap(cfg DERPConfig) *tailcfg.DERPMap {
Str("url", addr.String()). Str("url", addr.String()).
Err(err). Err(err).
Msg("Could not load DERP map from path") Msg("Could not load DERP map from path")
break break
} }
@ -144,7 +156,7 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
Msg("Failed to fetch namespaces") Msg("Failed to fetch namespaces")
} }
for _, namespace := range *namespaces { for _, namespace := range namespaces {
h.setLastStateChangeToNow(namespace.Name) h.setLastStateChangeToNow(namespace.Name)
} }
} }

34
dns.go
View File

@ -10,6 +10,10 @@ import (
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
) )
const (
ByteSize = 8
)
// 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.
@ -30,7 +34,9 @@ import (
// 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(ipPrefix netaddr.IPPrefix, baseDomain string) ([]dnsname.FQDN, error) { func generateMagicDNSRootDomains(
ipPrefix netaddr.IPPrefix,
) []dnsname.FQDN {
// TODO(juanfont): we are not handing out IPv6 addresses yet // TODO(juanfont): we are not handing out IPv6 addresses yet
// and in fact this is Tailscale.com's range (note the fd7a:115c:a1e0: range in the fc00::/7 network) // and in fact this is Tailscale.com's range (note the fd7a:115c:a1e0: range in the fc00::/7 network)
ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.")
@ -41,15 +47,15 @@ func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) (
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
lastOctet := maskBits / 8 lastOctet := maskBits / ByteSize
// wildcardBits is the number of bits not under the mask in the lastOctet // wildcardBits is the number of bits not under the mask in the lastOctet
wildcardBits := 8 - maskBits%8 wildcardBits := ByteSize - maskBits%ByteSize
// min is the value in the lastOctet byte of the IP // min is the value in the lastOctet byte of the IP
// max is basically 2^wildcardBits - i.e., the value when all the wildcardBits are set to 1 // max is basically 2^wildcardBits - i.e., the value when all the wildcardBits are set to 1
min := uint(netRange.IP[lastOctet]) min := uint(netRange.IP[lastOctet])
max := uint((min + 1<<uint(wildcardBits)) - 1) max := (min + 1<<uint(wildcardBits)) - 1
// here we generate the base domain (e.g., 100.in-addr.arpa., 16.172.in-addr.arpa., etc.) // here we generate the base domain (e.g., 100.in-addr.arpa., 16.172.in-addr.arpa., etc.)
rdnsSlice := []string{} rdnsSlice := []string{}
@ -66,18 +72,27 @@ func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) (
} }
fqdns = append(fqdns, fqdn) fqdns = append(fqdns, fqdn)
} }
return fqdns, nil
return fqdns
} }
func getMapResponseDNSConfig(dnsConfigOrig *tailcfg.DNSConfig, baseDomain string, m Machine, peers Machines) (*tailcfg.DNSConfig, error) { func getMapResponseDNSConfig(
dnsConfigOrig *tailcfg.DNSConfig,
baseDomain string,
machine Machine,
peers Machines,
) *tailcfg.DNSConfig {
var dnsConfig *tailcfg.DNSConfig 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 = dnsConfigOrig.Clone()
dnsConfig.Domains = append(dnsConfig.Domains, fmt.Sprintf("%s.%s", m.Namespace.Name, baseDomain)) dnsConfig.Domains = append(
dnsConfig.Domains,
fmt.Sprintf("%s.%s", machine.Namespace.Name, baseDomain),
)
namespaceSet := set.New(set.ThreadSafe) namespaceSet := set.New(set.ThreadSafe)
namespaceSet.Add(m.Namespace) namespaceSet.Add(machine.Namespace)
for _, p := range peers { for _, p := range peers {
namespaceSet.Add(p.Namespace) namespaceSet.Add(p.Namespace)
} }
@ -88,5 +103,6 @@ func getMapResponseDNSConfig(dnsConfigOrig *tailcfg.DNSConfig, baseDomain string
} else { } else {
dnsConfig = dnsConfigOrig dnsConfig = dnsConfigOrig
} }
return dnsConfig, nil
return dnsConfig
} }

View File

@ -11,13 +11,13 @@ import (
func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
prefix := netaddr.MustParseIPPrefix("100.64.0.0/10") prefix := netaddr.MustParseIPPrefix("100.64.0.0/10")
domains, err := generateMagicDNSRootDomains(prefix, "foobar.headscale.net") domains := generateMagicDNSRootDomains(prefix)
c.Assert(err, check.IsNil)
found := false found := false
for _, domain := range domains { for _, domain := range domains {
if domain == "64.100.in-addr.arpa." { if domain == "64.100.in-addr.arpa." {
found = true found = true
break break
} }
} }
@ -27,6 +27,7 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
for _, domain := range domains { for _, domain := range domains {
if domain == "100.100.in-addr.arpa." { if domain == "100.100.in-addr.arpa." {
found = true found = true
break break
} }
} }
@ -36,6 +37,7 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
for _, domain := range domains { for _, domain := range domains {
if domain == "127.100.in-addr.arpa." { if domain == "127.100.in-addr.arpa." {
found = true found = true
break break
} }
} }
@ -44,13 +46,13 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
prefix := netaddr.MustParseIPPrefix("172.16.0.0/16") prefix := netaddr.MustParseIPPrefix("172.16.0.0/16")
domains, err := generateMagicDNSRootDomains(prefix, "headscale.net") domains := generateMagicDNSRootDomains(prefix)
c.Assert(err, check.IsNil)
found := false found := false
for _, domain := range domains { for _, domain := range domains {
if domain == "0.16.172.in-addr.arpa." { if domain == "0.16.172.in-addr.arpa." {
found = true found = true
break break
} }
} }
@ -60,6 +62,7 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
for _, domain := range domains { for _, domain := range domains {
if domain == "255.16.172.in-addr.arpa." { if domain == "255.16.172.in-addr.arpa." {
found = true found = true
break break
} }
} }
@ -67,100 +70,120 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
} }
func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
n1, err := h.CreateNamespace("shared1") namespaceShared1, err := app.CreateNamespace("shared1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
n2, err := h.CreateNamespace("shared2") namespaceShared2, err := app.CreateNamespace("shared2")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
n3, err := h.CreateNamespace("shared3") namespaceShared3, err := app.CreateNamespace("shared3")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak1n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) preAuthKeyInShared1, err := app.CreatePreAuthKey(
namespaceShared1.Name,
false,
false,
nil,
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak2n2, err := h.CreatePreAuthKey(n2.Name, false, false, nil) preAuthKeyInShared2, err := app.CreatePreAuthKey(
namespaceShared2.Name,
false,
false,
nil,
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak3n3, err := h.CreatePreAuthKey(n3.Name, false, false, nil) preAuthKeyInShared3, err := app.CreatePreAuthKey(
namespaceShared3.Name,
false,
false,
nil,
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak4n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) PreAuthKey2InShared1, err := app.CreatePreAuthKey(
namespaceShared1.Name,
false,
false,
nil,
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1") _, err = app.GetMachine(namespaceShared1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
m1 := &Machine{ machineInShared1 := &Machine{
ID: 1, ID: 1,
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1", Name: "test_get_shared_nodes_1",
NamespaceID: n1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *n1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.1", IPAddress: "100.64.0.1",
AuthKeyID: uint(pak1n1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
h.db.Save(m1) app.db.Save(machineInShared1)
_, err = h.GetMachine(n1.Name, m1.Name) _, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Name)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
m2 := &Machine{ machineInShared2 := &Machine{
ID: 2, ID: 2,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2", Name: "test_get_shared_nodes_2",
NamespaceID: n2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *n2, Namespace: *namespaceShared2,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.2", IPAddress: "100.64.0.2",
AuthKeyID: uint(pak2n2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
h.db.Save(m2) app.db.Save(machineInShared2)
_, err = h.GetMachine(n2.Name, m2.Name) _, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Name)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
m3 := &Machine{ machineInShared3 := &Machine{
ID: 3, ID: 3,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_3", Name: "test_get_shared_nodes_3",
NamespaceID: n3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *n3, Namespace: *namespaceShared3,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.3", IPAddress: "100.64.0.3",
AuthKeyID: uint(pak3n3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
h.db.Save(m3) app.db.Save(machineInShared3)
_, err = h.GetMachine(n3.Name, m3.Name) _, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Name)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
m4 := &Machine{ machine2InShared1 := &Machine{
ID: 4, ID: 4,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_4", Name: "test_get_shared_nodes_4",
NamespaceID: n1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *n1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.4", IPAddress: "100.64.0.4",
AuthKeyID: uint(pak4n1.ID), AuthKeyID: uint(PreAuthKey2InShared1.ID),
} }
h.db.Save(m4) app.db.Save(machine2InShared1)
err = h.AddSharedMachineToNamespace(m2, n1) err = app.AddSharedMachineToNamespace(machineInShared2, namespaceShared1)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
baseDomain := "foobar.headscale.net" baseDomain := "foobar.headscale.net"
@ -170,122 +193,146 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Proxied: true, Proxied: true,
} }
m1peers, err := h.getPeers(m1) peersOfMachineInShared1, err := app.getPeers(machineInShared1)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
dnsConfig, err := getMapResponseDNSConfig(&dnsConfigOrig, baseDomain, *m1, m1peers) dnsConfig := getMapResponseDNSConfig(
c.Assert(err, check.IsNil) &dnsConfigOrig,
baseDomain,
*machineInShared1,
peersOfMachineInShared1,
)
c.Assert(dnsConfig, check.NotNil) c.Assert(dnsConfig, check.NotNil)
c.Assert(len(dnsConfig.Routes), check.Equals, 2) c.Assert(len(dnsConfig.Routes), check.Equals, 2)
routeN1 := fmt.Sprintf("%s.%s", n1.Name, baseDomain) domainRouteShared1 := fmt.Sprintf("%s.%s", namespaceShared1.Name, baseDomain)
_, ok := dnsConfig.Routes[routeN1] _, ok := dnsConfig.Routes[domainRouteShared1]
c.Assert(ok, check.Equals, true) c.Assert(ok, check.Equals, true)
routeN2 := fmt.Sprintf("%s.%s", n2.Name, baseDomain) domainRouteShared2 := fmt.Sprintf("%s.%s", namespaceShared2.Name, baseDomain)
_, ok = dnsConfig.Routes[routeN2] _, ok = dnsConfig.Routes[domainRouteShared2]
c.Assert(ok, check.Equals, true) c.Assert(ok, check.Equals, true)
routeN3 := fmt.Sprintf("%s.%s", n3.Name, baseDomain) domainRouteShared3 := fmt.Sprintf("%s.%s", namespaceShared3.Name, baseDomain)
_, ok = dnsConfig.Routes[routeN3] _, ok = dnsConfig.Routes[domainRouteShared3]
c.Assert(ok, check.Equals, false) c.Assert(ok, check.Equals, false)
} }
func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
n1, err := h.CreateNamespace("shared1") namespaceShared1, err := app.CreateNamespace("shared1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
n2, err := h.CreateNamespace("shared2") namespaceShared2, err := app.CreateNamespace("shared2")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
n3, err := h.CreateNamespace("shared3") namespaceShared3, err := app.CreateNamespace("shared3")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak1n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) preAuthKeyInShared1, err := app.CreatePreAuthKey(
namespaceShared1.Name,
false,
false,
nil,
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak2n2, err := h.CreatePreAuthKey(n2.Name, false, false, nil) preAuthKeyInShared2, err := app.CreatePreAuthKey(
namespaceShared2.Name,
false,
false,
nil,
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak3n3, err := h.CreatePreAuthKey(n3.Name, false, false, nil) preAuthKeyInShared3, err := app.CreatePreAuthKey(
namespaceShared3.Name,
false,
false,
nil,
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak4n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) preAuthKey2InShared1, err := app.CreatePreAuthKey(
namespaceShared1.Name,
false,
false,
nil,
)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1") _, err = app.GetMachine(namespaceShared1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
m1 := &Machine{ machineInShared1 := &Machine{
ID: 1, ID: 1,
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1", Name: "test_get_shared_nodes_1",
NamespaceID: n1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *n1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.1", IPAddress: "100.64.0.1",
AuthKeyID: uint(pak1n1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
h.db.Save(m1) app.db.Save(machineInShared1)
_, err = h.GetMachine(n1.Name, m1.Name) _, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Name)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
m2 := &Machine{ machineInShared2 := &Machine{
ID: 2, ID: 2,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2", Name: "test_get_shared_nodes_2",
NamespaceID: n2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *n2, Namespace: *namespaceShared2,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.2", IPAddress: "100.64.0.2",
AuthKeyID: uint(pak2n2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
h.db.Save(m2) app.db.Save(machineInShared2)
_, err = h.GetMachine(n2.Name, m2.Name) _, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Name)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
m3 := &Machine{ machineInShared3 := &Machine{
ID: 3, ID: 3,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_3", Name: "test_get_shared_nodes_3",
NamespaceID: n3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *n3, Namespace: *namespaceShared3,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.3", IPAddress: "100.64.0.3",
AuthKeyID: uint(pak3n3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
h.db.Save(m3) app.db.Save(machineInShared3)
_, err = h.GetMachine(n3.Name, m3.Name) _, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Name)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
m4 := &Machine{ machine2InShared1 := &Machine{
ID: 4, ID: 4,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_4", Name: "test_get_shared_nodes_4",
NamespaceID: n1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *n1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: "authKey", RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.4", IPAddress: "100.64.0.4",
AuthKeyID: uint(pak4n1.ID), AuthKeyID: uint(preAuthKey2InShared1.ID),
} }
h.db.Save(m4) app.db.Save(machine2InShared1)
err = h.AddSharedMachineToNamespace(m2, n1) err = app.AddSharedMachineToNamespace(machineInShared2, namespaceShared1)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
baseDomain := "foobar.headscale.net" baseDomain := "foobar.headscale.net"
@ -295,11 +342,15 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Proxied: false, Proxied: false,
} }
m1peers, err := h.getPeers(m1) peersOfMachine1Shared1, err := app.getPeers(machineInShared1)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
dnsConfig, err := getMapResponseDNSConfig(&dnsConfigOrig, baseDomain, *m1, m1peers) dnsConfig := getMapResponseDNSConfig(
c.Assert(err, check.IsNil) &dnsConfigOrig,
baseDomain,
*machineInShared1,
peersOfMachine1Shared1,
)
c.Assert(dnsConfig, check.NotNil) c.Assert(dnsConfig, check.NotNil)
c.Assert(len(dnsConfig.Routes), check.Equals, 0) c.Assert(len(dnsConfig.Routes), check.Equals, 0)
c.Assert(len(dnsConfig.Domains), check.Equals, 1) c.Assert(len(dnsConfig.Domains), check.Equals, 1)

View File

@ -1,80 +0,0 @@
# Configuration reference
Headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order:
- `/etc/headscale`
- `~/.headscale`
- current working directory
```yaml
server_url: http://headscale.mydomain.net
listen_addr: 0.0.0.0:8080
ip_prefix: 100.64.0.0/10
disable_check_updates: false
```
`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. `ip_prefix` is the IP prefix (range) in which IP addresses for nodes will be allocated (default 100.64.0.0/10, e.g., 192.168.4.0/24, 10.0.0.0/8). `disable_check_updates` disables the automatic check for updates.
```yaml
log_level: debug
```
`log_level` can be used to set the Log level for Headscale, it defaults to `debug`, and the available levels are: `trace`, `debug`, `info`, `warn` and `error`.
```yaml
private_key_path: private.key
```
`private_key_path` is the path to the Wireguard private key. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
```yaml
derp_map_path: derp.yaml
```
`derp_map_path` is the path to the [DERP](https://pkg.go.dev/tailscale.com/derp) map file. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
```yaml
ephemeral_node_inactivity_timeout": "30m"
```
`ephemeral_node_inactivity_timeout` is the timeout after which inactive ephemeral node records will be deleted from the database. The default is 30 minutes. This value must be higher than 65 seconds (the keepalive timeout for the HTTP long poll is 60 seconds, plus a few seconds to avoid race conditions).
PostgresSQL
```yaml
db_host: localhost
db_port: 5432
db_name: headscale
db_user: foo
db_pass: bar
```
SQLite
```yaml
db_type: sqlite3
db_path: db.sqlite
```
The fields starting with `db_` are used for the DB connection information.
### TLS configuration
Please check [`TLS.md`](TLS.md).
### DNS configuration
Please refer to [`DNS.md`](DNS.md).
### Policy ACLs
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
For instance, instead of referring to users when defining groups you must
use namespaces (which are the equivalent to user/logins in Tailscale.com).
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
### Apple devices
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.

View File

@ -1,38 +0,0 @@
# DNS in headscale
headscale supports Tailscale's DNS configuration and MagicDNS. Please have a look to their KB to better understand what this means:
- https://tailscale.com/kb/1054/dns/
- https://tailscale.com/kb/1081/magicdns/
- https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
Long story short, you can define the DNS servers you want to use in your tailnets, activate MagicDNS (so you don't have to remember the IP addresses of your nodes), define search domains, as well as predefined hosts. headscale will inject that settings into your nodes.
## Configuration reference
The setup is done via the `config.yaml` file, under the `dns_config` key.
```yaml
server_url: http://127.0.0.1:8001
listen_addr: 0.0.0.0:8001
private_key_path: private.key
dns_config:
nameservers:
- 1.1.1.1
- 8.8.8.8
restricted_nameservers:
foo.bar.com:
- 1.1.1.1
darp.headscale.net:
- 1.1.1.1
- 8.8.8.8
domains: []
magic_dns: true
base_domain: example.com
```
- `nameservers`: The list of DNS servers to use.
- `domains`: Search domains to inject.
- `magic_dns`: Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). Only works if there is at least a nameserver defined.
- `base_domain`: Defines the base domain to create the hostnames for MagicDNS. `base_domain` must be a FQDNs, without the trailing dot. The FQDN of the hosts will be `hostname.namespace.base_domain` (e.g., _myhost.mynamespace.example.com_).
- `restricted_nameservers`: Split DNS (see https://tailscale.com/kb/1054/dns/), list of search domains and the DNS to query for each one.

42
docs/README.md Normal file
View File

@ -0,0 +1,42 @@
# headscale documentation
This page contains the official and community contributed documentation for `headscale`.
If you are having trouble with following the documentation or get unexpected results,
please ask on [Discord](https://discord.gg/XcQxk2VHjx) instead of opening an Issue.
## Official documentation
### How-to
- [Running headscale on Linux](running-headscale-linux.md)
### References
- [Configuration](../config-example.yaml)
- [Glossary](glossary.md)
- [TLS](tls.md)
## Community documentation
Community documentation is not actively maintained by the headscale authors and is
written by community members. It is _not_ verified by `headscale` developers.
**It might be outdated and it might miss necessary steps**.
- [Running headscale in a container](running-headscale-container.md)
## Misc
### Policy ACLs
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
For instance, instead of referring to users when defining groups you must
use namespaces (which are the equivalent to user/logins in Tailscale.com).
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
### Apple devices
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.

View File

@ -1,149 +0,0 @@
# Running headscale
1. Download the headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your $PATH or use the docker container
```shell
docker pull headscale/headscale:x.x.x
```
<!--
or
```shell
docker pull ghrc.io/juanfont/headscale:x.x.x
``` -->
2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running
```shell
docker run --name headscale \
-e POSTGRES_DB=headscale
-e POSTGRES_USER=foo \
-e POSTGRES_PASSWORD=bar \
-p 5432:5432 \
-d postgres
```
3. Create a WireGuard private key and headscale configuration
```shell
wg genkey > private.key
cp config.yaml.example config.yaml
```
4. Create a namespace
```shell
headscale namespaces create myfirstnamespace
```
or docker:
the db.sqlite mount is only needed if you use sqlite
```shell
touch db.sqlite
docker run \
-v $(pwd)/private.key:/private.key \
-v $(pwd)/config.json:/config.json \
-v $(pwd)/derp.yaml:/derp.yaml \
-v $(pwd)/db.sqlite:/db.sqlite \
-p 127.0.0.1:8080:8080 \
headscale/headscale:x.x.x \
headscale namespaces create myfirstnamespace
```
or if your server is already running in docker:
```shell
docker exec <container_name> headscale create myfirstnamespace
```
5. Run the server
```shell
headscale serve
```
or docker:
the db.sqlite mount is only needed if you use sqlite
```shell
docker run \
-v $(pwd)/private.key:/private.key \
-v $(pwd)/config.json:/config.json \
-v $(pwd)/derp.yaml:/derp.yaml \
-v $(pwd)/db.sqlite:/db.sqlite \
-p 127.0.0.1:8080:8080 \
headscale/headscale:x.x.x headscale serve
```
6. If you used tailscale.com before in your nodes, make sure you clear the tailscaled data folder
```shell
systemctl stop tailscaled
rm -fr /var/lib/tailscale
systemctl start tailscaled
```
7. Add your first machine
```shell
tailscale up --login-server YOUR_HEADSCALE_URL
```
8. Navigate to the URL you will get with `tailscale up`, where you'll find your machine key.
9. In the server, register your machine to a namespace with the CLI
```shell
headscale -n myfirstnamespace nodes register YOURMACHINEKEY
```
or docker:
```shell
docker run \
-v $(pwd)/private.key:/private.key \
-v $(pwd)/config.json:/config.json \
-v $(pwd)/derp.yaml:/derp.yaml \
headscale/headscale:x.x.x \
headscale -n myfirstnamespace nodes register YOURMACHINEKEY
```
or if your server is already running in docker:
```shell
docker exec <container_name> headscale -n myfirstnamespace nodes register YOURMACHINEKEY
```
Alternatively, you can use Auth Keys to register your machines:
1. Create an authkey
```shell
headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
```
or docker:
```shell
docker run \
-v $(pwd)/private.key:/private.key \
-v $(pwd)/config.json:/config.json \
-v$(pwd)/derp.yaml:/derp.yaml \
-v $(pwd)/db.sqlite:/db.sqlite \
headscale/headscale:x.x.x \
headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
```
or if your server is already running in docker:
```shell
docker exec <container_name> headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
```
2. Use the authkey from your machine to register it
```shell
tailscale up --login-server YOUR_HEADSCALE_URL --authkey YOURAUTHKEY
```
If you create an authkey with the `--ephemeral` flag, that key will create ephemeral nodes. This implies that `--reusable` is true.
Please bear in mind that all headscale commands support adding `-o json` or `-o json-line` to get nicely JSON-formatted output.

5
docs/examples/README.md Normal file
View File

@ -0,0 +1,5 @@
# Examples
This directory contains examples on how to run `headscale` on different platforms.
All examples are provided by the community and they are not verified by the `headscale` authors.

View File

@ -1,5 +1,7 @@
# Deploying headscale on Kubernetes # Deploying headscale on Kubernetes
**Note:** This is contributed by the community and not verified by the headscale authors.
This directory contains [Kustomize](https://kustomize.io) templates that deploy This directory contains [Kustomize](https://kustomize.io) templates that deploy
headscale in various configurations. headscale in various configurations.
@ -24,6 +26,7 @@ Configure DERP servers by editing `base/site/derp.yaml` if needed.
You'll somehow need to get `headscale:latest` into your cluster image registry. You'll somehow need to get `headscale:latest` into your cluster image registry.
An easy way to do this with k3s: An easy way to do this with k3s:
- Reconfigure k3s to use docker instead of containerd (`k3s server --docker`) - Reconfigure k3s to use docker instead of containerd (`k3s server --docker`)
- `docker build -t headscale:latest ..` from here - `docker build -t headscale:latest ..` from here
@ -61,11 +64,11 @@ Use the wrapper script to remotely operate headscale to perform administrative
tasks like creating namespaces, authkeys, etc. tasks like creating namespaces, authkeys, etc.
``` ```
[c@nix-slate:~/Projects/headscale/k8s]$ ./headscale.bash [c@nix-slate:~/Projects/headscale/k8s]$ ./headscale.bash
headscale is an open source implementation of the Tailscale control server headscale is an open source implementation of the Tailscale control server
https://gitlab.com/juanfont/headscale https://github.com/juanfont/headscale
Usage: Usage:
headscale [command] headscale [command]

View File

@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: headscale
annotations:
kubernetes.io/ingress.class: traefik
spec:
rules:
- host: $(PUBLIC_HOSTNAME)
http:
paths:
- backend:
service:
name: headscale
port:
number: 8080
path: /
pathType: Prefix

View File

@ -0,0 +1,42 @@
namespace: headscale
resources:
- configmap.yaml
- ingress.yaml
- service.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: headscale-site
files:
- derp.yaml=site/derp.yaml
envs:
- site/public.env
- name: headscale-etc
literals:
- config.json={}
secretGenerator:
- name: headscale
files:
- secrets/private-key
vars:
- name: PUBLIC_PROTO
objRef:
kind: ConfigMap
name: headscale-site
apiVersion: v1
fieldRef:
fieldPath: data.public-proto
- name: PUBLIC_HOSTNAME
objRef:
kind: ConfigMap
name: headscale-site
apiVersion: v1
fieldRef:
fieldPath: data.public-hostname
- name: CONTACT_EMAIL
objRef:
kind: ConfigMap
name: headscale-site
apiVersion: v1
fieldRef:
fieldPath: data.contact-email

View File

@ -8,6 +8,6 @@ spec:
selector: selector:
app: headscale app: headscale
ports: ports:
- name: http - name: http
targetPort: http targetPort: http
port: 8080 port: 8080

View File

@ -0,0 +1,76 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: headscale
spec:
replicas: 2
selector:
matchLabels:
app: headscale
template:
metadata:
labels:
app: headscale
spec:
containers:
- name: headscale
image: "headscale:latest"
imagePullPolicy: IfNotPresent
command: ["/go/bin/headscale", "serve"]
env:
- name: SERVER_URL
value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
- name: LISTEN_ADDR
valueFrom:
configMapKeyRef:
name: headscale-config
key: listen_addr
- name: DERP_MAP_PATH
value: /vol/config/derp.yaml
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
valueFrom:
configMapKeyRef:
name: headscale-config
key: ephemeral_node_inactivity_timeout
- name: DB_TYPE
value: postgres
- name: DB_HOST
value: postgres.headscale.svc.cluster.local
- name: DB_PORT
value: "5432"
- name: DB_USER
value: headscale
- name: DB_PASS
valueFrom:
secretKeyRef:
name: postgresql
key: password
- name: DB_NAME
value: headscale
ports:
- name: http
protocol: TCP
containerPort: 8080
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 30
timeoutSeconds: 5
periodSeconds: 15
volumeMounts:
- name: config
mountPath: /vol/config
- name: secret
mountPath: /vol/secret
- name: etc
mountPath: /etc/headscale
volumes:
- name: config
configMap:
name: headscale-site
- name: etc
configMap:
name: headscale-etc
- name: secret
secret:
secretName: headscale

View File

@ -0,0 +1,13 @@
namespace: headscale
bases:
- ../base
resources:
- deployment.yaml
- postgres-service.yaml
- postgres-statefulset.yaml
generatorOptions:
disableNameSuffixHash: true
secretGenerator:
- name: postgresql
files:
- secrets/password

View File

@ -8,6 +8,6 @@ spec:
selector: selector:
app: postgres app: postgres
ports: ports:
- name: postgres - name: postgres
targetPort: postgres targetPort: postgres
port: 5432 port: 5432

View File

@ -0,0 +1,49 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: "postgres:13"
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgresql
key: password
- name: POSTGRES_USER
value: headscale
ports:
- name: postgres
protocol: TCP
containerPort: 5432
livenessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 30
timeoutSeconds: 5
periodSeconds: 15
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: pgdata
spec:
storageClassName: local-path
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi

View File

@ -6,6 +6,6 @@ metadata:
traefik.ingress.kubernetes.io/router.tls: "true" traefik.ingress.kubernetes.io/router.tls: "true"
spec: spec:
tls: tls:
- hosts: - hosts:
- $(PUBLIC_HOSTNAME) - $(PUBLIC_HOSTNAME)
secretName: production-cert secretName: production-cert

View File

@ -0,0 +1,9 @@
namespace: headscale
bases:
- ../base
resources:
- production-issuer.yaml
patches:
- path: ingress-patch.yaml
target:
kind: Ingress

View File

@ -11,6 +11,6 @@ spec:
# Secret resource used to store the account's private key. # Secret resource used to store the account's private key.
name: letsencrypt-production-acc-key name: letsencrypt-production-acc-key
solvers: solvers:
- http01: - http01:
ingress: ingress:
class: traefik class: traefik

View File

@ -1,5 +1,5 @@
namespace: headscale namespace: headscale
bases: bases:
- ../base - ../base
resources: resources:
- statefulset.yaml - statefulset.yaml

View File

@ -0,0 +1,77 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: headscale
spec:
serviceName: headscale
replicas: 1
selector:
matchLabels:
app: headscale
template:
metadata:
labels:
app: headscale
spec:
containers:
- name: headscale
image: "headscale:latest"
imagePullPolicy: IfNotPresent
command: ["/go/bin/headscale", "serve"]
env:
- name: SERVER_URL
value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
- name: LISTEN_ADDR
valueFrom:
configMapKeyRef:
name: headscale-config
key: listen_addr
- name: DERP_MAP_PATH
value: /vol/config/derp.yaml
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
valueFrom:
configMapKeyRef:
name: headscale-config
key: ephemeral_node_inactivity_timeout
- name: DB_TYPE
value: sqlite3
- name: DB_PATH
value: /vol/data/db.sqlite
ports:
- name: http
protocol: TCP
containerPort: 8080
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 30
timeoutSeconds: 5
periodSeconds: 15
volumeMounts:
- name: config
mountPath: /vol/config
- name: data
mountPath: /vol/data
- name: secret
mountPath: /vol/secret
- name: etc
mountPath: /etc/headscale
volumes:
- name: config
configMap:
name: headscale-site
- name: etc
configMap:
name: headscale-etc
- name: secret
secret:
secretName: headscale
volumeClaimTemplates:
- metadata:
name: data
spec:
storageClassName: local-path
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi

View File

@ -6,6 +6,6 @@ metadata:
traefik.ingress.kubernetes.io/router.tls: "true" traefik.ingress.kubernetes.io/router.tls: "true"
spec: spec:
tls: tls:
- hosts: - hosts:
- $(PUBLIC_HOSTNAME) - $(PUBLIC_HOSTNAME)
secretName: staging-cert secretName: staging-cert

View File

@ -0,0 +1,9 @@
namespace: headscale
bases:
- ../base
resources:
- staging-issuer.yaml
patches:
- path: ingress-patch.yaml
target:
kind: Ingress

View File

@ -11,6 +11,6 @@ spec:
# Secret resource used to store the account's private key. # Secret resource used to store the account's private key.
name: letsencrypt-staging-acc-key name: letsencrypt-staging-acc-key
solvers: solvers:
- http01: - http01:
ingress: ingress:
class: traefik class: traefik

View File

@ -0,0 +1,148 @@
# Running headscale in a container
**Note:** the container documentation is maintained by the _community_ and there is no guarentee
it is up to date, or working.
## Goal
This documentation has the goal of showing a user how-to set up and run `headscale` in a container.
[Docker](https://www.docker.com) is used as the reference container implementation, but there is no reason that it should
not work with alternatives like [Podman](https://podman.io). The Docker image can be found on Docker Hub [here](https://hub.docker.com/r/headscale/headscale).
## Configure and run `headscale`
1. Prepare a directory on the host Docker node in your directory of choice, used to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
```shell
mkdir ./headscale && cd ./headscale
mkdir ./config
```
2. Create an empty SQlite datebase in the headscale directory:
```shell
touch ./config/db.sqlite
```
3. **(Strongly Recommended)** Download a copy of the [example configuration](../config-example.yaml) from the [headscale repository](https://github.com/juanfont/headscale/).
Using wget:
```shell
wget -O ./config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml
```
Using curl:
```shell
curl https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml -o ./config/config.yaml
```
**(Advanced)** If you would like to hand craft a config file **instead** of downloading the example config file, create a blank `headscale` configuration in the headscale directory to edit:
```shell
touch ./config/config.yaml
```
Modify the config file to your preferences before launching Docker container.
4. Start the headscale server while working in the host headscale directory:
```shell
docker run \
--name headscale \
--detach \
--rm \
--volume $(pwd)/config:/etc/headscale/ \
--publish 127.0.0.1:8080:8080 \
headscale/headscale:<VERSION> \
headscale serve
```
This command will mount `config/` under `/etc/headscale`, forward port 8080 out of the container so the
`headscale` instance becomes available and then detach so headscale runs in the background.
5. Verify `headscale` is running:
Follow the container logs:
```shell
docker logs --follow headscale
```
Verify running containers:
```shell
docker ps
```
Verify `headscale` is available:
```shell
curl http://127.0.0.1:8080/metrics
```
6. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
```shell
docker exec headscale -- headscale namespaces create myfirstnamespace
```
### Register a machine (normal login)
On a client machine, execute the `tailscale` login command:
```shell
tailscale up --login-server YOUR_HEADSCALE_URL
```
To register a machine when running `headscale` in a container, take the headscale command and pass it to the container:
```shell
docker exec headscale -- \
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY>
```
### Register machine using a pre authenticated key
Generate a key using the command line:
```shell
docker exec headscale -- \
headscale --namespace myfirstnamespace preauthkeys create --reusable --expiration 24h
```
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
```shell
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
```
## Debugging headscale running in Docker
The `headscale/headscale` Docker container is based on a "distroless" image that does not contain a shell or any other debug tools. If you need to debug your application running in the Docker container, you can use the `-debug` variant, for example `headscale/headscale:x.x.x-debug`.
### Running the debug Docker container
To run the debug Docker container, use the exact same commands as above, but replace `headscale/headscale:x.x.x` with `headscale/headscale:x.x.x-debug` (`x.x.x` is the version of headscale). The two containers are compatible with each other, so you can alternate between them.
### Executing commands in the debug container
The default command in the debug container is to run `headscale`, which is located at `/bin/headscale` inside the container.
Additionally, the debug container includes a minimalist Busybox shell.
To launch a shell in the container, use:
```
docker run -it headscale/headscale:x.x.x-debug sh
```
You can also execute commands directly, such as `ls /bin` in this example:
```
docker run headscale/headscale:x.x.x-debug ls /bin
```
Using `docker exec` allows you to run commands in an existing container.

View File

@ -0,0 +1,172 @@
# Running headscale on Linux
## Goal
This documentation has the goal of showing a user how-to set up and run `headscale` on Linux.
In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd)
describing how to make `headscale` run properly in a server environment.
## Configure and run `headscale`
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
```shell
wget --output-document=/usr/local/bin/headscale \
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>
```
2. Make `headscale` executable:
```shell
chmod +x /usr/local/bin/headscale
```
3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
```shell
# Directory for configuration
mkdir -p /etc/headscale
# Directory for Database, and other variable data (like certificates)
mkdir -p /var/lib/headscale
```
4. Create an empty SQLite database:
```shell
touch /var/lib/headscale/db.sqlite
```
5. Create a `headscale` configuration:
```shell
touch /etc/headscale/config.yaml
```
It is **strongly recommended** to copy and modifiy the [example configuration](../config-example.yaml)
from the [headscale repository](../)
6. Start the headscale server:
```shell
headscale serve
```
This command will start `headscale` in the current terminal session.
---
To continue the tutorial, open a new terminal and let it run in the background.
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/).
To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing.
7. Verify `headscale` is running:
Verify `headscale` is available:
```shell
curl http://127.0.0.1:8080/metrics
```
8. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
```shell
headscale namespaces create myfirstnamespace
```
### Register a machine (normal login)
On a client machine, execute the `tailscale` login command:
```shell
tailscale up --login-server YOUR_HEADSCALE_URL
```
Register the machine:
```shell
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY>
```
### Register machine using a pre authenticated key
Generate a key using the command line:
```shell
headscale --namespace myfirstnamespace preauthkeys create --reusable --expiration 24h
```
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
```shell
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
```
## Running `headscale` in the background with SystemD
This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/).
This should work on most modern Linux distributions.
1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing:
```systemd
[Unit]
Description=headscale controller
After=syslog.target
After=network.target
[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/local/bin/headscale serve
Restart=always
RestartSec=5
# Optional security enhancements
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/headscale /var/run/headscale
AmbientCapabilities=CAP_NET_BIND_SERVICE
RuntimeDirectory=headscale
[Install]
WantedBy=multi-user.target
```
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with a SystemD friendly path:
```yaml
unix_socket: /var/run/headscale/headscale.sock
```
3. Reload SystemD to load the new configuration file:
```shell
systemctl daemon-reload
```
4. Enable and start the new `headscale` service:
```shell
systemctl enable headscale
systemctl start headscale
```
5. Verify the headscale service:
```shell
systemctl status headscale
```
Verify `headscale` is available:
```shell
curl http://127.0.0.1:8080/metrics
```
`headscale` will now run in the background and start at boot.

View File

@ -1,5 +1,9 @@
# Running the service via TLS (optional) # Running the service via TLS (optional)
## Let's Encrypt / ACME
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from. The certificate will automatically be renewed as needed.
```yaml ```yaml
tls_letsencrypt_hostname: "" tls_letsencrypt_hostname: ""
tls_letsencrypt_listen: ":http" tls_letsencrypt_listen: ":http"
@ -7,21 +11,21 @@ tls_letsencrypt_cache_dir: ".cache"
tls_letsencrypt_challenge_type: HTTP-01 tls_letsencrypt_challenge_type: HTTP-01
``` ```
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from. The certificate will automatically be renewed as needed. ### Challenge type HTTP-01
```yaml
tls_cert_path: ""
tls_key_path: ""
```
headscale can also be configured to expose its web service via TLS. To configure the certificate and key file manually, set the `tls_cert_path` and `tls_cert_path` configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
## Challenge type HTTP-01
The default challenge type `HTTP-01` requires that headscale is reachable on port 80 for the Let's Encrypt automated validation, in addition to whatever port is configured in `listen_addr`. By default, headscale listens on port 80 on all local IPs for Let's Encrypt automated validation. The default challenge type `HTTP-01` requires that headscale is reachable on port 80 for the Let's Encrypt automated validation, in addition to whatever port is configured in `listen_addr`. By default, headscale listens on port 80 on all local IPs for Let's Encrypt automated validation.
If you need to change the ip and/or port used by headscale for the Let's Encrypt validation process, set `tls_letsencrypt_listen` to the appropriate value. This can be handy if you are running headscale as a non-root user (or can't run `setcap`). Keep in mind, however, that Let's Encrypt will _only_ connect to port 80 for the validation callback, so if you change `tls_letsencrypt_listen` you will also need to configure something else (e.g. a firewall rule) to forward the traffic from port 80 to the ip:port combination specified in `tls_letsencrypt_listen`. If you need to change the ip and/or port used by headscale for the Let's Encrypt validation process, set `tls_letsencrypt_listen` to the appropriate value. This can be handy if you are running headscale as a non-root user (or can't run `setcap`). Keep in mind, however, that Let's Encrypt will _only_ connect to port 80 for the validation callback, so if you change `tls_letsencrypt_listen` you will also need to configure something else (e.g. a firewall rule) to forward the traffic from port 80 to the ip:port combination specified in `tls_letsencrypt_listen`.
## Challenge type TLS-ALPN-01 ### Challenge type TLS-ALPN-01
Alternatively, `tls_letsencrypt_challenge_type` can be set to `TLS-ALPN-01`. In this configuration, headscale listens on the ip:port combination defined in `listen_addr`. Let's Encrypt will _only_ connect to port 443 for the validation callback, so if `listen_addr` is not set to port 443, something else (e.g. a firewall rule) will be required to forward the traffic from port 443 to the ip:port combination specified in `listen_addr`. Alternatively, `tls_letsencrypt_challenge_type` can be set to `TLS-ALPN-01`. In this configuration, headscale listens on the ip:port combination defined in `listen_addr`. Let's Encrypt will _only_ connect to port 443 for the validation callback, so if `listen_addr` is not set to port 443, something else (e.g. a firewall rule) will be required to forward the traffic from port 443 to the ip:port combination specified in `listen_addr`.
## Bring your own certificate
headscale can also be configured to expose its web service via TLS. To configure the certificate and key file manually, set the `tls_cert_path` and `tls_cert_path` configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
```yaml
tls_cert_path: ""
tls_key_path: ""
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,303 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
// protoc v3.18.1
// source: headscale/v1/headscale.proto
package v1
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
var File_headscale_v1_headscale_proto protoreflect.FileDescriptor
var file_headscale_v1_headscale_proto_rawDesc = []byte{
0x0a, 0x1c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x68,
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65,
0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1a, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72,
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, 0x32, 0xf4,
0x12, 0x0a, 0x10, 0x48, 0x65, 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, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93,
0x02, 0x1a, 0x12, 0x18, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65,
0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x7c, 0x0a, 0x0f,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 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, 0x43,
0x72, 0x65, 0x61, 0x74, 0x65, 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, 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, 0x22, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61,
0x6d, 0x65, 0x73, 0x70, 0x61, 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, 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, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 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, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x82, 0xd3, 0xe4,
0x93, 0x02, 0x30, 0x22, 0x2e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6f, 0x6c, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
0x7d, 0x2f, 0x72, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x7b, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61,
0x6d, 0x65, 0x7d, 0x12, 0x80, 0x01, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 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, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 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, 0x44, 0x65, 0x6c,
0x65, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x2a, 0x18, 0x2f, 0x61,
0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f,
0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x76, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x61,
0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65,
0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x80,
0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 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, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 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, 0x63, 0x61, 0x6c, 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, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x76, 0x31, 0x2f, 0x70, 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, 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, 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, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x22, 0x19, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65,
0x79, 0x2f, 0x65, 0x78, 0x70, 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, 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, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 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, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4,
0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65,
0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x12, 0x89, 0x01, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75,
0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x27,
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, 0x68, 0x69, 0x6e, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 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, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x76, 0x31, 0x2f, 0x64, 0x65, 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, 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, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 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, 0x12, 0x80, 0x01, 0x0a, 0x0f, 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, 0x65,
0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 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, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4,
0x93, 0x02, 0x1a, 0x22, 0x18, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x7e, 0x0a,
0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x22,
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65,
0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x2a,
0x1c, 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, 0x12, 0x85, 0x01,
0x0a, 0x0d, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12,
0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45,
0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 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, 0x65,
0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x6e, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3,
0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x8d, 0x01, 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x30, 0x22, 0x2e, 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, 0x73, 0x68, 0x61, 0x72, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x7d, 0x12, 0x95, 0x01, 0x0a, 0x0e, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72,
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73,
0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x38, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x32, 0x22, 0x30, 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, 0x75, 0x6e, 0x73, 0x68, 0x61, 0x72,
0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x12, 0x8b, 0x01,
0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74,
0x65, 0x12, 0x24, 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,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 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, 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, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x97, 0x01, 0x0a, 0x13,
0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75,
0x74, 0x65, 0x73, 0x12, 0x28, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61,
0x62, 0x6c, 0x65, 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, 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, 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 file_headscale_v1_headscale_proto_goTypes = []interface{}{
(*GetNamespaceRequest)(nil), // 0: headscale.v1.GetNamespaceRequest
(*CreateNamespaceRequest)(nil), // 1: headscale.v1.CreateNamespaceRequest
(*RenameNamespaceRequest)(nil), // 2: headscale.v1.RenameNamespaceRequest
(*DeleteNamespaceRequest)(nil), // 3: headscale.v1.DeleteNamespaceRequest
(*ListNamespacesRequest)(nil), // 4: headscale.v1.ListNamespacesRequest
(*CreatePreAuthKeyRequest)(nil), // 5: headscale.v1.CreatePreAuthKeyRequest
(*ExpirePreAuthKeyRequest)(nil), // 6: headscale.v1.ExpirePreAuthKeyRequest
(*ListPreAuthKeysRequest)(nil), // 7: headscale.v1.ListPreAuthKeysRequest
(*DebugCreateMachineRequest)(nil), // 8: headscale.v1.DebugCreateMachineRequest
(*GetMachineRequest)(nil), // 9: headscale.v1.GetMachineRequest
(*RegisterMachineRequest)(nil), // 10: headscale.v1.RegisterMachineRequest
(*DeleteMachineRequest)(nil), // 11: headscale.v1.DeleteMachineRequest
(*ExpireMachineRequest)(nil), // 12: headscale.v1.ExpireMachineRequest
(*ListMachinesRequest)(nil), // 13: headscale.v1.ListMachinesRequest
(*ShareMachineRequest)(nil), // 14: headscale.v1.ShareMachineRequest
(*UnshareMachineRequest)(nil), // 15: headscale.v1.UnshareMachineRequest
(*GetMachineRouteRequest)(nil), // 16: headscale.v1.GetMachineRouteRequest
(*EnableMachineRoutesRequest)(nil), // 17: headscale.v1.EnableMachineRoutesRequest
(*GetNamespaceResponse)(nil), // 18: headscale.v1.GetNamespaceResponse
(*CreateNamespaceResponse)(nil), // 19: headscale.v1.CreateNamespaceResponse
(*RenameNamespaceResponse)(nil), // 20: headscale.v1.RenameNamespaceResponse
(*DeleteNamespaceResponse)(nil), // 21: headscale.v1.DeleteNamespaceResponse
(*ListNamespacesResponse)(nil), // 22: headscale.v1.ListNamespacesResponse
(*CreatePreAuthKeyResponse)(nil), // 23: headscale.v1.CreatePreAuthKeyResponse
(*ExpirePreAuthKeyResponse)(nil), // 24: headscale.v1.ExpirePreAuthKeyResponse
(*ListPreAuthKeysResponse)(nil), // 25: headscale.v1.ListPreAuthKeysResponse
(*DebugCreateMachineResponse)(nil), // 26: headscale.v1.DebugCreateMachineResponse
(*GetMachineResponse)(nil), // 27: headscale.v1.GetMachineResponse
(*RegisterMachineResponse)(nil), // 28: headscale.v1.RegisterMachineResponse
(*DeleteMachineResponse)(nil), // 29: headscale.v1.DeleteMachineResponse
(*ExpireMachineResponse)(nil), // 30: headscale.v1.ExpireMachineResponse
(*ListMachinesResponse)(nil), // 31: headscale.v1.ListMachinesResponse
(*ShareMachineResponse)(nil), // 32: headscale.v1.ShareMachineResponse
(*UnshareMachineResponse)(nil), // 33: headscale.v1.UnshareMachineResponse
(*GetMachineRouteResponse)(nil), // 34: headscale.v1.GetMachineRouteResponse
(*EnableMachineRoutesResponse)(nil), // 35: headscale.v1.EnableMachineRoutesResponse
}
var file_headscale_v1_headscale_proto_depIdxs = []int32{
0, // 0: headscale.v1.HeadscaleService.GetNamespace:input_type -> headscale.v1.GetNamespaceRequest
1, // 1: headscale.v1.HeadscaleService.CreateNamespace:input_type -> headscale.v1.CreateNamespaceRequest
2, // 2: headscale.v1.HeadscaleService.RenameNamespace:input_type -> headscale.v1.RenameNamespaceRequest
3, // 3: headscale.v1.HeadscaleService.DeleteNamespace:input_type -> headscale.v1.DeleteNamespaceRequest
4, // 4: headscale.v1.HeadscaleService.ListNamespaces:input_type -> headscale.v1.ListNamespacesRequest
5, // 5: headscale.v1.HeadscaleService.CreatePreAuthKey:input_type -> headscale.v1.CreatePreAuthKeyRequest
6, // 6: headscale.v1.HeadscaleService.ExpirePreAuthKey:input_type -> headscale.v1.ExpirePreAuthKeyRequest
7, // 7: headscale.v1.HeadscaleService.ListPreAuthKeys:input_type -> headscale.v1.ListPreAuthKeysRequest
8, // 8: headscale.v1.HeadscaleService.DebugCreateMachine:input_type -> headscale.v1.DebugCreateMachineRequest
9, // 9: headscale.v1.HeadscaleService.GetMachine:input_type -> headscale.v1.GetMachineRequest
10, // 10: headscale.v1.HeadscaleService.RegisterMachine:input_type -> headscale.v1.RegisterMachineRequest
11, // 11: headscale.v1.HeadscaleService.DeleteMachine:input_type -> headscale.v1.DeleteMachineRequest
12, // 12: headscale.v1.HeadscaleService.ExpireMachine:input_type -> headscale.v1.ExpireMachineRequest
13, // 13: headscale.v1.HeadscaleService.ListMachines:input_type -> headscale.v1.ListMachinesRequest
14, // 14: headscale.v1.HeadscaleService.ShareMachine:input_type -> headscale.v1.ShareMachineRequest
15, // 15: headscale.v1.HeadscaleService.UnshareMachine:input_type -> headscale.v1.UnshareMachineRequest
16, // 16: headscale.v1.HeadscaleService.GetMachineRoute:input_type -> headscale.v1.GetMachineRouteRequest
17, // 17: headscale.v1.HeadscaleService.EnableMachineRoutes:input_type -> headscale.v1.EnableMachineRoutesRequest
18, // 18: headscale.v1.HeadscaleService.GetNamespace:output_type -> headscale.v1.GetNamespaceResponse
19, // 19: headscale.v1.HeadscaleService.CreateNamespace:output_type -> headscale.v1.CreateNamespaceResponse
20, // 20: headscale.v1.HeadscaleService.RenameNamespace:output_type -> headscale.v1.RenameNamespaceResponse
21, // 21: headscale.v1.HeadscaleService.DeleteNamespace:output_type -> headscale.v1.DeleteNamespaceResponse
22, // 22: headscale.v1.HeadscaleService.ListNamespaces:output_type -> headscale.v1.ListNamespacesResponse
23, // 23: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
24, // 24: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
25, // 25: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
26, // 26: headscale.v1.HeadscaleService.DebugCreateMachine:output_type -> headscale.v1.DebugCreateMachineResponse
27, // 27: headscale.v1.HeadscaleService.GetMachine:output_type -> headscale.v1.GetMachineResponse
28, // 28: headscale.v1.HeadscaleService.RegisterMachine:output_type -> headscale.v1.RegisterMachineResponse
29, // 29: headscale.v1.HeadscaleService.DeleteMachine:output_type -> headscale.v1.DeleteMachineResponse
30, // 30: headscale.v1.HeadscaleService.ExpireMachine:output_type -> headscale.v1.ExpireMachineResponse
31, // 31: headscale.v1.HeadscaleService.ListMachines:output_type -> headscale.v1.ListMachinesResponse
32, // 32: headscale.v1.HeadscaleService.ShareMachine:output_type -> headscale.v1.ShareMachineResponse
33, // 33: headscale.v1.HeadscaleService.UnshareMachine:output_type -> headscale.v1.UnshareMachineResponse
34, // 34: headscale.v1.HeadscaleService.GetMachineRoute:output_type -> headscale.v1.GetMachineRouteResponse
35, // 35: headscale.v1.HeadscaleService.EnableMachineRoutes:output_type -> headscale.v1.EnableMachineRoutesResponse
18, // [18:36] is the sub-list for method output_type
0, // [0:18] 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 extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_headscale_v1_headscale_proto_init() }
func file_headscale_v1_headscale_proto_init() {
if File_headscale_v1_headscale_proto != nil {
return
}
file_headscale_v1_namespace_proto_init()
file_headscale_v1_preauthkey_proto_init()
file_headscale_v1_machine_proto_init()
file_headscale_v1_routes_proto_init()
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_headscale_v1_headscale_proto_rawDesc,
NumEnums: 0,
NumMessages: 0,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_headscale_v1_headscale_proto_goTypes,
DependencyIndexes: file_headscale_v1_headscale_proto_depIdxs,
}.Build()
File_headscale_v1_headscale_proto = out.File
file_headscale_v1_headscale_proto_rawDesc = nil
file_headscale_v1_headscale_proto_goTypes = nil
file_headscale_v1_headscale_proto_depIdxs = nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,721 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package v1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// HeadscaleServiceClient is the client API for HeadscaleService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HeadscaleServiceClient interface {
// --- Namespace start ---
GetNamespace(ctx context.Context, in *GetNamespaceRequest, opts ...grpc.CallOption) (*GetNamespaceResponse, error)
CreateNamespace(ctx context.Context, in *CreateNamespaceRequest, opts ...grpc.CallOption) (*CreateNamespaceResponse, error)
RenameNamespace(ctx context.Context, in *RenameNamespaceRequest, opts ...grpc.CallOption) (*RenameNamespaceResponse, error)
DeleteNamespace(ctx context.Context, in *DeleteNamespaceRequest, opts ...grpc.CallOption) (*DeleteNamespaceResponse, error)
ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error)
// --- PreAuthKeys start ---
CreatePreAuthKey(ctx context.Context, in *CreatePreAuthKeyRequest, opts ...grpc.CallOption) (*CreatePreAuthKeyResponse, error)
ExpirePreAuthKey(ctx context.Context, in *ExpirePreAuthKeyRequest, opts ...grpc.CallOption) (*ExpirePreAuthKeyResponse, error)
ListPreAuthKeys(ctx context.Context, in *ListPreAuthKeysRequest, opts ...grpc.CallOption) (*ListPreAuthKeysResponse, error)
// --- Machine start ---
DebugCreateMachine(ctx context.Context, in *DebugCreateMachineRequest, opts ...grpc.CallOption) (*DebugCreateMachineResponse, error)
GetMachine(ctx context.Context, in *GetMachineRequest, opts ...grpc.CallOption) (*GetMachineResponse, error)
RegisterMachine(ctx context.Context, in *RegisterMachineRequest, opts ...grpc.CallOption) (*RegisterMachineResponse, error)
DeleteMachine(ctx context.Context, in *DeleteMachineRequest, opts ...grpc.CallOption) (*DeleteMachineResponse, error)
ExpireMachine(ctx context.Context, in *ExpireMachineRequest, opts ...grpc.CallOption) (*ExpireMachineResponse, error)
ListMachines(ctx context.Context, in *ListMachinesRequest, opts ...grpc.CallOption) (*ListMachinesResponse, error)
ShareMachine(ctx context.Context, in *ShareMachineRequest, opts ...grpc.CallOption) (*ShareMachineResponse, error)
UnshareMachine(ctx context.Context, in *UnshareMachineRequest, opts ...grpc.CallOption) (*UnshareMachineResponse, error)
// --- Route start ---
GetMachineRoute(ctx context.Context, in *GetMachineRouteRequest, opts ...grpc.CallOption) (*GetMachineRouteResponse, error)
EnableMachineRoutes(ctx context.Context, in *EnableMachineRoutesRequest, opts ...grpc.CallOption) (*EnableMachineRoutesResponse, error)
}
type headscaleServiceClient struct {
cc grpc.ClientConnInterface
}
func NewHeadscaleServiceClient(cc grpc.ClientConnInterface) HeadscaleServiceClient {
return &headscaleServiceClient{cc}
}
func (c *headscaleServiceClient) GetNamespace(ctx context.Context, in *GetNamespaceRequest, opts ...grpc.CallOption) (*GetNamespaceResponse, error) {
out := new(GetNamespaceResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetNamespace", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) CreateNamespace(ctx context.Context, in *CreateNamespaceRequest, opts ...grpc.CallOption) (*CreateNamespaceResponse, error) {
out := new(CreateNamespaceResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreateNamespace", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) RenameNamespace(ctx context.Context, in *RenameNamespaceRequest, opts ...grpc.CallOption) (*RenameNamespaceResponse, error) {
out := new(RenameNamespaceResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameNamespace", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) DeleteNamespace(ctx context.Context, in *DeleteNamespaceRequest, opts ...grpc.CallOption) (*DeleteNamespaceResponse, error) {
out := new(DeleteNamespaceResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteNamespace", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) {
out := new(ListNamespacesResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListNamespaces", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context, in *CreatePreAuthKeyRequest, opts ...grpc.CallOption) (*CreatePreAuthKeyResponse, error) {
out := new(CreatePreAuthKeyResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreatePreAuthKey", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context, in *ExpirePreAuthKeyRequest, opts ...grpc.CallOption) (*ExpirePreAuthKeyResponse, error) {
out := new(ExpirePreAuthKeyResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpirePreAuthKey", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, in *ListPreAuthKeysRequest, opts ...grpc.CallOption) (*ListPreAuthKeysResponse, error) {
out := new(ListPreAuthKeysResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListPreAuthKeys", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) DebugCreateMachine(ctx context.Context, in *DebugCreateMachineRequest, opts ...grpc.CallOption) (*DebugCreateMachineResponse, error) {
out := new(DebugCreateMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DebugCreateMachine", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) GetMachine(ctx context.Context, in *GetMachineRequest, opts ...grpc.CallOption) (*GetMachineResponse, error) {
out := new(GetMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetMachine", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) RegisterMachine(ctx context.Context, in *RegisterMachineRequest, opts ...grpc.CallOption) (*RegisterMachineResponse, error) {
out := new(RegisterMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RegisterMachine", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) DeleteMachine(ctx context.Context, in *DeleteMachineRequest, opts ...grpc.CallOption) (*DeleteMachineResponse, error) {
out := new(DeleteMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteMachine", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) ExpireMachine(ctx context.Context, in *ExpireMachineRequest, opts ...grpc.CallOption) (*ExpireMachineResponse, error) {
out := new(ExpireMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpireMachine", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) ListMachines(ctx context.Context, in *ListMachinesRequest, opts ...grpc.CallOption) (*ListMachinesResponse, error) {
out := new(ListMachinesResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListMachines", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) ShareMachine(ctx context.Context, in *ShareMachineRequest, opts ...grpc.CallOption) (*ShareMachineResponse, error) {
out := new(ShareMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ShareMachine", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) UnshareMachine(ctx context.Context, in *UnshareMachineRequest, opts ...grpc.CallOption) (*UnshareMachineResponse, error) {
out := new(UnshareMachineResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/UnshareMachine", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) GetMachineRoute(ctx context.Context, in *GetMachineRouteRequest, opts ...grpc.CallOption) (*GetMachineRouteResponse, error) {
out := new(GetMachineRouteResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetMachineRoute", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *headscaleServiceClient) EnableMachineRoutes(ctx context.Context, in *EnableMachineRoutesRequest, opts ...grpc.CallOption) (*EnableMachineRoutesResponse, error) {
out := new(EnableMachineRoutesResponse)
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/EnableMachineRoutes", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// HeadscaleServiceServer is the server API for HeadscaleService service.
// All implementations must embed UnimplementedHeadscaleServiceServer
// for forward compatibility
type HeadscaleServiceServer interface {
// --- Namespace start ---
GetNamespace(context.Context, *GetNamespaceRequest) (*GetNamespaceResponse, error)
CreateNamespace(context.Context, *CreateNamespaceRequest) (*CreateNamespaceResponse, error)
RenameNamespace(context.Context, *RenameNamespaceRequest) (*RenameNamespaceResponse, error)
DeleteNamespace(context.Context, *DeleteNamespaceRequest) (*DeleteNamespaceResponse, error)
ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error)
// --- PreAuthKeys start ---
CreatePreAuthKey(context.Context, *CreatePreAuthKeyRequest) (*CreatePreAuthKeyResponse, error)
ExpirePreAuthKey(context.Context, *ExpirePreAuthKeyRequest) (*ExpirePreAuthKeyResponse, error)
ListPreAuthKeys(context.Context, *ListPreAuthKeysRequest) (*ListPreAuthKeysResponse, error)
// --- Machine start ---
DebugCreateMachine(context.Context, *DebugCreateMachineRequest) (*DebugCreateMachineResponse, error)
GetMachine(context.Context, *GetMachineRequest) (*GetMachineResponse, error)
RegisterMachine(context.Context, *RegisterMachineRequest) (*RegisterMachineResponse, error)
DeleteMachine(context.Context, *DeleteMachineRequest) (*DeleteMachineResponse, error)
ExpireMachine(context.Context, *ExpireMachineRequest) (*ExpireMachineResponse, error)
ListMachines(context.Context, *ListMachinesRequest) (*ListMachinesResponse, error)
ShareMachine(context.Context, *ShareMachineRequest) (*ShareMachineResponse, error)
UnshareMachine(context.Context, *UnshareMachineRequest) (*UnshareMachineResponse, error)
// --- Route start ---
GetMachineRoute(context.Context, *GetMachineRouteRequest) (*GetMachineRouteResponse, error)
EnableMachineRoutes(context.Context, *EnableMachineRoutesRequest) (*EnableMachineRoutesResponse, error)
mustEmbedUnimplementedHeadscaleServiceServer()
}
// UnimplementedHeadscaleServiceServer must be embedded to have forward compatible implementations.
type UnimplementedHeadscaleServiceServer struct {
}
func (UnimplementedHeadscaleServiceServer) GetNamespace(context.Context, *GetNamespaceRequest) (*GetNamespaceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetNamespace not implemented")
}
func (UnimplementedHeadscaleServiceServer) CreateNamespace(context.Context, *CreateNamespaceRequest) (*CreateNamespaceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateNamespace not implemented")
}
func (UnimplementedHeadscaleServiceServer) RenameNamespace(context.Context, *RenameNamespaceRequest) (*RenameNamespaceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RenameNamespace not implemented")
}
func (UnimplementedHeadscaleServiceServer) DeleteNamespace(context.Context, *DeleteNamespaceRequest) (*DeleteNamespaceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteNamespace not implemented")
}
func (UnimplementedHeadscaleServiceServer) ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListNamespaces not implemented")
}
func (UnimplementedHeadscaleServiceServer) CreatePreAuthKey(context.Context, *CreatePreAuthKeyRequest) (*CreatePreAuthKeyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreatePreAuthKey not implemented")
}
func (UnimplementedHeadscaleServiceServer) ExpirePreAuthKey(context.Context, *ExpirePreAuthKeyRequest) (*ExpirePreAuthKeyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExpirePreAuthKey not implemented")
}
func (UnimplementedHeadscaleServiceServer) ListPreAuthKeys(context.Context, *ListPreAuthKeysRequest) (*ListPreAuthKeysResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListPreAuthKeys not implemented")
}
func (UnimplementedHeadscaleServiceServer) DebugCreateMachine(context.Context, *DebugCreateMachineRequest) (*DebugCreateMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DebugCreateMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) GetMachine(context.Context, *GetMachineRequest) (*GetMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) RegisterMachine(context.Context, *RegisterMachineRequest) (*RegisterMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RegisterMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) DeleteMachine(context.Context, *DeleteMachineRequest) (*DeleteMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) ExpireMachine(context.Context, *ExpireMachineRequest) (*ExpireMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExpireMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) ListMachines(context.Context, *ListMachinesRequest) (*ListMachinesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
}
func (UnimplementedHeadscaleServiceServer) ShareMachine(context.Context, *ShareMachineRequest) (*ShareMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ShareMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) UnshareMachine(context.Context, *UnshareMachineRequest) (*UnshareMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UnshareMachine not implemented")
}
func (UnimplementedHeadscaleServiceServer) GetMachineRoute(context.Context, *GetMachineRouteRequest) (*GetMachineRouteResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMachineRoute not implemented")
}
func (UnimplementedHeadscaleServiceServer) EnableMachineRoutes(context.Context, *EnableMachineRoutesRequest) (*EnableMachineRoutesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method EnableMachineRoutes not implemented")
}
func (UnimplementedHeadscaleServiceServer) mustEmbedUnimplementedHeadscaleServiceServer() {}
// UnsafeHeadscaleServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to HeadscaleServiceServer will
// result in compilation errors.
type UnsafeHeadscaleServiceServer interface {
mustEmbedUnimplementedHeadscaleServiceServer()
}
func RegisterHeadscaleServiceServer(s grpc.ServiceRegistrar, srv HeadscaleServiceServer) {
s.RegisterService(&HeadscaleService_ServiceDesc, srv)
}
func _HeadscaleService_GetNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetNamespaceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).GetNamespace(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/GetNamespace",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).GetNamespace(ctx, req.(*GetNamespaceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_CreateNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateNamespaceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).CreateNamespace(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/CreateNamespace",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).CreateNamespace(ctx, req.(*CreateNamespaceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_RenameNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RenameNamespaceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).RenameNamespace(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/RenameNamespace",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).RenameNamespace(ctx, req.(*RenameNamespaceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_DeleteNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteNamespaceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).DeleteNamespace(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/DeleteNamespace",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).DeleteNamespace(ctx, req.(*DeleteNamespaceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_ListNamespaces_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListNamespacesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).ListNamespaces(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/ListNamespaces",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).ListNamespaces(ctx, req.(*ListNamespacesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_CreatePreAuthKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreatePreAuthKeyRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).CreatePreAuthKey(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/CreatePreAuthKey",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).CreatePreAuthKey(ctx, req.(*CreatePreAuthKeyRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_ExpirePreAuthKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ExpirePreAuthKeyRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).ExpirePreAuthKey(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/ExpirePreAuthKey",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).ExpirePreAuthKey(ctx, req.(*ExpirePreAuthKeyRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_ListPreAuthKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListPreAuthKeysRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).ListPreAuthKeys(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/ListPreAuthKeys",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).ListPreAuthKeys(ctx, req.(*ListPreAuthKeysRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_DebugCreateMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DebugCreateMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).DebugCreateMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/DebugCreateMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).DebugCreateMachine(ctx, req.(*DebugCreateMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_GetMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).GetMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/GetMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).GetMachine(ctx, req.(*GetMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_RegisterMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).RegisterMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/RegisterMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).RegisterMachine(ctx, req.(*RegisterMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_DeleteMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).DeleteMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/DeleteMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).DeleteMachine(ctx, req.(*DeleteMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_ExpireMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ExpireMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).ExpireMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/ExpireMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).ExpireMachine(ctx, req.(*ExpireMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_ListMachines_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListMachinesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).ListMachines(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/ListMachines",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).ListMachines(ctx, req.(*ListMachinesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_ShareMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ShareMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).ShareMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/ShareMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).ShareMachine(ctx, req.(*ShareMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_UnshareMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UnshareMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).UnshareMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/UnshareMachine",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).UnshareMachine(ctx, req.(*UnshareMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_GetMachineRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMachineRouteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).GetMachineRoute(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/GetMachineRoute",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).GetMachineRoute(ctx, req.(*GetMachineRouteRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_EnableMachineRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EnableMachineRoutesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).EnableMachineRoutes(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/headscale.v1.HeadscaleService/EnableMachineRoutes",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).EnableMachineRoutes(ctx, req.(*EnableMachineRoutesRequest))
}
return interceptor(ctx, in, info, handler)
}
// HeadscaleService_ServiceDesc is the grpc.ServiceDesc for HeadscaleService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var HeadscaleService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "headscale.v1.HeadscaleService",
HandlerType: (*HeadscaleServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetNamespace",
Handler: _HeadscaleService_GetNamespace_Handler,
},
{
MethodName: "CreateNamespace",
Handler: _HeadscaleService_CreateNamespace_Handler,
},
{
MethodName: "RenameNamespace",
Handler: _HeadscaleService_RenameNamespace_Handler,
},
{
MethodName: "DeleteNamespace",
Handler: _HeadscaleService_DeleteNamespace_Handler,
},
{
MethodName: "ListNamespaces",
Handler: _HeadscaleService_ListNamespaces_Handler,
},
{
MethodName: "CreatePreAuthKey",
Handler: _HeadscaleService_CreatePreAuthKey_Handler,
},
{
MethodName: "ExpirePreAuthKey",
Handler: _HeadscaleService_ExpirePreAuthKey_Handler,
},
{
MethodName: "ListPreAuthKeys",
Handler: _HeadscaleService_ListPreAuthKeys_Handler,
},
{
MethodName: "DebugCreateMachine",
Handler: _HeadscaleService_DebugCreateMachine_Handler,
},
{
MethodName: "GetMachine",
Handler: _HeadscaleService_GetMachine_Handler,
},
{
MethodName: "RegisterMachine",
Handler: _HeadscaleService_RegisterMachine_Handler,
},
{
MethodName: "DeleteMachine",
Handler: _HeadscaleService_DeleteMachine_Handler,
},
{
MethodName: "ExpireMachine",
Handler: _HeadscaleService_ExpireMachine_Handler,
},
{
MethodName: "ListMachines",
Handler: _HeadscaleService_ListMachines_Handler,
},
{
MethodName: "ShareMachine",
Handler: _HeadscaleService_ShareMachine_Handler,
},
{
MethodName: "UnshareMachine",
Handler: _HeadscaleService_UnshareMachine_Handler,
},
{
MethodName: "GetMachineRoute",
Handler: _HeadscaleService_GetMachineRoute_Handler,
},
{
MethodName: "EnableMachineRoutes",
Handler: _HeadscaleService_EnableMachineRoutes_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "headscale/v1/headscale.proto",
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,801 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
// protoc v3.18.1
// source: headscale/v1/namespace.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Namespace struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
}
func (x *Namespace) Reset() {
*x = Namespace{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Namespace) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Namespace) ProtoMessage() {}
func (x *Namespace) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_proto_msgTypes[0]
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 Namespace.ProtoReflect.Descriptor instead.
func (*Namespace) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{0}
}
func (x *Namespace) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *Namespace) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Namespace) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
type GetNamespaceRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *GetNamespaceRequest) Reset() {
*x = GetNamespaceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetNamespaceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetNamespaceRequest) ProtoMessage() {}
func (x *GetNamespaceRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_proto_msgTypes[1]
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 GetNamespaceRequest.ProtoReflect.Descriptor instead.
func (*GetNamespaceRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{1}
}
func (x *GetNamespaceRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type GetNamespaceResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Namespace *Namespace `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
}
func (x *GetNamespaceResponse) Reset() {
*x = GetNamespaceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetNamespaceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetNamespaceResponse) ProtoMessage() {}
func (x *GetNamespaceResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_proto_msgTypes[2]
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 GetNamespaceResponse.ProtoReflect.Descriptor instead.
func (*GetNamespaceResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{2}
}
func (x *GetNamespaceResponse) GetNamespace() *Namespace {
if x != nil {
return x.Namespace
}
return nil
}
type CreateNamespaceRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *CreateNamespaceRequest) Reset() {
*x = CreateNamespaceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateNamespaceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateNamespaceRequest) ProtoMessage() {}
func (x *CreateNamespaceRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_proto_msgTypes[3]
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 CreateNamespaceRequest.ProtoReflect.Descriptor instead.
func (*CreateNamespaceRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{3}
}
func (x *CreateNamespaceRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type CreateNamespaceResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Namespace *Namespace `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
}
func (x *CreateNamespaceResponse) Reset() {
*x = CreateNamespaceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateNamespaceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateNamespaceResponse) ProtoMessage() {}
func (x *CreateNamespaceResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_proto_msgTypes[4]
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 CreateNamespaceResponse.ProtoReflect.Descriptor instead.
func (*CreateNamespaceResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{4}
}
func (x *CreateNamespaceResponse) GetNamespace() *Namespace {
if x != nil {
return x.Namespace
}
return nil
}
type RenameNamespaceRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
OldName string `protobuf:"bytes,1,opt,name=old_name,json=oldName,proto3" json:"old_name,omitempty"`
NewName string `protobuf:"bytes,2,opt,name=new_name,json=newName,proto3" json:"new_name,omitempty"`
}
func (x *RenameNamespaceRequest) Reset() {
*x = RenameNamespaceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RenameNamespaceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RenameNamespaceRequest) ProtoMessage() {}
func (x *RenameNamespaceRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_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 RenameNamespaceRequest.ProtoReflect.Descriptor instead.
func (*RenameNamespaceRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{5}
}
func (x *RenameNamespaceRequest) GetOldName() string {
if x != nil {
return x.OldName
}
return ""
}
func (x *RenameNamespaceRequest) GetNewName() string {
if x != nil {
return x.NewName
}
return ""
}
type RenameNamespaceResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Namespace *Namespace `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
}
func (x *RenameNamespaceResponse) Reset() {
*x = RenameNamespaceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RenameNamespaceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RenameNamespaceResponse) ProtoMessage() {}
func (x *RenameNamespaceResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_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 RenameNamespaceResponse.ProtoReflect.Descriptor instead.
func (*RenameNamespaceResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{6}
}
func (x *RenameNamespaceResponse) GetNamespace() *Namespace {
if x != nil {
return x.Namespace
}
return nil
}
type DeleteNamespaceRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *DeleteNamespaceRequest) Reset() {
*x = DeleteNamespaceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DeleteNamespaceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteNamespaceRequest) ProtoMessage() {}
func (x *DeleteNamespaceRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_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 DeleteNamespaceRequest.ProtoReflect.Descriptor instead.
func (*DeleteNamespaceRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{7}
}
func (x *DeleteNamespaceRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type DeleteNamespaceResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *DeleteNamespaceResponse) Reset() {
*x = DeleteNamespaceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DeleteNamespaceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteNamespaceResponse) ProtoMessage() {}
func (x *DeleteNamespaceResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_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 DeleteNamespaceResponse.ProtoReflect.Descriptor instead.
func (*DeleteNamespaceResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{8}
}
type ListNamespacesRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *ListNamespacesRequest) Reset() {
*x = ListNamespacesRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListNamespacesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListNamespacesRequest) ProtoMessage() {}
func (x *ListNamespacesRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_proto_msgTypes[9]
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 ListNamespacesRequest.ProtoReflect.Descriptor instead.
func (*ListNamespacesRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{9}
}
type ListNamespacesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Namespaces []*Namespace `protobuf:"bytes,1,rep,name=namespaces,proto3" json:"namespaces,omitempty"`
}
func (x *ListNamespacesResponse) Reset() {
*x = ListNamespacesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_namespace_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListNamespacesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListNamespacesResponse) ProtoMessage() {}
func (x *ListNamespacesResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_namespace_proto_msgTypes[10]
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 ListNamespacesResponse.ProtoReflect.Descriptor instead.
func (*ListNamespacesResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_namespace_proto_rawDescGZIP(), []int{10}
}
func (x *ListNamespacesResponse) GetNamespaces() []*Namespace {
if x != nil {
return x.Namespaces
}
return nil
}
var File_headscale_v1_namespace_proto protoreflect.FileDescriptor
var file_headscale_v1_namespace_proto_rawDesc = []byte{
0x0a, 0x1c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 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, 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, 0x6a, 0x0a,
0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x39,
0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 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, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09,
0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x29, 0x0a, 0x13, 0x47, 0x65, 0x74,
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x22, 0x4d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x09,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
0x61, 0x63, 0x65, 0x22, 0x2c, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x22, 0x50, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x09,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
0x61, 0x63, 0x65, 0x22, 0x4e, 0x0a, 0x16, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a,
0x08, 0x6f, 0x6c, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x07, 0x6f, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x4e,
0x61, 0x6d, 0x65, 0x22, 0x50, 0x0a, 0x17, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35,
0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65,
0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x2c, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x22, 0x19, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17,
0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x51, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x37, 0x0a, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x0a,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 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 (
file_headscale_v1_namespace_proto_rawDescOnce sync.Once
file_headscale_v1_namespace_proto_rawDescData = file_headscale_v1_namespace_proto_rawDesc
)
func file_headscale_v1_namespace_proto_rawDescGZIP() []byte {
file_headscale_v1_namespace_proto_rawDescOnce.Do(func() {
file_headscale_v1_namespace_proto_rawDescData = protoimpl.X.CompressGZIP(file_headscale_v1_namespace_proto_rawDescData)
})
return file_headscale_v1_namespace_proto_rawDescData
}
var file_headscale_v1_namespace_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_headscale_v1_namespace_proto_goTypes = []interface{}{
(*Namespace)(nil), // 0: headscale.v1.Namespace
(*GetNamespaceRequest)(nil), // 1: headscale.v1.GetNamespaceRequest
(*GetNamespaceResponse)(nil), // 2: headscale.v1.GetNamespaceResponse
(*CreateNamespaceRequest)(nil), // 3: headscale.v1.CreateNamespaceRequest
(*CreateNamespaceResponse)(nil), // 4: headscale.v1.CreateNamespaceResponse
(*RenameNamespaceRequest)(nil), // 5: headscale.v1.RenameNamespaceRequest
(*RenameNamespaceResponse)(nil), // 6: headscale.v1.RenameNamespaceResponse
(*DeleteNamespaceRequest)(nil), // 7: headscale.v1.DeleteNamespaceRequest
(*DeleteNamespaceResponse)(nil), // 8: headscale.v1.DeleteNamespaceResponse
(*ListNamespacesRequest)(nil), // 9: headscale.v1.ListNamespacesRequest
(*ListNamespacesResponse)(nil), // 10: headscale.v1.ListNamespacesResponse
(*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp
}
var file_headscale_v1_namespace_proto_depIdxs = []int32{
11, // 0: headscale.v1.Namespace.created_at:type_name -> google.protobuf.Timestamp
0, // 1: headscale.v1.GetNamespaceResponse.namespace:type_name -> headscale.v1.Namespace
0, // 2: headscale.v1.CreateNamespaceResponse.namespace:type_name -> headscale.v1.Namespace
0, // 3: headscale.v1.RenameNamespaceResponse.namespace:type_name -> headscale.v1.Namespace
0, // 4: headscale.v1.ListNamespacesResponse.namespaces:type_name -> headscale.v1.Namespace
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_headscale_v1_namespace_proto_init() }
func file_headscale_v1_namespace_proto_init() {
if File_headscale_v1_namespace_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_headscale_v1_namespace_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Namespace); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetNamespaceRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetNamespaceResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateNamespaceRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateNamespaceResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RenameNamespaceRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RenameNamespaceResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DeleteNamespaceRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DeleteNamespaceResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListNamespacesRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_namespace_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListNamespacesResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_headscale_v1_namespace_proto_rawDesc,
NumEnums: 0,
NumMessages: 11,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_headscale_v1_namespace_proto_goTypes,
DependencyIndexes: file_headscale_v1_namespace_proto_depIdxs,
MessageInfos: file_headscale_v1_namespace_proto_msgTypes,
}.Build()
File_headscale_v1_namespace_proto = out.File
file_headscale_v1_namespace_proto_rawDesc = nil
file_headscale_v1_namespace_proto_goTypes = nil
file_headscale_v1_namespace_proto_depIdxs = nil
}

View File

@ -0,0 +1,640 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
// protoc v3.18.1
// source: headscale/v1/preauthkey.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type PreAuthKey struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
Reusable bool `protobuf:"varint,4,opt,name=reusable,proto3" json:"reusable,omitempty"`
Ephemeral bool `protobuf:"varint,5,opt,name=ephemeral,proto3" json:"ephemeral,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"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
}
func (x *PreAuthKey) Reset() {
*x = PreAuthKey{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PreAuthKey) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PreAuthKey) ProtoMessage() {}
func (x *PreAuthKey) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[0]
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 PreAuthKey.ProtoReflect.Descriptor instead.
func (*PreAuthKey) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{0}
}
func (x *PreAuthKey) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
func (x *PreAuthKey) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *PreAuthKey) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *PreAuthKey) GetReusable() bool {
if x != nil {
return x.Reusable
}
return false
}
func (x *PreAuthKey) GetEphemeral() bool {
if x != nil {
return x.Ephemeral
}
return false
}
func (x *PreAuthKey) GetUsed() bool {
if x != nil {
return x.Used
}
return false
}
func (x *PreAuthKey) GetExpiration() *timestamppb.Timestamp {
if x != nil {
return x.Expiration
}
return nil
}
func (x *PreAuthKey) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
type CreatePreAuthKeyRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,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"`
Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
}
func (x *CreatePreAuthKeyRequest) Reset() {
*x = CreatePreAuthKeyRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreatePreAuthKeyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreatePreAuthKeyRequest) ProtoMessage() {}
func (x *CreatePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[1]
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 CreatePreAuthKeyRequest.ProtoReflect.Descriptor instead.
func (*CreatePreAuthKeyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{1}
}
func (x *CreatePreAuthKeyRequest) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
func (x *CreatePreAuthKeyRequest) GetReusable() bool {
if x != nil {
return x.Reusable
}
return false
}
func (x *CreatePreAuthKeyRequest) GetEphemeral() bool {
if x != nil {
return x.Ephemeral
}
return false
}
func (x *CreatePreAuthKeyRequest) GetExpiration() *timestamppb.Timestamp {
if x != nil {
return x.Expiration
}
return nil
}
type CreatePreAuthKeyResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
PreAuthKey *PreAuthKey `protobuf:"bytes,1,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"`
}
func (x *CreatePreAuthKeyResponse) Reset() {
*x = CreatePreAuthKeyResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreatePreAuthKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreatePreAuthKeyResponse) ProtoMessage() {}
func (x *CreatePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[2]
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 CreatePreAuthKeyResponse.ProtoReflect.Descriptor instead.
func (*CreatePreAuthKeyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{2}
}
func (x *CreatePreAuthKeyResponse) GetPreAuthKey() *PreAuthKey {
if x != nil {
return x.PreAuthKey
}
return nil
}
type ExpirePreAuthKeyRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
}
func (x *ExpirePreAuthKeyRequest) Reset() {
*x = ExpirePreAuthKeyRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ExpirePreAuthKeyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExpirePreAuthKeyRequest) ProtoMessage() {}
func (x *ExpirePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[3]
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 ExpirePreAuthKeyRequest.ProtoReflect.Descriptor instead.
func (*ExpirePreAuthKeyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{3}
}
func (x *ExpirePreAuthKeyRequest) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
func (x *ExpirePreAuthKeyRequest) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
type ExpirePreAuthKeyResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *ExpirePreAuthKeyResponse) Reset() {
*x = ExpirePreAuthKeyResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ExpirePreAuthKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExpirePreAuthKeyResponse) ProtoMessage() {}
func (x *ExpirePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[4]
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 ExpirePreAuthKeyResponse.ProtoReflect.Descriptor instead.
func (*ExpirePreAuthKeyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{4}
}
type ListPreAuthKeysRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
}
func (x *ListPreAuthKeysRequest) Reset() {
*x = ListPreAuthKeysRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListPreAuthKeysRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListPreAuthKeysRequest) ProtoMessage() {}
func (x *ListPreAuthKeysRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_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 ListPreAuthKeysRequest.ProtoReflect.Descriptor instead.
func (*ListPreAuthKeysRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{5}
}
func (x *ListPreAuthKeysRequest) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
type ListPreAuthKeysResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
PreAuthKeys []*PreAuthKey `protobuf:"bytes,1,rep,name=pre_auth_keys,json=preAuthKeys,proto3" json:"pre_auth_keys,omitempty"`
}
func (x *ListPreAuthKeysResponse) Reset() {
*x = ListPreAuthKeysResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListPreAuthKeysResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListPreAuthKeysResponse) ProtoMessage() {}
func (x *ListPreAuthKeysResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_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 ListPreAuthKeysResponse.ProtoReflect.Descriptor instead.
func (*ListPreAuthKeysResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{6}
}
func (x *ListPreAuthKeysResponse) GetPreAuthKeys() []*PreAuthKey {
if x != nil {
return x.PreAuthKeys
}
return nil
}
var File_headscale_v1_preauthkey_proto protoreflect.FileDescriptor
var file_headscale_v1_preauthkey_proto_rawDesc = []byte{
0x0a, 0x1d, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70,
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,
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, 0x91,
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,
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b,
0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a,
0x08, 0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52,
0x08, 0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68,
0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70,
0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18,
0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x3a, 0x0a, 0x0a, 0x65,
0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 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, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70,
0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74,
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,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
0x41, 0x74, 0x22, 0xad, 0x01, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65,
0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 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, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08,
0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08,
0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65,
0x6d, 0x65, 0x72, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68,
0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 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, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x22, 0x56, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41,
0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a,
0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01,
0x20, 0x01, 0x28, 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, 0x0a,
0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x22, 0x49, 0x0a, 0x17, 0x45, 0x78,
0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 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, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
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,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x57, 0x0a, 0x17, 0x4c, 0x69, 0x73,
0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68,
0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 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 (
file_headscale_v1_preauthkey_proto_rawDescOnce sync.Once
file_headscale_v1_preauthkey_proto_rawDescData = file_headscale_v1_preauthkey_proto_rawDesc
)
func file_headscale_v1_preauthkey_proto_rawDescGZIP() []byte {
file_headscale_v1_preauthkey_proto_rawDescOnce.Do(func() {
file_headscale_v1_preauthkey_proto_rawDescData = protoimpl.X.CompressGZIP(file_headscale_v1_preauthkey_proto_rawDescData)
})
return file_headscale_v1_preauthkey_proto_rawDescData
}
var file_headscale_v1_preauthkey_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_headscale_v1_preauthkey_proto_goTypes = []interface{}{
(*PreAuthKey)(nil), // 0: headscale.v1.PreAuthKey
(*CreatePreAuthKeyRequest)(nil), // 1: headscale.v1.CreatePreAuthKeyRequest
(*CreatePreAuthKeyResponse)(nil), // 2: headscale.v1.CreatePreAuthKeyResponse
(*ExpirePreAuthKeyRequest)(nil), // 3: headscale.v1.ExpirePreAuthKeyRequest
(*ExpirePreAuthKeyResponse)(nil), // 4: headscale.v1.ExpirePreAuthKeyResponse
(*ListPreAuthKeysRequest)(nil), // 5: headscale.v1.ListPreAuthKeysRequest
(*ListPreAuthKeysResponse)(nil), // 6: headscale.v1.ListPreAuthKeysResponse
(*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp
}
var file_headscale_v1_preauthkey_proto_depIdxs = []int32{
7, // 0: headscale.v1.PreAuthKey.expiration:type_name -> google.protobuf.Timestamp
7, // 1: headscale.v1.PreAuthKey.created_at:type_name -> google.protobuf.Timestamp
7, // 2: headscale.v1.CreatePreAuthKeyRequest.expiration:type_name -> google.protobuf.Timestamp
0, // 3: headscale.v1.CreatePreAuthKeyResponse.pre_auth_key:type_name -> headscale.v1.PreAuthKey
0, // 4: headscale.v1.ListPreAuthKeysResponse.pre_auth_keys:type_name -> headscale.v1.PreAuthKey
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_headscale_v1_preauthkey_proto_init() }
func file_headscale_v1_preauthkey_proto_init() {
if File_headscale_v1_preauthkey_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_headscale_v1_preauthkey_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PreAuthKey); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_preauthkey_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreatePreAuthKeyRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_preauthkey_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreatePreAuthKeyResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_preauthkey_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ExpirePreAuthKeyRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_preauthkey_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ExpirePreAuthKeyResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_preauthkey_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListPreAuthKeysRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_headscale_v1_preauthkey_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListPreAuthKeysResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_headscale_v1_preauthkey_proto_rawDesc,
NumEnums: 0,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_headscale_v1_preauthkey_proto_goTypes,
DependencyIndexes: file_headscale_v1_preauthkey_proto_depIdxs,
MessageInfos: file_headscale_v1_preauthkey_proto_msgTypes,
}.Build()
File_headscale_v1_preauthkey_proto = out.File
file_headscale_v1_preauthkey_proto_rawDesc = nil
file_headscale_v1_preauthkey_proto_goTypes = nil
file_headscale_v1_preauthkey_proto_depIdxs = nil
}

View File

@ -0,0 +1,424 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
// protoc v3.18.1
// source: headscale/v1/routes.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Routes struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
AdvertisedRoutes []string `protobuf:"bytes,1,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
EnabledRoutes []string `protobuf:"bytes,2,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
}
func (x *Routes) Reset() {
*x = Routes{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Routes) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Routes) ProtoMessage() {}
func (x *Routes) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[0]
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 Routes.ProtoReflect.Descriptor instead.
func (*Routes) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{0}
}
func (x *Routes) GetAdvertisedRoutes() []string {
if x != nil {
return x.AdvertisedRoutes
}
return nil
}
func (x *Routes) GetEnabledRoutes() []string {
if x != nil {
return x.EnabledRoutes
}
return nil
}
type GetMachineRouteRequest 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 *GetMachineRouteRequest) Reset() {
*x = GetMachineRouteRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetMachineRouteRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetMachineRouteRequest) ProtoMessage() {}
func (x *GetMachineRouteRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[1]
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 GetMachineRouteRequest.ProtoReflect.Descriptor instead.
func (*GetMachineRouteRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{1}
}
func (x *GetMachineRouteRequest) GetMachineId() uint64 {
if x != nil {
return x.MachineId
}
return 0
}
type GetMachineRouteResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Routes *Routes `protobuf:"bytes,1,opt,name=routes,proto3" json:"routes,omitempty"`
}
func (x *GetMachineRouteResponse) Reset() {
*x = GetMachineRouteResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetMachineRouteResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetMachineRouteResponse) ProtoMessage() {}
func (x *GetMachineRouteResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[2]
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 GetMachineRouteResponse.ProtoReflect.Descriptor instead.
func (*GetMachineRouteResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{2}
}
func (x *GetMachineRouteResponse) GetRoutes() *Routes {
if x != nil {
return x.Routes
}
return nil
}
type EnableMachineRoutesRequest 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"`
Routes []string `protobuf:"bytes,2,rep,name=routes,proto3" json:"routes,omitempty"`
}
func (x *EnableMachineRoutesRequest) Reset() {
*x = EnableMachineRoutesRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *EnableMachineRoutesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EnableMachineRoutesRequest) ProtoMessage() {}
func (x *EnableMachineRoutesRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[3]
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 EnableMachineRoutesRequest.ProtoReflect.Descriptor instead.
func (*EnableMachineRoutesRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{3}
}
func (x *EnableMachineRoutesRequest) GetMachineId() uint64 {
if x != nil {
return x.MachineId
}
return 0
}
func (x *EnableMachineRoutesRequest) GetRoutes() []string {
if x != nil {
return x.Routes
}
return nil
}
type EnableMachineRoutesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Routes *Routes `protobuf:"bytes,1,opt,name=routes,proto3" json:"routes,omitempty"`
}
func (x *EnableMachineRoutesResponse) Reset() {
*x = EnableMachineRoutesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_headscale_v1_routes_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *EnableMachineRoutesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EnableMachineRoutesResponse) ProtoMessage() {}
func (x *EnableMachineRoutesResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_routes_proto_msgTypes[4]
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 EnableMachineRoutesResponse.ProtoReflect.Descriptor instead.
func (*EnableMachineRoutesResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_routes_proto_rawDescGZIP(), []int{4}
}
func (x *EnableMachineRoutesResponse) GetRoutes() *Routes {
if x != nil {
return x.Routes
}
return nil
}
var File_headscale_v1_routes_proto protoreflect.FileDescriptor
var file_headscale_v1_routes_proto_rawDesc = []byte{
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,
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x5c, 0x0a, 0x06, 0x52, 0x6f, 0x75,
0x74, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65,
0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10,
0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73,
0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74,
0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65,
0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x37, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 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, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f,
0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x72,
0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x68, 0x65,
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65,
0x73, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x53, 0x0a, 0x1a, 0x45, 0x6e, 0x61,
0x62, 0x6c, 0x65, 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, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73,
0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x4b,
0x0a, 0x1b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52,
0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a,
0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x75,
0x74, 0x65, 0x73, 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 (
file_headscale_v1_routes_proto_rawDescOnce sync.Once
file_headscale_v1_routes_proto_rawDescData = file_headscale_v1_routes_proto_rawDesc
)
func file_headscale_v1_routes_proto_rawDescGZIP() []byte {
file_headscale_v1_routes_proto_rawDescOnce.Do(func() {
file_headscale_v1_routes_proto_rawDescData = protoimpl.X.CompressGZIP(file_headscale_v1_routes_proto_rawDescData)
})
return file_headscale_v1_routes_proto_rawDescData
}
var file_headscale_v1_routes_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_headscale_v1_routes_proto_goTypes = []interface{}{
(*Routes)(nil), // 0: headscale.v1.Routes
(*GetMachineRouteRequest)(nil), // 1: headscale.v1.GetMachineRouteRequest
(*GetMachineRouteResponse)(nil), // 2: headscale.v1.GetMachineRouteResponse
(*EnableMachineRoutesRequest)(nil), // 3: headscale.v1.EnableMachineRoutesRequest
(*EnableMachineRoutesResponse)(nil), // 4: headscale.v1.EnableMachineRoutesResponse
}
var file_headscale_v1_routes_proto_depIdxs = []int32{
0, // 0: headscale.v1.GetMachineRouteResponse.routes:type_name -> headscale.v1.Routes
0, // 1: headscale.v1.EnableMachineRoutesResponse.routes:type_name -> headscale.v1.Routes
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_headscale_v1_routes_proto_init() }
func file_headscale_v1_routes_proto_init() {
if File_headscale_v1_routes_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_headscale_v1_routes_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Routes); 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[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetMachineRouteRequest); 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[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetMachineRouteResponse); 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[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*EnableMachineRoutesRequest); 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[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*EnableMachineRoutesResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_headscale_v1_routes_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_headscale_v1_routes_proto_goTypes,
DependencyIndexes: file_headscale_v1_routes_proto_depIdxs,
MessageInfos: file_headscale_v1_routes_proto_msgTypes,
}.Build()
File_headscale_v1_routes_proto = out.File
file_headscale_v1_routes_proto_rawDesc = nil
file_headscale_v1_routes_proto_goTypes = nil
file_headscale_v1_routes_proto_depIdxs = nil
}

View File

@ -0,0 +1,43 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/device.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}

View File

@ -0,0 +1,920 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/headscale.proto",
"version": "version not set"
},
"tags": [
{
"name": "HeadscaleService"
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/api/v1/debug/machine": {
"post": {
"summary": "--- Machine start ---",
"operationId": "HeadscaleService_DebugCreateMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1DebugCreateMachineResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1DebugCreateMachineRequest"
}
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine": {
"get": {
"operationId": "HeadscaleService_ListMachines",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1ListMachinesResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "namespace",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine/register": {
"post": {
"operationId": "HeadscaleService_RegisterMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1RegisterMachineResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine/{machineId}": {
"get": {
"operationId": "HeadscaleService_GetMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1GetMachineResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
}
],
"tags": [
"HeadscaleService"
]
},
"delete": {
"operationId": "HeadscaleService_DeleteMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1DeleteMachineResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine/{machineId}/expire": {
"post": {
"operationId": "HeadscaleService_ExpireMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1ExpireMachineResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine/{machineId}/routes": {
"get": {
"summary": "--- Route start ---",
"operationId": "HeadscaleService_GetMachineRoute",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1GetMachineRouteResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
}
],
"tags": [
"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"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine/{machineId}/share/{namespace}": {
"post": {
"operationId": "HeadscaleService_ShareMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1ShareMachineResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
},
{
"name": "namespace",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/machine/{machineId}/unshare/{namespace}": {
"post": {
"operationId": "HeadscaleService_UnshareMachine",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1UnshareMachineResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "machineId",
"in": "path",
"required": true,
"type": "string",
"format": "uint64"
},
{
"name": "namespace",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/namespace": {
"get": {
"operationId": "HeadscaleService_ListNamespaces",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1ListNamespacesResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"HeadscaleService"
]
},
"post": {
"operationId": "HeadscaleService_CreateNamespace",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1CreateNamespaceResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1CreateNamespaceRequest"
}
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/namespace/{name}": {
"get": {
"summary": "--- Namespace start ---",
"operationId": "HeadscaleService_GetNamespace",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1GetNamespaceResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
},
"delete": {
"operationId": "HeadscaleService_DeleteNamespace",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1DeleteNamespaceResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/namespace/{oldName}/rename/{newName}": {
"post": {
"operationId": "HeadscaleService_RenameNamespace",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1RenameNamespaceResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "oldName",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "newName",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/preauthkey": {
"get": {
"operationId": "HeadscaleService_ListPreAuthKeys",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1ListPreAuthKeysResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "namespace",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"HeadscaleService"
]
},
"post": {
"summary": "--- PreAuthKeys start ---",
"operationId": "HeadscaleService_CreatePreAuthKey",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1CreatePreAuthKeyResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1CreatePreAuthKeyRequest"
}
}
],
"tags": [
"HeadscaleService"
]
}
},
"/api/v1/preauthkey/expire": {
"post": {
"operationId": "HeadscaleService_ExpirePreAuthKey",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1ExpirePreAuthKeyResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1ExpirePreAuthKeyRequest"
}
}
],
"tags": [
"HeadscaleService"
]
}
}
},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
},
"v1CreateNamespaceRequest": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"v1CreateNamespaceResponse": {
"type": "object",
"properties": {
"namespace": {
"$ref": "#/definitions/v1Namespace"
}
}
},
"v1CreatePreAuthKeyRequest": {
"type": "object",
"properties": {
"namespace": {
"type": "string"
},
"reusable": {
"type": "boolean"
},
"ephemeral": {
"type": "boolean"
},
"expiration": {
"type": "string",
"format": "date-time"
}
}
},
"v1CreatePreAuthKeyResponse": {
"type": "object",
"properties": {
"preAuthKey": {
"$ref": "#/definitions/v1PreAuthKey"
}
}
},
"v1DebugCreateMachineRequest": {
"type": "object",
"properties": {
"namespace": {
"type": "string"
},
"key": {
"type": "string"
},
"name": {
"type": "string"
},
"routes": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"v1DebugCreateMachineResponse": {
"type": "object",
"properties": {
"machine": {
"$ref": "#/definitions/v1Machine"
}
}
},
"v1DeleteMachineResponse": {
"type": "object"
},
"v1DeleteNamespaceResponse": {
"type": "object"
},
"v1EnableMachineRoutesResponse": {
"type": "object",
"properties": {
"routes": {
"$ref": "#/definitions/v1Routes"
}
}
},
"v1ExpireMachineResponse": {
"type": "object",
"properties": {
"machine": {
"$ref": "#/definitions/v1Machine"
}
}
},
"v1ExpirePreAuthKeyRequest": {
"type": "object",
"properties": {
"namespace": {
"type": "string"
},
"key": {
"type": "string"
}
}
},
"v1ExpirePreAuthKeyResponse": {
"type": "object"
},
"v1GetMachineResponse": {
"type": "object",
"properties": {
"machine": {
"$ref": "#/definitions/v1Machine"
}
}
},
"v1GetMachineRouteResponse": {
"type": "object",
"properties": {
"routes": {
"$ref": "#/definitions/v1Routes"
}
}
},
"v1GetNamespaceResponse": {
"type": "object",
"properties": {
"namespace": {
"$ref": "#/definitions/v1Namespace"
}
}
},
"v1ListMachinesResponse": {
"type": "object",
"properties": {
"machines": {
"type": "array",
"items": {
"$ref": "#/definitions/v1Machine"
}
}
}
},
"v1ListNamespacesResponse": {
"type": "object",
"properties": {
"namespaces": {
"type": "array",
"items": {
"$ref": "#/definitions/v1Namespace"
}
}
}
},
"v1ListPreAuthKeysResponse": {
"type": "object",
"properties": {
"preAuthKeys": {
"type": "array",
"items": {
"$ref": "#/definitions/v1PreAuthKey"
}
}
}
},
"v1Machine": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uint64"
},
"machineKey": {
"type": "string"
},
"nodeKey": {
"type": "string"
},
"discoKey": {
"type": "string"
},
"ipAddress": {
"type": "string"
},
"name": {
"type": "string"
},
"namespace": {
"$ref": "#/definitions/v1Namespace"
},
"registered": {
"type": "boolean"
},
"registerMethod": {
"$ref": "#/definitions/v1RegisterMethod"
},
"lastSeen": {
"type": "string",
"format": "date-time"
},
"lastSuccessfulUpdate": {
"type": "string",
"format": "date-time"
},
"expiry": {
"type": "string",
"format": "date-time"
},
"preAuthKey": {
"$ref": "#/definitions/v1PreAuthKey"
},
"createdAt": {
"type": "string",
"format": "date-time"
}
}
},
"v1Namespace": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
}
}
},
"v1PreAuthKey": {
"type": "object",
"properties": {
"namespace": {
"type": "string"
},
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"reusable": {
"type": "boolean"
},
"ephemeral": {
"type": "boolean"
},
"used": {
"type": "boolean"
},
"expiration": {
"type": "string",
"format": "date-time"
},
"createdAt": {
"type": "string",
"format": "date-time"
}
}
},
"v1RegisterMachineResponse": {
"type": "object",
"properties": {
"machine": {
"$ref": "#/definitions/v1Machine"
}
}
},
"v1RegisterMethod": {
"type": "string",
"enum": [
"REGISTER_METHOD_UNSPECIFIED",
"REGISTER_METHOD_AUTH_KEY",
"REGISTER_METHOD_CLI",
"REGISTER_METHOD_OIDC"
],
"default": "REGISTER_METHOD_UNSPECIFIED"
},
"v1RenameNamespaceResponse": {
"type": "object",
"properties": {
"namespace": {
"$ref": "#/definitions/v1Namespace"
}
}
},
"v1Routes": {
"type": "object",
"properties": {
"advertisedRoutes": {
"type": "array",
"items": {
"type": "string"
}
},
"enabledRoutes": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"v1ShareMachineResponse": {
"type": "object",
"properties": {
"machine": {
"$ref": "#/definitions/v1Machine"
}
}
},
"v1UnshareMachineResponse": {
"type": "object",
"properties": {
"machine": {
"$ref": "#/definitions/v1Machine"
}
}
}
}
}

View File

@ -0,0 +1,43 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/machine.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}

View File

@ -0,0 +1,43 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/namespace.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}

View File

@ -0,0 +1,43 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/preauthkey.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}

View File

@ -0,0 +1,43 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/routes.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}

141
go.mod
View File

@ -1,45 +1,138 @@
module github.com/juanfont/headscale module github.com/juanfont/headscale
go 1.16 go 1.17
require ( require (
github.com/AlecAivazis/survey/v2 v2.3.2 github.com/AlecAivazis/survey/v2 v2.3.2
github.com/Microsoft/go-winio v0.5.0 // indirect github.com/coreos/go-oidc/v3 v3.1.0
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/containerd/continuity v0.1.0 // indirect
github.com/docker/cli v20.10.8+incompatible // indirect
github.com/docker/docker v20.10.8+incompatible // indirect
github.com/efekarakus/termcolor v1.0.1 github.com/efekarakus/termcolor v1.0.1
github.com/fatih/set v0.2.1 // indirect github.com/fatih/set v0.2.1
github.com/gin-gonic/gin v1.7.4 github.com/gin-gonic/gin v1.7.4
github.com/gofrs/uuid v4.0.0+incompatible github.com/gofrs/uuid v4.1.0+incompatible
github.com/google/go-github v17.0.0+incompatible // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/google/go-querystring v1.1.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/infobloxopen/protoc-gen-gorm v1.0.1
github.com/klauspost/compress v1.13.5 github.com/klauspost/compress v1.13.6
github.com/lib/pq v1.10.3 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/opencontainers/runc v1.0.2 // indirect
github.com/ory/dockertest/v3 v3.7.0 github.com/ory/dockertest/v3 v3.7.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/philip-bui/grpc-zerolog v1.0.1
github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_golang v1.11.0
github.com/pterm/pterm v0.12.30 github.com/pterm/pterm v0.12.30
github.com/rs/zerolog v1.25.0 github.com/rs/zerolog v1.26.0
github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.2.1 github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.8.1 github.com/spf13/viper v1.9.0
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88 github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/zsais/go-gin-prometheus v0.1.0 github.com/zsais/go-gin-prometheus v0.1.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247
google.golang.org/grpc v1.42.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
google.golang.org/protobuf v1.27.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
gorm.io/datatypes v1.0.2 gorm.io/datatypes v1.0.2
gorm.io/driver/postgres v1.1.1 gorm.io/driver/postgres v1.1.1
gorm.io/driver/sqlite v1.1.5 gorm.io/driver/sqlite v1.1.5
gorm.io/gorm v1.21.15 gorm.io/gorm v1.21.15
inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
tailscale.com v1.14.2 tailscale.com v1.20.3
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/atomicgo/cursor v0.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/continuity v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.8+incompatible // indirect
github.com/docker/docker v20.10.8+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.9.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gookit/color v1.4.2 // indirect
github.com/hashicorp/go-version v1.2.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.10.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.1.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.8.1 // indirect
github.com/jackc/pgx/v4 v4.13.0 // indirect
github.com/jinzhu/gorm v1.9.16 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.3 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.8 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.0.3 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
golang.org/x/net v0.0.0-20211205041911-012df41ee64c // indirect
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
) )

630
go.sum

File diff suppressed because it is too large Load Diff

407
grpcv1.go Normal file
View File

@ -0,0 +1,407 @@
//nolint
package headscale
import (
"context"
"encoding/json"
"time"
"github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"gorm.io/datatypes"
"tailscale.com/tailcfg"
)
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
v1.UnimplementedHeadscaleServiceServer
h *Headscale
}
func newHeadscaleV1APIServer(h *Headscale) v1.HeadscaleServiceServer {
return headscaleV1APIServer{
h: h,
}
}
func (api headscaleV1APIServer) GetNamespace(
ctx context.Context,
request *v1.GetNamespaceRequest,
) (*v1.GetNamespaceResponse, error) {
namespace, err := api.h.GetNamespace(request.GetName())
if err != nil {
return nil, err
}
return &v1.GetNamespaceResponse{Namespace: namespace.toProto()}, nil
}
func (api headscaleV1APIServer) CreateNamespace(
ctx context.Context,
request *v1.CreateNamespaceRequest,
) (*v1.CreateNamespaceResponse, error) {
namespace, err := api.h.CreateNamespace(request.GetName())
if err != nil {
return nil, err
}
return &v1.CreateNamespaceResponse{Namespace: namespace.toProto()}, nil
}
func (api headscaleV1APIServer) RenameNamespace(
ctx context.Context,
request *v1.RenameNamespaceRequest,
) (*v1.RenameNamespaceResponse, error) {
err := api.h.RenameNamespace(request.GetOldName(), request.GetNewName())
if err != nil {
return nil, err
}
namespace, err := api.h.GetNamespace(request.GetNewName())
if err != nil {
return nil, err
}
return &v1.RenameNamespaceResponse{Namespace: namespace.toProto()}, nil
}
func (api headscaleV1APIServer) DeleteNamespace(
ctx context.Context,
request *v1.DeleteNamespaceRequest,
) (*v1.DeleteNamespaceResponse, error) {
err := api.h.DestroyNamespace(request.GetName())
if err != nil {
return nil, err
}
return &v1.DeleteNamespaceResponse{}, nil
}
func (api headscaleV1APIServer) ListNamespaces(
ctx context.Context,
request *v1.ListNamespacesRequest,
) (*v1.ListNamespacesResponse, error) {
namespaces, err := api.h.ListNamespaces()
if err != nil {
return nil, err
}
response := make([]*v1.Namespace, len(namespaces))
for index, namespace := range namespaces {
response[index] = namespace.toProto()
}
log.Trace().Caller().Interface("namespaces", response).Msg("")
return &v1.ListNamespacesResponse{Namespaces: response}, nil
}
func (api headscaleV1APIServer) CreatePreAuthKey(
ctx context.Context,
request *v1.CreatePreAuthKeyRequest,
) (*v1.CreatePreAuthKeyResponse, error) {
var expiration time.Time
if request.GetExpiration() != nil {
expiration = request.GetExpiration().AsTime()
}
preAuthKey, err := api.h.CreatePreAuthKey(
request.GetNamespace(),
request.GetReusable(),
request.GetEphemeral(),
&expiration,
)
if err != nil {
return nil, err
}
return &v1.CreatePreAuthKeyResponse{PreAuthKey: preAuthKey.toProto()}, nil
}
func (api headscaleV1APIServer) ExpirePreAuthKey(
ctx context.Context,
request *v1.ExpirePreAuthKeyRequest,
) (*v1.ExpirePreAuthKeyResponse, error) {
preAuthKey, err := api.h.GetPreAuthKey(request.GetNamespace(), request.Key)
if err != nil {
return nil, err
}
err = api.h.ExpirePreAuthKey(preAuthKey)
if err != nil {
return nil, err
}
return &v1.ExpirePreAuthKeyResponse{}, nil
}
func (api headscaleV1APIServer) ListPreAuthKeys(
ctx context.Context,
request *v1.ListPreAuthKeysRequest,
) (*v1.ListPreAuthKeysResponse, error) {
preAuthKeys, err := api.h.ListPreAuthKeys(request.GetNamespace())
if err != nil {
return nil, err
}
response := make([]*v1.PreAuthKey, len(preAuthKeys))
for index, key := range preAuthKeys {
response[index] = key.toProto()
}
return &v1.ListPreAuthKeysResponse{PreAuthKeys: response}, nil
}
func (api headscaleV1APIServer) RegisterMachine(
ctx context.Context,
request *v1.RegisterMachineRequest,
) (*v1.RegisterMachineResponse, error) {
log.Trace().
Str("namespace", request.GetNamespace()).
Str("machine_key", request.GetKey()).
Msg("Registering machine")
machine, err := api.h.RegisterMachine(
request.GetKey(),
request.GetNamespace(),
)
if err != nil {
return nil, err
}
return &v1.RegisterMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) GetMachine(
ctx context.Context,
request *v1.GetMachineRequest,
) (*v1.GetMachineResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
return &v1.GetMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) DeleteMachine(
ctx context.Context,
request *v1.DeleteMachineRequest,
) (*v1.DeleteMachineResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
err = api.h.DeleteMachine(
machine,
)
if err != nil {
return nil, err
}
return &v1.DeleteMachineResponse{}, nil
}
func (api headscaleV1APIServer) ExpireMachine(
ctx context.Context,
request *v1.ExpireMachineRequest,
) (*v1.ExpireMachineResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
api.h.ExpireMachine(
machine,
)
log.Trace().
Str("machine", machine.Name).
Time("expiry", *machine.Expiry).
Msg("machine expired")
return &v1.ExpireMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) ListMachines(
ctx context.Context,
request *v1.ListMachinesRequest,
) (*v1.ListMachinesResponse, error) {
if request.GetNamespace() != "" {
machines, err := api.h.ListMachinesInNamespace(request.GetNamespace())
if err != nil {
return nil, err
}
sharedMachines, err := api.h.ListSharedMachinesInNamespace(
request.GetNamespace(),
)
if err != nil {
return nil, err
}
machines = append(machines, sharedMachines...)
response := make([]*v1.Machine, len(machines))
for index, machine := range machines {
response[index] = machine.toProto()
}
return &v1.ListMachinesResponse{Machines: response}, nil
}
machines, err := api.h.ListMachines()
if err != nil {
return nil, err
}
response := make([]*v1.Machine, len(machines))
for index, machine := range machines {
response[index] = machine.toProto()
}
return &v1.ListMachinesResponse{Machines: response}, nil
}
func (api headscaleV1APIServer) ShareMachine(
ctx context.Context,
request *v1.ShareMachineRequest,
) (*v1.ShareMachineResponse, error) {
destinationNamespace, err := api.h.GetNamespace(request.GetNamespace())
if err != nil {
return nil, err
}
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
err = api.h.AddSharedMachineToNamespace(machine, destinationNamespace)
if err != nil {
return nil, err
}
return &v1.ShareMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) UnshareMachine(
ctx context.Context,
request *v1.UnshareMachineRequest,
) (*v1.UnshareMachineResponse, error) {
destinationNamespace, err := api.h.GetNamespace(request.GetNamespace())
if err != nil {
return nil, err
}
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
err = api.h.RemoveSharedMachineFromNamespace(machine, destinationNamespace)
if err != nil {
return nil, err
}
return &v1.UnshareMachineResponse{Machine: machine.toProto()}, nil
}
func (api headscaleV1APIServer) GetMachineRoute(
ctx context.Context,
request *v1.GetMachineRouteRequest,
) (*v1.GetMachineRouteResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
routes, err := machine.RoutesToProto()
if err != nil {
return nil, err
}
return &v1.GetMachineRouteResponse{
Routes: routes,
}, nil
}
func (api headscaleV1APIServer) EnableMachineRoutes(
ctx context.Context,
request *v1.EnableMachineRoutesRequest,
) (*v1.EnableMachineRoutesResponse, error) {
machine, err := api.h.GetMachineByID(request.GetMachineId())
if err != nil {
return nil, err
}
err = api.h.EnableRoutes(machine, request.GetRoutes()...)
if err != nil {
return nil, err
}
routes, err := machine.RoutesToProto()
if err != nil {
return nil, err
}
return &v1.EnableMachineRoutesResponse{
Routes: routes,
}, nil
}
// The following service calls are for testing and debugging
func (api headscaleV1APIServer) DebugCreateMachine(
ctx context.Context,
request *v1.DebugCreateMachineRequest,
) (*v1.DebugCreateMachineResponse, error) {
namespace, err := api.h.GetNamespace(request.GetNamespace())
if err != nil {
return nil, err
}
routes, err := stringToIPPrefix(request.GetRoutes())
if err != nil {
return nil, err
}
log.Trace().
Caller().
Interface("route-prefix", routes).
Interface("route-str", request.GetRoutes()).
Msg("")
hostinfo := tailcfg.Hostinfo{
RoutableIPs: routes,
OS: "TestOS",
Hostname: "DebugTestMachine",
}
log.Trace().Caller().Interface("hostinfo", hostinfo).Msg("")
hostinfoJson, err := json.Marshal(hostinfo)
if err != nil {
return nil, err
}
newMachine := Machine{
MachineKey: request.GetKey(),
Name: request.GetName(),
Namespace: *namespace,
Expiry: &time.Time{},
LastSeen: &time.Time{},
LastSuccessfulUpdate: &time.Time{},
HostInfo: datatypes.JSON(hostinfoJson),
}
// log.Trace().Caller().Interface("machine", newMachine).Msg("")
if err := api.h.db.Create(&newMachine).Error; err != nil {
return nil, err
}
return &v1.DebugCreateMachineResponse{Machine: newMachine.toProto()}, nil
}
func (api headscaleV1APIServer) mustEmbedUnimplementedHeadscaleServiceServer() {}

1195
integration_cli_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,77 @@
//go:build integration
// +build integration
package headscale
import (
"bytes"
"fmt"
"time"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
func ExecuteCommand(
resource *dockertest.Resource,
cmd []string,
env []string,
) (string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
// TODO(kradalby): Make configurable
timeout := DOCKER_EXECUTE_TIMEOUT
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 "", res.err
}
if res.exitCode != 0 {
fmt.Println("Command: ", cmd)
fmt.Println("stdout: ", stdout.String())
fmt.Println("stderr: ", stderr.String())
return "", fmt.Errorf("command failed with: %s", stderr.String())
}
return stdout.String(), nil
case <-time.After(timeout):
return "", fmt.Errorf("command timed out after %s", timeout)
}
}
func DockerRestartPolicy(config *docker.HostConfig) {
// set AutoRemove to true so that stopped container goes away by itself
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{
Name: "no",
}
}

View File

@ -18,28 +18,17 @@ import (
"testing" "testing"
"time" "time"
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"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype" "tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"inet.af/netaddr"
) )
var ( var tailscaleVersions = []string{"1.20.2", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
integrationTmpDir string
ih Headscale
)
var (
pool dockertest.Pool
network dockertest.Network
headscale dockertest.Resource
)
var tailscaleVersions = []string{"1.14.3", "1.12.3"}
type TestNamespace struct { type TestNamespace struct {
count int count int
@ -50,6 +39,10 @@ type IntegrationTestSuite struct {
suite.Suite suite.Suite
stats *suite.SuiteInformation stats *suite.SuiteInformation
pool dockertest.Pool
network dockertest.Network
headscale dockertest.Resource
namespaces map[string]TestNamespace namespaces map[string]TestNamespace
} }
@ -74,54 +67,31 @@ func TestIntegrationTestSuite(t *testing.T) {
// we have potentially saved the logs. // we have potentially saved the logs.
for _, scales := range s.namespaces { for _, scales := range s.namespaces {
for _, tailscale := range scales.tailscales { for _, tailscale := range scales.tailscales {
if err := pool.Purge(&tailscale); err != nil { if err := s.pool.Purge(&tailscale); err != nil {
log.Printf("Could not purge resource: %s\n", err) log.Printf("Could not purge resource: %s\n", err)
} }
} }
} }
if !s.stats.Passed() { if !s.stats.Passed() {
err := saveLog(&headscale, "test_output") err := s.saveLog(&s.headscale, "test_output")
if err != nil { if err != nil {
log.Printf("Could not save log: %s\n", err) log.Printf("Could not save log: %s\n", err)
} }
} }
if err := pool.Purge(&headscale); err != nil { if err := s.pool.Purge(&s.headscale); err != nil {
log.Printf("Could not purge resource: %s\n", err) log.Printf("Could not purge resource: %s\n", err)
} }
if err := network.Close(); err != nil { if err := s.network.Close(); err != nil {
log.Printf("Could not close network: %s\n", err) log.Printf("Could not close network: %s\n", err)
} }
} }
func executeCommand(resource *dockertest.Resource, cmd []string, env []string) (string, error) { func (s *IntegrationTestSuite) saveLog(
var stdout bytes.Buffer resource *dockertest.Resource,
var stderr bytes.Buffer basePath string,
) error {
exitCode, err := resource.Exec(
cmd,
dockertest.ExecOptions{
Env: env,
StdOut: &stdout,
StdErr: &stderr,
},
)
if err != nil {
return "", err
}
if exitCode != 0 {
fmt.Println("Command: ", cmd)
fmt.Println("stdout: ", stdout.String())
fmt.Println("stderr: ", stderr.String())
return "", fmt.Errorf("command failed with: %s", stderr.String())
}
return stdout.String(), nil
}
func saveLog(resource *dockertest.Resource, basePath string) error {
err := os.MkdirAll(basePath, os.ModePerm) err := os.MkdirAll(basePath, os.ModePerm)
if err != nil { if err != nil {
return err return err
@ -130,7 +100,7 @@ func saveLog(resource *dockertest.Resource, basePath string) error {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
err = pool.Client.Logs( err = s.pool.Client.Logs(
docker.LogsOptions{ docker.LogsOptions{
Context: context.TODO(), Context: context.TODO(),
Container: resource.Container.ID, Container: resource.Container.ID,
@ -150,12 +120,20 @@ func saveLog(resource *dockertest.Resource, basePath string) error {
fmt.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath) fmt.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) err = ioutil.WriteFile(
path.Join(basePath, resource.Container.Name+".stdout.log"),
[]byte(stdout.String()),
0o644,
)
if err != nil { if err != nil {
return err return err
} }
err = ioutil.WriteFile(path.Join(basePath, resource.Container.Name+".stderr.log"), []byte(stdout.String()), 0o644) err = ioutil.WriteFile(
path.Join(basePath, resource.Container.Name+".stderr.log"),
[]byte(stdout.String()),
0o644,
)
if err != nil { if err != nil {
return err return err
} }
@ -163,15 +141,9 @@ func saveLog(resource *dockertest.Resource, basePath string) error {
return nil return nil
} }
func dockerRestartPolicy(config *docker.HostConfig) { func (s *IntegrationTestSuite) tailscaleContainer(
// set AutoRemove to true so that stopped container goes away by itself namespace, identifier, version string,
config.AutoRemove = true ) (string, *dockertest.Resource) {
config.RestartPolicy = docker.RestartPolicy{
Name: "no",
}
}
func tailscaleContainer(namespace, identifier, version string) (string, *dockertest.Resource) {
tailscaleBuildOptions := &dockertest.BuildOptions{ tailscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.tailscale", Dockerfile: "Dockerfile.tailscale",
ContextDir: ".", ContextDir: ".",
@ -182,36 +154,50 @@ func tailscaleContainer(namespace, identifier, version string) (string, *dockert
}, },
}, },
} }
hostname := fmt.Sprintf("%s-tailscale-%s-%s", namespace, strings.Replace(version, ".", "-", -1), identifier) hostname := fmt.Sprintf(
"%s-tailscale-%s-%s",
namespace,
strings.Replace(version, ".", "-", -1),
identifier,
)
tailscaleOptions := &dockertest.RunOptions{ tailscaleOptions := &dockertest.RunOptions{
Name: hostname, Name: hostname,
Networks: []*dockertest.Network{&network}, Networks: []*dockertest.Network{&s.network},
Cmd: []string{"tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"}, Cmd: []string{
"tailscaled",
"--tun=userspace-networking",
"--socks5-server=localhost:1055",
},
} }
pts, err := pool.BuildAndRunWithBuildOptions(tailscaleBuildOptions, tailscaleOptions, dockerRestartPolicy) pts, err := s.pool.BuildAndRunWithBuildOptions(
tailscaleBuildOptions,
tailscaleOptions,
DockerRestartPolicy,
)
if err != nil { if err != nil {
log.Fatalf("Could not start resource: %s", err) log.Fatalf("Could not start resource: %s", err)
} }
fmt.Printf("Created %s container\n", hostname) fmt.Printf("Created %s container\n", hostname)
return hostname, pts return hostname, pts
} }
func (s *IntegrationTestSuite) SetupSuite() { func (s *IntegrationTestSuite) SetupSuite() {
var err error var err error
h = Headscale{ app = Headscale{
dbType: "sqlite3", dbType: "sqlite3",
dbString: "integration_test_db.sqlite3", dbString: "integration_test_db.sqlite3",
} }
if ppool, err := dockertest.NewPool(""); err == nil { if ppool, err := dockertest.NewPool(""); err == nil {
pool = *ppool s.pool = *ppool
} else { } else {
log.Fatalf("Could not connect to docker: %s", err) log.Fatalf("Could not connect to docker: %s", err)
} }
if pnetwork, err := pool.CreateNetwork("headscale-test"); err == nil { if pnetwork, err := s.pool.CreateNetwork("headscale-test"); err == nil {
network = *pnetwork s.network = *pnetwork
} else { } else {
log.Fatalf("Could not create network: %s", err) log.Fatalf("Could not create network: %s", err)
} }
@ -231,13 +217,13 @@ func (s *IntegrationTestSuite) SetupSuite() {
Mounts: []string{ Mounts: []string{
fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath), fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath),
}, },
Networks: []*dockertest.Network{&network}, Networks: []*dockertest.Network{&s.network},
Cmd: []string{"headscale", "serve"}, Cmd: []string{"headscale", "serve"},
} }
fmt.Println("Creating headscale container") fmt.Println("Creating headscale container")
if pheadscale, err := pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, dockerRestartPolicy); err == nil { if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
headscale = *pheadscale s.headscale = *pheadscale
} else { } else {
log.Fatalf("Could not start resource: %s", err) log.Fatalf("Could not start resource: %s", err)
} }
@ -248,23 +234,30 @@ func (s *IntegrationTestSuite) SetupSuite() {
for i := 0; i < scales.count; i++ { for i := 0; i < scales.count; i++ {
version := tailscaleVersions[i%len(tailscaleVersions)] version := tailscaleVersions[i%len(tailscaleVersions)]
hostname, container := tailscaleContainer(namespace, fmt.Sprint(i), version) hostname, container := s.tailscaleContainer(
namespace,
fmt.Sprint(i),
version,
)
scales.tailscales[hostname] = *container scales.tailscales[hostname] = *container
} }
} }
fmt.Println("Waiting for headscale to be ready") fmt.Println("Waiting for headscale to be ready")
hostEndpoint := fmt.Sprintf("localhost:%s", headscale.GetPort("8080/tcp")) hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8080/tcp"))
if err := 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 {
return err return err
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status code not OK") return fmt.Errorf("status code not OK")
} }
return nil return nil
}); err != nil { }); err != nil {
// TODO(kradalby): If we cannot access headscale, or any other fatal error during // TODO(kradalby): If we cannot access headscale, or any other fatal error during
@ -277,17 +270,17 @@ func (s *IntegrationTestSuite) SetupSuite() {
for namespace, scales := range s.namespaces { for namespace, scales := range s.namespaces {
fmt.Printf("Creating headscale namespace: %s\n", namespace) fmt.Printf("Creating headscale namespace: %s\n", namespace)
result, err := executeCommand( result, err := ExecuteCommand(
&headscale, &s.headscale,
[]string{"headscale", "namespaces", "create", namespace}, []string{"headscale", "namespaces", "create", namespace},
[]string{}, []string{},
) )
assert.Nil(s.T(), err)
fmt.Println("headscale create namespace result: ", result) fmt.Println("headscale create namespace result: ", result)
assert.Nil(s.T(), err)
fmt.Printf("Creating pre auth key for %s\n", namespace) fmt.Printf("Creating pre auth key for %s\n", namespace)
authKey, err := executeCommand( preAuthResult, err := ExecuteCommand(
&headscale, &s.headscale,
[]string{ []string{
"headscale", "headscale",
"--namespace", "--namespace",
@ -297,14 +290,24 @@ func (s *IntegrationTestSuite) SetupSuite() {
"--reusable", "--reusable",
"--expiration", "--expiration",
"24h", "24h",
"--output",
"json",
}, },
[]string{}, []string{"LOG_LEVEL=error"},
) )
assert.Nil(s.T(), err) 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" headscaleEndpoint := "http://headscale:8080"
fmt.Printf("Joining tailscale containers to headscale at %s\n", headscaleEndpoint) fmt.Printf(
"Joining tailscale containers to headscale at %s\n",
headscaleEndpoint,
)
for hostname, tailscale := range scales.tailscales { for hostname, tailscale := range scales.tailscales {
command := []string{ command := []string{
"tailscale", "tailscale",
@ -312,14 +315,14 @@ func (s *IntegrationTestSuite) SetupSuite() {
"-login-server", "-login-server",
headscaleEndpoint, headscaleEndpoint,
"--authkey", "--authkey",
strings.TrimSuffix(authKey, "\n"), preAuthKey.Key,
"--hostname", "--hostname",
hostname, hostname,
} }
fmt.Println("Join command:", command) fmt.Println("Join command:", command)
fmt.Printf("Running join command for %s\n", hostname) fmt.Printf("Running join command for %s\n", hostname)
result, err := executeCommand( result, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@ -338,15 +341,18 @@ func (s *IntegrationTestSuite) SetupSuite() {
func (s *IntegrationTestSuite) TearDownSuite() { func (s *IntegrationTestSuite) TearDownSuite() {
} }
func (s *IntegrationTestSuite) HandleStats(suiteName string, stats *suite.SuiteInformation) { func (s *IntegrationTestSuite) HandleStats(
suiteName string,
stats *suite.SuiteInformation,
) {
s.stats = stats s.stats = stats
} }
func (s *IntegrationTestSuite) TestListNodes() { func (s *IntegrationTestSuite) TestListNodes() {
for namespace, scales := range s.namespaces { for namespace, scales := range s.namespaces {
fmt.Println("Listing nodes") fmt.Println("Listing nodes")
result, err := executeCommand( result, err := ExecuteCommand(
&headscale, &s.headscale,
[]string{"headscale", "--namespace", namespace, "nodes", "list"}, []string{"headscale", "--namespace", namespace, "nodes", "list"},
[]string{}, []string{},
) )
@ -387,46 +393,50 @@ func (s *IntegrationTestSuite) TestGetIpAddresses() {
} }
} }
func (s *IntegrationTestSuite) TestStatus() { // TODO(kradalby): fix this test
for _, scales := range s.namespaces { // We need some way to impot ipnstate.Status from multiple go packages.
ips, err := getIPs(scales.tailscales) // Currently it will only work with 1.18.x since that is the last
assert.Nil(s.T(), err) // version we have in go.mod
// func (s *IntegrationTestSuite) TestStatus() {
for hostname, tailscale := range scales.tailscales { // for _, scales := range s.namespaces {
s.T().Run(hostname, func(t *testing.T) { // ips, err := getIPs(scales.tailscales)
command := []string{"tailscale", "status", "--json"} // assert.Nil(s.T(), err)
//
fmt.Printf("Getting status for %s\n", hostname) // for hostname, tailscale := range scales.tailscales {
result, err := executeCommand( // s.T().Run(hostname, func(t *testing.T) {
&tailscale, // command := []string{"tailscale", "status", "--json"}
command, //
[]string{}, // fmt.Printf("Getting status for %s\n", hostname)
) // result, err := ExecuteCommand(
assert.Nil(t, err) // &tailscale,
// command,
var status ipnstate.Status // []string{},
err = json.Unmarshal([]byte(result), &status) // )
assert.Nil(s.T(), err) // assert.Nil(t, err)
//
// TODO(kradalby): Replace this check with peer length of SAME namespace // var status ipnstate.Status
// Check if we have as many nodes in status // err = json.Unmarshal([]byte(result), &status)
// as we have IPs/tailscales // assert.Nil(s.T(), err)
// lines := strings.Split(result, "\n") //
// assert.Equal(t, len(ips), len(lines)-1) // // TODO(kradalby): Replace this check with peer length of SAME namespace
// assert.Equal(t, len(scales.tailscales), len(lines)-1) // // Check if we have as many nodes in status
// // as we have IPs/tailscales
peerIps := getIPsfromIPNstate(status) // // lines := strings.Split(result, "\n")
// // assert.Equal(t, len(ips), len(lines)-1)
// Check that all hosts is present in all hosts status // // assert.Equal(t, len(scales.tailscales), len(lines)-1)
for ipHostname, ip := range ips { //
if hostname != ipHostname { // peerIps := getIPsfromIPNstate(status)
assert.Contains(t, peerIps, ip) //
} // // 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 { func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP {
ips := make([]netaddr.IP, 0) ips := make([]netaddr.IP, 0)
@ -458,8 +468,14 @@ func (s *IntegrationTestSuite) TestPingAllPeers() {
ip.String(), ip.String(),
} }
fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip) fmt.Printf(
result, err := executeCommand( "Pinging from %s (%s) to %s (%s)\n",
hostname,
ips[hostname],
peername,
ip,
)
result, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@ -478,22 +494,35 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
main := s.namespaces["main"] main := s.namespaces["main"]
shared := s.namespaces["shared"] shared := s.namespaces["shared"]
result, err := executeCommand( result, err := ExecuteCommand(
&headscale, &s.headscale,
[]string{"headscale", "nodes", "list", "-o", "json", "--namespace", "shared"}, []string{
"headscale",
"nodes",
"list",
"--output",
"json",
"--namespace",
"shared",
},
[]string{}, []string{},
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
var machineList []Machine var machineList []v1.Machine
err = json.Unmarshal([]byte(result), &machineList) err = json.Unmarshal([]byte(result), &machineList)
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for _, machine := range machineList { for _, machine := range machineList {
result, err := ExecuteCommand(
result, err := executeCommand( &s.headscale,
&headscale, []string{
[]string{"headscale", "nodes", "share", "--namespace", "shared", fmt.Sprint(machine.ID), "main"}, "headscale",
"nodes",
"share",
"--identifier", fmt.Sprint(machine.Id),
"--namespace", "main",
},
[]string{}, []string{},
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
@ -501,8 +530,8 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
fmt.Println("Shared node with result: ", result) fmt.Println("Shared node with result: ", result)
} }
result, err = executeCommand( result, err = ExecuteCommand(
&headscale, &s.headscale,
[]string{"headscale", "nodes", "list", "--namespace", "main"}, []string{"headscale", "nodes", "list", "--namespace", "main"},
[]string{}, []string{},
) )
@ -545,8 +574,14 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
ip.String(), ip.String(),
} }
fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, mainIps[hostname], peername, ip) fmt.Printf(
result, err := executeCommand( "Pinging from %s (%s) to %s (%s)\n",
hostname,
mainIps[hostname],
peername,
ip,
)
result, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@ -569,7 +604,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
for hostname, tailscale := range scales.tailscales { for hostname, tailscale := range scales.tailscales {
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", hostname)} command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", hostname)}
_, err := executeCommand( _, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@ -578,7 +613,6 @@ func (s *IntegrationTestSuite) TestTailDrop() {
for peername, ip := range ips { for peername, ip := range ips {
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
if peername != hostname { if peername != hostname {
// Under normal circumstances, we should be able to send a file // Under normal circumstances, we should be able to send a file
// using `tailscale file cp` - but not in userspace networking mode // using `tailscale file cp` - but not in userspace networking mode
// So curl! // So curl!
@ -603,10 +637,20 @@ func (s *IntegrationTestSuite) TestTailDrop() {
"PUT", "PUT",
"--upload-file", "--upload-file",
fmt.Sprintf("/tmp/file_from_%s", hostname), fmt.Sprintf("/tmp/file_from_%s", hostname),
fmt.Sprintf("%s/v0/put/file_from_%s", peerAPI, hostname), fmt.Sprintf(
"%s/v0/put/file_from_%s",
peerAPI,
hostname,
),
} }
fmt.Printf("Sending file from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip) fmt.Printf(
_, err = executeCommand( "Sending file from %s (%s) to %s (%s)\n",
hostname,
ips[hostname],
peername,
ip,
)
_, err = ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{"ALL_PROXY=socks5://localhost:1055"}, []string{"ALL_PROXY=socks5://localhost:1055"},
@ -633,7 +677,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
"get", "get",
"/tmp/", "/tmp/",
} }
_, err := executeCommand( _, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@ -646,15 +690,25 @@ func (s *IntegrationTestSuite) TestTailDrop() {
"ls", "ls",
fmt.Sprintf("/tmp/file_from_%s", peername), fmt.Sprintf("/tmp/file_from_%s", peername),
} }
fmt.Printf("Checking file in %s (%s) from %s (%s)\n", hostname, ips[hostname], peername, ip) fmt.Printf(
result, err := executeCommand( "Checking file in %s (%s) from %s (%s)\n",
hostname,
ips[hostname],
peername,
ip,
)
result, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
) )
assert.Nil(t, err) assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", peername, result) fmt.Printf("Result for %s: %s\n", peername, result)
assert.Equal(t, result, fmt.Sprintf("/tmp/file_from_%s\n", peername)) assert.Equal(
t,
result,
fmt.Sprintf("/tmp/file_from_%s\n", peername),
)
} }
}) })
} }
@ -685,7 +739,7 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
peername, peername,
ip, ip,
) )
result, err := executeCommand( result, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@ -705,7 +759,7 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e
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{},
@ -721,10 +775,13 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e
ips[hostname] = ip ips[hostname] = ip
} }
return ips, nil return ips, nil
} }
func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]string, error) { func getAPIURLs(
tailscales map[string]dockertest.Resource,
) (map[netaddr.IP]string, error) {
fts := make(map[netaddr.IP]string) fts := make(map[netaddr.IP]string)
for _, tailscale := range tailscales { for _, tailscale := range tailscales {
command := []string{ command := []string{
@ -733,7 +790,7 @@ func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]strin
"/run/tailscale/tailscaled.sock", "/run/tailscale/tailscaled.sock",
"http://localhost/localapi/v0/file-targets", "http://localhost/localapi/v0/file-targets",
} }
result, err := executeCommand( result, err := ExecuteCommand(
&tailscale, &tailscale,
command, command,
[]string{}, []string{},
@ -758,5 +815,6 @@ func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]strin
} }
} }
} }
return fts, nil return fts, nil
} }

View File

@ -1 +0,0 @@
SEmQwCu+tGywQWEUsf93TpTRUvlB7WhnCdHgWrSXjEA=

View File

@ -1,18 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: headscale
annotations:
kubernetes.io/ingress.class: traefik
spec:
rules:
- host: $(PUBLIC_HOSTNAME)
http:
paths:
- backend:
service:
name: headscale
port:
number: 8080
path: /
pathType: Prefix

View File

@ -1,42 +0,0 @@
namespace: headscale
resources:
- configmap.yaml
- ingress.yaml
- service.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: headscale-site
files:
- derp.yaml=site/derp.yaml
envs:
- site/public.env
- name: headscale-etc
literals:
- config.json={}
secretGenerator:
- name: headscale
files:
- secrets/private-key
vars:
- name: PUBLIC_PROTO
objRef:
kind: ConfigMap
name: headscale-site
apiVersion: v1
fieldRef:
fieldPath: data.public-proto
- name: PUBLIC_HOSTNAME
objRef:
kind: ConfigMap
name: headscale-site
apiVersion: v1
fieldRef:
fieldPath: data.public-hostname
- name: CONTACT_EMAIL
objRef:
kind: ConfigMap
name: headscale-site
apiVersion: v1
fieldRef:
fieldPath: data.contact-email

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