Compare commits

..

86 Commits

Author SHA1 Message Date
Kristoffer Dalby
e05c5e0b93 Merge pull request #303 from kradalby/migrate_ipaddresses 2022-01-31 08:57:22 +00:00
Kristoffer Dalby
9e3318ca27 Simplify postgres uuid-ossp stirng 2022-01-30 14:53:40 +00:00
Kristoffer Dalby
e9adfcd678 Migrate ip_address field to ip_addresses
The automatic migration did not pick up this change and it breaks
current setups as it only adds a new field which is empty.

This means that clients who dont request a new IP will not have any IP
at all.
2022-01-30 14:18:24 +00:00
Kristoffer Dalby
5b5ecd52e1 Merge pull request #208 from enoperm/ipv6
Support for IPv6 prefixes in namespaces
2022-01-30 10:46:20 +00:00
Kristoffer Dalby
eddd62eee0 Merge branch 'main' into ipv6 2022-01-30 10:09:52 +00:00
Kristoffer Dalby
38c27f6bf8 Merge pull request #300 from stensonb/patch-2
Typo
2022-01-30 10:09:35 +00:00
Kristoffer Dalby
90fb9aa4ed Merge branch 'main' into ipv6 2022-01-30 10:08:44 +00:00
Kristoffer Dalby
3af1253a65 Merge branch 'main' into patch-2 2022-01-30 10:08:28 +00:00
Kristoffer Dalby
eb1ce64b7c Merge pull request #301 from kradalby/goreleaser
Set goreleaser to only care about Go 1.17
2022-01-30 10:08:04 +00:00
Kristoffer Dalby
2c9ed63021 Merge branch 'main' into goreleaser 2022-01-30 10:07:35 +00:00
Kristoffer Dalby
4c779d306b Merge pull request #302 from kradalby/build-avoidance
Set up build avoidance
2022-01-30 10:07:30 +00:00
Kristoffer Dalby
0862f60ff0 Put depth in the correct place 2022-01-30 09:54:26 +00:00
Kristoffer Dalby
991175f2aa Add depth and avoidance for build 2022-01-30 09:49:15 +00:00
Kristoffer Dalby
1815040d98 Set up build build avoidance
This commit configures the CI to run specific parts of the CI when
relevant changes has been made.

This should help us not have to deal with the integration tests when we
do doc/admin changes.
2022-01-30 09:43:48 +00:00
Kristoffer Dalby
71ab4c9b2c Fix type according to config schema 2022-01-30 08:59:25 +00:00
Kristoffer Dalby
e0c22a414b Remove wrong comment 2022-01-30 08:56:28 +00:00
Kristoffer Dalby
4e63bba4fe Only compat go 1.17 in go mod tidy 2022-01-30 08:55:41 +00:00
Kristoffer Dalby
445c04baf7 Fix lint 2022-01-30 08:35:10 +00:00
Kristoffer Dalby
ad4e3a89e0 Format changelog 2022-01-30 08:25:49 +00:00
Kristoffer Dalby
6f6018bad5 Merge branch 'main' into ipv6 2022-01-30 08:21:11 +00:00
Bryan Stenson
ccd41b9a13 Typo 2022-01-29 22:34:51 -08:00
Juan Font
d8ce440309 Merge pull request #299 from kradalby/0124prep
Tag 0.12.4 in CHANGELOG
2022-01-30 01:13:10 +01:00
Kristoffer Dalby
2f576b2fb1 Tag 0.12.4 in CHANGELOG 2022-01-29 20:04:56 +00:00
Kristoffer Dalby
853a5288f1 Merge pull request #292 from kradalby/socket-permission
Make Unix socket permissions configurable
2022-01-29 19:55:11 +00:00
Kristoffer Dalby
cd0df1e46f Merge branch 'main' into socket-permission 2022-01-29 19:30:49 +00:00
Juan Font
b195c87418 Merge pull request #290 from kradalby/generate-privkey
Add generate private-key command
2022-01-29 20:24:52 +01:00
Csaba Sarkadi
45bcf39894 fixup! fixup! cmd/headscale/cli/utils: merge ip_prefix with ip_prefixes in config 2022-01-29 16:52:27 +01:00
Csaba Sarkadi
0a1db89d33 fixup! cmd/headscale/cli/utils: merge ip_prefix with ip_prefixes in config 2022-01-29 16:27:36 +01:00
Kristoffer Dalby
dbfb9e16e0 Merge branch 'main' into socket-permission 2022-01-29 15:26:19 +00:00
Kristoffer Dalby
8aa2606853 Merge branch 'main' into generate-privkey 2022-01-29 15:26:15 +00:00
Kristoffer Dalby
a238a8b33a Merge pull request #291 from kradalby/tailscale-203
Upgrade to latest tailscale
2022-01-29 15:26:06 +00:00
Csaba Sarkadi
74f26d3685 fixup! CHANGELOG: document breaking configuration change regarding multiple prefixes 2022-01-29 16:08:02 +01:00
Csaba Sarkadi
e66f8b0eeb cmd/headscale/cli/utils: merge ip_prefix with ip_prefixes in config 2022-01-29 16:04:15 +01:00
Kristoffer Dalby
e7b69dbf91 Merge branch 'main' into generate-privkey 2022-01-29 14:35:24 +00:00
Kristoffer Dalby
13f23d2e7e Merge branch 'main' into socket-permission 2022-01-29 14:34:36 +00:00
Csaba Sarkadi
7a86321252 CHANGELOG: document breaking configuration change regarding multiple prefixes 2022-01-29 15:33:54 +01:00
Kristoffer Dalby
7aace7eb6b Update CHANGELOG.md 2022-01-29 14:33:12 +00:00
Kristoffer Dalby
7a6be36f46 Merge branch 'main' into tailscale-203 2022-01-29 14:32:04 +00:00
Kristoffer Dalby
bb27c80bad Update CHANGELOG.md 2022-01-29 14:31:42 +00:00
Csaba Sarkadi
c0c3b7d511 Merge remote-tracking branch 'origin/main' into ipv6 2022-01-29 15:27:49 +01:00
Csaba Sarkadi
6220836050 utils: extract GetIPPrefixEndpoints from anonymous function 2022-01-29 15:26:28 +01:00
Kristoffer Dalby
b122d06f12 Merge pull request #278 from enoperm/pollnetmap-update-only 2022-01-29 13:46:08 +00:00
Kristoffer Dalby
6f9ed958ca Merge branch 'main' into pollnetmap-update-only 2022-01-29 13:12:09 +00:00
Kristoffer Dalby
39ce59fcb1 Merge branch 'main' into generate-privkey 2022-01-29 13:11:26 +00:00
Kristoffer Dalby
052fccdc98 Merge pull request #289 from juanfont/whitespace 2022-01-29 13:09:16 +00:00
Csaba Sarkadi
17411b65f3 fixup! fixup! update CHANGELOG
prettier changes
2022-01-29 13:48:14 +01:00
Csaba Sarkadi
bf7ee78324 config-example: add configuration for a dual-stack tailnet 2022-01-28 22:13:45 +01:00
Csaba Sarkadi
fbe5054a67 fixup! update CHANGELOG 2022-01-28 22:00:13 +01:00
Csaba Sarkadi
761147ea3b update CHANGELOG 2022-01-28 21:59:08 +01:00
Csaba Sarkadi
25ccf5ef18 PollNetMapStream: do not create any rows during long-poll operation 2022-01-28 21:59:08 +01:00
Kristoffer Dalby
b4f8961e44 Make Unix socket permissions configurable 2022-01-28 18:58:22 +00:00
Kristoffer Dalby
726ccc8c1f Upgrade to latest tailscale 2022-01-28 18:15:41 +00:00
Kristoffer Dalby
126e694f26 Add generate private-key command
This commit adds a command to generate a private key for headscale.

Mostly useful for systems were you drive the deployment from another
machine and use a secret management system.
2022-01-28 18:08:52 +00:00
Kristoffer Dalby
ab45cd37f8 Only golint new problems 2022-01-28 17:40:39 +00:00
Kristoffer Dalby
f59071ff1c Trim whitespace from privateKey before parsing 2022-01-28 17:23:01 +00:00
Kristoffer Dalby
4d60aeae18 Merge pull request #282 from ryanfowler/main 2022-01-23 21:23:50 +00:00
Ryan Fowler
67d1dd984f Fix missing return in PollNetMapHandler 2022-01-21 21:48:58 -08:00
Juan Font
b02f8dd45d Merge pull request #274 from majst01/reduce-binary-size
Strip binary, update to go-1.17.6
2022-01-20 11:38:52 +01:00
Kristoffer Dalby
3837f1714a Merge branch 'main' into reduce-binary-size 2022-01-17 08:47:19 +00:00
Kristoffer Dalby
ed5498ef86 Merge pull request #276 from jimt/patch-1 2022-01-17 08:46:42 +00:00
Csaba Sarkadi
e2f8c69e2e integration-test: use tailscale ip to test dual-stack MagicDNS 2022-01-16 14:18:22 +01:00
Csaba Sarkadi
beb3e9abc2 integration-test: taildrop test refactor 2022-01-16 14:18:22 +01:00
Csaba Sarkadi
78039f4cea integration-test: use TUN devices, enable IPv6 addresses on local interfaces in containers 2022-01-16 14:18:22 +01:00
Csaba Sarkadi
ed39b91f71 Dockerfiles: specify origin registry explicitly 2022-01-16 14:18:22 +01:00
Csaba Sarkadi
8f632e9062 machine: isOutdated: handle machines without LastSuccefulUpdate set 2022-01-16 14:18:22 +01:00
Csaba Sarkadi
a32175f791 PollNetMapHandler: refactor with chan lifetimes in mind
* Resolves an issue where sometimes attempted sends on a closed channel
  happened by ensuring the channels remain open for the entire goroutine.
* May be of help with regards to issue #203
2022-01-16 14:18:22 +01:00
Csaba Sarkadi
d35fb8bba0 integration-test: add IPv6 prefix to configuration 2022-01-16 14:18:22 +01:00
Csaba Sarkadi
115d0cbe85 dns: IPv6 roots generation 2022-01-16 14:18:22 +01:00
Csaba Sarkadi
1a6e5d8770 Add support for multiple IP prefixes 2022-01-16 14:18:22 +01:00
Csaba Sarkadi
3a3aecb774 Regenerate files based on ProtoBuf schema. 2022-01-16 14:17:51 +01:00
Csaba Sarkadi
8b40343277 Add multiple IP prefixes support to ProtoBuf schema 2022-01-16 14:17:27 +01:00
Csaba Sarkadi
7ec8346179 Do not assume IPv4 during Tailscale node construction 2022-01-15 16:06:34 +01:00
Csaba Sarkadi
46cdce00af Do not assume IPv4 during address generation 2022-01-15 16:06:34 +01:00
Jim Tittsler
86f3f26a18 Fix typos 2022-01-15 16:44:36 +09:00
Stefan Majer
febbb6006f use most recent go 1.17 for tests 2022-01-14 12:59:53 +01:00
Stefan Majer
1d68509463 Strip binary, update to go-1.17.6 2022-01-14 09:19:16 +01:00
Kristoffer Dalby
b6d0c4f2aa Merge pull request #273 from juanfont/tailscale-1-20
Add new tailscale version to integration test
2022-01-13 22:21:27 +00:00
Kristoffer Dalby
2c057c2d89 Update integration_test.go 2022-01-13 19:16:12 +00:00
Juan Font
cf7effda1b Merge pull request #272 from juanfont/cl-0.12.3
Prepare CHANGELOG for 0.12.3
2022-01-13 17:02:37 +01:00
Juan Font Alonso
19effe7034 Updated changelog for 0.12.3 2022-01-13 12:42:56 +01:00
Juan Font
41053482b3 Merge pull request #271 from juanfont/minor-security-fixes
Minor security updates in go.mod
2022-01-12 22:13:28 +01:00
Juan Font
e463283a58 Merge branch 'main' into minor-security-fixes 2022-01-12 17:10:15 +01:00
Juan Font
99814b468b Merge pull request #270 from artemklevtsov/docker-alpine
Add docker alpine image
2022-01-12 17:10:05 +01:00
Juan Font Alonso
163ecb2a6b go.sum updated 2022-01-12 15:22:16 +01:00
Juan Font Alonso
4660b265d9 Minor security updates in go.mod 2022-01-12 15:17:55 +01:00
Artem Klevtsov
1b6bad0b63 Add docker alpine image 2022-01-12 19:29:59 +07:00
49 changed files with 1340 additions and 979 deletions

