Merge branch 'main' into docs-acl-modifications
This commit is contained in:
commit
dd219d0ff6
85 changed files with 5041 additions and 3551 deletions
|
@ -3,6 +3,7 @@
|
|||
// development
|
||||
integration_test.go
|
||||
integration_test/
|
||||
!integration_test/etc_embedded_derp/tls/server.crt
|
||||
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -7,3 +7,4 @@ Dockerfile* @ohdearaugustin
|
|||
.goreleaser.yaml @ohdearaugustin
|
||||
/docs/ @ohdearaugustin
|
||||
/.github/workflows/ @ohdearaugustin
|
||||
/.github/renovate.json @ohdearaugustin
|
||||
|
|
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
|
@ -1,10 +1,10 @@
|
|||
<!-- Please tick if the following things apply. You… -->
|
||||
|
||||
- [] read the [CONTRIBUTING guidelines](README.md#user-content-contributing)
|
||||
- [] raised a GitHub issue or discussed it on the projects chat beforehand
|
||||
- [] added unit tests
|
||||
- [] added integration tests
|
||||
- [] updated documentation if needed
|
||||
- [] updated CHANGELOG.md
|
||||
- [ ] read the [CONTRIBUTING guidelines](README.md#user-content-contributing)
|
||||
- [ ] raised a GitHub issue or discussed it on the projects chat beforehand
|
||||
- [ ] added unit tests
|
||||
- [ ] added integration tests
|
||||
- [ ] updated documentation if needed
|
||||
- [ ] updated CHANGELOG.md
|
||||
|
||||
<!-- If applicable, please reference the issue using `Fixes #XXX` and add tests to cover your new code. -->
|
||||
|
|
38
.github/renovate.json
vendored
Normal file
38
.github/renovate.json
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"baseBranches": ["main"],
|
||||
"username": "renovate-release",
|
||||
"gitAuthor": "Renovate Bot <bot@renovateapp.com>",
|
||||
"branchPrefix": "renovateaction/",
|
||||
"onboarding": false,
|
||||
"extends": ["config:base", ":rebaseStalePrs"],
|
||||
"ignorePresets": [":prHourlyLimit2"],
|
||||
"enabledManagers": ["dockerfile", "gomod", "github-actions","regex" ],
|
||||
"includeForks": true,
|
||||
"repositories": ["juanfont/headscale"],
|
||||
"platform": "github",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDatasources": ["go"],
|
||||
"groupName": "Go modules",
|
||||
"groupSlug": "gomod",
|
||||
"separateMajorMinor": false
|
||||
},
|
||||
{
|
||||
"matchDatasources": ["docker"],
|
||||
"groupName": "Dockerfiles",
|
||||
"groupSlug": "dockerfiles"
|
||||
}
|
||||
],
|
||||
"regexManagers": [
|
||||
{
|
||||
"fileMatch": [
|
||||
".github/workflows/.*.yml$"
|
||||
],
|
||||
"matchStrings": [
|
||||
"\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"
|
||||
],
|
||||
"datasourceTemplate": "golang-version",
|
||||
"depNameTemplate": "actions/go-version"
|
||||
}
|
||||
]
|
||||
}
|
15
.github/workflows/contributors.yml
vendored
15
.github/workflows/contributors.yml
vendored
|
@ -4,13 +4,24 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
add-contributors:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: BobAnkh/add-contributors@master
|
||||
- name: Delete upstream contributor branch
|
||||
# Allow continue on failure to account for when the
|
||||
# upstream branch is deleted or does not exist.
|
||||
continue-on-error: true
|
||||
run: git push origin --delete update-contributors
|
||||
- name: Create up-to-date contributors branch
|
||||
run: git checkout -B update-contributors
|
||||
- name: Push empty contributors branch
|
||||
run: git push origin update-contributors
|
||||
- name: Switch back to main
|
||||
run: git checkout main
|
||||
- uses: BobAnkh/add-contributors@v0.2.2
|
||||
with:
|
||||
CONTRIBUTOR: "## Contributors"
|
||||
COLUMN_PER_ROW: "6"
|
||||
|
|
27
.github/workflows/renovatebot.yml
vendored
Normal file
27
.github/workflows/renovatebot.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: Renovate
|
||||
on:
|
||||
schedule:
|
||||
- cron: "* * 5,20 * *" # Every 5th and 20th of the month
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get token
|
||||
id: get_token
|
||||
uses: machine-learning-apps/actions-app-token@master
|
||||
with:
|
||||
APP_PEM: ${{ secrets.RENOVATEBOT_SECRET }}
|
||||
APP_ID: ${{ secrets.RENOVATEBOT_APP_ID }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.0.0
|
||||
|
||||
- name: Self-hosted Renovate
|
||||
uses: renovatebot/github-action@v31.81.3
|
||||
with:
|
||||
configurationFile: .github/renovate.json
|
||||
token: "x-access-token:${{ steps.get_token.outputs.app_token }}"
|
||||
# env:
|
||||
# LOG_LEVEL: "debug"
|
2
.github/workflows/test-integration.yml
vendored
2
.github/workflows/test-integration.yml
vendored
|
@ -29,4 +29,4 @@ jobs:
|
|||
|
||||
- name: Run Integration tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: go test -tags integration -timeout 30m
|
||||
run: make test_integration
|
||||
|
|
|
@ -29,6 +29,7 @@ linters:
|
|||
- wrapcheck
|
||||
- dupl
|
||||
- makezero
|
||||
- maintidx
|
||||
|
||||
# We might want to enable this, but it might be a lot of work
|
||||
- cyclop
|
||||
|
@ -48,6 +49,7 @@ linters-settings:
|
|||
- ip
|
||||
- ok
|
||||
- c
|
||||
- tt
|
||||
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
|
|
|
@ -10,15 +10,12 @@ builds:
|
|||
- id: darwin-amd64
|
||||
main: ./cmd/headscale/headscale.go
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
env:
|
||||
- PKG_CONFIG_SYSROOT_DIR=/sysroot/macos/amd64
|
||||
- PKG_CONFIG_PATH=/sysroot/macos/amd64/usr/local/lib/pkgconfig
|
||||
- CC=o64-clang
|
||||
- CXX=o64-clang++
|
||||
flags:
|
||||
- -mod=readonly
|
||||
ldflags:
|
||||
|
@ -27,46 +24,40 @@ builds:
|
|||
- id: linux-armhf
|
||||
main: ./cmd/headscale/headscale.go
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm
|
||||
goarm:
|
||||
- "7"
|
||||
env:
|
||||
- CC=arm-linux-gnueabihf-gcc
|
||||
- CXX=arm-linux-gnueabihf-g++
|
||||
- CGO_FLAGS=--sysroot=/sysroot/linux/armhf
|
||||
- CGO_LDFLAGS=--sysroot=/sysroot/linux/armhf
|
||||
- PKG_CONFIG_SYSROOT_DIR=/sysroot/linux/armhf
|
||||
- PKG_CONFIG_PATH=/sysroot/linux/armhf/opt/vc/lib/pkgconfig:/sysroot/linux/armhf/usr/lib/arm-linux-gnueabihf/pkgconfig:/sysroot/linux/armhf/usr/lib/pkgconfig:/sysroot/linux/armhf/usr/local/lib/pkgconfig
|
||||
flags:
|
||||
- -mod=readonly
|
||||
ldflags:
|
||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||
|
||||
- id: linux-amd64
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
main: ./cmd/headscale/headscale.go
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
ldflags:
|
||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||
|
||||
- id: linux-arm64
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=aarch64-linux-gnu-gcc
|
||||
main: ./cmd/headscale/headscale.go
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
ldflags:
|
||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||
|
||||
|
|
86
CHANGELOG.md
86
CHANGELOG.md
|
@ -1,17 +1,75 @@
|
|||
# CHANGELOG
|
||||
|
||||
**TBD (TBD):**
|
||||
## 0.15.0 (2022-xx-xx)
|
||||
|
||||
**Note:** Take a backup of your database before upgrading.
|
||||
|
||||
### BREAKING
|
||||
|
||||
- Boundaries between Namespaces has been removed and all nodes can communicate by default [#357](https://github.com/juanfont/headscale/pull/357)
|
||||
- To limit access between nodes, use [ACLs](./docs/acls.md).
|
||||
- `/metrics` is now a configurable host:port endpoint: [#344](https://github.com/juanfont/headscale/pull/344). You must update your `config.yaml` file to include:
|
||||
```yaml
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359)
|
||||
- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
|
||||
- Add shorthand aliases for commands and subcommands [#376](https://github.com/juanfont/headscale/pull/376)
|
||||
- Add `/windows` endpoint for Windows configuration instructions + registry file download [#392](https://github.com/juanfont/headscale/pull/392)
|
||||
- Added embedded DERP server into Headscale [#388](https://github.com/juanfont/headscale/pull/388)
|
||||
|
||||
### Changes
|
||||
|
||||
- Fix a bug were the same IP could be assigned to multiple hosts if joined in quick succession [#346](https://github.com/juanfont/headscale/pull/346)
|
||||
- Simplify the code behind registration of machines [#366](https://github.com/juanfont/headscale/pull/366)
|
||||
- Nodes are now only written to database if they are registrated successfully
|
||||
- Fix a limitation in the ACLs that prevented users to write rules with `*` as source [#374](https://github.com/juanfont/headscale/issues/374)
|
||||
- Reduce the overhead of marshal/unmarshal for Hostinfo, routes and endpoints by using specific types in Machine [#371](https://github.com/juanfont/headscale/pull/371)
|
||||
- Apply normalization function to FQDN on hostnames when hosts registers and retrieve informations [#363](https://github.com/juanfont/headscale/issues/363)
|
||||
|
||||
## 0.14.0 (2022-02-24)
|
||||
|
||||
**UPCOMING ### BREAKING
|
||||
From the **next\*\* version (`0.15.0`), all machines will be able to communicate regardless of
|
||||
if they are in the same namespace. This means that the behaviour currently limited to ACLs
|
||||
will become default. From version `0.15.0`, all limitation of communications must be done
|
||||
with ACLs.
|
||||
|
||||
This is a part of aligning `headscale`'s behaviour with Tailscale's upstream behaviour.
|
||||
|
||||
### BREAKING
|
||||
|
||||
- ACLs have been rewritten to align with the bevaviour Tailscale Control Panel provides. **NOTE:** This is only active if you use ACLs
|
||||
- Namespaces are now treated as Users
|
||||
- All machines can communicate with all machines by default
|
||||
- Tags should now work correctly and adding a host to Headscale should now reload the rules.
|
||||
- The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features
|
||||
|
||||
### Features
|
||||
|
||||
- Add support for configurable mTLS [docs](docs/tls.md#configuring-mutual-tls-authentication-mtls) [#297](https://github.com/juanfont/headscale/pull/297)
|
||||
|
||||
### Changes
|
||||
|
||||
- Remove dependency on CGO (switch from CGO SQLite to pure Go) [#346](https://github.com/juanfont/headscale/pull/346)
|
||||
|
||||
**0.13.0 (2022-02-18):**
|
||||
|
||||
**Features**:
|
||||
### Features
|
||||
|
||||
- Add IPv6 support to the prefix assigned to namespaces
|
||||
- Add API Key support
|
||||
- Enable remote control of `headscale` via CLI [docs](docs/remote-cli.md)
|
||||
- Enable HTTP API (beta, subject to change)
|
||||
- OpenID Connect users will be mapped per namespaces
|
||||
- Each user will get its own namespace, created if it does not exist
|
||||
- `oidc.domain_map` option has been removed
|
||||
- `strip_email_domain` option has been added (see [config-example.yaml](./config_example.yaml))
|
||||
|
||||
**Changes**:
|
||||
### Changes
|
||||
|
||||
- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208)
|
||||
- Upgrade `tailscale` (1.20.4) and other dependencies to latest [#314](https://github.com/juanfont/headscale/pull/314)
|
||||
|
@ -20,35 +78,35 @@
|
|||
|
||||
**0.12.4 (2022-01-29):**
|
||||
|
||||
**Changes**:
|
||||
### 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):**
|
||||
## 0.12.3 (2022-01-13)
|
||||
|
||||
**Changes**:
|
||||
### 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!
|
||||
|
||||
**Changes**:
|
||||
### Changes
|
||||
|
||||
- Fix Docker release [#258](https://github.com/juanfont/headscale/pull/258)
|
||||
- Rewrite main docs [#262](https://github.com/juanfont/headscale/pull/262)
|
||||
- Improve Docker docs [#263](https://github.com/juanfont/headscale/pull/263)
|
||||
|
||||
**0.12.1 (2021-12-24):**
|
||||
## 0.12.1 (2021-12-24)
|
||||
|
||||
(We are skipping 0.12.0 to correct a mishap done weeks ago with the version tagging)
|
||||
|
||||
**BREAKING**:
|
||||
### BREAKING
|
||||
|
||||
- Upgrade to Tailscale 1.18 [#229](https://github.com/juanfont/headscale/pull/229)
|
||||
- This change requires a new format for private key, private keys are now generated automatically:
|
||||
|
@ -56,19 +114,19 @@ Happy New Year!
|
|||
2. Restart `headscale`, a new key will be generated.
|
||||
3. Restart all Tailscale clients to fetch the new key
|
||||
|
||||
**Changes**:
|
||||
### Changes
|
||||
|
||||
- Unify configuration example [#197](https://github.com/juanfont/headscale/pull/197)
|
||||
- Add stricter linting and formatting [#223](https://github.com/juanfont/headscale/pull/223)
|
||||
|
||||
**Features**:
|
||||
### Features
|
||||
|
||||
- Add gRPC and HTTP API (HTTP API is currently disabled) [#204](https://github.com/juanfont/headscale/pull/204)
|
||||
- Use gRPC between the CLI and the server [#206](https://github.com/juanfont/headscale/pull/206), [#212](https://github.com/juanfont/headscale/pull/212)
|
||||
- Beta OpenID Connect support [#126](https://github.com/juanfont/headscale/pull/126), [#227](https://github.com/juanfont/headscale/pull/227)
|
||||
|
||||
**0.11.0 (2021-10-25):**
|
||||
## 0.11.0 (2021-10-25)
|
||||
|
||||
**BREAKING**:
|
||||
### BREAKING
|
||||
|
||||
- Make headscale fetch DERP map from URL and file [#196](https://github.com/juanfont/headscale/pull/196)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Builder image
|
||||
FROM docker.io/golang:1.17.7-bullseye AS build
|
||||
FROM docker.io/golang:1.17.8-bullseye AS build
|
||||
ENV GOPATH /go
|
||||
WORKDIR /go/src/headscale
|
||||
|
||||
|
@ -8,7 +8,7 @@ RUN go mod download
|
|||
|
||||
COPY . .
|
||||
|
||||
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
|
||||
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
||||
RUN strip /go/bin/headscale
|
||||
RUN test -e /go/bin/headscale
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Builder image
|
||||
FROM docker.io/golang:1.17.7-alpine AS build
|
||||
FROM docker.io/golang:1.17.8-alpine AS build
|
||||
ENV GOPATH /go
|
||||
WORKDIR /go/src/headscale
|
||||
|
||||
|
@ -9,7 +9,7 @@ RUN go mod download
|
|||
|
||||
COPY . .
|
||||
|
||||
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
|
||||
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
||||
RUN strip /go/bin/headscale
|
||||
RUN test -e /go/bin/headscale
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Builder image
|
||||
FROM docker.io/golang:1.17.7-bullseye AS build
|
||||
FROM docker.io/golang:1.17.8-bullseye AS build
|
||||
ENV GOPATH /go
|
||||
WORKDIR /go/src/headscale
|
||||
|
||||
|
@ -8,7 +8,7 @@ RUN go mod download
|
|||
|
||||
COPY . .
|
||||
|
||||
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
|
||||
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
||||
RUN test -e /go/bin/headscale
|
||||
|
||||
# Debug image
|
||||
|
|
|
@ -7,5 +7,10 @@ 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.list | tee /etc/apt/sources.list.d/tailscale.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y tailscale=${TAILSCALE_VERSION} dnsutils \
|
||||
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
|
||||
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
|
7
Makefile
7
Makefile
|
@ -10,7 +10,7 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
|
|||
|
||||
|
||||
build:
|
||||
go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
|
||||
GGO_ENABLED=0 go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
|
||||
|
||||
dev: lint test build
|
||||
|
||||
|
@ -18,11 +18,14 @@ test:
|
|||
@go test -coverprofile=coverage.out ./...
|
||||
|
||||
test_integration:
|
||||
go test -tags integration -timeout 30m -count=1 ./...
|
||||
go test -failfast -tags integration -timeout 30m -count=1 ./...
|
||||
|
||||
test_integration_cli:
|
||||
go test -tags integration -v integration_cli_test.go integration_common_test.go
|
||||
|
||||
test_integration_derp:
|
||||
go test -tags integration -v integration_embedded_derp_test.go integration_common_test.go
|
||||
|
||||
coverprofile_func:
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
|
|
292
README.md
292
README.md
|
@ -2,44 +2,68 @@
|
|||
|
||||
![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)
|
||||
|
||||
An open source, self-hosted implementation of the Tailscale coordination server.
|
||||
An open source, self-hosted implementation of the Tailscale control server.
|
||||
|
||||
Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat.
|
||||
|
||||
**Note:** Always select the same GitHub tag as the released version you use to ensure you have the correct example configuration and documentation. The `main` branch might contain unreleased changes.
|
||||
**Note:** Always select the same GitHub tag as the released version you use
|
||||
to ensure you have the correct example configuration and documentation.
|
||||
The `main` branch might contain unreleased changes.
|
||||
|
||||
## Overview
|
||||
## What is Tailscale
|
||||
|
||||
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using [NAT traversal](https://tailscale.com/blog/how-nat-traversal-works/).
|
||||
Tailscale is [a modern VPN](https://tailscale.com/) built on top of
|
||||
[Wireguard](https://www.wireguard.com/).
|
||||
It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/)
|
||||
between the computers of your networks - using
|
||||
[NAT traversal](https://tailscale.com/blog/how-nat-traversal-works/).
|
||||
|
||||
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS (Windows and macOS/iOS), and the 'coordination/control server'.
|
||||
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS
|
||||
(Windows and macOS/iOS), and the control server.
|
||||
|
||||
The control server works as an exchange point of Wireguard public keys for the nodes in the Tailscale network. It also assigns the IP addresses of the clients, creates the boundaries between each user, enables sharing machines between users, and exposes the advertised routes of your nodes.
|
||||
The control server works as an exchange point of Wireguard public keys for the
|
||||
nodes in the Tailscale network. It assigns the IP addresses of the clients,
|
||||
creates the boundaries between each user, enables sharing machines between users,
|
||||
and exposes the advertised routes of your nodes.
|
||||
|
||||
headscale implements this coordination server.
|
||||
A [Tailscale network (tailnet)](https://tailscale.com/kb/1136/tailnet/) is private
|
||||
network which Tailscale assigns to a user in terms of private users or an
|
||||
organisations.
|
||||
|
||||
## Design goal
|
||||
|
||||
`headscale` aims to implement a self-hosted, open source alternative to the Tailscale
|
||||
control server. `headscale` has a narrower scope and an instance of `headscale`
|
||||
implements a _single_ Tailnet, which is typically what a single organisation, or
|
||||
home/personal setup would use.
|
||||
|
||||
`headscale` uses terms that maps to Tailscale's control server, consult the
|
||||
[glossary](./docs/glossary.md) for explainations.
|
||||
|
||||
## Support
|
||||
|
||||
If you like `headscale` and find it useful, there is sponsorship and donation buttons available in the repo.
|
||||
If you like `headscale` and find it useful, there is a sponsorship and donation
|
||||
buttons available in the repo.
|
||||
|
||||
If you would like to sponsor features, bugs or prioritisation, reach out to one of the maintainers.
|
||||
If you would like to sponsor features, bugs or prioritisation, reach out to
|
||||
one of the maintainers.
|
||||
|
||||
## Status
|
||||
## Features
|
||||
|
||||
- [x] Base functionality (nodes can communicate with each other)
|
||||
- [x] Node registration through the web flow
|
||||
- [x] Network changes are relayed to the nodes
|
||||
- [x] Namespaces support (~tailnets in Tailscale.com naming)
|
||||
- [x] Routing (advertise & accept, including exit nodes)
|
||||
- [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support)
|
||||
- [x] JSON-formatted output
|
||||
- [x] ACLs
|
||||
- [x] Taildrop (File Sharing)
|
||||
- [x] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10)
|
||||
- [x] DNS (passing DNS servers to nodes)
|
||||
- [x] Single-Sign-On (via Open ID Connect)
|
||||
- [x] Share nodes between namespaces
|
||||
- [x] MagicDNS (see `docs/`)
|
||||
- Full "base" support of Tailscale's features
|
||||
- Configurable DNS
|
||||
- [Split DNS](https://tailscale.com/kb/1054/dns/#using-dns-settings-in-the-admin-console)
|
||||
- Node registration
|
||||
- Single-Sign-On (via Open ID Connect)
|
||||
- Pre authenticated key
|
||||
- Taildrop (File Sharing)
|
||||
- [Access control lists](https://tailscale.com/kb/1018/acls/)
|
||||
- [MagicDNS](https://tailscale.com/kb/1081/magicdns)
|
||||
- Support for multiple IP ranges in the tailnet
|
||||
- Dual stack (IPv4 and IPv6)
|
||||
- Routing advertising (including exit nodes)
|
||||
- Ephemeral nodes
|
||||
- Embedded [DERP server](https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp)
|
||||
|
||||
## Client OS support
|
||||
|
||||
|
@ -47,15 +71,12 @@ If you would like to sponsor features, bugs or prioritisation, reach out to one
|
|||
| ------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| Linux | Yes |
|
||||
| OpenBSD | Yes |
|
||||
| FreeBSD | Yes |
|
||||
| macOS | Yes (see `/apple` on your headscale for more information) |
|
||||
| Windows | Yes [docs](./docs/windows-client.md) |
|
||||
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
|
||||
| iOS | Not yet |
|
||||
|
||||
## Roadmap
|
||||
|
||||
Suggestions/PRs welcomed!
|
||||
|
||||
## Running headscale
|
||||
|
||||
Please have a look at the documentation under [`docs/`](docs/).
|
||||
|
@ -67,11 +88,15 @@ Please have a look at the documentation under [`docs/`](docs/).
|
|||
|
||||
## Contributing
|
||||
|
||||
To contribute to Headscale you would need the lastest version of [Go](https://golang.org) and [Buf](https://buf.build)(Protobuf generator).
|
||||
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
|
||||
and [Buf](https://buf.build)(Protobuf generator).
|
||||
|
||||
PRs and suggestions are welcome.
|
||||
|
||||
### Code style
|
||||
|
||||
To ensure we have some consistency with a growing number of contributions, 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
|
||||
formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and
|
||||
|
@ -98,7 +123,8 @@ make install-protobuf-plugins
|
|||
|
||||
### Testing and building
|
||||
|
||||
Some parts of the project require the generation of Go code from Protobuf (if changes are made in `proto/`) and it must be (re-)generated with:
|
||||
Some parts of the project require the generation of Go code from Protobuf
|
||||
(if changes are made in `proto/`) and it must be (re-)generated with:
|
||||
|
||||
```shell
|
||||
make generate
|
||||
|
@ -122,6 +148,13 @@ make build
|
|||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/kradalby>
|
||||
<img src=https://avatars.githubusercontent.com/u/98431?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kristoffer Dalby/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Kristoffer Dalby</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/juanfont>
|
||||
<img src=https://avatars.githubusercontent.com/u/181059?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Juan Font/>
|
||||
|
@ -130,10 +163,10 @@ make build
|
|||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/kradalby>
|
||||
<img src=https://avatars.githubusercontent.com/u/98431?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kristoffer Dalby/>
|
||||
<a href=https://github.com/restanrm>
|
||||
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Kristoffer Dalby</b></sub>
|
||||
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
|
@ -150,6 +183,29 @@ make build
|
|||
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/e-zk>
|
||||
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>e-zk</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/arch4ngel>
|
||||
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Justin Angel</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/ItalyPaleAle>
|
||||
<img src=https://avatars.githubusercontent.com/u/43508?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Alessandro (Ale) Segala/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/unreality>
|
||||
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
|
||||
|
@ -157,6 +213,20 @@ make build
|
|||
<sub style="font-size:14px"><b>unreality</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/reynico>
|
||||
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Nico</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/negbie>
|
||||
<img src=https://avatars.githubusercontent.com/u/20154956?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Eugen Biegler/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Eugen Biegler</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/qbit>
|
||||
<img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/>
|
||||
|
@ -166,6 +236,27 @@ make build
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/fdelucchijr>
|
||||
<img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Fernando De Lucchi</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/hdhoang>
|
||||
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Hoàng Đức Hiếu/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/dragetd>
|
||||
<img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Michael G.</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/ptman>
|
||||
<img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/>
|
||||
|
@ -187,6 +278,22 @@ make build
|
|||
<sub style="font-size:14px"><b>Silver Bullet</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/majst01>
|
||||
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/lachy2849>
|
||||
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>lachy2849</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/t56k>
|
||||
<img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/>
|
||||
|
@ -194,6 +301,20 @@ make build
|
|||
<sub style="font-size:14px"><b>thomas</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/aberoham>
|
||||
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/artemklevtsov>
|
||||
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/awoimbee>
|
||||
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
|
||||
|
@ -201,6 +322,15 @@ make build
|
|||
<sub style="font-size:14px"><b>Arthur Woimbée</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/stensonb>
|
||||
<img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/fkr>
|
||||
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
|
||||
|
@ -208,8 +338,6 @@ make build
|
|||
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/felixonmars>
|
||||
<img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/>
|
||||
|
@ -217,6 +345,57 @@ make build
|
|||
<sub style="font-size:14px"><b>Felix Yan</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/JJGadgets>
|
||||
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>JJGadgets</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/madjam002>
|
||||
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/jimt>
|
||||
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/piec>
|
||||
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/rcursaru>
|
||||
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>rcursaru</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/renovate-bot>
|
||||
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>WhiteSource Renovate</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/ryanfowler>
|
||||
<img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/shaananc>
|
||||
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
|
||||
|
@ -224,6 +403,15 @@ make build
|
|||
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/m-tanner-dev0>
|
||||
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Tanner</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/Teteros>
|
||||
<img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/>
|
||||
|
@ -252,8 +440,6 @@ make build
|
|||
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/zekker6>
|
||||
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
|
||||
|
@ -261,6 +447,15 @@ make build
|
|||
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/Bpazy>
|
||||
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ZiYuan/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>ZiYuan</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/derelm>
|
||||
<img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/>
|
||||
|
@ -275,6 +470,27 @@ make build
|
|||
<sub style="font-size:14px"><b>ignoramous</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/lion24>
|
||||
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lion24/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>lion24</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/pernila>
|
||||
<img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=pernila/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>pernila</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/Wakeful-Cloud>
|
||||
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Wakeful-Cloud</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/xpzouying>
|
||||
<img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/>
|
||||
|
|
303
acls.go
303
acls.go
|
@ -5,23 +5,23 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/tailscale/hujson"
|
||||
"gopkg.in/yaml.v3"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const (
|
||||
errEmptyPolicy = Error("empty policy")
|
||||
errInvalidAction = Error("invalid action")
|
||||
errInvalidUserSection = Error("invalid user section")
|
||||
errInvalidGroup = Error("invalid group")
|
||||
errInvalidTag = Error("invalid tag")
|
||||
errInvalidNamespace = Error("invalid namespace")
|
||||
errInvalidPortFormat = Error("invalid port format")
|
||||
errEmptyPolicy = Error("empty policy")
|
||||
errInvalidAction = Error("invalid action")
|
||||
errInvalidGroup = Error("invalid group")
|
||||
errInvalidTag = Error("invalid tag")
|
||||
errInvalidPortFormat = Error("invalid port format")
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -54,28 +54,52 @@ func (h *Headscale) LoadACLPolicy(path string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
ast, err := hujson.Parse(policyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ast.Standardize()
|
||||
policyBytes = ast.Pack()
|
||||
err = json.Unmarshal(policyBytes, &policy)
|
||||
if err != nil {
|
||||
return err
|
||||
switch filepath.Ext(path) {
|
||||
case ".yml", ".yaml":
|
||||
log.Debug().
|
||||
Str("path", path).
|
||||
Bytes("file", policyBytes).
|
||||
Msg("Loading ACLs from YAML")
|
||||
|
||||
err := yaml.Unmarshal(policyBytes, &policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Trace().
|
||||
Interface("policy", policy).
|
||||
Msg("Loaded policy from YAML")
|
||||
|
||||
default:
|
||||
ast, err := hujson.Parse(policyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ast.Standardize()
|
||||
policyBytes = ast.Pack()
|
||||
err = json.Unmarshal(policyBytes, &policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if policy.IsZero() {
|
||||
return errEmptyPolicy
|
||||
}
|
||||
|
||||
h.aclPolicy = &policy
|
||||
|
||||
return h.UpdateACLRules()
|
||||
}
|
||||
|
||||
func (h *Headscale) UpdateACLRules() error {
|
||||
rules, err := h.generateACLRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.aclRules = rules
|
||||
|
||||
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
||||
h.aclRules = rules
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -83,16 +107,23 @@ func (h *Headscale) LoadACLPolicy(path string) error {
|
|||
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||
rules := []tailcfg.FilterRule{}
|
||||
|
||||
if h.aclPolicy == nil {
|
||||
return nil, errEmptyPolicy
|
||||
}
|
||||
|
||||
machines, err := h.ListMachines()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for index, acl := range h.aclPolicy.ACLs {
|
||||
if acl.Action != "accept" {
|
||||
return nil, errInvalidAction
|
||||
}
|
||||
|
||||
filterRule := tailcfg.FilterRule{}
|
||||
|
||||
srcIPs := []string{}
|
||||
for innerIndex, user := range acl.Users {
|
||||
srcs, err := h.generateACLPolicySrcIP(user)
|
||||
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
|
||||
|
@ -101,11 +132,10 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
|||
}
|
||||
srcIPs = append(srcIPs, srcs...)
|
||||
}
|
||||
filterRule.SrcIPs = srcIPs
|
||||
|
||||
destPorts := []tailcfg.NetPortRange{}
|
||||
for innerIndex, ports := range acl.Ports {
|
||||
dests, err := h.generateACLPolicyDestPorts(ports)
|
||||
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
|
||||
|
@ -124,11 +154,17 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
|||
return rules, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) {
|
||||
return h.expandAlias(u)
|
||||
func (h *Headscale) generateACLPolicySrcIP(
|
||||
machines []Machine,
|
||||
aclPolicy ACLPolicy,
|
||||
u string,
|
||||
) ([]string, error) {
|
||||
return expandAlias(machines, aclPolicy, u, h.cfg.OIDC.StripEmaildomain)
|
||||
}
|
||||
|
||||
func (h *Headscale) generateACLPolicyDestPorts(
|
||||
machines []Machine,
|
||||
aclPolicy ACLPolicy,
|
||||
d string,
|
||||
) ([]tailcfg.NetPortRange, error) {
|
||||
tokens := strings.Split(d, ":")
|
||||
|
@ -149,11 +185,16 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
|||
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
||||
}
|
||||
|
||||
expanded, err := h.expandAlias(alias)
|
||||
expanded, err := expandAlias(
|
||||
machines,
|
||||
aclPolicy,
|
||||
alias,
|
||||
h.cfg.OIDC.StripEmaildomain,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ports, err := h.expandPorts(tokens[len(tokens)-1])
|
||||
ports, err := expandPorts(tokens[len(tokens)-1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -172,21 +213,33 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
|||
return dests, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
||||
// expandalias has an input of either
|
||||
// - a namespace
|
||||
// - a group
|
||||
// - a tag
|
||||
// and transform these in IPAddresses.
|
||||
func expandAlias(
|
||||
machines []Machine,
|
||||
aclPolicy ACLPolicy,
|
||||
alias string,
|
||||
stripEmailDomain bool,
|
||||
) ([]string, error) {
|
||||
ips := []string{}
|
||||
if alias == "*" {
|
||||
return []string{"*"}, nil
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("alias", alias).
|
||||
Msg("Expanding")
|
||||
|
||||
if strings.HasPrefix(alias, "group:") {
|
||||
if _, ok := h.aclPolicy.Groups[alias]; !ok {
|
||||
return nil, errInvalidGroup
|
||||
namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain)
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
ips := []string{}
|
||||
for _, n := range h.aclPolicy.Groups[alias] {
|
||||
nodes, err := h.ListMachinesInNamespace(n)
|
||||
if err != nil {
|
||||
return nil, errInvalidNamespace
|
||||
}
|
||||
for _, n := range namespaces {
|
||||
nodes := filterMachinesByNamespace(machines, n)
|
||||
for _, node := range nodes {
|
||||
ips = append(ips, node.IPAddresses.ToStringSlice()...)
|
||||
}
|
||||
|
@ -196,35 +249,17 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
|||
}
|
||||
|
||||
if strings.HasPrefix(alias, "tag:") {
|
||||
if _, ok := h.aclPolicy.TagOwners[alias]; !ok {
|
||||
return nil, errInvalidTag
|
||||
owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain)
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
|
||||
// This will have HORRIBLE performance.
|
||||
// We need to change the data model to better store tags
|
||||
machines := []Machine{}
|
||||
if err := h.db.Where("registered").Find(&machines).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips := []string{}
|
||||
for _, machine := range machines {
|
||||
hostinfo := tailcfg.Hostinfo{}
|
||||
if len(machine.HostInfo) != 0 {
|
||||
hi, err := machine.HostInfo.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(hi, &hostinfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME: Check TagOwners allows this
|
||||
for _, t := range hostinfo.RequestTags {
|
||||
if alias[4:] == t {
|
||||
for _, namespace := range owners {
|
||||
machines := filterMachinesByNamespace(machines, namespace)
|
||||
for _, machine := range machines {
|
||||
hi := machine.GetHostInfo()
|
||||
for _, t := range hi.RequestTags {
|
||||
if alias == t {
|
||||
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -233,38 +268,75 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
|||
return ips, nil
|
||||
}
|
||||
|
||||
n, err := h.GetNamespace(alias)
|
||||
if err == nil {
|
||||
nodes, err := h.ListMachinesInNamespace(n.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips := []string{}
|
||||
for _, n := range nodes {
|
||||
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
||||
}
|
||||
// if alias is a namespace
|
||||
nodes := filterMachinesByNamespace(machines, alias)
|
||||
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
|
||||
|
||||
for _, n := range nodes {
|
||||
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
||||
}
|
||||
if len(ips) > 0 {
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
if h, ok := h.aclPolicy.Hosts[alias]; ok {
|
||||
// if alias is an host
|
||||
if h, ok := aclPolicy.Hosts[alias]; ok {
|
||||
return []string{h.String()}, nil
|
||||
}
|
||||
|
||||
// if alias is an IP
|
||||
ip, err := netaddr.ParseIP(alias)
|
||||
if err == nil {
|
||||
return []string{ip.String()}, nil
|
||||
}
|
||||
|
||||
// if alias is an CIDR
|
||||
cidr, err := netaddr.ParseIPPrefix(alias)
|
||||
if err == nil {
|
||||
return []string{cidr.String()}, nil
|
||||
}
|
||||
|
||||
return nil, errInvalidUserSection
|
||||
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
||||
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
|
||||
// that are correctly tagged since they should not be listed as being in the namespace
|
||||
// we assume in this function that we only have nodes from 1 namespace.
|
||||
func excludeCorrectlyTaggedNodes(
|
||||
aclPolicy ACLPolicy,
|
||||
nodes []Machine,
|
||||
namespace string,
|
||||
) []Machine {
|
||||
out := []Machine{}
|
||||
tags := []string{}
|
||||
for tag, ns := range aclPolicy.TagOwners {
|
||||
if containsString(ns, namespace) {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
// for each machine if tag is in tags list, don't append it.
|
||||
for _, machine := range nodes {
|
||||
hi := machine.GetHostInfo()
|
||||
|
||||
found := false
|
||||
for _, t := range hi.RequestTags {
|
||||
if containsString(tags, t) {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
out = append(out, machine)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
||||
if portsStr == "*" {
|
||||
return &[]tailcfg.PortRange{
|
||||
{First: portRangeBegin, Last: portRangeEnd},
|
||||
|
@ -306,3 +378,82 @@ func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
|||
|
||||
return &ports, nil
|
||||
}
|
||||
|
||||
func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
|
||||
out := []Machine{}
|
||||
for _, machine := range machines {
|
||||
if machine.Namespace.Name == namespace {
|
||||
out = append(out, machine)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
|
||||
// a group cannot be composed of groups.
|
||||
func expandTagOwners(
|
||||
aclPolicy ACLPolicy,
|
||||
tag string,
|
||||
stripEmailDomain bool,
|
||||
) ([]string, error) {
|
||||
var owners []string
|
||||
ows, ok := aclPolicy.TagOwners[tag]
|
||||
if !ok {
|
||||
return []string{}, fmt.Errorf(
|
||||
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
|
||||
errInvalidTag,
|
||||
tag,
|
||||
)
|
||||
}
|
||||
for _, owner := range ows {
|
||||
if strings.HasPrefix(owner, "group:") {
|
||||
gs, err := expandGroup(aclPolicy, owner, stripEmailDomain)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
owners = append(owners, gs...)
|
||||
} else {
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
}
|
||||
|
||||
return owners, nil
|
||||
}
|
||||
|
||||
// expandGroup will return the list of namespace inside the group
|
||||
// after some validation.
|
||||
func expandGroup(
|
||||
aclPolicy ACLPolicy,
|
||||
group string,
|
||||
stripEmailDomain bool,
|
||||
) ([]string, error) {
|
||||
outGroups := []string{}
|
||||
aclGroups, ok := aclPolicy.Groups[group]
|
||||
if !ok {
|
||||
return []string{}, fmt.Errorf(
|
||||
"group %v isn't registered. %w",
|
||||
group,
|
||||
errInvalidGroup,
|
||||
)
|
||||
}
|
||||
for _, group := range aclGroups {
|
||||
if strings.HasPrefix(group, "group:") {
|
||||
return []string{}, fmt.Errorf(
|
||||
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
|
||||
errInvalidGroup,
|
||||
)
|
||||
}
|
||||
grp, err := NormalizeToFQDNRules(group, stripEmailDomain)
|
||||
if err != nil {
|
||||
return []string{}, fmt.Errorf(
|
||||
"failed to normalize group %q, err: %w",
|
||||
group,
|
||||
errInvalidGroup,
|
||||
)
|
||||
}
|
||||
outGroups = append(outGroups, grp)
|
||||
}
|
||||
|
||||
return outGroups, nil
|
||||
}
|
||||
|
|
1067
acls_test.go
1067
acls_test.go
File diff suppressed because it is too large
Load diff
|
@ -5,23 +5,24 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
"gopkg.in/yaml.v3"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// ACLPolicy represents a Tailscale ACL Policy.
|
||||
type ACLPolicy struct {
|
||||
Groups Groups `json:"Groups"`
|
||||
Hosts Hosts `json:"Hosts"`
|
||||
TagOwners TagOwners `json:"TagOwners"`
|
||||
ACLs []ACL `json:"ACLs"`
|
||||
Tests []ACLTest `json:"Tests"`
|
||||
Groups Groups `json:"Groups" yaml:"Groups"`
|
||||
Hosts Hosts `json:"Hosts" yaml:"Hosts"`
|
||||
TagOwners TagOwners `json:"TagOwners" yaml:"TagOwners"`
|
||||
ACLs []ACL `json:"ACLs" yaml:"ACLs"`
|
||||
Tests []ACLTest `json:"Tests" yaml:"Tests"`
|
||||
}
|
||||
|
||||
// ACL is a basic rule for the ACL Policy.
|
||||
type ACL struct {
|
||||
Action string `json:"Action"`
|
||||
Users []string `json:"Users"`
|
||||
Ports []string `json:"Ports"`
|
||||
Action string `json:"Action" yaml:"Action"`
|
||||
Users []string `json:"Users" yaml:"Users"`
|
||||
Ports []string `json:"Ports" yaml:"Ports"`
|
||||
}
|
||||
|
||||
// Groups references a series of alias in the ACL rules.
|
||||
|
@ -35,9 +36,9 @@ type TagOwners map[string][]string
|
|||
|
||||
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
|
||||
type ACLTest struct {
|
||||
User string `json:"User"`
|
||||
Allow []string `json:"Allow"`
|
||||
Deny []string `json:"Deny,omitempty"`
|
||||
User string `json:"User" yaml:"User"`
|
||||
Allow []string `json:"Allow" yaml:"Allow"`
|
||||
Deny []string `json:"Deny,omitempty" yaml:"Deny,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
|
||||
|
@ -69,6 +70,27 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML allows to parse the Hosts directly into netaddr objects.
|
||||
func (hosts *Hosts) UnmarshalYAML(data []byte) error {
|
||||
newHosts := Hosts{}
|
||||
hostIPPrefixMap := make(map[string]string)
|
||||
|
||||
err := yaml.Unmarshal(data, &hostIPPrefixMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for host, prefixStr := range hostIPPrefixMap {
|
||||
prefix, err := netaddr.ParseIPPrefix(prefixStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newHosts[host] = prefix
|
||||
}
|
||||
*hosts = newHosts
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsZero is perhaps a bit naive here.
|
||||
func (policy ACLPolicy) IsZero() bool {
|
||||
if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 {
|
||||
|
|
246
api.go
246
api.go
|
@ -22,7 +22,7 @@ import (
|
|||
|
||||
const (
|
||||
reservedResponseHeaderSize = 4
|
||||
RegisterMethodAuthKey = "authKey"
|
||||
RegisterMethodAuthKey = "authkey"
|
||||
RegisterMethodOIDC = "oidc"
|
||||
RegisterMethodCLI = "cli"
|
||||
ErrRegisterMethodCLIDoesNotSupportExpire = Error(
|
||||
|
@ -45,22 +45,21 @@ type registerWebAPITemplateConfig struct {
|
|||
}
|
||||
|
||||
var registerWebAPITemplate = template.Must(
|
||||
template.New("registerweb").Parse(`<html>
|
||||
template.New("registerweb").Parse(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Registration - Headscale</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>headscale</h1>
|
||||
<p>
|
||||
Run the command below in the headscale server to add this machine to your network:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<code>
|
||||
<b>headscale -n NAMESPACE nodes register --key {{.Key}}</b>
|
||||
</code>
|
||||
</p>
|
||||
|
||||
<h1>headscale</h1>
|
||||
<h2>Machine registration</h2>
|
||||
<p>
|
||||
Run the command below in the headscale server to add this machine to your network:
|
||||
</p>
|
||||
<pre><code>headscale -n NAMESPACE nodes register --key {{.Key}}</code></pre>
|
||||
</body>
|
||||
</html>`),
|
||||
)
|
||||
</html>
|
||||
`))
|
||||
|
||||
// RegisterWebAPI shows a simple message in the browser to point to the CLI
|
||||
// Listens in /register.
|
||||
|
@ -125,25 +124,63 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
|
|||
machine, err := h.GetMachineByMachineKey(machineKey)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine")
|
||||
newMachine := Machine{
|
||||
Expiry: &time.Time{},
|
||||
MachineKey: MachinePublicKeyStripPrefix(machineKey),
|
||||
Name: req.Hostinfo.Hostname,
|
||||
}
|
||||
if err := h.db.Create(&newMachine).Error; err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Could not create row")
|
||||
machineRegistrations.WithLabelValues("unknown", "web", "error", machine.Namespace.Name).
|
||||
Inc()
|
||||
|
||||
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
|
||||
|
||||
// If the machine has AuthKey set, handle registration via PreAuthKeys
|
||||
if req.Auth.AuthKey != "" {
|
||||
h.handleAuthKey(ctx, machineKey, req)
|
||||
|
||||
return
|
||||
}
|
||||
machine = &newMachine
|
||||
hname, err := NormalizeToFQDNRules(
|
||||
req.Hostinfo.Hostname,
|
||||
h.cfg.OIDC.StripEmaildomain,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "RegistrationHandler").
|
||||
Str("hostinfo.name", req.Hostinfo.Hostname).
|
||||
Err(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The machine did not have a key to authenticate, which means
|
||||
// that we rely on a method that calls back some how (OpenID or CLI)
|
||||
// We create the machine and then keep it around until a callback
|
||||
// happens
|
||||
newMachine := Machine{
|
||||
MachineKey: machineKeyStr,
|
||||
Name: hname,
|
||||
NodeKey: NodePublicKeyStripPrefix(req.NodeKey),
|
||||
LastSeen: &now,
|
||||
Expiry: &time.Time{},
|
||||
}
|
||||
|
||||
if !req.Expiry.IsZero() {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", req.Hostinfo.Hostname).
|
||||
Time("expiry", req.Expiry).
|
||||
Msg("Non-zero expiry time requested")
|
||||
newMachine.Expiry = &req.Expiry
|
||||
}
|
||||
|
||||
h.registrationCache.Set(
|
||||
machineKeyStr,
|
||||
newMachine,
|
||||
registerCacheExpiration,
|
||||
)
|
||||
|
||||
h.handleMachineRegistrationNew(ctx, machineKey, req)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if machine.Registered {
|
||||
// The machine is already registered, so we need to pass through reauth or key update.
|
||||
if machine != nil {
|
||||
// If the NodeKey stored in headscale is the same as the key presented in a registration
|
||||
// request, then we have a node that is either:
|
||||
// - Trying to log out (sending a expiry in the past)
|
||||
|
@ -180,15 +217,6 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
// If the machine has AuthKey set, handle registration via PreAuthKeys
|
||||
if req.Auth.AuthKey != "" {
|
||||
h.handleAuthKey(ctx, machineKey, req, *machine)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.handleMachineRegistrationNew(ctx, machineKey, req, *machine)
|
||||
}
|
||||
|
||||
func (h *Headscale) getMapResponse(
|
||||
|
@ -261,7 +289,16 @@ func (h *Headscale) getMapResponse(
|
|||
|
||||
var respBody []byte
|
||||
if req.Compress == "zstd" {
|
||||
src, _ := json.Marshal(resp)
|
||||
src, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "getMapResponse").
|
||||
Err(err).
|
||||
Msg("Failed to marshal response for the client")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoder, _ := zstd.NewWriter(nil)
|
||||
srcCompressed := encoder.EncodeAll(src, nil)
|
||||
|
@ -290,7 +327,16 @@ func (h *Headscale) getMapKeepAliveResponse(
|
|||
var respBody []byte
|
||||
var err error
|
||||
if mapRequest.Compress == "zstd" {
|
||||
src, _ := json.Marshal(mapResponse)
|
||||
src, err := json.Marshal(mapResponse)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "getMapKeepAliveResponse").
|
||||
Err(err).
|
||||
Msg("Failed to marshal keepalive response for the client")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
encoder, _ := zstd.NewWriter(nil)
|
||||
srcCompressed := encoder.EncodeAll(src, nil)
|
||||
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
||||
|
@ -384,7 +430,7 @@ func (h *Headscale) handleMachineExpired(
|
|||
Msg("Machine registration has expired. Sending a authurl to register")
|
||||
|
||||
if registerRequest.Auth.AuthKey != "" {
|
||||
h.handleAuthKey(ctx, machineKey, registerRequest, machine)
|
||||
h.handleAuthKey(ctx, machineKey, registerRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -447,13 +493,12 @@ func (h *Headscale) handleMachineRegistrationNew(
|
|||
ctx *gin.Context,
|
||||
machineKey key.MachinePublic,
|
||||
registerRequest tailcfg.RegisterRequest,
|
||||
machine Machine,
|
||||
) {
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
|
||||
// The machine registration is new, redirect the client to the registration URL
|
||||
log.Debug().
|
||||
Str("machine", machine.Name).
|
||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||
Msg("The node is sending us a new NodeKey, sending auth url")
|
||||
if h.cfg.OIDC.Issuer != "" {
|
||||
resp.AuthURL = fmt.Sprintf(
|
||||
|
@ -466,24 +511,6 @@ func (h *Headscale) handleMachineRegistrationNew(
|
|||
strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey))
|
||||
}
|
||||
|
||||
if !registerRequest.Expiry.IsZero() {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Time("expiry", registerRequest.Expiry).
|
||||
Msg("Non-zero expiry time requested, adding to cache")
|
||||
h.requestedExpiryCache.Set(
|
||||
machineKey.String(),
|
||||
registerRequest.Expiry,
|
||||
requestedExpiryCacheExpiration,
|
||||
)
|
||||
}
|
||||
|
||||
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
|
||||
|
||||
// save the NodeKey
|
||||
h.db.Save(&machine)
|
||||
|
||||
respBody, err := encode(resp, &machineKey, h.privateKey)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
|
@ -502,19 +529,21 @@ func (h *Headscale) handleAuthKey(
|
|||
ctx *gin.Context,
|
||||
machineKey key.MachinePublic,
|
||||
registerRequest tailcfg.RegisterRequest,
|
||||
machine Machine,
|
||||
) {
|
||||
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
|
||||
|
||||
log.Debug().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
|
||||
pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||
Err(err).
|
||||
Msg("Failed authentication via AuthKey")
|
||||
resp.MachineAuthorized = false
|
||||
|
@ -523,71 +552,66 @@ func (h *Headscale) handleAuthKey(
|
|||
log.Error().
|
||||
Caller().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||
Err(err).
|
||||
Msg("Cannot encode message")
|
||||
ctx.String(http.StatusInternalServerError, "")
|
||||
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||
Inc()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusUnauthorized, "application/json; charset=utf-8", respBody)
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||
Msg("Failed authentication via AuthKey")
|
||||
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||
Inc()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if machine.isRegistered() {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("machine already registered, reauthenticating")
|
||||
log.Debug().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||
Msg("Authentication key was valid, proceeding to acquire IP addresses")
|
||||
|
||||
h.RefreshMachine(&machine, registerRequest.Expiry)
|
||||
} else {
|
||||
log.Debug().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Msg("Authentication key was valid, proceeding to acquire IP addresses")
|
||||
ips, err := h.getAvailableIPs()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Msg("Failed to find an available IP address")
|
||||
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
|
||||
Inc()
|
||||
nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
|
||||
now := time.Now().UTC()
|
||||
|
||||
return
|
||||
}
|
||||
log.Info().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Str("ips", strings.Join(ips.ToStringSlice(), ",")).
|
||||
Msgf("Assigning %s to %s", strings.Join(ips.ToStringSlice(), ","), machine.Name)
|
||||
|
||||
machine.Expiry = ®isterRequest.Expiry
|
||||
machine.AuthKeyID = uint(pak.ID)
|
||||
machine.IPAddresses = ips
|
||||
machine.NamespaceID = pak.NamespaceID
|
||||
|
||||
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
|
||||
// we update it just in case
|
||||
machine.Registered = true
|
||||
machine.RegisterMethod = RegisterMethodAuthKey
|
||||
h.db.Save(&machine)
|
||||
machineToRegister := Machine{
|
||||
Name: registerRequest.Hostinfo.Hostname,
|
||||
NamespaceID: pak.Namespace.ID,
|
||||
MachineKey: machineKeyStr,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
Expiry: ®isterRequest.Expiry,
|
||||
NodeKey: nodeKey,
|
||||
LastSeen: &now,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
|
||||
pak.Used = true
|
||||
h.db.Save(&pak)
|
||||
machine, err := h.RegisterMachine(
|
||||
machineToRegister,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("could not register machine")
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||
Inc()
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not register machine",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.UsePreAuthKey(pak)
|
||||
|
||||
resp.MachineAuthorized = true
|
||||
resp.User = *pak.Namespace.toUser()
|
||||
|
@ -596,21 +620,21 @@ func (h *Headscale) handleAuthKey(
|
|||
log.Error().
|
||||
Caller().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||
Err(err).
|
||||
Msg("Cannot encode message")
|
||||
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
|
||||
Inc()
|
||||
ctx.String(http.StatusInternalServerError, "Extremely sad!")
|
||||
|
||||
return
|
||||
}
|
||||
machineRegistrations.WithLabelValues("new", "authkey", "success", machine.Namespace.Name).
|
||||
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
|
||||
Inc()
|
||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
|
||||
log.Info().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||||
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
|
||||
Msg("Successfully authenticated via AuthKey")
|
||||
}
|
||||
|
|
154
app.go
154
app.go
|
@ -55,19 +55,24 @@ const (
|
|||
HTTPReadTimeout = 30 * time.Second
|
||||
privateKeyFileMode = 0o600
|
||||
|
||||
requestedExpiryCacheExpiration = time.Minute * 5
|
||||
requestedExpiryCacheCleanupInterval = time.Minute * 10
|
||||
registerCacheExpiration = time.Minute * 15
|
||||
registerCacheCleanup = time.Minute * 20
|
||||
|
||||
errUnsupportedDatabase = Error("unsupported DB")
|
||||
errUnsupportedLetsEncryptChallengeType = Error(
|
||||
"unknown value for Lets Encrypt challenge type",
|
||||
)
|
||||
|
||||
DisabledClientAuth = "disabled"
|
||||
RelaxedClientAuth = "relaxed"
|
||||
EnforcedClientAuth = "enforced"
|
||||
)
|
||||
|
||||
// Config contains the initial Headscale configuration.
|
||||
type Config struct {
|
||||
ServerURL string
|
||||
Addr string
|
||||
MetricsAddr string
|
||||
GRPCAddr string
|
||||
GRPCAllowInsecure bool
|
||||
EphemeralNodeInactivityTimeout time.Duration
|
||||
|
@ -90,8 +95,9 @@ type Config struct {
|
|||
TLSLetsEncryptCacheDir string
|
||||
TLSLetsEncryptChallengeType string
|
||||
|
||||
TLSCertPath string
|
||||
TLSKeyPath string
|
||||
TLSCertPath string
|
||||
TLSKeyPath string
|
||||
TLSClientAuthMode tls.ClientAuthType
|
||||
|
||||
ACMEURL string
|
||||
ACMEEmail string
|
||||
|
@ -107,17 +113,23 @@ type Config struct {
|
|||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
Issuer string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
MatchMap map[string]string
|
||||
Issuer string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
StripEmaildomain bool
|
||||
}
|
||||
|
||||
type DERPConfig struct {
|
||||
URLs []url.URL
|
||||
Paths []string
|
||||
AutoUpdate bool
|
||||
UpdateFrequency time.Duration
|
||||
ServerEnabled bool
|
||||
ServerRegionID int
|
||||
ServerRegionCode string
|
||||
ServerRegionName string
|
||||
STUNEnabled bool
|
||||
STUNAddr string
|
||||
URLs []url.URL
|
||||
Paths []string
|
||||
AutoUpdate bool
|
||||
UpdateFrequency time.Duration
|
||||
}
|
||||
|
||||
type CLIConfig struct {
|
||||
|
@ -136,21 +148,43 @@ type Headscale struct {
|
|||
dbDebug bool
|
||||
privateKey *key.MachinePrivate
|
||||
|
||||
DERPMap *tailcfg.DERPMap
|
||||
DERPMap *tailcfg.DERPMap
|
||||
DERPServer *DERPServer
|
||||
|
||||
aclPolicy *ACLPolicy
|
||||
aclRules []tailcfg.FilterRule
|
||||
|
||||
lastStateChange sync.Map
|
||||
|
||||
oidcProvider *oidc.Provider
|
||||
oauth2Config *oauth2.Config
|
||||
oidcStateCache *cache.Cache
|
||||
oidcProvider *oidc.Provider
|
||||
oauth2Config *oauth2.Config
|
||||
|
||||
requestedExpiryCache *cache.Cache
|
||||
registrationCache *cache.Cache
|
||||
|
||||
ipAllocationMutex sync.Mutex
|
||||
}
|
||||
|
||||
// Look up the TLS constant relative to user-supplied TLS client
|
||||
// authentication mode. If an unknown mode is supplied, the default
|
||||
// value, tls.RequireAnyClientCert, is returned. The returned boolean
|
||||
// indicates if the supplied mode was valid.
|
||||
func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
|
||||
switch mode {
|
||||
case DisabledClientAuth:
|
||||
// Client cert is _not_ required.
|
||||
return tls.NoClientCert, true
|
||||
case RelaxedClientAuth:
|
||||
// Client cert required, but _not verified_.
|
||||
return tls.RequireAnyClientCert, true
|
||||
case EnforcedClientAuth:
|
||||
// Client cert is _required and verified_.
|
||||
return tls.RequireAndVerifyClientCert, true
|
||||
default:
|
||||
// Return the default when an unknown value is supplied.
|
||||
return tls.RequireAnyClientCert, false
|
||||
}
|
||||
}
|
||||
|
||||
// NewHeadscale returns the Headscale app.
|
||||
func NewHeadscale(cfg Config) (*Headscale, error) {
|
||||
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
||||
if err != nil {
|
||||
|
@ -174,18 +208,18 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
|||
return nil, errUnsupportedDatabase
|
||||
}
|
||||
|
||||
requestedExpiryCache := cache.New(
|
||||
requestedExpiryCacheExpiration,
|
||||
requestedExpiryCacheCleanupInterval,
|
||||
registrationCache := cache.New(
|
||||
registerCacheExpiration,
|
||||
registerCacheCleanup,
|
||||
)
|
||||
|
||||
app := Headscale{
|
||||
cfg: cfg,
|
||||
dbType: cfg.DBtype,
|
||||
dbString: dbString,
|
||||
privateKey: privKey,
|
||||
aclRules: tailcfg.FilterAllowAll, // default allowall
|
||||
requestedExpiryCache: requestedExpiryCache,
|
||||
cfg: cfg,
|
||||
dbType: cfg.DBtype,
|
||||
dbString: dbString,
|
||||
privateKey: privKey,
|
||||
aclRules: tailcfg.FilterAllowAll, // default allowall
|
||||
registrationCache: registrationCache,
|
||||
}
|
||||
|
||||
err = app.initDB()
|
||||
|
@ -211,6 +245,14 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if cfg.DERP.ServerEnabled {
|
||||
embeddedDERPServer, err := app.NewDERPServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app.DERPServer = embeddedDERPServer
|
||||
}
|
||||
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
|
@ -406,11 +448,17 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
|
|||
return os.Remove(h.cfg.UnixSocket)
|
||||
}
|
||||
|
||||
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
|
||||
router := gin.Default()
|
||||
func (h *Headscale) createPrometheusRouter() *gin.Engine {
|
||||
promRouter := gin.Default()
|
||||
|
||||
prometheus := ginprometheus.NewPrometheus("gin")
|
||||
prometheus.Use(router)
|
||||
prometheus.Use(promRouter)
|
||||
|
||||
return promRouter
|
||||
}
|
||||
|
||||
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
|
||||
router := gin.Default()
|
||||
|
||||
router.GET(
|
||||
"/health",
|
||||
|
@ -422,11 +470,19 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
|
|||
router.POST("/machine/:id", h.RegistrationHandler)
|
||||
router.GET("/oidc/register/:mkey", h.RegisterOIDC)
|
||||
router.GET("/oidc/callback", h.OIDCCallback)
|
||||
router.GET("/apple", h.AppleMobileConfig)
|
||||
router.GET("/apple", h.AppleConfigMessage)
|
||||
router.GET("/apple/:platform", h.ApplePlatformConfig)
|
||||
router.GET("/windows", h.WindowsConfigMessage)
|
||||
router.GET("/windows/tailscale.reg", h.WindowsRegConfig)
|
||||
router.GET("/swagger", SwaggerUI)
|
||||
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
|
||||
|
||||
if h.cfg.DERP.ServerEnabled {
|
||||
router.Any("/derp", h.DERPHandler)
|
||||
router.Any("/derp/probe", h.DERPProbeHandler)
|
||||
router.Any("/bootstrap-dns", h.DERPBootstrapDNSHandler)
|
||||
}
|
||||
|
||||
api := router.Group("/api")
|
||||
api.Use(h.httpAuthenticationMiddleware)
|
||||
{
|
||||
|
@ -445,6 +501,13 @@ func (h *Headscale) Serve() error {
|
|||
// Fetch an initial DERP Map before we start serving
|
||||
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
||||
|
||||
if h.cfg.DERP.ServerEnabled {
|
||||
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
||||
if h.cfg.DERP.STUNEnabled {
|
||||
go h.ServeSTUN()
|
||||
}
|
||||
}
|
||||
|
||||
if h.cfg.DERP.AutoUpdate {
|
||||
derpMapCancelChannel := make(chan struct{})
|
||||
defer func() { derpMapCancelChannel <- struct{}{} }()
|
||||
|
@ -622,6 +685,27 @@ func (h *Headscale) Serve() error {
|
|||
log.Info().
|
||||
Msgf("listening and serving HTTP on: %s", h.cfg.Addr)
|
||||
|
||||
promRouter := h.createPrometheusRouter()
|
||||
|
||||
promHTTPServer := &http.Server{
|
||||
Addr: h.cfg.MetricsAddr,
|
||||
Handler: promRouter,
|
||||
ReadTimeout: HTTPReadTimeout,
|
||||
WriteTimeout: 0,
|
||||
}
|
||||
|
||||
var promHTTPListener net.Listener
|
||||
promHTTPListener, err = net.Listen("tcp", h.cfg.MetricsAddr)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bind to TCP address: %w", err)
|
||||
}
|
||||
|
||||
errorGroup.Go(func() error { return promHTTPServer.Serve(promHTTPListener) })
|
||||
|
||||
log.Info().
|
||||
Msgf("listening and serving metrics on: %s", h.cfg.MetricsAddr)
|
||||
|
||||
return errorGroup.Wait()
|
||||
}
|
||||
|
||||
|
@ -676,12 +760,18 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
||||
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
|
||||
}
|
||||
|
||||
log.Info().Msg(fmt.Sprintf(
|
||||
"Client authentication (mTLS) is \"%s\". See the docs to learn about configuring this setting.",
|
||||
h.cfg.TLSClientAuthMode))
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ClientAuth: tls.RequireAnyClientCert,
|
||||
ClientAuth: h.cfg.TLSClientAuthMode,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
Certificates: make([]tls.Certificate, 1),
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
|
||||
|
||||
return tlsConfig, err
|
||||
|
|
22
app_test.go
22
app_test.go
|
@ -5,7 +5,6 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"gopkg.in/check.v1"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
@ -50,10 +49,6 @@ func (s *Suite) ResetDB(c *check.C) {
|
|||
cfg: cfg,
|
||||
dbType: "sqlite3",
|
||||
dbString: tmpDir + "/headscale_test.db",
|
||||
requestedExpiryCache: cache.New(
|
||||
requestedExpiryCacheExpiration,
|
||||
requestedExpiryCacheCleanupInterval,
|
||||
),
|
||||
}
|
||||
err = app.initDB()
|
||||
if err != nil {
|
||||
|
@ -65,3 +60,20 @@ func (s *Suite) ResetDB(c *check.C) {
|
|||
}
|
||||
app.db = db
|
||||
}
|
||||
|
||||
// Enusre an error is returned when an invalid auth mode
|
||||
// is supplied.
|
||||
func (s *Suite) TestInvalidClientAuthMode(c *check.C) {
|
||||
_, isValid := LookupTLSClientAuthMode("invalid")
|
||||
c.Assert(isValid, check.Equals, false)
|
||||
}
|
||||
|
||||
// Ensure that all client auth modes return a nil error.
|
||||
func (s *Suite) TestAuthModes(c *check.C) {
|
||||
modes := []string{"disabled", "relaxed", "enforced"}
|
||||
|
||||
for _, v := range modes {
|
||||
_, isValid := LookupTLSClientAuthMode(v)
|
||||
c.Assert(isValid, check.Equals, true)
|
||||
}
|
||||
}
|
||||
|
|
41
cli_test.go
41
cli_test.go
|
@ -1,41 +0,0 @@
|
|||
package headscale
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func (s *Suite) TestRegisterMachine(c *check.C) {
|
||||
namespace, err := app.CreateNamespace("test")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
machine := Machine{
|
||||
ID: 0,
|
||||
MachineKey: "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e",
|
||||
NodeKey: "bar",
|
||||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
NamespaceID: namespace.ID,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("10.0.0.1")},
|
||||
Expiry: &now,
|
||||
}
|
||||
err = app.db.Save(&machine).Error
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = app.GetMachine(namespace.Name, machine.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
machineAfterRegistering, err := app.RegisterMachine(
|
||||
machine.MachineKey,
|
||||
namespace.Name,
|
||||
)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(machineAfterRegistering.Registered, check.Equals, true)
|
||||
|
||||
_, err = machineAfterRegistering.GetHostInfo()
|
||||
c.Assert(err, check.IsNil)
|
||||
}
|
|
@ -36,13 +36,15 @@ func init() {
|
|||
}
|
||||
|
||||
var apiKeysCmd = &cobra.Command{
|
||||
Use: "apikeys",
|
||||
Short: "Handle the Api keys in Headscale",
|
||||
Use: "apikeys",
|
||||
Short: "Handle the Api keys in Headscale",
|
||||
Aliases: []string{"apikey", "api"},
|
||||
}
|
||||
|
||||
var listAPIKeys = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List the Api keys for headscale",
|
||||
Use: "list",
|
||||
Short: "List the Api keys for headscale",
|
||||
Aliases: []string{"ls", "show"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
|
@ -107,6 +109,7 @@ var createAPIKeyCmd = &cobra.Command{
|
|||
Creates a new Api key, the Api key is only visible on creation
|
||||
and cannot be retrieved again.
|
||||
If you loose a key, create a new one and revoke (expire) the old one.`,
|
||||
Aliases: []string{"c", "new"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
|
@ -144,7 +147,7 @@ If you loose a key, create a new one and revoke (expire) the old one.`,
|
|||
var expireAPIKeyCmd = &cobra.Command{
|
||||
Use: "expire",
|
||||
Short: "Expire an ApiKey",
|
||||
Aliases: []string{"revoke"},
|
||||
Aliases: []string{"revoke", "exp", "e"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
|
|
|
@ -13,8 +13,9 @@ func init() {
|
|||
}
|
||||
|
||||
var generateCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate commands",
|
||||
Use: "generate",
|
||||
Short: "Generate commands",
|
||||
Aliases: []string{"gen"},
|
||||
}
|
||||
|
||||
var generatePrivateKeyCmd = &cobra.Command{
|
||||
|
|
|
@ -25,13 +25,15 @@ const (
|
|||
)
|
||||
|
||||
var namespaceCmd = &cobra.Command{
|
||||
Use: "namespaces",
|
||||
Short: "Manage the namespaces of Headscale",
|
||||
Use: "namespaces",
|
||||
Short: "Manage the namespaces of Headscale",
|
||||
Aliases: []string{"namespace", "ns", "user", "users"},
|
||||
}
|
||||
|
||||
var createNamespaceCmd = &cobra.Command{
|
||||
Use: "create NAME",
|
||||
Short: "Creates a new namespace",
|
||||
Use: "create NAME",
|
||||
Short: "Creates a new namespace",
|
||||
Aliases: []string{"c", "new"},
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errMissingParameter
|
||||
|
@ -72,8 +74,9 @@ var createNamespaceCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
var destroyNamespaceCmd = &cobra.Command{
|
||||
Use: "destroy NAME",
|
||||
Short: "Destroys a namespace",
|
||||
Use: "destroy NAME",
|
||||
Short: "Destroys a namespace",
|
||||
Aliases: []string{"delete"},
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errMissingParameter
|
||||
|
@ -144,8 +147,9 @@ var destroyNamespaceCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
var listNamespacesCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all the namespaces",
|
||||
Use: "list",
|
||||
Short: "List all the namespaces",
|
||||
Aliases: []string{"ls", "show"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
|
@ -197,8 +201,9 @@ var listNamespacesCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
var renameNamespaceCmd = &cobra.Command{
|
||||
Use: "rename OLD_NAME NEW_NAME",
|
||||
Short: "Renames a namespace",
|
||||
Use: "rename OLD_NAME NEW_NAME",
|
||||
Short: "Renames a namespace",
|
||||
Aliases: []string{"mv"},
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
expectedArguments := 2
|
||||
if len(args) < expectedArguments {
|
||||
|
|
|
@ -46,35 +46,12 @@ func init() {
|
|||
log.Fatalf(err.Error())
|
||||
}
|
||||
nodeCmd.AddCommand(deleteNodeCmd)
|
||||
|
||||
shareMachineCmd.Flags().StringP("namespace", "n", "", "Namespace")
|
||||
err = shareMachineCmd.MarkFlagRequired("namespace")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
shareMachineCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
|
||||
err = shareMachineCmd.MarkFlagRequired("identifier")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
nodeCmd.AddCommand(shareMachineCmd)
|
||||
|
||||
unshareMachineCmd.Flags().StringP("namespace", "n", "", "Namespace")
|
||||
err = unshareMachineCmd.MarkFlagRequired("namespace")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
unshareMachineCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
|
||||
err = unshareMachineCmd.MarkFlagRequired("identifier")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
nodeCmd.AddCommand(unshareMachineCmd)
|
||||
}
|
||||
|
||||
var nodeCmd = &cobra.Command{
|
||||
Use: "nodes",
|
||||
Short: "Manage the nodes of Headscale",
|
||||
Use: "nodes",
|
||||
Short: "Manage the nodes of Headscale",
|
||||
Aliases: []string{"node", "machine", "machines"},
|
||||
}
|
||||
|
||||
var registerNodeCmd = &cobra.Command{
|
||||
|
@ -128,8 +105,9 @@ var registerNodeCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
var listNodesCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List nodes",
|
||||
Use: "list",
|
||||
Short: "List nodes",
|
||||
Aliases: []string{"ls", "show"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
namespace, err := cmd.Flags().GetString("namespace")
|
||||
|
@ -188,7 +166,7 @@ var expireNodeCmd = &cobra.Command{
|
|||
Use: "expire",
|
||||
Short: "Expire (log out) a machine in your network",
|
||||
Long: "Expiring a node will keep the node in the database and force it to reauthenticate.",
|
||||
Aliases: []string{"logout"},
|
||||
Aliases: []string{"logout", "exp", "e"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
|
@ -230,8 +208,9 @@ var expireNodeCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
var deleteNodeCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete a node",
|
||||
Use: "delete",
|
||||
Short: "Delete a node",
|
||||
Aliases: []string{"del"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
|
@ -317,139 +296,6 @@ var deleteNodeCmd = &cobra.Command{
|
|||
},
|
||||
}
|
||||
|
||||
func sharingWorker(
|
||||
cmd *cobra.Command,
|
||||
) (string, *v1.Machine, *v1.Namespace, error) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
namespaceStr, err := cmd.Flags().GetString("namespace")
|
||||
if err != nil {
|
||||
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
|
||||
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
ctx, client, conn, cancel := getHeadscaleCLIClient()
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
|
||||
identifier, err := cmd.Flags().GetUint64("identifier")
|
||||
if err != nil {
|
||||
ErrorOutput(err, fmt.Sprintf("Error converting ID to integer: %s", err), output)
|
||||
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
machineRequest := &v1.GetMachineRequest{
|
||||
MachineId: identifier,
|
||||
}
|
||||
|
||||
machineResponse, err := client.GetMachine(ctx, machineRequest)
|
||||
if err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()),
|
||||
output,
|
||||
)
|
||||
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
namespaceRequest := &v1.GetNamespaceRequest{
|
||||
Name: namespaceStr,
|
||||
}
|
||||
|
||||
namespaceResponse, err := client.GetNamespace(ctx, namespaceRequest)
|
||||
if err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()),
|
||||
output,
|
||||
)
|
||||
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
return output, machineResponse.GetMachine(), namespaceResponse.GetNamespace(), nil
|
||||
}
|
||||
|
||||
var shareMachineCmd = &cobra.Command{
|
||||
Use: "share",
|
||||
Short: "Shares a node from the current namespace to the specified one",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, machine, namespace, err := sharingWorker(cmd)
|
||||
if err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
|
||||
output,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx, client, conn, cancel := getHeadscaleCLIClient()
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
|
||||
request := &v1.ShareMachineRequest{
|
||||
MachineId: machine.Id,
|
||||
Namespace: namespace.Name,
|
||||
}
|
||||
|
||||
response, err := client.ShareMachine(ctx, request)
|
||||
if err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
fmt.Sprintf("Error sharing node: %s", status.Convert(err).Message()),
|
||||
output,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
SuccessOutput(response.Machine, "Node shared", output)
|
||||
},
|
||||
}
|
||||
|
||||
var unshareMachineCmd = &cobra.Command{
|
||||
Use: "unshare",
|
||||
Short: "Unshares a node from the specified namespace",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, machine, namespace, err := sharingWorker(cmd)
|
||||
if err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
|
||||
output,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx, client, conn, cancel := getHeadscaleCLIClient()
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
|
||||
request := &v1.UnshareMachineRequest{
|
||||
MachineId: machine.Id,
|
||||
Namespace: namespace.Name,
|
||||
}
|
||||
|
||||
response, err := client.UnshareMachine(ctx, request)
|
||||
if err != nil {
|
||||
ErrorOutput(
|
||||
err,
|
||||
fmt.Sprintf("Error unsharing node: %s", status.Convert(err).Message()),
|
||||
output,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
SuccessOutput(response.Machine, "Node unshared", output)
|
||||
},
|
||||
}
|
||||
|
||||
func nodesToPtables(
|
||||
currentNamespace string,
|
||||
machines []*v1.Machine,
|
||||
|
|
|
@ -35,13 +35,15 @@ func init() {
|
|||
}
|
||||
|
||||
var preauthkeysCmd = &cobra.Command{
|
||||
Use: "preauthkeys",
|
||||
Short: "Handle the preauthkeys in Headscale",
|
||||
Use: "preauthkeys",
|
||||
Short: "Handle the preauthkeys in Headscale",
|
||||
Aliases: []string{"preauthkey", "authkey", "pre"},
|
||||
}
|
||||
|
||||
var listPreAuthKeys = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List the preauthkeys for this namespace",
|
||||
Use: "list",
|
||||
Short: "List the preauthkeys for this namespace",
|
||||
Aliases: []string{"ls", "show"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
|
@ -118,8 +120,9 @@ var listPreAuthKeys = &cobra.Command{
|
|||
}
|
||||
|
||||
var createPreAuthKeyCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Creates a new preauthkey in the specified namespace",
|
||||
Use: "create",
|
||||
Short: "Creates a new preauthkey in the specified namespace",
|
||||
Aliases: []string{"c", "new"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
|
@ -172,8 +175,9 @@ var createPreAuthKeyCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
var expirePreAuthKeyCmd = &cobra.Command{
|
||||
Use: "expire KEY",
|
||||
Short: "Expire a preauthkey",
|
||||
Use: "expire KEY",
|
||||
Short: "Expire a preauthkey",
|
||||
Aliases: []string{"revoke", "exp", "e"},
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errMissingParameter
|
||||
|
|
|
@ -35,13 +35,15 @@ func init() {
|
|||
}
|
||||
|
||||
var routesCmd = &cobra.Command{
|
||||
Use: "routes",
|
||||
Short: "Manage the routes of Headscale",
|
||||
Use: "routes",
|
||||
Short: "Manage the routes of Headscale",
|
||||
Aliases: []string{"r", "route"},
|
||||
}
|
||||
|
||||
var listRoutesCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List routes advertised and enabled by a given node",
|
||||
Use: "list",
|
||||
Short: "List routes advertised and enabled by a given node",
|
||||
Aliases: []string{"ls", "show"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -19,12 +18,12 @@ var serveCmd = &cobra.Command{
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
h, err := getHeadscaleApp()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
log.Fatal().Caller().Err(err).Msg("Error initializing")
|
||||
}
|
||||
|
||||
err = h.Serve()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
log.Fatal().Caller().Err(err).Msg("Error starting server")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -50,6 +49,7 @@ func LoadConfig(path string) error {
|
|||
|
||||
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
|
||||
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
|
||||
viper.SetDefault("tls_client_auth_mode", "relaxed")
|
||||
|
||||
viper.SetDefault("log_level", "info")
|
||||
|
||||
|
@ -64,6 +64,8 @@ func LoadConfig(path string) error {
|
|||
viper.SetDefault("cli.timeout", "5s")
|
||||
viper.SetDefault("cli.insecure", false)
|
||||
|
||||
viper.SetDefault("oidc.strip_email_domain", true)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("fatal error reading config file: %w", err)
|
||||
}
|
||||
|
@ -92,6 +94,20 @@ func LoadConfig(path string) error {
|
|||
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
|
||||
errorText += "Fatal config error: server_url must start with https:// or http://\n"
|
||||
}
|
||||
|
||||
_, authModeValid := headscale.LookupTLSClientAuthMode(
|
||||
viper.GetString("tls_client_auth_mode"),
|
||||
)
|
||||
|
||||
if !authModeValid {
|
||||
errorText += fmt.Sprintf(
|
||||
"Invalid tls_client_auth_mode supplied: %s. Accepted values: %s, %s, %s.",
|
||||
viper.GetString("tls_client_auth_mode"),
|
||||
headscale.DisabledClientAuth,
|
||||
headscale.RelaxedClientAuth,
|
||||
headscale.EnforcedClientAuth)
|
||||
}
|
||||
|
||||
if errorText != "" {
|
||||
//nolint
|
||||
return errors.New(strings.TrimSuffix(errorText, "\n"))
|
||||
|
@ -101,6 +117,13 @@ func LoadConfig(path string) error {
|
|||
}
|
||||
|
||||
func GetDERPConfig() headscale.DERPConfig {
|
||||
serverEnabled := viper.GetBool("derp.server.enabled")
|
||||
serverRegionID := viper.GetInt("derp.server.region_id")
|
||||
serverRegionCode := viper.GetString("derp.server.region_code")
|
||||
serverRegionName := viper.GetString("derp.server.region_name")
|
||||
stunEnabled := viper.GetBool("derp.server.stun.enabled")
|
||||
stunAddr := viper.GetString("derp.server.stun.listen_addr")
|
||||
|
||||
urlStrs := viper.GetStringSlice("derp.urls")
|
||||
|
||||
urls := make([]url.URL, len(urlStrs))
|
||||
|
@ -122,10 +145,16 @@ func GetDERPConfig() headscale.DERPConfig {
|
|||
updateFrequency := viper.GetDuration("derp.update_frequency")
|
||||
|
||||
return headscale.DERPConfig{
|
||||
URLs: urls,
|
||||
Paths: paths,
|
||||
AutoUpdate: autoUpdate,
|
||||
UpdateFrequency: updateFrequency,
|
||||
ServerEnabled: serverEnabled,
|
||||
ServerRegionID: serverRegionID,
|
||||
ServerRegionCode: serverRegionCode,
|
||||
ServerRegionName: serverRegionName,
|
||||
STUNEnabled: stunEnabled,
|
||||
STUNAddr: stunAddr,
|
||||
URLs: urls,
|
||||
Paths: paths,
|
||||
AutoUpdate: autoUpdate,
|
||||
UpdateFrequency: updateFrequency,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,9 +310,14 @@ func getHeadscaleConfig() headscale.Config {
|
|||
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
|
||||
}
|
||||
|
||||
tlsClientAuthMode, _ := headscale.LookupTLSClientAuthMode(
|
||||
viper.GetString("tls_client_auth_mode"),
|
||||
)
|
||||
|
||||
return headscale.Config{
|
||||
ServerURL: viper.GetString("server_url"),
|
||||
Addr: viper.GetString("listen_addr"),
|
||||
MetricsAddr: viper.GetString("metrics_listen_addr"),
|
||||
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
||||
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
|
||||
|
||||
|
@ -312,8 +346,9 @@ func getHeadscaleConfig() headscale.Config {
|
|||
),
|
||||
TLSLetsEncryptChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
|
||||
|
||||
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
|
||||
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
|
||||
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
|
||||
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
|
||||
TLSClientAuthMode: tlsClientAuthMode,
|
||||
|
||||
DNSConfig: dnsConfig,
|
||||
|
||||
|
@ -324,9 +359,10 @@ func getHeadscaleConfig() headscale.Config {
|
|||
UnixSocketPermission: GetFileMode("unix_socket_permission"),
|
||||
|
||||
OIDC: headscale.OIDCConfig{
|
||||
Issuer: viper.GetString("oidc.issuer"),
|
||||
ClientID: viper.GetString("oidc.client_id"),
|
||||
ClientSecret: viper.GetString("oidc.client_secret"),
|
||||
Issuer: viper.GetString("oidc.issuer"),
|
||||
ClientID: viper.GetString("oidc.client_id"),
|
||||
ClientSecret: viper.GetString("oidc.client_secret"),
|
||||
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
||||
},
|
||||
|
||||
CLI: headscale.CLIConfig{
|
||||
|
@ -356,8 +392,6 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
|||
|
||||
cfg := getHeadscaleConfig()
|
||||
|
||||
cfg.OIDC.MatchMap = loadOIDCMatchMap()
|
||||
|
||||
app, err := headscale.NewHeadscale(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -514,18 +548,6 @@ func (tokenAuth) RequireTransportSecurity() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// loadOIDCMatchMap is a wrapper around viper to verifies that the keys in
|
||||
// the match map is valid regex strings.
|
||||
func loadOIDCMatchMap() map[string]string {
|
||||
strMap := viper.GetStringMapString("oidc.domain_map")
|
||||
|
||||
for oidcMatcher := range strMap {
|
||||
_ = regexp.MustCompile(oidcMatcher)
|
||||
}
|
||||
|
||||
return strMap
|
||||
}
|
||||
|
||||
func GetFileMode(key string) fs.FileMode {
|
||||
modeStr := viper.GetString(key)
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
|
|||
// Test that config file was interpreted correctly
|
||||
c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080")
|
||||
c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080")
|
||||
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
|
||||
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
|
||||
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
|
||||
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
|
||||
|
|
|
@ -16,6 +16,12 @@ server_url: http://127.0.0.1:8080
|
|||
#
|
||||
listen_addr: 0.0.0.0:8080
|
||||
|
||||
# Address to listen to /metrics, you may want
|
||||
# to keep this endpoint private to your internal
|
||||
# network
|
||||
#
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
|
||||
# Address to listen for gRPC.
|
||||
# gRPC is used for controlling a headscale server
|
||||
# remotely with the CLI
|
||||
|
@ -49,6 +55,26 @@ ip_prefixes:
|
|||
# headscale needs a list of DERP servers that can be presented
|
||||
# to the clients.
|
||||
derp:
|
||||
server:
|
||||
# If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
|
||||
# The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
|
||||
enabled: false
|
||||
|
||||
# Region ID to use for the embedded DERP server.
|
||||
# The local DERP prevails if the region ID collides with other region ID coming from
|
||||
# the regular DERP config.
|
||||
region_id: 999
|
||||
|
||||
# Region code and name are displayed in the Tailscale UI to identify a DERP region
|
||||
region_code: "headscale"
|
||||
region_name: "Headscale Embedded DERP"
|
||||
|
||||
# If enabled, also listens in UDP at the configured address for STUN connections to help on NAT traversal
|
||||
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
|
||||
stun:
|
||||
enabled: false
|
||||
listen_addr: "0.0.0.0:3478"
|
||||
|
||||
# List of externally available DERP maps encoded in JSON
|
||||
urls:
|
||||
- https://controlplane.tailscale.com/derpmap/default
|
||||
|
@ -105,12 +131,19 @@ acme_email: ""
|
|||
# Domain name to request a TLS certificate for:
|
||||
tls_letsencrypt_hostname: ""
|
||||
|
||||
# Client (Tailscale/Browser) authentication mode (mTLS)
|
||||
# Acceptable values:
|
||||
# - disabled: client authentication disabled
|
||||
# - relaxed: client certificate is required but not verified
|
||||
# - enforced: client certificate is required and verified
|
||||
tls_client_auth_mode: relaxed
|
||||
|
||||
# Path to store certificates and metadata needed by
|
||||
# letsencrypt
|
||||
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
|
||||
|
||||
# Type of ACME challenge to use, currently supported types:
|
||||
# HTTP-01 or TLS_ALPN-01
|
||||
# HTTP-01 or TLS-ALPN-01
|
||||
# See [docs/tls.md](docs/tls.md) for more information
|
||||
tls_letsencrypt_challenge_type: HTTP-01
|
||||
# When HTTP-01 challenge is chosen, letsencrypt must set up a
|
||||
|
@ -125,7 +158,8 @@ tls_key_path: ""
|
|||
log_level: info
|
||||
|
||||
# Path to a file containg ACL policies.
|
||||
# Recommended path: /etc/headscale/acl.hujson
|
||||
# ACLs can be defined as YAML or HUJSON.
|
||||
# https://tailscale.com/kb/1018/acls/
|
||||
acl_policy_path: ""
|
||||
|
||||
## DNS
|
||||
|
@ -180,7 +214,9 @@ unix_socket_permission: "0770"
|
|||
# client_id: "your-oidc-client-id"
|
||||
# client_secret: "your-oidc-client-secret"
|
||||
#
|
||||
# # Domain map is used to map incomming users (by their email) to
|
||||
# # a namespace. The key can be a string, or regex.
|
||||
# domain_map:
|
||||
# ".*": default-namespace
|
||||
# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
|
||||
# This will transform `first-name.last-name@example.com` to the namespace `first-name.last-name`
|
||||
# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
|
||||
# namespace: `first-name.last-name.example.com`
|
||||
#
|
||||
# strip_email_domain: true
|
||||
|
|
139
db.go
139
db.go
|
@ -1,12 +1,19 @@
|
|||
package headscale
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -33,6 +40,38 @@ func (h *Headscale) initDB() error {
|
|||
|
||||
_ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")
|
||||
|
||||
// If the Machine table has a column for registered,
|
||||
// find all occourences of "false" and drop them. Then
|
||||
// remove the column.
|
||||
if db.Migrator().HasColumn(&Machine{}, "registered") {
|
||||
log.Info().
|
||||
Msg(`Database has legacy "registered" column in machine, removing...`)
|
||||
|
||||
machines := Machines{}
|
||||
if err := h.db.Not("registered").Find(&machines).Error; err != nil {
|
||||
log.Error().Err(err).Msg("Error accessing db")
|
||||
}
|
||||
|
||||
for _, machine := range machines {
|
||||
log.Info().
|
||||
Str("machine", machine.Name).
|
||||
Str("machine_key", machine.MachineKey).
|
||||
Msg("Deleting unregistered machine")
|
||||
if err := h.db.Delete(&Machine{}, machine.ID).Error; err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("machine", machine.Name).
|
||||
Str("machine_key", machine.MachineKey).
|
||||
Msg("Error deleting unregistered machine")
|
||||
}
|
||||
}
|
||||
|
||||
err := db.Migrator().DropColumn(&Machine{}, "registered")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error dropping registered column")
|
||||
}
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(&Machine{})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -53,10 +92,7 @@ func (h *Headscale) initDB() error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(&SharedMachine{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = db.Migrator().DropTable("shared_machines")
|
||||
|
||||
err = db.AutoMigrate(&APIKey{})
|
||||
if err != nil {
|
||||
|
@ -81,10 +117,24 @@ func (h *Headscale) openDB() (*gorm.DB, error) {
|
|||
|
||||
switch h.dbType {
|
||||
case Sqlite:
|
||||
db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
Logger: log,
|
||||
})
|
||||
db, err = gorm.Open(
|
||||
sqlite.Open(h.dbString+"?_synchronous=1&_journal_mode=WAL"),
|
||||
&gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
Logger: log,
|
||||
},
|
||||
)
|
||||
|
||||
db.Exec("PRAGMA foreign_keys=ON")
|
||||
|
||||
// The pure Go SQLite library does not handle locking in
|
||||
// the same way as the C based one and we cant use the gorm
|
||||
// connection pool as of 2022/02/23.
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxIdleConns(1)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
sqlDB.SetConnMaxIdleTime(time.Hour)
|
||||
|
||||
case Postgres:
|
||||
db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
|
@ -129,3 +179,74 @@ func (h *Headscale) setValue(key string, value string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is a "wrapper" type around tailscales
|
||||
// Hostinfo to allow us to add database "serialization"
|
||||
// methods. This allows us to use a typed values throughout
|
||||
// the code and not have to marshal/unmarshal and error
|
||||
// check all over the code.
|
||||
type HostInfo tailcfg.Hostinfo
|
||||
|
||||
func (hi *HostInfo) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(value, hi)
|
||||
|
||||
case string:
|
||||
return json.Unmarshal([]byte(value), hi)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
|
||||
}
|
||||
}
|
||||
|
||||
// Value return json value, implement driver.Valuer interface.
|
||||
func (hi HostInfo) Value() (driver.Value, error) {
|
||||
bytes, err := json.Marshal(hi)
|
||||
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
type IPPrefixes []netaddr.IPPrefix
|
||||
|
||||
func (i *IPPrefixes) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(value, i)
|
||||
|
||||
case string:
|
||||
return json.Unmarshal([]byte(value), i)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
|
||||
}
|
||||
}
|
||||
|
||||
// Value return json value, implement driver.Valuer interface.
|
||||
func (i IPPrefixes) Value() (driver.Value, error) {
|
||||
bytes, err := json.Marshal(i)
|
||||
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
type StringList []string
|
||||
|
||||
func (i *StringList) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(value, i)
|
||||
|
||||
case string:
|
||||
return json.Unmarshal([]byte(value), i)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination)
|
||||
}
|
||||
}
|
||||
|
||||
// Value return json value, implement driver.Valuer interface.
|
||||
func (i StringList) Value() (driver.Value, error) {
|
||||
bytes, err := json.Marshal(i)
|
||||
|
||||
return string(bytes), err
|
||||
}
|
||||
|
|
1
derp.go
1
derp.go
|
@ -148,6 +148,7 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
|
|||
case <-ticker.C:
|
||||
log.Info().Msg("Fetching DERPMap updates")
|
||||
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
||||
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
||||
|
||||
namespaces, err := h.ListNamespaces()
|
||||
if err != nil {
|
||||
|
|
233
derp_server.go
Normal file
233
derp_server.go
Normal file
|
@ -0,0 +1,233 @@
|
|||
package headscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// fastStartHeader is the header (with value "1") that signals to the HTTP
|
||||
// server that the DERP HTTP client does not want the HTTP 101 response
|
||||
// headers and it will begin writing & reading the DERP protocol immediately
|
||||
// following its HTTP request.
|
||||
const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
type DERPServer struct {
|
||||
tailscaleDERP *derp.Server
|
||||
region tailcfg.DERPRegion
|
||||
}
|
||||
|
||||
func (h *Headscale) NewDERPServer() (*DERPServer, error) {
|
||||
server := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
|
||||
region, err := h.generateRegionLocalDERP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DERPServer{server, region}, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
|
||||
serverURL, err := url.Parse(h.cfg.ServerURL)
|
||||
if err != nil {
|
||||
return tailcfg.DERPRegion{}, err
|
||||
}
|
||||
var host string
|
||||
var port int
|
||||
host, portStr, err := net.SplitHostPort(serverURL.Host)
|
||||
if err != nil {
|
||||
if serverURL.Scheme == "https" {
|
||||
host = serverURL.Host
|
||||
port = 443
|
||||
} else {
|
||||
host = serverURL.Host
|
||||
port = 80
|
||||
}
|
||||
} else {
|
||||
port, err = strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return tailcfg.DERPRegion{}, err
|
||||
}
|
||||
}
|
||||
|
||||
localDERPregion := tailcfg.DERPRegion{
|
||||
RegionID: h.cfg.DERP.ServerRegionID,
|
||||
RegionCode: h.cfg.DERP.ServerRegionCode,
|
||||
RegionName: h.cfg.DERP.ServerRegionName,
|
||||
Avoid: false,
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: fmt.Sprintf("%d", h.cfg.DERP.ServerRegionID),
|
||||
RegionID: h.cfg.DERP.ServerRegionID,
|
||||
HostName: host,
|
||||
DERPPort: port,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if h.cfg.DERP.STUNEnabled {
|
||||
_, portStr, err := net.SplitHostPort(h.cfg.DERP.STUNAddr)
|
||||
if err != nil {
|
||||
return tailcfg.DERPRegion{}, err
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return tailcfg.DERPRegion{}, err
|
||||
}
|
||||
localDERPregion.Nodes[0].STUNPort = port
|
||||
}
|
||||
|
||||
return localDERPregion, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) DERPHandler(ctx *gin.Context) {
|
||||
log.Trace().Caller().Msgf("/derp request from %v", ctx.ClientIP())
|
||||
up := strings.ToLower(ctx.Request.Header.Get("Upgrade"))
|
||||
if up != "websocket" && up != "derp" {
|
||||
if up != "" {
|
||||
log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
|
||||
}
|
||||
ctx.String(http.StatusUpgradeRequired, "DERP requires connection upgrade")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fastStart := ctx.Request.Header.Get(fastStartHeader) == "1"
|
||||
|
||||
hijacker, ok := ctx.Writer.(http.Hijacker)
|
||||
if !ok {
|
||||
log.Error().Caller().Msg("DERP requires Hijacker interface from Gin")
|
||||
ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
netConn, conn, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
log.Error().Caller().Err(err).Msgf("Hijack failed")
|
||||
ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !fastStart {
|
||||
pubKey := h.privateKey.Public()
|
||||
pubKeyStr := pubKey.UntypedHexString() // nolint
|
||||
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
||||
"Upgrade: DERP\r\n"+
|
||||
"Connection: Upgrade\r\n"+
|
||||
"Derp-Version: %v\r\n"+
|
||||
"Derp-Public-Key: %s\r\n\r\n",
|
||||
derp.ProtocolVersion,
|
||||
pubKeyStr)
|
||||
}
|
||||
|
||||
h.DERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||
}
|
||||
|
||||
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
|
||||
switch ctx.Request.Method {
|
||||
case "HEAD", "GET":
|
||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
default:
|
||||
ctx.String(http.StatusMethodNotAllowed, "bogus probe method")
|
||||
}
|
||||
}
|
||||
|
||||
// DERPBootstrapDNSHandler implements the /bootsrap-dns endpoint
|
||||
// Described in https://github.com/tailscale/tailscale/issues/1405,
|
||||
// this endpoint provides a way to help a client when it fails to start up
|
||||
// because its DNS are broken.
|
||||
// The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
|
||||
// They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
|
||||
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
|
||||
func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
|
||||
dnsEntries := make(map[string][]net.IP)
|
||||
|
||||
resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
var r net.Resolver
|
||||
for _, region := range h.DERPMap.Regions {
|
||||
for _, node := range region.Nodes { // we don't care if we override some nodes
|
||||
addrs, err := r.LookupIP(resolvCtx, "ip", node.HostName)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msgf("bootstrap DNS lookup failed %q", node.HostName)
|
||||
|
||||
continue
|
||||
}
|
||||
dnsEntries[node.HostName] = addrs
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, dnsEntries)
|
||||
}
|
||||
|
||||
// ServeSTUN starts a STUN server on the configured addr.
|
||||
func (h *Headscale) ServeSTUN() {
|
||||
packetConn, err := net.ListenPacket("udp", h.cfg.DERP.STUNAddr)
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("failed to open STUN listener: %v", err)
|
||||
}
|
||||
log.Info().Msgf("STUN server started at %s", packetConn.LocalAddr())
|
||||
|
||||
udpConn, ok := packetConn.(*net.UDPConn)
|
||||
if !ok {
|
||||
log.Fatal().Msg("STUN listener is not a UDP listener")
|
||||
}
|
||||
serverSTUNListener(context.Background(), udpConn)
|
||||
}
|
||||
|
||||
func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
|
||||
var buf [64 << 10]byte
|
||||
var (
|
||||
bytesRead int
|
||||
udpAddr *net.UDPAddr
|
||||
err error
|
||||
)
|
||||
for {
|
||||
bytesRead, udpAddr, err = packetConn.ReadFromUDP(buf[:])
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Error().Caller().Err(err).Msgf("STUN ReadFrom")
|
||||
time.Sleep(time.Second)
|
||||
|
||||
continue
|
||||
}
|
||||
log.Trace().Caller().Msgf("STUN request from %v", udpAddr)
|
||||
pkt := buf[:bytesRead]
|
||||
if !stun.Is(pkt) {
|
||||
log.Trace().Caller().Msgf("UDP packet is not STUN")
|
||||
|
||||
continue
|
||||
}
|
||||
txid, err := stun.ParseBindingRequest(pkt)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msgf("STUN parse error")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
res := stun.Response(txid, udpAddr.IP, uint16(udpAddr.Port))
|
||||
_, err = packetConn.WriteTo(res, udpAddr)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msgf("Issue writing to UDP")
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
16
dns.go
16
dns.go
|
@ -163,7 +163,11 @@ func getMapResponseDNSConfig(
|
|||
dnsConfig = dnsConfigOrig.Clone()
|
||||
dnsConfig.Domains = append(
|
||||
dnsConfig.Domains,
|
||||
fmt.Sprintf("%s.%s", machine.Namespace.Name, baseDomain),
|
||||
fmt.Sprintf(
|
||||
"%s.%s",
|
||||
machine.Namespace.Name,
|
||||
baseDomain,
|
||||
),
|
||||
)
|
||||
|
||||
namespaceSet := set.New(set.ThreadSafe)
|
||||
|
@ -171,8 +175,14 @@ func getMapResponseDNSConfig(
|
|||
for _, p := range peers {
|
||||
namespaceSet.Add(p.Namespace)
|
||||
}
|
||||
for _, namespace := range namespaceSet.List() {
|
||||
dnsRoute := fmt.Sprintf("%s.%s", namespace.(Namespace).Name, baseDomain)
|
||||
for _, ns := range namespaceSet.List() {
|
||||
namespace, ok := ns.(Namespace)
|
||||
if !ok {
|
||||
dnsConfig = dnsConfigOrig
|
||||
|
||||
continue
|
||||
}
|
||||
dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain)
|
||||
dnsConfig.Routes[dnsRoute] = nil
|
||||
}
|
||||
} else {
|
||||
|
|
19
dns_test.go
19
dns_test.go
|
@ -164,7 +164,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: namespaceShared1.ID,
|
||||
Namespace: *namespaceShared1,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
|
||||
AuthKeyID: uint(preAuthKeyInShared1.ID),
|
||||
|
@ -182,7 +181,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: namespaceShared2.ID,
|
||||
Namespace: *namespaceShared2,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
|
||||
AuthKeyID: uint(preAuthKeyInShared2.ID),
|
||||
|
@ -200,7 +198,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||
Name: "test_get_shared_nodes_3",
|
||||
NamespaceID: namespaceShared3.ID,
|
||||
Namespace: *namespaceShared3,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
|
||||
AuthKeyID: uint(preAuthKeyInShared3.ID),
|
||||
|
@ -218,16 +215,12 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: namespaceShared1.ID,
|
||||
Namespace: *namespaceShared1,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
|
||||
AuthKeyID: uint(PreAuthKey2InShared1.ID),
|
||||
}
|
||||
app.db.Save(machine2InShared1)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machineInShared2, namespaceShared1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
baseDomain := "foobar.headscale.net"
|
||||
dnsConfigOrig := tailcfg.DNSConfig{
|
||||
Routes: make(map[string][]dnstype.Resolver),
|
||||
|
@ -245,7 +238,8 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||
peersOfMachineInShared1,
|
||||
)
|
||||
c.Assert(dnsConfig, check.NotNil)
|
||||
c.Assert(len(dnsConfig.Routes), check.Equals, 2)
|
||||
|
||||
c.Assert(len(dnsConfig.Routes), check.Equals, 3)
|
||||
|
||||
domainRouteShared1 := fmt.Sprintf("%s.%s", namespaceShared1.Name, baseDomain)
|
||||
_, ok := dnsConfig.Routes[domainRouteShared1]
|
||||
|
@ -257,7 +251,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||
|
||||
domainRouteShared3 := fmt.Sprintf("%s.%s", namespaceShared3.Name, baseDomain)
|
||||
_, ok = dnsConfig.Routes[domainRouteShared3]
|
||||
c.Assert(ok, check.Equals, false)
|
||||
c.Assert(ok, check.Equals, true)
|
||||
}
|
||||
|
||||
func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
||||
|
@ -313,7 +307,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: namespaceShared1.ID,
|
||||
Namespace: *namespaceShared1,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
|
||||
AuthKeyID: uint(preAuthKeyInShared1.ID),
|
||||
|
@ -331,7 +324,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: namespaceShared2.ID,
|
||||
Namespace: *namespaceShared2,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
|
||||
AuthKeyID: uint(preAuthKeyInShared2.ID),
|
||||
|
@ -349,7 +341,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||
Name: "test_get_shared_nodes_3",
|
||||
NamespaceID: namespaceShared3.ID,
|
||||
Namespace: *namespaceShared3,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
|
||||
AuthKeyID: uint(preAuthKeyInShared3.ID),
|
||||
|
@ -367,16 +358,12 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: namespaceShared1.ID,
|
||||
Namespace: *namespaceShared1,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
|
||||
AuthKeyID: uint(preAuthKey2InShared1.ID),
|
||||
}
|
||||
app.db.Save(machine2InShared1)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machineInShared2, namespaceShared1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
baseDomain := "foobar.headscale.net"
|
||||
dnsConfigOrig := tailcfg.DNSConfig{
|
||||
Routes: make(map[string][]dnstype.Resolver),
|
||||
|
|
|
@ -10,7 +10,7 @@ please ask on [Discord](https://discord.gg/XcQxk2VHjx) instead of opening an Iss
|
|||
### How-to
|
||||
|
||||
- [Running headscale on Linux](running-headscale-linux.md)
|
||||
- [Control headscale remotly](remote-cli.md)
|
||||
- [Control headscale remotely](remote-cli.md)
|
||||
- [Using a Windows client with headscale](windows-client.md)
|
||||
|
||||
### References
|
||||
|
@ -39,6 +39,14 @@ use namespaces (which are the equivalent to user/logins in Tailscale.com).
|
|||
|
||||
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
|
||||
|
||||
When using ACL's the Namespace borders are no longer applied. All machines
|
||||
whichever the Namespace have the ability to communicate with other hosts as
|
||||
long as the ACL's permits this exchange.
|
||||
|
||||
The [ACLs](acls.md) document should help understand a fictional case of setting
|
||||
up ACLs in a small company. All concepts presented in this document could be
|
||||
applied outside of business oriented usage.
|
||||
|
||||
### Apple devices
|
||||
|
||||
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.
|
||||
|
|
141
docs/acls.md
Normal file
141
docs/acls.md
Normal file
|
@ -0,0 +1,141 @@
|
|||
# ACLs use case example
|
||||
|
||||
Let's build an example use case for a small business (It may be the place where
|
||||
ACL's are the most useful).
|
||||
|
||||
We have a small company with a boss, an admin, two developers and an intern.
|
||||
|
||||
The boss should have access to all servers but not to the users hosts. Admin
|
||||
should also have access to all hosts except that their permissions should be
|
||||
limited to maintaining the hosts (for example purposes). The developers can do
|
||||
anything they want on dev hosts, but only watch on productions hosts. Intern
|
||||
can only interact with the development servers.
|
||||
|
||||
Each user have at least a device connected to the network and we have some
|
||||
servers.
|
||||
|
||||
- database.prod
|
||||
- database.dev
|
||||
- app-server1.prod
|
||||
- app-server1.dev
|
||||
- billing.internal
|
||||
|
||||
## Setup of the network
|
||||
|
||||
Let's create the namespaces. Each user should have his own namespace. The users
|
||||
here are represented as namespaces.
|
||||
|
||||
```bash
|
||||
headscale namespaces create boss
|
||||
headscale namespaces create admin1
|
||||
headscale namespaces create dev1
|
||||
headscale namespaces create dev2
|
||||
headscale namespaces create intern1
|
||||
```
|
||||
|
||||
We don't need to create namespaces for the servers because the servers will be
|
||||
tagged. When registering the servers we will need to add the flag
|
||||
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
||||
registering the server should be allowed to do it. Since anyone can add tags to
|
||||
a server they can register, the check of the tags is done on headscale server
|
||||
and only valid tags are applied. A tag is valid if the namespace that is
|
||||
registering it is allowed to do it.
|
||||
|
||||
Here are the ACL's to implement the same permissions as above:
|
||||
|
||||
```json
|
||||
{
|
||||
// groups are collections of users having a common scope. A user can be in multiple groups
|
||||
// groups cannot be composed of groups
|
||||
"groups": {
|
||||
"group:boss": ["boss"],
|
||||
"group:dev": ["dev1", "dev2"],
|
||||
"group:admin": ["admin1"],
|
||||
"group:intern": ["intern1"]
|
||||
},
|
||||
// tagOwners in tailscale is an association between a TAG and the people allowed to set this TAG on a server.
|
||||
// This is documented [here](https://tailscale.com/kb/1068/acl-tags#defining-a-tag)
|
||||
// and explained [here](https://tailscale.com/blog/rbac-like-it-was-meant-to-be/)
|
||||
"tagOwners": {
|
||||
// the administrators can add servers in production
|
||||
"tag:prod-databases": ["group:admin"],
|
||||
"tag:prod-app-servers": ["group:admin"],
|
||||
|
||||
// the boss can tag any server as internal
|
||||
"tag:internal": ["group:boss"],
|
||||
|
||||
// dev can add servers for dev purposes as well as admins
|
||||
"tag:dev-databases": ["group:admin", "group:dev"],
|
||||
"tag:dev-app-servers": ["group:admin", "group:dev"]
|
||||
|
||||
// interns cannot add servers
|
||||
},
|
||||
"acls": [
|
||||
// boss have access to all servers
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:boss"],
|
||||
"ports": [
|
||||
"tag:prod-databases:*",
|
||||
"tag:prod-app-servers:*",
|
||||
"tag:internal:*",
|
||||
"tag:dev-databases:*",
|
||||
"tag:dev-app-servers:*"
|
||||
]
|
||||
},
|
||||
|
||||
// admin have only access to administrative ports of the servers
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:admin"],
|
||||
"ports": [
|
||||
"tag:prod-databases:22",
|
||||
"tag:prod-app-servers:22",
|
||||
"tag:internal:22",
|
||||
"tag:dev-databases:22",
|
||||
"tag:dev-app-servers:22"
|
||||
]
|
||||
},
|
||||
|
||||
// developers have access to databases servers and application servers on all ports
|
||||
// they can only view the applications servers in prod and have no access to databases servers in production
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:dev"],
|
||||
"ports": [
|
||||
"tag:dev-databases:*",
|
||||
"tag:dev-app-servers:*",
|
||||
"tag:prod-app-servers:80,443"
|
||||
]
|
||||
},
|
||||
|
||||
// servers should be able to talk to database. Database should not be able to initiate connections to
|
||||
// applications servers
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["tag:dev-app-servers"],
|
||||
"ports": ["tag:dev-databases:5432"]
|
||||
},
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["tag:prod-app-servers"],
|
||||
"ports": ["tag:prod-databases:5432"]
|
||||
},
|
||||
|
||||
// interns have access to dev-app-servers only in reading mode
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:intern"],
|
||||
"ports": ["tag:dev-app-servers:80,443"]
|
||||
},
|
||||
|
||||
// We still have to allow internal namespaces communications since nothing guarantees that each user have
|
||||
// their own namespaces.
|
||||
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
|
||||
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
|
||||
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
|
||||
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
|
||||
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
|
||||
]
|
||||
}
|
||||
```
|
|
@ -5,4 +5,5 @@ metadata:
|
|||
data:
|
||||
server_url: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
|
||||
listen_addr: "0.0.0.0:8080"
|
||||
metrics_listen_addr: "127.0.0.1:9090"
|
||||
ephemeral_node_inactivity_timeout: "30m"
|
||||
|
|
|
@ -25,6 +25,11 @@ spec:
|
|||
configMapKeyRef:
|
||||
name: headscale-config
|
||||
key: listen_addr
|
||||
- name: METRICS_LISTEN_ADDR
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: headscale-config
|
||||
key: metrics_listen_addr
|
||||
- name: DERP_MAP_PATH
|
||||
value: /vol/config/derp.yaml
|
||||
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
|
||||
|
|
|
@ -26,6 +26,11 @@ spec:
|
|||
configMapKeyRef:
|
||||
name: headscale-config
|
||||
key: listen_addr
|
||||
- name: METRICS_LISTEN_ADDR
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: headscale-config
|
||||
key: metrics_listen_addr
|
||||
- name: DERP_MAP_PATH
|
||||
value: /vol/config/derp.yaml
|
||||
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# Glossary
|
||||
|
||||
- Namespace: Collection of Tailscale nodes that can see each other. In Tailscale.com this is called Tailnet.
|
||||
| Term | Description |
|
||||
| --------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| Machine | A machine is a single entity connected to `headscale`, typically an installation of Tailscale. Also known as **Node** |
|
||||
| Namespace | A namespace is a logical grouping of machines "owned" by the same entity, in Tailscale, this is typically a User |
|
||||
|
|
14
docs/tls.md
14
docs/tls.md
|
@ -29,3 +29,17 @@ headscale can also be configured to expose its web service via TLS. To configure
|
|||
tls_cert_path: ""
|
||||
tls_key_path: ""
|
||||
```
|
||||
|
||||
### Configuring Mutual TLS Authentication (mTLS)
|
||||
|
||||
mTLS is a method by which an HTTPS server authenticates clients, e.g. Tailscale, using TLS certificates. This can be configured by applying one of the following values to the `tls_client_auth_mode` setting in the configuration file.
|
||||
|
||||
| Value | Behavior |
|
||||
| ------------------- | ---------------------------------------------------------- |
|
||||
| `disabled` | Disable mTLS. |
|
||||
| `relaxed` (default) | A client certificate is required, but it is not verified. |
|
||||
| `enforced` | Requires clients to supply a certificate that is verified. |
|
||||
|
||||
```yaml
|
||||
tls_client_auth_mode: ""
|
||||
```
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -7,11 +7,10 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -37,7 +36,7 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
|
|||
0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76,
|
||||
0x31, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19,
|
||||
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69,
|
||||
0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xcb, 0x15, 0x0a, 0x10, 0x48, 0x65,
|
||||
0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xa3, 0x13, 0x0a, 0x10, 0x48, 0x65,
|
||||
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x77,
|
||||
0x0a, 0x0c, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x21,
|
||||
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65,
|
||||
|
@ -152,68 +151,50 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{
|
|||
0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12,
|
||||
0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||
0x12, 0x8d, 0x01, 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||
0x65, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30,
|
||||
0x22, 0x2e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||
0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73,
|
||||
0x68, 0x61, 0x72, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d,
|
||||
0x12, 0x95, 0x01, 0x0a, 0x0e, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68,
|
||||
0x69, 0x6e, 0x65, 0x12, 0x23, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
|
||||
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d,
|
||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38,
|
||||
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x32, 0x22, 0x30, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f,
|
||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||
0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x75, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x2f, 0x7b, 0x6e, 0x61,
|
||||
0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x12, 0x8b, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74,
|
||||
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x24, 0x2e, 0x68,
|
||||
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d,
|
||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74,
|
||||
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02,
|
||||
0x25, 0x12, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||
0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f,
|
||||
0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x97, 0x01, 0x0a, 0x13, 0x45, 0x6e, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x28,
|
||||
0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e,
|
||||
0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65,
|
||||
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
|
||||
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61,
|
||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x22, 0x23, 0x2f, 0x61, 0x70,
|
||||
0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61,
|
||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73,
|
||||
0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79,
|
||||
0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x22,
|
||||
0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x3a,
|
||||
0x01, 0x2a, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b,
|
||||
0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
|
||||
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65,
|
||||
0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02,
|
||||
0x1a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65,
|
||||
0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x6a, 0x0a, 0x0b, 0x4c,
|
||||
0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61,
|
||||
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70,
|
||||
0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68,
|
||||
0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
|
||||
0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
|
||||
0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 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,
|
||||
0x12, 0x8b, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52,
|
||||
0x6f, 0x75, 0x74, 0x65, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f,
|
||||
0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61,
|
||||
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63,
|
||||
0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x12, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||
0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68,
|
||||
0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x97,
|
||||
0x01, 0x0a, 0x13, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||
0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x28, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
|
||||
0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68,
|
||||
0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x29, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x6f, 0x75,
|
||||
0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4,
|
||||
0x93, 0x02, 0x25, 0x22, 0x23, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x63,
|
||||
0x68, 0x69, 0x6e, 0x65, 0x2f, 0x7b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64,
|
||||
0x7d, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61,
|
||||
0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
|
||||
0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70,
|
||||
0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65,
|
||||
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74,
|
||||
0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x22, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
|
||||
0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x3a, 0x01, 0x2a, 0x12, 0x77, 0x0a, 0x0c, 0x45, 0x78,
|
||||
0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61,
|
||||
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65,
|
||||
0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e,
|
||||
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70,
|
||||
0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||
0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65,
|
||||
0x3a, 0x01, 0x2a, 0x12, 0x6a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65,
|
||||
0x79, 0x73, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12,
|
||||
0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x42,
|
||||
0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75,
|
||||
0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
|
||||
0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
}
|
||||
|
||||
var file_headscale_v1_headscale_proto_goTypes = []interface{}{
|
||||
|
@ -231,34 +212,30 @@ var file_headscale_v1_headscale_proto_goTypes = []interface{}{
|
|||
(*DeleteMachineRequest)(nil), // 11: headscale.v1.DeleteMachineRequest
|
||||
(*ExpireMachineRequest)(nil), // 12: headscale.v1.ExpireMachineRequest
|
||||
(*ListMachinesRequest)(nil), // 13: headscale.v1.ListMachinesRequest
|
||||
(*ShareMachineRequest)(nil), // 14: headscale.v1.ShareMachineRequest
|
||||
(*UnshareMachineRequest)(nil), // 15: headscale.v1.UnshareMachineRequest
|
||||
(*GetMachineRouteRequest)(nil), // 16: headscale.v1.GetMachineRouteRequest
|
||||
(*EnableMachineRoutesRequest)(nil), // 17: headscale.v1.EnableMachineRoutesRequest
|
||||
(*CreateApiKeyRequest)(nil), // 18: headscale.v1.CreateApiKeyRequest
|
||||
(*ExpireApiKeyRequest)(nil), // 19: headscale.v1.ExpireApiKeyRequest
|
||||
(*ListApiKeysRequest)(nil), // 20: headscale.v1.ListApiKeysRequest
|
||||
(*GetNamespaceResponse)(nil), // 21: headscale.v1.GetNamespaceResponse
|
||||
(*CreateNamespaceResponse)(nil), // 22: headscale.v1.CreateNamespaceResponse
|
||||
(*RenameNamespaceResponse)(nil), // 23: headscale.v1.RenameNamespaceResponse
|
||||
(*DeleteNamespaceResponse)(nil), // 24: headscale.v1.DeleteNamespaceResponse
|
||||
(*ListNamespacesResponse)(nil), // 25: headscale.v1.ListNamespacesResponse
|
||||
(*CreatePreAuthKeyResponse)(nil), // 26: headscale.v1.CreatePreAuthKeyResponse
|
||||
(*ExpirePreAuthKeyResponse)(nil), // 27: headscale.v1.ExpirePreAuthKeyResponse
|
||||
(*ListPreAuthKeysResponse)(nil), // 28: headscale.v1.ListPreAuthKeysResponse
|
||||
(*DebugCreateMachineResponse)(nil), // 29: headscale.v1.DebugCreateMachineResponse
|
||||
(*GetMachineResponse)(nil), // 30: headscale.v1.GetMachineResponse
|
||||
(*RegisterMachineResponse)(nil), // 31: headscale.v1.RegisterMachineResponse
|
||||
(*DeleteMachineResponse)(nil), // 32: headscale.v1.DeleteMachineResponse
|
||||
(*ExpireMachineResponse)(nil), // 33: headscale.v1.ExpireMachineResponse
|
||||
(*ListMachinesResponse)(nil), // 34: headscale.v1.ListMachinesResponse
|
||||
(*ShareMachineResponse)(nil), // 35: headscale.v1.ShareMachineResponse
|
||||
(*UnshareMachineResponse)(nil), // 36: headscale.v1.UnshareMachineResponse
|
||||
(*GetMachineRouteResponse)(nil), // 37: headscale.v1.GetMachineRouteResponse
|
||||
(*EnableMachineRoutesResponse)(nil), // 38: headscale.v1.EnableMachineRoutesResponse
|
||||
(*CreateApiKeyResponse)(nil), // 39: headscale.v1.CreateApiKeyResponse
|
||||
(*ExpireApiKeyResponse)(nil), // 40: headscale.v1.ExpireApiKeyResponse
|
||||
(*ListApiKeysResponse)(nil), // 41: headscale.v1.ListApiKeysResponse
|
||||
(*GetMachineRouteRequest)(nil), // 14: headscale.v1.GetMachineRouteRequest
|
||||
(*EnableMachineRoutesRequest)(nil), // 15: headscale.v1.EnableMachineRoutesRequest
|
||||
(*CreateApiKeyRequest)(nil), // 16: headscale.v1.CreateApiKeyRequest
|
||||
(*ExpireApiKeyRequest)(nil), // 17: headscale.v1.ExpireApiKeyRequest
|
||||
(*ListApiKeysRequest)(nil), // 18: headscale.v1.ListApiKeysRequest
|
||||
(*GetNamespaceResponse)(nil), // 19: headscale.v1.GetNamespaceResponse
|
||||
(*CreateNamespaceResponse)(nil), // 20: headscale.v1.CreateNamespaceResponse
|
||||
(*RenameNamespaceResponse)(nil), // 21: headscale.v1.RenameNamespaceResponse
|
||||
(*DeleteNamespaceResponse)(nil), // 22: headscale.v1.DeleteNamespaceResponse
|
||||
(*ListNamespacesResponse)(nil), // 23: headscale.v1.ListNamespacesResponse
|
||||
(*CreatePreAuthKeyResponse)(nil), // 24: headscale.v1.CreatePreAuthKeyResponse
|
||||
(*ExpirePreAuthKeyResponse)(nil), // 25: headscale.v1.ExpirePreAuthKeyResponse
|
||||
(*ListPreAuthKeysResponse)(nil), // 26: headscale.v1.ListPreAuthKeysResponse
|
||||
(*DebugCreateMachineResponse)(nil), // 27: headscale.v1.DebugCreateMachineResponse
|
||||
(*GetMachineResponse)(nil), // 28: headscale.v1.GetMachineResponse
|
||||
(*RegisterMachineResponse)(nil), // 29: headscale.v1.RegisterMachineResponse
|
||||
(*DeleteMachineResponse)(nil), // 30: headscale.v1.DeleteMachineResponse
|
||||
(*ExpireMachineResponse)(nil), // 31: headscale.v1.ExpireMachineResponse
|
||||
(*ListMachinesResponse)(nil), // 32: headscale.v1.ListMachinesResponse
|
||||
(*GetMachineRouteResponse)(nil), // 33: headscale.v1.GetMachineRouteResponse
|
||||
(*EnableMachineRoutesResponse)(nil), // 34: headscale.v1.EnableMachineRoutesResponse
|
||||
(*CreateApiKeyResponse)(nil), // 35: headscale.v1.CreateApiKeyResponse
|
||||
(*ExpireApiKeyResponse)(nil), // 36: headscale.v1.ExpireApiKeyResponse
|
||||
(*ListApiKeysResponse)(nil), // 37: headscale.v1.ListApiKeysResponse
|
||||
}
|
||||
var file_headscale_v1_headscale_proto_depIdxs = []int32{
|
||||
0, // 0: headscale.v1.HeadscaleService.GetNamespace:input_type -> headscale.v1.GetNamespaceRequest
|
||||
|
@ -275,36 +252,32 @@ var file_headscale_v1_headscale_proto_depIdxs = []int32{
|
|||
11, // 11: headscale.v1.HeadscaleService.DeleteMachine:input_type -> headscale.v1.DeleteMachineRequest
|
||||
12, // 12: headscale.v1.HeadscaleService.ExpireMachine:input_type -> headscale.v1.ExpireMachineRequest
|
||||
13, // 13: headscale.v1.HeadscaleService.ListMachines:input_type -> headscale.v1.ListMachinesRequest
|
||||
14, // 14: headscale.v1.HeadscaleService.ShareMachine:input_type -> headscale.v1.ShareMachineRequest
|
||||
15, // 15: headscale.v1.HeadscaleService.UnshareMachine:input_type -> headscale.v1.UnshareMachineRequest
|
||||
16, // 16: headscale.v1.HeadscaleService.GetMachineRoute:input_type -> headscale.v1.GetMachineRouteRequest
|
||||
17, // 17: headscale.v1.HeadscaleService.EnableMachineRoutes:input_type -> headscale.v1.EnableMachineRoutesRequest
|
||||
18, // 18: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest
|
||||
19, // 19: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest
|
||||
20, // 20: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest
|
||||
21, // 21: headscale.v1.HeadscaleService.GetNamespace:output_type -> headscale.v1.GetNamespaceResponse
|
||||
22, // 22: headscale.v1.HeadscaleService.CreateNamespace:output_type -> headscale.v1.CreateNamespaceResponse
|
||||
23, // 23: headscale.v1.HeadscaleService.RenameNamespace:output_type -> headscale.v1.RenameNamespaceResponse
|
||||
24, // 24: headscale.v1.HeadscaleService.DeleteNamespace:output_type -> headscale.v1.DeleteNamespaceResponse
|
||||
25, // 25: headscale.v1.HeadscaleService.ListNamespaces:output_type -> headscale.v1.ListNamespacesResponse
|
||||
26, // 26: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
|
||||
27, // 27: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
|
||||
28, // 28: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
|
||||
29, // 29: headscale.v1.HeadscaleService.DebugCreateMachine:output_type -> headscale.v1.DebugCreateMachineResponse
|
||||
30, // 30: headscale.v1.HeadscaleService.GetMachine:output_type -> headscale.v1.GetMachineResponse
|
||||
31, // 31: headscale.v1.HeadscaleService.RegisterMachine:output_type -> headscale.v1.RegisterMachineResponse
|
||||
32, // 32: headscale.v1.HeadscaleService.DeleteMachine:output_type -> headscale.v1.DeleteMachineResponse
|
||||
33, // 33: headscale.v1.HeadscaleService.ExpireMachine:output_type -> headscale.v1.ExpireMachineResponse
|
||||
34, // 34: headscale.v1.HeadscaleService.ListMachines:output_type -> headscale.v1.ListMachinesResponse
|
||||
35, // 35: headscale.v1.HeadscaleService.ShareMachine:output_type -> headscale.v1.ShareMachineResponse
|
||||
36, // 36: headscale.v1.HeadscaleService.UnshareMachine:output_type -> headscale.v1.UnshareMachineResponse
|
||||
37, // 37: headscale.v1.HeadscaleService.GetMachineRoute:output_type -> headscale.v1.GetMachineRouteResponse
|
||||
38, // 38: headscale.v1.HeadscaleService.EnableMachineRoutes:output_type -> headscale.v1.EnableMachineRoutesResponse
|
||||
39, // 39: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse
|
||||
40, // 40: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
|
||||
41, // 41: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
|
||||
21, // [21:42] is the sub-list for method output_type
|
||||
0, // [0:21] is the sub-list for method input_type
|
||||
14, // 14: headscale.v1.HeadscaleService.GetMachineRoute:input_type -> headscale.v1.GetMachineRouteRequest
|
||||
15, // 15: headscale.v1.HeadscaleService.EnableMachineRoutes:input_type -> headscale.v1.EnableMachineRoutesRequest
|
||||
16, // 16: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest
|
||||
17, // 17: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest
|
||||
18, // 18: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest
|
||||
19, // 19: headscale.v1.HeadscaleService.GetNamespace:output_type -> headscale.v1.GetNamespaceResponse
|
||||
20, // 20: headscale.v1.HeadscaleService.CreateNamespace:output_type -> headscale.v1.CreateNamespaceResponse
|
||||
21, // 21: headscale.v1.HeadscaleService.RenameNamespace:output_type -> headscale.v1.RenameNamespaceResponse
|
||||
22, // 22: headscale.v1.HeadscaleService.DeleteNamespace:output_type -> headscale.v1.DeleteNamespaceResponse
|
||||
23, // 23: headscale.v1.HeadscaleService.ListNamespaces:output_type -> headscale.v1.ListNamespacesResponse
|
||||
24, // 24: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
|
||||
25, // 25: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
|
||||
26, // 26: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
|
||||
27, // 27: headscale.v1.HeadscaleService.DebugCreateMachine:output_type -> headscale.v1.DebugCreateMachineResponse
|
||||
28, // 28: headscale.v1.HeadscaleService.GetMachine:output_type -> headscale.v1.GetMachineResponse
|
||||
29, // 29: headscale.v1.HeadscaleService.RegisterMachine:output_type -> headscale.v1.RegisterMachineResponse
|
||||
30, // 30: headscale.v1.HeadscaleService.DeleteMachine:output_type -> headscale.v1.DeleteMachineResponse
|
||||
31, // 31: headscale.v1.HeadscaleService.ExpireMachine:output_type -> headscale.v1.ExpireMachineResponse
|
||||
32, // 32: headscale.v1.HeadscaleService.ListMachines:output_type -> headscale.v1.ListMachinesResponse
|
||||
33, // 33: headscale.v1.HeadscaleService.GetMachineRoute:output_type -> headscale.v1.GetMachineRouteResponse
|
||||
34, // 34: headscale.v1.HeadscaleService.EnableMachineRoutes:output_type -> headscale.v1.EnableMachineRoutesResponse
|
||||
35, // 35: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse
|
||||
36, // 36: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
|
||||
37, // 37: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
|
||||
19, // [19:38] is the sub-list for method output_type
|
||||
0, // [0:19] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
|
|
|
@ -625,150 +625,6 @@ func local_request_HeadscaleService_ListMachines_0(ctx context.Context, marshale
|
|||
|
||||
}
|
||||
|
||||
func request_HeadscaleService_ShareMachine_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ShareMachineRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["machine_id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
|
||||
}
|
||||
|
||||
protoReq.MachineId, err = runtime.Uint64(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
|
||||
}
|
||||
|
||||
val, ok = pathParams["namespace"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace")
|
||||
}
|
||||
|
||||
protoReq.Namespace, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err)
|
||||
}
|
||||
|
||||
msg, err := client.ShareMachine(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_HeadscaleService_ShareMachine_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ShareMachineRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["machine_id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
|
||||
}
|
||||
|
||||
protoReq.MachineId, err = runtime.Uint64(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
|
||||
}
|
||||
|
||||
val, ok = pathParams["namespace"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace")
|
||||
}
|
||||
|
||||
protoReq.Namespace, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err)
|
||||
}
|
||||
|
||||
msg, err := server.ShareMachine(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_HeadscaleService_UnshareMachine_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq UnshareMachineRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["machine_id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
|
||||
}
|
||||
|
||||
protoReq.MachineId, err = runtime.Uint64(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
|
||||
}
|
||||
|
||||
val, ok = pathParams["namespace"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace")
|
||||
}
|
||||
|
||||
protoReq.Namespace, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err)
|
||||
}
|
||||
|
||||
msg, err := client.UnshareMachine(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_HeadscaleService_UnshareMachine_0(ctx context.Context, marshaler runtime.Marshaler, server HeadscaleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq UnshareMachineRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["machine_id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "machine_id")
|
||||
}
|
||||
|
||||
protoReq.MachineId, err = runtime.Uint64(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "machine_id", err)
|
||||
}
|
||||
|
||||
val, ok = pathParams["namespace"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "namespace")
|
||||
}
|
||||
|
||||
protoReq.Namespace, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "namespace", err)
|
||||
}
|
||||
|
||||
msg, err := server.UnshareMachine(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_HeadscaleService_GetMachineRoute_0(ctx context.Context, marshaler runtime.Marshaler, client HeadscaleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetMachineRouteRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
@ -1305,52 +1161,6 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_HeadscaleService_ShareMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/ShareMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/share/{namespace}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_HeadscaleService_ShareMachine_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_HeadscaleService_ShareMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_HeadscaleService_UnshareMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/headscale.v1.HeadscaleService/UnshareMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/unshare/{namespace}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_HeadscaleService_UnshareMachine_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_HeadscaleService_UnshareMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_HeadscaleService_GetMachineRoute_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
@ -1787,46 +1597,6 @@ func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *runtime.Ser
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_HeadscaleService_ShareMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/ShareMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/share/{namespace}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_HeadscaleService_ShareMachine_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_HeadscaleService_ShareMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_HeadscaleService_UnshareMachine_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/headscale.v1.HeadscaleService/UnshareMachine", runtime.WithHTTPPathPattern("/api/v1/machine/{machine_id}/unshare/{namespace}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_HeadscaleService_UnshareMachine_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_HeadscaleService_UnshareMachine_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_HeadscaleService_GetMachineRoute_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
@ -1959,10 +1729,6 @@ var (
|
|||
|
||||
pattern_HeadscaleService_ListMachines_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "machine"}, ""))
|
||||
|
||||
pattern_HeadscaleService_ShareMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"api", "v1", "machine", "machine_id", "share", "namespace"}, ""))
|
||||
|
||||
pattern_HeadscaleService_UnshareMachine_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"api", "v1", "machine", "machine_id", "unshare", "namespace"}, ""))
|
||||
|
||||
pattern_HeadscaleService_GetMachineRoute_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "routes"}, ""))
|
||||
|
||||
pattern_HeadscaleService_EnableMachineRoutes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "machine", "machine_id", "routes"}, ""))
|
||||
|
@ -2003,10 +1769,6 @@ var (
|
|||
|
||||
forward_HeadscaleService_ListMachines_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_HeadscaleService_ShareMachine_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_HeadscaleService_UnshareMachine_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_HeadscaleService_GetMachineRoute_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_HeadscaleService_EnableMachineRoutes_0 = runtime.ForwardResponseMessage
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc (unknown)
|
||||
// source: headscale/v1/headscale.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
|
@ -36,8 +39,6 @@ type HeadscaleServiceClient interface {
|
|||
DeleteMachine(ctx context.Context, in *DeleteMachineRequest, opts ...grpc.CallOption) (*DeleteMachineResponse, error)
|
||||
ExpireMachine(ctx context.Context, in *ExpireMachineRequest, opts ...grpc.CallOption) (*ExpireMachineResponse, error)
|
||||
ListMachines(ctx context.Context, in *ListMachinesRequest, opts ...grpc.CallOption) (*ListMachinesResponse, error)
|
||||
ShareMachine(ctx context.Context, in *ShareMachineRequest, opts ...grpc.CallOption) (*ShareMachineResponse, error)
|
||||
UnshareMachine(ctx context.Context, in *UnshareMachineRequest, opts ...grpc.CallOption) (*UnshareMachineResponse, error)
|
||||
// --- Route start ---
|
||||
GetMachineRoute(ctx context.Context, in *GetMachineRouteRequest, opts ...grpc.CallOption) (*GetMachineRouteResponse, error)
|
||||
EnableMachineRoutes(ctx context.Context, in *EnableMachineRoutesRequest, opts ...grpc.CallOption) (*EnableMachineRoutesResponse, error)
|
||||
|
@ -181,24 +182,6 @@ func (c *headscaleServiceClient) ListMachines(ctx context.Context, in *ListMachi
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *headscaleServiceClient) ShareMachine(ctx context.Context, in *ShareMachineRequest, opts ...grpc.CallOption) (*ShareMachineResponse, error) {
|
||||
out := new(ShareMachineResponse)
|
||||
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ShareMachine", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *headscaleServiceClient) UnshareMachine(ctx context.Context, in *UnshareMachineRequest, opts ...grpc.CallOption) (*UnshareMachineResponse, error) {
|
||||
out := new(UnshareMachineResponse)
|
||||
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/UnshareMachine", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *headscaleServiceClient) GetMachineRoute(ctx context.Context, in *GetMachineRouteRequest, opts ...grpc.CallOption) (*GetMachineRouteResponse, error) {
|
||||
out := new(GetMachineRouteResponse)
|
||||
err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetMachineRoute", in, out, opts...)
|
||||
|
@ -265,8 +248,6 @@ type HeadscaleServiceServer interface {
|
|||
DeleteMachine(context.Context, *DeleteMachineRequest) (*DeleteMachineResponse, error)
|
||||
ExpireMachine(context.Context, *ExpireMachineRequest) (*ExpireMachineResponse, error)
|
||||
ListMachines(context.Context, *ListMachinesRequest) (*ListMachinesResponse, error)
|
||||
ShareMachine(context.Context, *ShareMachineRequest) (*ShareMachineResponse, error)
|
||||
UnshareMachine(context.Context, *UnshareMachineRequest) (*UnshareMachineResponse, error)
|
||||
// --- Route start ---
|
||||
GetMachineRoute(context.Context, *GetMachineRouteRequest) (*GetMachineRouteResponse, error)
|
||||
EnableMachineRoutes(context.Context, *EnableMachineRoutesRequest) (*EnableMachineRoutesResponse, error)
|
||||
|
@ -323,12 +304,6 @@ func (UnimplementedHeadscaleServiceServer) ExpireMachine(context.Context, *Expir
|
|||
func (UnimplementedHeadscaleServiceServer) ListMachines(context.Context, *ListMachinesRequest) (*ListMachinesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
|
||||
}
|
||||
func (UnimplementedHeadscaleServiceServer) ShareMachine(context.Context, *ShareMachineRequest) (*ShareMachineResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ShareMachine not implemented")
|
||||
}
|
||||
func (UnimplementedHeadscaleServiceServer) UnshareMachine(context.Context, *UnshareMachineRequest) (*UnshareMachineResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnshareMachine not implemented")
|
||||
}
|
||||
func (UnimplementedHeadscaleServiceServer) GetMachineRoute(context.Context, *GetMachineRouteRequest) (*GetMachineRouteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMachineRoute not implemented")
|
||||
}
|
||||
|
@ -609,42 +584,6 @@ func _HeadscaleService_ListMachines_Handler(srv interface{}, ctx context.Context
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HeadscaleService_ShareMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ShareMachineRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HeadscaleServiceServer).ShareMachine(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/headscale.v1.HeadscaleService/ShareMachine",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HeadscaleServiceServer).ShareMachine(ctx, req.(*ShareMachineRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HeadscaleService_UnshareMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UnshareMachineRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HeadscaleServiceServer).UnshareMachine(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/headscale.v1.HeadscaleService/UnshareMachine",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HeadscaleServiceServer).UnshareMachine(ctx, req.(*UnshareMachineRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HeadscaleService_GetMachineRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetMachineRouteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
|
@ -798,14 +737,6 @@ var HeadscaleService_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "ListMachines",
|
||||
Handler: _HeadscaleService_ListMachines_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ShareMachine",
|
||||
Handler: _HeadscaleService_ShareMachine_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UnshareMachine",
|
||||
Handler: _HeadscaleService_UnshareMachine_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetMachineRoute",
|
||||
Handler: _HeadscaleService_GetMachineRoute_Handler,
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -86,13 +85,12 @@ type Machine struct {
|
|||
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"`
|
||||
Namespace *Namespace `protobuf:"bytes,7,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Registered bool `protobuf:"varint,8,opt,name=registered,proto3" json:"registered,omitempty"`
|
||||
RegisterMethod RegisterMethod `protobuf:"varint,9,opt,name=register_method,json=registerMethod,proto3,enum=headscale.v1.RegisterMethod" json:"register_method,omitempty"`
|
||||
LastSeen *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
|
||||
LastSuccessfulUpdate *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=last_successful_update,json=lastSuccessfulUpdate,proto3" json:"last_successful_update,omitempty"`
|
||||
Expiry *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=expiry,proto3" json:"expiry,omitempty"`
|
||||
PreAuthKey *PreAuthKey `protobuf:"bytes,13,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
LastSeen *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
|
||||
LastSuccessfulUpdate *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=last_successful_update,json=lastSuccessfulUpdate,proto3" json:"last_successful_update,omitempty"`
|
||||
Expiry *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=expiry,proto3" json:"expiry,omitempty"`
|
||||
PreAuthKey *PreAuthKey `protobuf:"bytes,11,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
RegisterMethod RegisterMethod `protobuf:"varint,13,opt,name=register_method,json=registerMethod,proto3,enum=headscale.v1.RegisterMethod" json:"register_method,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Machine) Reset() {
|
||||
|
@ -176,20 +174,6 @@ func (x *Machine) GetNamespace() *Namespace {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *Machine) GetRegistered() bool {
|
||||
if x != nil {
|
||||
return x.Registered
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Machine) GetRegisterMethod() RegisterMethod {
|
||||
if x != nil {
|
||||
return x.RegisterMethod
|
||||
}
|
||||
return RegisterMethod_REGISTER_METHOD_UNSPECIFIED
|
||||
}
|
||||
|
||||
func (x *Machine) GetLastSeen() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.LastSeen
|
||||
|
@ -225,6 +209,13 @@ func (x *Machine) GetCreatedAt() *timestamppb.Timestamp {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *Machine) GetRegisterMethod() RegisterMethod {
|
||||
if x != nil {
|
||||
return x.RegisterMethod
|
||||
}
|
||||
return RegisterMethod_REGISTER_METHOD_UNSPECIFIED
|
||||
}
|
||||
|
||||
type RegisterMachineRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
@ -694,210 +685,6 @@ func (x *ListMachinesResponse) GetMachines() []*Machine {
|
|||
return nil
|
||||
}
|
||||
|
||||
type ShareMachineRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
MachineId uint64 `protobuf:"varint,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ShareMachineRequest) Reset() {
|
||||
*x = ShareMachineRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ShareMachineRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ShareMachineRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ShareMachineRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[11]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ShareMachineRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ShareMachineRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_machine_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *ShareMachineRequest) GetMachineId() uint64 {
|
||||
if x != nil {
|
||||
return x.MachineId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ShareMachineRequest) GetNamespace() string {
|
||||
if x != nil {
|
||||
return x.Namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ShareMachineResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Machine *Machine `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ShareMachineResponse) Reset() {
|
||||
*x = ShareMachineResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ShareMachineResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ShareMachineResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ShareMachineResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[12]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ShareMachineResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ShareMachineResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_machine_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *ShareMachineResponse) GetMachine() *Machine {
|
||||
if x != nil {
|
||||
return x.Machine
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UnshareMachineRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
MachineId uint64 `protobuf:"varint,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
func (x *UnshareMachineRequest) Reset() {
|
||||
*x = UnshareMachineRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *UnshareMachineRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UnshareMachineRequest) ProtoMessage() {}
|
||||
|
||||
func (x *UnshareMachineRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[13]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UnshareMachineRequest.ProtoReflect.Descriptor instead.
|
||||
func (*UnshareMachineRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_machine_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *UnshareMachineRequest) GetMachineId() uint64 {
|
||||
if x != nil {
|
||||
return x.MachineId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *UnshareMachineRequest) GetNamespace() string {
|
||||
if x != nil {
|
||||
return x.Namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type UnshareMachineResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Machine *Machine `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||
}
|
||||
|
||||
func (x *UnshareMachineResponse) Reset() {
|
||||
*x = UnshareMachineResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *UnshareMachineResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UnshareMachineResponse) ProtoMessage() {}
|
||||
|
||||
func (x *UnshareMachineResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[14]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UnshareMachineResponse.ProtoReflect.Descriptor instead.
|
||||
func (*UnshareMachineResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_machine_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *UnshareMachineResponse) GetMachine() *Machine {
|
||||
if x != nil {
|
||||
return x.Machine
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DebugCreateMachineRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
@ -912,7 +699,7 @@ type DebugCreateMachineRequest struct {
|
|||
func (x *DebugCreateMachineRequest) Reset() {
|
||||
*x = DebugCreateMachineRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[15]
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
@ -925,7 +712,7 @@ func (x *DebugCreateMachineRequest) String() string {
|
|||
func (*DebugCreateMachineRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DebugCreateMachineRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[15]
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[11]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
@ -938,7 +725,7 @@ func (x *DebugCreateMachineRequest) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use DebugCreateMachineRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DebugCreateMachineRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_machine_proto_rawDescGZIP(), []int{15}
|
||||
return file_headscale_v1_machine_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *DebugCreateMachineRequest) GetNamespace() string {
|
||||
|
@ -980,7 +767,7 @@ type DebugCreateMachineResponse struct {
|
|||
func (x *DebugCreateMachineResponse) Reset() {
|
||||
*x = DebugCreateMachineResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[16]
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
@ -993,7 +780,7 @@ func (x *DebugCreateMachineResponse) String() string {
|
|||
func (*DebugCreateMachineResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DebugCreateMachineResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[16]
|
||||
mi := &file_headscale_v1_machine_proto_msgTypes[12]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
@ -1006,7 +793,7 @@ func (x *DebugCreateMachineResponse) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use DebugCreateMachineResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DebugCreateMachineResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_machine_proto_rawDescGZIP(), []int{16}
|
||||
return file_headscale_v1_machine_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *DebugCreateMachineResponse) GetMachine() *Machine {
|
||||
|
@ -1027,7 +814,7 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{
|
|||
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
|
||||
0x61, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x68, 0x65, 0x61, 0x64, 0x73,
|
||||
0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b,
|
||||
0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfd, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63,
|
||||
0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdd, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63,
|
||||
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,
|
||||
0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||
|
@ -1041,33 +828,31 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{
|
|||
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63,
|
||||
0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
|
||||
0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
|
||||
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72,
|
||||
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||
0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x72,
|
||||
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09,
|
||||
0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68,
|
||||
0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68,
|
||||
0x6f, 0x64, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18,
|
||||
0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||
0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c,
|
||||
0x61, 0x73, 0x74, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75,
|
||||
0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63,
|
||||
0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a,
|
||||
0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 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, 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,
|
||||
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x09, 0x6c,
|
||||
0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
|
||||
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74,
|
||||
0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x75, 0x63,
|
||||
0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x09,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
|
||||
0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c,
|
||||
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79,
|
||||
0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
|
||||
0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72,
|
||||
0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0b, 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, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41,
|
||||
0x74, 0x12, 0x45, 0x0a, 0x0f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65,
|
||||
0x74, 0x68, 0x6f, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61,
|
||||
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74,
|
||||
0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74,
|
||||
0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 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,
|
||||
|
@ -1105,51 +890,31 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{
|
|||
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, 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,
|
||||
0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 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 (
|
||||
|
@ -1165,7 +930,7 @@ func file_headscale_v1_machine_proto_rawDescGZIP() []byte {
|
|||
}
|
||||
|
||||
var file_headscale_v1_machine_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_headscale_v1_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
|
||||
var file_headscale_v1_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
||||
var file_headscale_v1_machine_proto_goTypes = []interface{}{
|
||||
(RegisterMethod)(0), // 0: headscale.v1.RegisterMethod
|
||||
(*Machine)(nil), // 1: headscale.v1.Machine
|
||||
|
@ -1179,36 +944,30 @@ var file_headscale_v1_machine_proto_goTypes = []interface{}{
|
|||
(*ExpireMachineResponse)(nil), // 9: headscale.v1.ExpireMachineResponse
|
||||
(*ListMachinesRequest)(nil), // 10: headscale.v1.ListMachinesRequest
|
||||
(*ListMachinesResponse)(nil), // 11: headscale.v1.ListMachinesResponse
|
||||
(*ShareMachineRequest)(nil), // 12: headscale.v1.ShareMachineRequest
|
||||
(*ShareMachineResponse)(nil), // 13: headscale.v1.ShareMachineResponse
|
||||
(*UnshareMachineRequest)(nil), // 14: headscale.v1.UnshareMachineRequest
|
||||
(*UnshareMachineResponse)(nil), // 15: headscale.v1.UnshareMachineResponse
|
||||
(*DebugCreateMachineRequest)(nil), // 16: headscale.v1.DebugCreateMachineRequest
|
||||
(*DebugCreateMachineResponse)(nil), // 17: headscale.v1.DebugCreateMachineResponse
|
||||
(*Namespace)(nil), // 18: headscale.v1.Namespace
|
||||
(*timestamppb.Timestamp)(nil), // 19: google.protobuf.Timestamp
|
||||
(*PreAuthKey)(nil), // 20: headscale.v1.PreAuthKey
|
||||
(*DebugCreateMachineRequest)(nil), // 12: headscale.v1.DebugCreateMachineRequest
|
||||
(*DebugCreateMachineResponse)(nil), // 13: headscale.v1.DebugCreateMachineResponse
|
||||
(*Namespace)(nil), // 14: headscale.v1.Namespace
|
||||
(*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp
|
||||
(*PreAuthKey)(nil), // 16: headscale.v1.PreAuthKey
|
||||
}
|
||||
var file_headscale_v1_machine_proto_depIdxs = []int32{
|
||||
18, // 0: headscale.v1.Machine.namespace:type_name -> headscale.v1.Namespace
|
||||
0, // 1: headscale.v1.Machine.register_method:type_name -> headscale.v1.RegisterMethod
|
||||
19, // 2: headscale.v1.Machine.last_seen:type_name -> google.protobuf.Timestamp
|
||||
19, // 3: headscale.v1.Machine.last_successful_update:type_name -> google.protobuf.Timestamp
|
||||
19, // 4: headscale.v1.Machine.expiry:type_name -> google.protobuf.Timestamp
|
||||
20, // 5: headscale.v1.Machine.pre_auth_key:type_name -> headscale.v1.PreAuthKey
|
||||
19, // 6: headscale.v1.Machine.created_at:type_name -> google.protobuf.Timestamp
|
||||
14, // 0: headscale.v1.Machine.namespace:type_name -> headscale.v1.Namespace
|
||||
15, // 1: headscale.v1.Machine.last_seen:type_name -> google.protobuf.Timestamp
|
||||
15, // 2: headscale.v1.Machine.last_successful_update:type_name -> google.protobuf.Timestamp
|
||||
15, // 3: headscale.v1.Machine.expiry:type_name -> google.protobuf.Timestamp
|
||||
16, // 4: headscale.v1.Machine.pre_auth_key:type_name -> headscale.v1.PreAuthKey
|
||||
15, // 5: headscale.v1.Machine.created_at:type_name -> google.protobuf.Timestamp
|
||||
0, // 6: headscale.v1.Machine.register_method:type_name -> headscale.v1.RegisterMethod
|
||||
1, // 7: headscale.v1.RegisterMachineResponse.machine:type_name -> headscale.v1.Machine
|
||||
1, // 8: headscale.v1.GetMachineResponse.machine:type_name -> headscale.v1.Machine
|
||||
1, // 9: headscale.v1.ExpireMachineResponse.machine:type_name -> headscale.v1.Machine
|
||||
1, // 10: headscale.v1.ListMachinesResponse.machines:type_name -> headscale.v1.Machine
|
||||
1, // 11: headscale.v1.ShareMachineResponse.machine:type_name -> headscale.v1.Machine
|
||||
1, // 12: headscale.v1.UnshareMachineResponse.machine:type_name -> headscale.v1.Machine
|
||||
1, // 13: headscale.v1.DebugCreateMachineResponse.machine:type_name -> headscale.v1.Machine
|
||||
14, // [14:14] is the sub-list for method output_type
|
||||
14, // [14:14] is the sub-list for method input_type
|
||||
14, // [14:14] is the sub-list for extension type_name
|
||||
14, // [14:14] is the sub-list for extension extendee
|
||||
0, // [0:14] is the sub-list for field type_name
|
||||
1, // 11: headscale.v1.DebugCreateMachineResponse.machine:type_name -> headscale.v1.Machine
|
||||
12, // [12:12] is the sub-list for method output_type
|
||||
12, // [12:12] is the sub-list for method input_type
|
||||
12, // [12:12] is the sub-list for extension type_name
|
||||
12, // [12:12] is the sub-list for extension extendee
|
||||
0, // [0:12] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_machine_proto_init() }
|
||||
|
@ -1352,54 +1111,6 @@ func file_headscale_v1_machine_proto_init() {
|
|||
}
|
||||
}
|
||||
file_headscale_v1_machine_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ShareMachineRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_headscale_v1_machine_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ShareMachineResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_headscale_v1_machine_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*UnshareMachineRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_headscale_v1_machine_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*UnshareMachineResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_headscale_v1_machine_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*DebugCreateMachineRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
|
@ -1411,7 +1122,7 @@ func file_headscale_v1_machine_proto_init() {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
file_headscale_v1_machine_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_headscale_v1_machine_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*DebugCreateMachineResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
|
@ -1430,7 +1141,7 @@ func file_headscale_v1_machine_proto_init() {
|
|||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_headscale_v1_machine_proto_rawDesc,
|
||||
NumEnums: 1,
|
||||
NumMessages: 17,
|
||||
NumMessages: 13,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -7,11 +7,10 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -181,6 +181,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"HeadscaleService"
|
||||
]
|
||||
|
@ -324,37 +338,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "machineId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"format": "uint64"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"HeadscaleService"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/machine/{machineId}/share/{namespace}": {
|
||||
"post": {
|
||||
"operationId": "HeadscaleService_ShareMachine",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1ShareMachineResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "machineId",
|
||||
|
@ -364,47 +347,14 @@
|
|||
"format": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"HeadscaleService"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/machine/{machineId}/unshare/{namespace}": {
|
||||
"post": {
|
||||
"operationId": "HeadscaleService_UnshareMachine",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1UnshareMachineResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "machineId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"format": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
"name": "routes",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
|
@ -935,12 +885,6 @@
|
|||
"namespace": {
|
||||
"$ref": "#/definitions/v1Namespace"
|
||||
},
|
||||
"registered": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"registerMethod": {
|
||||
"$ref": "#/definitions/v1RegisterMethod"
|
||||
},
|
||||
"lastSeen": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
|
@ -959,6 +903,9 @@
|
|||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"registerMethod": {
|
||||
"$ref": "#/definitions/v1RegisterMethod"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1050,22 +997,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1ShareMachineResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"machine": {
|
||||
"$ref": "#/definitions/v1Machine"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1UnshareMachineResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"machine": {
|
||||
"$ref": "#/definitions/v1Machine"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
46
go.mod
46
go.mod
|
@ -8,58 +8,60 @@ require (
|
|||
github.com/efekarakus/termcolor v1.0.1
|
||||
github.com/fatih/set v0.2.1
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/glebarez/sqlite v1.3.5
|
||||
github.com/gofrs/uuid v4.2.0+incompatible
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3
|
||||
github.com/infobloxopen/protoc-gen-gorm v1.1.0
|
||||
github.com/klauspost/compress v1.14.2
|
||||
github.com/klauspost/compress v1.14.4
|
||||
github.com/ory/dockertest/v3 v3.8.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/pterm/pterm v0.12.36
|
||||
github.com/pterm/pterm v0.12.37
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/soheilhy/cmux v0.1.5
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tailscale/hujson v0.0.0-20211215203138-ffd971c5f362
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||
github.com/zsais/go-gin-prometheus v0.1.0
|
||||
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
google.golang.org/genproto v0.0.0-20220210181026-6fee9acbd336
|
||||
google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7
|
||||
google.golang.org/grpc v1.44.0
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gorm.io/datatypes v1.0.5
|
||||
gorm.io/driver/postgres v1.2.3
|
||||
gorm.io/driver/sqlite v1.2.6
|
||||
gorm.io/gorm v1.22.5
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gorm.io/driver/postgres v1.3.1
|
||||
gorm.io/gorm v1.23.1
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||
tailscale.com v1.20.4
|
||||
tailscale.com v1.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/containerd/continuity v0.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.12.0 // indirect
|
||||
github.com/docker/cli v20.10.12+incompatible // indirect
|
||||
github.com/docker/docker v20.10.12+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.14.8 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
||||
|
@ -70,6 +72,7 @@ require (
|
|||
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gookit/color v1.5.0 // indirect
|
||||
github.com/hashicorp/go-version v1.4.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
|
@ -92,13 +95,14 @@ require (
|
|||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lib/pq v1.10.3 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.11 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
|
@ -112,6 +116,7 @@ require (
|
|||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
|
@ -120,7 +125,7 @@ require (
|
|||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.6 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
|
@ -128,14 +133,17 @@ require (
|
|||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
gorm.io/driver/mysql v1.2.3 // indirect
|
||||
modernc.org/libc v1.14.5 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.0.5 // indirect
|
||||
modernc.org/sqlite v1.14.7 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
|
92
grpcv1.go
92
grpcv1.go
|
@ -3,12 +3,10 @@ package headscale
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/datatypes"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
|
@ -159,9 +157,11 @@ func (api headscaleV1APIServer) RegisterMachine(
|
|||
Str("namespace", request.GetNamespace()).
|
||||
Str("machine_key", request.GetKey()).
|
||||
Msg("Registering machine")
|
||||
machine, err := api.h.RegisterMachine(
|
||||
|
||||
machine, err := api.h.RegisterMachineFromAuthCallback(
|
||||
request.GetKey(),
|
||||
request.GetNamespace(),
|
||||
RegisterMethodCLI,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -232,15 +232,6 @@ func (api headscaleV1APIServer) ListMachines(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
sharedMachines, err := api.h.ListSharedMachinesInNamespace(
|
||||
request.GetNamespace(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
machines = append(machines, sharedMachines...)
|
||||
|
||||
response := make([]*v1.Machine, len(machines))
|
||||
for index, machine := range machines {
|
||||
response[index] = machine.toProto()
|
||||
|
@ -262,50 +253,6 @@ func (api headscaleV1APIServer) ListMachines(
|
|||
return &v1.ListMachinesResponse{Machines: response}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) ShareMachine(
|
||||
ctx context.Context,
|
||||
request *v1.ShareMachineRequest,
|
||||
) (*v1.ShareMachineResponse, error) {
|
||||
destinationNamespace, err := api.h.GetNamespace(request.GetNamespace())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
machine, err := api.h.GetMachineByID(request.GetMachineId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = api.h.AddSharedMachineToNamespace(machine, destinationNamespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.ShareMachineResponse{Machine: machine.toProto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) UnshareMachine(
|
||||
ctx context.Context,
|
||||
request *v1.UnshareMachineRequest,
|
||||
) (*v1.UnshareMachineResponse, error) {
|
||||
destinationNamespace, err := api.h.GetNamespace(request.GetNamespace())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
machine, err := api.h.GetMachineByID(request.GetMachineId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = api.h.RemoveSharedMachineFromNamespace(machine, destinationNamespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.UnshareMachineResponse{Machine: machine.toProto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) GetMachineRoute(
|
||||
ctx context.Context,
|
||||
request *v1.GetMachineRouteRequest,
|
||||
|
@ -315,13 +262,8 @@ func (api headscaleV1APIServer) GetMachineRoute(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := machine.RoutesToProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.GetMachineRouteResponse{
|
||||
Routes: routes,
|
||||
Routes: machine.RoutesToProto(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -339,13 +281,8 @@ func (api headscaleV1APIServer) EnableMachineRoutes(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := machine.RoutesToProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.EnableMachineRoutesResponse{
|
||||
Routes: routes,
|
||||
Routes: machine.RoutesToProto(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -432,13 +369,6 @@ func (api headscaleV1APIServer) DebugCreateMachine(
|
|||
Hostname: "DebugTestMachine",
|
||||
}
|
||||
|
||||
log.Trace().Caller().Interface("hostinfo", hostinfo).Msg("")
|
||||
|
||||
hostinfoJson, err := json.Marshal(hostinfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newMachine := Machine{
|
||||
MachineKey: request.GetKey(),
|
||||
Name: request.GetName(),
|
||||
|
@ -448,14 +378,14 @@ func (api headscaleV1APIServer) DebugCreateMachine(
|
|||
LastSeen: &time.Time{},
|
||||
LastSuccessfulUpdate: &time.Time{},
|
||||
|
||||
HostInfo: datatypes.JSON(hostinfoJson),
|
||||
HostInfo: HostInfo(hostinfo),
|
||||
}
|
||||
|
||||
// log.Trace().Caller().Interface("machine", newMachine).Msg("")
|
||||
|
||||
if err := api.h.db.Create(&newMachine).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api.h.registrationCache.Set(
|
||||
request.GetKey(),
|
||||
newMachine,
|
||||
registerCacheExpiration,
|
||||
)
|
||||
|
||||
return &v1.DebugCreateMachineResponse{Machine: newMachine.toProto()}, nil
|
||||
}
|
||||
|
|
|
@ -529,7 +529,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||
namespace, err := s.createNamespace("machine-namespace")
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
sharedNamespace, err := s.createNamespace("shared-namespace")
|
||||
secondNamespace, err := s.createNamespace("other-namespace")
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
// Randomly generated machine keys
|
||||
|
@ -589,7 +589,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||
|
||||
assert.Len(s.T(), machines, len(machineKeys))
|
||||
|
||||
// Test list all nodes after added shared
|
||||
// Test list all nodes after added seconds
|
||||
listAllResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
|
@ -621,20 +621,14 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||
assert.Equal(s.T(), "machine-4", listAll[3].Name)
|
||||
assert.Equal(s.T(), "machine-5", listAll[4].Name)
|
||||
|
||||
assert.True(s.T(), listAll[0].Registered)
|
||||
assert.True(s.T(), listAll[1].Registered)
|
||||
assert.True(s.T(), listAll[2].Registered)
|
||||
assert.True(s.T(), listAll[3].Registered)
|
||||
assert.True(s.T(), listAll[4].Registered)
|
||||
|
||||
sharedMachineKeys := []string{
|
||||
otherNamespaceMachineKeys := []string{
|
||||
"b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
|
||||
"dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
|
||||
}
|
||||
sharedMachines := make([]*v1.Machine, len(sharedMachineKeys))
|
||||
otherNamespaceMachines := make([]*v1.Machine, len(otherNamespaceMachineKeys))
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
for index, machineKey := range sharedMachineKeys {
|
||||
for index, machineKey := range otherNamespaceMachineKeys {
|
||||
_, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
|
@ -642,9 +636,9 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||
"debug",
|
||||
"create-node",
|
||||
"--name",
|
||||
fmt.Sprintf("shared-machine-%d", index+1),
|
||||
fmt.Sprintf("otherNamespace-machine-%d", index+1),
|
||||
"--namespace",
|
||||
sharedNamespace.Name,
|
||||
secondNamespace.Name,
|
||||
"--key",
|
||||
machineKey,
|
||||
"--output",
|
||||
|
@ -660,7 +654,7 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||
"headscale",
|
||||
"nodes",
|
||||
"--namespace",
|
||||
sharedNamespace.Name,
|
||||
secondNamespace.Name,
|
||||
"register",
|
||||
"--key",
|
||||
machineKey,
|
||||
|
@ -675,13 +669,13 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||
err = json.Unmarshal([]byte(machineResult), &machine)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
sharedMachines[index] = &machine
|
||||
otherNamespaceMachines[index] = &machine
|
||||
}
|
||||
|
||||
assert.Len(s.T(), sharedMachines, len(sharedMachineKeys))
|
||||
assert.Len(s.T(), otherNamespaceMachines, len(otherNamespaceMachineKeys))
|
||||
|
||||
// Test list all nodes after added shared
|
||||
listAllWithSharedResult, err := ExecuteCommand(
|
||||
// Test list all nodes after added otherNamespace
|
||||
listAllWithotherNamespaceResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
|
@ -694,31 +688,31 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
var listAllWithShared []v1.Machine
|
||||
err = json.Unmarshal([]byte(listAllWithSharedResult), &listAllWithShared)
|
||||
var listAllWithotherNamespace []v1.Machine
|
||||
err = json.Unmarshal(
|
||||
[]byte(listAllWithotherNamespaceResult),
|
||||
&listAllWithotherNamespace,
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
// All nodes, machines + shared
|
||||
assert.Len(s.T(), listAllWithShared, 7)
|
||||
// All nodes, machines + otherNamespace
|
||||
assert.Len(s.T(), listAllWithotherNamespace, 7)
|
||||
|
||||
assert.Equal(s.T(), uint64(6), listAllWithShared[5].Id)
|
||||
assert.Equal(s.T(), uint64(7), listAllWithShared[6].Id)
|
||||
assert.Equal(s.T(), uint64(6), listAllWithotherNamespace[5].Id)
|
||||
assert.Equal(s.T(), uint64(7), listAllWithotherNamespace[6].Id)
|
||||
|
||||
assert.Equal(s.T(), "shared-machine-1", listAllWithShared[5].Name)
|
||||
assert.Equal(s.T(), "shared-machine-2", listAllWithShared[6].Name)
|
||||
assert.Equal(s.T(), "otherNamespace-machine-1", listAllWithotherNamespace[5].Name)
|
||||
assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name)
|
||||
|
||||
assert.True(s.T(), listAllWithShared[5].Registered)
|
||||
assert.True(s.T(), listAllWithShared[6].Registered)
|
||||
|
||||
// Test list all nodes after added shared
|
||||
listOnlySharedMachineNamespaceResult, err := ExecuteCommand(
|
||||
// Test list all nodes after added otherNamespace
|
||||
listOnlyotherNamespaceMachineNamespaceResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--namespace",
|
||||
sharedNamespace.Name,
|
||||
secondNamespace.Name,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
|
@ -726,23 +720,28 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
var listOnlySharedMachineNamespace []v1.Machine
|
||||
var listOnlyotherNamespaceMachineNamespace []v1.Machine
|
||||
err = json.Unmarshal(
|
||||
[]byte(listOnlySharedMachineNamespaceResult),
|
||||
&listOnlySharedMachineNamespace,
|
||||
[]byte(listOnlyotherNamespaceMachineNamespaceResult),
|
||||
&listOnlyotherNamespaceMachineNamespace,
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
assert.Len(s.T(), listOnlySharedMachineNamespace, 2)
|
||||
assert.Len(s.T(), listOnlyotherNamespaceMachineNamespace, 2)
|
||||
|
||||
assert.Equal(s.T(), uint64(6), listOnlySharedMachineNamespace[0].Id)
|
||||
assert.Equal(s.T(), uint64(7), listOnlySharedMachineNamespace[1].Id)
|
||||
assert.Equal(s.T(), uint64(6), listOnlyotherNamespaceMachineNamespace[0].Id)
|
||||
assert.Equal(s.T(), uint64(7), listOnlyotherNamespaceMachineNamespace[1].Id)
|
||||
|
||||
assert.Equal(s.T(), "shared-machine-1", listOnlySharedMachineNamespace[0].Name)
|
||||
assert.Equal(s.T(), "shared-machine-2", listOnlySharedMachineNamespace[1].Name)
|
||||
|
||||
assert.True(s.T(), listOnlySharedMachineNamespace[0].Registered)
|
||||
assert.True(s.T(), listOnlySharedMachineNamespace[1].Registered)
|
||||
assert.Equal(
|
||||
s.T(),
|
||||
"otherNamespace-machine-1",
|
||||
listOnlyotherNamespaceMachineNamespace[0].Name,
|
||||
)
|
||||
assert.Equal(
|
||||
s.T(),
|
||||
"otherNamespace-machine-2",
|
||||
listOnlyotherNamespaceMachineNamespace[1].Name,
|
||||
)
|
||||
|
||||
// Delete a machines
|
||||
_, err = ExecuteCommand(
|
||||
|
@ -786,120 +785,6 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
|
|||
assert.Nil(s.T(), err)
|
||||
|
||||
assert.Len(s.T(), listOnlyMachineNamespaceAfterDelete, 4)
|
||||
|
||||
// test: share node
|
||||
|
||||
shareMachineResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"share",
|
||||
"--namespace",
|
||||
namespace.Name,
|
||||
"--identifier",
|
||||
"7",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
var shareMachine v1.Machine
|
||||
err = json.Unmarshal([]byte(shareMachineResult), &shareMachine)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
assert.Equal(s.T(), uint64(7), shareMachine.Id)
|
||||
|
||||
assert.Equal(s.T(), "shared-machine-2", shareMachine.Name)
|
||||
|
||||
assert.True(s.T(), shareMachine.Registered)
|
||||
|
||||
// Test: list main namespace after machine has been shared
|
||||
listOnlyMachineNamespaceAfterShareResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--namespace",
|
||||
namespace.Name,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
var listOnlyMachineNamespaceAfterShare []v1.Machine
|
||||
err = json.Unmarshal(
|
||||
[]byte(listOnlyMachineNamespaceAfterShareResult),
|
||||
&listOnlyMachineNamespaceAfterShare,
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
assert.Len(s.T(), listOnlyMachineNamespaceAfterShare, 5)
|
||||
|
||||
assert.Equal(s.T(), uint64(7), listOnlyMachineNamespaceAfterShare[4].Id)
|
||||
|
||||
assert.Equal(s.T(), "shared-machine-2", listOnlyMachineNamespaceAfterShare[4].Name)
|
||||
|
||||
assert.True(s.T(), listOnlyMachineNamespaceAfterShare[4].Registered)
|
||||
|
||||
// test: unshare node
|
||||
|
||||
unshareMachineResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"unshare",
|
||||
"--namespace",
|
||||
namespace.Name,
|
||||
"--identifier",
|
||||
"7",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
var unshareMachine v1.Machine
|
||||
err = json.Unmarshal([]byte(unshareMachineResult), &unshareMachine)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
assert.Equal(s.T(), uint64(7), unshareMachine.Id)
|
||||
|
||||
assert.Equal(s.T(), "shared-machine-2", unshareMachine.Name)
|
||||
|
||||
assert.True(s.T(), unshareMachine.Registered)
|
||||
|
||||
// Test: list main namespace after machine has been shared
|
||||
listOnlyMachineNamespaceAfterUnshareResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--namespace",
|
||||
namespace.Name,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
var listOnlyMachineNamespaceAfterUnshare []v1.Machine
|
||||
err = json.Unmarshal(
|
||||
[]byte(listOnlyMachineNamespaceAfterUnshareResult),
|
||||
&listOnlyMachineNamespaceAfterUnshare,
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
assert.Len(s.T(), listOnlyMachineNamespaceAfterUnshare, 4)
|
||||
}
|
||||
|
||||
func (s *IntegrationCLITestSuite) TestNodeExpireCommand() {
|
||||
|
@ -1082,7 +967,6 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
|
|||
|
||||
assert.Equal(s.T(), uint64(1), machine.Id)
|
||||
assert.Equal(s.T(), "route-machine", machine.Name)
|
||||
assert.True(s.T(), machine.Registered)
|
||||
|
||||
listAllResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
|
|
|
@ -6,6 +6,7 @@ package headscale
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
|
@ -18,8 +19,15 @@ const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
|
|||
var (
|
||||
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
|
||||
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
|
||||
|
||||
tailscaleVersions = []string{"1.22.0", "1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
|
||||
)
|
||||
|
||||
type TestNamespace struct {
|
||||
count int
|
||||
tailscales map[string]dockertest.Resource
|
||||
}
|
||||
|
||||
type ExecuteCommandConfig struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
@ -119,3 +127,35 @@ func DockerAllowNetworkAdministration(config *docker.HostConfig) {
|
|||
Target: "/dev/net/tun",
|
||||
})
|
||||
}
|
||||
|
||||
func getIPs(
|
||||
tailscales map[string]dockertest.Resource,
|
||||
) (map[string][]netaddr.IP, error) {
|
||||
ips := make(map[string][]netaddr.IP)
|
||||
for hostname, tailscale := range tailscales {
|
||||
command := []string{"tailscale", "ip"}
|
||||
|
||||
result, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, address := range strings.Split(result, "\n") {
|
||||
address = strings.TrimSuffix(address, "\n")
|
||||
if len(address) < 1 {
|
||||
continue
|
||||
}
|
||||
ip, err := netaddr.ParseIP(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips[hostname] = append(ips[hostname], ip)
|
||||
}
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
|
396
integration_embedded_derp_test.go
Normal file
396
integration_embedded_derp_test.go
Normal file
|
@ -0,0 +1,396 @@
|
|||
//go:build integration
|
||||
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/ccding/go-stun/stun"
|
||||
)
|
||||
|
||||
const (
|
||||
headscaleHostname = "headscale-derp"
|
||||
namespaceName = "derpnamespace"
|
||||
totalContainers = 3
|
||||
)
|
||||
|
||||
type IntegrationDERPTestSuite struct {
|
||||
suite.Suite
|
||||
stats *suite.SuiteInformation
|
||||
|
||||
pool dockertest.Pool
|
||||
networks map[int]dockertest.Network // so we keep the containers isolated
|
||||
headscale dockertest.Resource
|
||||
|
||||
tailscales map[string]dockertest.Resource
|
||||
joinWaitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
func TestDERPIntegrationTestSuite(t *testing.T) {
|
||||
s := new(IntegrationDERPTestSuite)
|
||||
|
||||
s.tailscales = make(map[string]dockertest.Resource)
|
||||
s.networks = make(map[int]dockertest.Network)
|
||||
|
||||
suite.Run(t, s)
|
||||
|
||||
// HandleStats, which allows us to check if we passed and save logs
|
||||
// is called after TearDown, so we cannot tear down containers before
|
||||
// we have potentially saved the logs.
|
||||
for _, tailscale := range s.tailscales {
|
||||
if err := s.pool.Purge(&tailscale); err != nil {
|
||||
log.Printf("Could not purge resource: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !s.stats.Passed() {
|
||||
err := s.saveLog(&s.headscale, "test_output")
|
||||
if err != nil {
|
||||
log.Printf("Could not save log: %s\n", err)
|
||||
}
|
||||
}
|
||||
if err := s.pool.Purge(&s.headscale); err != nil {
|
||||
log.Printf("Could not purge resource: %s\n", err)
|
||||
}
|
||||
|
||||
for _, network := range s.networks {
|
||||
if err := network.Close(); err != nil {
|
||||
log.Printf("Could not close network: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) SetupSuite() {
|
||||
if ppool, err := dockertest.NewPool(""); err == nil {
|
||||
s.pool = *ppool
|
||||
} else {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
for i := 0; i < totalContainers; i++ {
|
||||
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
|
||||
s.networks[i] = *pnetwork
|
||||
} else {
|
||||
log.Fatalf("Could not create network: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile",
|
||||
ContextDir: ".",
|
||||
}
|
||||
|
||||
currentPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not determine current path: %s", err)
|
||||
}
|
||||
|
||||
headscaleOptions := &dockertest.RunOptions{
|
||||
Name: headscaleHostname,
|
||||
Mounts: []string{
|
||||
fmt.Sprintf("%s/integration_test/etc_embedded_derp:/etc/headscale", currentPath),
|
||||
},
|
||||
Cmd: []string{"headscale", "serve"},
|
||||
ExposedPorts: []string{"8443/tcp", "3478/udp"},
|
||||
PortBindings: map[docker.Port][]docker.PortBinding{
|
||||
"8443/tcp": {{HostPort: "8443"}},
|
||||
"3478/udp": {{HostPort: "3478"}},
|
||||
},
|
||||
}
|
||||
|
||||
log.Println("Creating headscale container")
|
||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||
s.headscale = *pheadscale
|
||||
} else {
|
||||
log.Fatalf("Could not start resource: %s", err)
|
||||
}
|
||||
log.Println("Created headscale container to test DERP")
|
||||
|
||||
log.Println("Creating tailscale containers")
|
||||
|
||||
for i := 0; i < totalContainers; i++ {
|
||||
version := tailscaleVersions[i%len(tailscaleVersions)]
|
||||
hostname, container := s.tailscaleContainer(
|
||||
fmt.Sprint(i),
|
||||
version,
|
||||
s.networks[i],
|
||||
)
|
||||
s.tailscales[hostname] = *container
|
||||
}
|
||||
|
||||
log.Println("Waiting for headscale to be ready")
|
||||
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8443/tcp"))
|
||||
|
||||
if err := s.pool.Retry(func() error {
|
||||
url := fmt.Sprintf("https://%s/health", hostEndpoint)
|
||||
insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
client := &http.Client{Transport: insecureTransport}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("status code not OK")
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
|
||||
// test setup, we need to abort and tear down. However, testify does not seem to
|
||||
// support that at the moment:
|
||||
// https://github.com/stretchr/testify/issues/849
|
||||
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
||||
}
|
||||
log.Println("headscale container is ready")
|
||||
|
||||
log.Printf("Creating headscale namespace: %s\n", namespaceName)
|
||||
result, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{"headscale", "namespaces", "create", namespaceName},
|
||||
[]string{},
|
||||
)
|
||||
log.Println("headscale create namespace result: ", result)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
log.Printf("Creating pre auth key for %s\n", namespaceName)
|
||||
preAuthResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"--namespace",
|
||||
namespaceName,
|
||||
"preauthkeys",
|
||||
"create",
|
||||
"--reusable",
|
||||
"--expiration",
|
||||
"24h",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
[]string{"LOG_LEVEL=error"},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
var preAuthKey v1.PreAuthKey
|
||||
err = json.Unmarshal([]byte(preAuthResult), &preAuthKey)
|
||||
assert.Nil(s.T(), err)
|
||||
assert.True(s.T(), preAuthKey.Reusable)
|
||||
|
||||
headscaleEndpoint := fmt.Sprintf("https://headscale:%s", s.headscale.GetPort("8443/tcp"))
|
||||
|
||||
log.Printf(
|
||||
"Joining tailscale containers to headscale at %s\n",
|
||||
headscaleEndpoint,
|
||||
)
|
||||
for hostname, tailscale := range s.tailscales {
|
||||
s.joinWaitGroup.Add(1)
|
||||
go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
|
||||
}
|
||||
|
||||
s.joinWaitGroup.Wait()
|
||||
|
||||
// The nodes need a bit of time to get their updated maps from headscale
|
||||
// TODO: See if we can have a more deterministic wait here.
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) Join(
|
||||
endpoint, key, hostname string,
|
||||
tailscale dockertest.Resource,
|
||||
) {
|
||||
defer s.joinWaitGroup.Done()
|
||||
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"up",
|
||||
"-login-server",
|
||||
endpoint,
|
||||
"--authkey",
|
||||
key,
|
||||
"--hostname",
|
||||
hostname,
|
||||
}
|
||||
|
||||
log.Println("Join command:", command)
|
||||
log.Printf("Running join command for %s\n", hostname)
|
||||
_, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
log.Printf("%s joined\n", hostname)
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) tailscaleContainer(identifier, version string, network dockertest.Network,
|
||||
) (string, *dockertest.Resource) {
|
||||
tailscaleBuildOptions := &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile.tailscale",
|
||||
ContextDir: ".",
|
||||
BuildArgs: []docker.BuildArg{
|
||||
{
|
||||
Name: "TAILSCALE_VERSION",
|
||||
Value: version,
|
||||
},
|
||||
},
|
||||
}
|
||||
hostname := fmt.Sprintf(
|
||||
"tailscale-%s-%s",
|
||||
strings.Replace(version, ".", "-", -1),
|
||||
identifier,
|
||||
)
|
||||
tailscaleOptions := &dockertest.RunOptions{
|
||||
Name: hostname,
|
||||
Networks: []*dockertest.Network{&network},
|
||||
Cmd: []string{
|
||||
"tailscaled", "--tun=tsdev",
|
||||
},
|
||||
|
||||
// expose the host IP address, so we can access it from inside the container
|
||||
ExtraHosts: []string{"host.docker.internal:host-gateway", "headscale:host-gateway"},
|
||||
}
|
||||
|
||||
pts, err := s.pool.BuildAndRunWithBuildOptions(
|
||||
tailscaleBuildOptions,
|
||||
tailscaleOptions,
|
||||
DockerRestartPolicy,
|
||||
DockerAllowLocalIPv6,
|
||||
DockerAllowNetworkAdministration,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start resource: %s", err)
|
||||
}
|
||||
log.Printf("Created %s container\n", hostname)
|
||||
|
||||
return hostname, pts
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) HandleStats(
|
||||
suiteName string,
|
||||
stats *suite.SuiteInformation,
|
||||
) {
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) saveLog(
|
||||
resource *dockertest.Resource,
|
||||
basePath string,
|
||||
) error {
|
||||
err := os.MkdirAll(basePath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
err = s.pool.Client.Logs(
|
||||
docker.LogsOptions{
|
||||
Context: context.TODO(),
|
||||
Container: resource.Container.ID,
|
||||
OutputStream: &stdout,
|
||||
ErrorStream: &stderr,
|
||||
Tail: "all",
|
||||
RawTerminal: false,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Follow: false,
|
||||
Timestamps: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
||||
|
||||
err = ioutil.WriteFile(
|
||||
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
||||
[]byte(stdout.String()),
|
||||
0o644,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(
|
||||
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
||||
[]byte(stdout.String()),
|
||||
0o644,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
|
||||
ips, err := getIPs(s.tailscales)
|
||||
assert.Nil(s.T(), err)
|
||||
for hostname, tailscale := range s.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=5",
|
||||
"--until-direct=false",
|
||||
peername,
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"Pinging using hostname from %s to %s\n",
|
||||
hostname,
|
||||
peername,
|
||||
)
|
||||
log.Println(command)
|
||||
result, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
log.Printf("Result for %s: %s\n", hostname, result)
|
||||
assert.Contains(t, result, "via DERP(headscale)")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationDERPTestSuite) TestDERPSTUN() {
|
||||
headscaleSTUNAddr := fmt.Sprintf("localhost:%s", s.headscale.GetPort("3478/udp"))
|
||||
client := stun.NewClient()
|
||||
client.SetVerbose(true)
|
||||
client.SetVVerbose(true)
|
||||
client.SetServerAddr(headscaleSTUNAddr)
|
||||
_, _, err := client.Discover()
|
||||
assert.Nil(s.T(), err)
|
||||
}
|
|
@ -15,6 +15,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -28,13 +29,6 @@ import (
|
|||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
var tailscaleVersions = []string{"1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
|
||||
|
||||
type TestNamespace struct {
|
||||
count int
|
||||
tailscales map[string]dockertest.Resource
|
||||
}
|
||||
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
stats *suite.SuiteInformation
|
||||
|
@ -44,17 +38,19 @@ type IntegrationTestSuite struct {
|
|||
headscale dockertest.Resource
|
||||
|
||||
namespaces map[string]TestNamespace
|
||||
|
||||
joinWaitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
func TestIntegrationTestSuite(t *testing.T) {
|
||||
s := new(IntegrationTestSuite)
|
||||
|
||||
s.namespaces = map[string]TestNamespace{
|
||||
"main": {
|
||||
count: 20,
|
||||
"thisspace": {
|
||||
count: 15,
|
||||
tailscales: make(map[string]dockertest.Resource),
|
||||
},
|
||||
"shared": {
|
||||
"otherspace": {
|
||||
count: 5,
|
||||
tailscales: make(map[string]dockertest.Resource),
|
||||
},
|
||||
|
@ -118,7 +114,7 @@ func (s *IntegrationTestSuite) saveLog(
|
|||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
||||
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
||||
|
||||
err = ioutil.WriteFile(
|
||||
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
||||
|
@ -141,6 +137,34 @@ func (s *IntegrationTestSuite) saveLog(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) Join(
|
||||
endpoint, key, hostname string,
|
||||
tailscale dockertest.Resource,
|
||||
) {
|
||||
defer s.joinWaitGroup.Done()
|
||||
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"up",
|
||||
"-login-server",
|
||||
endpoint,
|
||||
"--authkey",
|
||||
key,
|
||||
"--hostname",
|
||||
hostname,
|
||||
}
|
||||
|
||||
log.Println("Join command:", command)
|
||||
log.Printf("Running join command for %s\n", hostname)
|
||||
_, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
log.Printf("%s joined\n", hostname)
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) tailscaleContainer(
|
||||
namespace, identifier, version string,
|
||||
) (string, *dockertest.Resource) {
|
||||
|
@ -178,7 +202,7 @@ func (s *IntegrationTestSuite) tailscaleContainer(
|
|||
if err != nil {
|
||||
log.Fatalf("Could not start resource: %s", err)
|
||||
}
|
||||
fmt.Printf("Created %s container\n", hostname)
|
||||
log.Printf("Created %s container\n", hostname)
|
||||
|
||||
return hostname, pts
|
||||
}
|
||||
|
@ -221,15 +245,15 @@ func (s *IntegrationTestSuite) SetupSuite() {
|
|||
Cmd: []string{"headscale", "serve"},
|
||||
}
|
||||
|
||||
fmt.Println("Creating headscale container")
|
||||
log.Println("Creating headscale container")
|
||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||
s.headscale = *pheadscale
|
||||
} else {
|
||||
log.Fatalf("Could not start resource: %s", err)
|
||||
}
|
||||
fmt.Println("Created headscale container")
|
||||
log.Println("Created headscale container")
|
||||
|
||||
fmt.Println("Creating tailscale containers")
|
||||
log.Println("Creating tailscale containers")
|
||||
for namespace, scales := range s.namespaces {
|
||||
for i := 0; i < scales.count; i++ {
|
||||
version := tailscaleVersions[i%len(tailscaleVersions)]
|
||||
|
@ -243,7 +267,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
|
|||
}
|
||||
}
|
||||
|
||||
fmt.Println("Waiting for headscale to be ready")
|
||||
log.Println("Waiting for headscale to be ready")
|
||||
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8080/tcp"))
|
||||
|
||||
if err := s.pool.Retry(func() error {
|
||||
|
@ -266,19 +290,19 @@ func (s *IntegrationTestSuite) SetupSuite() {
|
|||
// https://github.com/stretchr/testify/issues/849
|
||||
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
||||
}
|
||||
fmt.Println("headscale container is ready")
|
||||
log.Println("headscale container is ready")
|
||||
|
||||
for namespace, scales := range s.namespaces {
|
||||
fmt.Printf("Creating headscale namespace: %s\n", namespace)
|
||||
log.Printf("Creating headscale namespace: %s\n", namespace)
|
||||
result, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{"headscale", "namespaces", "create", namespace},
|
||||
[]string{},
|
||||
)
|
||||
fmt.Println("headscale create namespace result: ", result)
|
||||
log.Println("headscale create namespace result: ", result)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
fmt.Printf("Creating pre auth key for %s\n", namespace)
|
||||
log.Printf("Creating pre auth key for %s\n", namespace)
|
||||
preAuthResult, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
|
@ -304,33 +328,16 @@ func (s *IntegrationTestSuite) SetupSuite() {
|
|||
|
||||
headscaleEndpoint := "http://headscale:8080"
|
||||
|
||||
fmt.Printf(
|
||||
log.Printf(
|
||||
"Joining tailscale containers to headscale at %s\n",
|
||||
headscaleEndpoint,
|
||||
)
|
||||
for hostname, tailscale := range scales.tailscales {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"up",
|
||||
"-login-server",
|
||||
headscaleEndpoint,
|
||||
"--authkey",
|
||||
preAuthKey.Key,
|
||||
"--hostname",
|
||||
hostname,
|
||||
}
|
||||
|
||||
fmt.Println("Join command:", command)
|
||||
fmt.Printf("Running join command for %s\n", hostname)
|
||||
result, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
fmt.Println("tailscale result: ", result)
|
||||
assert.Nil(s.T(), err)
|
||||
fmt.Printf("%s joined\n", hostname)
|
||||
s.joinWaitGroup.Add(1)
|
||||
go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
|
||||
}
|
||||
|
||||
s.joinWaitGroup.Wait()
|
||||
}
|
||||
|
||||
// The nodes need a bit of time to get their updated maps from headscale
|
||||
|
@ -350,7 +357,7 @@ func (s *IntegrationTestSuite) HandleStats(
|
|||
|
||||
func (s *IntegrationTestSuite) TestListNodes() {
|
||||
for namespace, scales := range s.namespaces {
|
||||
fmt.Println("Listing nodes")
|
||||
log.Println("Listing nodes")
|
||||
result, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{"headscale", "--namespace", namespace, "nodes", "list"},
|
||||
|
@ -358,7 +365,7 @@ func (s *IntegrationTestSuite) TestListNodes() {
|
|||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
fmt.Printf("List nodes: \n%s\n", result)
|
||||
log.Printf("List nodes: \n%s\n", result)
|
||||
|
||||
// Chck that the correct count of host is present in node list
|
||||
lines := strings.Split(result, "\n")
|
||||
|
@ -381,7 +388,7 @@ func (s *IntegrationTestSuite) TestGetIpAddresses() {
|
|||
s.T().Run(hostname, func(t *testing.T) {
|
||||
assert.NotNil(t, ip)
|
||||
|
||||
fmt.Printf("IP for %s: %s\n", hostname, ip)
|
||||
log.Printf("IP for %s: %s\n", hostname, ip)
|
||||
|
||||
// c.Assert(ip.Valid(), check.IsTrue)
|
||||
assert.True(t, ip.Is4() || ip.Is6())
|
||||
|
@ -410,7 +417,7 @@ func (s *IntegrationTestSuite) TestGetIpAddresses() {
|
|||
// s.T().Run(hostname, func(t *testing.T) {
|
||||
// command := []string{"tailscale", "status", "--json"}
|
||||
//
|
||||
// fmt.Printf("Getting status for %s\n", hostname)
|
||||
// log.Printf("Getting status for %s\n", hostname)
|
||||
// result, err := ExecuteCommand(
|
||||
// &tailscale,
|
||||
// command,
|
||||
|
@ -452,6 +459,7 @@ func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP {
|
|||
return ips
|
||||
}
|
||||
|
||||
// TODO: Adopt test for cross communication between namespaces
|
||||
func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
|
||||
for _, scales := range s.namespaces {
|
||||
ips, err := getIPs(scales.tailscales)
|
||||
|
@ -476,7 +484,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
|
|||
ip.String(),
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
log.Printf(
|
||||
"Pinging from %s to %s (%s)\n",
|
||||
hostname,
|
||||
peername,
|
||||
|
@ -488,7 +496,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
|
|||
[]string{},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("Result for %s: %s\n", hostname, result)
|
||||
log.Printf("Result for %s: %s\n", hostname, result)
|
||||
assert.Contains(t, result, "pong")
|
||||
})
|
||||
}
|
||||
|
@ -497,111 +505,6 @@ func (s *IntegrationTestSuite) TestPingAllPeersByAddress() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestSharedNodes() {
|
||||
main := s.namespaces["main"]
|
||||
shared := s.namespaces["shared"]
|
||||
|
||||
result, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
"--namespace",
|
||||
"shared",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
var machineList []v1.Machine
|
||||
err = json.Unmarshal([]byte(result), &machineList)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
for _, machine := range machineList {
|
||||
result, err := ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"share",
|
||||
"--identifier", fmt.Sprint(machine.Id),
|
||||
"--namespace", "main",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
fmt.Println("Shared node with result: ", result)
|
||||
}
|
||||
|
||||
result, err = ExecuteCommand(
|
||||
&s.headscale,
|
||||
[]string{"headscale", "nodes", "list", "--namespace", "main"},
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
fmt.Println("Nodelist after sharing", result)
|
||||
|
||||
// Chck that the correct count of host is present in node list
|
||||
lines := strings.Split(result, "\n")
|
||||
assert.Equal(s.T(), len(main.tailscales)+len(shared.tailscales), len(lines)-2)
|
||||
|
||||
for hostname := range main.tailscales {
|
||||
assert.Contains(s.T(), result, hostname)
|
||||
}
|
||||
|
||||
for hostname := range shared.tailscales {
|
||||
assert.Contains(s.T(), result, hostname)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
sharedIps, err := getIPs(shared.tailscales)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
for hostname, tailscale := range main.tailscales {
|
||||
for peername, peerIPs := range sharedIps {
|
||||
for i, ip := range peerIPs {
|
||||
// We currently cant ping ourselves, so skip that.
|
||||
if peername == hostname {
|
||||
continue
|
||||
}
|
||||
s.T().
|
||||
Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) {
|
||||
// We are only interested in "direct ping" which means what we
|
||||
// might need a couple of more attempts before reaching the node.
|
||||
command := []string{
|
||||
"tailscale", "ping",
|
||||
"--timeout=15s",
|
||||
"--c=20",
|
||||
"--until-direct=true",
|
||||
ip.String(),
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
"Pinging from %s to %s (%s)\n",
|
||||
hostname,
|
||||
peername,
|
||||
ip,
|
||||
)
|
||||
result, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("Result for %s: %s\n", hostname, result)
|
||||
assert.Contains(t, result, "pong")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestTailDrop() {
|
||||
for _, scales := range s.namespaces {
|
||||
ips, err := getIPs(scales.tailscales)
|
||||
|
@ -616,6 +519,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
|
|||
}
|
||||
time.Sleep(sleepInverval)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -638,7 +542,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
|
|||
fmt.Sprintf("%s:", peername),
|
||||
}
|
||||
retry(10, 1*time.Second, func() error {
|
||||
fmt.Printf(
|
||||
log.Printf(
|
||||
"Sending file from %s to %s\n",
|
||||
hostname,
|
||||
peername,
|
||||
|
@ -677,7 +581,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
|
|||
"ls",
|
||||
fmt.Sprintf("/tmp/file_from_%s", peername),
|
||||
}
|
||||
fmt.Printf(
|
||||
log.Printf(
|
||||
"Checking file in %s (%s) from %s (%s)\n",
|
||||
hostname,
|
||||
ips[hostname],
|
||||
|
@ -690,7 +594,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
|
|||
[]string{},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("Result for %s: %s\n", peername, result)
|
||||
log.Printf("Result for %s: %s\n", peername, result)
|
||||
assert.Equal(
|
||||
t,
|
||||
fmt.Sprintf("/tmp/file_from_%s\n", peername),
|
||||
|
@ -720,7 +624,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByHostname() {
|
|||
fmt.Sprintf("%s.%s.headscale.net", peername, namespace),
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
log.Printf(
|
||||
"Pinging using hostname from %s to %s\n",
|
||||
hostname,
|
||||
peername,
|
||||
|
@ -731,7 +635,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByHostname() {
|
|||
[]string{},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("Result for %s: %s\n", hostname, result)
|
||||
log.Printf("Result for %s: %s\n", hostname, result)
|
||||
assert.Contains(t, result, "pong")
|
||||
})
|
||||
}
|
||||
|
@ -754,7 +658,7 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
|
|||
fmt.Sprintf("%s.%s.headscale.net", peername, namespace),
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
log.Printf(
|
||||
"Resolving name %s from %s\n",
|
||||
peername,
|
||||
hostname,
|
||||
|
@ -765,7 +669,7 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
|
|||
[]string{},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("Result for %s: %s\n", hostname, result)
|
||||
log.Printf("Result for %s: %s\n", hostname, result)
|
||||
|
||||
for _, ip := range ips {
|
||||
assert.Contains(t, result, ip.String())
|
||||
|
@ -776,38 +680,6 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
|
|||
}
|
||||
}
|
||||
|
||||
func getIPs(
|
||||
tailscales map[string]dockertest.Resource,
|
||||
) (map[string][]netaddr.IP, error) {
|
||||
ips := make(map[string][]netaddr.IP)
|
||||
for hostname, tailscale := range tailscales {
|
||||
command := []string{"tailscale", "ip"}
|
||||
|
||||
result, err := ExecuteCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, address := range strings.Split(result, "\n") {
|
||||
address = strings.TrimSuffix(address, "\n")
|
||||
if len(address) < 1 {
|
||||
continue
|
||||
}
|
||||
ip, err := netaddr.ParseIP(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips[hostname] = append(ips[hostname], ip)
|
||||
}
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func getAPIURLs(
|
||||
tailscales map[string]dockertest.Resource,
|
||||
) (map[netaddr.IP]string, error) {
|
||||
|
|
|
@ -14,6 +14,7 @@ dns_config:
|
|||
db_path: /tmp/integration_test_db.sqlite3
|
||||
private_key_path: private.key
|
||||
listen_addr: 0.0.0.0:8080
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
server_url: http://headscale:8080
|
||||
|
||||
derp:
|
||||
|
|
29
integration_test/etc_embedded_derp/config.yaml
Normal file
29
integration_test/etc_embedded_derp/config.yaml
Normal file
|
@ -0,0 +1,29 @@
|
|||
log_level: trace
|
||||
acl_policy_path: ""
|
||||
db_type: sqlite3
|
||||
ephemeral_node_inactivity_timeout: 30m
|
||||
ip_prefixes:
|
||||
- fd7a:115c:a1e0::/48
|
||||
- 100.64.0.0/10
|
||||
dns_config:
|
||||
base_domain: headscale.net
|
||||
magic_dns: true
|
||||
domains: []
|
||||
nameservers:
|
||||
- 1.1.1.1
|
||||
db_path: /tmp/integration_test_db.sqlite3
|
||||
private_key_path: private.key
|
||||
listen_addr: 0.0.0.0:8443
|
||||
server_url: https://headscale:8443
|
||||
tls_cert_path: "/etc/headscale/tls/server.crt"
|
||||
tls_key_path: "/etc/headscale/tls/server.key"
|
||||
tls_client_auth_mode: disabled
|
||||
derp:
|
||||
server:
|
||||
enabled: true
|
||||
region_id: 999
|
||||
region_code: "headscale"
|
||||
region_name: "Headscale Embedded DERP"
|
||||
stun:
|
||||
enabled: true
|
||||
listen_addr: "0.0.0.0:3478"
|
22
integration_test/etc_embedded_derp/tls/server.crt
Normal file
22
integration_test/etc_embedded_derp/tls/server.crt
Normal file
|
@ -0,0 +1,22 @@
|
|||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC8jCCAdqgAwIBAgIULbu+UbSTMG/LtxooLLh7BgSEyqEwDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJaGVhZHNjYWxlMCAXDTIyMDMwNTE2NDgwM1oYDzI1MjEx
|
||||
MTA0MTY0ODAzWjAUMRIwEAYDVQQDDAloZWFkc2NhbGUwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQDqcfpToLZUF0rlNwXkkt3lbyw4Cl4TJdx36o2PKaOK
|
||||
U+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1WATuQJlMeg+2UJXGaTGRKkkbPMy3
|
||||
5m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6sXmNeETJvBixpBev9yKJuVXgqHNS4
|
||||
NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH14rav8Uimonl8UTNVXufMzyUOuoaQ
|
||||
TGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3uQJXy0m8I6OrIoXLNxnqYMfFls79
|
||||
9SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJRG1E3ZiLAgMBAAGjOjA4MBQGA1Ud
|
||||
EQQNMAuCCWhlYWRzY2FsZTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
|
||||
AwEwDQYJKoZIhvcNAQELBQADggEBANGlVN7NCsJaKz0k0nhlRGK+tcxn2p1PXN/i
|
||||
Iy+JX8ahixPC4ocRwOhrXgb390ZXLLwq08HrWYRB/Wi1VUzCp5d8dVxvrR43dJ+v
|
||||
L2EOBiIKgcu2C3pWW1qRR46/EoXUU9kSH2VNBvIhNufi32kEOidoDzxtQf6qVCoF
|
||||
guUt1JkAqrynv1UvR/2ZRM/WzM/oJ8qfECwrwDxyYhkqU5Z5jCWg0C6kPIBvNdzt
|
||||
B0eheWS+ZxVwkePTR4e17kIafwknth3lo+orxVrq/xC+OVM1bGrt2ZyD64ZvEqQl
|
||||
w6kgbzBdLScAQptWOFThwhnJsg0UbYKimZsnYmjVEuN59TJv92M=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
(Expires on Nov 4 16:48:03 2521 GMT)
|
||||
|
28
integration_test/etc_embedded_derp/tls/server.key
Normal file
28
integration_test/etc_embedded_derp/tls/server.key
Normal file
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDqcfpToLZUF0rl
|
||||
NwXkkt3lbyw4Cl4TJdx36o2PKaOKU+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1
|
||||
WATuQJlMeg+2UJXGaTGRKkkbPMy35m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6s
|
||||
XmNeETJvBixpBev9yKJuVXgqHNS4NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH1
|
||||
4rav8Uimonl8UTNVXufMzyUOuoaQTGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3
|
||||
uQJXy0m8I6OrIoXLNxnqYMfFls799SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJ
|
||||
RG1E3ZiLAgMBAAECggEBALu1Ni/u5Qy++YA8ZcN0s6UXNdhItLmv/q0kZuLQ+9et
|
||||
CT8VZfFInLndTdsaXenDKLHdryunviFA8SV+q7P2lMbek+Xs735EiyMnMBFWxLIZ
|
||||
FWNGOeQERGL19QCmLEOmEi2b+iWJQHlKaMWpbPXL3w11a+lKjIBNO4ALfoJ5QveZ
|
||||
cGMKsJdm/mpqBvLeNeh2eAFk3Gp6sT1g80Ge8NkgyzFBNIqnut0eerM15kPTc6Qz
|
||||
12JLaOXUuV3PrcB4PN4nOwrTDg88GDNOQtc1Pc9r4nOHyLfr8X7QEtj1wXSwmOuK
|
||||
d6ynMnAmoxVA9wEnupLbil1bzohRzpsTpkmDruYaBEECgYEA/Z09I8D6mt2NVqIE
|
||||
KyvLjBK39ijSV9r3/lvB2Ple2OOL5YQEd+yTrIFy+3zdUnDgD1zmNnXjmjvHZ9Lc
|
||||
IFf2o06AF84QLNB5gLPdDQkGNFdDqUxljBrfAfE3oANmPS/B0SijMGOOOiDO2FtO
|
||||
xl1nfRr78mswuRs9awoUWCdNRKUCgYEA7KaTYKIQW/FEjw9lshp74q5vbn6zoXF5
|
||||
7N8VkwI+bBVNvRbM9XZ8qhfgRdu9eXs5oL/N4mSYY54I8fA//pJ0Z2vpmureMm1V
|
||||
mL5WBUmSD9DIbAchoK+sRiQhVmNMBQC6cHMABA7RfXvBeGvWrm9pKCS6ZLgLjkjp
|
||||
PsmAcaXQcW8CgYEA2inAxljjOwUK6FNGsrxhxIT1qtNC3kCGxE+6WSNq67gSR8Vg
|
||||
8qiX//T7LEslOB3RIGYRwxd2St7RkgZZRZllmOWWWuPwFhzf6E7RAL2akLvggGov
|
||||
kG4tGEagSw2hjVDfsUT73ExHtMk0Jfmlsg33UC8+PDLpHtLH6qQpDAwC8+ECgYEA
|
||||
o+AqOIWhvHmT11l7O915Ip1WzvZwYADbxLsrDnVEUsZh4epTHjvh0kvcY6PqTqCV
|
||||
ZIrOANNWb811Nkz/k8NJVoD08PFp0xPBbZeIq/qpachTsfMyRzq/mobUiyUR9Hjv
|
||||
ooUQYr78NOApNsG+lWbTNBhS9wI4BlzZIECbcJe5g4MCgYEAndRoy8S+S0Hx/S8a
|
||||
O3hzXeDmivmgWqn8NVD4AKOovpkz4PaIVVQbAQkiNfAx8/DavPvjEKAbDezJ4ECV
|
||||
j7IsOWtDVI7pd6eF9fTcECwisrda8aUoiOap8AQb48153Vx+g2N4Vy3uH0xJs4cz
|
||||
TDALZPOBg8VlV+HEFDP43sp9Bf0=
|
||||
-----END PRIVATE KEY-----
|
525
machine.go
525
machine.go
|
@ -2,8 +2,6 @@ package headscale
|
|||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
@ -14,17 +12,24 @@ import (
|
|||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gorm.io/datatypes"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
const (
|
||||
errMachineNotFound = Error("machine not found")
|
||||
errMachineAlreadyRegistered = Error("machine already registered")
|
||||
errMachineRouteIsNotAvailable = Error("route is not available on machine")
|
||||
errMachineAddressesInvalid = Error("failed to parse machine addresses")
|
||||
errMachineNotFound = Error("machine not found")
|
||||
errMachineRouteIsNotAvailable = Error("route is not available on machine")
|
||||
errMachineAddressesInvalid = Error("failed to parse machine addresses")
|
||||
errMachineNotFoundRegistrationCache = Error(
|
||||
"machine not found in registration cache",
|
||||
)
|
||||
errCouldNotConvertMachineInterface = Error("failed to convert machine interface")
|
||||
errHostnameTooLong = Error("Hostname too long")
|
||||
)
|
||||
|
||||
const (
|
||||
maxHostnameLength = 255
|
||||
)
|
||||
|
||||
// Machine is a Headscale client.
|
||||
|
@ -38,18 +43,19 @@ type Machine struct {
|
|||
NamespaceID uint
|
||||
Namespace Namespace `gorm:"foreignKey:NamespaceID"`
|
||||
|
||||
Registered bool // temp
|
||||
RegisterMethod string
|
||||
AuthKeyID uint
|
||||
AuthKey *PreAuthKey
|
||||
|
||||
// TODO(kradalby): This seems like irrelevant information?
|
||||
AuthKeyID uint
|
||||
AuthKey *PreAuthKey
|
||||
|
||||
LastSeen *time.Time
|
||||
LastSuccessfulUpdate *time.Time
|
||||
Expiry *time.Time
|
||||
|
||||
HostInfo datatypes.JSON
|
||||
Endpoints datatypes.JSON
|
||||
EnabledRoutes datatypes.JSON
|
||||
HostInfo HostInfo
|
||||
Endpoints StringList
|
||||
EnabledRoutes IPPrefixes
|
||||
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
@ -61,11 +67,6 @@ type (
|
|||
MachinesP []*Machine
|
||||
)
|
||||
|
||||
// For the time being this method is rather naive.
|
||||
func (machine Machine) isRegistered() bool {
|
||||
return machine.Registered
|
||||
}
|
||||
|
||||
type MachineAddresses []netaddr.IP
|
||||
|
||||
func (ma MachineAddresses) ToStringSlice() []string {
|
||||
|
@ -112,22 +113,124 @@ func (machine Machine) isExpired() bool {
|
|||
// If Expiry is not set, the client has not indicated that
|
||||
// it wants an expiry time, it is therefor considered
|
||||
// to mean "not expired"
|
||||
if machine.Expiry.IsZero() {
|
||||
if machine.Expiry == nil || machine.Expiry.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
return time.Now().UTC().After(*machine.Expiry)
|
||||
}
|
||||
|
||||
func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
|
||||
func containsAddresses(inputs []string, addrs []string) bool {
|
||||
for _, addr := range addrs {
|
||||
if containsString(inputs, addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchSourceAndDestinationWithRule.
|
||||
func matchSourceAndDestinationWithRule(
|
||||
ruleSources []string,
|
||||
ruleDestinations []string,
|
||||
source []string,
|
||||
destination []string,
|
||||
) bool {
|
||||
return containsAddresses(ruleSources, source) &&
|
||||
containsAddresses(ruleDestinations, destination)
|
||||
}
|
||||
|
||||
// getFilteredByACLPeerss should return the list of peers authorized to be accessed from machine.
|
||||
func getFilteredByACLPeers(
|
||||
machines []Machine,
|
||||
rules []tailcfg.FilterRule,
|
||||
machine *Machine,
|
||||
) Machines {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("Finding peers filtered by ACLs")
|
||||
|
||||
peers := make(map[uint64]Machine)
|
||||
// Aclfilter peers here. We are itering through machines in all namespaces and search through the computed aclRules
|
||||
// for match between rule SrcIPs and DstPorts. If the rule is a match we allow the machine to be viewable.
|
||||
for _, peer := range machines {
|
||||
if peer.ID == machine.ID {
|
||||
continue
|
||||
}
|
||||
for _, rule := range rules {
|
||||
var dst []string
|
||||
for _, d := range rule.DstPorts {
|
||||
dst = append(dst, d.IP)
|
||||
}
|
||||
if matchSourceAndDestinationWithRule(
|
||||
rule.SrcIPs,
|
||||
dst,
|
||||
machine.IPAddresses.ToStringSlice(),
|
||||
peer.IPAddresses.ToStringSlice(),
|
||||
) || // match source and destination
|
||||
matchSourceAndDestinationWithRule(
|
||||
rule.SrcIPs,
|
||||
dst,
|
||||
peer.IPAddresses.ToStringSlice(),
|
||||
machine.IPAddresses.ToStringSlice(),
|
||||
) || // match return path
|
||||
matchSourceAndDestinationWithRule(
|
||||
rule.SrcIPs,
|
||||
dst,
|
||||
machine.IPAddresses.ToStringSlice(),
|
||||
[]string{"*"},
|
||||
) || // match source and all destination
|
||||
matchSourceAndDestinationWithRule(
|
||||
rule.SrcIPs,
|
||||
dst,
|
||||
[]string{"*"},
|
||||
[]string{"*"},
|
||||
) || // match source and all destination
|
||||
matchSourceAndDestinationWithRule(
|
||||
rule.SrcIPs,
|
||||
dst,
|
||||
[]string{"*"},
|
||||
peer.IPAddresses.ToStringSlice(),
|
||||
) || // match source and all destination
|
||||
matchSourceAndDestinationWithRule(
|
||||
rule.SrcIPs,
|
||||
dst,
|
||||
[]string{"*"},
|
||||
machine.IPAddresses.ToStringSlice(),
|
||||
) { // match all sources and source
|
||||
peers[peer.ID] = peer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
authorizedPeers := make([]Machine, 0, len(peers))
|
||||
for _, m := range peers {
|
||||
authorizedPeers = append(authorizedPeers, m)
|
||||
}
|
||||
sort.Slice(
|
||||
authorizedPeers,
|
||||
func(i, j int) bool { return authorizedPeers[i].ID < authorizedPeers[j].ID },
|
||||
)
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msgf("Found some machines: %v", machines)
|
||||
|
||||
return authorizedPeers
|
||||
}
|
||||
|
||||
func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("Finding direct peers")
|
||||
|
||||
machines := Machines{}
|
||||
if err := h.db.Preload("Namespace").Where("namespace_id = ? AND machine_key <> ? AND registered",
|
||||
machine.NamespaceID, machine.MachineKey).Find(&machines).Error; err != nil {
|
||||
if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("machine_key <> ?",
|
||||
machine.MachineKey).Find(&machines).Error; err != nil {
|
||||
log.Error().Err(err).Msg("Error accessing db")
|
||||
|
||||
return Machines{}, err
|
||||
|
@ -138,109 +241,40 @@ func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
|
|||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msgf("Found direct machines: %s", machines.String())
|
||||
Msgf("Found peers: %s", machines.String())
|
||||
|
||||
return machines, nil
|
||||
}
|
||||
|
||||
// getShared fetches machines that are shared to the `Namespace` of the machine we are getting peers for.
|
||||
func (h *Headscale) getShared(machine *Machine) (Machines, error) {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("Finding shared peers")
|
||||
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
|
||||
var peers Machines
|
||||
var err error
|
||||
|
||||
sharedMachines := []SharedMachine{}
|
||||
if err := h.db.Preload("Namespace").Preload("Machine").Preload("Machine.Namespace").Where("namespace_id = ?",
|
||||
machine.NamespaceID).Find(&sharedMachines).Error; err != nil {
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
peers := make(Machines, 0)
|
||||
for _, sharedMachine := range sharedMachines {
|
||||
peers = append(peers, sharedMachine.Machine)
|
||||
}
|
||||
|
||||
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msgf("Found shared peers: %s", peers.String())
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
// getSharedTo fetches the machines of the namespaces this machine is shared in.
|
||||
func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("Finding peers in namespaces this machine is shared with")
|
||||
|
||||
sharedMachines := []SharedMachine{}
|
||||
if err := h.db.Preload("Namespace").Preload("Machine").Preload("Machine.Namespace").Where("machine_id = ?",
|
||||
machine.ID).Find(&sharedMachines).Error; err != nil {
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
peers := make(Machines, 0)
|
||||
for _, sharedMachine := range sharedMachines {
|
||||
namespaceMachines, err := h.ListMachinesInNamespace(
|
||||
sharedMachine.Namespace.Name,
|
||||
)
|
||||
// If ACLs rules are defined, filter visible host list with the ACLs
|
||||
// else use the classic namespace scope
|
||||
if h.aclPolicy != nil {
|
||||
var machines []Machine
|
||||
machines, err = h.ListMachines()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error retrieving list of machines")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
peers = getFilteredByACLPeers(machines, h.aclRules, machine)
|
||||
} else {
|
||||
peers, err = h.ListPeers(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
peers = append(peers, namespaceMachines...)
|
||||
}
|
||||
|
||||
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msgf("Found peers we are shared with: %s", peers.String())
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
|
||||
direct, err := h.getDirectPeers(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
shared, err := h.getShared(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
sharedTo, err := h.getSharedTo(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
peers := append(direct, shared...)
|
||||
peers = append(peers, sharedTo...)
|
||||
|
||||
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
|
@ -258,7 +292,7 @@ func (h *Headscale) getValidPeers(machine *Machine) (Machines, error) {
|
|||
}
|
||||
|
||||
for _, peer := range peers {
|
||||
if peer.isRegistered() && !peer.isExpired() {
|
||||
if !peer.isExpired() {
|
||||
validPeers = append(validPeers, peer)
|
||||
}
|
||||
}
|
||||
|
@ -347,13 +381,6 @@ func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) {
|
|||
|
||||
// DeleteMachine softs deletes a Machine from the database.
|
||||
func (h *Headscale) DeleteMachine(machine *Machine) error {
|
||||
err := h.RemoveSharedMachineFromAllNamespaces(machine)
|
||||
if err != nil && errors.Is(err, errMachineNotShared) {
|
||||
return err
|
||||
}
|
||||
|
||||
machine.Registered = false
|
||||
h.db.Save(&machine) // we mark it as unregistered, just in case
|
||||
if err := h.db.Delete(&machine).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -371,11 +398,6 @@ func (h *Headscale) TouchMachine(machine *Machine) error {
|
|||
|
||||
// HardDeleteMachine hard deletes a Machine from the database.
|
||||
func (h *Headscale) HardDeleteMachine(machine *Machine) error {
|
||||
err := h.RemoveSharedMachineFromAllNamespaces(machine)
|
||||
if err != nil && errors.Is(err, errMachineNotShared) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.db.Unscoped().Delete(&machine).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -384,20 +406,8 @@ func (h *Headscale) HardDeleteMachine(machine *Machine) error {
|
|||
}
|
||||
|
||||
// GetHostInfo returns a Hostinfo struct for the machine.
|
||||
func (machine *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) {
|
||||
hostinfo := tailcfg.Hostinfo{}
|
||||
if len(machine.HostInfo) != 0 {
|
||||
hi, err := machine.HostInfo.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(hi, &hostinfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &hostinfo, nil
|
||||
func (machine *Machine) GetHostInfo() tailcfg.Hostinfo {
|
||||
return tailcfg.Hostinfo(machine.HostInfo)
|
||||
}
|
||||
|
||||
func (h *Headscale) isOutdated(machine *Machine) bool {
|
||||
|
@ -407,17 +417,9 @@ func (h *Headscale) isOutdated(machine *Machine) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
sharedMachines, _ := h.getShared(machine)
|
||||
|
||||
namespaceSet := set.New(set.ThreadSafe)
|
||||
namespaceSet.Add(machine.Namespace.Name)
|
||||
|
||||
// Check if any of our shared namespaces has updates that we have
|
||||
// not propagated.
|
||||
for _, sharedMachine := range sharedMachines {
|
||||
namespaceSet.Add(sharedMachine.Namespace.Name)
|
||||
}
|
||||
|
||||
namespaces := make([]string, namespaceSet.Size())
|
||||
for index, namespace := range namespaceSet.List() {
|
||||
if name, ok := namespace.(string); ok {
|
||||
|
@ -532,55 +534,15 @@ func (machine Machine) toNode(
|
|||
[]netaddr.IPPrefix{},
|
||||
addrs...) // we append the node own IP, as it is required by the clients
|
||||
|
||||
// TODO(kradalby): Needs investigation, We probably dont need this condition
|
||||
// now that we dont have shared nodes
|
||||
if includeRoutes {
|
||||
routesStr := []string{}
|
||||
if len(machine.EnabledRoutes) != 0 {
|
||||
allwIps, err := machine.EnabledRoutes.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(allwIps, &routesStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, routeStr := range routesStr {
|
||||
ip, err := netaddr.ParseIPPrefix(routeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowedIPs = append(allowedIPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
endpoints := []string{}
|
||||
if len(machine.Endpoints) != 0 {
|
||||
be, err := machine.Endpoints.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(be, &endpoints)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
hostinfo := tailcfg.Hostinfo{}
|
||||
if len(machine.HostInfo) != 0 {
|
||||
hi, err := machine.HostInfo.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(hi, &hostinfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowedIPs = append(allowedIPs, machine.EnabledRoutes...)
|
||||
}
|
||||
|
||||
var derp string
|
||||
if hostinfo.NetInfo != nil {
|
||||
derp = fmt.Sprintf("127.3.3.40:%d", hostinfo.NetInfo.PreferredDERP)
|
||||
if machine.HostInfo.NetInfo != nil {
|
||||
derp = fmt.Sprintf("127.3.3.40:%d", machine.HostInfo.NetInfo.PreferredDERP)
|
||||
} else {
|
||||
derp = "127.3.3.40:0" // Zero means disconnected or unknown.
|
||||
}
|
||||
|
@ -600,10 +562,19 @@ func (machine Machine) toNode(
|
|||
machine.Namespace.Name,
|
||||
baseDomain,
|
||||
)
|
||||
if len(hostname) > maxHostnameLength {
|
||||
return nil, fmt.Errorf(
|
||||
"hostname %q is too long it cannot except 255 ASCII chars: %w",
|
||||
hostname,
|
||||
errHostnameTooLong,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
hostname = machine.Name
|
||||
}
|
||||
|
||||
hostInfo := machine.GetHostInfo()
|
||||
|
||||
node := tailcfg.Node{
|
||||
ID: tailcfg.NodeID(machine.ID), // this is the actual ID
|
||||
StableID: tailcfg.StableNodeID(
|
||||
|
@ -617,15 +588,15 @@ func (machine Machine) toNode(
|
|||
DiscoKey: discoKey,
|
||||
Addresses: addrs,
|
||||
AllowedIPs: allowedIPs,
|
||||
Endpoints: endpoints,
|
||||
Endpoints: machine.Endpoints,
|
||||
DERP: derp,
|
||||
|
||||
Hostinfo: hostinfo,
|
||||
Hostinfo: hostInfo.View(),
|
||||
Created: machine.CreatedAt,
|
||||
LastSeen: machine.LastSeen,
|
||||
|
||||
KeepAlive: true,
|
||||
MachineAuthorized: machine.Registered,
|
||||
MachineAuthorized: !machine.isExpired(),
|
||||
Capabilities: []string{tailcfg.CapabilityFileSharing},
|
||||
}
|
||||
|
||||
|
@ -643,8 +614,6 @@ func (machine *Machine) toProto() *v1.Machine {
|
|||
Name: machine.Name,
|
||||
Namespace: machine.Namespace.toProto(),
|
||||
|
||||
Registered: machine.Registered,
|
||||
|
||||
// TODO(kradalby): Implement register method enum converter
|
||||
// RegisterMethod: ,
|
||||
|
||||
|
@ -672,73 +641,52 @@ func (machine *Machine) toProto() *v1.Machine {
|
|||
return machineProto
|
||||
}
|
||||
|
||||
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
|
||||
func (h *Headscale) RegisterMachine(
|
||||
func (h *Headscale) RegisterMachineFromAuthCallback(
|
||||
machineKeyStr string,
|
||||
namespaceName string,
|
||||
registrationMethod string,
|
||||
) (*Machine, error) {
|
||||
namespace, err := h.GetNamespace(namespaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if machineInterface, ok := h.registrationCache.Get(machineKeyStr); ok {
|
||||
if registrationMachine, ok := machineInterface.(Machine); ok {
|
||||
namespace, err := h.GetNamespace(namespaceName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to find namespace in register machine from auth callback, %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
var machineKey key.MachinePublic
|
||||
err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registrationMachine.NamespaceID = namespace.ID
|
||||
registrationMachine.RegisterMethod = registrationMethod
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine_key_str", machineKeyStr).
|
||||
Str("machine_key", machineKey.String()).
|
||||
Msg("Registering machine")
|
||||
machine, err := h.RegisterMachine(
|
||||
registrationMachine,
|
||||
)
|
||||
|
||||
machine, err := h.GetMachineByMachineKey(machineKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(kradalby): Currently, if it fails to find a requested expiry, non will be set
|
||||
// This means that if a user is to slow with register a machine, it will possibly not
|
||||
// have the correct expiry.
|
||||
requestedTime := time.Time{}
|
||||
if requestedTimeIf, found := h.requestedExpiryCache.Get(machineKey.String()); found {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("Expiry time found in cache, assigning to node")
|
||||
if reqTime, ok := requestedTimeIf.(time.Time); ok {
|
||||
requestedTime = reqTime
|
||||
return machine, err
|
||||
} else {
|
||||
return nil, errCouldNotConvertMachineInterface
|
||||
}
|
||||
}
|
||||
|
||||
if machine.isRegistered() {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("machine already registered, reauthenticating")
|
||||
return nil, errMachineNotFoundRegistrationCache
|
||||
}
|
||||
|
||||
h.RefreshMachine(machine, requestedTime)
|
||||
|
||||
return machine, nil
|
||||
}
|
||||
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
|
||||
func (h *Headscale) RegisterMachine(machine Machine,
|
||||
) (*Machine, error) {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine_key", machine.MachineKey).
|
||||
Msg("Registering machine")
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("Attempting to register machine")
|
||||
|
||||
if machine.isRegistered() {
|
||||
err := errMachineAlreadyRegistered
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Str("machine", machine.Name).
|
||||
Msg("Attempting to register machine")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
h.ipAllocationMutex.Lock()
|
||||
defer h.ipAllocationMutex.Unlock()
|
||||
|
||||
ips, err := h.getAvailableIPs()
|
||||
if err != nil {
|
||||
|
@ -751,17 +699,8 @@ func (h *Headscale) RegisterMachine(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Str("ip", strings.Join(ips.ToStringSlice(), ",")).
|
||||
Msg("Found IP for host")
|
||||
|
||||
machine.IPAddresses = ips
|
||||
machine.NamespaceID = namespace.ID
|
||||
machine.Registered = true
|
||||
machine.RegisterMethod = RegisterMethodCLI
|
||||
machine.Expiry = &requestedTime
|
||||
|
||||
h.db.Save(&machine)
|
||||
|
||||
log.Trace().
|
||||
|
@ -770,40 +709,15 @@ func (h *Headscale) RegisterMachine(
|
|||
Str("ip", strings.Join(ips.ToStringSlice(), ",")).
|
||||
Msg("Machine registered with the database")
|
||||
|
||||
return machine, nil
|
||||
return &machine, nil
|
||||
}
|
||||
|
||||
func (machine *Machine) GetAdvertisedRoutes() ([]netaddr.IPPrefix, error) {
|
||||
hostInfo, err := machine.GetHostInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hostInfo.RoutableIPs, nil
|
||||
func (machine *Machine) GetAdvertisedRoutes() []netaddr.IPPrefix {
|
||||
return machine.HostInfo.RoutableIPs
|
||||
}
|
||||
|
||||
func (machine *Machine) GetEnabledRoutes() ([]netaddr.IPPrefix, error) {
|
||||
data, err := machine.EnabledRoutes.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routesStr := []string{}
|
||||
err = json.Unmarshal(data, &routesStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes := make([]netaddr.IPPrefix, len(routesStr))
|
||||
for index, routeStr := range routesStr {
|
||||
route, err := netaddr.ParseIPPrefix(routeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes[index] = route
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
func (machine *Machine) GetEnabledRoutes() []netaddr.IPPrefix {
|
||||
return machine.EnabledRoutes
|
||||
}
|
||||
|
||||
func (machine *Machine) IsRoutesEnabled(routeStr string) bool {
|
||||
|
@ -812,10 +726,7 @@ func (machine *Machine) IsRoutesEnabled(routeStr string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
enabledRoutes, err := machine.GetEnabledRoutes()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
enabledRoutes := machine.GetEnabledRoutes()
|
||||
|
||||
for _, enabledRoute := range enabledRoutes {
|
||||
if route == enabledRoute {
|
||||
|
@ -839,13 +750,8 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
|
|||
newRoutes[index] = route
|
||||
}
|
||||
|
||||
availableRoutes, err := machine.GetAdvertisedRoutes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, newRoute := range newRoutes {
|
||||
if !containsIPPrefix(availableRoutes, newRoute) {
|
||||
if !containsIPPrefix(machine.GetAdvertisedRoutes(), newRoute) {
|
||||
return fmt.Errorf(
|
||||
"route (%s) is not available on node %s: %w",
|
||||
machine.Name,
|
||||
|
@ -854,30 +760,19 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
|
|||
}
|
||||
}
|
||||
|
||||
routes, err := json.Marshal(newRoutes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
machine.EnabledRoutes = datatypes.JSON(routes)
|
||||
machine.EnabledRoutes = newRoutes
|
||||
h.db.Save(&machine)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (machine *Machine) RoutesToProto() (*v1.Routes, error) {
|
||||
availableRoutes, err := machine.GetAdvertisedRoutes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (machine *Machine) RoutesToProto() *v1.Routes {
|
||||
availableRoutes := machine.GetAdvertisedRoutes()
|
||||
|
||||
enabledRoutes, err := machine.GetEnabledRoutes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enabledRoutes := machine.GetEnabledRoutes()
|
||||
|
||||
return &v1.Routes{
|
||||
AdvertisedRoutes: ipPrefixToString(availableRoutes),
|
||||
EnabledRoutes: ipPrefixToString(enabledRoutes),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
|
484
machine_test.go
484
machine_test.go
|
@ -1,11 +1,15 @@
|
|||
package headscale
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (s *Suite) TestGetMachine(c *check.C) {
|
||||
|
@ -25,16 +29,12 @@ func (s *Suite) TestGetMachine(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
app.db.Save(machine)
|
||||
|
||||
machineFromDB, err := app.GetMachine("test", "testmachine")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = machineFromDB.GetHostInfo()
|
||||
_, err = app.GetMachine("test", "testmachine")
|
||||
c.Assert(err, check.IsNil)
|
||||
}
|
||||
|
||||
|
@ -55,16 +55,12 @@ func (s *Suite) TestGetMachineByID(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
|
||||
machineByID, err := app.GetMachineByID(0)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = machineByID.GetHostInfo()
|
||||
_, err = app.GetMachineByID(0)
|
||||
c.Assert(err, check.IsNil)
|
||||
}
|
||||
|
||||
|
@ -78,7 +74,6 @@ func (s *Suite) TestDeleteMachine(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(1),
|
||||
}
|
||||
|
@ -101,7 +96,6 @@ func (s *Suite) TestHardDeleteMachine(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testmachine3",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(1),
|
||||
}
|
||||
|
@ -114,7 +108,7 @@ func (s *Suite) TestHardDeleteMachine(c *check.C) {
|
|||
c.Assert(err, check.NotNil)
|
||||
}
|
||||
|
||||
func (s *Suite) TestGetDirectPeers(c *check.C) {
|
||||
func (s *Suite) TestListPeers(c *check.C) {
|
||||
namespace, err := app.CreateNamespace("test")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
|
@ -132,7 +126,6 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
|
|||
DiscoKey: "faa" + strconv.Itoa(index),
|
||||
Name: "testmachine" + strconv.Itoa(index),
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
|
@ -142,10 +135,7 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
|
|||
machine0ByID, err := app.GetMachineByID(0)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = machine0ByID.GetHostInfo()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfMachine0, err := app.getDirectPeers(machine0ByID)
|
||||
peersOfMachine0, err := app.ListPeers(machine0ByID)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
c.Assert(len(peersOfMachine0), check.Equals, 9)
|
||||
|
@ -154,6 +144,85 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
|
|||
c.Assert(peersOfMachine0[8].Name, check.Equals, "testmachine10")
|
||||
}
|
||||
|
||||
func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
|
||||
type base struct {
|
||||
namespace *Namespace
|
||||
key *PreAuthKey
|
||||
}
|
||||
|
||||
stor := make([]base, 0)
|
||||
|
||||
for _, name := range []string{"test", "admin"} {
|
||||
namespace, err := app.CreateNamespace(name)
|
||||
c.Assert(err, check.IsNil)
|
||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
stor = append(stor, base{namespace, pak})
|
||||
}
|
||||
|
||||
_, err := app.GetMachineByID(0)
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
for index := 0; index <= 10; index++ {
|
||||
machine := Machine{
|
||||
ID: uint64(index),
|
||||
MachineKey: "foo" + strconv.Itoa(index),
|
||||
NodeKey: "bar" + strconv.Itoa(index),
|
||||
DiscoKey: "faa" + strconv.Itoa(index),
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))),
|
||||
},
|
||||
Name: "testmachine" + strconv.Itoa(index),
|
||||
NamespaceID: stor[index%2].namespace.ID,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(stor[index%2].key.ID),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
}
|
||||
|
||||
app.aclPolicy = &ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:test": {"admin"},
|
||||
},
|
||||
Hosts: map[string]netaddr.IPPrefix{},
|
||||
TagOwners: map[string][]string{},
|
||||
ACLs: []ACL{
|
||||
{Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}},
|
||||
{Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}},
|
||||
},
|
||||
Tests: []ACLTest{},
|
||||
}
|
||||
|
||||
err = app.UpdateACLRules()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
adminMachine, err := app.GetMachineByID(1)
|
||||
c.Logf("Machine(%v), namespace: %v", adminMachine.Name, adminMachine.Namespace)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
testMachine, err := app.GetMachineByID(2)
|
||||
c.Logf("Machine(%v), namespace: %v", testMachine.Name, testMachine.Namespace)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
machines, err := app.ListMachines()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfTestMachine := getFilteredByACLPeers(machines, app.aclRules, testMachine)
|
||||
peersOfAdminMachine := getFilteredByACLPeers(machines, app.aclRules, adminMachine)
|
||||
|
||||
c.Log(peersOfTestMachine)
|
||||
c.Assert(len(peersOfTestMachine), check.Equals, 4)
|
||||
c.Assert(peersOfTestMachine[0].Name, check.Equals, "testmachine4")
|
||||
c.Assert(peersOfTestMachine[1].Name, check.Equals, "testmachine6")
|
||||
c.Assert(peersOfTestMachine[3].Name, check.Equals, "testmachine10")
|
||||
|
||||
c.Log(peersOfAdminMachine)
|
||||
c.Assert(len(peersOfAdminMachine), check.Equals, 9)
|
||||
c.Assert(peersOfAdminMachine[0].Name, check.Equals, "testmachine2")
|
||||
c.Assert(peersOfAdminMachine[2].Name, check.Equals, "testmachine4")
|
||||
c.Assert(peersOfAdminMachine[5].Name, check.Equals, "testmachine7")
|
||||
}
|
||||
|
||||
func (s *Suite) TestExpireMachine(c *check.C) {
|
||||
namespace, err := app.CreateNamespace("test")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
@ -171,7 +240,6 @@ func (s *Suite) TestExpireMachine(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
Expiry: &time.Time{},
|
||||
|
@ -208,3 +276,381 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
|
|||
c.Assert(deserialized[i], check.Equals, input[i])
|
||||
}
|
||||
}
|
||||
|
||||
// nolint
|
||||
func Test_getFilteredByACLPeers(t *testing.T) {
|
||||
type args struct {
|
||||
machines []Machine
|
||||
rules []tailcfg.FilterRule
|
||||
machine *Machine
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Machines
|
||||
}{
|
||||
{
|
||||
name: "all hosts can talk to each other",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
machine: &Machine{ // current machine
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
want: Machines{
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "One host can talk to another, but not all hosts",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
machine: &Machine{ // current machine
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
want: Machines{
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "host cannot directly talk to destination, but return path is authorized",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.3"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
machine: &Machine{ // current machine
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
},
|
||||
want: Machines{
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rules allows all hosts to reach one destination",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
machine: &Machine{ // current machine
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
want: Machines{
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rules allows all hosts to reach one destination, destination can reach all hosts",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
machine: &Machine{ // current machine
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
},
|
||||
want: Machines{
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rule allows all hosts to reach all destinations",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
machine: &Machine{ // current machine
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
},
|
||||
want: Machines{
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "without rule all communications are forbidden",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
||||
},
|
||||
machine: &Machine{ // current machine
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
},
|
||||
want: Machines{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := getFilteredByACLPeers(
|
||||
tt.args.machines,
|
||||
tt.args.rules,
|
||||
tt.args.machine,
|
||||
)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getFilteredByACLPeers() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
105
namespaces.go
105
namespaces.go
|
@ -2,7 +2,10 @@ package headscale
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
|
@ -16,8 +19,16 @@ const (
|
|||
errNamespaceExists = Error("Namespace already exists")
|
||||
errNamespaceNotFound = Error("Namespace not found")
|
||||
errNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found")
|
||||
errInvalidNamespaceName = Error("Invalid namespace name")
|
||||
)
|
||||
|
||||
const (
|
||||
// value related to RFC 1123 and 952.
|
||||
labelHostnameLength = 63
|
||||
)
|
||||
|
||||
var invalidCharsInNamespaceRegex = regexp.MustCompile("[^a-z0-9-.]+")
|
||||
|
||||
// Namespace is the way Headscale implements the concept of users in Tailscale
|
||||
//
|
||||
// At the end of the day, users in Tailscale are some kind of 'bubbles' or namespaces
|
||||
|
@ -30,6 +41,10 @@ type Namespace struct {
|
|||
// CreateNamespace creates a new Namespace. Returns error if could not be created
|
||||
// or another namespace already exists.
|
||||
func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
|
||||
err := CheckForFQDNRules(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namespace := Namespace{}
|
||||
if err := h.db.Where("name = ?", name).First(&namespace).Error; err == nil {
|
||||
return nil, errNamespaceExists
|
||||
|
@ -84,10 +99,15 @@ func (h *Headscale) DestroyNamespace(name string) error {
|
|||
// RenameNamespace renames a Namespace. Returns error if the Namespace does
|
||||
// not exist or if another Namespace exists with the new name.
|
||||
func (h *Headscale) RenameNamespace(oldName, newName string) error {
|
||||
var err error
|
||||
oldNamespace, err := h.GetNamespace(oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CheckForFQDNRules(newName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = h.GetNamespace(newName)
|
||||
if err == nil {
|
||||
return errNamespaceExists
|
||||
|
@ -130,6 +150,10 @@ func (h *Headscale) ListNamespaces() ([]Namespace, error) {
|
|||
|
||||
// ListMachinesInNamespace gets all the nodes in a given namespace.
|
||||
func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
|
||||
err := CheckForFQDNRules(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namespace, err := h.GetNamespace(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -143,33 +167,12 @@ func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
|
|||
return machines, nil
|
||||
}
|
||||
|
||||
// ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace.
|
||||
func (h *Headscale) ListSharedMachinesInNamespace(name string) ([]Machine, error) {
|
||||
namespace, err := h.GetNamespace(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sharedMachines := []SharedMachine{}
|
||||
if err := h.db.Preload("Namespace").Where(&SharedMachine{NamespaceID: namespace.ID}).Find(&sharedMachines).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
machines := []Machine{}
|
||||
for _, sharedMachine := range sharedMachines {
|
||||
machine, err := h.GetMachineByID(
|
||||
sharedMachine.MachineID,
|
||||
) // otherwise not everything comes filled
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
machines = append(machines, *machine)
|
||||
}
|
||||
|
||||
return machines, nil
|
||||
}
|
||||
|
||||
// SetMachineNamespace assigns a Machine to a namespace.
|
||||
func (h *Headscale) SetMachineNamespace(machine *Machine, namespaceName string) error {
|
||||
err := CheckForFQDNRules(namespaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namespace, err := h.GetNamespace(namespaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -233,3 +236,55 @@ func (n *Namespace) toProto() *v1.Namespace {
|
|||
CreatedAt: timestamppb.New(n.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeToFQDNRules will replace forbidden chars in namespace
|
||||
// it can also return an error if the namespace doesn't respect RFC 952 and 1123.
|
||||
func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
|
||||
name = strings.ToLower(name)
|
||||
name = strings.ReplaceAll(name, "'", "")
|
||||
atIdx := strings.Index(name, "@")
|
||||
if stripEmailDomain && atIdx > 0 {
|
||||
name = name[:atIdx]
|
||||
} else {
|
||||
name = strings.ReplaceAll(name, "@", ".")
|
||||
}
|
||||
name = invalidCharsInNamespaceRegex.ReplaceAllString(name, "-")
|
||||
|
||||
for _, elt := range strings.Split(name, ".") {
|
||||
if len(elt) > labelHostnameLength {
|
||||
return "", fmt.Errorf(
|
||||
"label %v is more than 63 chars: %w",
|
||||
elt,
|
||||
errInvalidNamespaceName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func CheckForFQDNRules(name string) error {
|
||||
if len(name) > labelHostnameLength {
|
||||
return fmt.Errorf(
|
||||
"Namespace must not be over 63 chars. %v doesn't comply with this rule: %w",
|
||||
name,
|
||||
errInvalidNamespaceName,
|
||||
)
|
||||
}
|
||||
if strings.ToLower(name) != name {
|
||||
return fmt.Errorf(
|
||||
"Namespace name should be lowercase. %v doesn't comply with this rule: %w",
|
||||
name,
|
||||
errInvalidNamespaceName,
|
||||
)
|
||||
}
|
||||
if invalidCharsInNamespaceRegex.MatchString(name) {
|
||||
return fmt.Errorf(
|
||||
"Namespace name should only be composed of lowercase ASCII letters numbers, hyphen and dots. %v doesn't comply with theses rules: %w",
|
||||
name,
|
||||
errInvalidNamespaceName,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package headscale
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
"gorm.io/gorm"
|
||||
"inet.af/netaddr"
|
||||
|
@ -53,7 +54,6 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
|
@ -72,23 +72,23 @@ func (s *Suite) TestRenameNamespace(c *check.C) {
|
|||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(namespaces), check.Equals, 1)
|
||||
|
||||
err = app.RenameNamespace("test", "test_renamed")
|
||||
err = app.RenameNamespace("test", "test-renamed")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = app.GetNamespace("test")
|
||||
c.Assert(err, check.Equals, errNamespaceNotFound)
|
||||
|
||||
_, err = app.GetNamespace("test_renamed")
|
||||
_, err = app.GetNamespace("test-renamed")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
err = app.RenameNamespace("test_does_not_exit", "test")
|
||||
err = app.RenameNamespace("test-does-not-exit", "test")
|
||||
c.Assert(err, check.Equals, errNamespaceNotFound)
|
||||
|
||||
namespaceTest2, err := app.CreateNamespace("test2")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(namespaceTest2.Name, check.Equals, "test2")
|
||||
|
||||
err = app.RenameNamespace("test2", "test_renamed")
|
||||
err = app.RenameNamespace("test2", "test-renamed")
|
||||
c.Assert(err, check.Equals, errNamespaceExists)
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
|
|||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: namespaceShared1.ID,
|
||||
Namespace: *namespaceShared1,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
|
||||
AuthKeyID: uint(preAuthKeyShared1.ID),
|
||||
|
@ -163,7 +162,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
|
|||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: namespaceShared2.ID,
|
||||
Namespace: *namespaceShared2,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
|
||||
AuthKeyID: uint(preAuthKeyShared2.ID),
|
||||
|
@ -181,7 +179,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
|
|||
Name: "test_get_shared_nodes_3",
|
||||
NamespaceID: namespaceShared3.ID,
|
||||
Namespace: *namespaceShared3,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
|
||||
AuthKeyID: uint(preAuthKeyShared3.ID),
|
||||
|
@ -199,15 +196,12 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
|
|||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: namespaceShared1.ID,
|
||||
Namespace: *namespaceShared1,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
|
||||
AuthKeyID: uint(preAuthKey2Shared1.ID),
|
||||
}
|
||||
app.db.Save(machine2InShared1)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machineInShared2, namespaceShared1)
|
||||
c.Assert(err, check.IsNil)
|
||||
peersOfMachine1InShared1, err := app.getPeers(machineInShared1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
|
@ -216,8 +210,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
|
|||
peersOfMachine1InShared1,
|
||||
)
|
||||
|
||||
log.Trace().Msgf("userProfiles %#v", userProfiles)
|
||||
c.Assert(len(userProfiles), check.Equals, 2)
|
||||
c.Assert(len(userProfiles), check.Equals, 3)
|
||||
|
||||
found := false
|
||||
for _, userProfiles := range userProfiles {
|
||||
|
@ -239,3 +232,143 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
|
|||
}
|
||||
c.Assert(found, check.Equals, true)
|
||||
}
|
||||
|
||||
func TestNormalizeToFQDNRules(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
stripEmailDomain bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "normalize simple name",
|
||||
args: args{
|
||||
name: "normalize-simple.name",
|
||||
stripEmailDomain: false,
|
||||
},
|
||||
want: "normalize-simple.name",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "normalize an email",
|
||||
args: args{
|
||||
name: "foo.bar@example.com",
|
||||
stripEmailDomain: false,
|
||||
},
|
||||
want: "foo.bar.example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "normalize an email domain should be removed",
|
||||
args: args{
|
||||
name: "foo.bar@example.com",
|
||||
stripEmailDomain: true,
|
||||
},
|
||||
want: "foo.bar",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "strip enabled no email passed as argument",
|
||||
args: args{
|
||||
name: "not-email-and-strip-enabled",
|
||||
stripEmailDomain: true,
|
||||
},
|
||||
want: "not-email-and-strip-enabled",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "normalize complex email",
|
||||
args: args{
|
||||
name: "foo.bar+complex-email@example.com",
|
||||
stripEmailDomain: false,
|
||||
},
|
||||
want: "foo.bar-complex-email.example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "namespace name with space",
|
||||
args: args{
|
||||
name: "name space",
|
||||
stripEmailDomain: false,
|
||||
},
|
||||
want: "name-space",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "namespace with quote",
|
||||
args: args{
|
||||
name: "Jamie's iPhone 5",
|
||||
stripEmailDomain: false,
|
||||
},
|
||||
want: "jamies-iphone-5",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NormalizeToFQDNRules(tt.args.name, tt.args.stripEmailDomain)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf(
|
||||
"NormalizeToFQDNRules() error = %v, wantErr %v",
|
||||
err,
|
||||
tt.wantErr,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("NormalizeToFQDNRules() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForFQDNRules(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid: namespace",
|
||||
args: args{name: "valid-namespace"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid: capitalized namespace",
|
||||
args: args{name: "Invalid-CapItaLIzed-namespace"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid: email as namespace",
|
||||
args: args{name: "foo.bar@example.com"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid: chars in namespace name",
|
||||
args: args{name: "super-namespace+name"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid: too long name for namespace",
|
||||
args: args{
|
||||
name: "super-long-namespace-name-that-should-be-a-little-more-than-63-chars",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := CheckForFQDNRules(tt.args.name); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CheckForFQDNRules() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
231
oidc.go
231
oidc.go
|
@ -9,23 +9,17 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
const (
|
||||
oidcStateCacheExpiration = time.Minute * 5
|
||||
oidcStateCacheCleanupInterval = time.Minute * 10
|
||||
randomByteSize = 16
|
||||
randomByteSize = 16
|
||||
)
|
||||
|
||||
type IDTokenClaims struct {
|
||||
|
@ -62,14 +56,6 @@ func (h *Headscale) initOIDC() error {
|
|||
}
|
||||
}
|
||||
|
||||
// init the state cache if it hasn't been already
|
||||
if h.oidcStateCache == nil {
|
||||
h.oidcStateCache = cache.New(
|
||||
oidcStateCacheExpiration,
|
||||
oidcStateCacheCleanupInterval,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -102,7 +88,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
|
|||
stateStr := hex.EncodeToString(randomBlob)[:32]
|
||||
|
||||
// place the machine key into the state cache, so it can be retrieved later
|
||||
h.oidcStateCache.Set(stateStr, machineKeyStr, oidcStateCacheExpiration)
|
||||
h.registrationCache.Set(stateStr, machineKeyStr, registerCacheExpiration)
|
||||
|
||||
authURL := h.oauth2Config.AuthCodeURL(stateStr)
|
||||
log.Debug().Msgf("Redirecting to %s for authentication", authURL)
|
||||
|
@ -126,7 +112,6 @@ var oidcCallbackTemplate = template.Must(
|
|||
</html>`),
|
||||
)
|
||||
|
||||
// TODO: Why is the entire machine registration logic duplicated here?
|
||||
// OIDCCallback handles the callback from the OIDC endpoint
|
||||
// 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
|
||||
|
@ -198,7 +183,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
// retrieve machinekey from state cache
|
||||
machineKeyIf, machineKeyFound := h.oidcStateCache.Get(state)
|
||||
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
|
||||
|
||||
if !machineKeyFound {
|
||||
log.Error().
|
||||
|
@ -208,10 +193,12 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
machineKeyStr, machineKeyOK := machineKeyIf.(string)
|
||||
machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
|
||||
|
||||
var machineKey key.MachinePublic
|
||||
err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
|
||||
err = machineKey.UnmarshalText(
|
||||
[]byte(MachinePublicKeyEnsurePrefix(machineKeyFromCache)),
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Msg("could not parse machine public key")
|
||||
|
@ -230,33 +217,19 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO(kradalby): Currently, if it fails to find a requested expiry, non will be set
|
||||
requestedTime := time.Time{}
|
||||
if requestedTimeIf, found := h.requestedExpiryCache.Get(machineKey.String()); found {
|
||||
if reqTime, ok := requestedTimeIf.(time.Time); ok {
|
||||
requestedTime = reqTime
|
||||
}
|
||||
}
|
||||
// retrieve machine information if it exist
|
||||
// The error is not important, because if it does not
|
||||
// exist, then this is a new machine and we will move
|
||||
// on to registration.
|
||||
machine, _ := h.GetMachineByMachineKey(machineKey)
|
||||
|
||||
// retrieve machine information
|
||||
machine, err := h.GetMachineByMachineKey(machineKey)
|
||||
if err != nil {
|
||||
log.Error().Msg("machine key not found in database")
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not get machine info from database",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if machine.isRegistered() {
|
||||
if machine != nil {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("machine already registered, reauthenticating")
|
||||
|
||||
h.RefreshMachine(machine, requestedTime)
|
||||
h.RefreshMachine(machine, *machine.Expiry)
|
||||
|
||||
var content bytes.Buffer
|
||||
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
|
||||
|
@ -280,111 +253,89 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
if namespaceName, ok := h.getNamespaceFromEmail(claims.Email); ok {
|
||||
// register the machine if it's new
|
||||
if !machine.Registered {
|
||||
log.Debug().Msg("Registering new machine after successful callback")
|
||||
|
||||
namespace, err := h.GetNamespace(namespaceName)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
namespace, err = h.CreateNamespace(namespaceName)
|
||||
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Caller().
|
||||
Msgf("could not create new namespace '%s'", namespaceName)
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not create new namespace",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Str("namespace", namespaceName).
|
||||
Msg("could not find or create namespace")
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not find or create namespace",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ips, err := h.getAvailableIPs()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("could not get an IP from the pool")
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not get an IP from the pool",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
machine.IPAddresses = ips
|
||||
machine.NamespaceID = namespace.ID
|
||||
machine.Registered = true
|
||||
machine.RegisterMethod = RegisterMethodOIDC
|
||||
machine.LastSuccessfulUpdate = &now
|
||||
machine.Expiry = &requestedTime
|
||||
h.db.Save(&machine)
|
||||
}
|
||||
|
||||
var content bytes.Buffer
|
||||
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
|
||||
User: claims.Email,
|
||||
Verb: "Authenticated",
|
||||
}); err != nil {
|
||||
log.Error().
|
||||
Str("func", "OIDCCallback").
|
||||
Str("type", "authenticate").
|
||||
Err(err).
|
||||
Msg("Could not render OIDC callback template")
|
||||
ctx.Data(
|
||||
http.StatusInternalServerError,
|
||||
"text/html; charset=utf-8",
|
||||
[]byte("Could not render OIDC callback template"),
|
||||
)
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
|
||||
namespaceName, err := NormalizeToFQDNRules(
|
||||
claims.Email,
|
||||
h.cfg.OIDC.StripEmaildomain,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Msgf("couldn't normalize email")
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"couldn't normalize email",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("email", claims.Email).
|
||||
Str("username", claims.Username).
|
||||
Str("machine", machine.Name).
|
||||
Msg("Email could not be mapped to a namespace")
|
||||
ctx.String(
|
||||
http.StatusBadRequest,
|
||||
"email from claim could not be mapped to a namespace",
|
||||
)
|
||||
}
|
||||
// register the machine if it's new
|
||||
log.Debug().Msg("Registering new machine after successful callback")
|
||||
|
||||
// getNamespaceFromEmail passes the users email through a list of "matchers"
|
||||
// and iterates through them until it matches and returns a namespace.
|
||||
// If no match is found, an empty string will be returned.
|
||||
// TODO(kradalby): golang Maps key order is not stable, so this list is _not_ deterministic. Find a way to make the list of keys stable, preferably in the order presented in a users configuration.
|
||||
func (h *Headscale) getNamespaceFromEmail(email string) (string, bool) {
|
||||
for match, namespace := range h.cfg.OIDC.MatchMap {
|
||||
regex := regexp.MustCompile(match)
|
||||
if regex.MatchString(email) {
|
||||
return namespace, true
|
||||
namespace, err := h.GetNamespace(namespaceName)
|
||||
if errors.Is(err, errNamespaceNotFound) {
|
||||
namespace, err = h.CreateNamespace(namespaceName)
|
||||
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Caller().
|
||||
Msgf("could not create new namespace '%s'", namespaceName)
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not create new namespace",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Str("namespace", namespaceName).
|
||||
Msg("could not find or create namespace")
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not find or create namespace",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return "", false
|
||||
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
|
||||
|
||||
_, err = h.RegisterMachineFromAuthCallback(
|
||||
machineKeyStr,
|
||||
namespace.Name,
|
||||
RegisterMethodOIDC,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("could not register machine")
|
||||
ctx.String(
|
||||
http.StatusInternalServerError,
|
||||
"could not register machine",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var content bytes.Buffer
|
||||
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
|
||||
User: claims.Email,
|
||||
Verb: "Authenticated",
|
||||
}); err != nil {
|
||||
log.Error().
|
||||
Str("func", "OIDCCallback").
|
||||
Str("type", "authenticate").
|
||||
Err(err).
|
||||
Msg("Could not render OIDC callback template")
|
||||
ctx.Data(
|
||||
http.StatusInternalServerError,
|
||||
"text/html; charset=utf-8",
|
||||
[]byte("Could not render OIDC callback template"),
|
||||
)
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
|
||||
}
|
||||
|
|
180
oidc_test.go
180
oidc_test.go
|
@ -1,180 +0,0 @@
|
|||
package headscale
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestHeadscale_getNamespaceFromEmail(t *testing.T) {
|
||||
type fields struct {
|
||||
cfg Config
|
||||
db *gorm.DB
|
||||
dbString string
|
||||
dbType string
|
||||
dbDebug bool
|
||||
privateKey *key.MachinePrivate
|
||||
aclPolicy *ACLPolicy
|
||||
aclRules []tailcfg.FilterRule
|
||||
lastStateChange sync.Map
|
||||
oidcProvider *oidc.Provider
|
||||
oauth2Config *oauth2.Config
|
||||
oidcStateCache *cache.Cache
|
||||
}
|
||||
type args struct {
|
||||
email string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want string
|
||||
want1 bool
|
||||
}{
|
||||
{
|
||||
name: "match all",
|
||||
fields: fields{
|
||||
cfg: Config{
|
||||
OIDC: OIDCConfig{
|
||||
MatchMap: map[string]string{
|
||||
".*": "space",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
email: "test@example.no",
|
||||
},
|
||||
want: "space",
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "match user",
|
||||
fields: fields{
|
||||
cfg: Config{
|
||||
OIDC: OIDCConfig{
|
||||
MatchMap: map[string]string{
|
||||
"specific@user\\.no": "user-namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
email: "specific@user.no",
|
||||
},
|
||||
want: "user-namespace",
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "match domain",
|
||||
fields: fields{
|
||||
cfg: Config{
|
||||
OIDC: OIDCConfig{
|
||||
MatchMap: map[string]string{
|
||||
".*@example\\.no": "example",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
email: "test@example.no",
|
||||
},
|
||||
want: "example",
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "multi match domain",
|
||||
fields: fields{
|
||||
cfg: Config{
|
||||
OIDC: OIDCConfig{
|
||||
MatchMap: map[string]string{
|
||||
".*@example\\.no": "exammple",
|
||||
".*@gmail\\.com": "gmail",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
email: "someuser@gmail.com",
|
||||
},
|
||||
want: "gmail",
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "no match domain",
|
||||
fields: fields{
|
||||
cfg: Config{
|
||||
OIDC: OIDCConfig{
|
||||
MatchMap: map[string]string{
|
||||
".*@dontknow.no": "never",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
email: "test@wedontknow.no",
|
||||
},
|
||||
want: "",
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "multi no match domain",
|
||||
fields: fields{
|
||||
cfg: Config{
|
||||
OIDC: OIDCConfig{
|
||||
MatchMap: map[string]string{
|
||||
".*@dontknow.no": "never",
|
||||
".*@wedontknow.no": "other",
|
||||
".*\\.no": "stuffy",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
email: "tasy@nonofthem.com",
|
||||
},
|
||||
want: "",
|
||||
want1: false,
|
||||
},
|
||||
}
|
||||
//nolint
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
app := &Headscale{
|
||||
cfg: test.fields.cfg,
|
||||
db: test.fields.db,
|
||||
dbString: test.fields.dbString,
|
||||
dbType: test.fields.dbType,
|
||||
dbDebug: test.fields.dbDebug,
|
||||
privateKey: test.fields.privateKey,
|
||||
aclPolicy: test.fields.aclPolicy,
|
||||
aclRules: test.fields.aclRules,
|
||||
lastStateChange: test.fields.lastStateChange,
|
||||
oidcProvider: test.fields.oidcProvider,
|
||||
oauth2Config: test.fields.oauth2Config,
|
||||
oidcStateCache: test.fields.oidcStateCache,
|
||||
}
|
||||
got, got1 := app.getNamespaceFromEmail(test.args.email)
|
||||
if got != test.want {
|
||||
t.Errorf(
|
||||
"Headscale.getNamespaceFromEmail() got = %v, want %v",
|
||||
got,
|
||||
test.want,
|
||||
)
|
||||
}
|
||||
if got1 != test.want1 {
|
||||
t.Errorf(
|
||||
"Headscale.getNamespaceFromEmail() got1 = %v, want %v",
|
||||
got1,
|
||||
test.want1,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -4,32 +4,125 @@ import (
|
|||
"bytes"
|
||||
"html/template"
|
||||
"net/http"
|
||||
textTemplate "text/template"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// AppleMobileConfig shows a simple message in the browser to point to the CLI
|
||||
// Listens in /register.
|
||||
func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
|
||||
// WindowsConfigMessage shows a simple message in the browser for how to configure the Windows Tailscale client.
|
||||
func (h *Headscale) WindowsConfigMessage(ctx *gin.Context) {
|
||||
winTemplate := template.Must(template.New("windows").Parse(`
|
||||
<html>
|
||||
<body>
|
||||
<h1>headscale</h1>
|
||||
<h2>Windows registry configuration</h2>
|
||||
<p>
|
||||
This page provides Windows registry information for the official Windows Tailscale client.
|
||||
<p>
|
||||
<p>
|
||||
The registry file will configure Tailscale to use <code>{{.URL}}</code> as its control server.
|
||||
<p>
|
||||
<h3>Caution</h3>
|
||||
<p>You should always download and inspect the registry file before installing it:</p>
|
||||
<pre><code>curl {{.URL}}/windows/tailscale.reg</code></pre>
|
||||
|
||||
<h2>Installation</h2>
|
||||
<p>Headscale can be set to the default server by running the registry file:</p>
|
||||
|
||||
<p>
|
||||
<a href="/windows/tailscale.reg" download="tailscale.reg">Windows registry file</a>
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
<li>Download the registry file, then run it</li>
|
||||
<li>Follow the prompts</li>
|
||||
<li>Install and run the official windows Tailscale client</li>
|
||||
<li>When the installation has finished, start Tailscale, and log in by clicking the icon in the system tray</li>
|
||||
</ol>
|
||||
<p>Or</p>
|
||||
<p>Open command prompt with Administrator rights. Issue the following commands to add the required registry entries:</p>
|
||||
<pre>
|
||||
<code>REG ADD "HKLM\Software\Tailscale IPN" /v UnattendedMode /t REG_SZ /d always
|
||||
REG ADD "HKLM\Software\Tailscale IPN" /v LoginURL /t REG_SZ /d "{{.URL}}"</code></pre>
|
||||
<p>
|
||||
Restart Tailscale and log in.
|
||||
<p>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
config := map[string]interface{}{
|
||||
"URL": h.cfg.ServerURL,
|
||||
}
|
||||
|
||||
var payload bytes.Buffer
|
||||
if err := winTemplate.Execute(&payload, config); err != nil {
|
||||
log.Error().
|
||||
Str("handler", "WindowsRegConfig").
|
||||
Err(err).
|
||||
Msg("Could not render Windows index template")
|
||||
ctx.Data(
|
||||
http.StatusInternalServerError,
|
||||
"text/html; charset=utf-8",
|
||||
[]byte("Could not render Windows index template"),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes())
|
||||
}
|
||||
|
||||
// WindowsRegConfig generates and serves a .reg file configured with the Headscale server address.
|
||||
func (h *Headscale) WindowsRegConfig(ctx *gin.Context) {
|
||||
config := WindowsRegistryConfig{
|
||||
URL: h.cfg.ServerURL,
|
||||
}
|
||||
|
||||
var content bytes.Buffer
|
||||
if err := windowsRegTemplate.Execute(&content, config); err != nil {
|
||||
log.Error().
|
||||
Str("handler", "WindowsRegConfig").
|
||||
Err(err).
|
||||
Msg("Could not render Apple macOS template")
|
||||
ctx.Data(
|
||||
http.StatusInternalServerError,
|
||||
"text/html; charset=utf-8",
|
||||
[]byte("Could not render Windows registry template"),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(
|
||||
http.StatusOK,
|
||||
"text/x-ms-regedit; charset=utf-8",
|
||||
content.Bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
// AppleConfigMessage shows a simple message in the browser to point the user to the iOS/MacOS profile and instructions for how to install it.
|
||||
func (h *Headscale) AppleConfigMessage(ctx *gin.Context) {
|
||||
appleTemplate := template.Must(template.New("apple").Parse(`
|
||||
<html>
|
||||
<body>
|
||||
<h1>Apple configuration profiles</h1>
|
||||
<h1>headscale</h1>
|
||||
<h2>Apple configuration profiles</h2>
|
||||
<p>
|
||||
This page provides <a href="https://support.apple.com/guide/mdm/mdm-overview-mdmbf9e668/web">configuration profiles</a> for the official Tailscale clients for <a href="https://apps.apple.com/us/app/tailscale/id1470499037?ls=1">iOS</a> and <a href="https://apps.apple.com/ca/app/tailscale/id1475387142?mt=12">macOS</a>.
|
||||
</p>
|
||||
<p>
|
||||
The profiles will configure Tailscale.app to use {{.Url}} as its control server.
|
||||
The profiles will configure Tailscale.app to use <code>{{.URL}}</code> as its control server.
|
||||
</p>
|
||||
|
||||
<h3>Caution</h3>
|
||||
<p>You should always inspect the profile before installing it:</p>
|
||||
<p>You should always download and inspect the profile before installing it:</p>
|
||||
<!--
|
||||
<p><code>curl {{.Url}}/apple/ios</code></p>
|
||||
<pre><code>curl {{.URL}}/apple/ios</code></pre>
|
||||
-->
|
||||
<p><code>curl {{.Url}}/apple/macos</code></p>
|
||||
<pre><code>curl {{.URL}}/apple/macos</code></pre>
|
||||
|
||||
<h2>Profiles</h2>
|
||||
|
||||
|
@ -191,6 +284,10 @@ func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
|
|||
)
|
||||
}
|
||||
|
||||
type WindowsRegistryConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type AppleMobileConfig struct {
|
||||
UUID uuid.UUID
|
||||
URL string
|
||||
|
@ -202,8 +299,16 @@ type AppleMobilePlatformConfig struct {
|
|||
URL string
|
||||
}
|
||||
|
||||
var commonTemplate = template.Must(
|
||||
template.New("mobileconfig").Parse(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
var windowsRegTemplate = textTemplate.Must(
|
||||
textTemplate.New("windowsconfig").Parse(`Windows Registry Editor Version 5.00
|
||||
|
||||
[HKEY_LOCAL_MACHINE\SOFTWARE\Tailscale IPN]
|
||||
"UnattendedMode"="always"
|
||||
"LoginURL"="{{.URL}}"
|
||||
`))
|
||||
|
||||
var commonTemplate = textTemplate.Must(
|
||||
textTemplate.New("mobileconfig").Parse(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
|
@ -229,7 +334,7 @@ var commonTemplate = template.Must(
|
|||
</plist>`),
|
||||
)
|
||||
|
||||
var iosTemplate = template.Must(template.New("iosTemplate").Parse(`
|
||||
var iosTemplate = textTemplate.Must(textTemplate.New("iosTemplate").Parse(`
|
||||
<dict>
|
||||
<key>PayloadType</key>
|
||||
<string>io.tailscale.ipn.ios</string>
|
32
poll.go
32
poll.go
|
@ -2,7 +2,6 @@ package headscale
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -11,7 +10,6 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
|
@ -85,12 +83,33 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
|||
Str("machine", machine.Name).
|
||||
Msg("Found machine in database")
|
||||
|
||||
hostinfo, _ := json.Marshal(req.Hostinfo)
|
||||
machine.Name = req.Hostinfo.Hostname
|
||||
machine.HostInfo = datatypes.JSON(hostinfo)
|
||||
hname, err := NormalizeToFQDNRules(
|
||||
req.Hostinfo.Hostname,
|
||||
h.cfg.OIDC.StripEmaildomain,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("hostinfo.name", req.Hostinfo.Hostname).
|
||||
Err(err)
|
||||
}
|
||||
machine.Name = hname
|
||||
machine.HostInfo = HostInfo(*req.Hostinfo)
|
||||
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
|
||||
now := time.Now().UTC()
|
||||
|
||||
// update ACLRules with peer informations (to update server tags if necessary)
|
||||
if h.aclPolicy != nil {
|
||||
err = h.UpdateACLRules()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
// From Tailscale client:
|
||||
//
|
||||
// ReadOnly is whether the client just wants to fetch the MapResponse,
|
||||
|
@ -100,8 +119,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
|||
// The intended use is for clients to discover the DERP map at start-up
|
||||
// before their first real endpoint update.
|
||||
if !req.ReadOnly {
|
||||
endpoints, _ := json.Marshal(req.Endpoints)
|
||||
machine.Endpoints = datatypes.JSON(endpoints)
|
||||
machine.Endpoints = req.Endpoints
|
||||
machine.LastSeen = &now
|
||||
}
|
||||
h.db.Updates(machine)
|
||||
|
|
|
@ -113,6 +113,12 @@ func (h *Headscale) ExpirePreAuthKey(k *PreAuthKey) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UsePreAuthKey marks a PreAuthKey as used.
|
||||
func (h *Headscale) UsePreAuthKey(k *PreAuthKey) {
|
||||
k.Used = true
|
||||
h.db.Save(k)
|
||||
}
|
||||
|
||||
// checkKeyValidity does the heavy lifting for validation of the PreAuthKey coming from a node
|
||||
// If returns no error and a PreAuthKey, it can be used.
|
||||
func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
|
||||
|
|
|
@ -80,7 +80,6 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testest",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
|
@ -105,7 +104,6 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testest",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
|
@ -143,7 +141,6 @@ func (*Suite) TestEphemeralKey(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testest",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
LastSeen: &now,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
|
|
|
@ -104,18 +104,6 @@ service HeadscaleService {
|
|||
get: "/api/v1/machine"
|
||||
};
|
||||
}
|
||||
|
||||
rpc ShareMachine(ShareMachineRequest) returns (ShareMachineResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/machine/{machine_id}/share/{namespace}"
|
||||
};
|
||||
}
|
||||
|
||||
rpc UnshareMachine(UnshareMachineRequest) returns (UnshareMachineResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/machine/{machine_id}/unshare/{namespace}"
|
||||
};
|
||||
}
|
||||
// --- Machine end ---
|
||||
|
||||
// --- Route start ---
|
||||
|
|
|
@ -22,16 +22,16 @@ message Machine {
|
|||
string name = 6;
|
||||
Namespace namespace = 7;
|
||||
|
||||
bool registered = 8;
|
||||
RegisterMethod register_method = 9;
|
||||
|
||||
google.protobuf.Timestamp last_seen = 10;
|
||||
google.protobuf.Timestamp last_successful_update = 11;
|
||||
google.protobuf.Timestamp expiry = 12;
|
||||
google.protobuf.Timestamp last_seen = 8;
|
||||
google.protobuf.Timestamp last_successful_update = 9;
|
||||
google.protobuf.Timestamp expiry = 10;
|
||||
|
||||
PreAuthKey pre_auth_key = 13;
|
||||
PreAuthKey pre_auth_key = 11;
|
||||
|
||||
google.protobuf.Timestamp created_at = 14;
|
||||
google.protobuf.Timestamp created_at = 12;
|
||||
|
||||
RegisterMethod register_method = 13;
|
||||
// google.protobuf.Timestamp updated_at = 14;
|
||||
// google.protobuf.Timestamp deleted_at = 15;
|
||||
|
||||
|
@ -80,24 +80,6 @@ message ListMachinesResponse {
|
|||
repeated Machine machines = 1;
|
||||
}
|
||||
|
||||
message ShareMachineRequest {
|
||||
uint64 machine_id = 1;
|
||||
string namespace = 2;
|
||||
}
|
||||
|
||||
message ShareMachineResponse {
|
||||
Machine machine = 1;
|
||||
}
|
||||
|
||||
message UnshareMachineRequest {
|
||||
uint64 machine_id = 1;
|
||||
string namespace = 2;
|
||||
}
|
||||
|
||||
message UnshareMachineResponse {
|
||||
Machine machine = 1;
|
||||
}
|
||||
|
||||
message DebugCreateMachineRequest {
|
||||
string namespace = 1;
|
||||
string key = 2;
|
||||
|
|
39
routes.go
39
routes.go
|
@ -1,9 +1,6 @@
|
|||
package headscale
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
|
@ -23,12 +20,7 @@ func (h *Headscale) GetAdvertisedNodeRoutes(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
hostInfo, err := machine.GetHostInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &hostInfo.RoutableIPs, nil
|
||||
return &machine.HostInfo.RoutableIPs, nil
|
||||
}
|
||||
|
||||
// Deprecated: use machine function instead
|
||||
|
@ -43,27 +35,7 @@ func (h *Headscale) GetEnabledNodeRoutes(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
data, err := machine.EnabledRoutes.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routesStr := []string{}
|
||||
err = json.Unmarshal(data, &routesStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes := make([]netaddr.IPPrefix, len(routesStr))
|
||||
for index, routeStr := range routesStr {
|
||||
route, err := netaddr.ParseIPPrefix(routeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes[index] = route
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
return machine.EnabledRoutes, nil
|
||||
}
|
||||
|
||||
// Deprecated: use machine function instead
|
||||
|
@ -135,12 +107,7 @@ func (h *Headscale) EnableNodeRoute(
|
|||
return errRouteIsNotAvailable
|
||||
}
|
||||
|
||||
routes, err := json.Marshal(enabledRoutes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
machine.EnabledRoutes = datatypes.JSON(routes)
|
||||
machine.EnabledRoutes = enabledRoutes
|
||||
h.db.Save(&machine)
|
||||
|
||||
return nil
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package headscale
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
"gorm.io/datatypes"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
@ -25,8 +22,6 @@ func (s *Suite) TestGetRoutes(c *check.C) {
|
|||
hostInfo := tailcfg.Hostinfo{
|
||||
RoutableIPs: []netaddr.IPPrefix{route},
|
||||
}
|
||||
hostinfo, err := json.Marshal(hostInfo)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
machine := Machine{
|
||||
ID: 0,
|
||||
|
@ -35,10 +30,9 @@ func (s *Suite) TestGetRoutes(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "test_get_route_machine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
HostInfo: datatypes.JSON(hostinfo),
|
||||
HostInfo: HostInfo(hostInfo),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
|
||||
|
@ -79,8 +73,6 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
|
|||
hostInfo := tailcfg.Hostinfo{
|
||||
RoutableIPs: []netaddr.IPPrefix{route, route2},
|
||||
}
|
||||
hostinfo, err := json.Marshal(hostInfo)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
machine := Machine{
|
||||
ID: 0,
|
||||
|
@ -89,10 +81,9 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "test_enable_route_machine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
HostInfo: datatypes.JSON(hostinfo),
|
||||
HostInfo: HostInfo(hostInfo),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
|
||||
|
|
81
sharing.go
81
sharing.go
|
@ -1,81 +0,0 @@
|
|||
package headscale
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
const (
|
||||
errSameNamespace = Error("Destination namespace same as origin")
|
||||
errMachineAlreadyShared = Error("Node already shared to this namespace")
|
||||
errMachineNotShared = Error("Machine not shared to this namespace")
|
||||
)
|
||||
|
||||
// SharedMachine is a join table to support sharing nodes between namespaces.
|
||||
type SharedMachine struct {
|
||||
gorm.Model
|
||||
MachineID uint64
|
||||
Machine Machine
|
||||
NamespaceID uint
|
||||
Namespace Namespace
|
||||
}
|
||||
|
||||
// AddSharedMachineToNamespace adds a machine as a shared node to a namespace.
|
||||
func (h *Headscale) AddSharedMachineToNamespace(
|
||||
machine *Machine,
|
||||
namespace *Namespace,
|
||||
) error {
|
||||
if machine.NamespaceID == namespace.ID {
|
||||
return errSameNamespace
|
||||
}
|
||||
|
||||
sharedMachines := []SharedMachine{}
|
||||
if err := h.db.Where("machine_id = ? AND namespace_id = ?", machine.ID, namespace.ID).Find(&sharedMachines).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(sharedMachines) > 0 {
|
||||
return errMachineAlreadyShared
|
||||
}
|
||||
|
||||
sharedMachine := SharedMachine{
|
||||
MachineID: machine.ID,
|
||||
Machine: *machine,
|
||||
NamespaceID: namespace.ID,
|
||||
Namespace: *namespace,
|
||||
}
|
||||
h.db.Save(&sharedMachine)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSharedMachineFromNamespace removes a shared machine from a namespace.
|
||||
func (h *Headscale) RemoveSharedMachineFromNamespace(
|
||||
machine *Machine,
|
||||
namespace *Namespace,
|
||||
) error {
|
||||
if machine.NamespaceID == namespace.ID {
|
||||
// Can't unshare from primary namespace
|
||||
return errMachineNotShared
|
||||
}
|
||||
|
||||
sharedMachine := SharedMachine{}
|
||||
result := h.db.Where("machine_id = ? AND namespace_id = ?", machine.ID, namespace.ID).
|
||||
Unscoped().
|
||||
Delete(&sharedMachine)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return errMachineNotShared
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSharedMachineFromAllNamespaces removes a machine as a shared node from all namespaces.
|
||||
func (h *Headscale) RemoveSharedMachineFromAllNamespaces(machine *Machine) error {
|
||||
sharedMachine := SharedMachine{}
|
||||
if result := h.db.Where("machine_id = ?", machine.ID).Unscoped().Delete(&sharedMachine); result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
341
sharing_test.go
341
sharing_test.go
|
@ -1,341 +0,0 @@
|
|||
package headscale
|
||||
|
||||
import (
|
||||
"gopkg.in/check.v1"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func CreateNodeNamespace(
|
||||
c *check.C,
|
||||
namespaceName, node, key, ip string,
|
||||
) (*Namespace, *Machine) {
|
||||
namespace, err := app.CreateNamespace(namespaceName)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak1, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = app.GetMachine(namespace.Name, node)
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
machine := &Machine{
|
||||
ID: 0,
|
||||
MachineKey: key,
|
||||
NodeKey: key,
|
||||
DiscoKey: key,
|
||||
Name: node,
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
|
||||
AuthKeyID: uint(pak1.ID),
|
||||
}
|
||||
app.db.Save(machine)
|
||||
|
||||
_, err = app.GetMachine(namespace.Name, machine.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
return namespace, machine
|
||||
}
|
||||
|
||||
func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) {
|
||||
namespace1, machine1 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared1",
|
||||
"test_get_shared_nodes_1",
|
||||
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
"100.64.0.1",
|
||||
)
|
||||
_, machine2 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared2",
|
||||
"test_get_shared_nodes_2",
|
||||
"dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
"100.64.0.2",
|
||||
)
|
||||
|
||||
peersOfMachine1BeforeShared, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1BeforeShared), check.Equals, 0)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machine2, namespace1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfMachine1AfterShared, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1AfterShared), check.Equals, 1)
|
||||
c.Assert(peersOfMachine1AfterShared[0].ID, check.Equals, machine2.ID)
|
||||
}
|
||||
|
||||
func (s *Suite) TestSameNamespace(c *check.C) {
|
||||
namespace1, machine1 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared1",
|
||||
"test_get_shared_nodes_1",
|
||||
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
"100.64.0.1",
|
||||
)
|
||||
|
||||
peersOfMachine1BeforeShare, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machine1, namespace1)
|
||||
c.Assert(err, check.Equals, errSameNamespace)
|
||||
}
|
||||
|
||||
func (s *Suite) TestUnshare(c *check.C) {
|
||||
namespace1, machine1 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared1",
|
||||
"test_unshare_1",
|
||||
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
"100.64.0.1",
|
||||
)
|
||||
_, machine2 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared2",
|
||||
"test_unshare_2",
|
||||
"dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
"100.64.0.2",
|
||||
)
|
||||
|
||||
peersOfMachine1BeforeShare, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machine2, namespace1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfMachine1BeforeShare, err = app.getShared(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 1)
|
||||
|
||||
err = app.RemoveSharedMachineFromNamespace(machine2, namespace1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfMachine1BeforeShare, err = app.getShared(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
|
||||
|
||||
err = app.RemoveSharedMachineFromNamespace(machine2, namespace1)
|
||||
c.Assert(err, check.Equals, errMachineNotShared)
|
||||
|
||||
err = app.RemoveSharedMachineFromNamespace(machine1, namespace1)
|
||||
c.Assert(err, check.Equals, errMachineNotShared)
|
||||
}
|
||||
|
||||
func (s *Suite) TestAlreadyShared(c *check.C) {
|
||||
namespace1, machine1 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared1",
|
||||
"test_get_shared_nodes_1",
|
||||
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
"100.64.0.1",
|
||||
)
|
||||
_, machine2 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared2",
|
||||
"test_get_shared_nodes_2",
|
||||
"dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
"100.64.0.2",
|
||||
)
|
||||
|
||||
peersOfMachine1BeforeShare, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machine2, namespace1)
|
||||
c.Assert(err, check.IsNil)
|
||||
err = app.AddSharedMachineToNamespace(machine2, namespace1)
|
||||
c.Assert(err, check.Equals, errMachineAlreadyShared)
|
||||
}
|
||||
|
||||
func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) {
|
||||
namespace1, machine1 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared1",
|
||||
"test_get_shared_nodes_1",
|
||||
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
"100.64.0.1",
|
||||
)
|
||||
_, machine2 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared2",
|
||||
"test_get_shared_nodes_2",
|
||||
"dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
"100.64.0.2",
|
||||
)
|
||||
|
||||
peersOfMachine1BeforeShare, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machine2, namespace1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfMachine1AfterShare, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1AfterShare), check.Equals, 1)
|
||||
c.Assert(peersOfMachine1AfterShare[0].Name, check.Equals, "test_get_shared_nodes_2")
|
||||
}
|
||||
|
||||
func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) {
|
||||
namespace1, machine1 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared1",
|
||||
"test_get_shared_nodes_1",
|
||||
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
"100.64.0.1",
|
||||
)
|
||||
_, machine2 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared2",
|
||||
"test_get_shared_nodes_2",
|
||||
"dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
"100.64.0.2",
|
||||
)
|
||||
_, machine3 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared3",
|
||||
"test_get_shared_nodes_3",
|
||||
"6e704bee83eb93db6fc2c417d7882964cd3f8cc87082cbb645982e34020c76c8",
|
||||
"100.64.0.3",
|
||||
)
|
||||
|
||||
pak4, err := app.CreatePreAuthKey(namespace1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
machine4 := &Machine{
|
||||
ID: 4,
|
||||
MachineKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
NodeKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
DiscoKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: namespace1.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
|
||||
AuthKeyID: uint(pak4.ID),
|
||||
}
|
||||
app.db.Save(machine4)
|
||||
|
||||
_, err = app.GetMachine(namespace1.Name, machine4.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfMachine1BeforeShare, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 1) // node1 can see node4
|
||||
c.Assert(peersOfMachine1BeforeShare[0].Name, check.Equals, machine4.Name)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machine2, namespace1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfMachine1AfterShare, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(
|
||||
len(peersOfMachine1AfterShare),
|
||||
check.Equals,
|
||||
2,
|
||||
) // node1 can see node2 (shared) and node4 (same namespace)
|
||||
c.Assert(peersOfMachine1AfterShare[0].Name, check.Equals, machine2.Name)
|
||||
c.Assert(peersOfMachine1AfterShare[1].Name, check.Equals, machine4.Name)
|
||||
|
||||
sharedOfMachine1, err := app.getShared(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(sharedOfMachine1), check.Equals, 1) // node1 can see node2 as shared
|
||||
c.Assert(sharedOfMachine1[0].Name, check.Equals, machine2.Name)
|
||||
|
||||
peersOfMachine3, err := app.getPeers(machine3)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine3), check.Equals, 0) // node3 is alone
|
||||
|
||||
peersOfMachine2, err := app.getPeers(machine2)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(
|
||||
len(peersOfMachine2),
|
||||
check.Equals,
|
||||
2,
|
||||
) // node2 should see node1 (sharedTo) and node4 (sharedTo), as is shared in namespace1
|
||||
c.Assert(peersOfMachine2[0].Name, check.Equals, machine1.Name)
|
||||
c.Assert(peersOfMachine2[1].Name, check.Equals, machine4.Name)
|
||||
}
|
||||
|
||||
func (s *Suite) TestDeleteSharedMachine(c *check.C) {
|
||||
namespace1, machine1 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared1",
|
||||
"test_get_shared_nodes_1",
|
||||
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
"100.64.0.1",
|
||||
)
|
||||
_, machine2 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared2",
|
||||
"test_get_shared_nodes_2",
|
||||
"dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
"100.64.0.2",
|
||||
)
|
||||
_, machine3 := CreateNodeNamespace(
|
||||
c,
|
||||
"shared3",
|
||||
"test_get_shared_nodes_3",
|
||||
"6e704bee83eb93db6fc2c417d7882964cd3f8cc87082cbb645982e34020c76c8",
|
||||
"100.64.0.3",
|
||||
)
|
||||
|
||||
pak4n1, err := app.CreatePreAuthKey(namespace1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
machine4 := &Machine{
|
||||
ID: 4,
|
||||
MachineKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
NodeKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
DiscoKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: namespace1.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
|
||||
AuthKeyID: uint(pak4n1.ID),
|
||||
}
|
||||
app.db.Save(machine4)
|
||||
|
||||
_, err = app.GetMachine(namespace1.Name, machine4.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfMachine1BeforeShare, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 1) // nodes 1 and 4
|
||||
c.Assert(peersOfMachine1BeforeShare[0].Name, check.Equals, machine4.Name)
|
||||
|
||||
err = app.AddSharedMachineToNamespace(machine2, namespace1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfMachine1AfterShare, err := app.getPeers(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine1AfterShare), check.Equals, 2) // nodes 1, 2, 4
|
||||
c.Assert(peersOfMachine1AfterShare[0].Name, check.Equals, machine2.Name)
|
||||
c.Assert(peersOfMachine1AfterShare[1].Name, check.Equals, machine4.Name)
|
||||
|
||||
sharedOfMachine1, err := app.getShared(machine1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(sharedOfMachine1), check.Equals, 1) // nodes 1, 2, 4
|
||||
c.Assert(sharedOfMachine1[0].Name, check.Equals, machine2.Name)
|
||||
|
||||
peersOfMachine3, err := app.getPeers(machine3)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(peersOfMachine3), check.Equals, 0) // node 3 is alone
|
||||
|
||||
sharedMachinesInNamespace1, err := app.ListSharedMachinesInNamespace(
|
||||
namespace1.Name,
|
||||
)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(sharedMachinesInNamespace1), check.Equals, 1)
|
||||
|
||||
err = app.DeleteMachine(machine2)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
sharedMachinesInNamespace1, err = app.ListSharedMachinesInNamespace(namespace1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(sharedMachinesInNamespace1), check.Equals, 0)
|
||||
}
|
10
tests/acls/acl_policy_basic_wildcards.yaml
Normal file
10
tests/acls/acl_policy_basic_wildcards.yaml
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
Hosts:
|
||||
host-1: 100.100.100.100/32
|
||||
subnet-1: 100.100.101.100/24
|
||||
ACLs:
|
||||
- Action: accept
|
||||
Users:
|
||||
- "*"
|
||||
Ports:
|
||||
- host-1:*
|
39
utils.go
39
utils.go
|
@ -157,9 +157,6 @@ func GetIPPrefixEndpoints(na netaddr.IPPrefix) (network, broadcast netaddr.IP) {
|
|||
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()
|
||||
if err != nil {
|
||||
|
@ -179,7 +176,7 @@ func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, erro
|
|||
switch {
|
||||
case ip.Compare(ipPrefixBroadcastAddress) == 0:
|
||||
fallthrough
|
||||
case containsIPs(usedIps, ip):
|
||||
case usedIps.Contains(ip):
|
||||
fallthrough
|
||||
case ip.IsZero() || ip.IsLoopback():
|
||||
ip = ip.Next()
|
||||
|
@ -192,29 +189,43 @@ func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, erro
|
|||
}
|
||||
}
|
||||
|
||||
func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
|
||||
func (h *Headscale) getUsedIPs() (*netaddr.IPSet, error) {
|
||||
// FIXME: This really deserves a better data model,
|
||||
// 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, 0, len(h.cfg.IPPrefixes)*len(addressesSlices))
|
||||
var ips netaddr.IPSetBuilder
|
||||
for _, slice := range addressesSlices {
|
||||
var a MachineAddresses
|
||||
err := a.Scan(slice)
|
||||
var machineAddresses MachineAddresses
|
||||
err := machineAddresses.Scan(slice)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read ip from database: %w", err)
|
||||
return &netaddr.IPSet{}, fmt.Errorf(
|
||||
"failed to read ip from database: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
for _, ip := range machineAddresses {
|
||||
ips.Add(ip)
|
||||
}
|
||||
ips = append(ips, a...)
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
ipSet, err := ips.IPSet()
|
||||
if err != nil {
|
||||
return &netaddr.IPSet{}, fmt.Errorf(
|
||||
"failed to build IP Set: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return ipSet, nil
|
||||
}
|
||||
|
||||
func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool {
|
||||
for _, v := range ips {
|
||||
if v == ip {
|
||||
func containsString(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
|
|||
ips, err := app.getAvailableIPs()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
namespace, err := app.CreateNamespace("test_ip")
|
||||
namespace, err := app.CreateNamespace("test-ip")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||
|
@ -36,7 +36,6 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
IPAddresses: ips,
|
||||
|
@ -48,9 +47,12 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
|
|||
c.Assert(err, check.IsNil)
|
||||
|
||||
expected := netaddr.MustParseIP("10.27.0.1")
|
||||
expectedIPSetBuilder := netaddr.IPSetBuilder{}
|
||||
expectedIPSetBuilder.Add(expected)
|
||||
expectedIPSet, _ := expectedIPSetBuilder.IPSet()
|
||||
|
||||
c.Assert(len(usedIps), check.Equals, 1)
|
||||
c.Assert(usedIps[0], check.Equals, expected)
|
||||
c.Assert(usedIps.Equal(expectedIPSet), check.Equals, true)
|
||||
c.Assert(usedIps.Contains(expected), check.Equals, true)
|
||||
|
||||
machine1, err := app.GetMachineByID(0)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
@ -64,6 +66,8 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
|
|||
c.Assert(err, check.IsNil)
|
||||
|
||||
for index := 1; index <= 350; index++ {
|
||||
app.ipAllocationMutex.Lock()
|
||||
|
||||
ips, err := app.getAvailableIPs()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
|
@ -80,23 +84,35 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
IPAddresses: ips,
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
|
||||
app.ipAllocationMutex.Unlock()
|
||||
}
|
||||
|
||||
usedIps, err := app.getUsedIPs()
|
||||
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
c.Assert(len(usedIps), check.Equals, 350)
|
||||
expected0 := netaddr.MustParseIP("10.27.0.1")
|
||||
expected9 := netaddr.MustParseIP("10.27.0.10")
|
||||
expected300 := netaddr.MustParseIP("10.27.0.45")
|
||||
|
||||
c.Assert(usedIps[0], check.Equals, netaddr.MustParseIP("10.27.0.1"))
|
||||
c.Assert(usedIps[9], check.Equals, netaddr.MustParseIP("10.27.0.10"))
|
||||
c.Assert(usedIps[300], check.Equals, netaddr.MustParseIP("10.27.1.45"))
|
||||
notExpectedIPSetBuilder := netaddr.IPSetBuilder{}
|
||||
notExpectedIPSetBuilder.Add(expected0)
|
||||
notExpectedIPSetBuilder.Add(expected9)
|
||||
notExpectedIPSetBuilder.Add(expected300)
|
||||
notExpectedIPSet, err := notExpectedIPSetBuilder.IPSet()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
// We actually expect it to be a lot larger
|
||||
c.Assert(usedIps.Equal(notExpectedIPSet), check.Equals, false)
|
||||
|
||||
c.Assert(usedIps.Contains(expected0), check.Equals, true)
|
||||
c.Assert(usedIps.Contains(expected9), check.Equals, true)
|
||||
c.Assert(usedIps.Contains(expected300), check.Equals, true)
|
||||
|
||||
// Check that we can read back the IPs
|
||||
machine1, err := app.GetMachineByID(1)
|
||||
|
@ -142,7 +158,7 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
|
|||
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)
|
||||
|
||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||
|
@ -158,7 +174,6 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
|
|||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue