mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-15 09:07:46 +00:00
Compare commits
125 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
83815f567d | ||
![]() |
7db91c68be | ||
![]() |
109115c13b | ||
![]() |
11e0402396 | ||
![]() |
fd94105483 | ||
![]() |
96e8142540 | ||
![]() |
9615138202 | ||
![]() |
9900b215cc | ||
![]() |
d5ea224e11 | ||
![]() |
024d6ee7c3 | ||
![]() |
f653b00258 | ||
![]() |
ff1ee4ca64 | ||
![]() |
830aa250e1 | ||
![]() |
f0bbc3c7d8 | ||
![]() |
994b4eef17 | ||
![]() |
f905812afa | ||
![]() |
d68d201722 | ||
![]() |
da209e89a7 | ||
![]() |
7940dbc78b | ||
![]() |
4d22b4252f | ||
![]() |
bf26e37e0e | ||
![]() |
e154e7a0fb | ||
![]() |
b28ebb5d20 | ||
![]() |
5840f88251 | ||
![]() |
2c2968473a | ||
![]() |
8f1f48b7d0 | ||
![]() |
536e8b71bf | ||
![]() |
acc43c39af | ||
![]() |
eae1b6a3de | ||
![]() |
31cc61478f | ||
![]() |
3095c1e150 | ||
![]() |
e1d5da5bd9 | ||
![]() |
5f818b7349 | ||
![]() |
0aac79f8fa | ||
![]() |
1e93347a26 | ||
![]() |
18867a4c84 | ||
![]() |
3b97c7bdec | ||
![]() |
203e6bc6b2 | ||
![]() |
e27753e46e | ||
![]() |
11fbef4bf0 | ||
![]() |
c4e6ad1ec7 | ||
![]() |
263a3f1983 | ||
![]() |
8acaea0fbe | ||
![]() |
bd6adfaec6 | ||
![]() |
4b4a5a4b93 | ||
![]() |
b098d84557 | ||
![]() |
b937f9b762 | ||
![]() |
55f3e07bd4 | ||
![]() |
2780623076 | ||
![]() |
75a342f96e | ||
![]() |
729cd54401 | ||
![]() |
a023f51971 | ||
![]() |
5076eb9215 | ||
![]() |
7edd0cd14c | ||
![]() |
7ce4738d8a | ||
![]() |
7287e0259c | ||
![]() |
d86de68b40 | ||
![]() |
4ba107a765 | ||
![]() |
187b016d09 | ||
![]() |
7010f5afad | ||
![]() |
48b73fa12f | ||
![]() |
1ecd0d7ca4 | ||
![]() |
6faaae0c5f | ||
![]() |
e4ef65be76 | ||
![]() |
39c661d408 | ||
![]() |
91a48d6a43 | ||
![]() |
123f0fa185 | ||
![]() |
ba3dffecbf | ||
![]() |
8735e5675c | ||
![]() |
3f5e06a0f8 | ||
![]() |
ba40a40b73 | ||
![]() |
b3732e7fb9 | ||
![]() |
104776ee84 | ||
![]() |
01e781e546 | ||
![]() |
e77c16b55a | ||
![]() |
987bbee1db | ||
![]() |
74d2fe1baa | ||
![]() |
98e63d5561 | ||
![]() |
059f13fc9d | ||
![]() |
ebd27b46af | ||
![]() |
ca8d814918 | ||
![]() |
0aeeaac361 | ||
![]() |
28ed8a5742 | ||
![]() |
f749be1490 | ||
![]() |
693bce1b10 | ||
![]() |
4f97e077db | ||
![]() |
c883e79884 | ||
![]() |
a613501ff2 | ||
![]() |
a054e2514a | ||
![]() |
c49fe26da7 | ||
![]() |
75afdc6306 | ||
![]() |
f02beaf075 | ||
![]() |
8bcc7e88f0 | ||
![]() |
0adbd720bf | ||
![]() |
d93a7f2e02 | ||
![]() |
88d7ac04bf | ||
![]() |
1f422af1c8 | ||
![]() |
53168d54d8 | ||
![]() |
b0ec945dbb | ||
![]() |
48ef6e5a6f | ||
![]() |
8d1adaaef3 | ||
![]() |
dd8c0d1e9e | ||
![]() |
57b79aa852 | ||
![]() |
2f883410d2 | ||
![]() |
6fa61380b2 | ||
![]() |
47b61c0cea | ||
![]() |
d739ac830f | ||
![]() |
26024fedc7 | ||
![]() |
a376b697c0 | ||
![]() |
bc2574680d | ||
![]() |
f194b41435 | ||
![]() |
350f7da55d | ||
![]() |
36f5f78f46 | ||
![]() |
55fe5b0b41 | ||
![]() |
7d1a5c00a0 | ||
![]() |
036061664e | ||
![]() |
5b1b40ce93 | ||
![]() |
a8d9fdce3c | ||
![]() |
700382cba4 | ||
![]() |
9698abbfd5 | ||
![]() |
5bfcf5c917 | ||
![]() |
8eb7d47072 | ||
![]() |
ab61c87701 | ||
![]() |
c1e6157847 | ||
![]() |
4c849539fc |
@@ -2,6 +2,7 @@
|
||||
// ignoring it let us speed up the integration test
|
||||
// development
|
||||
integration_test.go
|
||||
integration_test/
|
||||
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
|
39
.github/workflows/lint.yml
vendored
Normal file
39
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
# The "build" workflow
|
||||
lint:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Install and run golangci-lint as a separate step, it's much faster this
|
||||
# way because this action has caching. It'll get run again in `make lint`
|
||||
# below, but it's still much faster in the end than installing
|
||||
# golangci-lint manually in the `Run lint` step.
|
||||
- uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
args: --timeout 5m
|
||||
|
||||
# Setup Go
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.16.3" # The Go version to download (if necessary) and use.
|
||||
|
||||
# Install all the dependencies
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
go version
|
||||
go install golang.org/x/lint/golint@latest
|
||||
sudo apt update
|
||||
sudo apt install -y make
|
||||
|
||||
- name: Run lint
|
||||
run: make lint
|
53
.github/workflows/release.yml
vendored
53
.github/workflows/release.yml
vendored
@@ -1,9 +1,11 @@
|
||||
name: goreleaser
|
||||
---
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*" # triggers only if push new tag version
|
||||
- "*" # triggers only if push new tag version
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
@@ -27,4 +29,49 @@ jobs:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/headscale
|
||||
ghcr.io/${{ github.repository_owner }}/headscale
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Login to GHCR
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
23
.github/workflows/test-integration.yml
vendored
Normal file
23
.github/workflows/test-integration.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
# The "build" workflow
|
||||
integration-test:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Setup Go
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.16.3"
|
||||
|
||||
- name: Run Integration tests
|
||||
run: go test -tags integration -timeout 30m
|
46
.github/workflows/test.yml
vendored
46
.github/workflows/test.yml
vendored
@@ -10,36 +10,24 @@ jobs:
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Install and run golangci-lint as a separate step, it's much faster this
|
||||
# way because this action has caching. It'll get run again in `make lint`
|
||||
# below, but it's still much faster in the end than installing
|
||||
# golangci-lint manually in the `Run lint` step.
|
||||
- uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
args: --timeout 2m
|
||||
|
||||
# Setup Go
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.16.3' # The Go version to download (if necessary) and use.
|
||||
# Setup Go
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.16.3" # The Go version to download (if necessary) and use.
|
||||
|
||||
# Install all the dependencies
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
go version
|
||||
go install golang.org/x/lint/golint@latest
|
||||
sudo apt update
|
||||
sudo apt install -y make
|
||||
|
||||
- name: Run tests
|
||||
run: make test
|
||||
# Install all the dependencies
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
go version
|
||||
sudo apt update
|
||||
sudo apt install -y make
|
||||
|
||||
- name: Run lint
|
||||
run: make lint
|
||||
- name: Run tests
|
||||
run: make test
|
||||
|
||||
- name: Run build
|
||||
run: make
|
||||
- name: Run build
|
||||
run: make
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -19,3 +19,8 @@ config.json
|
||||
*.key
|
||||
/db.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Exclude Jetbrains Editors
|
||||
.idea
|
||||
|
||||
test_output/
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM golang:latest AS build
|
||||
FROM golang:1.17.1-bullseye AS build
|
||||
ENV GOPATH /go
|
||||
|
||||
COPY go.mod go.sum /go/src/headscale/
|
||||
@@ -10,7 +10,7 @@ COPY . /go/src/headscale
|
||||
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
|
||||
RUN test -e /go/bin/headscale
|
||||
|
||||
FROM ubuntu:latest
|
||||
FROM ubuntu:20.04
|
||||
|
||||
COPY --from=build /go/bin/headscale /usr/local/bin/headscale
|
||||
ENV TZ UTC
|
||||
|
@@ -1,9 +1,11 @@
|
||||
FROM ubuntu:latest
|
||||
|
||||
ARG TAILSCALE_VERSION
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y gnupg curl \
|
||||
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \
|
||||
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y tailscale \
|
||||
&& apt-get install -y tailscale=${TAILSCALE_VERSION} \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
4
Makefile
4
Makefile
@@ -2,7 +2,7 @@
|
||||
version = $(shell ./scripts/version-at-commit.sh)
|
||||
|
||||
build:
|
||||
go build -ldflags "-s -w -X main.version=$(version)" cmd/headscale/headscale.go
|
||||
go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.version=$(version)" cmd/headscale/headscale.go
|
||||
|
||||
dev: lint test build
|
||||
|
||||
@@ -20,7 +20,7 @@ coverprofile_html:
|
||||
|
||||
lint:
|
||||
golint
|
||||
golangci-lint run
|
||||
golangci-lint run --timeout 5m
|
||||
|
||||
compress: build
|
||||
upx --brute headscale
|
||||
|
124
README.md
124
README.md
@@ -18,77 +18,109 @@ Headscale implements this coordination server.
|
||||
|
||||
- [x] Base functionality (nodes can communicate with each other)
|
||||
- [x] Node registration through the web flow
|
||||
- [x] Network changes are relied to the nodes
|
||||
- [x] Network changes are relayed to the nodes
|
||||
- [x] Namespace support (~equivalent to multi-user in Tailscale.com)
|
||||
- [x] Routing (advertise & accept, including exit nodes)
|
||||
- [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support)
|
||||
- [X] JSON-formatted output
|
||||
- [X] ACLs
|
||||
- [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10)
|
||||
- [ ] Share nodes between ~~users~~ namespaces
|
||||
- [ ] DNS
|
||||
- [X] DNS (passing DNS servers to nodes)
|
||||
- [X] Share nodes between ~~users~~ namespaces
|
||||
- [ ] MagicDNS / Smart DNS
|
||||
|
||||
|
||||
## Roadmap 🤷
|
||||
|
||||
We are now focusing on adding integration tests with the official clients.
|
||||
|
||||
Suggestions/PRs welcomed!
|
||||
|
||||
|
||||
|
||||
## Running it
|
||||
|
||||
1. Download the Headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your PATH
|
||||
|
||||
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
|
||||
```
|
||||
```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
|
||||
```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
|
||||
```
|
||||
# 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
|
||||
```
|
||||
```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:8000:8000 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
|
||||
```
|
||||
```shell
|
||||
headscale serve
|
||||
```
|
||||
or docker:
|
||||
|
||||
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
|
||||
```
|
||||
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:8000:8000 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
|
||||
```
|
||||
```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 node register YOURMACHINEKEY
|
||||
```
|
||||
```shell
|
||||
headscale -n myfirstnamespace node 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 node register YOURMACHINEKEY
|
||||
```
|
||||
or if your server is already running in docker:
|
||||
```shell
|
||||
docker exec <container_name> headscale -n myfistnamespace node register YOURMACHINEKEY
|
||||
```
|
||||
|
||||
Alternatively, you can use Auth Keys to register your machines:
|
||||
|
||||
@@ -96,11 +128,19 @@ Alternatively, you can use Auth Keys to register your machines:
|
||||
```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
|
||||
```
|
||||
```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.
|
||||
|
||||
@@ -183,7 +223,7 @@ Alternatively, `tls_letsencrypt_challenge_type` can be set to `TLS-ALPN-01`. In
|
||||
|
||||
### Policy ACLs
|
||||
|
||||
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
|
||||
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).
|
||||
|
297
api.go
297
api.go
@@ -13,9 +13,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
@@ -35,8 +33,6 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// spew.Dump(c.Params)
|
||||
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(`
|
||||
<html>
|
||||
<body>
|
||||
@@ -82,14 +78,16 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine")
|
||||
m = Machine{
|
||||
Expiry: &req.Expiry,
|
||||
MachineKey: mKey.HexString(),
|
||||
Name: req.Hostinfo.Hostname,
|
||||
NodeKey: wgkey.Key(req.NodeKey).HexString(),
|
||||
Expiry: &req.Expiry,
|
||||
MachineKey: mKey.HexString(),
|
||||
Name: req.Hostinfo.Hostname,
|
||||
NodeKey: wgkey.Key(req.NodeKey).HexString(),
|
||||
LastSuccessfulUpdate: &now,
|
||||
}
|
||||
if err := h.db.Create(&m).Error; err != nil {
|
||||
log.Error().
|
||||
@@ -215,272 +213,12 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
|
||||
c.Data(200, "application/json; charset=utf-8", respBody)
|
||||
}
|
||||
|
||||
// PollNetMapHandler takes care of /machine/:id/map
|
||||
//
|
||||
// This is the busiest endpoint, as it keeps the HTTP long poll that updates
|
||||
// the clients when something in the network changes.
|
||||
//
|
||||
// The clients POST stuff like HostInfo and their Endpoints here, but
|
||||
// only after their first request (marked with the ReadOnly field).
|
||||
//
|
||||
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
|
||||
func (h *Headscale) PollNetMapHandler(c *gin.Context) {
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Msg("PollNetMapHandler called")
|
||||
body, _ := io.ReadAll(c.Request.Body)
|
||||
mKeyStr := c.Param("id")
|
||||
mKey, err := wgkey.ParseHex(mKeyStr)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Err(err).
|
||||
Msg("Cannot parse client key")
|
||||
c.String(http.StatusBadRequest, "")
|
||||
return
|
||||
}
|
||||
req := tailcfg.MapRequest{}
|
||||
err = decode(body, &req, &mKey, h.privateKey)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Err(err).
|
||||
Msg("Cannot decode message")
|
||||
c.String(http.StatusBadRequest, "")
|
||||
return
|
||||
}
|
||||
|
||||
var m Machine
|
||||
if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
log.Warn().
|
||||
Str("handler", "PollNetMap").
|
||||
Msgf("Ignoring request, cannot find machine with key %s", mKey.HexString())
|
||||
c.String(http.StatusUnauthorized, "")
|
||||
return
|
||||
}
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Str("machine", m.Name).
|
||||
Msg("Found machine in database")
|
||||
|
||||
hostinfo, _ := json.Marshal(req.Hostinfo)
|
||||
m.Name = req.Hostinfo.Hostname
|
||||
m.HostInfo = datatypes.JSON(hostinfo)
|
||||
m.DiscoKey = wgkey.Key(req.DiscoKey).HexString()
|
||||
now := time.Now().UTC()
|
||||
|
||||
// From Tailscale client:
|
||||
//
|
||||
// ReadOnly is whether the client just wants to fetch the MapResponse,
|
||||
// without updating their Endpoints. The Endpoints field will be ignored and
|
||||
// LastSeen will not be updated and peers will not be notified of changes.
|
||||
//
|
||||
// The intended use is for clients to discover the DERP map at start-up
|
||||
// before their first real endpoint update.
|
||||
if !req.ReadOnly {
|
||||
endpoints, _ := json.Marshal(req.Endpoints)
|
||||
m.Endpoints = datatypes.JSON(endpoints)
|
||||
m.LastSeen = &now
|
||||
}
|
||||
h.db.Save(&m)
|
||||
|
||||
update := make(chan []byte, 1)
|
||||
|
||||
pollData := make(chan []byte, 1)
|
||||
defer close(pollData)
|
||||
|
||||
cancelKeepAlive := make(chan []byte, 1)
|
||||
defer close(cancelKeepAlive)
|
||||
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Str("machine", m.Name).
|
||||
Msg("Storing update channel")
|
||||
h.clientsPolling.Store(m.ID, update)
|
||||
|
||||
data, err := h.getMapResponse(mKey, req, m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Str("machine", m.Name).
|
||||
Err(err).
|
||||
Msg("Failed to get Map response")
|
||||
c.String(http.StatusInternalServerError, ":(")
|
||||
return
|
||||
}
|
||||
|
||||
// We update our peers if the client is not sending ReadOnly in the MapRequest
|
||||
// so we don't distribute its initial request (it comes with
|
||||
// empty endpoints to peers)
|
||||
|
||||
// Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696
|
||||
log.Debug().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Str("machine", m.Name).
|
||||
Bool("readOnly", req.ReadOnly).
|
||||
Bool("omitPeers", req.OmitPeers).
|
||||
Bool("stream", req.Stream).
|
||||
Msg("Client map request processed")
|
||||
|
||||
if req.ReadOnly {
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Client is starting up. Asking for DERP map")
|
||||
c.Data(200, "application/json; charset=utf-8", *data)
|
||||
return
|
||||
}
|
||||
if req.OmitPeers && !req.Stream {
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Client sent endpoint update and is ok with a response without peer list")
|
||||
c.Data(200, "application/json; charset=utf-8", *data)
|
||||
return
|
||||
} else if req.OmitPeers && req.Stream {
|
||||
log.Warn().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Ignoring request, don't know how to handle it")
|
||||
c.String(http.StatusBadRequest, "")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Client is ready to access the tailnet")
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Sending initial map")
|
||||
pollData <- *data
|
||||
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Notifying peers")
|
||||
peers, _ := h.getPeers(m)
|
||||
for _, p := range *peers {
|
||||
pUp, ok := h.clientsPolling.Load(uint64(p.ID))
|
||||
if ok {
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Str("peer", m.Name).
|
||||
Str("address", p.Addresses[0].String()).
|
||||
Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0])
|
||||
pUp.(chan []byte) <- []byte{}
|
||||
} else {
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Str("peer", m.Name).
|
||||
Msgf("Peer %s does not appear to be polling", p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
go h.keepAlive(cancelKeepAlive, pollData, mKey, req, m)
|
||||
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
select {
|
||||
case data := <-pollData:
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Int("bytes", len(data)).
|
||||
Msg("Sending data")
|
||||
_, err := w.Write(data)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Err(err).
|
||||
Msg("Cannot write data")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
m.LastSeen = &now
|
||||
h.db.Save(&m)
|
||||
return true
|
||||
|
||||
case <-update:
|
||||
log.Debug().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Received a request for update")
|
||||
data, err := h.getMapResponse(mKey, req, m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Err(err).
|
||||
Msg("Could not get the map update")
|
||||
}
|
||||
_, err = w.Write(*data)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Err(err).
|
||||
Msg("Could not write the map response")
|
||||
}
|
||||
return true
|
||||
|
||||
case <-c.Request.Context().Done():
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("The client has closed the connection")
|
||||
now := time.Now().UTC()
|
||||
m.LastSeen = &now
|
||||
h.db.Save(&m)
|
||||
cancelKeepAlive <- []byte{}
|
||||
h.clientsPolling.Delete(m.ID)
|
||||
close(update)
|
||||
return false
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgkey.Key, req tailcfg.MapRequest, m Machine) {
|
||||
for {
|
||||
select {
|
||||
case <-cancel:
|
||||
return
|
||||
|
||||
default:
|
||||
data, err := h.getMapKeepAliveResponse(mKey, req, m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "keepAlive").
|
||||
Err(err).
|
||||
Msg("Error generating the keep alive msg")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("func", "keepAlive").
|
||||
Str("machine", m.Name).
|
||||
Msg("Sending keepalive")
|
||||
pollData <- *data
|
||||
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) {
|
||||
log.Trace().
|
||||
Str("func", "getMapResponse").
|
||||
Str("machine", req.Hostinfo.Hostname).
|
||||
Msg("Creating Map response")
|
||||
node, err := m.toNode()
|
||||
node, err := m.toNode(true)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "getMapResponse").
|
||||
@@ -504,20 +242,30 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac
|
||||
}
|
||||
|
||||
resp := tailcfg.MapResponse{
|
||||
KeepAlive: false,
|
||||
Node: node,
|
||||
Peers: *peers,
|
||||
DNS: []netaddr.IP{},
|
||||
KeepAlive: false,
|
||||
Node: node,
|
||||
Peers: *peers,
|
||||
//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},
|
||||
}
|
||||
log.Trace().
|
||||
Str("func", "getMapResponse").
|
||||
Str("machine", req.Hostinfo.Hostname).
|
||||
Msgf("Generated map response: %s", tailMapResponseToString(resp))
|
||||
|
||||
var respBody []byte
|
||||
if req.Compress == "zstd" {
|
||||
src, _ := json.Marshal(resp)
|
||||
|
||||
encoder, _ := zstd.NewWriter(nil)
|
||||
srcCompressed := encoder.EncodeAll(src, nil)
|
||||
respBody, err = encodeMsg(srcCompressed, &mKey, h.privateKey)
|
||||
@@ -530,7 +278,6 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// spew.Dump(resp)
|
||||
// declare the incoming size on the first 4 bytes
|
||||
data := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
|
||||
@@ -607,7 +354,7 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key,
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", m.Name).
|
||||
Str("ip", ip.String()).
|
||||
Msgf("Assining %s to %s", ip, m.Name)
|
||||
Msgf("Assigning %s to %s", ip, m.Name)
|
||||
|
||||
m.AuthKeyID = uint(pak.ID)
|
||||
m.IPAddress = ip.String()
|
||||
|
51
app.go
51
app.go
@@ -43,6 +43,8 @@ type Config struct {
|
||||
|
||||
TLSCertPath string
|
||||
TLSKeyPath string
|
||||
|
||||
DNSConfig *tailcfg.DNSConfig
|
||||
}
|
||||
|
||||
// Headscale represents the base app of the service
|
||||
@@ -58,7 +60,10 @@ type Headscale struct {
|
||||
aclPolicy *ACLPolicy
|
||||
aclRules *[]tailcfg.FilterRule
|
||||
|
||||
clientsPolling sync.Map
|
||||
clientsUpdateChannels sync.Map
|
||||
clientsUpdateChannelMutex sync.Mutex
|
||||
|
||||
lastStateChange sync.Map
|
||||
}
|
||||
|
||||
// NewHeadscale returns the Headscale app
|
||||
@@ -107,9 +112,9 @@ func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, target, http.StatusFound)
|
||||
}
|
||||
|
||||
// ExpireEphemeralNodes deletes ephemeral machine records that have not been
|
||||
// expireEphemeralNodes deletes ephemeral machine records that have not been
|
||||
// seen for longer than h.cfg.EphemeralNodeInactivityTimeout
|
||||
func (h *Headscale) ExpireEphemeralNodes(milliSeconds int64) {
|
||||
func (h *Headscale) expireEphemeralNodes(milliSeconds int64) {
|
||||
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
|
||||
for range ticker.C {
|
||||
h.expireEphemeralNodesWorker()
|
||||
@@ -135,6 +140,7 @@ func (h *Headscale) expireEphemeralNodesWorker() {
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("machine", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database")
|
||||
}
|
||||
h.notifyChangesToPeers(&m)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +170,17 @@ func (h *Headscale) Serve() error {
|
||||
r.POST("/machine/:id", h.RegistrationHandler)
|
||||
var err error
|
||||
|
||||
timeout := 30 * time.Second
|
||||
|
||||
go h.watchForKVUpdates(5000)
|
||||
go h.expireEphemeralNodes(5000)
|
||||
|
||||
s := &http.Server{
|
||||
Addr: h.cfg.Addr,
|
||||
Handler: r,
|
||||
ReadTimeout: timeout,
|
||||
WriteTimeout: timeout,
|
||||
}
|
||||
|
||||
if h.cfg.TLSLetsEncryptHostname != "" {
|
||||
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
||||
@@ -177,9 +193,11 @@ func (h *Headscale) Serve() error {
|
||||
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
|
||||
}
|
||||
s := &http.Server{
|
||||
Addr: h.cfg.Addr,
|
||||
TLSConfig: m.TLSConfig(),
|
||||
Handler: r,
|
||||
Addr: h.cfg.Addr,
|
||||
TLSConfig: m.TLSConfig(),
|
||||
Handler: r,
|
||||
ReadTimeout: timeout,
|
||||
WriteTimeout: timeout,
|
||||
}
|
||||
if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" {
|
||||
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
|
||||
@@ -204,12 +222,29 @@ func (h *Headscale) Serve() error {
|
||||
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
|
||||
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://")
|
||||
}
|
||||
err = r.Run(h.cfg.Addr)
|
||||
err = s.ListenAndServe()
|
||||
} else {
|
||||
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
||||
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
|
||||
}
|
||||
err = r.RunTLS(h.cfg.Addr, h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
|
||||
err = s.ListenAndServeTLS(h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Headscale) setLastStateChangeToNow(namespace string) {
|
||||
now := time.Now().UTC()
|
||||
h.lastStateChange.Store(namespace, now)
|
||||
}
|
||||
|
||||
func (h *Headscale) getLastStateChange(namespace string) time.Time {
|
||||
if wrapped, ok := h.lastStateChange.Load(namespace); ok {
|
||||
lastChange, _ := wrapped.(time.Time)
|
||||
return lastChange
|
||||
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
h.lastStateChange.Store(namespace, now)
|
||||
return now
|
||||
}
|
||||
|
@@ -3,8 +3,10 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -94,9 +96,14 @@ var listNamespacesCmd = &cobra.Command{
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("ID\tName\n")
|
||||
|
||||
d := pterm.TableData{{"ID", "Name", "Created"}}
|
||||
for _, n := range *namespaces {
|
||||
fmt.Printf("%d\t%s\n", n.ID, n.Name)
|
||||
d = append(d, []string{strconv.FormatUint(uint64(n.ID), 10), n.Name, n.CreatedAt.Format("2006-01-02 15:04:05")})
|
||||
}
|
||||
err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -8,7 +8,11 @@ import (
|
||||
"time"
|
||||
|
||||
survey "github.com/AlecAivazis/survey/v2"
|
||||
"github.com/juanfont/headscale"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -21,6 +25,7 @@ func init() {
|
||||
nodeCmd.AddCommand(listNodesCmd)
|
||||
nodeCmd.AddCommand(registerNodeCmd)
|
||||
nodeCmd.AddCommand(deleteNodeCmd)
|
||||
nodeCmd.AddCommand(shareMachineCmd)
|
||||
}
|
||||
|
||||
var nodeCmd = &cobra.Command{
|
||||
@@ -33,7 +38,7 @@ var registerNodeCmd = &cobra.Command{
|
||||
Short: "Registers a machine to your network",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("Missing parameters")
|
||||
return fmt.Errorf("missing parameters")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -75,9 +80,26 @@ var listNodesCmd = &cobra.Command{
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
}
|
||||
|
||||
namespace, err := h.GetNamespace(n)
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching namespace: %s", err)
|
||||
}
|
||||
|
||||
machines, err := h.ListMachinesInNamespace(n)
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching machines: %s", err)
|
||||
}
|
||||
|
||||
sharedMachines, err := h.ListSharedMachinesInNamespace(n)
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching shared machines: %s", err)
|
||||
}
|
||||
|
||||
allMachines := append(*machines, *sharedMachines...)
|
||||
|
||||
if strings.HasPrefix(o, "json") {
|
||||
JsonOutput(machines, err, o)
|
||||
JsonOutput(allMachines, err, o)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,19 +107,15 @@ var listNodesCmd = &cobra.Command{
|
||||
log.Fatalf("Error getting nodes: %s", err)
|
||||
}
|
||||
|
||||
fmt.Printf("ID\tname\t\tlast seen\t\tephemeral\n")
|
||||
for _, m := range *machines {
|
||||
var ephemeral bool
|
||||
if m.AuthKey != nil && m.AuthKey.Ephemeral {
|
||||
ephemeral = true
|
||||
}
|
||||
var lastSeen time.Time
|
||||
if m.LastSeen != nil {
|
||||
lastSeen = *m.LastSeen
|
||||
}
|
||||
fmt.Printf("%d\t%s\t%s\t%t\n", m.ID, m.Name, lastSeen.Format("2006-01-02 15:04:05"), ephemeral)
|
||||
d, err := nodesToPtables(*namespace, allMachines)
|
||||
if err != nil {
|
||||
log.Fatalf("Error converting to table: %s", err)
|
||||
}
|
||||
|
||||
err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -106,7 +124,7 @@ var deleteNodeCmd = &cobra.Command{
|
||||
Short: "Delete a node",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("Missing parameters")
|
||||
return fmt.Errorf("missing parameters")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -144,3 +162,95 @@ var deleteNodeCmd = &cobra.Command{
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var shareMachineCmd = &cobra.Command{
|
||||
Use: "share ID namespace",
|
||||
Short: "Shares a node from the current namespace to the specified one",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("missing parameters")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
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)
|
||||
}
|
||||
|
||||
_, err = h.GetNamespace(namespace)
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching origin namespace: %s", err)
|
||||
}
|
||||
|
||||
destinationNamespace, err := h.GetNamespace(args[1])
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching destination namespace: %s", err)
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Error converting ID to integer: %s", err)
|
||||
}
|
||||
machine, err := h.GetMachineByID(uint64(id))
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting node: %s", err)
|
||||
}
|
||||
|
||||
err = h.AddSharedMachineToNamespace(machine, destinationNamespace)
|
||||
if strings.HasPrefix(output, "json") {
|
||||
JsonOutput(map[string]string{"Result": "Node shared"}, err, output)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("Error sharing node: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Node shared!")
|
||||
},
|
||||
}
|
||||
|
||||
func nodesToPtables(currentNamespace headscale.Namespace, machines []headscale.Machine) (pterm.TableData, error) {
|
||||
d := pterm.TableData{{"ID", "Name", "NodeKey", "Namespace", "IP address", "Ephemeral", "Last seen", "Online"}}
|
||||
|
||||
for _, machine := range machines {
|
||||
var ephemeral bool
|
||||
if machine.AuthKey != nil && machine.AuthKey.Ephemeral {
|
||||
ephemeral = true
|
||||
}
|
||||
var lastSeen time.Time
|
||||
var lastSeenTime string
|
||||
if machine.LastSeen != nil {
|
||||
lastSeen = *machine.LastSeen
|
||||
lastSeenTime = lastSeen.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
nKey, err := wgkey.ParseHex(machine.NodeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodeKey := tailcfg.NodeKey(nKey)
|
||||
|
||||
var online string
|
||||
if lastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online
|
||||
online = pterm.LightGreen("true")
|
||||
} else {
|
||||
online = pterm.LightRed("false")
|
||||
}
|
||||
|
||||
var namespace string
|
||||
if currentNamespace.ID == machine.NamespaceID {
|
||||
namespace = pterm.LightMagenta(machine.Namespace.Name)
|
||||
} else {
|
||||
namespace = pterm.LightYellow(machine.Namespace.Name)
|
||||
}
|
||||
d = append(d, []string{strconv.FormatUint(machine.ID, 10), machine.Name, nodeKey.ShortString(), namespace, machine.IPAddress, strconv.FormatBool(ephemeral), lastSeenTime, online})
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
@@ -3,10 +3,12 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hako/durafmt"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -54,6 +56,8 @@ var listPreAuthKeys = &cobra.Command{
|
||||
fmt.Printf("Error getting the list of keys: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Expiration", "Created"}}
|
||||
for _, k := range *keys {
|
||||
expiration := "-"
|
||||
if k.Expiration != nil {
|
||||
@@ -67,15 +71,19 @@ var listPreAuthKeys = &cobra.Command{
|
||||
reusable = fmt.Sprintf("%v", k.Reusable)
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
"key: %s, namespace: %s, reusable: %s, ephemeral: %v, expiration: %s, created_at: %s\n",
|
||||
d = append(d, []string{
|
||||
strconv.FormatUint(k.ID, 10),
|
||||
k.Key,
|
||||
k.Namespace.Name,
|
||||
reusable,
|
||||
k.Ephemeral,
|
||||
strconv.FormatBool(k.Ephemeral),
|
||||
expiration,
|
||||
k.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -15,6 +16,9 @@ func init() {
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
enableRouteCmd.Flags().BoolP("all", "a", false, "Enable all routes advertised by the node")
|
||||
|
||||
routesCmd.AddCommand(listRoutesCmd)
|
||||
routesCmd.AddCommand(enableRouteCmd)
|
||||
}
|
||||
@@ -44,19 +48,25 @@ var listRoutesCmd = &cobra.Command{
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
}
|
||||
routes, err := h.GetNodeRoutes(n, args[0])
|
||||
|
||||
if strings.HasPrefix(o, "json") {
|
||||
JsonOutput(routes, err, o)
|
||||
return
|
||||
}
|
||||
|
||||
availableRoutes, err := h.GetAdvertisedNodeRoutes(n, args[0])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(routes)
|
||||
if strings.HasPrefix(o, "json") {
|
||||
// TODO: Add enable/disabled information to this interface
|
||||
JsonOutput(availableRoutes, err, o)
|
||||
return
|
||||
}
|
||||
|
||||
d := h.RoutesToPtables(n, args[0], *availableRoutes)
|
||||
|
||||
err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,32 +74,74 @@ var enableRouteCmd = &cobra.Command{
|
||||
Use: "enable node-name route",
|
||||
Short: "Allows exposing a route declared by this node to the rest of the nodes",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("Missing parameters")
|
||||
all, err := cmd.Flags().GetBool("all")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting namespace: %s", err)
|
||||
}
|
||||
|
||||
if all {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("Missing parameters")
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("Missing parameters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
n, err := cmd.Flags().GetString("namespace")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting namespace: %s", err)
|
||||
}
|
||||
|
||||
o, _ := cmd.Flags().GetString("output")
|
||||
|
||||
all, err := cmd.Flags().GetBool("all")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting namespace: %s", err)
|
||||
}
|
||||
|
||||
h, err := getHeadscaleApp()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
}
|
||||
route, err := h.EnableNodeRoute(n, args[0], args[1])
|
||||
if strings.HasPrefix(o, "json") {
|
||||
JsonOutput(route, err, o)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
if all {
|
||||
availableRoutes, err := h.GetAdvertisedNodeRoutes(n, args[0])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, availableRoute := range *availableRoutes {
|
||||
err = h.EnableNodeRoute(n, args[0], availableRoute.String())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(o, "json") {
|
||||
JsonOutput(availableRoute, err, o)
|
||||
} else {
|
||||
fmt.Printf("Enabled route %s\n", availableRoute)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = h.EnableNodeRoute(n, args[0], args[1])
|
||||
|
||||
if strings.HasPrefix(o, "json") {
|
||||
JsonOutput(args[1], err, o)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Enabled route %s\n", args[1])
|
||||
}
|
||||
fmt.Printf("Enabled route %s\n", route)
|
||||
},
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ var serveCmd = &cobra.Command{
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
}
|
||||
go h.ExpireEphemeralNodes(5000)
|
||||
|
||||
err = h.Serve()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing: %s", err)
|
||||
|
@@ -16,6 +16,7 @@ import (
|
||||
"gopkg.in/yaml.v2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
)
|
||||
|
||||
type ErrorOutput struct {
|
||||
@@ -39,7 +40,9 @@ func LoadConfig(path string) error {
|
||||
|
||||
viper.SetDefault("ip_prefix", "100.64.0.0/10")
|
||||
|
||||
viper.SetDefault("log_level", "debug")
|
||||
viper.SetDefault("log_level", "info")
|
||||
|
||||
viper.SetDefault("dns_config", nil)
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
@@ -70,6 +73,45 @@ func LoadConfig(path string) error {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetDNSConfig() *tailcfg.DNSConfig {
|
||||
if viper.IsSet("dns_config") {
|
||||
dnsConfig := &tailcfg.DNSConfig{}
|
||||
|
||||
if viper.IsSet("dns_config.nameservers") {
|
||||
nameserversStr := viper.GetStringSlice("dns_config.nameservers")
|
||||
|
||||
nameservers := make([]netaddr.IP, len(nameserversStr))
|
||||
resolvers := make([]dnstype.Resolver, len(nameserversStr))
|
||||
|
||||
for index, nameserverStr := range nameserversStr {
|
||||
nameserver, err := netaddr.ParseIP(nameserverStr)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "getDNSConfig").
|
||||
Err(err).
|
||||
Msgf("Could not parse nameserver IP: %s", nameserverStr)
|
||||
}
|
||||
|
||||
nameservers[index] = nameserver
|
||||
resolvers[index] = dnstype.Resolver{
|
||||
Addr: nameserver.String(),
|
||||
}
|
||||
}
|
||||
|
||||
dnsConfig.Nameservers = nameservers
|
||||
dnsConfig.Resolvers = resolvers
|
||||
}
|
||||
if viper.IsSet("dns_config.domains") {
|
||||
dnsConfig.Domains = viper.GetStringSlice("dns_config.domains")
|
||||
}
|
||||
|
||||
return dnsConfig
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func absPath(path string) string {
|
||||
@@ -126,6 +168,8 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||
|
||||
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
|
||||
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
|
||||
|
||||
DNSConfig: GetDNSConfig(),
|
||||
}
|
||||
|
||||
h, err := headscale.NewHeadscale(cfg)
|
||||
|
@@ -22,14 +22,12 @@ func main() {
|
||||
colors = true
|
||||
default:
|
||||
// no color, return text as is.
|
||||
log.Trace().Msg("Colors are not supported, disabling")
|
||||
colors = false
|
||||
}
|
||||
|
||||
// Adhere to no-color.org manifesto of allowing users to
|
||||
// turn off color in cli/services
|
||||
if _, noColorIsSet := os.LookupEnv("NO_COLOR"); noColorIsSet {
|
||||
log.Trace().Msg("NO_COLOR is set, disabling colors")
|
||||
colors = false
|
||||
}
|
||||
|
||||
|
@@ -58,7 +58,7 @@ func (*Suite) TestPostgresConfigLoading(c *check.C) {
|
||||
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.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
|
||||
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
||||
}
|
||||
|
||||
func (*Suite) TestSqliteConfigLoading(c *check.C) {
|
||||
@@ -92,6 +92,37 @@ func (*Suite) TestSqliteConfigLoading(c *check.C) {
|
||||
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
|
||||
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
|
||||
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
|
||||
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
||||
}
|
||||
|
||||
func (*Suite) TestDNSConfigLoading(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)
|
||||
|
||||
dnsConfig := cli.GetDNSConfig()
|
||||
fmt.Println(dnsConfig)
|
||||
|
||||
c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1")
|
||||
|
||||
c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1")
|
||||
}
|
||||
|
||||
func writeConfig(c *check.C, tmpDir string, configYaml []byte) {
|
||||
|
@@ -16,5 +16,10 @@
|
||||
"tls_letsencrypt_challenge_type": "HTTP-01",
|
||||
"tls_cert_path": "",
|
||||
"tls_key_path": "",
|
||||
"acl_policy_path": ""
|
||||
"acl_policy_path": "",
|
||||
"dns_config": {
|
||||
"nameservers": [
|
||||
"1.1.1.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -12,5 +12,10 @@
|
||||
"tls_letsencrypt_challenge_type": "HTTP-01",
|
||||
"tls_cert_path": "",
|
||||
"tls_key_path": "",
|
||||
"acl_policy_path": ""
|
||||
"acl_policy_path": "",
|
||||
"dns_config": {
|
||||
"nameservers": [
|
||||
"1.1.1.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
5
db.go
5
db.go
@@ -44,6 +44,11 @@ func (h *Headscale) initDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(&SharedMachine{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.setValue("db_version", dbVersion)
|
||||
return err
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# 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
|
||||
# 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
|
||||
|
42
go.mod
42
go.mod
@@ -3,36 +3,36 @@ module github.com/juanfont/headscale
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.0.5
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2
|
||||
github.com/Microsoft/go-winio v0.5.0 // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
|
||||
github.com/containerd/continuity v0.1.0 // indirect
|
||||
github.com/docker/cli v20.10.8+incompatible // indirect
|
||||
github.com/docker/docker v20.10.8+incompatible // indirect
|
||||
github.com/efekarakus/termcolor v1.0.1 // indirect
|
||||
github.com/gin-gonic/gin v1.7.2
|
||||
github.com/efekarakus/termcolor v1.0.1
|
||||
github.com/gin-gonic/gin v1.7.4
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
|
||||
github.com/klauspost/compress v1.13.1
|
||||
github.com/lib/pq v1.10.2 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.7 // indirect
|
||||
github.com/klauspost/compress v1.13.5
|
||||
github.com/lib/pq v1.10.3 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/opencontainers/runc v1.0.1 // indirect
|
||||
github.com/ory/dockertest/v3 v3.7.0 // indirect
|
||||
github.com/rs/zerolog v1.23.0 // indirect
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/opencontainers/runc v1.0.2 // indirect
|
||||
github.com/ory/dockertest/v3 v3.7.0
|
||||
github.com/pterm/pterm v0.12.30
|
||||
github.com/rs/zerolog v1.25.0
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
|
||||
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
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gorm.io/datatypes v1.0.1
|
||||
gorm.io/driver/postgres v1.1.0
|
||||
gorm.io/driver/sqlite v1.1.4
|
||||
gorm.io/gorm v1.21.11
|
||||
inet.af/netaddr v0.0.0-20210603230628-bf05d8b52dda
|
||||
tailscale.com v1.10.0
|
||||
gorm.io/datatypes v1.0.2
|
||||
gorm.io/driver/postgres v1.1.1
|
||||
gorm.io/driver/sqlite v1.1.5
|
||||
gorm.io/gorm v1.21.15
|
||||
inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e
|
||||
tailscale.com v1.14.2
|
||||
)
|
||||
|
184
go.sum
184
go.sum
@@ -39,8 +39,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AlecAivazis/survey/v2 v2.0.5 h1:xpZp+Q55wi5C7Iaze+40onHnEkex1jSc34CltJjOoPM=
|
||||
github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
@@ -49,6 +49,9 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
||||
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||
github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||
github.com/MarvinJWendt/testza v0.2.1 h1:eitywm1lzygA2KCyn55jFVdOaXj5I9LeOsLNeifd2Kw=
|
||||
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
@@ -92,6 +95,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.38.52/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
@@ -109,7 +114,6 @@ github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
@@ -150,18 +154,19 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
|
||||
github.com/daixiang0/gci v0.2.4/go.mod h1:+AV8KmHTGxxwp/pY84TLQfFKp2vuKXXJVzF3kD/hfR4=
|
||||
github.com/daixiang0/gci v0.2.7/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
|
||||
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denis-tingajkin/go-header v0.3.1/go.mod h1:sq/2IxMhaZX+RRcgHfCRx/m0M5na0fBt4/CRe7Lrji0=
|
||||
github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk=
|
||||
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/docker/cli v20.10.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
@@ -192,6 +197,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
@@ -206,8 +212,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
|
||||
github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/gliderlabs/ssh v0.3.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
@@ -222,6 +228,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
@@ -237,8 +244,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
|
||||
@@ -254,8 +261,9 @@ github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzz
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -297,7 +305,6 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
||||
@@ -357,6 +364,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gookit/color v1.3.1/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ=
|
||||
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/goreleaser/chglog v0.1.2/go.mod h1:tTZsFuSZK4epDXfjMkxzcGbrIOXprf0JFp47BjIr3B8=
|
||||
@@ -411,6 +420,8 @@ github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
@@ -420,6 +431,7 @@ github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
@@ -432,12 +444,17 @@ github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.8.1 h1:ySBX7Q87vOMqKU2bbmKbUvtYhauDFclYbNDYIE1/h6s=
|
||||
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU=
|
||||
github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||
@@ -447,8 +464,9 @@ github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6 h1:b1105ZGEMFe7aCvrT1Cca3VoVb4ZFMaFJLJcg/3zD+8=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
@@ -458,18 +476,20 @@ github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrU
|
||||
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
|
||||
github.com/jackc/pgtype v1.7.0 h1:6f4kVsW01QftE38ufBYxKciO6gyioXSC0ABIRLcZrGs=
|
||||
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs=
|
||||
github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
|
||||
github.com/jackc/pgx/v4 v4.11.0 h1:J86tSWd3Y7nKjwT/43xZBvpi04keQWx8gNC2YkdJhZI=
|
||||
github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570=
|
||||
github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
@@ -496,6 +516,7 @@ github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/rasw
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
|
||||
@@ -522,8 +543,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
|
||||
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4=
|
||||
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -548,8 +569,9 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
@@ -582,13 +604,16 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
|
||||
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
|
||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
||||
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
@@ -601,6 +626,8 @@ github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnN
|
||||
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
|
||||
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
||||
github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=
|
||||
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
|
||||
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
@@ -668,8 +695,8 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runc v1.0.1 h1:G18PGckGdAm3yVQRWDVQ1rLSLntiniKJ0cNRT2Tm5gs=
|
||||
github.com/opencontainers/runc v1.0.1/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
|
||||
github.com/opencontainers/runc v1.0.2 h1:opHZMaswlyxz1OuGpBE53Dwe4/xF7EZTY0A2L/FpCOg=
|
||||
github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
|
||||
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
@@ -731,12 +758,18 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
|
||||
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
|
||||
github.com/pterm/pterm v0.12.30 h1:ZfXzqtOJVKZ2Uhd+L5o6jmbO44PH3Mee4mxq303nh1Y=
|
||||
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
|
||||
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||
github.com/quasilyte/go-ruleguard v0.2.0/go.mod h1:2RT/tf0Ce0UDj5y243iWKosQogJd8+1G3Rs2fxmlYnw=
|
||||
github.com/quasilyte/go-ruleguard v0.2.1/go.mod h1:hN2rVc/uS4bQhQKTio2XaSJSafJwqBUWWwtssT3cQmc=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20200805063351-8f842688393c/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
@@ -744,10 +777,11 @@ github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/rs/zerolog v1.23.0 h1:UskrK+saS9P9Y789yNNulYKdARjPZuS35B8gJF2x60g=
|
||||
github.com/rs/zerolog v1.23.0/go.mod h1:6c7hFfxPOy7TacJc4Fcdi24/J0NKYGzjG8FWRI916Qo=
|
||||
github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
|
||||
github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryancurrah/gomodguard v1.1.0/go.mod h1:4O8tr7hBODaGE6VIhfJDHcwzh5GUccKSJBU0UMXJFVM=
|
||||
github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA=
|
||||
@@ -764,8 +798,9 @@ github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAx
|
||||
github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
@@ -796,8 +831,8 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
|
||||
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
|
||||
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
|
||||
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
@@ -831,8 +866,10 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 h1:reREUgl2FG+o7YCsrZB8XLjnuKv5hEIWtnOdAbRAXZI=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2/go.mod h1:STqf+YV0ADdzk4ejtXFsGqDpATP9JoL0OB+hiFQbkdE=
|
||||
github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88 h1:q5Sxx79nhG4xWsYEJBlLdqo1hNhUV31/NhA4qQ1SKAY=
|
||||
github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88/go.mod h1:iTDXJsA6A2wNNjurgic2rk+is6uzU4U2NLm4T+edr6M=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
@@ -846,6 +883,7 @@ github.com/tomarrell/wrapcheck v0.0.0-20200807122107-df9e8bcb914d/go.mod h1:yiFB
|
||||
github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0=
|
||||
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
@@ -874,6 +912,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -936,13 +976,15 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -990,6 +1032,7 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
@@ -1028,10 +1071,10 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8=
|
||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1070,10 +1113,11 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1114,6 +1158,7 @@ golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1130,7 +1175,6 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1140,13 +1184,16 @@ golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg=
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1155,8 +1202,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7-0.20210524175448-3115f89c4b99 h1:ZEXtoJu1S0ie/EmdYnjY3CqaCCZxnldL+K1ftMITD2Q=
|
||||
golang.org/x/text v0.3.7-0.20210524175448-3115f89c4b99/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1251,6 +1299,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1258,9 +1307,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210521230051-c27ff9b9f6f7/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.15-0.20210525143335-94c0476d63e3/go.mod h1:f/UVhQ6vXZKDodGB3Glgwu9B3djRxR14jIbcuxD8NBw=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.16/go.mod h1:f80rkFY2CKQklps1GHE15k/M4Tq78aofbr1iQM5MTVY=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@@ -1403,25 +1451,25 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/datatypes v1.0.1 h1:6npnXbBtjpSb7FFVA2dG/llyTN8tvZfbUqs+WyLrYgQ=
|
||||
gorm.io/datatypes v1.0.1/go.mod h1:HEHoUU3/PO5ZXfAJcVWl11+zWlE16+O0X2DgJEb4Ixs=
|
||||
gorm.io/driver/mysql v1.0.5 h1:WAAmvLK2rG0tCOqrf5XcLi2QUwugd4rcVJ/W3aoon9o=
|
||||
gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI=
|
||||
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
|
||||
gorm.io/driver/postgres v1.1.0 h1:afBljg7PtJ5lA6YUWluV2+xovIPhS+YiInuL3kUjrbk=
|
||||
gorm.io/datatypes v1.0.2 h1:ChZ5VfWGB23qEr1kZosidvG9CF9HIczwoxLhBS7Ebs4=
|
||||
gorm.io/datatypes v1.0.2/go.mod h1:1O1JVE4grFGcQTOGQbIBitiXUP6Sv84/KZU7eWeUv1k=
|
||||
gorm.io/driver/mysql v1.1.2 h1:OofcyE2lga734MxwcCW9uB4mWNXMr50uaGRVwQL2B0M=
|
||||
gorm.io/driver/mysql v1.1.2/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM=
|
||||
gorm.io/driver/postgres v1.1.0/go.mod h1:hXQIwafeRjJvUm+OMxcFWyswJ/vevcpPLlGocwAwuqw=
|
||||
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
||||
gorm.io/driver/postgres v1.1.1 h1:tWLmqYCyaoh89fi7DhM6QggujrOnmfo3H98AzgNAAu0=
|
||||
gorm.io/driver/postgres v1.1.1/go.mod h1:tpe2xN7aCst1NUdYyWQyxPtnHC+Zfp6NEux9PXD1OU0=
|
||||
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||
gorm.io/driver/sqlserver v1.0.7 h1:uwUtb0kdFwW5PkRbd2KJ2h4wlsqvLSjox1XVg/RnzRE=
|
||||
gorm.io/driver/sqlserver v1.0.7/go.mod h1:ng66aHI47ZIKz/vvnxzDoonzmTS8HXP+JYlgg67wOog=
|
||||
gorm.io/driver/sqlite v1.1.5 h1:JU8G59VyKu1x1RMQgjefQnkZjDe9wHc1kARDZPu5dZs=
|
||||
gorm.io/driver/sqlite v1.1.5/go.mod h1:NpaYMcVKEh6vLJ47VP6T7Weieu4H1Drs3dGD/K6GrGc=
|
||||
gorm.io/driver/sqlserver v1.0.9 h1:P7Dm/BKqsrOjyhRSnLXvG2g1W/eJUgxdrdBwgJw3tEg=
|
||||
gorm.io/driver/sqlserver v1.0.9/go.mod h1:iBdxY2CepkTt9Q1r84RbZA1qCai300Qlp8kQf9qE9II=
|
||||
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.21.3/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.21.6/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
gorm.io/gorm v1.21.11 h1:CxkXW6Cc+VIBlL8yJEHq+Co4RYXdSLiMKNvgoZPjLK4=
|
||||
gorm.io/gorm v1.21.11/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
gorm.io/gorm v1.21.14/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
gorm.io/gorm v1.21.15 h1:gAyaDoPw0lCyrSFWhBlahbUA1U4P5RViC1uIqoB+1Rk=
|
||||
gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1434,9 +1482,9 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY=
|
||||
honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210603230628-bf05d8b52dda h1:N1UNOTFyoz00Zw10uv9elxer4zdyqqhsMOOqAFPVTfM=
|
||||
inet.af/netaddr v0.0.0-20210603230628-bf05d8b52dda/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e h1:tvgqez5ZQoBBiBAGNU/fmJy247yB/7++kcLOEoMYup0=
|
||||
inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e/go.mod h1:fG3G1dekmK8oDX3iVzt8c0zICLMLSN8SjdxbXVt0WjU=
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756/go.mod h1:ViGMZRA6+RA318D7GCncrjv5gHUrPYrNDejjU12tikA=
|
||||
@@ -1446,13 +1494,9 @@ mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIa
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
tailscale.com v1.10.0 h1:3EWYxpXkCmXsMh1WgqoEjQ/xalxzxU+YD5ZmtaHS5cY=
|
||||
tailscale.com v1.10.0/go.mod h1:kgFF5AZPTltwdXjX2/ci4ghlcO3qKNWVIjD9s39pr8c=
|
||||
tailscale.com v1.10.2 h1:0EbwydLGDxw7//yB5/1GTKz3hDJvGTUCajPZZPMDDGQ=
|
||||
tailscale.com v1.10.2/go.mod h1:kgFF5AZPTltwdXjX2/ci4ghlcO3qKNWVIjD9s39pr8c=
|
||||
tailscale.com v1.14.2 h1:iSbnr+3eVt0ZsRJQG+KLw46K8Hq2fbCMSH/dGoJXnkY=
|
||||
tailscale.com v1.14.2/go.mod h1:WbJAb2AwrBO8jGQgxZ8E2mj3l628a3ysdPV7bJHBKlY=
|
||||
|
@@ -1,34 +1,97 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"inet.af/netaddr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
var _ = check.Suite(&IntegrationSuite{})
|
||||
var (
|
||||
integrationTmpDir string
|
||||
ih Headscale
|
||||
)
|
||||
|
||||
type IntegrationSuite struct{}
|
||||
var (
|
||||
pool dockertest.Pool
|
||||
network dockertest.Network
|
||||
headscale dockertest.Resource
|
||||
)
|
||||
|
||||
var integrationTmpDir string
|
||||
var ih Headscale
|
||||
var tailscaleVersions = []string{"1.14.3", "1.12.3"}
|
||||
|
||||
var pool dockertest.Pool
|
||||
var network dockertest.Network
|
||||
var headscale dockertest.Resource
|
||||
var tailscaleCount int = 10
|
||||
var tailscales map[string]dockertest.Resource
|
||||
type TestNamespace struct {
|
||||
count int
|
||||
tailscales map[string]dockertest.Resource
|
||||
}
|
||||
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
stats *suite.SuiteInformation
|
||||
|
||||
namespaces map[string]TestNamespace
|
||||
}
|
||||
|
||||
func TestIntegrationTestSuite(t *testing.T) {
|
||||
s := new(IntegrationTestSuite)
|
||||
|
||||
s.namespaces = map[string]TestNamespace{
|
||||
"main": {
|
||||
count: 20,
|
||||
tailscales: make(map[string]dockertest.Resource),
|
||||
},
|
||||
"shared": {
|
||||
count: 5,
|
||||
tailscales: make(map[string]dockertest.Resource),
|
||||
},
|
||||
}
|
||||
|
||||
suite.Run(t, s)
|
||||
|
||||
// HandleStats, which allows us to check if we passed and save logs
|
||||
// is called after TearDown, so we cannot tear down containers before
|
||||
// we have potentially saved the logs.
|
||||
for _, scales := range s.namespaces {
|
||||
for _, tailscale := range scales.tailscales {
|
||||
if err := pool.Purge(&tailscale); err != nil {
|
||||
log.Printf("Could not purge resource: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !s.stats.Passed() {
|
||||
err := saveLog(&headscale, "test_output")
|
||||
if err != nil {
|
||||
log.Printf("Could not save log: %s\n", err)
|
||||
}
|
||||
}
|
||||
if err := pool.Purge(&headscale); err != nil {
|
||||
log.Printf("Could not purge resource: %s\n", err)
|
||||
}
|
||||
|
||||
if err := network.Close(); err != nil {
|
||||
log.Printf("Could not close network: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func executeCommand(resource *dockertest.Resource, cmd []string) (string, error) {
|
||||
var stdout bytes.Buffer
|
||||
@@ -55,6 +118,48 @@ func executeCommand(resource *dockertest.Resource, cmd []string) (string, error)
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
func saveLog(resource *dockertest.Resource, basePath string) error {
|
||||
err := os.MkdirAll(basePath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
err = pool.Client.Logs(
|
||||
docker.LogsOptions{
|
||||
Context: context.TODO(),
|
||||
Container: resource.Container.ID,
|
||||
OutputStream: &stdout,
|
||||
ErrorStream: &stderr,
|
||||
Tail: "all",
|
||||
RawTerminal: false,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Follow: false,
|
||||
Timestamps: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
||||
|
||||
err = ioutil.WriteFile(path.Join(basePath, resource.Container.Name+".stdout.log"), []byte(stdout.String()), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(path.Join(basePath, resource.Container.Name+".stderr.log"), []byte(stdout.String()), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dockerRestartPolicy(config *docker.HostConfig) {
|
||||
// set AutoRemove to true so that stopped container goes away by itself
|
||||
config.AutoRemove = true
|
||||
@@ -63,7 +168,33 @@ func dockerRestartPolicy(config *docker.HostConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationSuite) SetUpSuite(c *check.C) {
|
||||
func tailscaleContainer(namespace, identifier, version string) (string, *dockertest.Resource) {
|
||||
tailscaleBuildOptions := &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile.tailscale",
|
||||
ContextDir: ".",
|
||||
BuildArgs: []docker.BuildArg{
|
||||
{
|
||||
Name: "TAILSCALE_VERSION",
|
||||
Value: version,
|
||||
},
|
||||
},
|
||||
}
|
||||
hostname := fmt.Sprintf("%s-tailscale-%s-%s", namespace, strings.Replace(version, ".", "-", -1), identifier)
|
||||
tailscaleOptions := &dockertest.RunOptions{
|
||||
Name: hostname,
|
||||
Networks: []*dockertest.Network{&network},
|
||||
Cmd: []string{"tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"},
|
||||
}
|
||||
|
||||
pts, err := pool.BuildAndRunWithBuildOptions(tailscaleBuildOptions, tailscaleOptions, dockerRestartPolicy)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start resource: %s", err)
|
||||
}
|
||||
fmt.Printf("Created %s container\n", hostname)
|
||||
return hostname, pts
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) SetupSuite() {
|
||||
var err error
|
||||
h = Headscale{
|
||||
dbType: "sqlite3",
|
||||
@@ -87,11 +218,6 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
|
||||
ContextDir: ".",
|
||||
}
|
||||
|
||||
tailscaleBuildOptions := &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile.tailscale",
|
||||
ContextDir: ".",
|
||||
}
|
||||
|
||||
currentPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not determine current path: %s", err)
|
||||
@@ -104,12 +230,10 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
|
||||
fmt.Sprintf("%s/derp.yaml:/etc/headscale/derp.yaml", currentPath),
|
||||
},
|
||||
Networks: []*dockertest.Network{&network},
|
||||
// Cmd: []string{"sleep", "3600"},
|
||||
Cmd: []string{"headscale", "serve"},
|
||||
Cmd: []string{"headscale", "serve"},
|
||||
PortBindings: map[docker.Port][]docker.PortBinding{
|
||||
"8080/tcp": []docker.PortBinding{{HostPort: "8080"}},
|
||||
"8080/tcp": {{HostPort: "8080"}},
|
||||
},
|
||||
Env: []string{},
|
||||
}
|
||||
|
||||
fmt.Println("Creating headscale container")
|
||||
@@ -121,24 +245,13 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
|
||||
fmt.Println("Created headscale container")
|
||||
|
||||
fmt.Println("Creating tailscale containers")
|
||||
tailscales = make(map[string]dockertest.Resource)
|
||||
for i := 0; i < tailscaleCount; i++ {
|
||||
hostname := fmt.Sprintf("tailscale%d", i)
|
||||
tailscaleOptions := &dockertest.RunOptions{
|
||||
Name: hostname,
|
||||
Networks: []*dockertest.Network{&network},
|
||||
// Make the container run until killed
|
||||
// Cmd: []string{"sleep", "3600"},
|
||||
Cmd: []string{"tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"},
|
||||
Env: []string{},
|
||||
}
|
||||
for namespace, scales := range s.namespaces {
|
||||
for i := 0; i < scales.count; i++ {
|
||||
version := tailscaleVersions[i%len(tailscaleVersions)]
|
||||
|
||||
if pts, err := pool.BuildAndRunWithBuildOptions(tailscaleBuildOptions, tailscaleOptions, dockerRestartPolicy); err == nil {
|
||||
tailscales[hostname] = *pts
|
||||
} else {
|
||||
log.Fatalf("Could not start resource: %s", err)
|
||||
hostname, container := tailscaleContainer(namespace, fmt.Sprint(i), version)
|
||||
scales.tailscales[hostname] = *container
|
||||
}
|
||||
fmt.Printf("Created %s container\n", hostname)
|
||||
}
|
||||
|
||||
fmt.Println("Waiting for headscale to be ready")
|
||||
@@ -159,69 +272,264 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
|
||||
}
|
||||
fmt.Println("headscale container is ready")
|
||||
|
||||
fmt.Println("Creating headscale namespace")
|
||||
result, err := executeCommand(
|
||||
&headscale,
|
||||
[]string{"headscale", "namespaces", "create", "test"},
|
||||
)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
fmt.Println("Creating pre auth key")
|
||||
authKey, err := executeCommand(
|
||||
&headscale,
|
||||
[]string{"headscale", "-n", "test", "preauthkeys", "create", "--reusable", "--expiration", "24h"},
|
||||
)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
headscaleEndpoint := fmt.Sprintf("http://headscale:%s", headscale.GetPort("8080/tcp"))
|
||||
|
||||
fmt.Printf("Joining tailscale containers to headscale at %s\n", headscaleEndpoint)
|
||||
for hostname, tailscale := range tailscales {
|
||||
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)
|
||||
result, err = executeCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
for namespace, scales := range s.namespaces {
|
||||
fmt.Printf("Creating headscale namespace: %s\n", namespace)
|
||||
result, err := executeCommand(
|
||||
&headscale,
|
||||
[]string{"headscale", "namespaces", "create", namespace},
|
||||
)
|
||||
fmt.Println("tailscale result: ", result)
|
||||
c.Assert(err, check.IsNil)
|
||||
fmt.Printf("%s joined\n", hostname)
|
||||
}
|
||||
}
|
||||
assert.Nil(s.T(), err)
|
||||
fmt.Println("headscale create namespace result: ", result)
|
||||
|
||||
func (s *IntegrationSuite) TearDownSuite(c *check.C) {
|
||||
if err := pool.Purge(&headscale); err != nil {
|
||||
log.Printf("Could not purge resource: %s\n", err)
|
||||
}
|
||||
fmt.Printf("Creating pre auth key for %s\n", namespace)
|
||||
authKey, err := executeCommand(
|
||||
&headscale,
|
||||
[]string{"headscale", "--namespace", namespace, "preauthkeys", "create", "--reusable", "--expiration", "24h"},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
for _, tailscale := range tailscales {
|
||||
if err := pool.Purge(&tailscale); err != nil {
|
||||
log.Printf("Could not purge resource: %s\n", err)
|
||||
headscaleEndpoint := fmt.Sprintf("http://headscale:%s", headscale.GetPort("8080/tcp"))
|
||||
|
||||
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}
|
||||
|
||||
fmt.Println("Join command:", command)
|
||||
fmt.Printf("Running join command for %s\n", hostname)
|
||||
result, err := executeCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
)
|
||||
fmt.Println("tailscale result: ", result)
|
||||
assert.Nil(s.T(), err)
|
||||
fmt.Printf("%s joined\n", hostname)
|
||||
}
|
||||
}
|
||||
|
||||
if err := network.Close(); err != nil {
|
||||
log.Printf("Could not close network: %s\n", err)
|
||||
// The nodes need a bit of time to get their updated maps from headscale
|
||||
// TODO: See if we can have a more deterministic wait here.
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) HandleStats(suiteName string, stats *suite.SuiteInformation) {
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestListNodes() {
|
||||
for namespace, scales := range s.namespaces {
|
||||
fmt.Println("Listing nodes")
|
||||
result, err := executeCommand(
|
||||
&headscale,
|
||||
[]string{"headscale", "--namespace", namespace, "nodes", "list"},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
fmt.Printf("List nodes: \n%s\n", result)
|
||||
|
||||
// Chck that the correct count of host is present in node list
|
||||
lines := strings.Split(result, "\n")
|
||||
assert.Equal(s.T(), len(scales.tailscales), len(lines)-2)
|
||||
|
||||
for hostname := range scales.tailscales {
|
||||
assert.Contains(s.T(), result, hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationSuite) TestListNodes(c *check.C) {
|
||||
fmt.Println("Listing nodes")
|
||||
func (s *IntegrationTestSuite) TestGetIpAddresses() {
|
||||
for _, scales := range s.namespaces {
|
||||
ipPrefix := netaddr.MustParseIPPrefix("100.64.0.0/10")
|
||||
ips, err := getIPs(scales.tailscales)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
for hostname := range scales.tailscales {
|
||||
s.T().Run(hostname, func(t *testing.T) {
|
||||
ip := ips[hostname]
|
||||
|
||||
fmt.Printf("IP for %s: %s\n", hostname, ip)
|
||||
|
||||
// c.Assert(ip.Valid(), check.IsTrue)
|
||||
assert.True(t, ip.Is4())
|
||||
assert.True(t, ipPrefix.Contains(ip))
|
||||
|
||||
ips[hostname] = ip
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestStatus() {
|
||||
for _, scales := range s.namespaces {
|
||||
ips, err := getIPs(scales.tailscales)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
for hostname, tailscale := range scales.tailscales {
|
||||
s.T().Run(hostname, func(t *testing.T) {
|
||||
command := []string{"tailscale", "status", "--json"}
|
||||
|
||||
fmt.Printf("Getting status for %s\n", hostname)
|
||||
result, err := executeCommand(
|
||||
&tailscale,
|
||||
command,
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var status ipnstate.Status
|
||||
err = json.Unmarshal([]byte(result), &status)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
// TODO(kradalby): Replace this check with peer length of SAME namespace
|
||||
// Check if we have as many nodes in status
|
||||
// as we have IPs/tailscales
|
||||
// lines := strings.Split(result, "\n")
|
||||
// assert.Equal(t, len(ips), len(lines)-1)
|
||||
// assert.Equal(t, len(scales.tailscales), len(lines)-1)
|
||||
|
||||
peerIps := getIPsfromIPNstate(status)
|
||||
|
||||
// Check that all hosts is present in all hosts status
|
||||
for ipHostname, ip := range ips {
|
||||
if hostname != ipHostname {
|
||||
assert.Contains(t, peerIps, ip)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP {
|
||||
ips := make([]netaddr.IP, 0)
|
||||
|
||||
for _, peer := range status.Peer {
|
||||
ips = append(ips, peer.TailscaleIPs...)
|
||||
}
|
||||
|
||||
return ips
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestPingAllPeers() {
|
||||
for _, 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) {
|
||||
// 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(),
|
||||
}
|
||||
|
||||
fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, ips[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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestSharedNodes() {
|
||||
main := s.namespaces["main"]
|
||||
shared := s.namespaces["shared"]
|
||||
|
||||
result, err := executeCommand(
|
||||
&headscale,
|
||||
[]string{"headscale", "-n", "test", "nodes", "list"},
|
||||
[]string{"headscale", "nodes", "list", "-o", "json", "--namespace", "shared"},
|
||||
)
|
||||
c.Assert(err, check.IsNil)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
for hostname, _ := range tailscales {
|
||||
c.Assert(strings.Contains(result, hostname), check.Equals, true)
|
||||
var machineList []Machine
|
||||
err = json.Unmarshal([]byte(result), &machineList)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
for _, machine := range machineList {
|
||||
|
||||
result, err := executeCommand(
|
||||
&headscale,
|
||||
[]string{"headscale", "nodes", "share", "--namespace", "shared", fmt.Sprint(machine.ID), "main"},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
|
||||
fmt.Println("Shared node with result: ", result)
|
||||
}
|
||||
|
||||
result, err = executeCommand(
|
||||
&headscale,
|
||||
[]string{"headscale", "nodes", "list", "--namespace", "main"},
|
||||
)
|
||||
assert.Nil(s.T(), err)
|
||||
fmt.Println("Nodelist after sharing", result)
|
||||
|
||||
// Chck that the correct count of host is present in node list
|
||||
lines := strings.Split(result, "\n")
|
||||
assert.Equal(s.T(), len(main.tailscales)+len(shared.tailscales), len(lines)-2)
|
||||
|
||||
for hostname := range main.tailscales {
|
||||
assert.Contains(s.T(), result, hostname)
|
||||
}
|
||||
|
||||
for hostname := range shared.tailscales {
|
||||
assert.Contains(s.T(), result, hostname)
|
||||
}
|
||||
|
||||
// TODO(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)
|
||||
|
||||
// mainIps, err := getIPs(main.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(),
|
||||
// }
|
||||
|
||||
// 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")
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
func (s *IntegrationSuite) TestGetIpAddresses(c *check.C) {
|
||||
ipPrefix := netaddr.MustParseIPPrefix("100.64.0.0/10")
|
||||
func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, error) {
|
||||
ips := make(map[string]netaddr.IP)
|
||||
for hostname, tailscale := range tailscales {
|
||||
command := []string{"tailscale", "ip"}
|
||||
@@ -230,17 +538,16 @@ func (s *IntegrationSuite) TestGetIpAddresses(c *check.C) {
|
||||
&tailscale,
|
||||
command,
|
||||
)
|
||||
c.Assert(err, check.IsNil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ip, err := netaddr.ParseIP(strings.TrimSuffix(result, "\n"))
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
fmt.Printf("IP for %s: %s", hostname, result)
|
||||
|
||||
// c.Assert(ip.Valid(), check.IsTrue)
|
||||
c.Assert(ip.Is4(), check.Equals, true)
|
||||
c.Assert(ipPrefix.Contains(ip), check.Equals, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ips[hostname] = ip
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"db_type": "sqlite3",
|
||||
"db_path": "/tmp/integration_test_db.sqlite3",
|
||||
"acl_policy_path": "",
|
||||
"log_level": "trace"
|
||||
"log_level": "debug"
|
||||
}
|
||||
|
@@ -89,7 +89,6 @@ Use "headscale [command] --help" for more information about a command.
|
||||
|
||||
# TODO / Ideas
|
||||
|
||||
- Github action to publish the docker image
|
||||
- Interpolate `email:` option to the ClusterIssuer from site configuration.
|
||||
This probably needs to be done with a transformer, kustomize vars don't seem to work.
|
||||
- Add kustomize examples for cloud-native ingress, load balancer
|
||||
|
208
machine.go
208
machine.go
@@ -2,6 +2,7 @@ package headscale
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -31,8 +32,9 @@ type Machine struct {
|
||||
AuthKeyID uint
|
||||
AuthKey *PreAuthKey
|
||||
|
||||
LastSeen *time.Time
|
||||
Expiry *time.Time
|
||||
LastSeen *time.Time
|
||||
LastSuccessfulUpdate *time.Time
|
||||
Expiry *time.Time
|
||||
|
||||
HostInfo datatypes.JSON
|
||||
Endpoints datatypes.JSON
|
||||
@@ -48,7 +50,9 @@ func (m Machine) isAlreadyRegistered() bool {
|
||||
return m.Registered
|
||||
}
|
||||
|
||||
func (m Machine) toNode() (*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) {
|
||||
nKey, err := wgkey.ParseHex(m.NodeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -83,24 +87,26 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
|
||||
allowedIPs := []netaddr.IPPrefix{}
|
||||
allowedIPs = append(allowedIPs, ip) // we append the node own IP, as it is required by the clients
|
||||
|
||||
routesStr := []string{}
|
||||
if len(m.EnabledRoutes) != 0 {
|
||||
allwIps, err := m.EnabledRoutes.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if includeRoutes {
|
||||
routesStr := []string{}
|
||||
if len(m.EnabledRoutes) != 0 {
|
||||
allwIps, err := m.EnabledRoutes.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(allwIps, &routesStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err = json.Unmarshal(allwIps, &routesStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, aip := range routesStr {
|
||||
ip, err := netaddr.ParseIPPrefix(aip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for _, routeStr := range routesStr {
|
||||
ip, err := netaddr.ParseIPPrefix(routeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowedIPs = append(allowedIPs, ip)
|
||||
}
|
||||
allowedIPs = append(allowedIPs, ip)
|
||||
}
|
||||
|
||||
endpoints := []string{}
|
||||
@@ -134,13 +140,20 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
|
||||
derp = "127.3.3.40:0" // Zero means disconnected or unknown.
|
||||
}
|
||||
|
||||
var keyExpiry time.Time
|
||||
if m.Expiry != nil {
|
||||
keyExpiry = *m.Expiry
|
||||
} else {
|
||||
keyExpiry = time.Time{}
|
||||
}
|
||||
|
||||
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,
|
||||
User: tailcfg.UserID(m.NamespaceID),
|
||||
Key: tailcfg.NodeKey(nKey),
|
||||
KeyExpiry: *m.Expiry,
|
||||
KeyExpiry: keyExpiry,
|
||||
Machine: tailcfg.MachineKey(mKey),
|
||||
DiscoKey: discoKey,
|
||||
Addresses: addrs,
|
||||
@@ -159,6 +172,11 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
|
||||
}
|
||||
|
||||
func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) {
|
||||
log.Trace().
|
||||
Str("func", "getPeers").
|
||||
Str("machine", m.Name).
|
||||
Msg("Finding peers")
|
||||
|
||||
machines := []Machine{}
|
||||
if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered",
|
||||
m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil {
|
||||
@@ -166,15 +184,34 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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 = ?",
|
||||
m.NamespaceID).Find(&sharedMachines).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peers := []*tailcfg.Node{}
|
||||
for _, mn := range machines {
|
||||
peer, err := mn.toNode()
|
||||
peer, err := mn.toNode(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peers = append(peers, peer)
|
||||
}
|
||||
for _, sharedMachine := range sharedMachines {
|
||||
peer, err := sharedMachine.Machine.toNode(false) // shared nodes do not expose their routes
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peers = append(peers, peer)
|
||||
}
|
||||
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
|
||||
|
||||
log.Trace().
|
||||
Str("func", "getPeers").
|
||||
Str("machine", m.Name).
|
||||
Msgf("Found peers: %s", tailNodesToString(peers))
|
||||
return &peers, nil
|
||||
}
|
||||
|
||||
@@ -190,18 +227,27 @@ func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error)
|
||||
return &m, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
return nil, fmt.Errorf("machine not found")
|
||||
}
|
||||
|
||||
// GetMachineByID finds a Machine by ID and returns the Machine struct
|
||||
func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) {
|
||||
m := Machine{}
|
||||
if result := h.db.Find(&Machine{ID: id}).First(&m); result.Error != nil {
|
||||
if result := h.db.Preload("Namespace").Find(&Machine{ID: id}).First(&m); result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// UpdateMachine takes a Machine struct pointer (typically already loaded from database
|
||||
// and updates it with the latest data from the database.
|
||||
func (h *Headscale) UpdateMachine(m *Machine) error {
|
||||
if result := h.db.Find(m).First(&m); result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteMachine softs deletes a Machine from the database
|
||||
func (h *Headscale) DeleteMachine(m *Machine) error {
|
||||
m.Registered = false
|
||||
@@ -238,3 +284,121 @@ func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) {
|
||||
}
|
||||
return &hostinfo, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) notifyChangesToPeers(m *Machine) {
|
||||
peers, err := h.getPeers(*m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "notifyChangesToPeers").
|
||||
Str("machine", m.Name).
|
||||
Msgf("Error getting peers: %s", err)
|
||||
return
|
||||
}
|
||||
for _, p := range *peers {
|
||||
log.Info().
|
||||
Str("func", "notifyChangesToPeers").
|
||||
Str("machine", m.Name).
|
||||
Str("peer", p.Name).
|
||||
Str("address", p.Addresses[0].String()).
|
||||
Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0])
|
||||
err := h.sendRequestOnUpdateChannel(p)
|
||||
if err != nil {
|
||||
log.Info().
|
||||
Str("func", "notifyChangesToPeers").
|
||||
Str("machine", m.Name).
|
||||
Str("peer", p.Name).
|
||||
Msgf("Peer %s does not appear to be polling", p.Name)
|
||||
}
|
||||
log.Trace().
|
||||
Str("func", "notifyChangesToPeers").
|
||||
Str("machine", m.Name).
|
||||
Str("peer", p.Name).
|
||||
Str("address", p.Addresses[0].String()).
|
||||
Msgf("Notified peer %s (%s)", p.Name, p.Addresses[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Headscale) getOrOpenUpdateChannel(m *Machine) <-chan struct{} {
|
||||
var updateChan chan struct{}
|
||||
if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok {
|
||||
if unwrapped, ok := storedChan.(chan struct{}); ok {
|
||||
updateChan = unwrapped
|
||||
} else {
|
||||
log.Error().
|
||||
Str("handler", "openUpdateChannel").
|
||||
Str("machine", m.Name).
|
||||
Msg("Failed to convert update channel to struct{}")
|
||||
}
|
||||
} else {
|
||||
log.Debug().
|
||||
Str("handler", "openUpdateChannel").
|
||||
Str("machine", m.Name).
|
||||
Msg("Update channel not found, creating")
|
||||
|
||||
updateChan = make(chan struct{})
|
||||
h.clientsUpdateChannels.Store(m.ID, updateChan)
|
||||
}
|
||||
return updateChan
|
||||
}
|
||||
|
||||
func (h *Headscale) closeUpdateChannel(m *Machine) {
|
||||
h.clientsUpdateChannelMutex.Lock()
|
||||
defer h.clientsUpdateChannelMutex.Unlock()
|
||||
|
||||
if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok {
|
||||
if unwrapped, ok := storedChan.(chan struct{}); ok {
|
||||
close(unwrapped)
|
||||
}
|
||||
}
|
||||
h.clientsUpdateChannels.Delete(m.ID)
|
||||
}
|
||||
|
||||
func (h *Headscale) sendRequestOnUpdateChannel(m *tailcfg.Node) error {
|
||||
h.clientsUpdateChannelMutex.Lock()
|
||||
defer h.clientsUpdateChannelMutex.Unlock()
|
||||
|
||||
pUp, ok := h.clientsUpdateChannels.Load(uint64(m.ID))
|
||||
if ok {
|
||||
log.Info().
|
||||
Str("func", "requestUpdate").
|
||||
Str("machine", m.Name).
|
||||
Msgf("Notifying peer %s", m.Name)
|
||||
|
||||
if update, ok := pUp.(chan struct{}); ok {
|
||||
log.Trace().
|
||||
Str("func", "requestUpdate").
|
||||
Str("machine", m.Name).
|
||||
Msgf("Update channel is %#v", update)
|
||||
|
||||
update <- struct{}{}
|
||||
|
||||
log.Trace().
|
||||
Str("func", "requestUpdate").
|
||||
Str("machine", m.Name).
|
||||
Msgf("Notified machine %s", m.Name)
|
||||
}
|
||||
} else {
|
||||
log.Info().
|
||||
Str("func", "requestUpdate").
|
||||
Str("machine", m.Name).
|
||||
Msgf("Machine %s does not appear to be polling", m.Name)
|
||||
return errors.New("machine does not seem to be polling")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Headscale) isOutdated(m *Machine) bool {
|
||||
err := h.UpdateMachine(m)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
lastChange := h.getLastStateChange(m.Namespace.Name)
|
||||
log.Trace().
|
||||
Str("func", "keepAlive").
|
||||
Str("machine", m.Name).
|
||||
Time("last_successful_update", *m.LastSuccessfulUpdate).
|
||||
Time("last_state_change", lastChange).
|
||||
Msgf("Checking if %s is missing updates", m.Name)
|
||||
return m.LastSuccessfulUpdate.Before(lastChange)
|
||||
}
|
||||
|
@@ -91,12 +91,34 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) {
|
||||
}
|
||||
|
||||
machines := []Machine{}
|
||||
if err := h.db.Preload("AuthKey").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
|
||||
if err := h.db.Preload("AuthKey").Preload("Namespace").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &machines, nil
|
||||
}
|
||||
|
||||
// ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace
|
||||
func (h *Headscale) ListSharedMachinesInNamespace(name string) (*[]Machine, error) {
|
||||
namespace, err := h.GetNamespace(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sharedMachines := []SharedMachine{}
|
||||
if err := h.db.Preload("Namespace").Where(&SharedMachine{NamespaceID: namespace.ID}).Find(&sharedMachines).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
machines := []Machine{}
|
||||
for _, sharedMachine := range sharedMachines {
|
||||
machine, err := h.GetMachineByID(sharedMachine.MachineID) // otherwise not everything comes filled
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
machines = append(machines, *machine)
|
||||
}
|
||||
return &machines, nil
|
||||
}
|
||||
|
||||
// SetMachineNamespace assigns a Machine to a namespace
|
||||
func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error {
|
||||
n, err := h.GetNamespace(namespaceName)
|
||||
@@ -169,25 +191,7 @@ func (h *Headscale) checkForNamespacesPendingUpdates() {
|
||||
continue
|
||||
}
|
||||
for _, m := range *machines {
|
||||
peers, _ := h.getPeers(m)
|
||||
for _, p := range *peers {
|
||||
pUp, ok := h.clientsPolling.Load(uint64(p.ID))
|
||||
if ok {
|
||||
log.Info().
|
||||
Str("func", "checkForNamespacesPendingUpdates").
|
||||
Str("machine", m.Name).
|
||||
Str("peer", m.Name).
|
||||
Str("address", p.Addresses[0].String()).
|
||||
Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0])
|
||||
pUp.(chan []byte) <- []byte{}
|
||||
} else {
|
||||
log.Info().
|
||||
Str("func", "checkForNamespacesPendingUpdates").
|
||||
Str("machine", m.Name).
|
||||
Str("peer", m.Name).
|
||||
Msgf("Peer %s does not appear to be polling", p.Name)
|
||||
}
|
||||
}
|
||||
h.notifyChangesToPeers(&m)
|
||||
}
|
||||
}
|
||||
newV, err := h.getValue("namespaces_pending_updates")
|
||||
|
454
poll.go
Normal file
454
poll.go
Normal file
@@ -0,0 +1,454 @@
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// PollNetMapHandler takes care of /machine/:id/map
|
||||
//
|
||||
// This is the busiest endpoint, as it keeps the HTTP long poll that updates
|
||||
// the clients when something in the network changes.
|
||||
//
|
||||
// The clients POST stuff like HostInfo and their Endpoints here, but
|
||||
// only after their first request (marked with the ReadOnly field).
|
||||
//
|
||||
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
|
||||
func (h *Headscale) PollNetMapHandler(c *gin.Context) {
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Msg("PollNetMapHandler called")
|
||||
body, _ := io.ReadAll(c.Request.Body)
|
||||
mKeyStr := c.Param("id")
|
||||
mKey, err := wgkey.ParseHex(mKeyStr)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Err(err).
|
||||
Msg("Cannot parse client key")
|
||||
c.String(http.StatusBadRequest, "")
|
||||
return
|
||||
}
|
||||
req := tailcfg.MapRequest{}
|
||||
err = decode(body, &req, &mKey, h.privateKey)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Err(err).
|
||||
Msg("Cannot decode message")
|
||||
c.String(http.StatusBadRequest, "")
|
||||
return
|
||||
}
|
||||
|
||||
var m Machine
|
||||
if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
log.Warn().
|
||||
Str("handler", "PollNetMap").
|
||||
Msgf("Ignoring request, cannot find machine with key %s", mKey.HexString())
|
||||
c.String(http.StatusUnauthorized, "")
|
||||
return
|
||||
}
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Str("machine", m.Name).
|
||||
Msg("Found machine in database")
|
||||
|
||||
hostinfo, _ := json.Marshal(req.Hostinfo)
|
||||
m.Name = req.Hostinfo.Hostname
|
||||
m.HostInfo = datatypes.JSON(hostinfo)
|
||||
m.DiscoKey = wgkey.Key(req.DiscoKey).HexString()
|
||||
now := time.Now().UTC()
|
||||
|
||||
// From Tailscale client:
|
||||
//
|
||||
// ReadOnly is whether the client just wants to fetch the MapResponse,
|
||||
// without updating their Endpoints. The Endpoints field will be ignored and
|
||||
// LastSeen will not be updated and peers will not be notified of changes.
|
||||
//
|
||||
// The intended use is for clients to discover the DERP map at start-up
|
||||
// before their first real endpoint update.
|
||||
if !req.ReadOnly {
|
||||
endpoints, _ := json.Marshal(req.Endpoints)
|
||||
m.Endpoints = datatypes.JSON(endpoints)
|
||||
m.LastSeen = &now
|
||||
}
|
||||
h.db.Save(&m)
|
||||
|
||||
data, err := h.getMapResponse(mKey, req, m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Str("machine", m.Name).
|
||||
Err(err).
|
||||
Msg("Failed to get Map response")
|
||||
c.String(http.StatusInternalServerError, ":(")
|
||||
return
|
||||
}
|
||||
|
||||
// We update our peers if the client is not sending ReadOnly in the MapRequest
|
||||
// so we don't distribute its initial request (it comes with
|
||||
// empty endpoints to peers)
|
||||
|
||||
// Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696
|
||||
log.Debug().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Str("machine", m.Name).
|
||||
Bool("readOnly", req.ReadOnly).
|
||||
Bool("omitPeers", req.OmitPeers).
|
||||
Bool("stream", req.Stream).
|
||||
Msg("Client map request processed")
|
||||
|
||||
if req.ReadOnly {
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Client is starting up. Probably interested in a DERP map")
|
||||
c.Data(200, "application/json; charset=utf-8", *data)
|
||||
return
|
||||
}
|
||||
|
||||
// There has been an update to _any_ of the nodes that the other nodes would
|
||||
// need to know about
|
||||
h.setLastStateChangeToNow(m.Namespace.Name)
|
||||
|
||||
// The request is not ReadOnly, so we need to set up channels for updating
|
||||
// peers via longpoll
|
||||
|
||||
// Only create update channel if it has not been created
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Str("machine", m.Name).
|
||||
Msg("Loading or creating update channel")
|
||||
updateChan := h.getOrOpenUpdateChannel(&m)
|
||||
|
||||
pollDataChan := make(chan []byte)
|
||||
// defer close(pollData)
|
||||
|
||||
keepAliveChan := make(chan []byte)
|
||||
|
||||
cancelKeepAlive := make(chan struct{})
|
||||
defer close(cancelKeepAlive)
|
||||
|
||||
if req.OmitPeers && !req.Stream {
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Client sent endpoint update and is ok with a response without peer list")
|
||||
c.Data(200, "application/json; charset=utf-8", *data)
|
||||
|
||||
// It sounds like we should update the nodes when we have received a endpoint update
|
||||
// even tho the comments in the tailscale code dont explicitly say so.
|
||||
go h.notifyChangesToPeers(&m)
|
||||
return
|
||||
} else if req.OmitPeers && req.Stream {
|
||||
log.Warn().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Ignoring request, don't know how to handle it")
|
||||
c.String(http.StatusBadRequest, "")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Client is ready to access the tailnet")
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Sending initial map")
|
||||
go func() { pollDataChan <- *data }()
|
||||
|
||||
log.Info().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("machine", m.Name).
|
||||
Msg("Notifying peers")
|
||||
go h.notifyChangesToPeers(&m)
|
||||
|
||||
h.PollNetMapStream(c, m, req, mKey, pollDataChan, keepAliveChan, updateChan, cancelKeepAlive)
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMap").
|
||||
Str("id", c.Param("id")).
|
||||
Str("machine", m.Name).
|
||||
Msg("Finished stream, closing PollNetMap session")
|
||||
}
|
||||
|
||||
// PollNetMapStream takes care of /machine/:id/map
|
||||
// stream logic, ensuring we communicate updates and data
|
||||
// to the connected clients.
|
||||
func (h *Headscale) PollNetMapStream(
|
||||
c *gin.Context,
|
||||
m Machine,
|
||||
req tailcfg.MapRequest,
|
||||
mKey wgkey.Key,
|
||||
pollDataChan chan []byte,
|
||||
keepAliveChan chan []byte,
|
||||
updateChan <-chan struct{},
|
||||
cancelKeepAlive chan struct{},
|
||||
) {
|
||||
go h.scheduledPollWorker(cancelKeepAlive, keepAliveChan, mKey, req, m)
|
||||
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Msg("Waiting for data to stream...")
|
||||
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
|
||||
|
||||
select {
|
||||
case data := <-pollDataChan:
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "pollData").
|
||||
Int("bytes", len(data)).
|
||||
Msg("Sending data received via pollData channel")
|
||||
_, err := w.Write(data)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "pollData").
|
||||
Err(err).
|
||||
Msg("Cannot write data")
|
||||
}
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "pollData").
|
||||
Int("bytes", len(data)).
|
||||
Msg("Data from pollData channel written successfully")
|
||||
// TODO: Abstract away all the database calls, this can cause race conditions
|
||||
// when an outdated machine object is kept alive, e.g. db is update from
|
||||
// command line, but then overwritten.
|
||||
err = h.UpdateMachine(&m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "pollData").
|
||||
Err(err).
|
||||
Msg("Cannot update machine from database")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
m.LastSeen = &now
|
||||
m.LastSuccessfulUpdate = &now
|
||||
h.db.Save(&m)
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "pollData").
|
||||
Int("bytes", len(data)).
|
||||
Msg("Machine updated successfully after sending pollData")
|
||||
return true
|
||||
|
||||
case data := <-keepAliveChan:
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "keepAlive").
|
||||
Int("bytes", len(data)).
|
||||
Msg("Sending keep alive message")
|
||||
_, err := w.Write(data)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "keepAlive").
|
||||
Err(err).
|
||||
Msg("Cannot write keep alive message")
|
||||
}
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "keepAlive").
|
||||
Int("bytes", len(data)).
|
||||
Msg("Keep alive sent successfully")
|
||||
// TODO: Abstract away all the database calls, this can cause race conditions
|
||||
// when an outdated machine object is kept alive, e.g. db is update from
|
||||
// command line, but then overwritten.
|
||||
err = h.UpdateMachine(&m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "keepAlive").
|
||||
Err(err).
|
||||
Msg("Cannot update machine from database")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
m.LastSeen = &now
|
||||
h.db.Save(&m)
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "keepAlive").
|
||||
Int("bytes", len(data)).
|
||||
Msg("Machine updated successfully after sending keep alive")
|
||||
return true
|
||||
|
||||
case <-updateChan:
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "update").
|
||||
Msg("Received a request for update")
|
||||
if h.isOutdated(&m) {
|
||||
log.Debug().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Time("last_successful_update", *m.LastSuccessfulUpdate).
|
||||
Time("last_state_change", h.getLastStateChange(m.Namespace.Name)).
|
||||
Msgf("There has been updates since the last successful update to %s", m.Name)
|
||||
data, err := h.getMapResponse(mKey, req, m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "update").
|
||||
Err(err).
|
||||
Msg("Could not get the map update")
|
||||
}
|
||||
_, err = w.Write(*data)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "update").
|
||||
Err(err).
|
||||
Msg("Could not write the map response")
|
||||
}
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "update").
|
||||
Msg("Updated Map has been sent")
|
||||
|
||||
// Keep track of the last successful update,
|
||||
// we sometimes end in a state were the update
|
||||
// is not picked up by a client and we use this
|
||||
// to determine if we should "force" an update.
|
||||
// TODO: Abstract away all the database calls, this can cause race conditions
|
||||
// when an outdated machine object is kept alive, e.g. db is update from
|
||||
// command line, but then overwritten.
|
||||
err = h.UpdateMachine(&m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "update").
|
||||
Err(err).
|
||||
Msg("Cannot update machine from database")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
m.LastSuccessfulUpdate = &now
|
||||
h.db.Save(&m)
|
||||
} else {
|
||||
log.Trace().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Time("last_successful_update", *m.LastSuccessfulUpdate).
|
||||
Time("last_state_change", h.getLastStateChange(m.Namespace.Name)).
|
||||
Msgf("%s is up to date", m.Name)
|
||||
}
|
||||
return true
|
||||
|
||||
case <-c.Request.Context().Done():
|
||||
log.Info().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Msg("The client has closed the connection")
|
||||
// TODO: Abstract away all the database calls, this can cause race conditions
|
||||
// when an outdated machine object is kept alive, e.g. db is update from
|
||||
// command line, but then overwritten.
|
||||
err := h.UpdateMachine(&m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("handler", "PollNetMapStream").
|
||||
Str("machine", m.Name).
|
||||
Str("channel", "Done").
|
||||
Err(err).
|
||||
Msg("Cannot update machine from database")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
m.LastSeen = &now
|
||||
h.db.Save(&m)
|
||||
|
||||
cancelKeepAlive <- struct{}{}
|
||||
|
||||
h.closeUpdateChannel(&m)
|
||||
|
||||
close(pollDataChan)
|
||||
|
||||
close(keepAliveChan)
|
||||
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Headscale) scheduledPollWorker(
|
||||
cancelChan <-chan struct{},
|
||||
keepAliveChan chan<- []byte,
|
||||
mKey wgkey.Key,
|
||||
req tailcfg.MapRequest,
|
||||
m Machine,
|
||||
) {
|
||||
keepAliveTicker := time.NewTicker(60 * time.Second)
|
||||
updateCheckerTicker := time.NewTicker(30 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancelChan:
|
||||
return
|
||||
|
||||
case <-keepAliveTicker.C:
|
||||
data, err := h.getMapKeepAliveResponse(mKey, req, m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "keepAlive").
|
||||
Err(err).
|
||||
Msg("Error generating the keep alive msg")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("func", "keepAlive").
|
||||
Str("machine", m.Name).
|
||||
Msg("Sending keepalive")
|
||||
keepAliveChan <- *data
|
||||
|
||||
case <-updateCheckerTicker.C:
|
||||
// Send an update request regardless of outdated or not, if data is sent
|
||||
// to the node is determined in the updateChan consumer block
|
||||
n, _ := m.toNode(true)
|
||||
err := h.sendRequestOnUpdateChannel(n)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("func", "keepAlive").
|
||||
Str("machine", m.Name).
|
||||
Err(err).
|
||||
Msgf("Failed to send update request to %s", m.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
136
routes.go
136
routes.go
@@ -2,62 +2,142 @@ package headscale
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"gorm.io/datatypes"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// GetNodeRoutes returns the subnet routes advertised by a node (identified by
|
||||
// GetAdvertisedNodeRoutes returns the subnet routes advertised by a node (identified by
|
||||
// namespace and node name)
|
||||
func (h *Headscale) GetNodeRoutes(namespace string, nodeName string) (*[]netaddr.IPPrefix, error) {
|
||||
func (h *Headscale) GetAdvertisedNodeRoutes(namespace string, nodeName string) (*[]netaddr.IPPrefix, error) {
|
||||
m, err := h.GetMachine(namespace, nodeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hi, err := m.GetHostInfo()
|
||||
hostInfo, err := m.GetHostInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hi.RoutableIPs, nil
|
||||
return &hostInfo.RoutableIPs, nil
|
||||
}
|
||||
|
||||
// GetEnabledNodeRoutes returns the subnet routes enabled by a node (identified by
|
||||
// namespace and node name)
|
||||
func (h *Headscale) GetEnabledNodeRoutes(namespace string, nodeName string) ([]netaddr.IPPrefix, error) {
|
||||
m, err := h.GetMachine(namespace, nodeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := m.EnabledRoutes.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routesStr := []string{}
|
||||
err = json.Unmarshal(data, &routesStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes := make([]netaddr.IPPrefix, len(routesStr))
|
||||
for index, routeStr := range routesStr {
|
||||
route, err := netaddr.ParseIPPrefix(routeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes[index] = route
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// IsNodeRouteEnabled checks if a certain route has been enabled
|
||||
func (h *Headscale) IsNodeRouteEnabled(namespace string, nodeName string, routeStr string) bool {
|
||||
route, err := netaddr.ParseIPPrefix(routeStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
enabledRoutes, err := h.GetEnabledNodeRoutes(namespace, nodeName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, enabledRoute := range enabledRoutes {
|
||||
if route == enabledRoute {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// EnableNodeRoute enables a subnet route advertised by a node (identified by
|
||||
// namespace and node name)
|
||||
func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr string) (*netaddr.IPPrefix, error) {
|
||||
func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr string) error {
|
||||
m, err := h.GetMachine(namespace, nodeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hi, err := m.GetHostInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
route, err := netaddr.ParseIPPrefix(routeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rIP := range hi.RoutableIPs {
|
||||
if rIP == route {
|
||||
routes, _ := json.Marshal([]string{routeStr}) // TODO: only one for the time being, so overwriting the rest
|
||||
m.EnabledRoutes = datatypes.JSON(routes)
|
||||
h.db.Save(&m)
|
||||
availableRoutes, err := h.GetAdvertisedNodeRoutes(namespace, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// THIS IS COMPLETELY USELESS.
|
||||
// The peers map is stored in memory in the server process.
|
||||
// Definitely not accessible from the CLI tool.
|
||||
// We need RPC to the server - or some kind of 'needsUpdate' field in the DB
|
||||
peers, _ := h.getPeers(*m)
|
||||
for _, p := range *peers {
|
||||
if pUp, ok := h.clientsPolling.Load(uint64(p.ID)); ok {
|
||||
pUp.(chan []byte) <- []byte{}
|
||||
}
|
||||
enabledRoutes, err := h.GetEnabledNodeRoutes(namespace, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
available := false
|
||||
for _, availableRoute := range *availableRoutes {
|
||||
// If the route is available, and not yet enabled, add it to the new routing table
|
||||
if route == availableRoute {
|
||||
available = true
|
||||
if !h.IsNodeRouteEnabled(namespace, nodeName, routeStr) {
|
||||
enabledRoutes = append(enabledRoutes, route)
|
||||
}
|
||||
return &rIP, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("could not find routable range")
|
||||
if !available {
|
||||
return fmt.Errorf("route (%s) is not available on node %s", nodeName, routeStr)
|
||||
}
|
||||
|
||||
routes, err := json.Marshal(enabledRoutes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.EnabledRoutes = datatypes.JSON(routes)
|
||||
h.db.Save(&m)
|
||||
|
||||
err = h.RequestMapUpdates(m.NamespaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RoutesToPtables converts the list of routes to a nice table
|
||||
func (h *Headscale) RoutesToPtables(namespace string, nodeName string, availableRoutes []netaddr.IPPrefix) pterm.TableData {
|
||||
d := pterm.TableData{{"Route", "Enabled"}}
|
||||
|
||||
for _, route := range availableRoutes {
|
||||
enabled := h.IsNodeRouteEnabled(namespace, nodeName, route.String())
|
||||
|
||||
d = append(d, []string{route.String(), strconv.FormatBool(enabled)})
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ func (s *Suite) TestGetRoutes(c *check.C) {
|
||||
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine("test", "testmachine")
|
||||
_, err = h.GetMachine("test", "test_get_route_machine")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
route, err := netaddr.ParseIPPrefix("10.0.0.0/24")
|
||||
@@ -33,7 +33,7 @@ func (s *Suite) TestGetRoutes(c *check.C) {
|
||||
MachineKey: "foo",
|
||||
NodeKey: "bar",
|
||||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
Name: "test_get_route_machine",
|
||||
NamespaceID: n.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
@@ -42,14 +42,87 @@ func (s *Suite) TestGetRoutes(c *check.C) {
|
||||
}
|
||||
h.db.Save(&m)
|
||||
|
||||
r, err := h.GetNodeRoutes("test", "testmachine")
|
||||
r, err := h.GetAdvertisedNodeRoutes("test", "test_get_route_machine")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(*r), check.Equals, 1)
|
||||
|
||||
_, err = h.EnableNodeRoute("test", "testmachine", "192.168.0.0/24")
|
||||
err = h.EnableNodeRoute("test", "test_get_route_machine", "192.168.0.0/24")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
_, err = h.EnableNodeRoute("test", "testmachine", "10.0.0.0/24")
|
||||
err = h.EnableNodeRoute("test", "test_get_route_machine", "10.0.0.0/24")
|
||||
c.Assert(err, check.IsNil)
|
||||
}
|
||||
|
||||
func (s *Suite) TestGetEnableRoutes(c *check.C) {
|
||||
n, err := h.CreateNamespace("test")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = h.GetMachine("test", "test_enable_route_machine")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
route, err := netaddr.ParseIPPrefix(
|
||||
"10.0.0.0/24",
|
||||
)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
route2, err := netaddr.ParseIPPrefix(
|
||||
"150.0.10.0/25",
|
||||
)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
hi := tailcfg.Hostinfo{
|
||||
RoutableIPs: []netaddr.IPPrefix{route, route2},
|
||||
}
|
||||
hostinfo, err := json.Marshal(hi)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
m := Machine{
|
||||
ID: 0,
|
||||
MachineKey: "foo",
|
||||
NodeKey: "bar",
|
||||
DiscoKey: "faa",
|
||||
Name: "test_enable_route_machine",
|
||||
NamespaceID: n.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
AuthKeyID: uint(pak.ID),
|
||||
HostInfo: datatypes.JSON(hostinfo),
|
||||
}
|
||||
h.db.Save(&m)
|
||||
|
||||
availableRoutes, err := h.GetAdvertisedNodeRoutes("test", "test_enable_route_machine")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(*availableRoutes), check.Equals, 2)
|
||||
|
||||
enabledRoutes, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(enabledRoutes), check.Equals, 0)
|
||||
|
||||
err = h.EnableNodeRoute("test", "test_enable_route_machine", "192.168.0.0/24")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
err = h.EnableNodeRoute("test", "test_enable_route_machine", "10.0.0.0/24")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
enabledRoutes1, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(enabledRoutes1), check.Equals, 1)
|
||||
|
||||
// Adding it twice will just let it pass through
|
||||
err = h.EnableNodeRoute("test", "test_enable_route_machine", "10.0.0.0/24")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
enabledRoutes2, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(enabledRoutes2), check.Equals, 1)
|
||||
|
||||
err = h.EnableNodeRoute("test", "test_enable_route_machine", "150.0.10.0/25")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
enabledRoutes3, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(enabledRoutes3), check.Equals, 2)
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e -o pipefail
|
||||
commit="$1"
|
||||
|
37
sharing.go
Normal file
37
sharing.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package headscale
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
const errorSameNamespace = Error("Destination namespace same as origin")
|
||||
const errorMachineAlreadyShared = Error("Node already shared to this namespace")
|
||||
|
||||
// SharedMachine is a join table to support sharing nodes between namespaces
|
||||
type SharedMachine struct {
|
||||
gorm.Model
|
||||
MachineID uint64
|
||||
Machine Machine
|
||||
NamespaceID uint
|
||||
Namespace Namespace
|
||||
}
|
||||
|
||||
// AddSharedMachineToNamespace adds a machine as a shared node to a namespace
|
||||
func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error {
|
||||
if m.NamespaceID == ns.ID {
|
||||
return errorSameNamespace
|
||||
}
|
||||
|
||||
sharedMachine := SharedMachine{}
|
||||
if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sharedMachine).Error; err == nil {
|
||||
return errorMachineAlreadyShared
|
||||
}
|
||||
|
||||
sharedMachine = SharedMachine{
|
||||
MachineID: m.ID,
|
||||
Machine: *m,
|
||||
NamespaceID: ns.ID,
|
||||
Namespace: *ns,
|
||||
}
|
||||
h.db.Save(&sharedMachine)
|
||||
|
||||
return nil
|
||||
}
|
359
sharing_test.go
Normal file
359
sharing_test.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"gopkg.in/check.v1"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (s *Suite) TestBasicSharedNodesInNamespace(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)
|
||||
|
||||
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)
|
||||
|
||||
p1sAfter, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(*p1sAfter), check.Equals, 1)
|
||||
c.Assert((*p1sAfter)[0].ID, check.Equals, tailcfg.NodeID(m2.ID))
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
p1s, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(*p1s), check.Equals, 0)
|
||||
|
||||
err = h.AddSharedMachineToNamespace(&m1, n1)
|
||||
c.Assert(err, check.Equals, errorSameNamespace)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
err = h.AddSharedMachineToNamespace(&m2, n1)
|
||||
c.Assert(err, check.Equals, errorMachineAlreadyShared)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
p1sAfter, err := h.getPeers(m1)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(*p1sAfter), check.Equals, 1)
|
||||
c.Assert(len((*p1sAfter)[0].AllowedIPs), check.Equals, 1)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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",
|
||||
Name: "test_get_shared_nodes_4",
|
||||
NamespaceID: n1.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: "authKey",
|
||||
IPAddress: "100.64.0.4",
|
||||
AuthKeyID: uint(pak4.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
|
||||
|
||||
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) // nodes 1, 2, 4
|
||||
|
||||
pAlone, err := h.getPeers(m3)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(*pAlone), check.Equals, 0) // node 3 is alone
|
||||
}
|
17
utils.go
17
utils.go
@@ -10,9 +10,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
@@ -58,6 +60,7 @@ func encode(v interface{}, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return encodeMsg(b, pubKey, privKey)
|
||||
}
|
||||
|
||||
@@ -139,3 +142,17 @@ func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func tailNodesToString(nodes []*tailcfg.Node) string {
|
||||
temp := make([]string, len(nodes))
|
||||
|
||||
for index, node := range nodes {
|
||||
temp[index] = node.Name
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp))
|
||||
}
|
||||
|
||||
func tailMapResponseToString(resp tailcfg.MapResponse) string {
|
||||
return fmt.Sprintf("{ Node: %s, Peers: %s }", resp.Node.Name, tailNodesToString(resp.Peers))
|
||||
}
|
||||
|
Reference in New Issue
Block a user