View File

@@ -14,22 +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.17.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
sudo apt update sudo apt update
sudo apt install -y make sudo apt install -y make
- name: Run build - 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

@@ -8,18 +8,55 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
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: golangci-lint - name: golangci-lint
if: steps.changed-files.outputs.any_changed == 'true'
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
with: with:
version: latest version: latest
# Only block PRs on new problems.
# 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: prettier-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
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: |
**/*.md
**/*.yml
**/*.yaml
**/*.ts
**/*.js
**/*.sass
**/*.css
**/*.scss
**/*.html
- name: Prettify code - name: Prettify code
if: steps.changed-files.outputs.any_changed == 'true'
uses: creyD/prettier_action@v4.0 uses: creyD/prettier_action@v4.0
with: with:
prettier_options: >- prettier_options: >-

View File

@@ -157,3 +157,67 @@ jobs:
run: | run: |
rm -rf /tmp/.buildx-cache-debug rm -rf /tmp/.buildx-cache-debug
mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug
docker-alpine-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache-alpine
key: ${{ runner.os }}-buildx-alpine-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-alpine-
- name: Docker meta
id: meta-alpine
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}-alpine
type=semver,pattern={{major}}.{{minor}}-alpine
type=semver,pattern={{major}}-alpine
type=raw,value=latest-alpine
type=sha,suffix=-alpine
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
file: Dockerfile.alpine
tags: ${{ steps.meta-alpine.outputs.tags }}
labels: ${{ steps.meta-alpine.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-alpine
cache-to: type=local,dest=/tmp/.buildx-cache-alpine-new
- 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.17.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.17.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

View File

@@ -1,8 +1,7 @@
# 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: release:
prerelease: auto prerelease: auto
@@ -33,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++

View File

@@ -2,6 +2,32 @@
**TBD (TBD):** **TBD (TBD):**
**0.13.0 (2022-xx-xx):**
**Features**:
- Add IPv6 support to the prefix assigned to namespaces
**Changes**:
- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208)
**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):** **0.12.2 (2022-01-11):**
Happy New Year! Happy New Year!

View File

@@ -1,5 +1,5 @@
# Builder image # Builder image
FROM golang:1.17.1-bullseye AS build FROM docker.io/golang:1.17.1-bullseye AS build
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale
@@ -9,6 +9,7 @@ RUN go mod download
COPY . . 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
# Production image # Production image

23
Dockerfile.alpine Normal file
View File

@@ -0,0 +1,23 @@
# Builder image
FROM docker.io/golang:1.17.1-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 docker.io/alpine:latest
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
EXPOSE 8080/tcp
CMD ["headscale"]

View File

@@ -1,5 +1,5 @@
# Builder image # Builder image
FROM golang:1.17.1-bullseye AS build FROM docker.io/golang:1.17.1-bullseye AS build
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale

View File

@@ -7,5 +7,5 @@ RUN apt-get update \
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \ && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \ && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
&& apt-get update \ && apt-get update \
&& apt-get install -y tailscale=${TAILSCALE_VERSION} \ && apt-get install -y tailscale=${TAILSCALE_VERSION} dnsutils \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@@ -18,7 +18,7 @@ test:
@go test -coverprofile=coverage.out ./... @go test -coverprofile=coverage.out ./...
test_integration: test_integration:
go test -tags integration -timeout 30m ./... go test -tags integration -timeout 30m -count=1 ./...
test_integration_cli: test_integration_cli:
go test -tags integration -v integration_cli_test.go integration_common_test.go go test -tags integration -v integration_cli_test.go integration_common_test.go

View File

@@ -65,7 +65,7 @@ To contribute to Headscale you would need the lastest version of [Go](https://go
### Code style ### Code style
To ensure we have some consistency with a growing number of contributes, this project has adopted linting and style/formatting rules: 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 The **Go** code is linted with [`golangci-lint`](https://golangci-lint.run) and
formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and
@@ -76,7 +76,7 @@ run `make lint` and `make fmt` before committing any code.
The **Proto** code is linted with [`buf`](https://docs.buf.build/lint/overview) and 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). formatted with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html).
The **rest** (markdown, yaml, etc) is formatted with [`prettier`](https://prettier.io). The **rest** (Markdown, YAML, etc) is formatted with [`prettier`](https://prettier.io).
Check out the `.golangci.yaml` and `Makefile` to see the specific configuration. Check out the `.golangci.yaml` and `Makefile` to see the specific configuration.
@@ -92,7 +92,7 @@ make install-protobuf-plugins
### Testing and building ### Testing and building
Some parts of the project requires the generation of Go code from Protobuf (if changes is made in `proto/`) and it must be (re-)generated with: 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 ```shell
make generate make generate

View File

@@ -25,8 +25,11 @@ const (
) )
const ( const (
Base8 = 8
Base10 = 10 Base10 = 10
BitSize16 = 16 BitSize16 = 16
BitSize32 = 32
BitSize64 = 64
portRangeBegin = 0 portRangeBegin = 0
portRangeEnd = 65535 portRangeEnd = 65535
expectedTokenItems = 2 expectedTokenItems = 2
@@ -185,7 +188,7 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
return nil, errInvalidNamespace return nil, errInvalidNamespace
} }
for _, node := range nodes { for _, node := range nodes {
ips = append(ips, node.IPAddress) ips = append(ips, node.IPAddresses.ToStringSlice()...)
} }
} }
@@ -219,7 +222,7 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
// FIXME: Check TagOwners allows this // FIXME: Check TagOwners allows this
for _, t := range hostinfo.RequestTags { for _, t := range hostinfo.RequestTags {
if alias[4:] == t { if alias[4:] == t {
ips = append(ips, machine.IPAddress) ips = append(ips, machine.IPAddresses.ToStringSlice()...)
break break
} }
@@ -238,7 +241,7 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
} }
ips := []string{} ips := []string{}
for _, n := range nodes { for _, n := range nodes {
ips = append(ips, n.IPAddress) ips = append(ips, n.IPAddresses.ToStringSlice()...)
} }
return ips, nil return ips, nil

View File

@@ -61,9 +61,9 @@ func (s *Suite) TestPortRange(c *check.C) {
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) {
@@ -75,11 +75,11 @@ func (s *Suite) TestPortWildcard(c *check.C) {
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) {
@@ -91,7 +91,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
_, err = app.GetMachine("testnamespace", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
ip, _ := app.getAvailableIP() ips, _ := app.getAvailableIPs()
machine := Machine{ machine := Machine{
ID: 0, ID: 0,
MachineKey: "foo", MachineKey: "foo",
@@ -101,7 +101,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: ip.String(), IPAddresses: ips,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
app.db.Save(&machine) app.db.Save(&machine)
@@ -116,12 +116,13 @@ func (s *Suite) TestPortNamespace(c *check.C) {
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(len(ips), check.Equals, 1)
c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String())
} }
func (s *Suite) TestPortGroup(c *check.C) { func (s *Suite) TestPortGroup(c *check.C) {
@@ -133,7 +134,7 @@ func (s *Suite) TestPortGroup(c *check.C) {
_, err = app.GetMachine("testnamespace", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
ip, _ := app.getAvailableIP() ips, _ := app.getAvailableIPs()
machine := Machine{ machine := Machine{
ID: 0, ID: 0,
MachineKey: "foo", MachineKey: "foo",
@@ -143,7 +144,7 @@ func (s *Suite) TestPortGroup(c *check.C) {
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: ip.String(), IPAddresses: ips,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
app.db.Save(&machine) app.db.Save(&machine)
@@ -156,10 +157,11 @@ func (s *Suite) TestPortGroup(c *check.C) {
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(len(ips), check.Equals, 1)
c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String())
} }

15
api.go
View File

@@ -497,6 +497,7 @@ func (h *Headscale) handleMachineRegistrationNew(
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody) ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
} }
// TODO: check if any locks are needed around IP allocation.
func (h *Headscale) handleAuthKey( func (h *Headscale) handleAuthKey(
ctx *gin.Context, ctx *gin.Context,
machineKey key.MachinePublic, machineKey key.MachinePublic,
@@ -554,14 +555,14 @@ func (h *Headscale) handleAuthKey(
log.Debug(). log.Debug().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", machine.Name). Str("machine", machine.Name).
Msg("Authentication key was valid, proceeding to acquire an IP address") Msg("Authentication key was valid, proceeding to acquire IP addresses")
ip, err := h.getAvailableIP() ips, err := h.getAvailableIPs()
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", machine.Name). Str("machine", machine.Name).
Msg("Failed to find an available IP") Msg("Failed to find an available IP address")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name). machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
Inc() Inc()
@@ -570,12 +571,12 @@ func (h *Headscale) handleAuthKey(
log.Info(). log.Info().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", machine.Name). Str("machine", machine.Name).
Str("ip", ip.String()). Str("ips", strings.Join(ips.ToStringSlice(), ",")).
Msgf("Assigning %s to %s", ip, machine.Name) Msgf("Assigning %s to %s", strings.Join(ips.ToStringSlice(), ","), machine.Name)
machine.Expiry = &registerRequest.Expiry machine.Expiry = &registerRequest.Expiry
machine.AuthKeyID = uint(pak.ID) machine.AuthKeyID = uint(pak.ID)
machine.IPAddress = ip.String() machine.IPAddresses = ips
machine.NamespaceID = pak.NamespaceID machine.NamespaceID = pak.NamespaceID
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey) machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
@@ -610,6 +611,6 @@ func (h *Headscale) handleAuthKey(
log.Info(). log.Info().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", machine.Name). Str("machine", machine.Name).
Str("ip", machine.IPAddress). Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey") Msg("Successfully authenticated via AuthKey")
} }

18
app.go
View File

@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@@ -68,7 +69,7 @@ type Config struct {
ServerURL string ServerURL string
Addr string Addr string
EphemeralNodeInactivityTimeout time.Duration EphemeralNodeInactivityTimeout time.Duration
IPPrefix netaddr.IPPrefix IPPrefixes []netaddr.IPPrefix
PrivateKeyPath string PrivateKeyPath string
BaseDomain string BaseDomain string
@@ -95,7 +96,8 @@ type Config struct {
DNSConfig *tailcfg.DNSConfig DNSConfig *tailcfg.DNSConfig
UnixSocket string UnixSocket string
UnixSocketPermission fs.FileMode
OIDC OIDCConfig OIDC OIDCConfig
@@ -197,9 +199,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
} }
if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS
magicDNSDomains := generateMagicDNSRootDomains( magicDNSDomains := generateMagicDNSRootDomains(app.cfg.IPPrefixes)
app.cfg.IPPrefix,
)
// we might have routes already from Split DNS // we might have routes already from Split DNS
if app.cfg.DNSConfig.Routes == nil { if app.cfg.DNSConfig.Routes == nil {
app.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver) app.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver)
@@ -426,6 +426,11 @@ func (h *Headscale) Serve() error {
return fmt.Errorf("failed to set up gRPC socket: %w", err) 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: // Handle common process-killing signals so we can gracefully shut down:
sigc := make(chan os.Signal, 1) sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
@@ -724,7 +729,8 @@ func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
return nil, fmt.Errorf("failed to read private key file: %w", err) return nil, fmt.Errorf("failed to read private key file: %w", err)
} }
privateKeyEnsurePrefix := PrivateKeyEnsurePrefix(string(privateKey)) trimmedPrivateKey := strings.TrimSpace(string(privateKey))
privateKeyEnsurePrefix := PrivateKeyEnsurePrefix(trimmedPrivateKey)
var machineKey key.MachinePrivate var machineKey key.MachinePrivate
if err = machineKey.UnmarshalText([]byte(privateKeyEnsurePrefix)); err != nil { if err = machineKey.UnmarshalText([]byte(privateKeyEnsurePrefix)); err != nil {

View File

@@ -41,7 +41,9 @@ func (s *Suite) ResetDB(c *check.C) {
c.Fatal(err) c.Fatal(err)
} }
cfg := Config{ cfg := Config{
IPPrefix: netaddr.MustParseIPPrefix("10.27.0.0/23"), IPPrefixes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.27.0.0/23"),
},
} }
app = Headscale{ app = Headscale{

View File

@@ -4,6 +4,7 @@ import (
"time" "time"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"inet.af/netaddr"
) )
func (s *Suite) TestRegisterMachine(c *check.C) { func (s *Suite) TestRegisterMachine(c *check.C) {
@@ -19,16 +20,17 @@ func (s *Suite) TestRegisterMachine(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
IPAddress: "10.0.0.1", IPAddresses: []netaddr.IP{netaddr.MustParseIP("10.0.0.1")},
Expiry: &now, Expiry: &now,
} }
app.db.Save(&machine) err = app.db.Save(&machine).Error
c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "testmachine") _, err = app.GetMachine(namespace.Name, machine.Name)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machineAfterRegistering, err := app.RegisterMachine( machineAfterRegistering, err := app.RegisterMachine(
"8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e", machine.MachineKey,
namespace.Name, namespace.Name,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

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

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"strconv" "strconv"
"strings"
"time" "time"
survey "github.com/AlecAivazis/survey/v2" survey "github.com/AlecAivazis/survey/v2"
@@ -459,7 +460,7 @@ func nodesToPtables(
"Name", "Name",
"NodeKey", "NodeKey",
"Namespace", "Namespace",
"IP address", "IP addresses",
"Ephemeral", "Ephemeral",
"Last seen", "Last seen",
"Online", "Online",
@@ -523,7 +524,7 @@ func nodesToPtables(
machine.Name, machine.Name,
nodeKey.ShortString(), nodeKey.ShortString(),
namespace, namespace,
machine.IpAddress, strings.Join(machine.IpAddresses, ", "),
strconv.FormatBool(ephemeral), strconv.FormatBool(ephemeral),
lastSeenTime, lastSeenTime,
online, online,

View File

@@ -5,10 +5,12 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
@@ -23,6 +25,10 @@ import (
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
) )
const (
PermissionFallback = 0o700
)
func LoadConfig(path string) error { func LoadConfig(path string) error {
viper.SetConfigName("config") viper.SetConfigName("config")
if path == "" { if path == "" {
@@ -41,13 +47,12 @@ func LoadConfig(path string) error {
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache") viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01") viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
viper.SetDefault("ip_prefix", "100.64.0.0/10")
viper.SetDefault("log_level", "info") viper.SetDefault("log_level", "info")
viper.SetDefault("dns_config", nil) viper.SetDefault("dns_config", nil)
viper.SetDefault("unix_socket", "/var/run/headscale.sock") viper.SetDefault("unix_socket", "/var/run/headscale.sock")
viper.SetDefault("unix_socket_permission", "0o770")
viper.SetDefault("cli.insecure", false) viper.SetDefault("cli.insecure", false)
viper.SetDefault("cli.timeout", "5s") viper.SetDefault("cli.timeout", "5s")
@@ -221,10 +226,57 @@ func getHeadscaleConfig() headscale.Config {
dnsConfig, baseDomain := GetDNSConfig() dnsConfig, baseDomain := GetDNSConfig()
derpConfig := GetDERPConfig() derpConfig := GetDERPConfig()
configuredPrefixes := viper.GetStringSlice("ip_prefixes")
parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1)
legacyPrefixField := viper.GetString("ip_prefix")
if len(legacyPrefixField) > 0 {
log.
Warn().
Msgf(
"%s, %s",
"use of 'ip_prefix' for configuration is deprecated",
"please see 'ip_prefixes' in the shipped example.",
)
legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField)
if err != nil {
panic(fmt.Errorf("failed to parse ip_prefix: %w", err))
}
parsedPrefixes = append(parsedPrefixes, legacyPrefix)
}
for i, prefixInConfig := range configuredPrefixes {
prefix, err := netaddr.ParseIPPrefix(prefixInConfig)
if err != nil {
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
}
parsedPrefixes = append(parsedPrefixes, prefix)
}
prefixes := make([]netaddr.IPPrefix, 0, len(parsedPrefixes))
{
// dedup
normalizedPrefixes := make(map[string]int, len(parsedPrefixes))
for i, p := range parsedPrefixes {
normalized, _ := p.Range().Prefix()
normalizedPrefixes[normalized.String()] = i
}
// convert back to list
for _, i := range normalizedPrefixes {
prefixes = append(prefixes, parsedPrefixes[i])
}
}
if len(prefixes) < 1 {
prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10"))
log.Warn().Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
}
return 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"),
IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")), IPPrefixes: prefixes,
PrivateKeyPath: absPath(viper.GetString("private_key_path")), PrivateKeyPath: absPath(viper.GetString("private_key_path")),
BaseDomain: baseDomain, BaseDomain: baseDomain,
@@ -257,7 +309,8 @@ func getHeadscaleConfig() headscale.Config {
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"), UnixSocket: viper.GetString("unix_socket"),
UnixSocketPermission: GetFileMode("unix_socket_permission"),
OIDC: headscale.OIDCConfig{ OIDC: headscale.OIDCConfig{
Issuer: viper.GetString("oidc.issuer"), Issuer: viper.GetString("oidc.issuer"),
@@ -448,3 +501,14 @@ func loadOIDCMatchMap() map[string]string {
return strMap 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,6 +1,7 @@
package main package main
import ( import (
"io/fs"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@@ -60,6 +61,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
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) {

View File

@@ -22,6 +22,13 @@ listen_addr: 0.0.0.0:8080
# autogenerated if it's missing # autogenerated if it's missing
private_key_path: /var/lib/headscale/private.key private_key_path: /var/lib/headscale/private.key
# List of IP prefixes to allocate tailaddresses from.
# Each prefix consists of either an IPv4 or IPv6 address,
# and the associated prefix length, delimited by a slash.
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
# DERP is a relay system that Tailscale uses when a direct # DERP is a relay system that Tailscale uses when a direct
# connection cannot be established. # connection cannot be established.
# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp # https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp
@@ -149,6 +156,7 @@ dns_config:
# Note: for local development, you probably want to change this to: # Note: for local development, you probably want to change this to:
# unix_socket: ./headscale.sock # unix_socket: ./headscale.sock
unix_socket: /var/run/headscale.sock unix_socket: /var/run/headscale.sock
unix_socket_permission: "0770"
# #
# headscale supports experimental OpenID connect support, # headscale supports experimental OpenID connect support,
# it is still being tested and might have some bugs, please # it is still being tested and might have some bugs, please

8
db.go
View File

@@ -28,20 +28,26 @@ 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";`)
} }
_ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")
err = db.AutoMigrate(&Machine{}) err = db.AutoMigrate(&Machine{})
if err != nil { if err != nil {
return err return err
} }
err = db.AutoMigrate(&KV{}) err = db.AutoMigrate(&KV{})
if err != nil { if err != nil {
return err return err
} }
err = db.AutoMigrate(&Namespace{}) err = db.AutoMigrate(&Namespace{})
if err != nil { if err != nil {
return err return err
} }
err = db.AutoMigrate(&PreAuthKey{}) err = db.AutoMigrate(&PreAuthKey{})
if err != nil { if err != nil {
return err return err

82
dns.go
View File

@@ -14,6 +14,11 @@ const (
ByteSize = 8 ByteSize = 8
) )
const (
ipv4AddressLength = 32
ipv6AddressLength = 128
)
// 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.
@@ -34,14 +39,28 @@ const (
// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask). // From the netmask we can find out the wildcard bits (the bits that are not set in the netmask).
// This allows us to then calculate the subnets included in the subsequent class block and generate the entries. // This allows us to then calculate the subnets included in the subsequent class block and generate the entries.
func generateMagicDNSRootDomains( func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN {
ipPrefix netaddr.IPPrefix, fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes))
) []dnsname.FQDN { for _, ipPrefix := range ipPrefixes {
// TODO(juanfont): we are not handing out IPv6 addresses yet var generateDNSRoot func(netaddr.IPPrefix) []dnsname.FQDN
// and in fact this is Tailscale.com's range (note the fd7a:115c:a1e0: range in the fc00::/7 network) switch ipPrefix.IP().BitLen() {
ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") case ipv4AddressLength:
fqdns := []dnsname.FQDN{ipv6base} generateDNSRoot = generateIPv4DNSRootDomain
case ipv6AddressLength:
generateDNSRoot = generateIPv6DNSRootDomain
default:
panic(fmt.Sprintf("unsupported IP version with address length %d", ipPrefix.IP().BitLen()))
}
fqdns = append(fqdns, generateDNSRoot(ipPrefix)...)
}
return fqdns
}
func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN {
// Conversion to the std lib net.IPnet, a bit easier to operate // Conversion to the std lib net.IPnet, a bit easier to operate
netRange := ipPrefix.IPNet() netRange := ipPrefix.IPNet()
maskBits, _ := netRange.Mask.Size() maskBits, _ := netRange.Mask.Size()
@@ -65,6 +84,7 @@ func generateMagicDNSRootDomains(
rdnsSlice = append(rdnsSlice, "in-addr.arpa.") rdnsSlice = append(rdnsSlice, "in-addr.arpa.")
rdnsBase := strings.Join(rdnsSlice, ".") rdnsBase := strings.Join(rdnsSlice, ".")
fqdns := make([]dnsname.FQDN, 0, max-min+1)
for i := min; i <= max; i++ { for i := min; i <= max; i++ {
fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.%s", i, rdnsBase)) fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.%s", i, rdnsBase))
if err != nil { if err != nil {
@@ -76,6 +96,54 @@ func generateMagicDNSRootDomains(
return fqdns return fqdns
} }
func generateIPv6DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN {
const nibbleLen = 4
maskBits, _ := ipPrefix.IPNet().Mask.Size()
expanded := ipPrefix.IP().StringExpanded()
nibbleStr := strings.Map(func(r rune) rune {
if r == ':' {
return -1
}
return r
}, expanded)
// TODO?: that does not look the most efficient implementation,
// but the inputs are not so long as to cause problems,
// and from what I can see, the generateMagicDNSRootDomains
// function is called only once over the lifetime of a server process.
prefixConstantParts := []string{}
for i := 0; i < maskBits/nibbleLen; i++ {
prefixConstantParts = append([]string{string(nibbleStr[i])}, prefixConstantParts...)
}
makeDomain := func(variablePrefix ...string) (dnsname.FQDN, error) {
prefix := strings.Join(append(variablePrefix, prefixConstantParts...), ".")
return dnsname.ToFQDN(fmt.Sprintf("%s.ip6.arpa", prefix))
}
var fqdns []dnsname.FQDN
if maskBits%4 == 0 {
dom, _ := makeDomain()
fqdns = append(fqdns, dom)
} else {
domCount := 1 << (maskBits % nibbleLen)
fqdns = make([]dnsname.FQDN, 0, domCount)
for i := 0; i < domCount; i++ {
varNibble := fmt.Sprintf("%x", i)
dom, err := makeDomain(varNibble)
if err != nil {
continue
}
fqdns = append(fqdns, dom)
}
}
return fqdns
}
func getMapResponseDNSConfig( func getMapResponseDNSConfig(
dnsConfigOrig *tailcfg.DNSConfig, dnsConfigOrig *tailcfg.DNSConfig,
baseDomain string, baseDomain string,

View File

@@ -10,8 +10,10 @@ import (
) )
func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
prefix := netaddr.MustParseIPPrefix("100.64.0.0/10") prefixes := []netaddr.IPPrefix{
domains := generateMagicDNSRootDomains(prefix) netaddr.MustParseIPPrefix("100.64.0.0/10"),
}
domains := generateMagicDNSRootDomains(prefixes)
found := false found := false
for _, domain := range domains { for _, domain := range domains {
@@ -45,8 +47,10 @@ 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") prefixes := []netaddr.IPPrefix{
domains := generateMagicDNSRootDomains(prefix) netaddr.MustParseIPPrefix("172.16.0.0/16"),
}
domains := generateMagicDNSRootDomains(prefixes)
found := false found := false
for _, domain := range domains { for _, domain := range domains {
@@ -69,6 +73,40 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
c.Assert(found, check.Equals, true) c.Assert(found, check.Equals, true)
} }
// Happens when netmask is a multiple of 4 bits (sounds likely).
func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) {
prefixes := []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48"),
}
domains := generateMagicDNSRootDomains(prefixes)
c.Assert(len(domains), check.Equals, 1)
c.Assert(domains[0].WithTrailingDot(), check.Equals, "0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.")
}
func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) {
prefixes := []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/50"),
}
domains := generateMagicDNSRootDomains(prefixes)
yieldsRoot := func(dom string) bool {
for _, candidate := range domains {
if candidate.WithTrailingDot() == dom {
return true
}
}
return false
}
c.Assert(len(domains), check.Equals, 4)
c.Assert(yieldsRoot("0.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true)
c.Assert(yieldsRoot("1.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true)
c.Assert(yieldsRoot("2.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true)
c.Assert(yieldsRoot("3.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true)
}
func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
namespaceShared1, err := app.CreateNamespace("shared1") namespaceShared1, err := app.CreateNamespace("shared1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -124,7 +162,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.1", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
app.db.Save(machineInShared1) app.db.Save(machineInShared1)
@@ -142,7 +180,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.2", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
app.db.Save(machineInShared2) app.db.Save(machineInShared2)
@@ -160,7 +198,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.3", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
app.db.Save(machineInShared3) app.db.Save(machineInShared3)
@@ -178,7 +216,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.4", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(PreAuthKey2InShared1.ID), AuthKeyID: uint(PreAuthKey2InShared1.ID),
} }
app.db.Save(machine2InShared1) app.db.Save(machine2InShared1)
@@ -273,7 +311,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.1", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
app.db.Save(machineInShared1) app.db.Save(machineInShared1)
@@ -291,7 +329,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.2", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
app.db.Save(machineInShared2) app.db.Save(machineInShared2)
@@ -309,7 +347,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.3", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
app.db.Save(machineInShared3) app.db.Save(machineInShared3)
@@ -327,7 +365,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.4", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(preAuthKey2InShared1.ID), AuthKeyID: uint(preAuthKey2InShared1.ID),
} }
app.db.Save(machine2InShared1) app.db.Save(machine2InShared1)

View File

@@ -21,7 +21,7 @@ wget --output-document=/usr/local/bin/headscale \
chmod +x /usr/local/bin/headscale chmod +x /usr/local/bin/headscale
``` ```
3. Prepare a direction to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database: 3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
```shell ```shell
# Directory for configuration # Directory for configuration
@@ -32,7 +32,7 @@ mkdir -p /etc/headscale
mkdir -p /var/lib/headscale mkdir -p /var/lib/headscale
``` ```
4. Create an empty SQlite datebase: 4. Create an empty SQLite database:
```shell ```shell
touch /var/lib/headscale/db.sqlite touch /var/lib/headscale/db.sqlite
@@ -44,7 +44,7 @@ touch /var/lib/headscale/db.sqlite
touch /etc/headscale/config.yaml touch /etc/headscale/config.yaml
``` ```
It is **strongly recommended** to copy and modifiy the [example configuration](../config-example.yaml) It is **strongly recommended** to copy and modify the [example configuration](../config-example.yaml)
from the [headscale repository](../) from the [headscale repository](../)
6. Start the headscale server: 6. Start the headscale server:
@@ -106,7 +106,7 @@ tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
## Running `headscale` in the background with SystemD ## Running `headscale` in the background with SystemD
In this section it will be demonstrated how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/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. This should work on most modern Linux distributions.
1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing: 1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing:

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.27.1
// protoc v3.18.1 // protoc v3.17.3
// source: headscale/v1/device.proto // source: headscale/v1/device.proto
package v1 package v1

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.27.1
// protoc v3.18.1 // protoc v3.17.3
// source: headscale/v1/headscale.proto // source: headscale/v1/headscale.proto
package v1 package v1

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.27.1
// protoc v3.18.1 // protoc v3.17.3
// source: headscale/v1/machine.proto // source: headscale/v1/machine.proto
package v1 package v1
@@ -82,7 +82,7 @@ type Machine struct {
MachineKey string `protobuf:"bytes,2,opt,name=machine_key,json=machineKey,proto3" json:"machine_key,omitempty"` MachineKey string `protobuf:"bytes,2,opt,name=machine_key,json=machineKey,proto3" json:"machine_key,omitempty"`
NodeKey string `protobuf:"bytes,3,opt,name=node_key,json=nodeKey,proto3" json:"node_key,omitempty"` NodeKey string `protobuf:"bytes,3,opt,name=node_key,json=nodeKey,proto3" json:"node_key,omitempty"`
DiscoKey string `protobuf:"bytes,4,opt,name=disco_key,json=discoKey,proto3" json:"disco_key,omitempty"` DiscoKey string `protobuf:"bytes,4,opt,name=disco_key,json=discoKey,proto3" json:"disco_key,omitempty"`
IpAddress string `protobuf:"bytes,5,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` IpAddresses []string `protobuf:"bytes,5,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"`
Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
Namespace *Namespace `protobuf:"bytes,7,opt,name=namespace,proto3" json:"namespace,omitempty"` Namespace *Namespace `protobuf:"bytes,7,opt,name=namespace,proto3" json:"namespace,omitempty"`
Registered bool `protobuf:"varint,8,opt,name=registered,proto3" json:"registered,omitempty"` Registered bool `protobuf:"varint,8,opt,name=registered,proto3" json:"registered,omitempty"`
@@ -154,11 +154,11 @@ func (x *Machine) GetDiscoKey() string {
return "" return ""
} }
func (x *Machine) GetIpAddress() string { func (x *Machine) GetIpAddresses() []string {
if x != nil { if x != nil {
return x.IpAddress return x.IpAddresses
} }
return "" return nil
} }
func (x *Machine) GetName() string { func (x *Machine) GetName() string {
@@ -1026,129 +1026,129 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 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, 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, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b,
0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf9, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfd, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04,
0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f,
0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6b, 0x65, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6b, 0x65,
0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x4b, 0x65, 0x79, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x4b, 0x65, 0x79,
0x12, 0x1b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a,
0x0a, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x05, 0x20,
0x09, 0x52, 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x12, 0x35, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x07, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63,
0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72,
0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x72, 0x65, 0x67, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52,
0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x72,
0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09,
0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x0e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68,
0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x37, 0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68,
0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x6f, 0x64, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18,
0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6c, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c,
0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75,
0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63,
0x66, 0x75, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a,
0x69, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x3a, 0x0a,
0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0d, 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, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65,
0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 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, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72,
0x65, 0x64, 0x41, 0x74, 0x22, 0x48, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x79, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a,
0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x4a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28,
0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 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, 0x48, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69,
0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 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, 0x4a, 0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a,
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15,
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x32,
0x0a, 0x11, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 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, 0x45, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x44, 0x65, 0x6c,
0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 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, 0x17, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x45, 0x78, 0x70,
0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 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, 0x48, 0x0a, 0x15, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61,
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x32, 0x0a, 0x11, 0x47, 0x65, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x33, 0x0a, 0x13, 0x4c, 0x69,
0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
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, 0x45,
0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 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, 0x17, 0x0a, 0x15,
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 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, 0x48, 0x0a, 0x15,
0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x33, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 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, 0x49, 0x0a, 0x14, 0x4c,
0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x08, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x52, 0x0a, 0x13, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 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, 0x12, 0x1c, 0x0a, 0x09,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x47, 0x0a, 0x14, 0x53, 0x68,
0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x22, 0x54, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 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, 0x12, 0x1c, 0x0a, 0x09, 0x6e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x55, 0x6e, 0x73,
0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x22, 0x77, 0x0a, 0x19, 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, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 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, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22,
0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x49, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52,
0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64,
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x4d, 0x0a, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x1a, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x52, 0x0a, 0x13, 0x53, 0x68,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18,
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64,
0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2a, 0x82, 0x01, 0x0a, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20,
0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x47,
0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x0a, 0x14, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
0x48, 0x4f, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x12, 0x17, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07,
0x0a, 0x13, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x54, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x68, 0x61,
0x44, 0x5f, 0x43, 0x4c, 0x49, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x45, 0x47, 0x49, 0x53, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f, 0x49, 0x44, 0x43, 0x10, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x03, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12,
0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01,
0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x49, 0x0a,
0x6f, 0x74, 0x6f, 0x33, 0x16, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52,
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x77, 0x0a, 0x19, 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, 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, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75,
0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65,
0x73, 0x22, 0x4d, 0x0a, 0x1a, 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, 0x12,
0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x2a, 0x82, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74,
0x68, 0x6f, 0x64, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f,
0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52,
0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x4b, 0x45, 0x59,
0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d,
0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x43, 0x4c, 0x49, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x52,
0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f,
0x49, 0x44, 0x43, 0x10, 0x03, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61,
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.27.1
// protoc v3.18.1 // protoc v3.17.3
// source: headscale/v1/namespace.proto // source: headscale/v1/namespace.proto
package v1 package v1

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.27.1
// protoc v3.18.1 // protoc v3.17.3
// source: headscale/v1/preauthkey.proto // source: headscale/v1/preauthkey.proto
package v1 package v1

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.27.1
// protoc v3.18.1 // protoc v3.17.3
// source: headscale/v1/routes.proto // source: headscale/v1/routes.proto
package v1 package v1

View File

@@ -775,8 +775,11 @@
"discoKey": { "discoKey": {
"type": "string" "type": "string"
}, },
"ipAddress": { "ipAddresses": {
"type": "string" "type": "array",
"items": {
"type": "string"
}
}, },
"name": { "name": {
"type": "string" "type": "string"

34
go.mod
View File

@@ -21,12 +21,12 @@ require (
github.com/rs/zerolog v1.26.0 github.com/rs/zerolog v1.26.0
github.com/soheilhy/cmux v0.1.5 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-20210923003652-c3758b31534b 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/zsais/go-gin-prometheus v0.1.0 github.com/zsais/go-gin-prometheus v0.1.0
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247 google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247
@@ -40,12 +40,12 @@ require (
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-20211027220019-c74959edd3b6 inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
tailscale.com v1.18.1 tailscale.com v1.20.3
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.0 // indirect github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/atomicgo/cursor v0.0.1 // indirect github.com/atomicgo/cursor v0.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
@@ -57,7 +57,7 @@ require (
github.com/docker/docker 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-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
@@ -92,30 +92,30 @@ require (
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.3 // indirect github.com/lib/pq v1.10.3 // indirect
github.com/magiconair/properties v1.8.5 // indirect github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.8 // indirect github.com/mattn/go-sqlite3 v1.14.8 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.0.2 // indirect github.com/opencontainers/runc v1.0.3 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.7.3 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.6.0 // indirect github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect
@@ -127,12 +127,12 @@ require (
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/net v0.0.0-20211205041911-012df41ee64c // indirect
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
) )

491
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -8,22 +8,48 @@ import (
"fmt" "fmt"
"time" "time"
"inet.af/netaddr"
"github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker" "github.com/ory/dockertest/v3/docker"
) )
const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
var IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
var IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
type ExecuteCommandConfig struct {
timeout time.Duration
}
type ExecuteCommandOption func(*ExecuteCommandConfig) error
func ExecuteCommandTimeout(timeout time.Duration) ExecuteCommandOption {
return ExecuteCommandOption(func(conf *ExecuteCommandConfig) error {
conf.timeout = timeout
return nil
})
}
func ExecuteCommand( func ExecuteCommand(
resource *dockertest.Resource, resource *dockertest.Resource,
cmd []string, cmd []string,
env []string, env []string,
options ...ExecuteCommandOption,
) (string, error) { ) (string, error) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
// TODO(kradalby): Make configurable execConfig := ExecuteCommandConfig{
timeout := DOCKER_EXECUTE_TIMEOUT timeout: DOCKER_EXECUTE_TIMEOUT,
}
for _, opt := range options {
if err := opt(&execConfig); err != nil {
return "", fmt.Errorf("execute-command/options: %w", err)
}
}
type result struct { type result struct {
exitCode int exitCode int
@@ -62,16 +88,33 @@ func ExecuteCommand(
} }
return stdout.String(), nil return stdout.String(), nil
case <-time.After(timeout): case <-time.After(execConfig.timeout):
return "", fmt.Errorf("command timed out after %s", timeout) return "", fmt.Errorf("command timed out after %s", execConfig.timeout)
} }
} }
func DockerRestartPolicy(config *docker.HostConfig) { func DockerRestartPolicy(config *docker.HostConfig) {
// set AutoRemove to true so that stopped container goes away by itself // set AutoRemove to true so that stopped container goes away by itself on error *immediately*.
config.AutoRemove = true // when set to false, containers remain until the end of the integration test.
config.AutoRemove = false
config.RestartPolicy = docker.RestartPolicy{ config.RestartPolicy = docker.RestartPolicy{
Name: "no", Name: "no",
} }
} }
func DockerAllowLocalIPv6(config *docker.HostConfig) {
if config.Sysctls == nil {
config.Sysctls = make(map[string]string, 1)
}
config.Sysctls["net.ipv6.conf.all.disable_ipv6"] = "0"
}
func DockerAllowNetworkAdministration(config *docker.HostConfig) {
config.CapAdd = append(config.CapAdd, "NET_ADMIN")
config.Mounts = append(config.Mounts, docker.HostMount{
Type: "bind",
Source: "/dev/net/tun",
Target: "/dev/net/tun",
})
}

View File

@@ -28,7 +28,7 @@ import (
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
) )
var tailscaleVersions = []string{"1.18.1", "1.16.2", "1.14.3", "1.12.3"} var tailscaleVersions = []string{"1.20.2", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
type TestNamespace struct { type TestNamespace struct {
count int count int
@@ -164,9 +164,7 @@ func (s *IntegrationTestSuite) tailscaleContainer(
Name: hostname, Name: hostname,
Networks: []*dockertest.Network{&s.network}, Networks: []*dockertest.Network{&s.network},
Cmd: []string{ Cmd: []string{
"tailscaled", "tailscaled", "--tun=tsdev",
"--tun=userspace-networking",
"--socks5-server=localhost:1055",
}, },
} }
@@ -174,6 +172,8 @@ func (s *IntegrationTestSuite) tailscaleContainer(
tailscaleBuildOptions, tailscaleBuildOptions,
tailscaleOptions, tailscaleOptions,
DockerRestartPolicy, DockerRestartPolicy,
DockerAllowLocalIPv6,
DockerAllowNetworkAdministration,
) )
if err != nil { if err != nil {
log.Fatalf("Could not start resource: %s", err) log.Fatalf("Could not start resource: %s", err)
@@ -372,70 +372,74 @@ func (s *IntegrationTestSuite) TestListNodes() {
func (s *IntegrationTestSuite) TestGetIpAddresses() { func (s *IntegrationTestSuite) TestGetIpAddresses() {
for _, scales := range s.namespaces { for _, scales := range s.namespaces {
ipPrefix := netaddr.MustParseIPPrefix("100.64.0.0/10")
ips, err := getIPs(scales.tailscales) ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for hostname := range scales.tailscales { for hostname, _ := range scales.tailscales {
s.T().Run(hostname, func(t *testing.T) { ips := ips[hostname]
ip, ok := ips[hostname] for _, ip := range ips {
s.T().Run(hostname, func(t *testing.T) {
assert.NotNil(t, ip)
assert.True(t, ok) fmt.Printf("IP for %s: %s\n", hostname, ip)
assert.NotNil(t, ip)
fmt.Printf("IP for %s: %s\n", hostname, ip) // c.Assert(ip.Valid(), check.IsTrue)
assert.True(t, ip.Is4() || ip.Is6())
// c.Assert(ip.Valid(), check.IsTrue) switch {
assert.True(t, ip.Is4()) case ip.Is4():
assert.True(t, ipPrefix.Contains(ip)) assert.True(t, IpPrefix4.Contains(ip))
}) case ip.Is6():
assert.True(t, IpPrefix6.Contains(ip))
}
})
}
} }
} }
} }
// TODO(kradalby): fix this test // TODO(kradalby): fix this test
// We need some way to impot ipnstate.Status from multiple go packages. // We need some way to import ipnstate.Status from multiple go packages.
// Currently it will only work with 1.18.x since that is the last // Currently it will only work with 1.18.x since that is the last
// version we have in go.mod // version we have in go.mod
// func (s *IntegrationTestSuite) TestStatus() { // func (s *IntegrationTestSuite) TestStatus() {
// for _, scales := range s.namespaces { // for _, scales := range s.namespaces {
// ips, err := getIPs(scales.tailscales) // ips, err := getIPs(scales.tailscales)
// assert.Nil(s.T(), err) // assert.Nil(s.T(), err)
// //
// for hostname, tailscale := range scales.tailscales { // for hostname, tailscale := range scales.tailscales {
// s.T().Run(hostname, func(t *testing.T) { // s.T().Run(hostname, func(t *testing.T) {
// command := []string{"tailscale", "status", "--json"} // command := []string{"tailscale", "status", "--json"}
// //
// fmt.Printf("Getting status for %s\n", hostname) // fmt.Printf("Getting status for %s\n", hostname)
// result, err := ExecuteCommand( // result, err := ExecuteCommand(
// &tailscale, // &tailscale,
// command, // command,
// []string{}, // []string{},
// ) // )
// assert.Nil(t, err) // assert.Nil(t, err)
// //
// var status ipnstate.Status // var status ipnstate.Status
// err = json.Unmarshal([]byte(result), &status) // err = json.Unmarshal([]byte(result), &status)
// assert.Nil(s.T(), err) // assert.Nil(s.T(), err)
// //
// // TODO(kradalby): Replace this check with peer length of SAME namespace // // TODO(kradalby): Replace this check with peer length of SAME namespace
// // Check if we have as many nodes in status // // Check if we have as many nodes in status
// // as we have IPs/tailscales // // as we have IPs/tailscales
// // lines := strings.Split(result, "\n") // // lines := strings.Split(result, "\n")
// // assert.Equal(t, len(ips), len(lines)-1) // // assert.Equal(t, len(ips), len(lines)-1)
// // assert.Equal(t, len(scales.tailscales), len(lines)-1) // // assert.Equal(t, len(scales.tailscales), len(lines)-1)
// //
// peerIps := getIPsfromIPNstate(status) // peerIps := getIPsfromIPNstate(status)
// //
// // Check that all hosts is present in all hosts status // // Check that all hosts is present in all hosts status
// for ipHostname, ip := range ips { // for ipHostname, ip := range ips {
// if hostname != ipHostname { // if hostname != ipHostname {
// assert.Contains(t, peerIps, ip) // assert.Contains(t, peerIps, ip)
// } // }
// } // }
// }) // })
// } // }
// } // }
// } // }
func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP { func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP {
@@ -448,16 +452,19 @@ func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP {
return ips return ips
} }
func (s *IntegrationTestSuite) TestPingAllPeers() { func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
for _, scales := range s.namespaces { for _, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales) ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for hostname, tailscale := range scales.tailscales { for hostname, tailscale := range scales.tailscales {
for peername, ip := range ips { for peername, peerIPs := range ips {
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { for i, ip := range peerIPs {
// We currently cant ping ourselves, so skip that. // We currently cant ping ourselves, so skip that.
if peername != hostname { if peername == hostname {
continue
}
s.T().Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) {
// We are only interested in "direct ping" which means what we // We are only interested in "direct ping" which means what we
// might need a couple of more attempts before reaching the node. // might need a couple of more attempts before reaching the node.
command := []string{ command := []string{
@@ -469,9 +476,8 @@ func (s *IntegrationTestSuite) TestPingAllPeers() {
} }
fmt.Printf( fmt.Printf(
"Pinging from %s (%s) to %s (%s)\n", "Pinging from %s to %s (%s)\n",
hostname, hostname,
ips[hostname],
peername, peername,
ip, ip,
) )
@@ -483,8 +489,8 @@ func (s *IntegrationTestSuite) TestPingAllPeers() {
assert.Nil(t, err) assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", hostname, result) fmt.Printf("Result for %s: %s\n", hostname, result)
assert.Contains(t, result, "pong") assert.Contains(t, result, "pong")
} })
}) }
} }
} }
} }
@@ -553,17 +559,17 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
// TODO(juanfont): We have to find out why do we need to wait // TODO(juanfont): We have to find out why do we need to wait
time.Sleep(100 * time.Second) // Wait for the nodes to receive updates time.Sleep(100 * time.Second) // Wait for the nodes to receive updates
mainIps, err := getIPs(main.tailscales)
assert.Nil(s.T(), err)
sharedIps, err := getIPs(shared.tailscales) sharedIps, err := getIPs(shared.tailscales)
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for hostname, tailscale := range main.tailscales { for hostname, tailscale := range main.tailscales {
for peername, ip := range sharedIps { for peername, peerIPs := range sharedIps {
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { for i, ip := range peerIPs {
// We currently cant ping ourselves, so skip that. // We currently cant ping ourselves, so skip that.
if peername != hostname { if peername == hostname {
continue
}
s.T().Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) {
// We are only interested in "direct ping" which means what we // We are only interested in "direct ping" which means what we
// might need a couple of more attempts before reaching the node. // might need a couple of more attempts before reaching the node.
command := []string{ command := []string{
@@ -575,9 +581,8 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
} }
fmt.Printf( fmt.Printf(
"Pinging from %s (%s) to %s (%s)\n", "Pinging from %s to %s (%s)\n",
hostname, hostname,
mainIps[hostname],
peername, peername,
ip, ip,
) )
@@ -589,8 +594,8 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
assert.Nil(t, err) assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", hostname, result) fmt.Printf("Result for %s: %s\n", hostname, result)
assert.Contains(t, result, "pong") assert.Contains(t, result, "pong")
} })
}) }
} }
} }
} }
@@ -599,9 +604,19 @@ func (s *IntegrationTestSuite) TestTailDrop() {
for _, scales := range s.namespaces { for _, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales) ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
apiURLs, err := getAPIURLs(scales.tailscales)
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
retry := func(times int, sleepInverval time.Duration, doWork func() error) (err error) {
for attempts := 0; attempts < times; attempts++ {
err = doWork()
if err == nil {
return
}
time.Sleep(sleepInverval)
}
return
}
for hostname, tailscale := range scales.tailscales { 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(
@@ -610,63 +625,31 @@ func (s *IntegrationTestSuite) TestTailDrop() {
[]string{}, []string{},
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for peername, ip := range ips { for peername, _ := range ips {
if peername == hostname {
continue
}
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 { command := []string{
// Under normal circumstances, we should be able to send a file "tailscale", "file", "cp",
// using `tailscale file cp` - but not in userspace networking mode fmt.Sprintf("/tmp/file_from_%s", hostname),
// So curl! fmt.Sprintf("%s:", peername),
peerAPI, ok := apiURLs[ip]
assert.True(t, ok)
// TODO(juanfont): We still have some issues with the test infrastructure, so
// lets run curl multiple times until it works.
attempts := 0
var err error
for {
command := []string{
"curl",
"--retry-connrefused",
"--retry-delay",
"30",
"--retry",
"10",
"--connect-timeout",
"60",
"-X",
"PUT",
"--upload-file",
fmt.Sprintf("/tmp/file_from_%s", 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,
)
_, err = ExecuteCommand(
&tailscale,
command,
[]string{"ALL_PROXY=socks5://localhost:1055"},
)
if err == nil {
break
} else {
time.Sleep(10 * time.Second)
attempts++
if attempts > 10 {
break
}
}
}
assert.Nil(t, err)
} }
retry(10, 1*time.Second, func() error {
fmt.Printf(
"Sending file from %s to %s\n",
hostname,
peername,
)
_, err := ExecuteCommand(
&tailscale,
command,
[]string{},
ExecuteCommandTimeout(60*time.Second),
)
return err
})
assert.Nil(t, err)
}) })
} }
} }
@@ -684,32 +667,70 @@ func (s *IntegrationTestSuite) TestTailDrop() {
) )
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for peername, ip := range ips { for peername, ip := range ips {
if peername == hostname {
continue
}
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 { command := []string{
command := []string{ "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,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", peername, result)
assert.Equal(
t,
result,
fmt.Sprintf("/tmp/file_from_%s\n", peername),
)
} }
fmt.Printf(
"Checking file in %s (%s) from %s (%s)\n",
hostname,
ips[hostname],
peername,
ip,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", peername, result)
assert.Equal(
t,
fmt.Sprintf("/tmp/file_from_%s\n", peername),
result,
)
})
}
}
}
}
func (s *IntegrationTestSuite) TestPingAllPeersByHostname() {
for namespace, scales := range s.namespaces {
ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err)
for hostname, tailscale := range scales.tailscales {
for peername, _ := range ips {
if peername == hostname {
continue
}
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
command := []string{
"tailscale", "ping",
"--timeout=10s",
"--c=20",
"--until-direct=true",
fmt.Sprintf("%s.%s.headscale.net", peername, namespace),
}
fmt.Printf(
"Pinging using hostname from %s to %s\n",
hostname,
peername,
)
result, err := ExecuteCommand(
&tailscale,
command,
[]string{},
)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", hostname, result)
assert.Contains(t, result, "pong")
}) })
} }
} }
@@ -721,32 +742,31 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
ips, err := getIPs(scales.tailscales) ips, err := getIPs(scales.tailscales)
assert.Nil(s.T(), err) assert.Nil(s.T(), err)
for hostname, tailscale := range scales.tailscales { for hostname, tailscale := range scales.tailscales {
for peername, ip := range ips { for peername, ips := range ips {
if peername == hostname {
continue
}
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 { command := []string{
command := []string{ "tailscale", "ip",
"tailscale", "ping", fmt.Sprintf("%s.%s.headscale.net", peername, namespace),
"--timeout=10s", }
"--c=20",
"--until-direct=true",
fmt.Sprintf("%s.%s.headscale.net", peername, namespace),
}
fmt.Printf( fmt.Printf(
"Pinging using Hostname (magicdns) from %s (%s) to %s (%s)\n", "Resolving name %s from %s\n",
hostname, peername,
ips[hostname], hostname,
peername, )
ip, result, err := ExecuteCommand(
) &tailscale,
result, err := ExecuteCommand( command,
&tailscale, []string{},
command, )
[]string{}, assert.Nil(t, err)
) fmt.Printf("Result for %s: %s\n", hostname, result)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", hostname, result) for _, ip := range ips {
assert.Contains(t, result, "pong") assert.Contains(t, result, ip.String())
} }
}) })
} }
@@ -754,8 +774,8 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
} }
} }
func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, error) { func getIPs(tailscales map[string]dockertest.Resource) (map[string][]netaddr.IP, error) {
ips := make(map[string]netaddr.IP) ips := make(map[string][]netaddr.IP)
for hostname, tailscale := range tailscales { for hostname, tailscale := range tailscales {
command := []string{"tailscale", "ip"} command := []string{"tailscale", "ip"}
@@ -768,12 +788,17 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e
return nil, err return nil, err
} }
ip, err := netaddr.ParseIP(strings.TrimSuffix(result, "\n")) for _, address := range strings.Split(result, "\n") {
if err != nil { address = strings.TrimSuffix(address, "\n")
return nil, err if len(address) < 1 {
continue
}
ip, err := netaddr.ParseIP(address)
if err != nil {
return nil, err
}
ips[hostname] = append(ips[hostname], ip)
} }
ips[hostname] = ip
} }
return ips, nil return ips, nil

View File

@@ -2,6 +2,9 @@ log_level: trace
acl_policy_path: "" acl_policy_path: ""
db_type: sqlite3 db_type: sqlite3
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10
dns_config: dns_config:
base_domain: headscale.net base_domain: headscale.net
magic_dns: true magic_dns: true

View File

@@ -1,6 +1,7 @@
package headscale package headscale
import ( import (
"database/sql/driver"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -23,6 +24,7 @@ const (
errMachineNotFound = Error("machine not found") errMachineNotFound = Error("machine not found")
errMachineAlreadyRegistered = Error("machine already registered") errMachineAlreadyRegistered = Error("machine already registered")
errMachineRouteIsNotAvailable = Error("route is not available on machine") errMachineRouteIsNotAvailable = Error("route is not available on machine")
errMachineAddressesInvalid = Error("failed to parse machine addresses")
) )
// Machine is a Headscale client. // Machine is a Headscale client.
@@ -31,7 +33,7 @@ type Machine struct {
MachineKey string `gorm:"type:varchar(64);unique_index"` MachineKey string `gorm:"type:varchar(64);unique_index"`
NodeKey string NodeKey string
DiscoKey string DiscoKey string
IPAddress string IPAddresses MachineAddresses
Name string Name string
NamespaceID uint NamespaceID uint
Namespace Namespace `gorm:"foreignKey:NamespaceID"` Namespace Namespace `gorm:"foreignKey:NamespaceID"`
@@ -64,6 +66,47 @@ func (machine Machine) isRegistered() bool {
return machine.Registered return machine.Registered
} }
type MachineAddresses []netaddr.IP
func (ma MachineAddresses) ToStringSlice() []string {
strSlice := make([]string, 0, len(ma))
for _, addr := range ma {
strSlice = append(strSlice, addr.String())
}
return strSlice
}
func (ma *MachineAddresses) Scan(destination interface{}) error {
switch value := destination.(type) {
case string:
addresses := strings.Split(value, ",")
*ma = (*ma)[:0]
for _, addr := range addresses {
if len(addr) < 1 {
continue
}
parsed, err := netaddr.ParseIP(addr)
if err != nil {
return err
}
*ma = append(*ma, parsed)
}
return nil
default:
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (ma MachineAddresses) Value() (driver.Value, error) {
addresses := strings.Join(ma.ToStringSlice(), ",")
return addresses, nil
}
// isExpired returns whether the machine registration has expired. // isExpired returns whether the machine registration has expired.
func (machine Machine) isExpired() bool { func (machine Machine) isExpired() bool {
// If Expiry is not set, the client has not indicated that // If Expiry is not set, the client has not indicated that
@@ -319,6 +362,14 @@ func (h *Headscale) DeleteMachine(machine *Machine) error {
return h.RequestMapUpdates(namespaceID) return h.RequestMapUpdates(namespaceID)
} }
func (h *Headscale) TouchMachine(machine *Machine) error {
return h.db.Updates(Machine{
ID: machine.ID,
LastSeen: machine.LastSeen,
LastSuccessfulUpdate: machine.LastSuccessfulUpdate,
}).Error
}
// HardDeleteMachine hard deletes a Machine from the database. // HardDeleteMachine hard deletes a Machine from the database.
func (h *Headscale) HardDeleteMachine(machine *Machine) error { func (h *Headscale) HardDeleteMachine(machine *Machine) error {
err := h.RemoveSharedMachineFromAllNamespaces(machine) err := h.RemoveSharedMachineFromAllNamespaces(machine)
@@ -377,14 +428,18 @@ func (h *Headscale) isOutdated(machine *Machine) bool {
} }
lastChange := h.getLastStateChange(namespaces...) lastChange := h.getLastStateChange(namespaces...)
lastUpdate := machine.CreatedAt
if machine.LastSuccessfulUpdate != nil {
lastUpdate = *machine.LastSuccessfulUpdate
}
log.Trace(). log.Trace().
Caller(). Caller().
Str("machine", machine.Name). Str("machine", machine.Name).
Time("last_successful_update", *machine.LastSuccessfulUpdate). Time("last_successful_update", lastChange).
Time("last_state_change", lastChange). Time("last_state_change", lastUpdate).
Msgf("Checking if %s is missing updates", machine.Name) Msgf("Checking if %s is missing updates", machine.Name)
return machine.LastSuccessfulUpdate.Before(lastChange) return lastUpdate.Before(lastChange)
} }
func (machine Machine) String() string { func (machine Machine) String() string {
@@ -470,22 +525,12 @@ func (machine Machine) toNode(
} }
addrs := []netaddr.IPPrefix{} addrs := []netaddr.IPPrefix{}
ip, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/32", machine.IPAddress)) for _, machineAddress := range machine.IPAddresses {
if err != nil { ip := netaddr.IPPrefixFrom(machineAddress, machineAddress.BitLen())
log.Trace(). addrs = append(addrs, ip)
Caller().
Str("ip", machine.IPAddress).
Msgf("Failed to parse IP Prefix from IP: %s", machine.IPAddress)
return nil, err
} }
addrs = append(addrs, ip) // missing the ipv6 ?
allowedIPs := []netaddr.IPPrefix{} allowedIPs := append([]netaddr.IPPrefix{}, addrs...) // we append the node own IP, as it is required by the clients
allowedIPs = append(
allowedIPs,
ip,
) // we append the node own IP, as it is required by the clients
if includeRoutes { if includeRoutes {
routesStr := []string{} routesStr := []string{}
@@ -592,11 +637,11 @@ func (machine *Machine) toProto() *v1.Machine {
Id: machine.ID, Id: machine.ID,
MachineKey: machine.MachineKey, MachineKey: machine.MachineKey,
NodeKey: machine.NodeKey, NodeKey: machine.NodeKey,
DiscoKey: machine.DiscoKey, DiscoKey: machine.DiscoKey,
IpAddress: machine.IPAddress, IpAddresses: machine.IPAddresses.ToStringSlice(),
Name: machine.Name, Name: machine.Name,
Namespace: machine.Namespace.toProto(), Namespace: machine.Namespace.toProto(),
Registered: machine.Registered, Registered: machine.Registered,
@@ -695,7 +740,7 @@ func (h *Headscale) RegisterMachine(
return nil, err return nil, err
} }
ip, err := h.getAvailableIP() ips, err := h.getAvailableIPs()
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@@ -709,10 +754,10 @@ func (h *Headscale) RegisterMachine(
log.Trace(). log.Trace().
Caller(). Caller().
Str("machine", machine.Name). Str("machine", machine.Name).
Str("ip", ip.String()). Str("ip", strings.Join(ips.ToStringSlice(), ",")).
Msg("Found IP for host") Msg("Found IP for host")
machine.IPAddress = ip.String() machine.IPAddresses = ips
machine.NamespaceID = namespace.ID machine.NamespaceID = namespace.ID
machine.Registered = true machine.Registered = true
machine.RegisterMethod = RegisterMethodCLI machine.RegisterMethod = RegisterMethodCLI
@@ -722,7 +767,7 @@ func (h *Headscale) RegisterMachine(
log.Trace(). log.Trace().
Caller(). Caller().
Str("machine", machine.Name). Str("machine", machine.Name).
Str("ip", ip.String()). Str("ip", strings.Join(ips.ToStringSlice(), ",")).
Msg("Machine registered with the database") Msg("Machine registered with the database")
return machine, nil return machine, nil

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"inet.af/netaddr"
) )
func (s *Suite) TestGetMachine(c *check.C) { func (s *Suite) TestGetMachine(c *check.C) {
@@ -199,3 +200,24 @@ func (s *Suite) TestExpireMachine(c *check.C) {
c.Assert(machineFromDB.isExpired(), check.Equals, true) c.Assert(machineFromDB.isExpired(), check.Equals, true)
} }
func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
input := MachineAddresses([]netaddr.IP{
netaddr.MustParseIP("192.0.2.1"),
netaddr.MustParseIP("2001:db8::1"),
})
serialized, err := input.Value()
c.Assert(err, check.IsNil)
if serial, ok := serialized.(string); ok {
c.Assert(serial, check.Equals, "192.0.2.1,2001:db8::1")
}
var deserialized MachineAddresses
err = deserialized.Scan(serialized)
c.Assert(err, check.IsNil)
c.Assert(len(deserialized), check.Equals, len(input))
for i := range deserialized {
c.Assert(deserialized[i], check.Equals, input[i])
}
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"gorm.io/gorm" "gorm.io/gorm"
"inet.af/netaddr"
) )
func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) { func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) {
@@ -146,7 +147,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.1", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyShared1.ID), AuthKeyID: uint(preAuthKeyShared1.ID),
} }
app.db.Save(machineInShared1) app.db.Save(machineInShared1)
@@ -164,7 +165,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.2", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyShared2.ID), AuthKeyID: uint(preAuthKeyShared2.ID),
} }
app.db.Save(machineInShared2) app.db.Save(machineInShared2)
@@ -182,7 +183,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.3", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyShared3.ID), AuthKeyID: uint(preAuthKeyShared3.ID),
} }
app.db.Save(machineInShared3) app.db.Save(machineInShared3)
@@ -200,7 +201,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.4", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(preAuthKey2Shared1.ID), AuthKeyID: uint(preAuthKey2Shared1.ID),
} }
app.db.Save(machine2InShared1) app.db.Save(machine2InShared1)

View File

@@ -126,6 +126,7 @@ var oidcCallbackTemplate = template.Must(
</html>`), </html>`),
) )
// TODO: Why is the entire machine registration logic duplicated here?
// OIDCCallback handles the callback from the OIDC endpoint // OIDCCallback handles the callback from the OIDC endpoint
// Retrieves the mkey from the state cache and adds the machine to the users email namespace // Retrieves the mkey from the state cache and adds the machine to the users email namespace
// TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities // TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities
@@ -316,7 +317,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return return
} }
ip, err := h.getAvailableIP() ips, err := h.getAvailableIPs()
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@@ -330,7 +331,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return return
} }
machine.IPAddress = ip.String() machine.IPAddresses = ips
machine.NamespaceID = namespace.ID machine.NamespaceID = namespace.ID
machine.Registered = true machine.Registered = true
machine.RegisterMethod = RegisterMethodOIDC machine.RegisterMethod = RegisterMethodOIDC

191
poll.go
View File

@@ -1,8 +1,10 @@
package headscale package headscale
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"time" "time"
@@ -74,6 +76,8 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String()) Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String())
ctx.String(http.StatusInternalServerError, "") ctx.String(http.StatusInternalServerError, "")
return
} }
log.Trace(). log.Trace().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
@@ -100,7 +104,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
machine.Endpoints = datatypes.JSON(endpoints) machine.Endpoints = datatypes.JSON(endpoints)
machine.LastSeen = &now machine.LastSeen = &now
} }
h.db.Save(&machine) h.db.Updates(machine)
data, err := h.getMapResponse(machineKey, req, machine) data, err := h.getMapResponse(machineKey, req, machine)
if err != nil { if err != nil {
@@ -152,14 +156,33 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
Str("id", ctx.Param("id")). Str("id", ctx.Param("id")).
Str("machine", machine.Name). Str("machine", machine.Name).
Msg("Loading or creating update channel") Msg("Loading or creating update channel")
updateChan := make(chan struct{})
pollDataChan := make(chan []byte) // TODO: could probably remove all that duplication once generics land.
closeChanWithLog := func(channel interface{}, name string) {
log.Trace().
Str("handler", "PollNetMap").
Str("machine", machine.Name).
Str("channel", "Done").
Msg(fmt.Sprintf("Closing %s channel", name))
switch c := channel.(type) {
case (chan struct{}):
close(c)
case (chan []byte):
close(c)
}
}
const chanSize = 8
updateChan := make(chan struct{}, chanSize)
defer closeChanWithLog(updateChan, "updateChan")
pollDataChan := make(chan []byte, chanSize)
defer closeChanWithLog(pollDataChan, "pollDataChan")
keepAliveChan := make(chan []byte) keepAliveChan := make(chan []byte)
defer closeChanWithLog(keepAliveChan, "keepAliveChan")
cancelKeepAlive := make(chan struct{})
defer close(cancelKeepAlive)
if req.OmitPeers && !req.Stream { if req.OmitPeers && !req.Stream {
log.Info(). log.Info().
@@ -172,7 +195,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
// even tho the comments in the tailscale code dont explicitly say so. // even tho the comments in the tailscale code dont explicitly say so.
updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "endpoint-update"). updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "endpoint-update").
Inc() Inc()
go func() { updateChan <- struct{}{} }() updateChan <- struct{}{}
return return
} else if req.OmitPeers && req.Stream { } else if req.OmitPeers && req.Stream {
@@ -193,7 +216,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
Str("machine", machine.Name). Str("machine", machine.Name).
Msg("Sending initial map") Msg("Sending initial map")
go func() { pollDataChan <- data }() pollDataChan <- data
log.Info(). log.Info().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
@@ -201,7 +224,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
Msg("Notifying peers") Msg("Notifying peers")
updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "full-update"). updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "full-update").
Inc() Inc()
go func() { updateChan <- struct{}{} }() updateChan <- struct{}{}
h.PollNetMapStream( h.PollNetMapStream(
ctx, ctx,
@@ -211,7 +234,6 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
pollDataChan, pollDataChan,
keepAliveChan, keepAliveChan,
updateChan, updateChan,
cancelKeepAlive,
) )
log.Trace(). log.Trace().
Str("handler", "PollNetMap"). Str("handler", "PollNetMap").
@@ -231,16 +253,20 @@ func (h *Headscale) PollNetMapStream(
pollDataChan chan []byte, pollDataChan chan []byte,
keepAliveChan chan []byte, keepAliveChan chan []byte,
updateChan chan struct{}, updateChan chan struct{},
cancelKeepAlive chan struct{},
) { ) {
go h.scheduledPollWorker( {
cancelKeepAlive, ctx, cancel := context.WithCancel(ctx.Request.Context())
updateChan, defer cancel()
keepAliveChan,
machineKey, go h.scheduledPollWorker(
mapRequest, ctx,
machine, updateChan,
) keepAliveChan,
machineKey,
mapRequest,
machine,
)
}
ctx.Stream(func(writer io.Writer) bool { ctx.Stream(func(writer io.Writer) bool {
log.Trace(). log.Trace().
@@ -289,6 +315,10 @@ func (h *Headscale) PollNetMapStream(
Str("channel", "pollData"). Str("channel", "pollData").
Err(err). Err(err).
Msg("Cannot update machine from database") Msg("Cannot update machine from database")
// client has been removed from database
// since the stream opened, terminate connection.
return false
} }
now := time.Now().UTC() now := time.Now().UTC()
machine.LastSeen = &now machine.LastSeen = &now
@@ -297,13 +327,22 @@ func (h *Headscale) PollNetMapStream(
Set(float64(now.Unix())) Set(float64(now.Unix()))
machine.LastSuccessfulUpdate = &now machine.LastSuccessfulUpdate = &now
h.db.Save(&machine) err = h.TouchMachine(machine)
log.Trace(). if err != nil {
Str("handler", "PollNetMapStream"). log.Error().
Str("machine", machine.Name). Str("handler", "PollNetMapStream").
Str("channel", "pollData"). Str("machine", machine.Name).
Int("bytes", len(data)). Str("channel", "pollData").
Msg("Machine entry in database updated successfully after sending pollData") Err(err).
Msg("Cannot update machine LastSuccessfulUpdate")
} else {
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Name).
Str("channel", "pollData").
Int("bytes", len(data)).
Msg("Machine entry in database updated successfully after sending pollData")
}
return true return true
@@ -342,16 +381,29 @@ func (h *Headscale) PollNetMapStream(
Str("channel", "keepAlive"). Str("channel", "keepAlive").
Err(err). Err(err).
Msg("Cannot update machine from database") Msg("Cannot update machine from database")
// client has been removed from database
// since the stream opened, terminate connection.
return false
} }
now := time.Now().UTC() now := time.Now().UTC()
machine.LastSeen = &now machine.LastSeen = &now
h.db.Save(&machine) err = h.TouchMachine(machine)
log.Trace(). if err != nil {
Str("handler", "PollNetMapStream"). log.Error().
Str("machine", machine.Name). Str("handler", "PollNetMapStream").
Str("channel", "keepAlive"). Str("machine", machine.Name).
Int("bytes", len(data)). Str("channel", "keepAlive").
Msg("Machine updated successfully after sending keep alive") Err(err).
Msg("Cannot update machine LastSeen")
} else {
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Name).
Str("channel", "keepAlive").
Int("bytes", len(data)).
Msg("Machine updated successfully after sending keep alive")
}
return true return true
@@ -364,10 +416,14 @@ func (h *Headscale) PollNetMapStream(
updateRequestsReceivedOnChannel.WithLabelValues(machine.Name, machine.Namespace.Name). updateRequestsReceivedOnChannel.WithLabelValues(machine.Name, machine.Namespace.Name).
Inc() Inc()
if h.isOutdated(machine) { if h.isOutdated(machine) {
var lastUpdate time.Time
if machine.LastSuccessfulUpdate != nil {
lastUpdate = *machine.LastSuccessfulUpdate
}
log.Debug(). log.Debug().
Str("handler", "PollNetMapStream"). Str("handler", "PollNetMapStream").
Str("machine", machine.Name). Str("machine", machine.Name).
Time("last_successful_update", *machine.LastSuccessfulUpdate). Time("last_successful_update", lastUpdate).
Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
Msgf("There has been updates since the last successful update to %s", machine.Name) Msgf("There has been updates since the last successful update to %s", machine.Name)
data, err := h.getMapResponse(machineKey, mapRequest, machine) data, err := h.getMapResponse(machineKey, mapRequest, machine)
@@ -415,6 +471,10 @@ func (h *Headscale) PollNetMapStream(
Str("channel", "update"). Str("channel", "update").
Err(err). Err(err).
Msg("Cannot update machine from database") Msg("Cannot update machine from database")
// client has been removed from database
// since the stream opened, terminate connection.
return false
} }
now := time.Now().UTC() now := time.Now().UTC()
@@ -422,12 +482,24 @@ func (h *Headscale) PollNetMapStream(
Set(float64(now.Unix())) Set(float64(now.Unix()))
machine.LastSuccessfulUpdate = &now machine.LastSuccessfulUpdate = &now
h.db.Save(&machine) err = h.TouchMachine(machine)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
Str("machine", machine.Name).
Str("channel", "update").
Err(err).
Msg("Cannot update machine LastSuccessfulUpdate")
}
} else { } else {
var lastUpdate time.Time
if machine.LastSuccessfulUpdate != nil {
lastUpdate = *machine.LastSuccessfulUpdate
}
log.Trace(). log.Trace().
Str("handler", "PollNetMapStream"). Str("handler", "PollNetMapStream").
Str("machine", machine.Name). Str("machine", machine.Name).
Time("last_successful_update", *machine.LastSuccessfulUpdate). Time("last_successful_update", lastUpdate).
Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
Msgf("%s is up to date", machine.Name) Msgf("%s is up to date", machine.Name)
} }
@@ -450,39 +522,22 @@ func (h *Headscale) PollNetMapStream(
Str("channel", "Done"). Str("channel", "Done").
Err(err). Err(err).
Msg("Cannot update machine from database") Msg("Cannot update machine from database")
// client has been removed from database
// since the stream opened, terminate connection.
return false
} }
now := time.Now().UTC() now := time.Now().UTC()
machine.LastSeen = &now machine.LastSeen = &now
h.db.Save(&machine) err = h.TouchMachine(machine)
if err != nil {
log.Trace(). log.Error().
Str("handler", "PollNetMapStream"). Str("handler", "PollNetMapStream").
Str("machine", machine.Name). Str("machine", machine.Name).
Str("channel", "Done"). Str("channel", "Done").
Msg("Cancelling keepAlive channel") Err(err).
cancelKeepAlive <- struct{}{} Msg("Cannot update machine LastSeen")
}
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Name).
Str("channel", "Done").
Msg("Closing update channel")
// h.closeUpdateChannel(m)
close(updateChan)
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Name).
Str("channel", "Done").
Msg("Closing pollData channel")
close(pollDataChan)
log.Trace().
Str("handler", "PollNetMapStream").
Str("machine", machine.Name).
Str("channel", "Done").
Msg("Closing keepAliveChan channel")
close(keepAliveChan)
return false return false
} }
@@ -490,7 +545,7 @@ func (h *Headscale) PollNetMapStream(
} }
func (h *Headscale) scheduledPollWorker( func (h *Headscale) scheduledPollWorker(
cancelChan <-chan struct{}, ctx context.Context,
updateChan chan<- struct{}, updateChan chan<- struct{},
keepAliveChan chan<- []byte, keepAliveChan chan<- []byte,
machineKey key.MachinePublic, machineKey key.MachinePublic,
@@ -502,7 +557,7 @@ func (h *Headscale) scheduledPollWorker(
for { for {
select { select {
case <-cancelChan: case <-ctx.Done():
return return
case <-keepAliveTicker.C: case <-keepAliveTicker.C:

View File

@@ -18,7 +18,7 @@ message Machine {
string machine_key = 2; string machine_key = 2;
string node_key = 3; string node_key = 3;
string disco_key = 4; string disco_key = 4;
string ip_address = 5; repeated string ip_addresses = 5;
string name = 6; string name = 6;
Namespace namespace = 7; Namespace namespace = 7;

View File

@@ -2,6 +2,7 @@ package headscale
import ( import (
"gopkg.in/check.v1" "gopkg.in/check.v1"
"inet.af/netaddr"
) )
func CreateNodeNamespace( func CreateNodeNamespace(
@@ -26,7 +27,7 @@ func CreateNodeNamespace(
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: ip, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(pak1.ID), AuthKeyID: uint(pak1.ID),
} }
app.db.Save(machine) app.db.Save(machine)
@@ -214,7 +215,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) {
NamespaceID: namespace1.ID, NamespaceID: namespace1.ID,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.4", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(pak4.ID), AuthKeyID: uint(pak4.ID),
} }
app.db.Save(machine4) app.db.Save(machine4)
@@ -294,7 +295,7 @@ func (s *Suite) TestDeleteSharedMachine(c *check.C) {
NamespaceID: namespace1.ID, NamespaceID: namespace1.ID,
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddress: "100.64.0.4", IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(pak4n1.ID), AuthKeyID: uint(pak4n1.ID),
} }
app.db.Save(machine4) app.db.Save(machine4)

View File

@@ -133,61 +133,78 @@ func encode(
return privKey.SealTo(*pubKey, b), nil return privKey.SealTo(*pubKey, b), nil
} }
func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { func (h *Headscale) getAvailableIPs() (ips MachineAddresses, err error) {
ipPrefix := h.cfg.IPPrefix ipPrefixes := h.cfg.IPPrefixes
for _, ipPrefix := range ipPrefixes {
var ip *netaddr.IP
ip, err = h.getAvailableIP(ipPrefix)
if err != nil {
return
}
ips = append(ips, *ip)
}
return
}
func GetIPPrefixEndpoints(na netaddr.IPPrefix) (network, broadcast netaddr.IP) {
ipRange := na.Range()
network = ipRange.From()
broadcast = ipRange.To()
return
}
// TODO: Is this concurrency safe?
// What would happen if multiple hosts were to register at the same time?
// Would we attempt to assign the same addresses to multiple nodes?
func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, error) {
usedIps, err := h.getUsedIPs() usedIps, err := h.getUsedIPs()
if err != nil { if err != nil {
return nil, err return nil, err
} }
ipPrefixNetworkAddress, ipPrefixBroadcastAddress := GetIPPrefixEndpoints(ipPrefix)
// Get the first IP in our prefix // Get the first IP in our prefix
ip := ipPrefix.IP() ip := ipPrefixNetworkAddress.Next()
for { for {
if !ipPrefix.Contains(ip) { if !ipPrefix.Contains(ip) {
return nil, errCouldNotAllocateIP return nil, errCouldNotAllocateIP
} }
// Some OS (including Linux) does not like when IPs ends with 0 or 255, which switch {
// is typically called network or broadcast. Lets avoid them and continue case ip.Compare(ipPrefixBroadcastAddress) == 0:
// to look when we get one of those traditionally reserved IPs. fallthrough
ipRaw := ip.As4() case containsIPs(usedIps, ip):
if ipRaw[3] == 0 || ipRaw[3] == 255 { fallthrough
case ip.IsZero() || ip.IsLoopback():
ip = ip.Next() ip = ip.Next()
continue continue
}
if ip.IsZero() && default:
ip.IsLoopback() {
ip = ip.Next()
continue
}
if !containsIPs(usedIps, ip) {
return &ip, nil return &ip, nil
} }
ip = ip.Next()
} }
} }
func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) { func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
var addresses []string // FIXME: This really deserves a better data model,
h.db.Model(&Machine{}).Pluck("ip_address", &addresses) // but this was quick to get running and it should be enough
// to begin experimenting with a dual stack tailnet.
var addressesSlices []string
h.db.Model(&Machine{}).Pluck("ip_addresses", &addressesSlices)
ips := make([]netaddr.IP, len(addresses)) ips := make([]netaddr.IP, 0, len(h.cfg.IPPrefixes)*len(addressesSlices))
for index, addr := range addresses { for _, slice := range addressesSlices {
if addr != "" { var a MachineAddresses
ip, err := netaddr.ParseIP(addr) err := a.Scan(slice)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse ip from database: %w", err) return nil, fmt.Errorf("failed to read ip from database: %w", err)
}
ips[index] = ip
} }
ips = append(ips, a...)
} }
return ips, nil return ips, nil

View File

@@ -6,17 +6,18 @@ import (
) )
func (s *Suite) TestGetAvailableIp(c *check.C) { func (s *Suite) TestGetAvailableIp(c *check.C) {
ip, err := app.getAvailableIP() ips, err := app.getAvailableIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
expected := netaddr.MustParseIP("10.27.0.1") expected := netaddr.MustParseIP("10.27.0.1")
c.Assert(ip.String(), check.Equals, expected.String()) c.Assert(len(ips), check.Equals, 1)
c.Assert(ips[0].String(), check.Equals, expected.String())
} }
func (s *Suite) TestGetUsedIps(c *check.C) { func (s *Suite) TestGetUsedIps(c *check.C) {
ip, err := app.getAvailableIP() ips, err := app.getAvailableIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
namespace, err := app.CreateNamespace("test_ip") namespace, err := app.CreateNamespace("test_ip")
@@ -38,22 +39,24 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
IPAddress: ip.String(), IPAddresses: ips,
} }
app.db.Save(&machine) app.db.Save(&machine)
ips, err := app.getUsedIPs() usedIps, err := app.getUsedIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
expected := netaddr.MustParseIP("10.27.0.1") expected := netaddr.MustParseIP("10.27.0.1")
c.Assert(ips[0], check.Equals, expected) c.Assert(len(usedIps), check.Equals, 1)
c.Assert(usedIps[0], check.Equals, expected)
machine1, err := app.GetMachineByID(0) machine1, err := app.GetMachineByID(0)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(machine1.IPAddress, check.Equals, expected.String()) c.Assert(len(machine1.IPAddresses), check.Equals, 1)
c.Assert(machine1.IPAddresses[0], check.Equals, expected)
} }
func (s *Suite) TestGetMultiIp(c *check.C) { func (s *Suite) TestGetMultiIp(c *check.C) {
@@ -61,7 +64,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
for index := 1; index <= 350; index++ { for index := 1; index <= 350; index++ {
ip, err := app.getAvailableIP() ips, err := app.getAvailableIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
@@ -80,59 +83,64 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
Registered: true, Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
IPAddress: ip.String(), IPAddresses: ips,
} }
app.db.Save(&machine) app.db.Save(&machine)
} }
ips, err := app.getUsedIPs() usedIps, err := app.getUsedIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(len(ips), check.Equals, 350) c.Assert(len(usedIps), check.Equals, 350)
c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.1")) c.Assert(usedIps[0], check.Equals, netaddr.MustParseIP("10.27.0.1"))
c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.10")) c.Assert(usedIps[9], check.Equals, netaddr.MustParseIP("10.27.0.10"))
c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.47")) c.Assert(usedIps[300], check.Equals, netaddr.MustParseIP("10.27.1.45"))
// Check that we can read back the IPs // Check that we can read back the IPs
machine1, err := app.GetMachineByID(1) machine1, err := app.GetMachineByID(1)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(len(machine1.IPAddresses), check.Equals, 1)
c.Assert( c.Assert(
machine1.IPAddress, machine1.IPAddresses[0],
check.Equals, check.Equals,
netaddr.MustParseIP("10.27.0.1").String(), netaddr.MustParseIP("10.27.0.1"),
) )
machine50, err := app.GetMachineByID(50) machine50, err := app.GetMachineByID(50)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(len(machine50.IPAddresses), check.Equals, 1)
c.Assert( c.Assert(
machine50.IPAddress, machine50.IPAddresses[0],
check.Equals, check.Equals,
netaddr.MustParseIP("10.27.0.50").String(), netaddr.MustParseIP("10.27.0.50"),
) )
expectedNextIP := netaddr.MustParseIP("10.27.1.97") expectedNextIP := netaddr.MustParseIP("10.27.1.95")
nextIP, err := app.getAvailableIP() nextIP, err := app.getAvailableIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(nextIP.String(), check.Equals, expectedNextIP.String()) c.Assert(len(nextIP), check.Equals, 1)
c.Assert(nextIP[0].String(), check.Equals, expectedNextIP.String())
// If we call get Available again, we should receive // If we call get Available again, we should receive
// the same IP, as it has not been reserved. // the same IP, as it has not been reserved.
nextIP2, err := app.getAvailableIP() nextIP2, err := app.getAvailableIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(nextIP2.String(), check.Equals, expectedNextIP.String()) c.Assert(len(nextIP2), check.Equals, 1)
c.Assert(nextIP2[0].String(), check.Equals, expectedNextIP.String())
} }
func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) { func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
ip, err := app.getAvailableIP() ips, err := app.getAvailableIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
expected := netaddr.MustParseIP("10.27.0.1") expected := netaddr.MustParseIP("10.27.0.1")
c.Assert(ip.String(), check.Equals, expected.String()) c.Assert(len(ips), check.Equals, 1)
c.Assert(ips[0].String(), check.Equals, expected.String())
namespace, err := app.CreateNamespace("test_ip") namespace, err := app.CreateNamespace("test_ip")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -156,8 +164,9 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
} }
app.db.Save(&machine) app.db.Save(&machine)
ip2, err := app.getAvailableIP() ips2, err := app.getAvailableIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(ip2.String(), check.Equals, expected.String()) c.Assert(len(ips2), check.Equals, 1)
c.Assert(ips2[0].String(), check.Equals, expected.String())
} }