mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-16 02:07:40 +00:00
Compare commits
170 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e8277595f5 | ||
![]() |
4d3b638a3d | ||
![]() |
6d162eeff9 | ||
![]() |
746d4037da | ||
![]() |
1237e02f7c | ||
![]() |
7da3d4ba50 | ||
![]() |
8853315dcc | ||
![]() |
5aaffaaecb | ||
![]() |
389a8d47a3 | ||
![]() |
a355769416 | ||
![]() |
1a8c9216d6 | ||
![]() |
81316ef644 | ||
![]() |
4d4d0de356 | ||
![]() |
b85adbc40a | ||
![]() |
aefbd66317 | ||
![]() |
d875cca69d | ||
![]() |
0e902fe949 | ||
![]() |
582eb57a09 | ||
![]() |
177f1eca06 | ||
![]() |
57f46ded83 | ||
![]() |
aa245c2d06 | ||
![]() |
e836db1ead | ||
![]() |
5420347d24 | ||
![]() |
9e2637d65f | ||
![]() |
c6046597ed | ||
![]() |
a46c8fe914 | ||
![]() |
f822816cdb | ||
![]() |
f3bf9b4bbb | ||
![]() |
9f02899261 | ||
![]() |
75f3e1fb03 | ||
![]() |
9fbfa7c1f5 | ||
![]() |
d5aef85bf2 | ||
![]() |
88b32e4b18 | ||
![]() |
e425e3ffd3 | ||
![]() |
355483fd86 | ||
![]() |
672d8474b9 | ||
![]() |
73e4d38670 | ||
![]() |
561c15bbe8 | ||
![]() |
b93aa723cb | ||
![]() |
636943c715 | ||
![]() |
0a6a67da85 | ||
![]() |
e9ffd366dd | ||
![]() |
4be0b3f556 | ||
![]() |
a0bfad6d6e | ||
![]() |
bb1f17f5af | ||
![]() |
95bc2ee241 | ||
![]() |
16a90e799c | ||
![]() |
4c2f84b211 | ||
![]() |
b799635fbb | ||
![]() |
bc145952d4 | ||
![]() |
2c5701917d | ||
![]() |
ed7b840fea | ||
![]() |
23372e29cd | ||
![]() |
fb569b0483 | ||
![]() |
e2b5638ca0 | ||
![]() |
8f5a1dce3e | ||
![]() |
6b0f5da113 | ||
![]() |
5159b6d085 | ||
![]() |
03d97c3872 | ||
![]() |
41c5a0ddf5 | ||
![]() |
19165a40d2 | ||
![]() |
d1ebcb59f1 | ||
![]() |
31344128a0 | ||
![]() |
86ecc2a234 | ||
![]() |
d1e8ac7ba5 | ||
![]() |
efe208fef5 | ||
![]() |
7b40e99aec | ||
![]() |
06706aab9a | ||
![]() |
0318af5a33 | ||
![]() |
995dcfc6ae | ||
![]() |
2236cc8bf7 | ||
![]() |
7bb354117b | ||
![]() |
18b00b5d8d | ||
![]() |
d2a162e3ee | ||
![]() |
d35f5fe498 | ||
![]() |
9e1253ada1 | ||
![]() |
244e79f575 | ||
![]() |
b4e6a32b4b | ||
![]() |
023cd8f4cd | ||
![]() |
10d24e64cd | ||
![]() |
37e191a75d | ||
![]() |
01a5fe3c51 | ||
![]() |
9e3339b4f1 | ||
![]() |
b06e34f144 | ||
![]() |
ddf042cab1 | ||
![]() |
687e8d12be | ||
![]() |
01f755ecf9 | ||
![]() |
8094e6fdef | ||
![]() |
061efa1822 | ||
![]() |
9a7472218e | ||
![]() |
7dcf4a5147 | ||
![]() |
306a80cf57 | ||
![]() |
a9a1a8fb3c | ||
![]() |
85ddc0db33 | ||
![]() |
fddc2aa8fa | ||
![]() |
be3a379d10 | ||
![]() |
d0daff180e | ||
![]() |
be36480a64 | ||
![]() |
9f52a64a6a | ||
![]() |
52511af8e4 | ||
![]() |
ddb6bd795c | ||
![]() |
271660a284 | ||
![]() |
0b0f7db534 | ||
![]() |
5a7b377f6f | ||
![]() |
654d2b9910 | ||
![]() |
829a8c4381 | ||
![]() |
5807562b56 | ||
![]() |
985c6e7cc9 | ||
![]() |
0d13e16fed | ||
![]() |
91d135e069 | ||
![]() |
3e1e07e8c1 | ||
![]() |
6c4c761408 | ||
![]() |
abfb1791f1 | ||
![]() |
7ce8c4c649 | ||
![]() |
2ddca366f2 | ||
![]() |
9a6ac6e3e6 | ||
![]() |
cc3e8705bd | ||
![]() |
809a5b84e7 | ||
![]() |
06ae2a6c50 | ||
![]() |
93517aa6f8 | ||
![]() |
5f0f3705c0 | ||
![]() |
70ae18c3a8 | ||
![]() |
6aa763a1ae | ||
![]() |
ebfb8c8c5e | ||
![]() |
30788e1a70 | ||
![]() |
27947c6746 | ||
![]() |
6924b7bf4c | ||
![]() |
fa8cd96108 | ||
![]() |
dd1e425d02 | ||
![]() |
7f2027d7f2 | ||
![]() |
48f5a9a18c | ||
![]() |
087c461762 | ||
![]() |
d579c1718c | ||
![]() |
4c5f667504 | ||
![]() |
4c4c95198b | ||
![]() |
5ce1526a06 | ||
![]() |
d70c3d6189 | ||
![]() |
9a0c9768ad | ||
![]() |
6884798404 | ||
![]() |
c4487b73c4 | ||
![]() |
32c3f09eb4 | ||
![]() |
d4dc133e20 | ||
![]() |
fc5153af3e | ||
![]() |
1a0f6f6e39 | ||
![]() |
6981543db6 | ||
![]() |
c9a411e341 | ||
![]() |
b02a9f9769 | ||
![]() |
a0fa652449 | ||
![]() |
2eef535b4b | ||
![]() |
61870a275f | ||
![]() |
088e8248d3 | ||
![]() |
da4a9dadd5 | ||
![]() |
02bc7314f4 | ||
![]() |
1a41a9f2c7 | ||
![]() |
040a18e6f8 | ||
![]() |
ec911981c2 | ||
![]() |
ef0f7c0c09 | ||
![]() |
e60ceefea9 | ||
![]() |
64185cc2bc | ||
![]() |
990ff153c0 | ||
![]() |
47dcc940c0 | ||
![]() |
8d60ae2c7e | ||
![]() |
19492650d4 | ||
![]() |
36ae14bccf | ||
![]() |
45e71ecba0 | ||
![]() |
e432e98413 | ||
![]() |
656237e167 | ||
![]() |
5dbf6b5127 | ||
![]() |
c9e4da3ff5 | ||
![]() |
3f3cfedffa |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -33,4 +33,4 @@ jobs:
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: headscale-linux
|
||||
path: headscale
|
||||
path: headscale
|
||||
|
24
.github/workflows/contributors.yml
vendored
Normal file
24
.github/workflows/contributors.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Contributors
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
add-contributors:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: BobAnkh/add-contributors@master
|
||||
with:
|
||||
CONTRIBUTOR: "## Contributors"
|
||||
COLUMN_PER_ROW: "6"
|
||||
ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
IMG_WIDTH: "100"
|
||||
FONT_SIZE: "14"
|
||||
PATH: "/README.md"
|
||||
COMMIT_MESSAGE: "docs(README): update contributors"
|
||||
AVATAR_SHAPE: "round"
|
||||
BRANCH: "update-contributors"
|
||||
PULL_REQUEST: "main"
|
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -4,25 +4,27 @@ name: release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*" # triggers only if push new tag version
|
||||
- "*" # triggers only if push new tag version
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-18.04 # due to CGO we need to user an older version
|
||||
runs-on: ubuntu-18.04 # due to CGO we need to user an older version
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
-
|
||||
name: Run GoReleaser
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y gcc-aarch64-linux-gnu
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
distribution: goreleaser
|
||||
@@ -34,13 +36,11 @@ jobs:
|
||||
docker-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Docker meta
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
@@ -53,21 +53,18 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
-
|
||||
name: Login to DockerHub
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Login to GHCR
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,6 +16,7 @@
|
||||
|
||||
/headscale
|
||||
config.json
|
||||
config.yaml
|
||||
*.key
|
||||
/db.sqlite
|
||||
*.sqlite3
|
||||
|
@@ -6,7 +6,7 @@ before:
|
||||
builds:
|
||||
- id: darwin-amd64
|
||||
main: ./cmd/headscale/headscale.go
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
@@ -20,9 +20,10 @@ builds:
|
||||
- -mod=readonly
|
||||
ldflags:
|
||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||
|
||||
- id: linux-armhf
|
||||
main: ./cmd/headscale/headscale.go
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
@@ -41,7 +42,6 @@ builds:
|
||||
ldflags:
|
||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||
|
||||
|
||||
- id: linux-amd64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
@@ -49,11 +49,21 @@ builds:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
main: ./cmd/headscale/headscale.go
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
ldflags:
|
||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||
|
||||
- id: linux-arm64
|
||||
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}}
|
||||
|
||||
@@ -63,16 +73,17 @@ archives:
|
||||
- darwin-amd64
|
||||
- linux-armhf
|
||||
- linux-amd64
|
||||
- linux-arm64
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
format: binary
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
name_template: "checksums.txt"
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
@@ -12,6 +12,11 @@ RUN test -e /go/bin/headscale
|
||||
|
||||
FROM ubuntu:20.04
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y ca-certificates \
|
||||
&& update-ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /go/bin/headscale /usr/local/bin/headscale
|
||||
ENV TZ UTC
|
||||
|
||||
|
407
README.md
407
README.md
@@ -1,9 +1,13 @@
|
||||
# Headscale
|
||||
# headscale
|
||||
|
||||
[](https://gitter.im/headscale-dev/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 
|
||||

|
||||
|
||||
An open source, self-hosted implementation of the Tailscale coordination 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.
|
||||
|
||||
## Overview
|
||||
|
||||
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using all kinds of [NAT traversal sorcery](https://tailscale.com/blog/how-nat-traversal-works/).
|
||||
@@ -12,14 +16,14 @@ Everything in Tailscale is Open Source, except the GUI clients for proprietary O
|
||||
|
||||
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.
|
||||
|
||||
Headscale implements this coordination server.
|
||||
headscale implements this coordination server.
|
||||
|
||||
## Status
|
||||
|
||||
- [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] Namespace support (~equivalent to multi-user in Tailscale.com)
|
||||
- [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
|
||||
@@ -27,245 +31,194 @@ Headscale implements this coordination server.
|
||||
- [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] Share nodes between ~~users~~ namespaces
|
||||
- [ ] MagicDNS / Smart DNS
|
||||
- [x] Share nodes between namespaces
|
||||
- [x] MagicDNS (see `docs/`)
|
||||
|
||||
## Client OS support
|
||||
|
||||
| OS | Supports headscale |
|
||||
| --- | --- |
|
||||
| Linux | Yes |
|
||||
| OpenBSD | Yes |
|
||||
| macOS | Yes (see `/apple` on your headscale for more information) |
|
||||
| Windows | Yes |
|
||||
| OS | Supports headscale |
|
||||
| ------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| Linux | Yes |
|
||||
| OpenBSD | Yes |
|
||||
| macOS | Yes (see `/apple` on your headscale for more information) |
|
||||
| Windows | Yes |
|
||||
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
|
||||
| iOS | Not yet |
|
||||
| iOS | Not yet |
|
||||
|
||||
## Roadmap 🤷
|
||||
|
||||
Suggestions/PRs welcomed!
|
||||
|
||||
## Running it
|
||||
|
||||
1. Download the Headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your PATH or use the docker container
|
||||
## Running headscale
|
||||
|
||||
```shell
|
||||
docker pull headscale/headscale:x.x.x
|
||||
```
|
||||
Please have a look at the documentation under [`docs/`](docs/).
|
||||
|
||||
<!--
|
||||
or
|
||||
```shell
|
||||
docker pull ghrc.io/juanfont/headscale:x.x.x
|
||||
``` -->
|
||||
|
||||
2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running
|
||||
|
||||
```shell
|
||||
docker run --name headscale -e POSTGRES_DB=headscale -e \
|
||||
POSTGRES_USER=foo -e POSTGRES_PASSWORD=bar -p 5432:5432 -d postgres
|
||||
```
|
||||
|
||||
3. Set some stuff up (headscale Wireguard keys & the config.json file)
|
||||
|
||||
```shell
|
||||
wg genkey > private.key
|
||||
wg pubkey < private.key > public.key # not needed
|
||||
|
||||
# Postgres
|
||||
cp config.json.postgres.example config.json
|
||||
# or
|
||||
# SQLite
|
||||
cp config.json.sqlite.example config.json
|
||||
```
|
||||
|
||||
4. Create a namespace (a namespace is a 'tailnet', a group of Tailscale nodes that can talk to each other)
|
||||
|
||||
```shell
|
||||
headscale namespaces create myfirstnamespace
|
||||
```
|
||||
|
||||
or docker:
|
||||
|
||||
the db.sqlite mount is only needed if you use sqlite
|
||||
|
||||
```shell
|
||||
touch db.sqlite
|
||||
docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derp.yaml:/derp.yaml -v $(pwd)/db.sqlite:/db.sqlite -p 127.0.0.1:8080:8080 headscale/headscale:x.x.x headscale namespaces create myfirstnamespace
|
||||
```
|
||||
|
||||
or if your server is already running in docker:
|
||||
|
||||
```shell
|
||||
docker exec <container_name> headscale create myfirstnamespace
|
||||
```
|
||||
|
||||
5. Run the server
|
||||
|
||||
```shell
|
||||
headscale serve
|
||||
```
|
||||
|
||||
or docker:
|
||||
|
||||
the db.sqlite mount is only needed if you use sqlite
|
||||
|
||||
```shell
|
||||
docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derp.yaml:/derp.yaml -v $(pwd)/db.sqlite:/db.sqlite -p 127.0.0.1:8080:8080 headscale/headscale:x.x.x headscale serve
|
||||
```
|
||||
|
||||
6. If you used tailscale.com before in your nodes, make sure you clear the tailscald data folder
|
||||
|
||||
```shell
|
||||
systemctl stop tailscaled
|
||||
rm -fr /var/lib/tailscale
|
||||
systemctl start tailscaled
|
||||
```
|
||||
|
||||
7. Add your first machine
|
||||
|
||||
```shell
|
||||
tailscale up --login-server YOUR_HEADSCALE_URL
|
||||
```
|
||||
|
||||
8. Navigate to the URL you will get with `tailscale up`, where you'll find your machine key.
|
||||
|
||||
9. In the server, register your machine to a namespace with the CLI
|
||||
```shell
|
||||
headscale -n myfirstnamespace nodes register YOURMACHINEKEY
|
||||
```
|
||||
or docker:
|
||||
```shell
|
||||
docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derp.yaml:/derp.yaml headscale/headscale:x.x.x headscale -n myfirstnamespace nodes register YOURMACHINEKEY
|
||||
```
|
||||
or if your server is already running in docker:
|
||||
```shell
|
||||
docker exec <container_name> headscale -n myfirstnamespace nodes register YOURMACHINEKEY
|
||||
```
|
||||
|
||||
Alternatively, you can use Auth Keys to register your machines:
|
||||
|
||||
1. Create an authkey
|
||||
|
||||
```shell
|
||||
headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
|
||||
```
|
||||
|
||||
or docker:
|
||||
|
||||
```shell
|
||||
docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v$(pwd)/derp.yaml:/derp.yaml -v $(pwd)/db.sqlite:/db.sqlite headscale/headscale:x.x.x headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
|
||||
```
|
||||
|
||||
or if your server is already running in docker:
|
||||
|
||||
```shell
|
||||
docker exec <container_name> headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
|
||||
```
|
||||
|
||||
2. Use the authkey from your machine to register it
|
||||
```shell
|
||||
tailscale up --login-server YOUR_HEADSCALE_URL --authkey YOURAUTHKEY
|
||||
```
|
||||
|
||||
If you create an authkey with the `--ephemeral` flag, that key will create ephemeral nodes. This implies that `--reusable` is true.
|
||||
|
||||
Please bear in mind that all the commands from headscale support adding `-o json` or `-o json-line` to get a nicely JSON-formatted output.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
Headscale's configuration file is named `config.json` or `config.yaml`. Headscale will look for it in `/etc/headscale`, `~/.headscale` and finally the directory from where the Headscale binary is executed.
|
||||
|
||||
```
|
||||
"server_url": "http://192.168.1.12:8080",
|
||||
"listen_addr": "0.0.0.0:8080",
|
||||
"ip_prefix": "100.64.0.0/10"
|
||||
```
|
||||
|
||||
`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. `ip_prefix` is the IP prefix (range) in which IP addresses for nodes will be allocated (default 100.64.0.0/10, e.g., 192.168.4.0/24, 10.0.0.0/8)
|
||||
|
||||
```
|
||||
"log_level": "debug"
|
||||
```
|
||||
|
||||
`log_level` can be used to set the Log level for Headscale, it defaults to `debug`, and the available levels are: `trace`, `debug`, `info`, `warn` and `error`.
|
||||
|
||||
```
|
||||
"private_key_path": "private.key",
|
||||
```
|
||||
|
||||
`private_key_path` is the path to the Wireguard private key. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||
|
||||
```
|
||||
"derp_map_path": "derp.yaml",
|
||||
```
|
||||
|
||||
`derp_map_path` is the path to the [DERP](https://pkg.go.dev/tailscale.com/derp) map file. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||
|
||||
```
|
||||
"ephemeral_node_inactivity_timeout": "30m",
|
||||
```
|
||||
|
||||
`ephemeral_node_inactivity_timeout` is the timeout after which inactive ephemeral node records will be deleted from the database. The default is 30 minutes. This value must be higher than 65 seconds (the keepalive timeout for the HTTP long poll is 60 seconds, plus a few seconds to avoid race conditions).
|
||||
|
||||
```
|
||||
"db_host": "localhost",
|
||||
"db_port": 5432,
|
||||
"db_name": "headscale",
|
||||
"db_user": "foo",
|
||||
"db_pass": "bar",
|
||||
```
|
||||
|
||||
The fields starting with `db_` are used for the PostgreSQL connection information.
|
||||
|
||||
### Running the service via TLS (optional)
|
||||
|
||||
```
|
||||
"tls_cert_path": ""
|
||||
"tls_key_path": ""
|
||||
```
|
||||
|
||||
Headscale can be configured to expose its web service via TLS. To configure the certificate and key file manually, set the `tls_cert_path` and `tls_cert_path` configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||
|
||||
```
|
||||
"tls_letsencrypt_hostname": "",
|
||||
"tls_letsencrypt_listen": ":http",
|
||||
"tls_letsencrypt_cache_dir": ".cache",
|
||||
"tls_letsencrypt_challenge_type": "HTTP-01",
|
||||
```
|
||||
|
||||
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) Headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from. The certificate will automatically be renewed as needed.
|
||||
|
||||
#### Challenge type HTTP-01
|
||||
|
||||
The default challenge type `HTTP-01` requires that Headscale is reachable on port 80 for the Let's Encrypt automated validation, in addition to whatever port is configured in `listen_addr`. By default, Headscale listens on port 80 on all local IPs for Let's Encrypt automated validation.
|
||||
|
||||
If you need to change the ip and/or port used by Headscale for the Let's Encrypt validation process, set `tls_letsencrypt_listen` to the appropriate value. This can be handy if you are running Headscale as a non-root user (or can't run `setcap`). Keep in mind, however, that Let's Encrypt will _only_ connect to port 80 for the validation callback, so if you change `tls_letsencrypt_listen` you will also need to configure something else (e.g. a firewall rule) to forward the traffic from port 80 to the ip:port combination specified in `tls_letsencrypt_listen`.
|
||||
|
||||
#### Challenge type TLS-ALPN-01
|
||||
|
||||
Alternatively, `tls_letsencrypt_challenge_type` can be set to `TLS-ALPN-01`. In this configuration, Headscale listens on the ip:port combination defined in `listen_addr`. Let's Encrypt will _only_ connect to port 443 for the validation callback, so if `listen_addr` is not set to port 443, something else (e.g. a firewall rule) will be required to forward the traffic from port 443 to the ip:port combination specified in `listen_addr`.
|
||||
|
||||
### Policy ACLs
|
||||
|
||||
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
|
||||
|
||||
For instance, instead of referring to users when defining groups you must
|
||||
use namespaces (which are the equivalent to user/logins in Tailscale.com).
|
||||
|
||||
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
|
||||
|
||||
### Apple devices
|
||||
|
||||
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
1. We have nothing to do with Tailscale, or Tailscale Inc.
|
||||
2. The purpose of writing this was to learn how Tailscale works.
|
||||
|
||||
## More on Tailscale
|
||||
|
||||
- https://tailscale.com/blog/how-tailscale-works/
|
||||
- https://tailscale.com/blog/tailscale-key-management/
|
||||
- https://tailscale.com/blog/an-unlikely-database-migration/
|
||||
## Contributors
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Juan Font</b></sub>
|
||||
</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/>
|
||||
<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/cure>
|
||||
<img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Ward Vandewege</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/ohdearaugustin>
|
||||
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
|
||||
<br />
|
||||
<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/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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Aaron Bieber</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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Paul Tötterman</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/cmars>
|
||||
<img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Casey Marshall</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/SilverBut>
|
||||
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Silver Bullet</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/>
|
||||
<br />
|
||||
<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/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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Arthur Woimbée</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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Felix Yan</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/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/>
|
||||
<br />
|
||||
<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/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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Teteros</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/gitter-badger>
|
||||
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>The Gitter Badger</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/tianon>
|
||||
<img src=https://avatars.githubusercontent.com/u/161631?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tianon Gravi/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Tianon Gravi</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/woudsma>
|
||||
<img src=https://avatars.githubusercontent.com/u/6162978?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tjerk Woudsma/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>Zakhar Bessarab</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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>derelm</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||
<a href=https://github.com/ignoramous>
|
||||
<img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
|
||||
<br />
|
||||
<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/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/>
|
||||
<br />
|
||||
<sub style="font-size:14px"><b>zy</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
65
api.go
65
api.go
@@ -82,7 +82,10 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
|
||||
|
||||
now := time.Now().UTC()
|
||||
var m Machine
|
||||
if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(
|
||||
result.Error,
|
||||
gorm.ErrRecordNotFound,
|
||||
) {
|
||||
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine")
|
||||
m = Machine{
|
||||
Expiry: &req.Expiry,
|
||||
@@ -225,7 +228,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
|
||||
Str("func", "getMapResponse").
|
||||
Str("machine", req.Hostinfo.Hostname).
|
||||
Msg("Creating Map response")
|
||||
node, err := m.toNode(true)
|
||||
node, err := m.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "getMapResponse").
|
||||
@@ -243,13 +246,9 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profile := tailcfg.UserProfile{
|
||||
ID: tailcfg.UserID(m.NamespaceID),
|
||||
LoginName: m.Namespace.Name,
|
||||
DisplayName: m.Namespace.Name,
|
||||
}
|
||||
profiles := getMapResponseUserProfiles(*m, peers)
|
||||
|
||||
nodePeers, err := peers.toNodes(true)
|
||||
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "getMapResponse").
|
||||
@@ -258,22 +257,26 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := tailcfg.MapResponse{
|
||||
KeepAlive: false,
|
||||
Node: node,
|
||||
Peers: nodePeers,
|
||||
// TODO(kradalby): As per tailscale docs, if DNSConfig is nil,
|
||||
// it means its not updated, maybe we can have some logic
|
||||
// to check and only pass updates when its updates.
|
||||
// This is probably more relevant if we try to implement
|
||||
// "MagicDNS"
|
||||
DNSConfig: h.cfg.DNSConfig,
|
||||
SearchPaths: []string{},
|
||||
Domain: "headscale.net",
|
||||
PacketFilter: *h.aclRules,
|
||||
DERPMap: h.cfg.DerpMap,
|
||||
UserProfiles: []tailcfg.UserProfile{profile},
|
||||
dnsConfig, err := getMapResponseDNSConfig(h.cfg.DNSConfig, h.cfg.BaseDomain, *m, peers)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "getMapResponse").
|
||||
Err(err).
|
||||
Msg("Failed generate the DNSConfig")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := tailcfg.MapResponse{
|
||||
KeepAlive: false,
|
||||
Node: node,
|
||||
Peers: nodePeers,
|
||||
DNSConfig: dnsConfig,
|
||||
Domain: h.cfg.BaseDomain,
|
||||
PacketFilter: *h.aclRules,
|
||||
DERPMap: h.DERPMap,
|
||||
UserProfiles: profiles,
|
||||
}
|
||||
|
||||
log.Trace().
|
||||
Str("func", "getMapResponse").
|
||||
Str("machine", req.Hostinfo.Hostname).
|
||||
@@ -329,7 +332,13 @@ func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapReque
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, req tailcfg.RegisterRequest, m Machine) {
|
||||
func (h *Headscale) handleAuthKey(
|
||||
c *gin.Context,
|
||||
db *gorm.DB,
|
||||
idKey wgkey.Key,
|
||||
req tailcfg.RegisterRequest,
|
||||
m Machine,
|
||||
) {
|
||||
log.Debug().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", req.Hostinfo.Hostname).
|
||||
@@ -337,6 +346,11 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key,
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
pak, err := h.checkKeyValidity(req.Auth.AuthKey)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", m.Name).
|
||||
Err(err).
|
||||
Msg("Failed authentication via AuthKey")
|
||||
resp.MachineAuthorized = false
|
||||
respBody, err := encode(resp, &idKey, h.privateKey)
|
||||
if err != nil {
|
||||
@@ -385,6 +399,9 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key,
|
||||
m.RegisterMethod = "authKey"
|
||||
db.Save(&m)
|
||||
|
||||
pak.Used = true
|
||||
db.Save(&pak)
|
||||
|
||||
resp.MachineAuthorized = true
|
||||
resp.User = *pak.Namespace.toUser()
|
||||
respBody, err := encode(resp, &idKey, h.privateKey)
|
||||
|
50
app.go
50
app.go
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -13,12 +14,13 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/zsais/go-gin-prometheus"
|
||||
ginprometheus "github.com/zsais/go-gin-prometheus"
|
||||
"golang.org/x/crypto/acme"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"gorm.io/gorm"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
@@ -27,9 +29,11 @@ type Config struct {
|
||||
ServerURL string
|
||||
Addr string
|
||||
PrivateKeyPath string
|
||||
DerpMap *tailcfg.DERPMap
|
||||
EphemeralNodeInactivityTimeout time.Duration
|
||||
IPPrefix netaddr.IPPrefix
|
||||
BaseDomain string
|
||||
|
||||
DERP DERPConfig
|
||||
|
||||
DBtype string
|
||||
DBpath string
|
||||
@@ -53,6 +57,13 @@ type Config struct {
|
||||
DNSConfig *tailcfg.DNSConfig
|
||||
}
|
||||
|
||||
type DERPConfig struct {
|
||||
URLs []url.URL
|
||||
Paths []string
|
||||
AutoUpdate bool
|
||||
UpdateFrequency time.Duration
|
||||
}
|
||||
|
||||
// Headscale represents the base app of the service
|
||||
type Headscale struct {
|
||||
cfg Config
|
||||
@@ -63,6 +74,8 @@ type Headscale struct {
|
||||
publicKey *wgkey.Key
|
||||
privateKey *wgkey.Private
|
||||
|
||||
DERPMap *tailcfg.DERPMap
|
||||
|
||||
aclPolicy *ACLPolicy
|
||||
aclRules *[]tailcfg.FilterRule
|
||||
|
||||
@@ -106,6 +119,20 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS
|
||||
magicDNSDomains, err := generateMagicDNSRootDomains(h.cfg.IPPrefix, h.cfg.BaseDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// we might have routes already from Split DNS
|
||||
if h.cfg.DNSConfig.Routes == nil {
|
||||
h.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver)
|
||||
}
|
||||
for _, d := range magicDNSDomains {
|
||||
h.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
|
||||
}
|
||||
}
|
||||
|
||||
return &h, nil
|
||||
}
|
||||
|
||||
@@ -137,11 +164,15 @@ func (h *Headscale) expireEphemeralNodesWorker() {
|
||||
return
|
||||
}
|
||||
for _, m := range *machines {
|
||||
if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral && time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
|
||||
if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral &&
|
||||
time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
|
||||
log.Info().Str("machine", m.Name).Msg("Ephemeral client removed from database")
|
||||
err = h.db.Unscoped().Delete(m).Error
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("machine", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database")
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("machine", m.Name).
|
||||
Msg("🤮 Cannot delete ephemeral machine from the database")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,6 +213,15 @@ func (h *Headscale) Serve() error {
|
||||
go h.watchForKVUpdates(5000)
|
||||
go h.expireEphemeralNodes(5000)
|
||||
|
||||
// Fetch an initial DERP Map before we start serving
|
||||
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
||||
|
||||
if h.cfg.DERP.AutoUpdate {
|
||||
derpMapCancelChannel := make(chan struct{})
|
||||
defer func() { derpMapCancelChannel <- struct{}{} }()
|
||||
go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
|
||||
}
|
||||
|
||||
s := &http.Server{
|
||||
Addr: h.cfg.Addr,
|
||||
Handler: r,
|
||||
@@ -257,7 +297,6 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
|
||||
|
||||
times = append(times, lastChange)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sort.Slice(times, func(i, j int) bool {
|
||||
@@ -268,7 +307,6 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
|
||||
|
||||
if len(times) == 0 {
|
||||
return time.Now().UTC()
|
||||
|
||||
} else {
|
||||
return times[0]
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ func init() {
|
||||
namespaceCmd.AddCommand(createNamespaceCmd)
|
||||
namespaceCmd.AddCommand(listNamespacesCmd)
|
||||
namespaceCmd.AddCommand(destroyNamespaceCmd)
|
||||
namespaceCmd.AddCommand(renameNamespaceCmd)
|
||||
}
|
||||
|
||||
var namespaceCmd = &cobra.Command{
|
||||
@@ -107,3 +108,31 @@ var listNamespacesCmd = &cobra.Command{
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var renameNamespaceCmd = &cobra.Command{
|
||||
Use: "rename OLD_NAME NEW_NAME",
|
||||
Short: "Renames a namespace",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("Missing parameters")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o, _ := cmd.Flags().GetString("output")
|
||||
h, err := getHeadscaleApp()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
}
|
||||
err = h.RenameNamespace(args[0], args[1])
|
||||
if strings.HasPrefix(o, "json") {
|
||||
JsonOutput(map[string]string{"Result": "Namespace renamed"}, err, o)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("Error renaming namespace: %s\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Namespace renamed\n")
|
||||
},
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ func init() {
|
||||
nodeCmd.AddCommand(registerNodeCmd)
|
||||
nodeCmd.AddCommand(deleteNodeCmd)
|
||||
nodeCmd.AddCommand(shareMachineCmd)
|
||||
nodeCmd.AddCommand(unshareMachineCmd)
|
||||
}
|
||||
|
||||
var nodeCmd = &cobra.Command{
|
||||
@@ -129,6 +130,7 @@ var deleteNodeCmd = &cobra.Command{
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
h, err := getHeadscaleApp()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
@@ -143,21 +145,32 @@ var deleteNodeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("Do you want to remove the node %s?", m.Name),
|
||||
}
|
||||
err = survey.AskOne(prompt, &confirm)
|
||||
if err != nil {
|
||||
return
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
if !force {
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("Do you want to remove the node %s?", m.Name),
|
||||
}
|
||||
err = survey.AskOne(prompt, &confirm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if confirm {
|
||||
if confirm || force {
|
||||
err = h.DeleteMachine(m)
|
||||
if strings.HasPrefix(output, "json") {
|
||||
JsonOutput(map[string]string{"Result": "Node deleted"}, err, output)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Error deleting node: %s", err)
|
||||
}
|
||||
fmt.Printf("Node deleted\n")
|
||||
} else {
|
||||
if strings.HasPrefix(output, "json") {
|
||||
JsonOutput(map[string]string{"Result": "Node not deleted"}, err, output)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Node not deleted\n")
|
||||
}
|
||||
},
|
||||
@@ -217,6 +230,55 @@ var shareMachineCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var unshareMachineCmd = &cobra.Command{
|
||||
Use: "unshare ID",
|
||||
Short: "Unshares a node from the specified namespace",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("missing parameters")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
namespace, err := cmd.Flags().GetString("namespace")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting namespace: %s", err)
|
||||
}
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
h, err := getHeadscaleApp()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
}
|
||||
|
||||
n, err := h.GetNamespace(namespace)
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching namespace: %s", err)
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Error converting ID to integer: %s", err)
|
||||
}
|
||||
machine, err := h.GetMachineByID(uint64(id))
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting node: %s", err)
|
||||
}
|
||||
|
||||
err = h.RemoveSharedMachineFromNamespace(machine, n)
|
||||
if strings.HasPrefix(output, "json") {
|
||||
JsonOutput(map[string]string{"Result": "Node unshared"}, err, output)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("Error unsharing node: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Node unshared!")
|
||||
},
|
||||
}
|
||||
|
||||
func nodesToPtables(currentNamespace headscale.Namespace, machines []headscale.Machine) (pterm.TableData, error) {
|
||||
d := pterm.TableData{{"ID", "Name", "NodeKey", "Namespace", "IP address", "Ephemeral", "Last seen", "Online"}}
|
||||
|
||||
|
@@ -57,7 +57,7 @@ var listPreAuthKeys = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Expiration", "Created"}}
|
||||
d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"}}
|
||||
for _, k := range *keys {
|
||||
expiration := "-"
|
||||
if k.Expiration != nil {
|
||||
@@ -76,6 +76,7 @@ var listPreAuthKeys = &cobra.Command{
|
||||
k.Key,
|
||||
reusable,
|
||||
strconv.FormatBool(k.Ephemeral),
|
||||
fmt.Sprintf("%v", k.Used),
|
||||
expiration,
|
||||
k.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
@@ -130,7 +131,7 @@ var createPreAuthKeyCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var expirePreAuthKeyCmd = &cobra.Command{
|
||||
Use: "expire",
|
||||
Use: "expire KEY",
|
||||
Short: "Expire a preauthkey",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
@@ -152,6 +153,10 @@ var expirePreAuthKeyCmd = &cobra.Command{
|
||||
|
||||
k, err := h.GetPreAuthKey(n, args[0])
|
||||
if err != nil {
|
||||
if strings.HasPrefix(o, "json") {
|
||||
JsonOutput(k, err, o)
|
||||
return
|
||||
}
|
||||
log.Fatalf("Error getting the key: %s", err)
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringP("output", "o", "", "Output format. Empty for human-readable, 'json' or 'json-line'")
|
||||
rootCmd.PersistentFlags().Bool("force", false, "Disable prompts and forces the execution")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
|
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/juanfont/headscale"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
@@ -51,21 +50,26 @@ func LoadConfig(path string) error {
|
||||
|
||||
// Collect any validation errors and return them all at once
|
||||
var errorText string
|
||||
if (viper.GetString("tls_letsencrypt_hostname") != "") && ((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) {
|
||||
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
|
||||
((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) {
|
||||
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
|
||||
}
|
||||
|
||||
if (viper.GetString("tls_letsencrypt_hostname") != "") && (viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") && (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
|
||||
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
|
||||
(viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") &&
|
||||
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
|
||||
// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
|
||||
log.Warn().
|
||||
Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
|
||||
}
|
||||
|
||||
if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") && (viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") {
|
||||
if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") &&
|
||||
(viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") {
|
||||
errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(viper.GetString("server_url"), "http://") && !strings.HasPrefix(viper.GetString("server_url"), "https://") {
|
||||
if !strings.HasPrefix(viper.GetString("server_url"), "http://") &&
|
||||
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
|
||||
errorText += "Fatal config error: server_url must start with https:// or http://\n"
|
||||
}
|
||||
if errorText != "" {
|
||||
@@ -73,10 +77,38 @@ func LoadConfig(path string) error {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetDNSConfig() *tailcfg.DNSConfig {
|
||||
func GetDERPConfig() headscale.DERPConfig {
|
||||
urlStrs := viper.GetStringSlice("derp.urls")
|
||||
|
||||
urls := make([]url.URL, len(urlStrs))
|
||||
for index, urlStr := range urlStrs {
|
||||
urlAddr, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("url", urlStr).
|
||||
Err(err).
|
||||
Msg("Failed to parse url, ignoring...")
|
||||
}
|
||||
|
||||
urls[index] = *urlAddr
|
||||
}
|
||||
|
||||
paths := viper.GetStringSlice("derp.paths")
|
||||
|
||||
autoUpdate := viper.GetBool("derp.auto_update_enabled")
|
||||
updateFrequency := viper.GetDuration("derp.update_frequency")
|
||||
|
||||
return headscale.DERPConfig{
|
||||
URLs: urls,
|
||||
Paths: paths,
|
||||
AutoUpdate: autoUpdate,
|
||||
UpdateFrequency: updateFrequency,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
||||
if viper.IsSet("dns_config") {
|
||||
dnsConfig := &tailcfg.DNSConfig{}
|
||||
|
||||
@@ -104,14 +136,58 @@ func GetDNSConfig() *tailcfg.DNSConfig {
|
||||
dnsConfig.Nameservers = nameservers
|
||||
dnsConfig.Resolvers = resolvers
|
||||
}
|
||||
|
||||
if viper.IsSet("dns_config.restricted_nameservers") {
|
||||
if len(dnsConfig.Nameservers) > 0 {
|
||||
dnsConfig.Routes = make(map[string][]dnstype.Resolver)
|
||||
restrictedDNS := viper.GetStringMapStringSlice("dns_config.restricted_nameservers")
|
||||
for domain, restrictedNameservers := range restrictedDNS {
|
||||
restrictedResolvers := make([]dnstype.Resolver, len(restrictedNameservers))
|
||||
for index, nameserverStr := range restrictedNameservers {
|
||||
nameserver, err := netaddr.ParseIP(nameserverStr)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "getDNSConfig").
|
||||
Err(err).
|
||||
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
|
||||
}
|
||||
restrictedResolvers[index] = dnstype.Resolver{
|
||||
Addr: nameserver.String(),
|
||||
}
|
||||
}
|
||||
dnsConfig.Routes[domain] = restrictedResolvers
|
||||
}
|
||||
} else {
|
||||
log.Warn().
|
||||
Msg("Warning: dns_config.restricted_nameservers is set, but no nameservers are configured. Ignoring restricted_nameservers.")
|
||||
}
|
||||
}
|
||||
|
||||
if viper.IsSet("dns_config.domains") {
|
||||
dnsConfig.Domains = viper.GetStringSlice("dns_config.domains")
|
||||
}
|
||||
|
||||
return dnsConfig
|
||||
if viper.IsSet("dns_config.magic_dns") {
|
||||
magicDNS := viper.GetBool("dns_config.magic_dns")
|
||||
if len(dnsConfig.Nameservers) > 0 {
|
||||
dnsConfig.Proxied = magicDNS
|
||||
} else if magicDNS {
|
||||
log.Warn().
|
||||
Msg("Warning: dns_config.magic_dns is set, but no nameservers are configured. Ignoring magic_dns.")
|
||||
}
|
||||
}
|
||||
|
||||
var baseDomain string
|
||||
if viper.IsSet("dns_config.base_domain") {
|
||||
baseDomain = viper.GetString("dns_config.base_domain")
|
||||
} else {
|
||||
baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled
|
||||
}
|
||||
|
||||
return dnsConfig, baseDomain
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func absPath(path string) string {
|
||||
@@ -127,29 +203,29 @@ func absPath(path string) string {
|
||||
}
|
||||
|
||||
func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||
derpPath := absPath(viper.GetString("derp_map_path"))
|
||||
derpMap, err := loadDerpMap(derpPath)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("path", derpPath).
|
||||
Err(err).
|
||||
Msg("Could not load DERP servers map file")
|
||||
}
|
||||
|
||||
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
|
||||
// to avoid races
|
||||
minInactivityTimeout, _ := time.ParseDuration("65s")
|
||||
if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout {
|
||||
err = fmt.Errorf("ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s\n", viper.GetString("ephemeral_node_inactivity_timeout"), minInactivityTimeout)
|
||||
err := fmt.Errorf(
|
||||
"ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s\n",
|
||||
viper.GetString("ephemeral_node_inactivity_timeout"),
|
||||
minInactivityTimeout,
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dnsConfig, baseDomain := GetDNSConfig()
|
||||
derpConfig := GetDERPConfig()
|
||||
|
||||
cfg := headscale.Config{
|
||||
ServerURL: viper.GetString("server_url"),
|
||||
Addr: viper.GetString("listen_addr"),
|
||||
PrivateKeyPath: absPath(viper.GetString("private_key_path")),
|
||||
DerpMap: derpMap,
|
||||
IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")),
|
||||
BaseDomain: baseDomain,
|
||||
|
||||
DERP: derpConfig,
|
||||
|
||||
EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"),
|
||||
|
||||
@@ -169,10 +245,10 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
|
||||
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
|
||||
|
||||
DNSConfig: dnsConfig,
|
||||
|
||||
ACMEEmail: viper.GetString("acme_email"),
|
||||
ACMEURL: viper.GetString("acme_url"),
|
||||
|
||||
DNSConfig: GetDNSConfig(),
|
||||
}
|
||||
|
||||
h, err := headscale.NewHeadscale(cfg)
|
||||
@@ -196,21 +272,6 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func loadDerpMap(path string) (*tailcfg.DERPMap, error) {
|
||||
derpFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer derpFile.Close()
|
||||
var derpMap tailcfg.DERPMap
|
||||
b, err := io.ReadAll(derpFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = yaml.Unmarshal(b, &derpMap)
|
||||
return &derpMap, err
|
||||
}
|
||||
|
||||
func JsonOutput(result interface{}, errResult error, outputFormat string) {
|
||||
var j []byte
|
||||
var err error
|
||||
@@ -242,3 +303,12 @@ func JsonOutput(result interface{}, errResult error, outputFormat string) {
|
||||
}
|
||||
fmt.Println(string(j))
|
||||
}
|
||||
|
||||
func HasJsonOutputFlag() bool {
|
||||
for _, arg := range os.Args {
|
||||
if arg == "json" || arg == "json-line" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@@ -62,7 +62,8 @@ func main() {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
|
||||
if !viper.GetBool("disable_check_updates") {
|
||||
jsonOutput := cli.HasJsonOutputFlag()
|
||||
if !viper.GetBool("disable_check_updates") && !jsonOutput {
|
||||
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && cli.Version != "dev" {
|
||||
githubTag := &latest.GithubTag{
|
||||
Owner: "juanfont",
|
||||
|
@@ -25,10 +25,9 @@ func (s *Suite) SetUpSuite(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TearDownSuite(c *check.C) {
|
||||
|
||||
}
|
||||
|
||||
func (*Suite) TestPostgresConfigLoading(c *check.C) {
|
||||
func (*Suite) TestConfigLoading(c *check.C) {
|
||||
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||
if err != nil {
|
||||
c.Fatal(err)
|
||||
@@ -41,7 +40,7 @@ func (*Suite) TestPostgresConfigLoading(c *check.C) {
|
||||
}
|
||||
|
||||
// Symlink the example config file
|
||||
err = os.Symlink(filepath.Clean(path+"/../../config.json.postgres.example"), filepath.Join(tmpDir, "config.json"))
|
||||
err = os.Symlink(filepath.Clean(path+"/../../config-example.yaml"), filepath.Join(tmpDir, "config.yaml"))
|
||||
if err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
@@ -53,40 +52,7 @@ func (*Suite) TestPostgresConfigLoading(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("derp_map_path"), check.Equals, "derp.yaml")
|
||||
c.Assert(viper.GetString("db_type"), check.Equals, "postgres")
|
||||
c.Assert(viper.GetString("db_port"), check.Equals, "5432")
|
||||
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
|
||||
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
|
||||
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
||||
}
|
||||
|
||||
func (*Suite) TestSqliteConfigLoading(c *check.C) {
|
||||
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||
if err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
|
||||
// Symlink the example config file
|
||||
err = os.Symlink(filepath.Clean(path+"/../../config.json.sqlite.example"), filepath.Join(tmpDir, "config.json"))
|
||||
if err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
|
||||
// Load example config, it should load without validation errors
|
||||
err = cli.LoadConfig(tmpDir)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
// 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("derp_map_path"), check.Equals, "derp.yaml")
|
||||
c.Assert(viper.GetStringSlice("derp.paths")[0], check.Equals, "derp-example.yaml")
|
||||
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
|
||||
c.Assert(viper.GetString("db_path"), check.Equals, "db.sqlite")
|
||||
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
|
||||
@@ -108,7 +74,7 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
|
||||
}
|
||||
|
||||
// Symlink the example config file
|
||||
err = os.Symlink(filepath.Clean(path+"/../../config.json.sqlite.example"), filepath.Join(tmpDir, "config.json"))
|
||||
err = os.Symlink(filepath.Clean(path+"/../../config-example.yaml"), filepath.Join(tmpDir, "config.yaml"))
|
||||
if err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
@@ -117,18 +83,18 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
|
||||
err = cli.LoadConfig(tmpDir)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
dnsConfig := cli.GetDNSConfig()
|
||||
fmt.Println(dnsConfig)
|
||||
dnsConfig, baseDomain := cli.GetDNSConfig()
|
||||
|
||||
c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1")
|
||||
|
||||
c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1")
|
||||
c.Assert(dnsConfig.Proxied, check.Equals, true)
|
||||
c.Assert(baseDomain, check.Equals, "example.com")
|
||||
}
|
||||
|
||||
func writeConfig(c *check.C, tmpDir string, configYaml []byte) {
|
||||
// Populate a custom config file
|
||||
configFile := filepath.Join(tmpDir, "config.yaml")
|
||||
err := ioutil.WriteFile(configFile, configYaml, 0644)
|
||||
err := ioutil.WriteFile(configFile, configYaml, 0o644)
|
||||
if err != nil {
|
||||
c.Fatalf("Couldn't write file %s", configFile)
|
||||
}
|
||||
@@ -139,10 +105,12 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
|
||||
if err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
//defer os.RemoveAll(tmpDir)
|
||||
// defer os.RemoveAll(tmpDir)
|
||||
fmt.Println(tmpDir)
|
||||
|
||||
configYaml := []byte("---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"")
|
||||
configYaml := []byte(
|
||||
"---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"",
|
||||
)
|
||||
writeConfig(c, tmpDir, configYaml)
|
||||
|
||||
// Check configuration validation errors (1)
|
||||
@@ -150,13 +118,23 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
|
||||
c.Assert(err, check.NotNil)
|
||||
// check.Matches can not handle multiline strings
|
||||
tmp := strings.ReplaceAll(err.Error(), "\n", "***")
|
||||
c.Assert(tmp, check.Matches, ".*Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both.*")
|
||||
c.Assert(tmp, check.Matches, ".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*")
|
||||
c.Assert(
|
||||
tmp,
|
||||
check.Matches,
|
||||
".*Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both.*",
|
||||
)
|
||||
c.Assert(
|
||||
tmp,
|
||||
check.Matches,
|
||||
".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*",
|
||||
)
|
||||
c.Assert(tmp, check.Matches, ".*Fatal config error: server_url must start with https:// or http://.*")
|
||||
fmt.Println(tmp)
|
||||
|
||||
// Check configuration validation errors (2)
|
||||
configYaml = []byte("---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"")
|
||||
configYaml = []byte(
|
||||
"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
|
||||
)
|
||||
writeConfig(c, tmpDir, configYaml)
|
||||
err = cli.LoadConfig(tmpDir)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
66
config-example.yaml
Normal file
66
config-example.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
# The url clients will connect to.
|
||||
# Typically this will be a domain.
|
||||
server_url: http://127.0.0.1:8080
|
||||
|
||||
# Address to listen to / bind to on the server
|
||||
listen_addr: 0.0.0.0:8080
|
||||
|
||||
# Path to WireGuard private key file
|
||||
private_key_path: private.key
|
||||
|
||||
derp:
|
||||
# List of externally available DERP maps encoded in JSON
|
||||
urls:
|
||||
- https://controlplane.tailscale.com/derpmap/default
|
||||
|
||||
# Locally available DERP map files encoded in YAML
|
||||
paths:
|
||||
- derp-example.yaml
|
||||
|
||||
# If enabled, a worker will be set up to periodically
|
||||
# refresh the given sources and update the derpmap
|
||||
# will be set up.
|
||||
auto_update_enabled: true
|
||||
|
||||
# How often should we check for updates?
|
||||
update_frequency: 24h
|
||||
|
||||
# Disables the automatic check for updates on startup
|
||||
disable_check_updates: false
|
||||
ephemeral_node_inactivity_timeout: 30m
|
||||
|
||||
# SQLite config
|
||||
db_type: sqlite3
|
||||
db_path: db.sqlite
|
||||
|
||||
# # Postgres config
|
||||
# db_type: postgres
|
||||
# db_host: localhost
|
||||
# db_port: 5432
|
||||
# db_name: headscale
|
||||
# db_user: foo
|
||||
# db_pass: bar
|
||||
|
||||
acme_url: https://acme-v02.api.letsencrypt.org/directory
|
||||
acme_email: ""
|
||||
|
||||
tls_letsencrypt_hostname: ""
|
||||
tls_letsencrypt_listen: ":http"
|
||||
tls_letsencrypt_cache_dir: ".cache"
|
||||
tls_letsencrypt_challenge_type: HTTP-01
|
||||
|
||||
tls_cert_path: ""
|
||||
tls_key_path: ""
|
||||
|
||||
# Path to a file containg ACL policies.
|
||||
acl_policy_path: ""
|
||||
|
||||
dns_config:
|
||||
# Upstream DNS servers
|
||||
nameservers:
|
||||
- 1.1.1.1
|
||||
domains: []
|
||||
|
||||
magic_dns: true
|
||||
base_domain: example.com
|
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"server_url": "http://127.0.0.1:8080",
|
||||
"listen_addr": "0.0.0.0:8080",
|
||||
"private_key_path": "private.key",
|
||||
"derp_map_path": "derp.yaml",
|
||||
"ephemeral_node_inactivity_timeout": "30m",
|
||||
"db_type": "postgres",
|
||||
"db_host": "localhost",
|
||||
"db_port": 5432,
|
||||
"db_name": "headscale",
|
||||
"db_user": "foo",
|
||||
"db_pass": "bar",
|
||||
"acme_url": "https://acme-v02.api.letsencrypt.org/directory",
|
||||
"acme_email": "",
|
||||
"tls_letsencrypt_hostname": "",
|
||||
"tls_letsencrypt_listen": ":http",
|
||||
"tls_letsencrypt_cache_dir": ".cache",
|
||||
"tls_letsencrypt_challenge_type": "HTTP-01",
|
||||
"tls_cert_path": "",
|
||||
"tls_key_path": "",
|
||||
"acl_policy_path": "",
|
||||
"dns_config": {
|
||||
"nameservers": [
|
||||
"1.1.1.1"
|
||||
]
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"server_url": "http://127.0.0.1:8080",
|
||||
"listen_addr": "0.0.0.0:8080",
|
||||
"private_key_path": "private.key",
|
||||
"derp_map_path": "derp.yaml",
|
||||
"ephemeral_node_inactivity_timeout": "30m",
|
||||
"db_type": "sqlite3",
|
||||
"db_path": "db.sqlite",
|
||||
"acme_url": "https://acme-v02.api.letsencrypt.org/directory",
|
||||
"acme_email": "",
|
||||
"tls_letsencrypt_hostname": "",
|
||||
"tls_letsencrypt_listen": ":http",
|
||||
"tls_letsencrypt_cache_dir": ".cache",
|
||||
"tls_letsencrypt_challenge_type": "HTTP-01",
|
||||
"tls_cert_path": "",
|
||||
"tls_key_path": "",
|
||||
"acl_policy_path": "",
|
||||
"dns_config": {
|
||||
"nameservers": [
|
||||
"1.1.1.1"
|
||||
]
|
||||
}
|
||||
}
|
15
derp-example.yaml
Normal file
15
derp-example.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
# If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/
|
||||
regions:
|
||||
900:
|
||||
regionid: 900
|
||||
regioncode: custom
|
||||
regionname: My Region
|
||||
nodes:
|
||||
- name: 1a
|
||||
regionid: 1
|
||||
hostname: myderp.mydomain.no
|
||||
ipv4: 123.123.123.123
|
||||
ipv6: "2604:a880:400:d1::828:b001"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
152
derp.go
Normal file
152
derp.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func loadDERPMapFromPath(path string) (*tailcfg.DERPMap, error) {
|
||||
derpFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer derpFile.Close()
|
||||
var derpMap tailcfg.DERPMap
|
||||
b, err := io.ReadAll(derpFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = yaml.Unmarshal(b, &derpMap)
|
||||
return &derpMap, err
|
||||
}
|
||||
|
||||
func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
resp, err := client.Get(addr.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var derpMap tailcfg.DERPMap
|
||||
err = json.Unmarshal(body, &derpMap)
|
||||
return &derpMap, err
|
||||
}
|
||||
|
||||
// mergeDERPMaps naively merges a list of DERPMaps into a single
|
||||
// DERPMap, it will _only_ look at the Regions, an integer.
|
||||
// If a region exists in two of the given DERPMaps, the region
|
||||
// form the _last_ DERPMap will be preserved.
|
||||
// An empty DERPMap list will result in a DERPMap with no regions
|
||||
func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
|
||||
result := tailcfg.DERPMap{
|
||||
OmitDefaultRegions: false,
|
||||
Regions: map[int]*tailcfg.DERPRegion{},
|
||||
}
|
||||
|
||||
for _, derpMap := range derpMaps {
|
||||
for id, region := range derpMap.Regions {
|
||||
result.Regions[id] = region
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func GetDERPMap(cfg DERPConfig) *tailcfg.DERPMap {
|
||||
derpMaps := make([]*tailcfg.DERPMap, 0)
|
||||
|
||||
for _, path := range cfg.Paths {
|
||||
log.Debug().
|
||||
Str("func", "GetDERPMap").
|
||||
Str("path", path).
|
||||
Msg("Loading DERPMap from path")
|
||||
derpMap, err := loadDERPMapFromPath(path)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "GetDERPMap").
|
||||
Str("path", path).
|
||||
Err(err).
|
||||
Msg("Could not load DERP map from path")
|
||||
break
|
||||
}
|
||||
|
||||
derpMaps = append(derpMaps, derpMap)
|
||||
}
|
||||
|
||||
for _, addr := range cfg.URLs {
|
||||
derpMap, err := loadDERPMapFromURL(addr)
|
||||
log.Debug().
|
||||
Str("func", "GetDERPMap").
|
||||
Str("url", addr.String()).
|
||||
Msg("Loading DERPMap from path")
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "GetDERPMap").
|
||||
Str("url", addr.String()).
|
||||
Err(err).
|
||||
Msg("Could not load DERP map from path")
|
||||
break
|
||||
}
|
||||
|
||||
derpMaps = append(derpMaps, derpMap)
|
||||
}
|
||||
|
||||
derpMap := mergeDERPMaps(derpMaps)
|
||||
|
||||
log.Trace().Interface("derpMap", derpMap).Msg("DERPMap loaded")
|
||||
|
||||
if len(derpMap.Regions) == 0 {
|
||||
log.Warn().
|
||||
Msg("DERP map is empty, not a single DERP map datasource was loaded correctly or contained a region")
|
||||
}
|
||||
|
||||
return derpMap
|
||||
}
|
||||
|
||||
func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
|
||||
log.Info().
|
||||
Dur("frequency", h.cfg.DERP.UpdateFrequency).
|
||||
Msg("Setting up a DERPMap update worker")
|
||||
ticker := time.NewTicker(h.cfg.DERP.UpdateFrequency)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancelChan:
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
log.Info().Msg("Fetching DERPMap updates")
|
||||
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
||||
|
||||
namespaces, err := h.ListNamespaces()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Failed to fetch namespaces")
|
||||
}
|
||||
|
||||
for _, namespace := range *namespaces {
|
||||
h.setLastStateChangeToNow(namespace.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
derp.yaml
146
derp.yaml
@@ -1,146 +0,0 @@
|
||||
# This file contains some of the official Tailscale DERP servers,
|
||||
# shamelessly taken from https://github.com/tailscale/tailscale/blob/main/net/dnsfallback/dns-fallback-servers.json
|
||||
#
|
||||
# If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/
|
||||
regions:
|
||||
1:
|
||||
regionid: 1
|
||||
regioncode: nyc
|
||||
regionname: New York City
|
||||
nodes:
|
||||
- name: 1a
|
||||
regionid: 1
|
||||
hostname: derp1.tailscale.com
|
||||
ipv4: 159.89.225.99
|
||||
ipv6: "2604:a880:400:d1::828:b001"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
- name: 1b
|
||||
regionid: 1
|
||||
hostname: derp1b.tailscale.com
|
||||
ipv4: 45.55.35.93
|
||||
ipv6: "2604:a880:800:a1::f:2001"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
2:
|
||||
regionid: 2
|
||||
regioncode: sfo
|
||||
regionname: San Francisco
|
||||
nodes:
|
||||
- name: 2a
|
||||
regionid: 2
|
||||
hostname: derp2.tailscale.com
|
||||
ipv4: 167.172.206.31
|
||||
ipv6: "2604:a880:2:d1::c5:7001"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
- name: 2b
|
||||
regionid: 2
|
||||
hostname: derp2b.tailscale.com
|
||||
ipv4: 64.227.106.23
|
||||
ipv6: "2604:a880:4:1d0::29:9000"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
3:
|
||||
regionid: 3
|
||||
regioncode: sin
|
||||
regionname: Singapore
|
||||
nodes:
|
||||
- name: 3a
|
||||
regionid: 3
|
||||
hostname: derp3.tailscale.com
|
||||
ipv4: 68.183.179.66
|
||||
ipv6: "2400:6180:0:d1::67d:8001"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
4:
|
||||
regionid: 4
|
||||
regioncode: fra
|
||||
regionname: Frankfurt
|
||||
nodes:
|
||||
- name: 4a
|
||||
regionid: 4
|
||||
hostname: derp4.tailscale.com
|
||||
ipv4: 167.172.182.26
|
||||
ipv6: "2a03:b0c0:3:e0::36e:900"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
- name: 4b
|
||||
regionid: 4
|
||||
hostname: derp4b.tailscale.com
|
||||
ipv4: 157.230.25.0
|
||||
ipv6: "2a03:b0c0:3:e0::58f:3001"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
5:
|
||||
regionid: 5
|
||||
regioncode: syd
|
||||
regionname: Sydney
|
||||
nodes:
|
||||
- name: 5a
|
||||
regionid: 5
|
||||
hostname: derp5.tailscale.com
|
||||
ipv4: 103.43.75.49
|
||||
ipv6: "2001:19f0:5801:10b7:5400:2ff:feaa:284c"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
6:
|
||||
regionid: 6
|
||||
regioncode: blr
|
||||
regionname: Bangalore
|
||||
nodes:
|
||||
- name: 6a
|
||||
regionid: 6
|
||||
hostname: derp6.tailscale.com
|
||||
ipv4: 68.183.90.120
|
||||
ipv6: "2400:6180:100:d0::982:d001"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
7:
|
||||
regionid: 7
|
||||
regioncode: tok
|
||||
regionname: Tokyo
|
||||
nodes:
|
||||
- name: 7a
|
||||
regionid: 7
|
||||
hostname: derp7.tailscale.com
|
||||
ipv4: 167.179.89.145
|
||||
ipv6: "2401:c080:1000:467f:5400:2ff:feee:22aa"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
8:
|
||||
regionid: 8
|
||||
regioncode: lhr
|
||||
regionname: London
|
||||
nodes:
|
||||
- name: 8a
|
||||
regionid: 8
|
||||
hostname: derp8.tailscale.com
|
||||
ipv4: 167.71.139.179
|
||||
ipv6: "2a03:b0c0:1:e0::3cc:e001"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
||||
9:
|
||||
regionid: 9
|
||||
regioncode: sao
|
||||
regionname: São Paulo
|
||||
nodes:
|
||||
- name: 9a
|
||||
regionid: 9
|
||||
hostname: derp9.tailscale.com
|
||||
ipv4: 207.148.3.137
|
||||
ipv6: "2001:19f0:6401:1d9c:5400:2ff:feef:bb82"
|
||||
stunport: 0
|
||||
stunonly: false
|
||||
derptestport: 0
|
92
dns.go
Normal file
92
dns.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/set"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`.
|
||||
// This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS
|
||||
// server (listening in 100.100.100.100 udp/53) should be used for.
|
||||
//
|
||||
// Tailscale.com includes in the list:
|
||||
// - the `BaseDomain` of the user
|
||||
// - the reverse DNS entry for IPv6 (0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa., see below more on IPv6)
|
||||
// - the reverse DNS entries for the IPv4 subnets covered by the user's `IPPrefix`.
|
||||
// In the public SaaS this is [64-127].100.in-addr.arpa.
|
||||
//
|
||||
// The main purpose of this function is then generating the list of IPv4 entries. For the 100.64.0.0/10, this
|
||||
// is clear, and could be hardcoded. But we are allowing any range as `IPPrefix`, so we need to find out the
|
||||
// subnets when we have 172.16.0.0/16 (i.e., [0-255].16.172.in-addr.arpa.), or any other subnet.
|
||||
//
|
||||
// How IN-ADDR.ARPA domains work is defined in RFC1035 (section 3.5). Tailscale.com seems to adhere to this,
|
||||
// and do not make use of RFC2317 ("Classless IN-ADDR.ARPA delegation") - hence generating the entries for the next
|
||||
// class block only.
|
||||
|
||||
// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask).
|
||||
// This allows us to then calculate the subnets included in the subsequent class block and generate the entries.
|
||||
func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) ([]dnsname.FQDN, error) {
|
||||
// TODO(juanfont): we are not handing out IPv6 addresses yet
|
||||
// and in fact this is Tailscale.com's range (note the fd7a:115c:a1e0: range in the fc00::/7 network)
|
||||
ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.")
|
||||
fqdns := []dnsname.FQDN{ipv6base}
|
||||
|
||||
// Conversion to the std lib net.IPnet, a bit easier to operate
|
||||
netRange := ipPrefix.IPNet()
|
||||
maskBits, _ := netRange.Mask.Size()
|
||||
|
||||
// lastOctet is the last IP byte covered by the mask
|
||||
lastOctet := maskBits / 8
|
||||
|
||||
// wildcardBits is the number of bits not under the mask in the lastOctet
|
||||
wildcardBits := 8 - maskBits%8
|
||||
|
||||
// min is the value in the lastOctet byte of the IP
|
||||
// max is basically 2^wildcardBits - i.e., the value when all the wildcardBits are set to 1
|
||||
min := uint(netRange.IP[lastOctet])
|
||||
max := uint((min + 1<<uint(wildcardBits)) - 1)
|
||||
|
||||
// here we generate the base domain (e.g., 100.in-addr.arpa., 16.172.in-addr.arpa., etc.)
|
||||
rdnsSlice := []string{}
|
||||
for i := lastOctet - 1; i >= 0; i-- {
|
||||
rdnsSlice = append(rdnsSlice, fmt.Sprintf("%d", netRange.IP[i]))
|
||||
}
|
||||
rdnsSlice = append(rdnsSlice, "in-addr.arpa.")
|
||||
rdnsBase := strings.Join(rdnsSlice, ".")
|
||||
|
||||
for i := min; i <= max; i++ {
|
||||
fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.%s", i, rdnsBase))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fqdns = append(fqdns, fqdn)
|
||||
}
|
||||
return fqdns, nil
|
||||
}
|
||||
|
||||
func getMapResponseDNSConfig(dnsConfigOrig *tailcfg.DNSConfig, baseDomain string, m Machine, peers Machines) (*tailcfg.DNSConfig, error) {
|
||||
var dnsConfig *tailcfg.DNSConfig
|
||||
if dnsConfigOrig != nil && dnsConfigOrig.Proxied { // if MagicDNS is enabled
|
||||
// Only inject the Search Domain of the current namespace - shared nodes should use their full FQDN
|
||||
dnsConfig = dnsConfigOrig.Clone()
|
||||
dnsConfig.Domains = append(dnsConfig.Domains, fmt.Sprintf("%s.%s", m.Namespace.Name, baseDomain))
|
||||
|
||||
namespaceSet := set.New(set.ThreadSafe)
|
||||
namespaceSet.Add(m.Namespace)
|
||||
for _, p := range peers {
|
||||
namespaceSet.Add(p.Namespace)
|
||||
}
|
||||
for _, namespace := range namespaceSet.List() {
|
||||
dnsRoute := fmt.Sprintf("%s.%s", namespace.(Namespace).Name, baseDomain)
|
||||
dnsConfig.Routes[dnsRoute] = nil
|
||||
}
|
||||
} else {
|
||||
dnsConfig = dnsConfigOrig
|
||||
}
|
||||
return dnsConfig, nil
|
||||
}
|
306
dns_test.go
Normal file
306
dns_test.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
)
|
||||
|
||||
func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
|
||||
prefix := netaddr.MustParseIPPrefix("100.64.0.0/10")
|
||||
domains, err := generateMagicDNSRootDomains(prefix, "foobar.headscale.net")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
found := false
|
||||
for _, domain := range domains {
|
||||
if domain == "64.100.in-addr.arpa." {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Assert(found, check.Equals, true)
|
||||
|
||||
found = false
|
||||
for _, domain := range domains {
|
||||
if domain == "100.100.in-addr.arpa." {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Assert(found, check.Equals, true)
|
||||
|
||||
found = false
|
||||
for _, domain := range domains {
|
||||
if domain == "127.100.in-addr.arpa." {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Assert(found, check.Equals, true)
|
||||
}
|
||||
|
||||
func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
|
||||
prefix := netaddr.MustParseIPPrefix("172.16.0.0/16")
|
||||
domains, err := generateMagicDNSRootDomains(prefix, "headscale.net")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
found := false
|
||||
for _, domain := range domains {
|
||||
if domain == "0.16.172.in-addr.arpa." {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Assert(found, check.Equals, true)
|
||||
|
||||
found = false
|
||||
for _, domain := range domains {
|
||||
if domain == "255.16.172.in-addr.arpa." {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Assert(found, check.Equals, true)
|
||||
}
|
||||
|
||||
func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
||||
n1, err := h.CreateNamespace("shared1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n2, err := h.CreateNamespace("shared2")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n3, err := h.CreateNamespace("shared3")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak1n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak2n2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak3n3, err := h.CreatePreAuthKey(n3.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak4n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
m1 := &Machine{
|
||||
ID: 1,
|
||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: n1.ID,
|
||||
Namespace: *n1,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.1",
|
||||
AuthKeyID: uint(pak1n1.ID),
|
||||
}
|
||||
h.db.Save(m1)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m2 := &Machine{
|
||||
ID: 2,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: n2.ID,
|
||||
Namespace: *n2,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.2",
|
||||
AuthKeyID: uint(pak2n2.ID),
|
||||
}
|
||||
h.db.Save(m2)
|
||||
|
||||
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m3 := &Machine{
|
||||
ID: 3,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_3",
|
||||
NamespaceID: n3.ID,
|
||||
Namespace: *n3,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.3",
|
||||
AuthKeyID: uint(pak3n3.ID),
|
||||
}
|
||||
h.db.Save(m3)
|
||||
|
||||
_, err = h.GetMachine(n3.Name, m3.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m4 := &Machine{
|
||||
ID: 4,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: n1.ID,
|
||||
Namespace: *n1,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.4",
|
||||
AuthKeyID: uint(pak4n1.ID),
|
||||
}
|
||||
h.db.Save(m4)
|
||||
|
||||
err = h.AddSharedMachineToNamespace(m2, n1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
baseDomain := "foobar.headscale.net"
|
||||
dnsConfigOrig := tailcfg.DNSConfig{
|
||||
Routes: make(map[string][]dnstype.Resolver),
|
||||
Domains: []string{baseDomain},
|
||||
Proxied: true,
|
||||
}
|
||||
|
||||
m1peers, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
dnsConfig, err := getMapResponseDNSConfig(&dnsConfigOrig, baseDomain, *m1, m1peers)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(dnsConfig, check.NotNil)
|
||||
c.Assert(len(dnsConfig.Routes), check.Equals, 2)
|
||||
|
||||
routeN1 := fmt.Sprintf("%s.%s", n1.Name, baseDomain)
|
||||
_, ok := dnsConfig.Routes[routeN1]
|
||||
c.Assert(ok, check.Equals, true)
|
||||
|
||||
routeN2 := fmt.Sprintf("%s.%s", n2.Name, baseDomain)
|
||||
_, ok = dnsConfig.Routes[routeN2]
|
||||
c.Assert(ok, check.Equals, true)
|
||||
|
||||
routeN3 := fmt.Sprintf("%s.%s", n3.Name, baseDomain)
|
||||
_, ok = dnsConfig.Routes[routeN3]
|
||||
c.Assert(ok, check.Equals, false)
|
||||
}
|
||||
|
||||
func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
||||
n1, err := h.CreateNamespace("shared1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n2, err := h.CreateNamespace("shared2")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n3, err := h.CreateNamespace("shared3")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak1n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak2n2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak3n3, err := h.CreatePreAuthKey(n3.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak4n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
m1 := &Machine{
|
||||
ID: 1,
|
||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: n1.ID,
|
||||
Namespace: *n1,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.1",
|
||||
AuthKeyID: uint(pak1n1.ID),
|
||||
}
|
||||
h.db.Save(m1)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m2 := &Machine{
|
||||
ID: 2,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: n2.ID,
|
||||
Namespace: *n2,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.2",
|
||||
AuthKeyID: uint(pak2n2.ID),
|
||||
}
|
||||
h.db.Save(m2)
|
||||
|
||||
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m3 := &Machine{
|
||||
ID: 3,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_3",
|
||||
NamespaceID: n3.ID,
|
||||
Namespace: *n3,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.3",
|
||||
AuthKeyID: uint(pak3n3.ID),
|
||||
}
|
||||
h.db.Save(m3)
|
||||
|
||||
_, err = h.GetMachine(n3.Name, m3.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m4 := &Machine{
|
||||
ID: 4,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: n1.ID,
|
||||
Namespace: *n1,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.4",
|
||||
AuthKeyID: uint(pak4n1.ID),
|
||||
}
|
||||
h.db.Save(m4)
|
||||
|
||||
err = h.AddSharedMachineToNamespace(m2, n1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
baseDomain := "foobar.headscale.net"
|
||||
dnsConfigOrig := tailcfg.DNSConfig{
|
||||
Routes: make(map[string][]dnstype.Resolver),
|
||||
Domains: []string{baseDomain},
|
||||
Proxied: false,
|
||||
}
|
||||
|
||||
m1peers, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
dnsConfig, err := getMapResponseDNSConfig(&dnsConfigOrig, baseDomain, *m1, m1peers)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(dnsConfig, check.NotNil)
|
||||
c.Assert(len(dnsConfig.Routes), check.Equals, 0)
|
||||
c.Assert(len(dnsConfig.Domains), check.Equals, 1)
|
||||
}
|
80
docs/Configuration.md
Normal file
80
docs/Configuration.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Configuration reference
|
||||
|
||||
Headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order:
|
||||
|
||||
- `/etc/headscale`
|
||||
- `~/.headscale`
|
||||
- current working directory
|
||||
|
||||
```yaml
|
||||
server_url: http://headscale.mydomain.net
|
||||
listen_addr: 0.0.0.0:8080
|
||||
ip_prefix: 100.64.0.0/10
|
||||
disable_check_updates: false
|
||||
```
|
||||
|
||||
`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. `ip_prefix` is the IP prefix (range) in which IP addresses for nodes will be allocated (default 100.64.0.0/10, e.g., 192.168.4.0/24, 10.0.0.0/8). `disable_check_updates` disables the automatic check for updates.
|
||||
|
||||
```yaml
|
||||
log_level: debug
|
||||
```
|
||||
|
||||
`log_level` can be used to set the Log level for Headscale, it defaults to `debug`, and the available levels are: `trace`, `debug`, `info`, `warn` and `error`.
|
||||
|
||||
```yaml
|
||||
private_key_path: private.key
|
||||
```
|
||||
|
||||
`private_key_path` is the path to the Wireguard private key. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||
|
||||
```yaml
|
||||
derp_map_path: derp.yaml
|
||||
```
|
||||
|
||||
`derp_map_path` is the path to the [DERP](https://pkg.go.dev/tailscale.com/derp) map file. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||
|
||||
```yaml
|
||||
ephemeral_node_inactivity_timeout": "30m"
|
||||
```
|
||||
|
||||
`ephemeral_node_inactivity_timeout` is the timeout after which inactive ephemeral node records will be deleted from the database. The default is 30 minutes. This value must be higher than 65 seconds (the keepalive timeout for the HTTP long poll is 60 seconds, plus a few seconds to avoid race conditions).
|
||||
|
||||
PostgresSQL
|
||||
|
||||
```yaml
|
||||
db_host: localhost
|
||||
db_port: 5432
|
||||
db_name: headscale
|
||||
db_user: foo
|
||||
db_pass: bar
|
||||
```
|
||||
|
||||
SQLite
|
||||
|
||||
```yaml
|
||||
db_type: sqlite3
|
||||
db_path: db.sqlite
|
||||
```
|
||||
|
||||
The fields starting with `db_` are used for the DB connection information.
|
||||
|
||||
### TLS configuration
|
||||
|
||||
Please check [`TLS.md`](TLS.md).
|
||||
|
||||
### DNS configuration
|
||||
|
||||
Please refer to [`DNS.md`](DNS.md).
|
||||
|
||||
### Policy ACLs
|
||||
|
||||
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
|
||||
|
||||
For instance, instead of referring to users when defining groups you must
|
||||
use namespaces (which are the equivalent to user/logins in Tailscale.com).
|
||||
|
||||
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
|
||||
|
||||
### Apple devices
|
||||
|
||||
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.
|
38
docs/DNS.md
Normal file
38
docs/DNS.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# DNS in headscale
|
||||
|
||||
headscale supports Tailscale's DNS configuration and MagicDNS. Please have a look to their KB to better understand what this means:
|
||||
|
||||
- https://tailscale.com/kb/1054/dns/
|
||||
- https://tailscale.com/kb/1081/magicdns/
|
||||
- https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
|
||||
|
||||
Long story short, you can define the DNS servers you want to use in your tailnets, activate MagicDNS (so you don't have to remember the IP addresses of your nodes), define search domains, as well as predefined hosts. headscale will inject that settings into your nodes.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
The setup is done via the `config.yaml` file, under the `dns_config` key.
|
||||
|
||||
```yaml
|
||||
server_url: http://127.0.0.1:8001
|
||||
listen_addr: 0.0.0.0:8001
|
||||
private_key_path: private.key
|
||||
dns_config:
|
||||
nameservers:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
restricted_nameservers:
|
||||
foo.bar.com:
|
||||
- 1.1.1.1
|
||||
darp.headscale.net:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
domains: []
|
||||
magic_dns: true
|
||||
base_domain: example.com
|
||||
```
|
||||
|
||||
- `nameservers`: The list of DNS servers to use.
|
||||
- `domains`: Search domains to inject.
|
||||
- `magic_dns`: Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). Only works if there is at least a nameserver defined.
|
||||
- `base_domain`: Defines the base domain to create the hostnames for MagicDNS. `base_domain` must be a FQDNs, without the trailing dot. The FQDN of the hosts will be `hostname.namespace.base_domain` (e.g., _myhost.mynamespace.example.com_).
|
||||
- `restricted_nameservers`: Split DNS (see https://tailscale.com/kb/1054/dns/), list of search domains and the DNS to query for each one.
|
3
docs/Glossary.md
Normal file
3
docs/Glossary.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Glossary
|
||||
|
||||
- Namespace: Collection of Tailscale nodes that can see each other. In Tailscale.com this is called Tailnet.
|
149
docs/Running.md
Normal file
149
docs/Running.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Running headscale
|
||||
|
||||
1. Download the headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your $PATH or use the docker container
|
||||
|
||||
```shell
|
||||
docker pull headscale/headscale:x.x.x
|
||||
```
|
||||
|
||||
<!--
|
||||
or
|
||||
```shell
|
||||
docker pull ghrc.io/juanfont/headscale:x.x.x
|
||||
``` -->
|
||||
|
||||
2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running
|
||||
|
||||
```shell
|
||||
docker run --name headscale \
|
||||
-e POSTGRES_DB=headscale
|
||||
-e POSTGRES_USER=foo \
|
||||
-e POSTGRES_PASSWORD=bar \
|
||||
-p 5432:5432 \
|
||||
-d postgres
|
||||
```
|
||||
|
||||
3. Create a WireGuard private key and headscale configuration
|
||||
|
||||
```shell
|
||||
wg genkey > private.key
|
||||
|
||||
cp config.yaml.example config.yaml
|
||||
```
|
||||
|
||||
4. Create a namespace
|
||||
|
||||
```shell
|
||||
headscale namespaces create myfirstnamespace
|
||||
```
|
||||
|
||||
or docker:
|
||||
|
||||
the db.sqlite mount is only needed if you use sqlite
|
||||
|
||||
```shell
|
||||
touch db.sqlite
|
||||
docker run \
|
||||
-v $(pwd)/private.key:/private.key \
|
||||
-v $(pwd)/config.json:/config.json \
|
||||
-v $(pwd)/derp.yaml:/derp.yaml \
|
||||
-v $(pwd)/db.sqlite:/db.sqlite \
|
||||
-p 127.0.0.1:8080:8080 \
|
||||
headscale/headscale:x.x.x \
|
||||
headscale namespaces create myfirstnamespace
|
||||
```
|
||||
|
||||
or if your server is already running in docker:
|
||||
|
||||
```shell
|
||||
docker exec <container_name> headscale create myfirstnamespace
|
||||
```
|
||||
|
||||
5. Run the server
|
||||
|
||||
```shell
|
||||
headscale serve
|
||||
```
|
||||
|
||||
or docker:
|
||||
|
||||
the db.sqlite mount is only needed if you use sqlite
|
||||
|
||||
```shell
|
||||
docker run \
|
||||
-v $(pwd)/private.key:/private.key \
|
||||
-v $(pwd)/config.json:/config.json \
|
||||
-v $(pwd)/derp.yaml:/derp.yaml \
|
||||
-v $(pwd)/db.sqlite:/db.sqlite \
|
||||
-p 127.0.0.1:8080:8080 \
|
||||
headscale/headscale:x.x.x headscale serve
|
||||
```
|
||||
|
||||
6. If you used tailscale.com before in your nodes, make sure you clear the tailscaled data folder
|
||||
|
||||
```shell
|
||||
systemctl stop tailscaled
|
||||
rm -fr /var/lib/tailscale
|
||||
systemctl start tailscaled
|
||||
```
|
||||
|
||||
7. Add your first machine
|
||||
|
||||
```shell
|
||||
tailscale up --login-server YOUR_HEADSCALE_URL
|
||||
```
|
||||
|
||||
8. Navigate to the URL you will get with `tailscale up`, where you'll find your machine key.
|
||||
|
||||
9. In the server, register your machine to a namespace with the CLI
|
||||
```shell
|
||||
headscale -n myfirstnamespace nodes register YOURMACHINEKEY
|
||||
```
|
||||
or docker:
|
||||
```shell
|
||||
docker run \
|
||||
-v $(pwd)/private.key:/private.key \
|
||||
-v $(pwd)/config.json:/config.json \
|
||||
-v $(pwd)/derp.yaml:/derp.yaml \
|
||||
headscale/headscale:x.x.x \
|
||||
headscale -n myfirstnamespace nodes register YOURMACHINEKEY
|
||||
```
|
||||
or if your server is already running in docker:
|
||||
```shell
|
||||
docker exec <container_name> headscale -n myfirstnamespace nodes register YOURMACHINEKEY
|
||||
```
|
||||
|
||||
Alternatively, you can use Auth Keys to register your machines:
|
||||
|
||||
1. Create an authkey
|
||||
|
||||
```shell
|
||||
headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
|
||||
```
|
||||
|
||||
or docker:
|
||||
|
||||
```shell
|
||||
docker run \
|
||||
-v $(pwd)/private.key:/private.key \
|
||||
-v $(pwd)/config.json:/config.json \
|
||||
-v$(pwd)/derp.yaml:/derp.yaml \
|
||||
-v $(pwd)/db.sqlite:/db.sqlite \
|
||||
headscale/headscale:x.x.x \
|
||||
headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
|
||||
```
|
||||
|
||||
or if your server is already running in docker:
|
||||
|
||||
```shell
|
||||
docker exec <container_name> headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
|
||||
```
|
||||
|
||||
2. Use the authkey from your machine to register it
|
||||
```shell
|
||||
tailscale up --login-server YOUR_HEADSCALE_URL --authkey YOURAUTHKEY
|
||||
```
|
||||
|
||||
If you create an authkey with the `--ephemeral` flag, that key will create ephemeral nodes. This implies that `--reusable` is true.
|
||||
|
||||
Please bear in mind that all headscale commands support adding `-o json` or `-o json-line` to get nicely JSON-formatted output.
|
27
docs/TLS.md
Normal file
27
docs/TLS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Running the service via TLS (optional)
|
||||
|
||||
```yaml
|
||||
tls_letsencrypt_hostname: ""
|
||||
tls_letsencrypt_listen: ":http"
|
||||
tls_letsencrypt_cache_dir: ".cache"
|
||||
tls_letsencrypt_challenge_type: HTTP-01
|
||||
```
|
||||
|
||||
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from. The certificate will automatically be renewed as needed.
|
||||
|
||||
```yaml
|
||||
tls_cert_path: ""
|
||||
tls_key_path: ""
|
||||
```
|
||||
|
||||
headscale can also be configured to expose its web service via TLS. To configure the certificate and key file manually, set the `tls_cert_path` and `tls_cert_path` configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||
|
||||
## Challenge type HTTP-01
|
||||
|
||||
The default challenge type `HTTP-01` requires that headscale is reachable on port 80 for the Let's Encrypt automated validation, in addition to whatever port is configured in `listen_addr`. By default, headscale listens on port 80 on all local IPs for Let's Encrypt automated validation.
|
||||
|
||||
If you need to change the ip and/or port used by headscale for the Let's Encrypt validation process, set `tls_letsencrypt_listen` to the appropriate value. This can be handy if you are running headscale as a non-root user (or can't run `setcap`). Keep in mind, however, that Let's Encrypt will _only_ connect to port 80 for the validation callback, so if you change `tls_letsencrypt_listen` you will also need to configure something else (e.g. a firewall rule) to forward the traffic from port 80 to the ip:port combination specified in `tls_letsencrypt_listen`.
|
||||
|
||||
## Challenge type TLS-ALPN-01
|
||||
|
||||
Alternatively, `tls_letsencrypt_challenge_type` can be set to `TLS-ALPN-01`. In this configuration, headscale listens on the ip:port combination defined in `listen_addr`. Let's Encrypt will _only_ connect to port 443 for the validation callback, so if `listen_addr` is not set to port 443, something else (e.g. a firewall rule) will be required to forward the traffic from port 443 to the ip:port combination specified in `listen_addr`.
|
4
go.mod
4
go.mod
@@ -21,7 +21,7 @@ require (
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/opencontainers/runc v1.0.2 // indirect
|
||||
github.com/ory/dockertest/v3 v3.7.0
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/pterm/pterm v0.12.30
|
||||
github.com/rs/zerolog v1.25.0
|
||||
github.com/spf13/cobra v1.2.1
|
||||
@@ -30,7 +30,7 @@ require (
|
||||
github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/zsais/go-gin-prometheus v0.1.0 // indirect
|
||||
github.com/zsais/go-gin-prometheus v0.1.0
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect
|
||||
|
@@ -230,7 +230,6 @@ func (s *IntegrationTestSuite) SetupSuite() {
|
||||
Name: "headscale",
|
||||
Mounts: []string{
|
||||
fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath),
|
||||
fmt.Sprintf("%s/derp.yaml:/etc/headscale/derp.yaml", currentPath),
|
||||
},
|
||||
Networks: []*dockertest.Network{&network},
|
||||
Cmd: []string{"headscale", "serve"},
|
||||
@@ -289,7 +288,16 @@ func (s *IntegrationTestSuite) SetupSuite() {
|
||||
fmt.Printf("Creating pre auth key for %s\n", namespace)
|
||||
authKey, err := executeCommand(
|
||||
&headscale,
|
||||
[]string{"headscale", "--namespace", namespace, "preauthkeys", "create", "--reusable", "--expiration", "24h"},
|
||||
[]string{
|
||||
"headscale",
|
||||
"--namespace",
|
||||
namespace,
|
||||
"preauthkeys",
|
||||
"create",
|
||||
"--reusable",
|
||||
"--expiration",
|
||||
"24h",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
@@ -298,7 +306,16 @@ func (s *IntegrationTestSuite) SetupSuite() {
|
||||
|
||||
fmt.Printf("Joining tailscale containers to headscale at %s\n", headscaleEndpoint)
|
||||
for hostname, tailscale := range scales.tailscales {
|
||||
command := []string{"tailscale", "up", "-login-server", headscaleEndpoint, "--authkey", strings.TrimSuffix(authKey, "\n"), "--hostname", hostname}
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"up",
|
||||
"-login-server",
|
||||
headscaleEndpoint,
|
||||
"--authkey",
|
||||
strings.TrimSuffix(authKey, "\n"),
|
||||
"--hostname",
|
||||
hostname,
|
||||
}
|
||||
|
||||
fmt.Println("Join command:", command)
|
||||
fmt.Printf("Running join command for %s\n", hostname)
|
||||
@@ -504,43 +521,43 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
|
||||
assert.Contains(s.T(), result, hostname)
|
||||
}
|
||||
|
||||
// TODO(kradalby): Figure out why these connections are not set up
|
||||
// // TODO: See if we can have a more deterministic wait here.
|
||||
// time.Sleep(100 * time.Second)
|
||||
// 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
|
||||
|
||||
// mainIps, err := getIPs(main.tailscales)
|
||||
// assert.Nil(s.T(), err)
|
||||
mainIps, err := getIPs(main.tailscales)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
// sharedIps, err := getIPs(shared.tailscales)
|
||||
// assert.Nil(s.T(), err)
|
||||
sharedIps, err := getIPs(shared.tailscales)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
// for hostname, tailscale := range main.tailscales {
|
||||
// for peername, ip := range sharedIps {
|
||||
// s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
||||
// // We currently cant ping ourselves, so skip that.
|
||||
// if peername != hostname {
|
||||
// // We are only interested in "direct ping" which means what we
|
||||
// // might need a couple of more attempts before reaching the node.
|
||||
// command := []string{
|
||||
// "tailscale", "ping",
|
||||
// "--timeout=1s",
|
||||
// "--c=20",
|
||||
// "--until-direct=true",
|
||||
// ip.String(),
|
||||
// }
|
||||
for hostname, tailscale := range main.tailscales {
|
||||
for peername, ip := range sharedIps {
|
||||
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
||||
// We currently cant ping ourselves, so skip that.
|
||||
if peername != hostname {
|
||||
// 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 (%s) to %s (%s)\n", hostname, mainIps[hostname], peername, ip)
|
||||
// result, err := executeCommand(
|
||||
// &tailscale,
|
||||
// command,
|
||||
// )
|
||||
// assert.Nil(t, err)
|
||||
// fmt.Printf("Result for %s: %s\n", hostname, result)
|
||||
// assert.Contains(t, result, "pong")
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, mainIps[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() {
|
||||
@@ -592,7 +609,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
|
||||
_, err = executeCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{"ALL_PROXY=socks5://localhost:1055/"},
|
||||
[]string{"ALL_PROXY=socks5://localhost:1055"},
|
||||
)
|
||||
if err == nil {
|
||||
break
|
||||
@@ -645,6 +662,44 @@ func (s *IntegrationTestSuite) TestTailDrop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestMagicDNS() {
|
||||
for namespace, scales := range s.namespaces {
|
||||
ips, err := getIPs(scales.tailscales)
|
||||
assert.Nil(s.T(), err)
|
||||
for hostname, tailscale := range scales.tailscales {
|
||||
for peername, ip := range ips {
|
||||
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
||||
if peername != hostname {
|
||||
command := []string{
|
||||
"tailscale", "ping",
|
||||
"--timeout=10s",
|
||||
"--c=20",
|
||||
"--until-direct=true",
|
||||
fmt.Sprintf("%s.%s.headscale.net", peername, namespace),
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
"Pinging using Hostname (magicdns) from %s (%s) to %s (%s)\n",
|
||||
hostname,
|
||||
ips[hostname],
|
||||
peername,
|
||||
ip,
|
||||
)
|
||||
result, err := executeCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("Result for %s: %s\n", hostname, result)
|
||||
assert.Contains(t, result, "pong")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, error) {
|
||||
ips := make(map[string]netaddr.IP)
|
||||
for hostname, tailscale := range tailscales {
|
||||
|
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"server_url": "http://headscale:8080",
|
||||
"listen_addr": "0.0.0.0:8080",
|
||||
"private_key_path": "private.key",
|
||||
"derp_map_path": "derp.yaml",
|
||||
"ephemeral_node_inactivity_timeout": "30m",
|
||||
"db_type": "sqlite3",
|
||||
"db_path": "/tmp/integration_test_db.sqlite3",
|
||||
"acl_policy_path": "",
|
||||
"log_level": "trace"
|
||||
}
|
20
integration_test/etc/config.yaml
Normal file
20
integration_test/etc/config.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
log_level: trace
|
||||
acl_policy_path: ""
|
||||
db_type: sqlite3
|
||||
ephemeral_node_inactivity_timeout: 30m
|
||||
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:8080
|
||||
server_url: http://headscale:8080
|
||||
|
||||
derp:
|
||||
urls:
|
||||
- https://controlplane.tailscale.com/derpmap/default
|
||||
auto_update_enabled: false
|
||||
update_frequency: 1m
|
@@ -1,7 +1,7 @@
|
||||
# Deploying Headscale on Kubernetes
|
||||
# Deploying headscale on Kubernetes
|
||||
|
||||
This directory contains [Kustomize](https://kustomize.io) templates that deploy
|
||||
Headscale in various configurations.
|
||||
headscale in various configurations.
|
||||
|
||||
These templates currently support Rancher k3s. Other clusters may require
|
||||
adaptation, especially around volume claims and ingress.
|
||||
@@ -72,10 +72,10 @@ Usage:
|
||||
|
||||
Available Commands:
|
||||
help Help about any command
|
||||
namespace Manage the namespaces of Headscale
|
||||
node Manage the nodes of Headscale
|
||||
preauthkey Handle the preauthkeys in Headscale
|
||||
routes Manage the routes of Headscale
|
||||
namespace Manage the namespaces of headscale
|
||||
node Manage the nodes of headscale
|
||||
preauthkey Handle the preauthkeys in headscale
|
||||
routes Manage the routes of headscale
|
||||
serve Launches the headscale server
|
||||
version Print the version.
|
||||
|
||||
|
76
machine.go
76
machine.go
@@ -63,7 +63,7 @@ func (h *Headscale) getDirectPeers(m *Machine) (Machines, error) {
|
||||
Msg("Finding direct peers")
|
||||
|
||||
machines := Machines{}
|
||||
if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered",
|
||||
if err := h.db.Preload("Namespace").Where("namespace_id = ? AND machine_key <> ? AND registered",
|
||||
m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil {
|
||||
log.Error().Err(err).Msg("Error accessing db")
|
||||
return Machines{}, err
|
||||
@@ -78,15 +78,15 @@ func (h *Headscale) getDirectPeers(m *Machine) (Machines, error) {
|
||||
return machines, nil
|
||||
}
|
||||
|
||||
// getShared fetches machines that are shared to the `Namespace` of the machine we are getting peers for
|
||||
func (h *Headscale) getShared(m *Machine) (Machines, error) {
|
||||
log.Trace().
|
||||
Str("func", "getShared").
|
||||
Str("machine", m.Name).
|
||||
Msg("Finding shared peers")
|
||||
|
||||
// We fetch here machines that are shared to the `Namespace` of the machine we are getting peers for
|
||||
sharedMachines := []SharedMachine{}
|
||||
if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?",
|
||||
if err := h.db.Preload("Namespace").Preload("Machine").Preload("Machine.Namespace").Where("namespace_id = ?",
|
||||
m.NamespaceID).Find(&sharedMachines).Error; err != nil {
|
||||
return Machines{}, err
|
||||
}
|
||||
@@ -105,6 +105,37 @@ func (h *Headscale) getShared(m *Machine) (Machines, error) {
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
// getSharedTo fetches the machines of the namespaces this machine is shared in
|
||||
func (h *Headscale) getSharedTo(m *Machine) (Machines, error) {
|
||||
log.Trace().
|
||||
Str("func", "getSharedTo").
|
||||
Str("machine", m.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 = ?",
|
||||
m.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 err != nil {
|
||||
return Machines{}, err
|
||||
}
|
||||
peers = append(peers, *namespaceMachines...)
|
||||
}
|
||||
|
||||
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
|
||||
|
||||
log.Trace().
|
||||
Str("func", "getSharedTo").
|
||||
Str("machine", m.Name).
|
||||
Msgf("Found peers we are shared with: %s", peers.String())
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) getPeers(m *Machine) (Machines, error) {
|
||||
direct, err := h.getDirectPeers(m)
|
||||
if err != nil {
|
||||
@@ -118,13 +149,24 @@ func (h *Headscale) getPeers(m *Machine) (Machines, error) {
|
||||
shared, err := h.getShared(m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "getDirectPeers").
|
||||
Str("func", "getShared").
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
sharedTo, err := h.getSharedTo(m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "sharedTo").
|
||||
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().
|
||||
@@ -179,6 +221,11 @@ func (h *Headscale) UpdateMachine(m *Machine) error {
|
||||
|
||||
// DeleteMachine softs deletes a Machine from the database
|
||||
func (h *Headscale) DeleteMachine(m *Machine) error {
|
||||
err := h.RemoveSharedMachineFromAllNamespaces(m)
|
||||
if err != nil && err != errorMachineNotShared {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Registered = false
|
||||
namespaceID := m.NamespaceID
|
||||
h.db.Save(&m) // we mark it as unregistered, just in case
|
||||
@@ -191,10 +238,16 @@ func (h *Headscale) DeleteMachine(m *Machine) error {
|
||||
|
||||
// HardDeleteMachine hard deletes a Machine from the database
|
||||
func (h *Headscale) HardDeleteMachine(m *Machine) error {
|
||||
err := h.RemoveSharedMachineFromAllNamespaces(m)
|
||||
if err != nil && err != errorMachineNotShared {
|
||||
return err
|
||||
}
|
||||
|
||||
namespaceID := m.NamespaceID
|
||||
if err := h.db.Unscoped().Delete(&m).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.RequestMapUpdates(namespaceID)
|
||||
}
|
||||
|
||||
@@ -273,11 +326,11 @@ func (ms MachinesP) String() string {
|
||||
return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp))
|
||||
}
|
||||
|
||||
func (ms Machines) toNodes(includeRoutes bool) ([]*tailcfg.Node, error) {
|
||||
func (ms Machines) toNodes(baseDomain string, dnsConfig *tailcfg.DNSConfig, includeRoutes bool) ([]*tailcfg.Node, error) {
|
||||
nodes := make([]*tailcfg.Node, len(ms))
|
||||
|
||||
for index, machine := range ms {
|
||||
node, err := machine.toNode(includeRoutes)
|
||||
node, err := machine.toNode(baseDomain, dnsConfig, includeRoutes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -290,7 +343,7 @@ func (ms Machines) toNodes(includeRoutes bool) ([]*tailcfg.Node, error) {
|
||||
|
||||
// toNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes
|
||||
// as per the expected behaviour in the official SaaS
|
||||
func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) {
|
||||
func (m Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, includeRoutes bool) (*tailcfg.Node, error) {
|
||||
nKey, err := wgkey.ParseHex(m.NodeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -385,10 +438,17 @@ func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) {
|
||||
keyExpiry = time.Time{}
|
||||
}
|
||||
|
||||
var hostname string
|
||||
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS
|
||||
hostname = fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, baseDomain)
|
||||
} else {
|
||||
hostname = m.Name
|
||||
}
|
||||
|
||||
n := tailcfg.Node{
|
||||
ID: tailcfg.NodeID(m.ID), // this is the actual ID
|
||||
StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)), // in headscale, unlike tailcontrol server, IDs are permanent
|
||||
Name: hostinfo.Hostname,
|
||||
Name: hostname,
|
||||
User: tailcfg.UserID(m.NamespaceID),
|
||||
Key: tailcfg.NodeKey(nKey),
|
||||
KeyExpiry: keyExpiry,
|
||||
|
@@ -59,6 +59,35 @@ func (h *Headscale) DestroyNamespace(name string) error {
|
||||
}
|
||||
|
||||
if result := h.db.Unscoped().Delete(&n); result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
n, err := h.GetNamespace(oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = h.GetNamespace(newName)
|
||||
if err == nil {
|
||||
return errorNamespaceExists
|
||||
}
|
||||
if !errors.Is(err, errorNamespaceNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
n.Name = newName
|
||||
|
||||
if result := h.db.Save(&n); result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
err = h.RequestMapUpdates(n.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -91,7 +120,7 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) {
|
||||
}
|
||||
|
||||
machines := []Machine{}
|
||||
if err := h.db.Preload("AuthKey").Preload("Namespace").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
|
||||
if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &machines, nil
|
||||
@@ -216,3 +245,22 @@ func (n *Namespace) toUser() *tailcfg.User {
|
||||
}
|
||||
return &u
|
||||
}
|
||||
|
||||
func getMapResponseUserProfiles(m Machine, peers Machines) []tailcfg.UserProfile {
|
||||
namespaceMap := make(map[string]Namespace)
|
||||
namespaceMap[m.Namespace.Name] = m.Namespace
|
||||
for _, p := range peers {
|
||||
namespaceMap[p.Namespace.Name] = p.Namespace // not worth checking if already is there
|
||||
}
|
||||
|
||||
profiles := []tailcfg.UserProfile{}
|
||||
for _, namespace := range namespaceMap {
|
||||
profiles = append(profiles,
|
||||
tailcfg.UserProfile{
|
||||
ID: tailcfg.UserID(namespace.ID),
|
||||
LoginName: namespace.Name,
|
||||
DisplayName: namespace.Name,
|
||||
})
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
@@ -46,3 +47,155 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
|
||||
err = h.DestroyNamespace("test")
|
||||
c.Assert(err, check.Equals, errorNamespaceNotEmpty)
|
||||
}
|
||||
|
||||
func (s *Suite) TestRenameNamespace(c *check.C) {
|
||||
n, err := h.CreateNamespace("test")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(n.Name, check.Equals, "test")
|
||||
|
||||
ns, err := h.ListNamespaces()
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(*ns), check.Equals, 1)
|
||||
|
||||
err = h.RenameNamespace("test", "test_renamed")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetNamespace("test")
|
||||
c.Assert(err, check.Equals, errorNamespaceNotFound)
|
||||
|
||||
_, err = h.GetNamespace("test_renamed")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
err = h.RenameNamespace("test_does_not_exit", "test")
|
||||
c.Assert(err, check.Equals, errorNamespaceNotFound)
|
||||
|
||||
n2, err := h.CreateNamespace("test2")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(n2.Name, check.Equals, "test2")
|
||||
|
||||
err = h.RenameNamespace("test2", "test_renamed")
|
||||
c.Assert(err, check.Equals, errorNamespaceExists)
|
||||
}
|
||||
|
||||
func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
|
||||
n1, err := h.CreateNamespace("shared1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n2, err := h.CreateNamespace("shared2")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n3, err := h.CreateNamespace("shared3")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak1n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak2n2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak3n3, err := h.CreatePreAuthKey(n3.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak4n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
m1 := &Machine{
|
||||
ID: 1,
|
||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: n1.ID,
|
||||
Namespace: *n1,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.1",
|
||||
AuthKeyID: uint(pak1n1.ID),
|
||||
}
|
||||
h.db.Save(m1)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m2 := &Machine{
|
||||
ID: 2,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: n2.ID,
|
||||
Namespace: *n2,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.2",
|
||||
AuthKeyID: uint(pak2n2.ID),
|
||||
}
|
||||
h.db.Save(m2)
|
||||
|
||||
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m3 := &Machine{
|
||||
ID: 3,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_3",
|
||||
NamespaceID: n3.ID,
|
||||
Namespace: *n3,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.3",
|
||||
AuthKeyID: uint(pak3n3.ID),
|
||||
}
|
||||
h.db.Save(m3)
|
||||
|
||||
_, err = h.GetMachine(n3.Name, m3.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m4 := &Machine{
|
||||
ID: 4,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: n1.ID,
|
||||
Namespace: *n1,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.4",
|
||||
AuthKeyID: uint(pak4n1.ID),
|
||||
}
|
||||
h.db.Save(m4)
|
||||
|
||||
err = h.AddSharedMachineToNamespace(m2, n1)
|
||||
c.Assert(err, check.IsNil)
|
||||
m1peers, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
userProfiles := getMapResponseUserProfiles(*m1, m1peers)
|
||||
|
||||
log.Trace().Msgf("userProfiles %#v", userProfiles)
|
||||
c.Assert(len(userProfiles), check.Equals, 2)
|
||||
|
||||
found := false
|
||||
for _, up := range userProfiles {
|
||||
if up.DisplayName == n1.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Assert(found, check.Equals, true)
|
||||
|
||||
found = false
|
||||
for _, up := range userProfiles {
|
||||
if up.DisplayName == n2.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Assert(found, check.Equals, true)
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
const errorAuthKeyNotFound = Error("AuthKey not found")
|
||||
const errorAuthKeyExpired = Error("AuthKey expired")
|
||||
const errorAuthKeyNotReusableAlreadyUsed = Error("AuthKey not reusable already used")
|
||||
const errSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used")
|
||||
|
||||
// PreAuthKey describes a pre-authorization key usable in a particular namespace
|
||||
type PreAuthKey struct {
|
||||
@@ -21,6 +21,7 @@ type PreAuthKey struct {
|
||||
Namespace Namespace
|
||||
Reusable bool
|
||||
Ephemeral bool `gorm:"default:false"`
|
||||
Used bool `gorm:"default:false"`
|
||||
|
||||
CreatedAt *time.Time
|
||||
Expiration *time.Time
|
||||
@@ -110,11 +111,10 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(machines) != 0 {
|
||||
return nil, errorAuthKeyNotReusableAlreadyUsed
|
||||
if len(machines) != 0 || pak.Used {
|
||||
return nil, errSingleUseAuthKeyHasBeenUsed
|
||||
}
|
||||
|
||||
// missing here validation on current usage
|
||||
return &pak, nil
|
||||
}
|
||||
|
||||
|
@@ -87,7 +87,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
|
||||
h.db.Save(&m)
|
||||
|
||||
p, err := h.checkKeyValidity(pak.Key)
|
||||
c.Assert(err, check.Equals, errorAuthKeyNotReusableAlreadyUsed)
|
||||
c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed)
|
||||
c.Assert(p, check.IsNil)
|
||||
}
|
||||
|
||||
@@ -180,3 +180,16 @@ func (*Suite) TestExpirePreauthKey(c *check.C) {
|
||||
c.Assert(err, check.Equals, errorAuthKeyExpired)
|
||||
c.Assert(p, check.IsNil)
|
||||
}
|
||||
|
||||
func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
|
||||
n, err := h.CreateNamespace("test6")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
pak.Used = true
|
||||
h.db.Save(&pak)
|
||||
|
||||
_, err = h.checkKeyValidity(pak.Key)
|
||||
c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed)
|
||||
}
|
||||
|
44
sharing.go
44
sharing.go
@@ -4,6 +4,7 @@ import "gorm.io/gorm"
|
||||
|
||||
const errorSameNamespace = Error("Destination namespace same as origin")
|
||||
const errorMachineAlreadyShared = Error("Node already shared to this namespace")
|
||||
const errorMachineNotShared = Error("Machine not shared to this namespace")
|
||||
|
||||
// SharedMachine is a join table to support sharing nodes between namespaces
|
||||
type SharedMachine struct {
|
||||
@@ -20,12 +21,15 @@ func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error
|
||||
return errorSameNamespace
|
||||
}
|
||||
|
||||
sharedMachine := SharedMachine{}
|
||||
if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sharedMachine).Error; err == nil {
|
||||
sharedMachines := []SharedMachine{}
|
||||
if err := h.db.Where("machine_id = ? AND namespace_id = ?", m.ID, ns.ID).Find(&sharedMachines).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(sharedMachines) > 0 {
|
||||
return errorMachineAlreadyShared
|
||||
}
|
||||
|
||||
sharedMachine = SharedMachine{
|
||||
sharedMachine := SharedMachine{
|
||||
MachineID: m.ID,
|
||||
Machine: *m,
|
||||
NamespaceID: ns.ID,
|
||||
@@ -35,3 +39,37 @@ func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSharedMachineFromNamespace removes a shared machine from a namespace
|
||||
func (h *Headscale) RemoveSharedMachineFromNamespace(m *Machine, ns *Namespace) error {
|
||||
if m.NamespaceID == ns.ID {
|
||||
return errorSameNamespace
|
||||
}
|
||||
|
||||
sharedMachine := SharedMachine{}
|
||||
result := h.db.Where("machine_id = ? AND namespace_id = ?", m.ID, ns.ID).Unscoped().Delete(&sharedMachine)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return errorMachineNotShared
|
||||
}
|
||||
|
||||
err := h.RequestMapUpdates(ns.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSharedMachineFromAllNamespaces removes a machine as a shared node from all namespaces
|
||||
func (h *Headscale) RemoveSharedMachineFromAllNamespaces(m *Machine) error {
|
||||
sharedMachine := SharedMachine{}
|
||||
if result := h.db.Where("machine_id = ?", m.ID).Unscoped().Delete(&sharedMachine); result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
372
sharing_test.go
372
sharing_test.go
@@ -4,32 +4,26 @@ import (
|
||||
"gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) {
|
||||
n1, err := h.CreateNamespace("shared1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n2, err := h.CreateNamespace("shared2")
|
||||
func CreateNodeNamespace(c *check.C, namespace, node, key, IP string) (*Namespace, *Machine) {
|
||||
n1, err := h.CreateNamespace(namespace)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||
_, err = h.GetMachine(n1.Name, node)
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
m1 := &Machine{
|
||||
ID: 0,
|
||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
Name: "test_get_shared_nodes_1",
|
||||
MachineKey: key,
|
||||
NodeKey: key,
|
||||
DiscoKey: key,
|
||||
Name: node,
|
||||
NamespaceID: n1.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.1",
|
||||
IPAddress: IP,
|
||||
AuthKeyID: uint(pak1.ID),
|
||||
}
|
||||
h.db.Save(m1)
|
||||
@@ -37,22 +31,12 @@ func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) {
|
||||
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m2 := &Machine{
|
||||
ID: 1,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: n2.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.2",
|
||||
AuthKeyID: uint(pak2.ID),
|
||||
}
|
||||
h.db.Save(m2)
|
||||
return n1, m1
|
||||
}
|
||||
|
||||
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) {
|
||||
n1, m1 := CreateNodeNamespace(c, "shared1", "test_get_shared_nodes_1", "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", "100.64.0.1")
|
||||
_, m2 := CreateNodeNamespace(c, "shared2", "test_get_shared_nodes_2", "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", "100.64.0.2")
|
||||
|
||||
p1s, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
@@ -68,54 +52,7 @@ func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestSameNamespace(c *check.C) {
|
||||
n1, err := h.CreateNamespace("shared1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n2, err := h.CreateNamespace("shared2")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
m1 := &Machine{
|
||||
ID: 0,
|
||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: n1.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.1",
|
||||
AuthKeyID: uint(pak1.ID),
|
||||
}
|
||||
h.db.Save(m1)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m2 := &Machine{
|
||||
ID: 1,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: n2.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.2",
|
||||
AuthKeyID: uint(pak2.ID),
|
||||
}
|
||||
h.db.Save(m2)
|
||||
|
||||
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
n1, m1 := CreateNodeNamespace(c, "shared1", "test_get_shared_nodes_1", "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", "100.64.0.1")
|
||||
|
||||
p1s, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
@@ -125,55 +62,35 @@ func (s *Suite) TestSameNamespace(c *check.C) {
|
||||
c.Assert(err, check.Equals, errorSameNamespace)
|
||||
}
|
||||
|
||||
func (s *Suite) TestUnshare(c *check.C) {
|
||||
n1, m1 := CreateNodeNamespace(c, "shared1", "test_unshare_1", "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", "100.64.0.1")
|
||||
_, m2 := CreateNodeNamespace(c, "shared2", "test_unshare_2", "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", "100.64.0.2")
|
||||
|
||||
p1s, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(p1s), check.Equals, 0)
|
||||
|
||||
err = h.AddSharedMachineToNamespace(m2, n1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
p1s, err = h.getShared(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(p1s), check.Equals, 1)
|
||||
|
||||
err = h.RemoveSharedMachineFromNamespace(m2, n1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
p1s, err = h.getShared(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(p1s), check.Equals, 0)
|
||||
|
||||
err = h.RemoveSharedMachineFromNamespace(m2, n1)
|
||||
c.Assert(err, check.Equals, errorMachineNotShared)
|
||||
}
|
||||
|
||||
func (s *Suite) TestAlreadyShared(c *check.C) {
|
||||
n1, err := h.CreateNamespace("shared1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n2, err := h.CreateNamespace("shared2")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
m1 := &Machine{
|
||||
ID: 0,
|
||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: n1.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.1",
|
||||
AuthKeyID: uint(pak1.ID),
|
||||
}
|
||||
h.db.Save(m1)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m2 := &Machine{
|
||||
ID: 1,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: n2.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.2",
|
||||
AuthKeyID: uint(pak2.ID),
|
||||
}
|
||||
h.db.Save(m2)
|
||||
|
||||
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
n1, m1 := CreateNodeNamespace(c, "shared1", "test_get_shared_nodes_1", "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", "100.64.0.1")
|
||||
_, m2 := CreateNodeNamespace(c, "shared2", "test_get_shared_nodes_2", "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", "100.64.0.2")
|
||||
|
||||
p1s, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
@@ -186,54 +103,8 @@ func (s *Suite) TestAlreadyShared(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) {
|
||||
n1, err := h.CreateNamespace("shared1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n2, err := h.CreateNamespace("shared2")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
m1 := &Machine{
|
||||
ID: 0,
|
||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: n1.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.1",
|
||||
AuthKeyID: uint(pak1.ID),
|
||||
}
|
||||
h.db.Save(m1)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m2 := &Machine{
|
||||
ID: 1,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: n2.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.2",
|
||||
AuthKeyID: uint(pak2.ID),
|
||||
}
|
||||
h.db.Save(m2)
|
||||
|
||||
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
n1, m1 := CreateNodeNamespace(c, "shared1", "test_get_shared_nodes_1", "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", "100.64.0.1")
|
||||
_, m2 := CreateNodeNamespace(c, "shared2", "test_get_shared_nodes_2", "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", "100.64.0.2")
|
||||
|
||||
p1s, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
@@ -249,86 +120,18 @@ func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) {
|
||||
n1, err := h.CreateNamespace("shared1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n2, err := h.CreateNamespace("shared2")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
n3, err := h.CreateNamespace("shared3")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak3, err := h.CreatePreAuthKey(n3.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
n1, m1 := CreateNodeNamespace(c, "shared1", "test_get_shared_nodes_1", "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", "100.64.0.1")
|
||||
_, m2 := CreateNodeNamespace(c, "shared2", "test_get_shared_nodes_2", "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", "100.64.0.2")
|
||||
_, m3 := CreateNodeNamespace(c, "shared3", "test_get_shared_nodes_3", "6e704bee83eb93db6fc2c417d7882964cd3f8cc87082cbb645982e34020c76c8", "100.64.0.3")
|
||||
|
||||
pak4, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
m1 := &Machine{
|
||||
ID: 0,
|
||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||
Name: "test_get_shared_nodes_1",
|
||||
NamespaceID: n1.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.1",
|
||||
AuthKeyID: uint(pak1.ID),
|
||||
}
|
||||
h.db.Save(m1)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m2 := &Machine{
|
||||
ID: 1,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_2",
|
||||
NamespaceID: n2.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.2",
|
||||
AuthKeyID: uint(pak2.ID),
|
||||
}
|
||||
h.db.Save(m2)
|
||||
|
||||
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m3 := &Machine{
|
||||
ID: 2,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
Name: "test_get_shared_nodes_3",
|
||||
NamespaceID: n3.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.3",
|
||||
AuthKeyID: uint(pak3.ID),
|
||||
}
|
||||
h.db.Save(m3)
|
||||
|
||||
_, err = h.GetMachine(n3.Name, m3.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m4 := &Machine{
|
||||
ID: 3,
|
||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||
ID: 4,
|
||||
MachineKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
NodeKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
DiscoKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: n1.ID,
|
||||
Registered: true,
|
||||
@@ -341,10 +144,64 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) {
|
||||
_, err = h.GetMachine(n1.Name, m4.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
p1s, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(p1s), check.Equals, 1) // node1 can see node4
|
||||
c.Assert(p1s[0].Name, check.Equals, m4.Name)
|
||||
|
||||
err = h.AddSharedMachineToNamespace(m2, n1)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
p1sAfter, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(p1sAfter), check.Equals, 2) // node1 can see node2 (shared) and node4 (same namespace)
|
||||
c.Assert(p1sAfter[0].Name, check.Equals, m2.Name)
|
||||
c.Assert(p1sAfter[1].Name, check.Equals, m4.Name)
|
||||
|
||||
node1shared, err := h.getShared(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(node1shared), check.Equals, 1) // node1 can see node2 as shared
|
||||
c.Assert(node1shared[0].Name, check.Equals, m2.Name)
|
||||
|
||||
pAlone, err := h.getPeers(m3)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(pAlone), check.Equals, 0) // node3 is alone
|
||||
|
||||
pSharedTo, err := h.getPeers(m2)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(pSharedTo), check.Equals, 2) // node2 should see node1 (sharedTo) and node4 (sharedTo), as is shared in namespace1
|
||||
c.Assert(pSharedTo[0].Name, check.Equals, m1.Name)
|
||||
c.Assert(pSharedTo[1].Name, check.Equals, m4.Name)
|
||||
}
|
||||
|
||||
func (s *Suite) TestDeleteSharedMachine(c *check.C) {
|
||||
n1, m1 := CreateNodeNamespace(c, "shared1", "test_get_shared_nodes_1", "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", "100.64.0.1")
|
||||
_, m2 := CreateNodeNamespace(c, "shared2", "test_get_shared_nodes_2", "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", "100.64.0.2")
|
||||
_, m3 := CreateNodeNamespace(c, "shared3", "test_get_shared_nodes_3", "6e704bee83eb93db6fc2c417d7882964cd3f8cc87082cbb645982e34020c76c8", "100.64.0.3")
|
||||
|
||||
pak4n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
m4 := &Machine{
|
||||
ID: 4,
|
||||
MachineKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
NodeKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
DiscoKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
|
||||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: n1.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.4",
|
||||
AuthKeyID: uint(pak4n1.ID),
|
||||
}
|
||||
h.db.Save(m4)
|
||||
|
||||
_, err = h.GetMachine(n1.Name, m4.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
p1s, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(p1s), check.Equals, 1) // nodes 1 and 4
|
||||
c.Assert(p1s[0].Name, check.Equals, "test_get_shared_nodes_4")
|
||||
c.Assert(p1s[0].Name, check.Equals, m4.Name)
|
||||
|
||||
err = h.AddSharedMachineToNamespace(m2, n1)
|
||||
c.Assert(err, check.IsNil)
|
||||
@@ -352,15 +209,26 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) {
|
||||
p1sAfter, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(p1sAfter), check.Equals, 2) // nodes 1, 2, 4
|
||||
c.Assert(p1sAfter[0].Name, check.Equals, "test_get_shared_nodes_2")
|
||||
c.Assert(p1sAfter[1].Name, check.Equals, "test_get_shared_nodes_4")
|
||||
c.Assert(p1sAfter[0].Name, check.Equals, m2.Name)
|
||||
c.Assert(p1sAfter[1].Name, check.Equals, m4.Name)
|
||||
|
||||
node1shared, err := h.getShared(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(node1shared), check.Equals, 1) // nodes 1, 2, 4
|
||||
c.Assert(node1shared[0].Name, check.Equals, "test_get_shared_nodes_2")
|
||||
c.Assert(node1shared[0].Name, check.Equals, m2.Name)
|
||||
|
||||
pAlone, err := h.getPeers(m3)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(pAlone), check.Equals, 0) // node 3 is alone
|
||||
|
||||
sharedMachines, err := h.ListSharedMachinesInNamespace(n1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(*sharedMachines), check.Equals, 1)
|
||||
|
||||
err = h.DeleteMachine(m2)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
sharedMachines, err = h.ListSharedMachinesInNamespace(n1.Name)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(*sharedMachines), check.Equals, 0)
|
||||
}
|
||||
|
Reference in New Issue
Block a user