Compare commits

..

18 Commits

Author SHA1 Message Date
Kristoffer Dalby
51fb014e58 Add notes about current ssh status
Signed-off-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-11-26 09:34:37 +01:00
Even Holthe
1acf5e5791 Add experimental flag to unit test 2022-11-25 14:49:00 +01:00
Even Holthe
fc00cb3aa4 Add SSH ACL to changelog 2022-11-25 14:49:00 +01:00
Kristoffer Dalby
b4c63a7e0a Mark the flag properly experimental
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Kristoffer Dalby
1297616a65 Enable SSH for tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Kristoffer Dalby
5038761eca Add feature flag for SSH, and warning
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Kristoffer Dalby
5b25c4c3d5 Allow WithEnv to be passed multiple times
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Kristoffer Dalby
f9ade5ea37 Mark all tests with Parallel
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Kristoffer Dalby
10feae2d4b Do not retry on permission denied in ssh
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Kristoffer Dalby
1ecb3e4c2a Strip newline from hostname
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Kristoffer Dalby
f72de09cd9 Add negative tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Even Holthe
794e57f1c2 SSH: add test between namespaces 2022-11-25 14:49:00 +01:00
Even Holthe
37bc8f364f SSH: Lint and typos 2022-11-25 14:49:00 +01:00
Kristoffer Dalby
5e0ba8c70f Make simple initial test case
This commit makes the initial SSH test a bit simpler:

- Use the same pattern/functions for all clients as other tests
- Only test within _one_ namespace/user to confirm the base case
- Use retry function, same as taildrop, there is some funky going on
  there...

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Kristoffer Dalby
d4a51b0d2a Add method to expose container id
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Kristoffer Dalby
44e37e21c2 Ensure we have ssh in container
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-25 14:49:00 +01:00
Even Holthe
93d03029e4 SSH integration test setup 2022-11-25 14:49:00 +01:00
Even Holthe
cd6df097ad Add SSH capability advertisement
Advertises the SSH capability, and parses the SSH ACLs to pass to the
tailscale client. Doesn’t support ‘autogroup’ ACL functionality.

Co-authored-by: Daniel Brooks <db48x@headline.com>
2022-11-25 14:49:00 +01:00
190 changed files with 5793 additions and 13606 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
# These are supported funding model platforms
ko_fi: headscale

View File

@@ -8,14 +8,9 @@ on:
branches: branches:
- main - main
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: write-all
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -37,34 +32,10 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
- name: Run build - name: Run build
id: build
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: | run: nix build
nix build |& tee build-result
BUILD_STATUS="${PIPESTATUS[0]}"
OLD_HASH=$(cat build-result | grep specified: | awk -F ':' '{print $2}' | sed 's/ //g') - uses: actions/upload-artifact@v2
NEW_HASH=$(cat build-result | grep got: | awk -F ':' '{print $2}' | sed 's/ //g')
echo "OLD_HASH=$OLD_HASH" >> $GITHUB_OUTPUT
echo "NEW_HASH=$NEW_HASH" >> $GITHUB_OUTPUT
exit $BUILD_STATUS
- name: Nix gosum diverging
uses: actions/github-script@v6
if: failure() && steps.build.outcome == 'failure'
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.pulls.createReviewComment({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Nix build failed with wrong gosum, please update "vendorSha256" (${{ steps.build.outputs.OLD_HASH }}) for the "headscale" package in flake.nix with the new SHA: ${{ steps.build.outputs.NEW_HASH }}'
})
- uses: actions/upload-artifact@v3
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
with: with:
name: headscale-linux name: headscale-linux

View File

@@ -1,45 +0,0 @@
name: Build documentation
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install python
uses: actions/setup-python@v4
with:
python-version: 3.x
- name: Setup cache
uses: actions/cache@v2
with:
key: ${{ github.ref }}
path: .cache
- name: Setup dependencies
run: pip install mkdocs-material pillow cairosvg mkdocs-minify-plugin
- name: Build docs
run: mkdocs build --strict
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: ./site
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

View File

@@ -1,23 +0,0 @@
name: GitHub Actions Version Updater
# Controls when the action will run.
on:
schedule:
# Automatically run on every Sunday
- cron: "0 0 * * 0"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
# [Required] Access token with `workflow` scope.
token: ${{ secrets.WORKFLOW_SECRET }}
- name: Run GitHub Actions Version Updater
uses: saadmk11/github-actions-version-updater@v0.7.1
with:
# [Required] Access token with `workflow` scope.
token: ${{ secrets.WORKFLOW_SECRET }}

View File

@@ -3,10 +3,6 @@ name: Lint
on: [push, pull_request] on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
golangci-lint: golangci-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -30,7 +26,7 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
with: with:
version: v1.51.2 version: v1.49.0
# Only block PRs on new problems. # Only block PRs on new problems.
# If this is not enabled, we will end up having PRs # If this is not enabled, we will end up having PRs
@@ -63,7 +59,7 @@ jobs:
- name: Prettify code - name: Prettify code
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
uses: creyD/prettier_action@v4.3 uses: creyD/prettier_action@v4.0
with: with:
prettier_options: >- prettier_options: >-
--check **/*.{ts,js,md,yaml,yml,sass,css,scss,html} --check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}

View File

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

View File

@@ -9,16 +9,155 @@ on:
jobs: jobs:
goreleaser: goreleaser:
runs-on: ubuntu-18.04 # due to CGO we need to user an older version
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19.0
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y gcc-aarch64-linux-gnu
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Docker meta
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
type=raw,value=develop
- 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 }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
build-args: |
VERSION=${{ steps.meta.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- uses: cachix/install-nix-action@v16 docker-debug-release:
runs-on: ubuntu-latest
- name: Run goreleaser steps:
run: nix develop --command -- goreleaser release --clean - name: Checkout
env: uses: actions/checkout@v3
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache-debug
key: ${{ runner.os }}-buildx-debug-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-debug-
- name: Docker meta
id: meta-debug
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
flavor: |
suffix=-debug,onlatest=true
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
type=raw,value=develop
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
file: Dockerfile.debug
tags: ${{ steps.meta-debug.outputs.tags }}
labels: ${{ steps.meta-debug.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-debug
cache-to: type=local,dest=/tmp/.buildx-cache-debug-new
build-args: |
VERSION=${{ steps.meta-debug.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache-debug
mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLAllowStarDst
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLAllowStarDst$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLAllowUser80Dst
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLAllowUser80Dst$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLAllowUserDst
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLAllowUserDst$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLDenyAllPort80
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLDenyAllPort80$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLDevice1CanAccessDevice2
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLDevice1CanAccessDevice2$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLHostsInNetMapTable
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLHostsInNetMapTable$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLNamedHostsCanReach
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLNamedHostsCanReach$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLNamedHostsCanReachBySubnet
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLNamedHostsCanReachBySubnet$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestAuthKeyLogoutAndRelogin
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestAuthKeyLogoutAndRelogin$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestAuthWebFlowAuthenticationPingAll
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestAuthWebFlowAuthenticationPingAll$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestAuthWebFlowLogoutAndRelogin
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestAuthWebFlowLogoutAndRelogin$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestCreateTailscale
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestCreateTailscale$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestEnablingRoutes
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestEnablingRoutes$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestEphemeral
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestEphemeral$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestExpireNode
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestExpireNode$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestHeadscale
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestHeadscale$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestOIDCAuthenticationPingAll
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestOIDCAuthenticationPingAll$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestOIDCExpireNodesBasedOnTokenExpiry
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestOIDCExpireNodesBasedOnTokenExpiry$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestPingAllByHostname
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestPingAllByHostname$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestPingAllByIP
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestPingAllByIP$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestPreAuthKeyCommand
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestPreAuthKeyCommand$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestPreAuthKeyCommandReusableEphemeral
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestPreAuthKeyCommandReusableEphemeral$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestPreAuthKeyCommandWithoutExpiry
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestPreAuthKeyCommandWithoutExpiry$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestResolveMagicDNS
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestResolveMagicDNS$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSHIsBlockedInACL
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestSSHIsBlockedInACL$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSHMultipleUsersAllToAll
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestSSHMultipleUsersAllToAll$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSHNoSSHConfigured
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestSSHNoSSHConfigured$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSHOneUserAllToAll
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestSSHOneUserAllToAll$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestSSUserOnlyIsolation
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestSSUserOnlyIsolation$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestTaildrop
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestTaildrop$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestTailscaleNodesJoiningHeadcale
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestTailscaleNodesJoiningHeadcale$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -1,57 +0,0 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestUserCommand
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestUserCommand$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View File

@@ -0,0 +1,27 @@
name: Integration Test v2 - kradalby
on: [pull_request]
jobs:
integration-test-v2-kradalby:
runs-on: [self-hosted, linux, x64, nixos, docker]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_v2_general

View File

@@ -2,10 +2,6 @@ name: Tests
on: [push, pull_request] on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest

12
.gitignore vendored
View File

@@ -1,5 +1,3 @@
ignored/
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe *.exe
*.exe~ *.exe~
@@ -14,9 +12,8 @@ ignored/
*.out *.out
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
vendor/ # vendor/
dist/
/headscale /headscale
config.json config.json
config.yaml config.yaml
@@ -29,15 +26,10 @@ derp.yaml
# Exclude Jetbrains Editors # Exclude Jetbrains Editors
.idea .idea
test_output/ test_output/
control_logs/
# Nix build output # Nix build output
result result
.direnv/ .direnv/
integration_test/etc/config.dump.yaml integration_test/etc/config.dump.yaml
# MkDocs
.cache
/site

View File

@@ -29,14 +29,6 @@ linters:
- execinquery - execinquery
- exhaustruct - exhaustruct
- nolintlint - nolintlint
- musttag # causes issues with imported libs
# deprecated
- structcheck # replaced by unused
- ifshort # deprecated by the owner
- varcheck # replaced by unused
- nosnakecase # replaced by revive
- deadcode # replaced by unused
# We should strive to enable these: # We should strive to enable these:
- wrapcheck - wrapcheck

View File

@@ -1,28 +1,21 @@
--- ---
before: before:
hooks: hooks:
- go mod tidy -compat=1.20 - go mod tidy -compat=1.19
- go mod vendor
release: release:
prerelease: auto prerelease: auto
builds: builds:
- id: headscale - id: darwin-amd64
main: ./cmd/headscale/headscale.go main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}" mod_timestamp: "{{ .CommitTimestamp }}"
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
targets: goos:
- darwin_amd64 - darwin
- darwin_arm64 goarch:
- freebsd_amd64 - amd64
- linux_386
- linux_amd64
- linux_arm64
- linux_arm_5
- linux_arm_6
- linux_arm_7
flags: flags:
- -mod=readonly - -mod=readonly
ldflags: ldflags:
@@ -30,56 +23,60 @@ builds:
tags: tags:
- ts2019 - ts2019
- id: darwin-arm64
main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- darwin
goarch:
- arm64
flags:
- -mod=readonly
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-amd64
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
main: ./cmd/headscale/headscale.go
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-arm64
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- arm64
main: ./cmd/headscale/headscale.go
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
archives: archives:
- id: golang-cross - id: golang-cross
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' builds:
- darwin-amd64
- darwin-arm64
- linux-amd64
- linux-arm64
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
format: binary format: binary
source:
enabled: true
name_template: "{{ .ProjectName }}_{{ .Version }}"
format: tar.gz
files:
- "vendor/"
nfpms:
# Configure nFPM for .deb and .rpm releases
#
# See https://nfpm.goreleaser.com/configuration/
# and https://goreleaser.com/customization/nfpm/
#
# Useful tools for debugging .debs:
# List file contents: dpkg -c dist/headscale...deb
# Package metadata: dpkg --info dist/headscale....deb
#
- builds:
- headscale
package_name: headscale
priority: optional
vendor: headscale
maintainer: Kristoffer Dalby <kristoffer@dalby.cc>
homepage: https://github.com/juanfont/headscale
license: BSD
bindir: /usr/bin
formats:
- deb
# - rpm
contents:
- src: ./config-example.yaml
dst: /etc/headscale/config.yaml
type: config|noreplace
file_info:
mode: 0644
- src: ./docs/packaging/headscale.systemd.service
dst: /etc/systemd/system/headscale.service
- dst: /var/lib/headscale
type: dir
- dst: /var/run/headscale
type: dir
scripts:
postinstall: ./docs/packaging/postinstall.sh
postremove: ./docs/packaging/postremove.sh
checksum: checksum:
name_template: "checksums.txt" name_template: "checksums.txt"
snapshot: snapshot:

View File

@@ -1 +0,0 @@
.github/workflows/test-integration-v2*

View File

@@ -1,93 +1,15 @@
# CHANGELOG # CHANGELOG
## 0.23.0 (2023-XX-XX) ## 0.17.0 (2022-XX-XX)
### Changes
## 0.22.0 (2023-04-20)
### Changes
- Add `.deb` packages to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
- Update and simplify the documentation to use new `.deb` packages [#1349](https://github.com/juanfont/headscale/pull/1349)
- Add 32-bit Arm platforms to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
- Fix longstanding bug that would prevent "\*" from working properly in ACLs (issue [#699](https://github.com/juanfont/headscale/issues/699)) [#1279](https://github.com/juanfont/headscale/pull/1279)
- Fix issue where IPv6 could not be used in, or while using ACLs (part of [#809](https://github.com/juanfont/headscale/issues/809)) [#1339](https://github.com/juanfont/headscale/pull/1339)
- Target Go 1.20 and Tailscale 1.38 for Headscale [#1323](https://github.com/juanfont/headscale/pull/1323)
## 0.21.0 (2023-03-20)
### Changes
- Adding "configtest" CLI command. [#1230](https://github.com/juanfont/headscale/pull/1230)
- Add documentation on connecting with iOS to `/apple` [#1261](https://github.com/juanfont/headscale/pull/1261)
- Update iOS compatibility and added documentation for iOS [#1264](https://github.com/juanfont/headscale/pull/1264)
- Allow to delete routes [#1244](https://github.com/juanfont/headscale/pull/1244)
## 0.20.0 (2023-02-03)
### Changes
- Fix wrong behaviour in exit nodes [#1159](https://github.com/juanfont/headscale/pull/1159)
- Align behaviour of `dns_config.restricted_nameservers` to tailscale [#1162](https://github.com/juanfont/headscale/pull/1162)
- Make OpenID Connect authenticated client expiry time configurable [#1191](https://github.com/juanfont/headscale/pull/1191)
- defaults to 180 days like Tailscale SaaS
- adds option to use the expiry time from the OpenID token for the node (see config-example.yaml)
- Set ControlTime in Map info sent to nodes [#1195](https://github.com/juanfont/headscale/pull/1195)
- Populate Tags field on Node updates sent [#1195](https://github.com/juanfont/headscale/pull/1195)
## 0.19.0 (2023-01-29)
### BREAKING ### BREAKING
- Rename Namespace to User [#1144](https://github.com/juanfont/headscale/pull/1144)
- **BACKUP your database before upgrading**
- Command line flags previously taking `--namespace` or `-n` will now require `--user` or `-u`
## 0.18.0 (2023-01-14)
### Changes
- Reworked routing and added support for subnet router failover [#1024](https://github.com/juanfont/headscale/pull/1024)
- Added an OIDC AllowGroups Configuration options and authorization check [#1041](https://github.com/juanfont/headscale/pull/1041)
- Set `db_ssl` to false by default [#1052](https://github.com/juanfont/headscale/pull/1052)
- Fix duplicate nodes due to incorrect implementation of the protocol [#1058](https://github.com/juanfont/headscale/pull/1058)
- Report if a machine is online in CLI more accurately [#1062](https://github.com/juanfont/headscale/pull/1062)
- Added config option for custom DNS records [#1035](https://github.com/juanfont/headscale/pull/1035)
- Expire nodes based on OIDC token expiry [#1067](https://github.com/juanfont/headscale/pull/1067)
- Remove ephemeral nodes on logout [#1098](https://github.com/juanfont/headscale/pull/1098)
- Performance improvements in ACLs [#1129](https://github.com/juanfont/headscale/pull/1129)
- OIDC client secret can be passed via a file [#1127](https://github.com/juanfont/headscale/pull/1127)
## 0.17.1 (2022-12-05)
### Changes
- Correct typo on macOS standalone profile link [#1028](https://github.com/juanfont/headscale/pull/1028)
- Update platform docs with Fast User Switching [#1016](https://github.com/juanfont/headscale/pull/1016)
## 0.17.0 (2022-11-26)
### BREAKING
- `noise.private_key_path` has been added and is required for the new noise protocol.
- Log level option `log_level` was moved to a distinct `log` config section and renamed to `level` [#768](https://github.com/juanfont/headscale/pull/768) - Log level option `log_level` was moved to a distinct `log` config section and renamed to `level` [#768](https://github.com/juanfont/headscale/pull/768)
- Removed Alpine Linux container image [#962](https://github.com/juanfont/headscale/pull/962) - Removed Alpine Linux container image [#962](https://github.com/juanfont/headscale/pull/962)
### Important Changes
- Added support for Tailscale TS2021 protocol [#738](https://github.com/juanfont/headscale/pull/738)
- Add experimental support for [SSH ACL](https://tailscale.com/kb/1018/acls/#tailscale-ssh) (see docs for limitations) [#847](https://github.com/juanfont/headscale/pull/847)
- Please note that this support should be considered _partially_ implemented
- SSH ACLs status:
- Support `accept` and `check` (SSH can be enabled and used for connecting and authentication)
- Rejecting connections **are not supported**, meaning that if you enable SSH, then assume that _all_ `ssh` connections **will be allowed**.
- If you decied to try this feature, please carefully managed permissions by blocking port `22` with regular ACLs or do _not_ set `--ssh` on your clients.
- We are currently improving our testing of the SSH ACLs, help us get an overview by testing and giving feedback.
- This feature should be considered dangerous and it is disabled by default. Enable by setting `HEADSCALE_EXPERIMENTAL_FEATURE_SSH=1`.
### Changes ### Changes
- Added support for Tailscale TS2021 protocol [#738](https://github.com/juanfont/headscale/pull/738)
- Add ability to specify config location via env var `HEADSCALE_CONFIG` [#674](https://github.com/juanfont/headscale/issues/674) - Add ability to specify config location via env var `HEADSCALE_CONFIG` [#674](https://github.com/juanfont/headscale/issues/674)
- Target Go 1.19 for Headscale [#778](https://github.com/juanfont/headscale/pull/778) - Target Go 1.19 for Headscale [#778](https://github.com/juanfont/headscale/pull/778)
- Target Tailscale v1.30.0 to build Headscale [#780](https://github.com/juanfont/headscale/pull/780) - Target Tailscale v1.30.0 to build Headscale [#780](https://github.com/juanfont/headscale/pull/780)
@@ -107,6 +29,14 @@
- Fix OIDC registration issues [#960](https://github.com/juanfont/headscale/pull/960) and [#971](https://github.com/juanfont/headscale/pull/971) - Fix OIDC registration issues [#960](https://github.com/juanfont/headscale/pull/960) and [#971](https://github.com/juanfont/headscale/pull/971)
- Add support for specifying NextDNS DNS-over-HTTPS resolver [#940](https://github.com/juanfont/headscale/pull/940) - Add support for specifying NextDNS DNS-over-HTTPS resolver [#940](https://github.com/juanfont/headscale/pull/940)
- Make more sslmode available for postgresql connection [#927](https://github.com/juanfont/headscale/pull/927) - Make more sslmode available for postgresql connection [#927](https://github.com/juanfont/headscale/pull/927)
- Add experimental support for [SSH ACL](https://tailscale.com/kb/1018/acls/#tailscale-ssh) (see docs for limitations) [#847](https://github.com/juanfont/headscale/pull/847)
- Please note that this support should be considered _partially_ implemented
- SSH ACLs status:
- Support `accept` and `check` (SSH can be enabled and used for connecting and authentication)
- Rejecting connections **are not supported**, meaning that if you enable SSH, then assume that _all_ `ssh` connections **will be allowed**.
- If you decied to try this feature, please carefully managed permissions by blocking port `22` with regular ACLs or do _not_ set `--ssh` on your clients.
- We are currently improving our testing of the SSH ACLs, help us get an overview by testing and giving feedback.
- This feature should be considered dangerous and it is disabled by default. Enable by setting `HEADSCALE_EXPERIMENTAL_FEATURE_SSH=1`.
## 0.16.4 (2022-08-21) ## 0.16.4 (2022-08-21)

View File

@@ -1,5 +1,5 @@
# Builder image # Builder image
FROM docker.io/golang:1.20-bullseye AS build FROM docker.io/golang:1.19.0-bullseye AS build
ARG VERSION=dev ARG VERSION=dev
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale

View File

@@ -1,5 +1,5 @@
# Builder image # Builder image
FROM docker.io/golang:1.20-bullseye AS build FROM docker.io/golang:1.19.0-bullseye AS build
ARG VERSION=dev ARG VERSION=dev
ENV GOPATH /go ENV GOPATH /go
WORKDIR /go/src/headscale WORKDIR /go/src/headscale
@@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.c
RUN test -e /go/bin/headscale RUN test -e /go/bin/headscale
# Debug image # Debug image
FROM docker.io/golang:1.20.0-bullseye FROM docker.io/golang:1.19.0-bullseye
COPY --from=build /go/bin/headscale /bin/headscale COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC ENV TZ UTC

View File

@@ -26,7 +26,7 @@ dev: lint test build
test: test:
@go test $(TAGS) -short -coverprofile=coverage.out ./... @go test $(TAGS) -short -coverprofile=coverage.out ./...
test_integration: test_integration_cli test_integration_derp test_integration_v2_general test_integration: test_integration_cli test_integration_derp test_integration_oidc test_integration_v2_general
test_integration_cli: test_integration_cli:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true docker network rm $$(docker network ls --filter name=headscale --quiet) || true
@@ -36,7 +36,7 @@ test_integration_cli:
-v ~/.cache/hs-integration-go:/go \ -v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \ -v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \ -v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./... go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
test_integration_derp: test_integration_derp:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true docker network rm $$(docker network ls --filter name=headscale --quiet) || true
@@ -46,7 +46,7 @@ test_integration_derp:
-v ~/.cache/hs-integration-go:/go \ -v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \ -v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \ -v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./... go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
test_integration_v2_general: test_integration_v2_general:
docker run \ docker run \
@@ -56,7 +56,13 @@ test_integration_v2_general:
-v $$PWD:$$PWD -w $$PWD/integration \ -v $$PWD:$$PWD -w $$PWD/integration \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
golang:1 \ golang:1 \
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast ./... -timeout 120m -parallel 8 go test $(TAGS) -failfast ./... -timeout 120m -parallel 8
coverprofile_func:
go tool cover -func=coverage.out
coverprofile_html:
go tool cover -html=coverage.out
lint: lint:
golangci-lint run --fix --timeout 10m golangci-lint run --fix --timeout 10m
@@ -74,4 +80,11 @@ compress: build
generate: generate:
rm -rf gen rm -rf gen
buf generate proto go run github.com/bufbuild/buf/cmd/buf generate proto
install-protobuf-plugins:
go install \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc

339
README.md
View File

@@ -75,26 +75,12 @@ one of the maintainers.
| macOS | Yes (see `/apple` on your headscale for more information) | | macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes [docs](./docs/windows-client.md) | | Windows | Yes [docs](./docs/windows-client.md) |
| Android | Yes [docs](./docs/android-client.md) | | Android | Yes [docs](./docs/android-client.md) |
| iOS | Yes [docs](./docs/iOS-client.md) | | iOS | Not yet |
## Running headscale ## Running headscale
Please have a look at the documentation under [`docs/`](docs/). Please have a look at the documentation under [`docs/`](docs/).
## Graphical Control Panels
Headscale provides an API for complete management of your Tailnet.
These are community projects not directly affiliated with the Headscale project.
| Name | Repository Link | Description | Status |
| --------------- | ---------------------------------------------------- | ------------------------------------------------------ | ------ |
| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple Headscale web UI for small-scale deployments. | Alpha |
## Talks
- Fosdem 2023 (video): [Headscale: How we are using integration testing to reimplement Tailscale](https://fosdem.org/2023/schedule/event/goheadscale/)
- presented by Juan Font Alonso and Kristoffer Dalby
## Disclaimer ## Disclaimer
1. We have nothing to do with Tailscale, or Tailscale Inc. 1. We have nothing to do with Tailscale, or Tailscale Inc.
@@ -188,6 +174,13 @@ make build
<sub style="font-size:14px"><b>Juan Font</b></sub> <sub style="font-size:14px"><b>Juan Font</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/restanrm>
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
<br />
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cure> <a href=https://github.com/cure>
<img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/> <img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/>
@@ -209,6 +202,8 @@ make build
<sub style="font-size:14px"><b>Benjamin Roberts</b></sub> <sub style="font-size:14px"><b>Benjamin Roberts</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/reynico> <a href=https://github.com/reynico>
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/> <img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
@@ -216,15 +211,6 @@ make build
<sub style="font-size:14px"><b>Nico</b></sub> <sub style="font-size:14px"><b>Nico</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/evenh>
<img src=https://avatars.githubusercontent.com/u/2701536?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Even Holthe/>
<br />
<sub style="font-size:14px"><b>Even Holthe</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/e-zk> <a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/> <img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
@@ -253,15 +239,6 @@ make build
<sub style="font-size:14px"><b>unreality</b></sub> <sub style="font-size:14px"><b>unreality</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mpldr>
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
<br />
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ohdearaugustin> <a href=https://github.com/ohdearaugustin>
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/> <img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
@@ -269,11 +246,13 @@ make build
<sub style="font-size:14px"><b>ohdearaugustin</b></sub> <sub style="font-size:14px"><b>ohdearaugustin</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/restanrm> <a href=https://github.com/mpldr>
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/> <img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
<br /> <br />
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub> <sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -283,13 +262,6 @@ make build
<sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub> <sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/christian-heusel>
<img src=https://avatars.githubusercontent.com/u/26827864?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Christian Heusel/>
<br />
<sub style="font-size:14px"><b>Christian Heusel</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mike-lloyd03> <a href=https://github.com/mike-lloyd03>
<img src=https://avatars.githubusercontent.com/u/49411532?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mike Lloyd/> <img src=https://avatars.githubusercontent.com/u/49411532?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mike Lloyd/>
@@ -304,8 +276,6 @@ make build
<sub style="font-size:14px"><b>Anton Schubert</b></sub> <sub style="font-size:14px"><b>Anton Schubert</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Niek> <a href=https://github.com/Niek>
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/> <img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
@@ -320,6 +290,8 @@ make build
<sub style="font-size:14px"><b>Eugen Biegler</b></sub> <sub style="font-size:14px"><b>Eugen Biegler</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/617a7a> <a href=https://github.com/617a7a>
<img src=https://avatars.githubusercontent.com/u/67651251?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Azz/> <img src=https://avatars.githubusercontent.com/u/67651251?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Azz/>
@@ -348,15 +320,6 @@ make build
<sub style="font-size:14px"><b>Laurent Marchaud</b></sub> <sub style="font-size:14px"><b>Laurent Marchaud</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/majst01>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fdelucchijr> <a href=https://github.com/fdelucchijr>
<img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/> <img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/>
@@ -371,6 +334,8 @@ make build
<sub style="font-size:14px"><b>Orville Q. Song</b></sub> <sub style="font-size:14px"><b>Orville Q. Song</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/hdhoang> <a href=https://github.com/hdhoang>
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hdhoang/> <img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hdhoang/>
@@ -385,15 +350,6 @@ make build
<sub style="font-size:14px"><b>bravechamp</b></sub> <sub style="font-size:14px"><b>bravechamp</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/bravechamp>
<img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
<br />
<sub style="font-size:14px"><b>bravechamp</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/deonthomasgy> <a href=https://github.com/deonthomasgy>
<img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/> <img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/>
@@ -422,6 +378,8 @@ make build
<sub style="font-size:14px"><b>Mevan Samaratunga</b></sub> <sub style="font-size:14px"><b>Mevan Samaratunga</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dragetd> <a href=https://github.com/dragetd>
<img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./> <img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./>
@@ -436,8 +394,6 @@ make build
<sub style="font-size:14px"><b>Paul Tötterman</b></sub> <sub style="font-size:14px"><b>Paul Tötterman</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/samson4649> <a href=https://github.com/samson4649>
<img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/> <img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/>
@@ -445,6 +401,13 @@ make build
<sub style="font-size:14px"><b>Samuel Lock</b></sub> <sub style="font-size:14px"><b>Samuel Lock</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/majst01>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kevin1sMe> <a href=https://github.com/kevin1sMe>
<img src=https://avatars.githubusercontent.com/u/6886076?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kevinlin/> <img src=https://avatars.githubusercontent.com/u/6886076?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kevinlin/>
@@ -452,13 +415,6 @@ make build
<sub style="font-size:14px"><b>kevinlin</b></sub> <sub style="font-size:14px"><b>kevinlin</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/QZAiXH>
<img src=https://avatars.githubusercontent.com/u/23068780?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Snack/>
<br />
<sub style="font-size:14px"><b>Snack</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/artemklevtsov> <a href=https://github.com/artemklevtsov>
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/> <img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
@@ -466,6 +422,8 @@ make build
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub> <sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cmars> <a href=https://github.com/cmars>
<img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/> <img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/>
@@ -473,22 +431,6 @@ make build
<sub style="font-size:14px"><b>Casey Marshall</b></sub> <sub style="font-size:14px"><b>Casey Marshall</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dbevacqua>
<img src=https://avatars.githubusercontent.com/u/6534306?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=dbevacqua/>
<br />
<sub style="font-size:14px"><b>dbevacqua</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/joshuataylor>
<img src=https://avatars.githubusercontent.com/u/225131?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Josh Taylor/>
<br />
<sub style="font-size:14px"><b>Josh Taylor</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/CNLHC> <a href=https://github.com/CNLHC>
<img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/> <img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/>
@@ -496,13 +438,6 @@ make build
<sub style="font-size:14px"><b>LiuHanCheng</b></sub> <sub style="font-size:14px"><b>LiuHanCheng</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/motiejus>
<img src=https://avatars.githubusercontent.com/u/107720?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Motiejus Jakštys/>
<br />
<sub style="font-size:14px"><b>Motiejus Jakštys</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pvinis> <a href=https://github.com/pvinis>
<img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/> <img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/>
@@ -524,8 +459,6 @@ make build
<sub style="font-size:14px"><b>Steven Honson</b></sub> <sub style="font-size:14px"><b>Steven Honson</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ratsclub> <a href=https://github.com/ratsclub>
<img src=https://avatars.githubusercontent.com/u/25647735?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Victor Freire/> <img src=https://avatars.githubusercontent.com/u/25647735?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Victor Freire/>
@@ -533,6 +466,15 @@ make build
<sub style="font-size:14px"><b>Victor Freire</b></sub> <sub style="font-size:14px"><b>Victor Freire</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lachy2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
<br />
<sub style="font-size:14px"><b>lachy2849</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/t56k> <a href=https://github.com/t56k>
<img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/> <img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/>
@@ -540,13 +482,6 @@ make build
<sub style="font-size:14px"><b>thomas</b></sub> <sub style="font-size:14px"><b>thomas</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/linsomniac>
<img src=https://avatars.githubusercontent.com/u/466380?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Sean Reifschneider/>
<br />
<sub style="font-size:14px"><b>Sean Reifschneider</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aberoham> <a href=https://github.com/aberoham>
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/> <img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
@@ -554,13 +489,6 @@ make build
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub> <sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/iFargle>
<img src=https://avatars.githubusercontent.com/u/124551390?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Albert Copeland/>
<br />
<sub style="font-size:14px"><b>Albert Copeland</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/puzpuzpuz> <a href=https://github.com/puzpuzpuz>
<img src=https://avatars.githubusercontent.com/u/37772591?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andrei Pechkurov/> <img src=https://avatars.githubusercontent.com/u/37772591?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andrei Pechkurov/>
@@ -568,15 +496,6 @@ make build
<sub style="font-size:14px"><b>Andrei Pechkurov</b></sub> <sub style="font-size:14px"><b>Andrei Pechkurov</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/theryecatcher>
<img src=https://avatars.githubusercontent.com/u/16442416?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anoop Sundaresh/>
<br />
<sub style="font-size:14px"><b>Anoop Sundaresh</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/apognu> <a href=https://github.com/apognu>
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/> <img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
@@ -584,13 +503,6 @@ make build
<sub style="font-size:14px"><b>Antoine POPINEAU</b></sub> <sub style="font-size:14px"><b>Antoine POPINEAU</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/tony1661>
<img src=https://avatars.githubusercontent.com/u/5287266?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antonio Fernandez/>
<br />
<sub style="font-size:14px"><b>Antonio Fernandez</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aofei> <a href=https://github.com/aofei>
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/> <img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
@@ -598,6 +510,8 @@ make build
<sub style="font-size:14px"><b>Aofei Sheng</b></sub> <sub style="font-size:14px"><b>Aofei Sheng</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/arnarg> <a href=https://github.com/arnarg>
<img src=https://avatars.githubusercontent.com/u/1291396?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arnar/> <img src=https://avatars.githubusercontent.com/u/1291396?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arnar/>
@@ -606,14 +520,12 @@ make build
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/avirut> <a href=https://github.com/awoimbee>
<img src=https://avatars.githubusercontent.com/u/27095602?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Avirut Mehta/> <img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
<br /> <br />
<sub style="font-size:14px"><b>Avirut Mehta</b></sub> <sub style="font-size:14px"><b>Arthur Woimbée</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/stensonb> <a href=https://github.com/stensonb>
<img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/> <img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/>
@@ -635,13 +547,6 @@ make build
<sub style="font-size:14px"><b>kundel</b></sub> <sub style="font-size:14px"><b>kundel</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fatih-acar>
<img src=https://avatars.githubusercontent.com/u/15028881?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=fatih-acar/>
<br />
<sub style="font-size:14px"><b>fatih-acar</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fkr> <a href=https://github.com/fkr>
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/> <img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
@@ -649,6 +554,8 @@ make build
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub> <sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/felixonmars> <a href=https://github.com/felixonmars>
<img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/> <img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/>
@@ -656,15 +563,6 @@ make build
<sub style="font-size:14px"><b>Felix Yan</b></sub> <sub style="font-size:14px"><b>Felix Yan</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/gabe565>
<img src=https://avatars.githubusercontent.com/u/7717888?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Gabe Cook/>
<br />
<sub style="font-size:14px"><b>Gabe Cook</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/JJGadgets> <a href=https://github.com/JJGadgets>
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/> <img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
@@ -672,13 +570,6 @@ make build
<sub style="font-size:14px"><b>JJGadgets</b></sub> <sub style="font-size:14px"><b>JJGadgets</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/hrtkpf>
<img src=https://avatars.githubusercontent.com/u/42646788?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hrtkpf/>
<br />
<sub style="font-size:14px"><b>hrtkpf</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jimt> <a href=https://github.com/jimt>
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/> <img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
@@ -686,22 +577,6 @@ make build
<sub style="font-size:14px"><b>Jim Tittsler</b></sub> <sub style="font-size:14px"><b>Jim Tittsler</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jsiebens>
<img src=https://avatars.githubusercontent.com/u/499769?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Johan Siebens/>
<br />
<sub style="font-size:14px"><b>Johan Siebens</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/johnae>
<img src=https://avatars.githubusercontent.com/u/28332?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=John Axel Eriksson/>
<br />
<sub style="font-size:14px"><b>John Axel Eriksson</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ShadowJonathan> <a href=https://github.com/ShadowJonathan>
<img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/> <img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/>
@@ -709,43 +584,6 @@ make build
<sub style="font-size:14px"><b>Jonathan de Jong</b></sub> <sub style="font-size:14px"><b>Jonathan de Jong</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/JulienFloris>
<img src=https://avatars.githubusercontent.com/u/20380255?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Julien Zweverink/>
<br />
<sub style="font-size:14px"><b>Julien Zweverink</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/win-t>
<img src=https://avatars.githubusercontent.com/u/1589120?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kurnia D Win/>
<br />
<sub style="font-size:14px"><b>Kurnia D Win</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/foxtrot>
<img src=https://avatars.githubusercontent.com/u/4153572?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Marc/>
<br />
<sub style="font-size:14px"><b>Marc</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/magf>
<img src=https://avatars.githubusercontent.com/u/11992737?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Maxim Gajdaj/>
<br />
<sub style="font-size:14px"><b>Maxim Gajdaj</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mikejsavage>
<img src=https://avatars.githubusercontent.com/u/579299?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael Savage/>
<br />
<sub style="font-size:14px"><b>Michael Savage</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/piec> <a href=https://github.com/piec>
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/> <img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
@@ -753,13 +591,6 @@ make build
<sub style="font-size:14px"><b>Pierre Carru</b></sub> <sub style="font-size:14px"><b>Pierre Carru</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Donran>
<img src=https://avatars.githubusercontent.com/u/4838348?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pontus N/>
<br />
<sub style="font-size:14px"><b>Pontus N</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nnsee> <a href=https://github.com/nnsee>
<img src=https://avatars.githubusercontent.com/u/36747857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Rasmus Moorats/> <img src=https://avatars.githubusercontent.com/u/36747857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Rasmus Moorats/>
@@ -767,6 +598,8 @@ make build
<sub style="font-size:14px"><b>Rasmus Moorats</b></sub> <sub style="font-size:14px"><b>Rasmus Moorats</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/rcursaru> <a href=https://github.com/rcursaru>
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/> <img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
@@ -788,8 +621,6 @@ make build
<sub style="font-size:14px"><b>Ryan Fowler</b></sub> <sub style="font-size:14px"><b>Ryan Fowler</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/shaananc> <a href=https://github.com/shaananc>
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/> <img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
@@ -811,6 +642,8 @@ make build
<sub style="font-size:14px"><b>sophware</b></sub> <sub style="font-size:14px"><b>sophware</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/m-tanner-dev0> <a href=https://github.com/m-tanner-dev0>
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/> <img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
@@ -825,15 +658,6 @@ make build
<sub style="font-size:14px"><b>Teteros</b></sub> <sub style="font-size:14px"><b>Teteros</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Teteros>
<img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/>
<br />
<sub style="font-size:14px"><b>Teteros</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/gitter-badger> <a href=https://github.com/gitter-badger>
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/> <img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
@@ -862,6 +686,8 @@ make build
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub> <sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/y0ngb1n> <a href=https://github.com/y0ngb1n>
<img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/> <img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/>
@@ -876,15 +702,6 @@ make build
<sub style="font-size:14px"><b>Yujie Xia</b></sub> <sub style="font-size:14px"><b>Yujie Xia</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/newellz2>
<img src=https://avatars.githubusercontent.com/u/52436542?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zachary Newell/>
<br />
<sub style="font-size:14px"><b>Zachary Newell</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/zekker6> <a href=https://github.com/zekker6>
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/> <img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
@@ -906,13 +723,6 @@ make build
<sub style="font-size:14px"><b>Ziyuan Han</b></sub> <sub style="font-size:14px"><b>Ziyuan Han</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/caelansar>
<img src=https://avatars.githubusercontent.com/u/31852257?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=caelansar/>
<br />
<sub style="font-size:14px"><b>caelansar</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/derelm> <a href=https://github.com/derelm>
<img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/> <img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/>
@@ -922,13 +732,6 @@ make build
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dnaq>
<img src=https://avatars.githubusercontent.com/u/1299717?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=dnaq/>
<br />
<sub style="font-size:14px"><b>dnaq</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nning> <a href=https://github.com/nning>
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/> <img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
@@ -943,13 +746,6 @@ make build
<sub style="font-size:14px"><b>ignoramous</b></sub> <sub style="font-size:14px"><b>ignoramous</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jimyag>
<img src=https://avatars.githubusercontent.com/u/69233189?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=jimyag/>
<br />
<sub style="font-size:14px"><b>jimyag</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/magichuihui> <a href=https://github.com/magichuihui>
<img src=https://avatars.githubusercontent.com/u/10866198?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=suhelen/> <img src=https://avatars.githubusercontent.com/u/10866198?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=suhelen/>
@@ -964,15 +760,6 @@ make build
<sub style="font-size:14px"><b>sharkonet</b></sub> <sub style="font-size:14px"><b>sharkonet</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ma6174>
<img src=https://avatars.githubusercontent.com/u/1449133?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ma6174/>
<br />
<sub style="font-size:14px"><b>ma6174</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/manju-rn> <a href=https://github.com/manju-rn>
<img src=https://avatars.githubusercontent.com/u/26291847?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=manju-rn/> <img src=https://avatars.githubusercontent.com/u/26291847?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=manju-rn/>
@@ -980,20 +767,15 @@ make build
<sub style="font-size:14px"><b>manju-rn</b></sub> <sub style="font-size:14px"><b>manju-rn</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nicholas-yap>
<img src=https://avatars.githubusercontent.com/u/38109533?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=nicholas-yap/>
<br />
<sub style="font-size:14px"><b>nicholas-yap</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pernila> <a href=https://github.com/pernila>
<img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tommi Pernila/> <img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=pernila/>
<br /> <br />
<sub style="font-size:14px"><b>Tommi Pernila</b></sub> <sub style="font-size:14px"><b>pernila</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/phpmalik> <a href=https://github.com/phpmalik>
<img src=https://avatars.githubusercontent.com/u/26834645?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=phpmalik/> <img src=https://avatars.githubusercontent.com/u/26834645?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=phpmalik/>
@@ -1008,8 +790,6 @@ make build
<sub style="font-size:14px"><b>Wakeful-Cloud</b></sub> <sub style="font-size:14px"><b>Wakeful-Cloud</b></sub>
</a> </a>
</td> </td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/xpzouying> <a href=https://github.com/xpzouying>
<img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/> <img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/>
@@ -1017,12 +797,5 @@ make build
<sub style="font-size:14px"><b>zy</b></sub> <sub style="font-size:14px"><b>zy</b></sub>
</a> </a>
</td> </td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/atorregrosa-smd>
<img src=https://avatars.githubusercontent.com/u/78434679?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Àlex Torregrosa/>
<br />
<sub style="font-size:14px"><b>Àlex Torregrosa</b></sub>
</a>
</td>
</tr> </tr>
</table> </table>

237
acls.go
View File

@@ -13,9 +13,7 @@ import (
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/tailscale/hujson" "github.com/tailscale/hujson"
"go4.org/netipx"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@@ -119,30 +117,13 @@ func (h *Headscale) LoadACLPolicy(path string) error {
} }
func (h *Headscale) UpdateACLRules() error { func (h *Headscale) UpdateACLRules() error {
machines, err := h.ListMachines() rules, err := h.generateACLRules()
if err != nil {
return err
}
if h.aclPolicy == nil {
return errEmptyPolicy
}
rules, err := generateACLRules(machines, *h.aclPolicy, h.cfg.OIDC.StripEmaildomain)
if err != nil { if err != nil {
return err return err
} }
log.Trace().Interface("ACL", rules).Msg("ACL rules generated") log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
h.aclRules = rules h.aclRules = rules
// Precompute a map of which sources can reach each destination, this is
// to provide quicker lookup when we calculate the peerlist for the map
// response to nodes.
aclPeerCacheMap := generateACLPeerCacheMap(rules)
h.aclPeerCacheMapRW.Lock()
h.aclPeerCacheMap = aclPeerCacheMap
h.aclPeerCacheMapRW.Unlock()
if featureEnableSSH() { if featureEnableSSH() {
sshRules, err := h.generateSSHRules() sshRules, err := h.generateSSHRules()
if err != nil { if err != nil {
@@ -160,88 +141,26 @@ func (h *Headscale) UpdateACLRules() error {
return nil return nil
} }
// generateACLPeerCacheMap takes a list of Tailscale filter rules and generates a map func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
// of which Sources ("*" and IPs) can access destinations. This is to speed up the
// process of generating MapResponses when deciding which Peers to inform nodes about.
func generateACLPeerCacheMap(rules []tailcfg.FilterRule) map[string]map[string]struct{} {
aclCachePeerMap := make(map[string]map[string]struct{})
for _, rule := range rules {
for _, srcIP := range rule.SrcIPs {
for _, ip := range expandACLPeerAddr(srcIP) {
if data, ok := aclCachePeerMap[ip]; ok {
for _, dstPort := range rule.DstPorts {
for _, dstIP := range expandACLPeerAddr(dstPort.IP) {
data[dstIP] = struct{}{}
}
}
} else {
dstPortsMap := make(map[string]struct{}, len(rule.DstPorts))
for _, dstPort := range rule.DstPorts {
for _, dstIP := range expandACLPeerAddr(dstPort.IP) {
dstPortsMap[dstIP] = struct{}{}
}
}
aclCachePeerMap[ip] = dstPortsMap
}
}
}
}
log.Trace().Interface("ACL Cache Map", aclCachePeerMap).Msg("ACL Peer Cache Map generated")
return aclCachePeerMap
}
// expandACLPeerAddr takes a "tailcfg.FilterRule" "IP" and expands it into
// something our cache logic can look up, which is "*" or single IP addresses.
// This is probably quite inefficient, but it is a result of
// "make it work, then make it fast", and a lot of the ACL stuff does not
// work, but people have tried to make it fast.
func expandACLPeerAddr(srcIP string) []string {
if ip, err := netip.ParseAddr(srcIP); err == nil {
return []string{ip.String()}
}
if cidr, err := netip.ParsePrefix(srcIP); err == nil {
addrs := []string{}
ipRange := netipx.RangeOfPrefix(cidr)
from := ipRange.From()
too := ipRange.To()
if from == too {
return []string{from.String()}
}
for from != too && from.Less(too) {
addrs = append(addrs, from.String())
from = from.Next()
}
addrs = append(addrs, too.String()) // Add the last IP address in the range
return addrs
}
// probably "*" or other string based "IP"
return []string{srcIP}
}
func generateACLRules(
machines []Machine,
aclPolicy ACLPolicy,
stripEmaildomain bool,
) ([]tailcfg.FilterRule, error) {
rules := []tailcfg.FilterRule{} rules := []tailcfg.FilterRule{}
for index, acl := range aclPolicy.ACLs { if h.aclPolicy == nil {
return nil, errEmptyPolicy
}
machines, err := h.ListMachines()
if err != nil {
return nil, err
}
for index, acl := range h.aclPolicy.ACLs {
if acl.Action != "accept" { if acl.Action != "accept" {
return nil, errInvalidAction return nil, errInvalidAction
} }
srcIPs := []string{} srcIPs := []string{}
for innerIndex, src := range acl.Sources { for innerIndex, src := range acl.Sources {
srcs, err := generateACLPolicySrc(machines, aclPolicy, src, stripEmaildomain) srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, src)
if err != nil { if err != nil {
log.Error(). log.Error().
Msgf("Error parsing ACL %d, Source %d", index, innerIndex) Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
@@ -261,12 +180,11 @@ func generateACLRules(
destPorts := []tailcfg.NetPortRange{} destPorts := []tailcfg.NetPortRange{}
for innerIndex, dest := range acl.Destinations { for innerIndex, dest := range acl.Destinations {
dests, err := generateACLPolicyDest( dests, err := h.generateACLPolicyDest(
machines, machines,
aclPolicy, *h.aclPolicy,
dest, dest,
needsWildcard, needsWildcard,
stripEmaildomain,
) )
if err != nil { if err != nil {
log.Error(). log.Error().
@@ -392,56 +310,29 @@ func sshCheckAction(duration string) (*tailcfg.SSHAction, error) {
}, nil }, nil
} }
func generateACLPolicySrc( func (h *Headscale) generateACLPolicySrcIP(
machines []Machine, machines []Machine,
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
src string, src string,
stripEmaildomain bool,
) ([]string, error) { ) ([]string, error) {
return expandAlias(machines, aclPolicy, src, stripEmaildomain) return expandAlias(machines, aclPolicy, src, h.cfg.OIDC.StripEmaildomain)
} }
func generateACLPolicyDest( func (h *Headscale) generateACLPolicyDest(
machines []Machine, machines []Machine,
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
dest string, dest string,
needsWildcard bool, needsWildcard bool,
stripEmaildomain bool,
) ([]tailcfg.NetPortRange, error) { ) ([]tailcfg.NetPortRange, error) {
var tokens []string tokens := strings.Split(dest, ":")
log.Trace().Str("destination", dest).Msg("generating policy destination")
// Check if there is a IPv4/6:Port combination, IPv6 has more than
// three ":".
tokens = strings.Split(dest, ":")
if len(tokens) < expectedTokenItems || len(tokens) > 3 { if len(tokens) < expectedTokenItems || len(tokens) > 3 {
port := tokens[len(tokens)-1] return nil, errInvalidPortFormat
maybeIPv6Str := strings.TrimSuffix(dest, ":"+port)
log.Trace().Str("maybeIPv6Str", maybeIPv6Str).Msg("")
if maybeIPv6, err := netip.ParseAddr(maybeIPv6Str); err != nil && !maybeIPv6.Is6() {
log.Trace().Err(err).Msg("trying to parse as IPv6")
return nil, fmt.Errorf(
"failed to parse destination, tokens %v: %w",
tokens,
errInvalidPortFormat,
)
} else {
tokens = []string{maybeIPv6Str, port}
}
} }
log.Trace().Strs("tokens", tokens).Msg("generating policy destination")
var alias string var alias string
// We can have here stuff like: // We can have here stuff like:
// git-server:* // git-server:*
// 192.168.1.0/24:22 // 192.168.1.0/24:22
// fd7a:115c:a1e0::2:22
// fd7a:115c:a1e0::2/128:22
// tag:montreal-webserver:80,443 // tag:montreal-webserver:80,443
// tag:api-server:443 // tag:api-server:443
// example-host-1:* // example-host-1:*
@@ -455,7 +346,7 @@ func generateACLPolicyDest(
machines, machines,
aclPolicy, aclPolicy,
alias, alias,
stripEmaildomain, h.cfg.OIDC.StripEmaildomain,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -492,7 +383,12 @@ func generateACLPolicyDest(
func parseProtocol(protocol string) ([]int, bool, error) { func parseProtocol(protocol string) ([]int, bool, error) {
switch protocol { switch protocol {
case "": case "":
return nil, false, nil return []int{
protocolICMP,
protocolIPv6ICMP,
protocolTCP,
protocolUDP,
}, false, nil
case "igmp": case "igmp":
return []int{protocolIGMP}, true, nil return []int{protocolIGMP}, true, nil
case "ipv4", "ip-in-ip": case "ipv4", "ip-in-ip":
@@ -530,15 +426,12 @@ func parseProtocol(protocol string) ([]int, bool, error) {
} }
// expandalias has an input of either // expandalias has an input of either
// - a user // - a namespace
// - a group // - a group
// - a tag // - a tag
// - a host
// - an ip
// - a cidr
// and transform these in IPAddresses. // and transform these in IPAddresses.
func expandAlias( func expandAlias(
machines Machines, machines []Machine,
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
alias string, alias string,
stripEmailDomain bool, stripEmailDomain bool,
@@ -553,12 +446,12 @@ func expandAlias(
Msg("Expanding") Msg("Expanding")
if strings.HasPrefix(alias, "group:") { if strings.HasPrefix(alias, "group:") {
users, err := expandGroup(aclPolicy, alias, stripEmailDomain) namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain)
if err != nil { if err != nil {
return ips, err return ips, err
} }
for _, n := range users { for _, n := range namespaces {
nodes := filterMachinesByUser(machines, n) nodes := filterMachinesByNamespace(machines, n)
for _, node := range nodes { for _, node := range nodes {
ips = append(ips, node.IPAddresses.ToStringSlice()...) ips = append(ips, node.IPAddresses.ToStringSlice()...)
} }
@@ -594,8 +487,8 @@ func expandAlias(
} }
// filter out machines per tag owner // filter out machines per tag owner
for _, user := range owners { for _, namespace := range owners {
machines := filterMachinesByUser(machines, user) machines := filterMachinesByNamespace(machines, namespace)
for _, machine := range machines { for _, machine := range machines {
hi := machine.GetHostInfo() hi := machine.GetHostInfo()
if contains(hi.RequestTags, alias) { if contains(hi.RequestTags, alias) {
@@ -607,8 +500,8 @@ func expandAlias(
return ips, nil return ips, nil
} }
// if alias is a user // if alias is a namespace
nodes := filterMachinesByUser(machines, alias) nodes := filterMachinesByNamespace(machines, alias)
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain) nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain)
for _, n := range nodes { for _, n := range nodes {
@@ -620,40 +513,19 @@ func expandAlias(
// if alias is an host // if alias is an host
if h, ok := aclPolicy.Hosts[alias]; ok { if h, ok := aclPolicy.Hosts[alias]; ok {
log.Trace().Str("host", h.String()).Msg("expandAlias got hosts entry") return []string{h.String()}, nil
return expandAlias(machines, aclPolicy, h.String(), stripEmailDomain)
} }
// if alias is an IP // if alias is an IP
if ip, err := netip.ParseAddr(alias); err == nil { ip, err := netip.ParseAddr(alias)
log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip") if err == nil {
ips := []string{ip.String()} return []string{ip.String()}, nil
matches := machines.FilterByIP(ip)
for _, machine := range matches {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
return lo.Uniq(ips), nil
} }
if cidr, err := netip.ParsePrefix(alias); err == nil { // if alias is an CIDR
log.Trace().Str("cidr", cidr.String()).Msg("expandAlias got cidr") cidr, err := netip.ParsePrefix(alias)
val := []string{cidr.String()} if err == nil {
// This is suboptimal and quite expensive, but if we only add the cidr, we will miss all the relevant IPv6 return []string{cidr.String()}, nil
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
for _, machine := range machines {
for _, ip := range machine.IPAddresses {
// log.Trace().
// Msgf("checking if machine ip (%s) is part of cidr (%s): %v, is single ip cidr (%v), addr: %s", ip.String(), cidr.String(), cidr.Contains(ip), cidr.IsSingleIP(), cidr.Addr().String())
if cidr.Contains(ip) {
val = append(val, machine.IPAddresses.ToStringSlice()...)
}
}
}
return lo.Uniq(val), nil
} }
log.Warn().Msgf("No IPs found with the alias %v", alias) log.Warn().Msgf("No IPs found with the alias %v", alias)
@@ -662,20 +534,20 @@ func expandAlias(
} }
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones // excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
// that are correctly tagged since they should not be listed as being in the user // that are correctly tagged since they should not be listed as being in the namespace
// we assume in this function that we only have nodes from 1 user. // we assume in this function that we only have nodes from 1 namespace.
func excludeCorrectlyTaggedNodes( func excludeCorrectlyTaggedNodes(
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
nodes []Machine, nodes []Machine,
user string, namespace string,
stripEmailDomain bool, stripEmailDomain bool,
) []Machine { ) []Machine {
out := []Machine{} out := []Machine{}
tags := []string{} tags := []string{}
for tag := range aclPolicy.TagOwners { for tag := range aclPolicy.TagOwners {
owners, _ := expandTagOwners(aclPolicy, user, stripEmailDomain) owners, _ := expandTagOwners(aclPolicy, namespace, stripEmailDomain)
ns := append(owners, user) ns := append(owners, namespace)
if contains(ns, user) { if contains(ns, namespace) {
tags = append(tags, tag) tags = append(tags, tag)
} }
} }
@@ -715,7 +587,6 @@ func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, err
ports := []tailcfg.PortRange{} ports := []tailcfg.PortRange{}
for _, portStr := range strings.Split(portsStr, ",") { for _, portStr := range strings.Split(portsStr, ",") {
log.Trace().Msgf("parsing portstring: %s", portStr)
rang := strings.Split(portStr, "-") rang := strings.Split(portStr, "-")
switch len(rang) { switch len(rang) {
case 1: case 1:
@@ -750,10 +621,10 @@ func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, err
return &ports, nil return &ports, nil
} }
func filterMachinesByUser(machines []Machine, user string) []Machine { func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
out := []Machine{} out := []Machine{}
for _, machine := range machines { for _, machine := range machines {
if machine.User.Name == user { if machine.Namespace.Name == namespace {
out = append(out, machine) out = append(out, machine)
} }
} }
@@ -761,7 +632,7 @@ func filterMachinesByUser(machines []Machine, user string) []Machine {
return out return out
} }
// expandTagOwners will return a list of user. An owner can be either a user or a group // expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
// a group cannot be composed of groups. // a group cannot be composed of groups.
func expandTagOwners( func expandTagOwners(
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
@@ -792,7 +663,7 @@ func expandTagOwners(
return owners, nil return owners, nil
} }
// expandGroup will return the list of user inside the group // expandGroup will return the list of namespace inside the group
// after some validation. // after some validation.
func expandGroup( func expandGroup(
aclPolicy ACLPolicy, aclPolicy ACLPolicy,

View File

@@ -54,7 +54,7 @@ func (s *Suite) TestBasicRule(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson") err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
} }
@@ -77,10 +77,10 @@ func (s *Suite) TestInvalidAction(c *check.C) {
func (s *Suite) TestSshRules(c *check.C) { func (s *Suite) TestSshRules(c *check.C) {
envknob.Setenv("HEADSCALE_EXPERIMENTAL_FEATURE_SSH", "1") envknob.Setenv("HEADSCALE_EXPERIMENTAL_FEATURE_SSH", "1")
user, err := app.CreateUser("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@@ -98,7 +98,7 @@ func (s *Suite) TestSshRules(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}, IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: user.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@@ -187,10 +187,10 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid. // match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
// the tag is matched in the Sources section. // the tag is matched in the Sources section.
func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) { func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
user, err := app.CreateUser("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@@ -208,7 +208,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}, IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: user.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@@ -237,10 +237,10 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid. // match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
// the tag is matched in the Destinations section. // the tag is matched in the Destinations section.
func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) { func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
user, err := app.CreateUser("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@@ -258,7 +258,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}, IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: user.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@@ -284,13 +284,13 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
} }
// need a test with: // need a test with:
// tag on a host that isn't owned by a tag owners. So the user // tag on a host that isn't owned by a tag owners. So the namespace
// of the host should be valid. // of the host should be valid.
func (s *Suite) TestInvalidTagValidUser(c *check.C) { func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
user, err := app.CreateUser("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@@ -308,7 +308,7 @@ func (s *Suite) TestInvalidTagValidUser(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}, IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: user.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@@ -333,13 +333,13 @@ func (s *Suite) TestInvalidTagValidUser(c *check.C) {
} }
// tag on a host is owned by a tag owner, the tag is valid. // tag on a host is owned by a tag owner, the tag is valid.
// an ACL rule is matching the tag to a user. It should not be valid since the // an ACL rule is matching the tag to a namespace. It should not be valid since the
// host should be tied to the tag now. // host should be tied to the tag now.
func (s *Suite) TestValidTagInvalidUser(c *check.C) { func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
user, err := app.CreateUser("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "webserver") _, err = app.GetMachine("user1", "webserver")
@@ -357,7 +357,7 @@ func (s *Suite) TestValidTagInvalidUser(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "webserver", Hostname: "webserver",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")}, IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: user.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@@ -376,7 +376,7 @@ func (s *Suite) TestValidTagInvalidUser(c *check.C) {
DiscoKey: "faab", DiscoKey: "faab",
Hostname: "user", Hostname: "user",
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")}, IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.2")},
UserID: user.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo2), HostInfo: HostInfo(hostInfo2),
@@ -411,7 +411,7 @@ func (s *Suite) TestPortRange(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson") err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
@@ -425,7 +425,7 @@ func (s *Suite) TestProtocolParsing(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_protocols.hujson") err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_protocols.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
@@ -439,7 +439,7 @@ func (s *Suite) TestPortWildcard(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
@@ -455,7 +455,7 @@ func (s *Suite) TestPortWildcardYAML(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.yaml") err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.yaml")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) rules, err := app.generateACLRules()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
@@ -467,14 +467,14 @@ func (s *Suite) TestPortWildcardYAML(c *check.C) {
c.Assert(rules[0].SrcIPs[0], check.Equals, "*") c.Assert(rules[0].SrcIPs[0], check.Equals, "*")
} }
func (s *Suite) TestPortUser(c *check.C) { func (s *Suite) TestPortNamespace(c *check.C) {
user, err := app.CreateUser("testuser") namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("testuser", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
ips, _ := app.getAvailableIPs() ips, _ := app.getAvailableIPs()
machine := Machine{ machine := Machine{
@@ -483,7 +483,7 @@ func (s *Suite) TestPortUser(c *check.C) {
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
UserID: user.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: ips, IPAddresses: ips,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -491,14 +491,11 @@ func (s *Suite) TestPortUser(c *check.C) {
app.db.Save(&machine) app.db.Save(&machine)
err = app.LoadACLPolicy( err = app.LoadACLPolicy(
"./tests/acls/acl_policy_basic_user_as_user.hujson", "./tests/acls/acl_policy_basic_namespace_as_user.hujson",
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machines, err := app.ListMachines() rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
rules, err := generateACLRules(machines, *app.aclPolicy, false)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
@@ -513,13 +510,13 @@ func (s *Suite) TestPortUser(c *check.C) {
} }
func (s *Suite) TestPortGroup(c *check.C) { func (s *Suite) TestPortGroup(c *check.C) {
user, err := app.CreateUser("testuser") namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("testuser", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
ips, _ := app.getAvailableIPs() ips, _ := app.getAvailableIPs()
machine := Machine{ machine := Machine{
@@ -528,7 +525,7 @@ func (s *Suite) TestPortGroup(c *check.C) {
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
UserID: user.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: ips, IPAddresses: ips,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@@ -538,10 +535,7 @@ func (s *Suite) TestPortGroup(c *check.C) {
err = app.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson") err = app.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machines, err := app.ListMachines() rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
rules, err := generateACLRules(machines, *app.aclPolicy, false)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil) c.Assert(rules, check.NotNil)
@@ -689,7 +683,7 @@ func Test_expandTagOwners(t *testing.T) {
wantErr: false, wantErr: false,
}, },
{ {
name: "expand with user and group", name: "expand with namespace and group",
args: args{ args: args{
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
Groups: Groups{"group:foo": []string{"user1", "user2"}}, Groups: Groups{"group:foo": []string{"user1", "user2"}},
@@ -843,10 +837,10 @@ func Test_expandPorts(t *testing.T) {
} }
} }
func Test_listMachinesInUser(t *testing.T) { func Test_listMachinesInNamespace(t *testing.T) {
type args struct { type args struct {
machines []Machine machines []Machine
user string namespace string
} }
tests := []struct { tests := []struct {
name string name string
@@ -854,54 +848,54 @@ func Test_listMachinesInUser(t *testing.T) {
want []Machine want []Machine
}{ }{
{ {
name: "1 machine in user", name: "1 machine in namespace",
args: args{ args: args{
machines: []Machine{ machines: []Machine{
{User: User{Name: "joe"}}, {Namespace: Namespace{Name: "joe"}},
}, },
user: "joe", namespace: "joe",
}, },
want: []Machine{ want: []Machine{
{User: User{Name: "joe"}}, {Namespace: Namespace{Name: "joe"}},
}, },
}, },
{ {
name: "3 machines, 2 in user", name: "3 machines, 2 in namespace",
args: args{ args: args{
machines: []Machine{ machines: []Machine{
{ID: 1, User: User{Name: "joe"}}, {ID: 1, Namespace: Namespace{Name: "joe"}},
{ID: 2, User: User{Name: "marc"}}, {ID: 2, Namespace: Namespace{Name: "marc"}},
{ID: 3, User: User{Name: "marc"}}, {ID: 3, Namespace: Namespace{Name: "marc"}},
}, },
user: "marc", namespace: "marc",
}, },
want: []Machine{ want: []Machine{
{ID: 2, User: User{Name: "marc"}}, {ID: 2, Namespace: Namespace{Name: "marc"}},
{ID: 3, User: User{Name: "marc"}}, {ID: 3, Namespace: Namespace{Name: "marc"}},
}, },
}, },
{ {
name: "5 machines, 0 in user", name: "5 machines, 0 in namespace",
args: args{ args: args{
machines: []Machine{ machines: []Machine{
{ID: 1, User: User{Name: "joe"}}, {ID: 1, Namespace: Namespace{Name: "joe"}},
{ID: 2, User: User{Name: "marc"}}, {ID: 2, Namespace: Namespace{Name: "marc"}},
{ID: 3, User: User{Name: "marc"}}, {ID: 3, Namespace: Namespace{Name: "marc"}},
{ID: 4, User: User{Name: "marc"}}, {ID: 4, Namespace: Namespace{Name: "marc"}},
{ID: 5, User: User{Name: "marc"}}, {ID: 5, Namespace: Namespace{Name: "marc"}},
}, },
user: "mickael", namespace: "mickael",
}, },
want: []Machine{}, want: []Machine{},
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
if got := filterMachinesByUser(test.args.machines, test.args.user); !reflect.DeepEqual( if got := filterMachinesByNamespace(test.args.machines, test.args.namespace); !reflect.DeepEqual(
got, got,
test.want, test.want,
) { ) {
t.Errorf("listMachinesInUser() = %v, want %v", got, test.want) t.Errorf("listMachinesInNamespace() = %v, want %v", got, test.want)
} }
}) })
} }
@@ -947,25 +941,25 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netip.MustParseAddr("100.64.0.3"),
}, },
User: User{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
}, },
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
@@ -985,25 +979,25 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netip.MustParseAddr("100.64.0.3"),
}, },
User: User{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
}, },
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
@@ -1025,88 +1019,6 @@ func Test_expandAlias(t *testing.T) {
want: []string{"10.0.0.3"}, want: []string{"10.0.0.3"},
wantErr: false, wantErr: false,
}, },
{
name: "simple host by ip passed through",
args: args{
alias: "10.0.0.1",
machines: []Machine{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.1"},
wantErr: false,
},
{
name: "simple host by ipv4 single ipv4",
args: args{
alias: "10.0.0.1",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netip.MustParseAddr("10.0.0.1"),
},
User: User{Name: "mickael"},
},
},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.1"},
wantErr: false,
},
{
name: "simple host by ipv4 single dual stack",
args: args{
alias: "10.0.0.1",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netip.MustParseAddr("10.0.0.1"),
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
},
User: User{Name: "mickael"},
},
},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.1", "fd7a:115c:a1e0:ab12:4843:2222:6273:2222"},
wantErr: false,
},
{
name: "simple host by ipv6 single dual stack",
args: args{
alias: "fd7a:115c:a1e0:ab12:4843:2222:6273:2222",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netip.MustParseAddr("10.0.0.1"),
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
},
User: User{Name: "mickael"},
},
},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"fd7a:115c:a1e0:ab12:4843:2222:6273:2222", "10.0.0.1"},
wantErr: false,
},
{
name: "simple host by hostname alias",
args: args{
alias: "testy",
machines: []Machine{},
aclPolicy: ACLPolicy{
Hosts: Hosts{
"testy": netip.MustParsePrefix("10.0.0.132/32"),
},
},
stripEmailDomain: true,
},
want: []string{"10.0.0.132/32"},
wantErr: false,
},
{ {
name: "private network", name: "private network",
args: args{ args: args{
@@ -1122,6 +1034,17 @@ func Test_expandAlias(t *testing.T) {
want: []string{"192.168.1.0/24"}, want: []string{"192.168.1.0/24"},
wantErr: false, wantErr: false,
}, },
{
name: "simple host",
args: args{
alias: "10.0.0.1",
machines: []Machine{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
want: []string{"10.0.0.1"},
wantErr: false,
},
{ {
name: "simple CIDR", name: "simple CIDR",
args: args{ args: args{
@@ -1142,7 +1065,7 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1153,7 +1076,7 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1164,13 +1087,13 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netip.MustParseAddr("100.64.0.3"),
}, },
User: User{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
@@ -1190,25 +1113,25 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netip.MustParseAddr("100.64.0.3"),
}, },
User: User{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
}, },
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
@@ -1231,27 +1154,27 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"}, ForcedTags: []string{"tag:hr-webserver"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"}, ForcedTags: []string{"tag:hr-webserver"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netip.MustParseAddr("100.64.0.3"),
}, },
User: User{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
}, },
aclPolicy: ACLPolicy{}, aclPolicy: ACLPolicy{},
@@ -1269,14 +1192,14 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"}, ForcedTags: []string{"tag:hr-webserver"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1287,13 +1210,13 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netip.MustParseAddr("100.64.0.3"),
}, },
User: User{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "mickael"}, Namespace: Namespace{Name: "mickael"},
}, },
}, },
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
@@ -1307,7 +1230,7 @@ func Test_expandAlias(t *testing.T) {
wantErr: false, wantErr: false,
}, },
{ {
name: "list host in user without correctly tagged servers", name: "list host in namespace without correctly tagged servers",
args: args{ args: args{
alias: "joe", alias: "joe",
machines: []Machine{ machines: []Machine{
@@ -1315,7 +1238,7 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1326,7 +1249,7 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1337,13 +1260,13 @@ func Test_expandAlias(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.3"), netip.MustParseAddr("100.64.0.3"),
}, },
User: User{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
@@ -1379,7 +1302,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
type args struct { type args struct {
aclPolicy ACLPolicy aclPolicy ACLPolicy
nodes []Machine nodes []Machine
user string namespace string
stripEmailDomain bool stripEmailDomain bool
} }
tests := []struct { tests := []struct {
@@ -1399,7 +1322,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1410,7 +1333,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1421,16 +1344,16 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
user: "joe", namespace: "joe",
stripEmailDomain: true, stripEmailDomain: true,
}, },
want: []Machine{ want: []Machine{
{ {
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")}, IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
}, },
@@ -1450,7 +1373,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1461,7 +1384,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1472,16 +1395,16 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
user: "joe", namespace: "joe",
stripEmailDomain: true, stripEmailDomain: true,
}, },
want: []Machine{ want: []Machine{
{ {
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")}, IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
}, },
@@ -1496,7 +1419,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "foo", Hostname: "foo",
@@ -1507,23 +1430,23 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
ForcedTags: []string{"tag:accountant-webserver"}, ForcedTags: []string{"tag:accountant-webserver"},
}, },
{ {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
user: "joe", namespace: "joe",
stripEmailDomain: true, stripEmailDomain: true,
}, },
want: []Machine{ want: []Machine{
{ {
IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")}, IPAddresses: MachineAddresses{netip.MustParseAddr("100.64.0.4")},
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
}, },
@@ -1538,7 +1461,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "hr-web1", Hostname: "hr-web1",
@@ -1549,7 +1472,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "hr-web2", Hostname: "hr-web2",
@@ -1560,10 +1483,10 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
user: "joe", namespace: "joe",
stripEmailDomain: true, stripEmailDomain: true,
}, },
want: []Machine{ want: []Machine{
@@ -1571,7 +1494,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("100.64.0.1"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "hr-web1", Hostname: "hr-web1",
@@ -1582,7 +1505,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("100.64.0.2"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
HostInfo: HostInfo{ HostInfo: HostInfo{
OS: "centos", OS: "centos",
Hostname: "hr-web2", Hostname: "hr-web2",
@@ -1593,7 +1516,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
IPAddresses: MachineAddresses{ IPAddresses: MachineAddresses{
netip.MustParseAddr("100.64.0.4"), netip.MustParseAddr("100.64.0.4"),
}, },
User: User{Name: "joe"}, Namespace: Namespace{Name: "joe"},
}, },
}, },
}, },
@@ -1603,7 +1526,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
got := excludeCorrectlyTaggedNodes( got := excludeCorrectlyTaggedNodes(
test.args.aclPolicy, test.args.aclPolicy,
test.args.nodes, test.args.nodes,
test.args.user, test.args.namespace,
test.args.stripEmailDomain, test.args.stripEmailDomain,
) )
if !reflect.DeepEqual(got, test.want) { if !reflect.DeepEqual(got, test.want) {
@@ -1612,138 +1535,3 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
}) })
} }
} }
func Test_expandACLPeerAddr(t *testing.T) {
type args struct {
srcIP string
}
tests := []struct {
name string
args args
want []string
}{
{
name: "asterix",
args: args{
srcIP: "*",
},
want: []string{"*"},
},
{
name: "ip",
args: args{
srcIP: "10.0.0.1",
},
want: []string{"10.0.0.1"},
},
{
name: "ip/32",
args: args{
srcIP: "10.0.0.1/32",
},
want: []string{"10.0.0.1"},
},
{
name: "ip/30",
args: args{
srcIP: "10.0.0.1/30",
},
want: []string{
"10.0.0.0",
"10.0.0.1",
"10.0.0.2",
"10.0.0.3",
},
},
{
name: "ip/28",
args: args{
srcIP: "192.168.0.128/28",
},
want: []string{
"192.168.0.128", "192.168.0.129", "192.168.0.130",
"192.168.0.131", "192.168.0.132", "192.168.0.133",
"192.168.0.134", "192.168.0.135", "192.168.0.136",
"192.168.0.137", "192.168.0.138", "192.168.0.139",
"192.168.0.140", "192.168.0.141", "192.168.0.142",
"192.168.0.143",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := expandACLPeerAddr(tt.args.srcIP); !reflect.DeepEqual(got, tt.want) {
t.Errorf("expandACLPeerAddr() = %v, want %v", got, tt.want)
}
})
}
}
func Test_expandACLPeerAddrV6(t *testing.T) {
type args struct {
srcIP string
}
tests := []struct {
name string
args args
want []string
}{
{
name: "asterix",
args: args{
srcIP: "*",
},
want: []string{"*"},
},
{
name: "ipfull",
args: args{
srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c:3166",
},
want: []string{"fd7a:115c:a1e0:ab12:4943:cd96:624c:3166"},
},
{
name: "ipzerocompression",
args: args{
srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c::",
},
want: []string{"fd7a:115c:a1e0:ab12:4943:cd96:624c:0"},
},
{
name: "ip/128",
args: args{
srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c:3166/128",
},
want: []string{"fd7a:115c:a1e0:ab12:4943:cd96:624c:3166"},
},
{
name: "ip/127",
args: args{
srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c:0000/127",
},
want: []string{
"fd7a:115c:a1e0:ab12:4943:cd96:624c:0",
"fd7a:115c:a1e0:ab12:4943:cd96:624c:1",
},
},
{
name: "ip/126",
args: args{
srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c:0000/126",
},
want: []string{
"fd7a:115c:a1e0:ab12:4943:cd96:624c:0",
"fd7a:115c:a1e0:ab12:4943:cd96:624c:1",
"fd7a:115c:a1e0:ab12:4943:cd96:624c:2",
"fd7a:115c:a1e0:ab12:4943:cd96:624c:3",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := expandACLPeerAddr(tt.args.srcIP); !reflect.DeepEqual(got, tt.want) {
t.Errorf("expandACLPeerAddr() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -34,7 +34,7 @@ type Groups map[string][]string
// Hosts are alias for IP addresses or subnets. // Hosts are alias for IP addresses or subnets.
type Hosts map[string]netip.Prefix type Hosts map[string]netip.Prefix
// TagOwners specify what users (users?) are allow to use certain tags. // TagOwners specify what users (namespaces?) are allow to use certain tags.
type TagOwners map[string][]string type TagOwners map[string][]string
// ACLTest is not implemented, but should be use to check if a certain rule is allowed. // ACLTest is not implemented, but should be use to check if a certain rule is allowed.
@@ -44,7 +44,7 @@ type ACLTest struct {
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"` Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
} }
// AutoApprovers specify which users (users?), groups or tags have their advertised routes // AutoApprovers specify which users (namespaces?), groups or tags have their advertised routes
// or exit node status automatically enabled. // or exit node status automatically enabled.
type AutoApprovers struct { type AutoApprovers struct {
Routes map[string][]string `json:"routes" yaml:"routes"` Routes map[string][]string `json:"routes" yaml:"routes"`
@@ -119,7 +119,7 @@ func (policy ACLPolicy) IsZero() bool {
return false return false
} }
// Returns the list of autoApproving users, groups or tags for a given IPPrefix. // Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix.
func (autoApprovers *AutoApprovers) GetRouteApprovers( func (autoApprovers *AutoApprovers) GetRouteApprovers(
prefix netip.Prefix, prefix netip.Prefix,
) ([]string, error) { ) ([]string, error) {

2
api.go
View File

@@ -78,7 +78,7 @@ var registerWebAPITemplate = template.Must(
<p> <p>
Run the command below in the headscale server to add this machine to your network: Run the command below in the headscale server to add this machine to your network:
</p> </p>
<pre><code>headscale nodes register --user USERNAME --key {{.Key}}</code></pre> <pre><code>headscale -n NAMESPACE nodes register --key {{.Key}}</code></pre>
</body> </body>
</html> </html>
`)) `))

View File

@@ -1,8 +1,6 @@
package headscale package headscale
import ( import (
"time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@@ -15,7 +13,7 @@ func (h *Headscale) generateMapResponse(
Str("func", "generateMapResponse"). Str("func", "generateMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname). Str("machine", mapRequest.Hostinfo.Hostname).
Msg("Creating Map response") Msg("Creating Map response")
node, err := h.toNode(*machine, h.cfg.BaseDomain, h.cfg.DNSConfig) node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@@ -39,7 +37,7 @@ func (h *Headscale) generateMapResponse(
profiles := h.getMapResponseUserProfiles(*machine, peers) profiles := h.getMapResponseUserProfiles(*machine, peers)
nodePeers, err := h.toNodes(peers, h.cfg.BaseDomain, h.cfg.DNSConfig) nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@@ -57,46 +55,16 @@ func (h *Headscale) generateMapResponse(
peers, peers,
) )
now := time.Now()
resp := tailcfg.MapResponse{ resp := tailcfg.MapResponse{
KeepAlive: false, KeepAlive: false,
Node: node, Node: node,
Peers: nodePeers,
// TODO: Only send if updated DNSConfig: dnsConfig,
DERPMap: h.DERPMap, Domain: h.cfg.BaseDomain,
// TODO: Only send if updated
Peers: nodePeers,
// TODO(kradalby): Implement:
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L1351-L1374
// PeersChanged
// PeersRemoved
// PeersChangedPatch
// PeerSeenChange
// OnlineChange
// TODO: Only send if updated
DNSConfig: dnsConfig,
// TODO: Only send if updated
Domain: h.cfg.BaseDomain,
// Do not instruct clients to collect services, we do not
// support or do anything with them
CollectServices: "false",
// TODO: Only send if updated
PacketFilter: h.aclRules, PacketFilter: h.aclRules,
SSHPolicy: h.sshPolicy,
DERPMap: h.DERPMap,
UserProfiles: profiles, UserProfiles: profiles,
// TODO: Only send if updated
SSHPolicy: h.sshPolicy,
ControlTime: &now,
Debug: &tailcfg.Debug{ Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled, DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort, RandomizeClientPort: h.cfg.RandomizeClientPort,

View File

@@ -29,7 +29,7 @@ type APIKey struct {
LastSeen *time.Time LastSeen *time.Time
} }
// CreateAPIKey creates a new ApiKey in a user, and returns it. // CreateAPIKey creates a new ApiKey in a namespace, and returns it.
func (h *Headscale) CreateAPIKey( func (h *Headscale) CreateAPIKey(
expiration *time.Time, expiration *time.Time,
) (string, *APIKey, error) { ) (string, *APIKey, error) {
@@ -64,7 +64,7 @@ func (h *Headscale) CreateAPIKey(
return keyStr, &key, nil return keyStr, &key, nil
} }
// ListAPIKeys returns the list of ApiKeys for a user. // ListAPIKeys returns the list of ApiKeys for a namespace.
func (h *Headscale) ListAPIKeys() ([]APIKey, error) { func (h *Headscale) ListAPIKeys() ([]APIKey, error) {
keys := []APIKey{} keys := []APIKey{}
if err := h.db.Find(&keys).Error; err != nil { if err := h.db.Find(&keys).Error; err != nil {

138
app.go
View File

@@ -81,14 +81,14 @@ type Headscale struct {
privateKey *key.MachinePrivate privateKey *key.MachinePrivate
noisePrivateKey *key.MachinePrivate noisePrivateKey *key.MachinePrivate
noiseMux *mux.Router
DERPMap *tailcfg.DERPMap DERPMap *tailcfg.DERPMap
DERPServer *DERPServer DERPServer *DERPServer
aclPolicy *ACLPolicy aclPolicy *ACLPolicy
aclRules []tailcfg.FilterRule aclRules []tailcfg.FilterRule
aclPeerCacheMapRW sync.RWMutex sshPolicy *tailcfg.SSHPolicy
aclPeerCacheMap map[string]map[string]struct{}
sshPolicy *tailcfg.SSHPolicy
lastStateChange *xsync.MapOf[string, time.Time] lastStateChange *xsync.MapOf[string, time.Time]
@@ -164,7 +164,6 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
aclRules: tailcfg.FilterAllowAll, // default allowall aclRules: tailcfg.FilterAllowAll, // default allowall
registrationCache: registrationCache, registrationCache: registrationCache,
pollNetMapStreamWG: sync.WaitGroup{}, pollNetMapStreamWG: sync.WaitGroup{},
lastStateChange: xsync.NewMapOf[time.Time](),
} }
err = app.initDB() err = app.initDB()
@@ -220,47 +219,29 @@ func (h *Headscale) expireEphemeralNodes(milliSeconds int64) {
} }
} }
// expireExpiredMachines expires machines that have an explicit expiry set
// after that expiry time has passed.
func (h *Headscale) expireExpiredMachines(milliSeconds int64) {
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
for range ticker.C {
h.expireExpiredMachinesWorker()
}
}
func (h *Headscale) failoverSubnetRoutes(milliSeconds int64) {
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
for range ticker.C {
err := h.handlePrimarySubnetFailover()
if err != nil {
log.Error().Err(err).Msg("failed to handle primary subnet failover")
}
}
}
func (h *Headscale) expireEphemeralNodesWorker() { func (h *Headscale) expireEphemeralNodesWorker() {
users, err := h.ListUsers() namespaces, err := h.ListNamespaces()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error listing users") log.Error().Err(err).Msg("Error listing namespaces")
return return
} }
for _, user := range users { for _, namespace := range namespaces {
machines, err := h.ListMachinesByUser(user.Name) machines, err := h.ListMachinesInNamespace(namespace.Name)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
Str("user", user.Name). Str("namespace", namespace.Name).
Msg("Error listing machines in user") Msg("Error listing machines in namespace")
return return
} }
expiredFound := false expiredFound := false
for _, machine := range machines { for _, machine := range machines {
if machine.isEphemeral() && machine.LastSeen != nil && if machine.AuthKey != nil && machine.LastSeen != nil &&
machine.AuthKey.Ephemeral &&
time.Now(). time.Now().
After(machine.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) { After(machine.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
expiredFound = true expiredFound = true
@@ -284,53 +265,6 @@ func (h *Headscale) expireEphemeralNodesWorker() {
} }
} }
func (h *Headscale) expireExpiredMachinesWorker() {
users, err := h.ListUsers()
if err != nil {
log.Error().Err(err).Msg("Error listing users")
return
}
for _, user := range users {
machines, err := h.ListMachinesByUser(user.Name)
if err != nil {
log.Error().
Err(err).
Str("user", user.Name).
Msg("Error listing machines in user")
return
}
expiredFound := false
for index, machine := range machines {
if machine.isExpired() &&
machine.Expiry.After(h.getLastStateChange(user)) {
expiredFound = true
err := h.ExpireMachine(&machines[index])
if err != nil {
log.Error().
Err(err).
Str("machine", machine.Hostname).
Str("name", machine.GivenName).
Msg("🤮 Cannot expire machine")
} else {
log.Info().
Str("machine", machine.Hostname).
Str("name", machine.GivenName).
Msg("Machine successfully expired")
}
}
}
if expiredFound {
h.setLastStateChangeToNow()
}
}
}
func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context, func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
req interface{}, req interface{},
info *grpc.UnaryServerInfo, info *grpc.UnaryServerInfo,
@@ -523,7 +457,17 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
apiRouter.Use(h.httpAuthenticationMiddleware) apiRouter.Use(h.httpAuthenticationMiddleware)
apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP) apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP)
router.PathPrefix("/").HandlerFunc(notFoundHandler) router.PathPrefix("/").HandlerFunc(stdoutHandler)
return router
}
func (h *Headscale) createNoiseMux() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/map", h.NoisePollNetMapHandler)
return router return router
} }
@@ -552,9 +496,6 @@ func (h *Headscale) Serve() error {
} }
go h.expireEphemeralNodes(updateInterval) go h.expireEphemeralNodes(updateInterval)
go h.expireExpiredMachines(updateInterval)
go h.failoverSubnetRoutes(updateInterval)
if zl.GlobalLevel() == zl.TraceLevel { if zl.GlobalLevel() == zl.TraceLevel {
zerolog.RespLog = true zerolog.RespLog = true
@@ -688,6 +629,12 @@ func (h *Headscale) Serve() error {
// over our main Addr. It also serves the legacy Tailcale API // over our main Addr. It also serves the legacy Tailcale API
router := h.createRouter(grpcGatewayMux) router := h.createRouter(grpcGatewayMux)
// This router is served only over the Noise connection, and exposes only the new API.
//
// The HTTP2 server that exposes this router is created for
// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
h.noiseMux = h.createNoiseMux()
httpServer := &http.Server{ httpServer := &http.Server{
Addr: h.cfg.Addr, Addr: h.cfg.Addr,
Handler: router, Handler: router,
@@ -910,31 +857,31 @@ func (h *Headscale) setLastStateChangeToNow() {
now := time.Now().UTC() now := time.Now().UTC()
users, err := h.ListUsers() namespaces, err := h.ListNamespaces()
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
Err(err). Err(err).
Msg("failed to fetch all users, failing to update last changed state.") Msg("failed to fetch all namespaces, failing to update last changed state.")
} }
for _, user := range users { for _, namespace := range namespaces {
lastStateUpdate.WithLabelValues(user.Name, "headscale").Set(float64(now.Unix())) lastStateUpdate.WithLabelValues(namespace.Name, "headscale").Set(float64(now.Unix()))
if h.lastStateChange == nil { if h.lastStateChange == nil {
h.lastStateChange = xsync.NewMapOf[time.Time]() h.lastStateChange = xsync.NewMapOf[time.Time]()
} }
h.lastStateChange.Store(user.Name, now) h.lastStateChange.Store(namespace.Name, now)
} }
} }
func (h *Headscale) getLastStateChange(users ...User) time.Time { func (h *Headscale) getLastStateChange(namespaces ...Namespace) time.Time {
times := []time.Time{} times := []time.Time{}
// getLastStateChange takes a list of users as a "filter", if no users // getLastStateChange takes a list of namespaces as a "filter", if no namespaces
// are past, then use the entier list of users and look for the last update // are past, then use the entier list of namespaces and look for the last update
if len(users) > 0 { if len(namespaces) > 0 {
for _, user := range users { for _, namespace := range namespaces {
if lastChange, ok := h.lastStateChange.Load(user.Name); ok { if lastChange, ok := h.lastStateChange.Load(namespace.Name); ok {
times = append(times, lastChange) times = append(times, lastChange)
} }
} }
@@ -959,7 +906,7 @@ func (h *Headscale) getLastStateChange(users ...User) time.Time {
} }
} }
func notFoundHandler( func stdoutHandler(
writer http.ResponseWriter, writer http.ResponseWriter,
req *http.Request, req *http.Request,
) { ) {
@@ -971,7 +918,6 @@ func notFoundHandler(
Interface("url", req.URL). Interface("url", req.URL).
Bytes("body", body). Bytes("body", body).
Msg("Request did not match") Msg("Request did not match")
writer.WriteHeader(http.StatusNotFound)
} }
func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) { func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {

View File

@@ -1,163 +0,0 @@
package main
//go:generate go run ./main.go
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"text/template"
)
var (
githubWorkflowPath = "../../.github/workflows/"
jobFileNameTemplate = `test-integration-v2-%s.yaml`
jobTemplate = template.Must(
template.New("jobTemplate").
Parse(`# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - {{.Name}}
on: [pull_request]
concurrency:
group: {{ "${{ github.workflow }}-$${{ github.head_ref || github.run_id }}" }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: {{ "${{ env.ACT }}" }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^{{.Name}}$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"
`),
)
)
const workflowFilePerm = 0o600
func removeTests() {
glob := fmt.Sprintf(jobFileNameTemplate, "*")
files, err := filepath.Glob(filepath.Join(githubWorkflowPath, glob))
if err != nil {
log.Fatalf("failed to find test files")
}
for _, file := range files {
err := os.Remove(file)
if err != nil {
log.Printf("failed to remove: %s", err)
}
}
}
func findTests() []string {
rgBin, err := exec.LookPath("rg")
if err != nil {
log.Fatalf("failed to find rg (ripgrep) binary")
}
args := []string{
"--regexp", "func (Test.+)\\(.*",
"../../integration/",
"--replace", "$1",
"--sort", "path",
"--no-line-number",
"--no-filename",
"--no-heading",
}
log.Printf("executing: %s %s", rgBin, strings.Join(args, " "))
ripgrep := exec.Command(
rgBin,
args...,
)
result, err := ripgrep.CombinedOutput()
if err != nil {
log.Printf("out: %s", result)
log.Fatalf("failed to run ripgrep: %s", err)
}
tests := strings.Split(string(result), "\n")
tests = tests[:len(tests)-1]
return tests
}
func main() {
type testConfig struct {
Name string
}
tests := findTests()
removeTests()
for _, test := range tests {
log.Printf("generating workflow for %s", test)
var content bytes.Buffer
if err := jobTemplate.Execute(&content, testConfig{
Name: test,
}); err != nil {
log.Fatalf("failed to render template: %s", err)
}
testPath := path.Join(githubWorkflowPath, fmt.Sprintf(jobFileNameTemplate, test))
err := os.WriteFile(testPath, content.Bytes(), workflowFilePerm)
if err != nil {
log.Fatalf("failed to write github job: %s", err)
}
}
}

View File

@@ -1,22 +0,0 @@
package cli
import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(configTestCmd)
}
var configTestCmd = &cobra.Command{
Use: "configtest",
Short: "Test the configuration.",
Long: "Run a test of the configuration and exit.",
Run: func(cmd *cobra.Command, args []string) {
_, err := getHeadscaleApp()
if err != nil {
log.Fatal().Caller().Err(err).Msg("Error initializing")
}
},
}

View File

@@ -27,14 +27,8 @@ func init() {
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") log.Fatal().Err(err).Msg("")
} }
createNodeCmd.Flags().StringP("user", "u", "", "User") createNodeCmd.Flags().StringP("namespace", "n", "", "Namespace")
err = createNodeCmd.MarkFlagRequired("namespace")
createNodeCmd.Flags().StringP("namespace", "n", "", "User")
createNodeNamespaceFlag := createNodeCmd.Flags().Lookup("namespace")
createNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
createNodeNamespaceFlag.Hidden = true
err = createNodeCmd.MarkFlagRequired("user")
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") log.Fatal().Err(err).Msg("")
} }
@@ -61,9 +55,9 @@ var createNodeCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
user, err := cmd.Flags().GetString("user") namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return return
} }
@@ -116,10 +110,10 @@ var createNodeCmd = &cobra.Command{
} }
request := &v1.DebugCreateMachineRequest{ request := &v1.DebugCreateMachineRequest{
Key: machineKey, Key: machineKey,
Name: name, Name: name,
User: user, Namespace: namespace,
Routes: routes, Routes: routes,
} }
response, err := client.DebugCreateMachine(ctx, request) response, err := client.DebugCreateMachine(ctx, request)

View File

@@ -16,11 +16,10 @@ const (
errMockOidcClientIDNotDefined = Error("MOCKOIDC_CLIENT_ID not defined") errMockOidcClientIDNotDefined = Error("MOCKOIDC_CLIENT_ID not defined")
errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined") errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined")
errMockOidcPortNotDefined = Error("MOCKOIDC_PORT not defined") errMockOidcPortNotDefined = Error("MOCKOIDC_PORT not defined")
accessTTL = 10 * time.Minute
refreshTTL = 60 * time.Minute refreshTTL = 60 * time.Minute
) )
var accessTTL = 2 * time.Minute
func init() { func init() {
rootCmd.AddCommand(mockOidcCmd) rootCmd.AddCommand(mockOidcCmd)
} }
@@ -55,16 +54,6 @@ func mockOIDC() error {
if portStr == "" { if portStr == "" {
return errMockOidcPortNotDefined return errMockOidcPortNotDefined
} }
accessTTLOverride := os.Getenv("MOCKOIDC_ACCESS_TTL")
if accessTTLOverride != "" {
newTTL, err := time.ParseDuration(accessTTLOverride)
if err != nil {
return err
}
accessTTL = newTTL
}
log.Info().Msgf("Access token TTL: %s", accessTTL)
port, err := strconv.Atoi(portStr) port, err := strconv.Atoi(portStr)
if err != nil { if err != nil {

View File

@@ -13,26 +13,26 @@ import (
) )
func init() { func init() {
rootCmd.AddCommand(userCmd) rootCmd.AddCommand(namespaceCmd)
userCmd.AddCommand(createUserCmd) namespaceCmd.AddCommand(createNamespaceCmd)
userCmd.AddCommand(listUsersCmd) namespaceCmd.AddCommand(listNamespacesCmd)
userCmd.AddCommand(destroyUserCmd) namespaceCmd.AddCommand(destroyNamespaceCmd)
userCmd.AddCommand(renameUserCmd) namespaceCmd.AddCommand(renameNamespaceCmd)
} }
const ( const (
errMissingParameter = headscale.Error("missing parameters") errMissingParameter = headscale.Error("missing parameters")
) )
var userCmd = &cobra.Command{ var namespaceCmd = &cobra.Command{
Use: "users", Use: "namespaces",
Short: "Manage the users of Headscale", Short: "Manage the namespaces of Headscale",
Aliases: []string{"user", "namespace", "namespaces", "ns"}, Aliases: []string{"namespace", "ns", "user", "users"},
} }
var createUserCmd = &cobra.Command{ var createNamespaceCmd = &cobra.Command{
Use: "create NAME", Use: "create NAME",
Short: "Creates a new user", Short: "Creates a new namespace",
Aliases: []string{"c", "new"}, Aliases: []string{"c", "new"},
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 { if len(args) < 1 {
@@ -44,7 +44,7 @@ var createUserCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
userName := args[0] namespaceName := args[0]
ctx, client, conn, cancel := getHeadscaleCLIClient() ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel() defer cancel()
@@ -52,15 +52,15 @@ var createUserCmd = &cobra.Command{
log.Trace().Interface("client", client).Msg("Obtained gRPC client") log.Trace().Interface("client", client).Msg("Obtained gRPC client")
request := &v1.CreateUserRequest{Name: userName} request := &v1.CreateNamespaceRequest{Name: namespaceName}
log.Trace().Interface("request", request).Msg("Sending CreateUser request") log.Trace().Interface("request", request).Msg("Sending CreateNamespace request")
response, err := client.CreateUser(ctx, request) response, err := client.CreateNamespace(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf( fmt.Sprintf(
"Cannot create user: %s", "Cannot create namespace: %s",
status.Convert(err).Message(), status.Convert(err).Message(),
), ),
output, output,
@@ -69,13 +69,13 @@ var createUserCmd = &cobra.Command{
return return
} }
SuccessOutput(response.User, "User created", output) SuccessOutput(response.Namespace, "Namespace created", output)
}, },
} }
var destroyUserCmd = &cobra.Command{ var destroyNamespaceCmd = &cobra.Command{
Use: "destroy NAME", Use: "destroy NAME",
Short: "Destroys a user", Short: "Destroys a namespace",
Aliases: []string{"delete"}, Aliases: []string{"delete"},
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 { if len(args) < 1 {
@@ -87,17 +87,17 @@ var destroyUserCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
userName := args[0] namespaceName := args[0]
request := &v1.GetUserRequest{ request := &v1.GetNamespaceRequest{
Name: userName, Name: namespaceName,
} }
ctx, client, conn, cancel := getHeadscaleCLIClient() ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
_, err := client.GetUser(ctx, request) _, err := client.GetNamespace(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
@@ -113,8 +113,8 @@ var destroyUserCmd = &cobra.Command{
if !force { if !force {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: fmt.Sprintf( Message: fmt.Sprintf(
"Do you want to remove the user '%s' and any associated preauthkeys?", "Do you want to remove the namespace '%s' and any associated preauthkeys?",
userName, namespaceName,
), ),
} }
err := survey.AskOne(prompt, &confirm) err := survey.AskOne(prompt, &confirm)
@@ -124,14 +124,14 @@ var destroyUserCmd = &cobra.Command{
} }
if confirm || force { if confirm || force {
request := &v1.DeleteUserRequest{Name: userName} request := &v1.DeleteNamespaceRequest{Name: namespaceName}
response, err := client.DeleteUser(ctx, request) response, err := client.DeleteNamespace(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf( fmt.Sprintf(
"Cannot destroy user: %s", "Cannot destroy namespace: %s",
status.Convert(err).Message(), status.Convert(err).Message(),
), ),
output, output,
@@ -139,16 +139,16 @@ var destroyUserCmd = &cobra.Command{
return return
} }
SuccessOutput(response, "User destroyed", output) SuccessOutput(response, "Namespace destroyed", output)
} else { } else {
SuccessOutput(map[string]string{"Result": "User not destroyed"}, "User not destroyed", output) SuccessOutput(map[string]string{"Result": "Namespace not destroyed"}, "Namespace not destroyed", output)
} }
}, },
} }
var listUsersCmd = &cobra.Command{ var listNamespacesCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List all the users", Short: "List all the namespaces",
Aliases: []string{"ls", "show"}, Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
@@ -157,13 +157,13 @@ var listUsersCmd = &cobra.Command{
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
request := &v1.ListUsersRequest{} request := &v1.ListNamespacesRequest{}
response, err := client.ListUsers(ctx, request) response, err := client.ListNamespaces(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Cannot get users: %s", status.Convert(err).Message()), fmt.Sprintf("Cannot get namespaces: %s", status.Convert(err).Message()),
output, output,
) )
@@ -171,19 +171,19 @@ var listUsersCmd = &cobra.Command{
} }
if output != "" { if output != "" {
SuccessOutput(response.Users, "", output) SuccessOutput(response.Namespaces, "", output)
return return
} }
tableData := pterm.TableData{{"ID", "Name", "Created"}} tableData := pterm.TableData{{"ID", "Name", "Created"}}
for _, user := range response.GetUsers() { for _, namespace := range response.GetNamespaces() {
tableData = append( tableData = append(
tableData, tableData,
[]string{ []string{
user.GetId(), namespace.GetId(),
user.GetName(), namespace.GetName(),
user.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"), namespace.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
}, },
) )
} }
@@ -200,9 +200,9 @@ var listUsersCmd = &cobra.Command{
}, },
} }
var renameUserCmd = &cobra.Command{ var renameNamespaceCmd = &cobra.Command{
Use: "rename OLD_NAME NEW_NAME", Use: "rename OLD_NAME NEW_NAME",
Short: "Renames a user", Short: "Renames a namespace",
Aliases: []string{"mv"}, Aliases: []string{"mv"},
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
expectedArguments := 2 expectedArguments := 2
@@ -219,17 +219,17 @@ var renameUserCmd = &cobra.Command{
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
request := &v1.RenameUserRequest{ request := &v1.RenameNamespaceRequest{
OldName: args[0], OldName: args[0],
NewName: args[1], NewName: args[1],
} }
response, err := client.RenameUser(ctx, request) response, err := client.RenameNamespace(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf( fmt.Sprintf(
"Cannot rename user: %s", "Cannot rename namespace: %s",
status.Convert(err).Message(), status.Convert(err).Message(),
), ),
output, output,
@@ -238,6 +238,6 @@ var renameUserCmd = &cobra.Command{
return return
} }
SuccessOutput(response.User, "User renamed", output) SuccessOutput(response.Namespace, "Namespace renamed", output)
}, },
} }

View File

@@ -19,24 +19,12 @@ import (
func init() { func init() {
rootCmd.AddCommand(nodeCmd) rootCmd.AddCommand(nodeCmd)
listNodesCmd.Flags().StringP("user", "u", "", "Filter by user") listNodesCmd.Flags().StringP("namespace", "n", "", "Filter by namespace")
listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags") listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags")
listNodesCmd.Flags().StringP("namespace", "n", "", "User")
listNodesNamespaceFlag := listNodesCmd.Flags().Lookup("namespace")
listNodesNamespaceFlag.Deprecated = deprecateNamespaceMessage
listNodesNamespaceFlag.Hidden = true
nodeCmd.AddCommand(listNodesCmd) nodeCmd.AddCommand(listNodesCmd)
registerNodeCmd.Flags().StringP("user", "u", "", "User") registerNodeCmd.Flags().StringP("namespace", "n", "", "Namespace")
err := registerNodeCmd.MarkFlagRequired("namespace")
registerNodeCmd.Flags().StringP("namespace", "n", "", "User")
registerNodeNamespaceFlag := registerNodeCmd.Flags().Lookup("namespace")
registerNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
registerNodeNamespaceFlag.Hidden = true
err := registerNodeCmd.MarkFlagRequired("user")
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
@@ -75,14 +63,9 @@ func init() {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
moveNodeCmd.Flags().StringP("user", "u", "", "New user") moveNodeCmd.Flags().StringP("namespace", "n", "", "New namespace")
moveNodeCmd.Flags().StringP("namespace", "n", "", "User") err = moveNodeCmd.MarkFlagRequired("namespace")
moveNodeNamespaceFlag := moveNodeCmd.Flags().Lookup("namespace")
moveNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
moveNodeNamespaceFlag.Hidden = true
err = moveNodeCmd.MarkFlagRequired("user")
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
@@ -110,9 +93,9 @@ var registerNodeCmd = &cobra.Command{
Short: "Registers a machine to your network", Short: "Registers a machine to your network",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
user, err := cmd.Flags().GetString("user") namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return return
} }
@@ -133,8 +116,8 @@ var registerNodeCmd = &cobra.Command{
} }
request := &v1.RegisterMachineRequest{ request := &v1.RegisterMachineRequest{
Key: machineKey, Key: machineKey,
User: user, Namespace: namespace,
} }
response, err := client.RegisterMachine(ctx, request) response, err := client.RegisterMachine(ctx, request)
@@ -163,9 +146,9 @@ var listNodesCmd = &cobra.Command{
Aliases: []string{"ls", "show"}, Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
user, err := cmd.Flags().GetString("user") namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return return
} }
@@ -181,7 +164,7 @@ var listNodesCmd = &cobra.Command{
defer conn.Close() defer conn.Close()
request := &v1.ListMachinesRequest{ request := &v1.ListMachinesRequest{
User: user, Namespace: namespace,
} }
response, err := client.ListMachines(ctx, request) response, err := client.ListMachines(ctx, request)
@@ -201,7 +184,7 @@ var listNodesCmd = &cobra.Command{
return return
} }
tableData, err := nodesToPtables(user, showTags, response.Machines) tableData, err := nodesToPtables(namespace, showTags, response.Machines)
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
@@ -405,7 +388,7 @@ var deleteNodeCmd = &cobra.Command{
var moveNodeCmd = &cobra.Command{ var moveNodeCmd = &cobra.Command{
Use: "move", Use: "move",
Short: "Move node to another user", Short: "Move node to another namespace",
Aliases: []string{"mv"}, Aliases: []string{"mv"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
@@ -421,11 +404,11 @@ var moveNodeCmd = &cobra.Command{
return return
} }
user, err := cmd.Flags().GetString("user") namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Error getting user: %s", err), fmt.Sprintf("Error getting namespace: %s", err),
output, output,
) )
@@ -456,7 +439,7 @@ var moveNodeCmd = &cobra.Command{
moveRequest := &v1.MoveMachineRequest{ moveRequest := &v1.MoveMachineRequest{
MachineId: identifier, MachineId: identifier,
User: user, Namespace: namespace,
} }
moveResponse, err := client.MoveMachine(ctx, moveRequest) moveResponse, err := client.MoveMachine(ctx, moveRequest)
@@ -473,12 +456,12 @@ var moveNodeCmd = &cobra.Command{
return return
} }
SuccessOutput(moveResponse.Machine, "Node moved to another user", output) SuccessOutput(moveResponse.Machine, "Node moved to another namespace", output)
}, },
} }
func nodesToPtables( func nodesToPtables(
currentUser string, currentNamespace string,
showTags bool, showTags bool,
machines []*v1.Machine, machines []*v1.Machine,
) (pterm.TableData, error) { ) (pterm.TableData, error) {
@@ -486,13 +469,11 @@ func nodesToPtables(
"ID", "ID",
"Hostname", "Hostname",
"Name", "Name",
"MachineKey",
"NodeKey", "NodeKey",
"User", "Namespace",
"IP addresses", "IP addresses",
"Ephemeral", "Ephemeral",
"Last seen", "Last seen",
"Expiration",
"Online", "Online",
"Expired", "Expired",
} }
@@ -519,24 +500,12 @@ func nodesToPtables(
} }
var expiry time.Time var expiry time.Time
var expiryTime string
if machine.Expiry != nil { if machine.Expiry != nil {
expiry = machine.Expiry.AsTime() expiry = machine.Expiry.AsTime()
expiryTime = expiry.Format("2006-01-02 15:04:05")
} else {
expiryTime = "N/A"
}
var machineKey key.MachinePublic
err := machineKey.UnmarshalText(
[]byte(headscale.MachinePublicKeyEnsurePrefix(machine.MachineKey)),
)
if err != nil {
machineKey = key.MachinePublic{}
} }
var nodeKey key.NodePublic var nodeKey key.NodePublic
err = nodeKey.UnmarshalText( err := nodeKey.UnmarshalText(
[]byte(headscale.NodePublicKeyEnsurePrefix(machine.NodeKey)), []byte(headscale.NodePublicKeyEnsurePrefix(machine.NodeKey)),
) )
if err != nil { if err != nil {
@@ -544,7 +513,9 @@ func nodesToPtables(
} }
var online string var online string
if machine.Online { if lastSeen.After(
time.Now().Add(-5 * time.Minute),
) { // TODO: Find a better way to reliably show if online
online = pterm.LightGreen("online") online = pterm.LightGreen("online")
} else { } else {
online = pterm.LightRed("offline") online = pterm.LightRed("offline")
@@ -577,12 +548,12 @@ func nodesToPtables(
} }
validTags = strings.TrimLeft(validTags, ",") validTags = strings.TrimLeft(validTags, ",")
var user string var namespace string
if currentUser == "" || (currentUser == machine.User.Name) { if currentNamespace == "" || (currentNamespace == machine.Namespace.Name) {
user = pterm.LightMagenta(machine.User.Name) namespace = pterm.LightMagenta(machine.Namespace.Name)
} else { } else {
// Shared into this user // Shared into this namespace
user = pterm.LightYellow(machine.User.Name) namespace = pterm.LightYellow(machine.Namespace.Name)
} }
var IPV4Address string var IPV4Address string
@@ -599,13 +570,11 @@ func nodesToPtables(
strconv.FormatUint(machine.Id, headscale.Base10), strconv.FormatUint(machine.Id, headscale.Base10),
machine.Name, machine.Name,
machine.GetGivenName(), machine.GetGivenName(),
machineKey.ShortString(),
nodeKey.ShortString(), nodeKey.ShortString(),
user, namespace,
strings.Join([]string{IPV4Address, IPV6Address}, ", "), strings.Join([]string{IPV4Address, IPV6Address}, ", "),
strconv.FormatBool(ephemeral), strconv.FormatBool(ephemeral),
lastSeenTime, lastSeenTime,
expiryTime,
online, online,
expired, expired,
} }

View File

@@ -20,14 +20,8 @@ const (
func init() { func init() {
rootCmd.AddCommand(preauthkeysCmd) rootCmd.AddCommand(preauthkeysCmd)
preauthkeysCmd.PersistentFlags().StringP("user", "u", "", "User") preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace")
err := preauthkeysCmd.MarkPersistentFlagRequired("namespace")
preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "User")
pakNamespaceFlag := preauthkeysCmd.PersistentFlags().Lookup("namespace")
pakNamespaceFlag.Deprecated = deprecateNamespaceMessage
pakNamespaceFlag.Hidden = true
err := preauthkeysCmd.MarkPersistentFlagRequired("user")
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") log.Fatal().Err(err).Msg("")
} }
@@ -52,14 +46,14 @@ var preauthkeysCmd = &cobra.Command{
var listPreAuthKeys = &cobra.Command{ var listPreAuthKeys = &cobra.Command{
Use: "list", Use: "list",
Short: "List the preauthkeys for this user", Short: "List the preauthkeys for this namespace",
Aliases: []string{"ls", "show"}, Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
user, err := cmd.Flags().GetString("user") namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return return
} }
@@ -69,7 +63,7 @@ var listPreAuthKeys = &cobra.Command{
defer conn.Close() defer conn.Close()
request := &v1.ListPreAuthKeysRequest{ request := &v1.ListPreAuthKeysRequest{
User: user, Namespace: namespace,
} }
response, err := client.ListPreAuthKeys(ctx, request) response, err := client.ListPreAuthKeys(ctx, request)
@@ -149,14 +143,14 @@ var listPreAuthKeys = &cobra.Command{
var createPreAuthKeyCmd = &cobra.Command{ var createPreAuthKeyCmd = &cobra.Command{
Use: "create", Use: "create",
Short: "Creates a new preauthkey in the specified user", Short: "Creates a new preauthkey in the specified namespace",
Aliases: []string{"c", "new"}, Aliases: []string{"c", "new"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
user, err := cmd.Flags().GetString("user") namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return return
} }
@@ -168,11 +162,11 @@ var createPreAuthKeyCmd = &cobra.Command{
log.Trace(). log.Trace().
Bool("reusable", reusable). Bool("reusable", reusable).
Bool("ephemeral", ephemeral). Bool("ephemeral", ephemeral).
Str("user", user). Str("namespace", namespace).
Msg("Preparing to create preauthkey") Msg("Preparing to create preauthkey")
request := &v1.CreatePreAuthKeyRequest{ request := &v1.CreatePreAuthKeyRequest{
User: user, Namespace: namespace,
Reusable: reusable, Reusable: reusable,
Ephemeral: ephemeral, Ephemeral: ephemeral,
AclTags: tags, AclTags: tags,
@@ -231,9 +225,9 @@ var expirePreAuthKeyCmd = &cobra.Command{
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
user, err := cmd.Flags().GetString("user") namespace, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return return
} }
@@ -243,8 +237,8 @@ var expirePreAuthKeyCmd = &cobra.Command{
defer conn.Close() defer conn.Close()
request := &v1.ExpirePreAuthKeyRequest{ request := &v1.ExpirePreAuthKeyRequest{
User: user, Namespace: namespace,
Key: args[0], Key: args[0],
} }
response, err := client.ExpirePreAuthKey(ctx, request) response, err := client.ExpirePreAuthKey(ctx, request)

View File

@@ -12,15 +12,10 @@ import (
"github.com/tcnksm/go-latest" "github.com/tcnksm/go-latest"
) )
const (
deprecateNamespaceMessage = "use --user"
)
var cfgFile string = "" var cfgFile string = ""
func init() { func init() {
if len(os.Args) > 1 && if len(os.Args) > 1 && (os.Args[1] == "version" || os.Args[1] == "mockoidc" || os.Args[1] == "completion") {
(os.Args[1] == "version" || os.Args[1] == "mockoidc" || os.Args[1] == "completion") {
return return
} }

View File

@@ -3,45 +3,37 @@ package cli
import ( import (
"fmt" "fmt"
"log" "log"
"net/netip"
"strconv" "strconv"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
const (
Base10 = 10
)
func init() { func init() {
rootCmd.AddCommand(routesCmd) rootCmd.AddCommand(routesCmd)
listRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)") listRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err := listRoutesCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
routesCmd.AddCommand(listRoutesCmd) routesCmd.AddCommand(listRoutesCmd)
enableRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)") enableRouteCmd.Flags().
err := enableRouteCmd.MarkFlagRequired("route") StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to enable")
enableRouteCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
enableRouteCmd.Flags().BoolP("all", "a", false, "All routes from host")
err = enableRouteCmd.MarkFlagRequired("identifier")
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
routesCmd.AddCommand(enableRouteCmd) routesCmd.AddCommand(enableRouteCmd)
disableRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)") nodeCmd.AddCommand(routesCmd)
err = disableRouteCmd.MarkFlagRequired("route")
if err != nil {
log.Fatalf(err.Error())
}
routesCmd.AddCommand(disableRouteCmd)
deleteRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)")
err = deleteRouteCmd.MarkFlagRequired("route")
if err != nil {
log.Fatalf(err.Error())
}
routesCmd.AddCommand(deleteRouteCmd)
} }
var routesCmd = &cobra.Command{ var routesCmd = &cobra.Command{
@@ -52,7 +44,7 @@ var routesCmd = &cobra.Command{
var listRoutesCmd = &cobra.Command{ var listRoutesCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List all routes", Short: "List routes advertised and enabled by a given node",
Aliases: []string{"ls", "show"}, Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
@@ -72,51 +64,28 @@ var listRoutesCmd = &cobra.Command{
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
var routes []*v1.Route request := &v1.GetMachineRouteRequest{
MachineId: machineID,
if machineID == 0 {
response, err := client.GetRoutes(ctx, &v1.GetRoutesRequest{})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
return
}
routes = response.Routes
} else {
response, err := client.GetMachineRoutes(ctx, &v1.GetMachineRoutesRequest{
MachineId: machineID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get routes for machine %d: %s", machineID, status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
return
}
routes = response.Routes
} }
tableData := routesToPtables(routes) response, err := client.GetMachineRoute(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
return
}
tableData := routesToPtables(response.Routes)
if err != nil { if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
@@ -138,12 +107,16 @@ var listRoutesCmd = &cobra.Command{
var enableRouteCmd = &cobra.Command{ var enableRouteCmd = &cobra.Command{
Use: "enable", Use: "enable",
Short: "Set a route as enabled", Short: "Set the enabled routes for a given node",
Long: `This command will make as enabled a given route.`, Long: `This command will take a list of routes that will _replace_
the current set of routes on a given node.
If you would like to disable a route, simply run the command again, but
omit the route you do not want to enable.
`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output") output, _ := cmd.Flags().GetString("output")
routeID, err := cmd.Flags().GetUint64("route") machineID, err := cmd.Flags().GetUint64("identifier")
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
@@ -158,13 +131,52 @@ var enableRouteCmd = &cobra.Command{
defer cancel() defer cancel()
defer conn.Close() defer conn.Close()
response, err := client.EnableRoute(ctx, &v1.EnableRouteRequest{ var routes []string
RouteId: routeID,
}) isAll, _ := cmd.Flags().GetBool("all")
if isAll {
response, err := client.GetMachineRoute(ctx, &v1.GetMachineRouteRequest{
MachineId: machineID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot get machine routes: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
routes = response.GetRoutes().GetAdvertisedRoutes()
} else {
routes, err = cmd.Flags().GetStringSlice("route")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting routes from flag: %s", err),
output,
)
return
}
}
request := &v1.EnableMachineRoutesRequest{
MachineId: machineID,
Routes: routes,
}
response, err := client.EnableMachineRoutes(ctx, request)
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Cannot enable route %d: %s", routeID, status.Convert(err).Message()), fmt.Sprintf(
"Cannot register machine: %s\n",
status.Convert(err).Message(),
),
output, output,
) )
@@ -172,127 +184,50 @@ var enableRouteCmd = &cobra.Command{
} }
if output != "" { if output != "" {
SuccessOutput(response, "", output) SuccessOutput(response.Routes, "", output)
return return
} }
},
}
var disableRouteCmd = &cobra.Command{ tableData := routesToPtables(response.Routes)
Use: "disable", if err != nil {
Short: "Set as disabled a given route", ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
Long: `This command will make as disabled a given route.`,
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
routeID, err := cmd.Flags().GetUint64("route") return
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil { if err != nil {
ErrorOutput( ErrorOutput(
err, err,
fmt.Sprintf("Error getting machine id from flag: %s", err), fmt.Sprintf("Failed to render pterm table: %s", err),
output, output,
) )
return return
} }
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
response, err := client.DisableRoute(ctx, &v1.DisableRouteRequest{
RouteId: routeID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot disable route %d: %s", routeID, status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response, "", output)
return
}
},
}
var deleteRouteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a given route",
Long: `This command will delete a given route.`,
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
routeID, err := cmd.Flags().GetUint64("route")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting machine id from flag: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
response, err := client.DeleteRoute(ctx, &v1.DeleteRouteRequest{
RouteId: routeID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot delete route %d: %s", routeID, status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response, "", output)
return
}
}, },
} }
// routesToPtables converts the list of routes to a nice table. // routesToPtables converts the list of routes to a nice table.
func routesToPtables(routes []*v1.Route) pterm.TableData { func routesToPtables(routes *v1.Routes) pterm.TableData {
tableData := pterm.TableData{{"ID", "Machine", "Prefix", "Advertised", "Enabled", "Primary"}} tableData := pterm.TableData{{"Route", "Enabled"}}
for _, route := range routes { for _, route := range routes.GetAdvertisedRoutes() {
var isPrimaryStr string enabled := isStringInSlice(routes.EnabledRoutes, route)
prefix, err := netip.ParsePrefix(route.Prefix)
if err != nil {
log.Printf("Error parsing prefix %s: %s", route.Prefix, err)
continue tableData = append(tableData, []string{route, strconv.FormatBool(enabled)})
}
if prefix == headscale.ExitRouteV4 || prefix == headscale.ExitRouteV6 {
isPrimaryStr = "-"
} else {
isPrimaryStr = strconv.FormatBool(route.IsPrimary)
}
tableData = append(tableData,
[]string{
strconv.FormatUint(route.Id, Base10),
route.Machine.GivenName,
route.Prefix,
strconv.FormatBool(route.Advertised),
strconv.FormatBool(route.Enabled),
isPrimaryStr,
})
} }
return tableData return tableData
} }
func isStringInSlice(strs []string, s string) bool {
for _, s2 := range strs {
if s == s2 {
return true
}
}
return false
}

View File

@@ -14,7 +14,7 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v2"
) )
const ( const (

View File

@@ -58,7 +58,7 @@ func (*Suite) TestConfigFileLoading(c *check.C) {
c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080") c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080")
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090") c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3") c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite") c.Assert(viper.GetString("db_path"), check.Equals, "./db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
@@ -101,7 +101,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080") c.Assert(viper.GetString("listen_addr"), check.Equals, "127.0.0.1:8080")
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090") c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3") c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite") c.Assert(viper.GetString("db_path"), check.Equals, "./db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")

View File

@@ -44,7 +44,9 @@ grpc_allow_insecure: false
# and Tailscale clients. # and Tailscale clients.
# The private key file will be autogenerated if it's missing. # The private key file will be autogenerated if it's missing.
# #
private_key_path: /var/lib/headscale/private.key # For production:
# /var/lib/headscale/private.key
private_key_path: ./private.key
# The Noise section includes specific configuration for the # The Noise section includes specific configuration for the
# TS2021 Noise protocol # TS2021 Noise protocol
@@ -53,16 +55,14 @@ noise:
# traffic between headscale and Tailscale clients when # traffic between headscale and Tailscale clients when
# using the new Noise-based protocol. It must be different # using the new Noise-based protocol. It must be different
# from the legacy private key. # from the legacy private key.
private_key_path: /var/lib/headscale/noise_private.key #
# For production:
# private_key_path: /var/lib/headscale/noise_private.key
private_key_path: ./noise_private.key
# List of IP prefixes to allocate tailaddresses from. # List of IP prefixes to allocate tailaddresses from.
# Each prefix consists of either an IPv4 or IPv6 address, # Each prefix consists of either an IPv4 or IPv6 address,
# and the associated prefix length, delimited by a slash. # and the associated prefix length, delimited by a slash.
# While this looks like it can take arbitrary values, it
# needs to be within IP ranges supported by the Tailscale
# client.
# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
ip_prefixes: ip_prefixes:
- fd7a:115c:a1e0::/48 - fd7a:115c:a1e0::/48
- 100.64.0.0/10 - 100.64.0.0/10
@@ -132,7 +132,8 @@ node_update_check_interval: 10s
db_type: sqlite3 db_type: sqlite3
# For production: # For production:
db_path: /var/lib/headscale/db.sqlite # db_path: /var/lib/headscale/db.sqlite
db_path: ./db.sqlite
# # Postgres config # # Postgres config
# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
@@ -166,7 +167,8 @@ tls_letsencrypt_hostname: ""
# Path to store certificates and metadata needed by # Path to store certificates and metadata needed by
# letsencrypt # letsencrypt
# For production: # For production:
tls_letsencrypt_cache_dir: /var/lib/headscale/cache # tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_cache_dir: ./cache
# Type of ACME challenge to use, currently supported types: # Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS-ALPN-01 # HTTP-01 or TLS-ALPN-01
@@ -233,17 +235,6 @@ dns_config:
# Search domains to inject. # Search domains to inject.
domains: [] domains: []
# Extra DNS records
# so far only A-records are supported (on the tailscale side)
# See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations
# extra_records:
# - name: "grafana.myvpn.example.com"
# type: "A"
# value: "100.64.0.3"
#
# # you can also put it in one line
# - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
# Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
# Only works if there is at least a nameserver defined. # Only works if there is at least a nameserver defined.
magic_dns: true magic_dns: true
@@ -251,12 +242,13 @@ dns_config:
# Defines the base domain to create the hostnames for MagicDNS. # Defines the base domain to create the hostnames for MagicDNS.
# `base_domain` must be a FQDNs, without the trailing dot. # `base_domain` must be a FQDNs, without the trailing dot.
# The FQDN of the hosts will be # The FQDN of the hosts will be
# `hostname.user.base_domain` (e.g., _myhost.myuser.example.com_). # `hostname.namespace.base_domain` (e.g., _myhost.mynamespace.example.com_).
base_domain: example.com base_domain: example.com
# Unix socket used for the CLI to connect without authentication # Unix socket used for the CLI to connect without authentication
# Note: for production you will want to set this to something like: # Note: for production you will want to set this to something like:
unix_socket: /var/run/headscale/headscale.sock # unix_socket: /var/run/headscale.sock
unix_socket: ./headscale.sock
unix_socket_permission: "0770" unix_socket_permission: "0770"
# #
# headscale supports experimental OpenID connect support, # headscale supports experimental OpenID connect support,
@@ -268,45 +260,26 @@ unix_socket_permission: "0770"
# issuer: "https://your-oidc.issuer.com/path" # issuer: "https://your-oidc.issuer.com/path"
# client_id: "your-oidc-client-id" # client_id: "your-oidc-client-id"
# client_secret: "your-oidc-client-secret" # client_secret: "your-oidc-client-secret"
# # Alternatively, set `client_secret_path` to read the secret from the file.
# # It resolves environment variables, making integration to systemd's
# # `LoadCredential` straightforward:
# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
# # client_secret and client_secret_path are mutually exclusive.
# #
# # The amount of time from a node is authenticated with OpenID until it # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
# # expires and needs to reauthenticate. # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
# # Setting the value to "0" will mean no expiry.
# expiry: 180d
#
# # Use the expiry from the token received from OpenID when the user logged
# # in, this will typically lead to frequent need to reauthenticate and should
# # only been enabled if you know what you are doing.
# # Note: enabling this will cause `oidc.expiry` to be ignored.
# use_expiry_from_token: false
#
# # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
# # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
# #
# scope: ["openid", "profile", "email", "custom"] # scope: ["openid", "profile", "email", "custom"]
# extra_params: # extra_params:
# domain_hint: example.com # domain_hint: example.com
# #
# # List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the # List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the
# # authentication request will be rejected. # authentication request will be rejected.
# #
# allowed_domains: # allowed_domains:
# - example.com # - example.com
# # Note: Groups from keycloak have a leading '/'
# allowed_groups:
# - /headscale
# allowed_users: # allowed_users:
# - alice@example.com # - alice@example.com
# #
# # If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed. # If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
# # This will transform `first-name.last-name@example.com` to the user `first-name.last-name` # This will transform `first-name.last-name@example.com` to the namespace `first-name.last-name`
# # If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following # If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
# user: `first-name.last-name.example.com` # namespace: `first-name.last-name.example.com`
# #
# strip_email_domain: true # strip_email_domain: true

110
config.go
View File

@@ -6,12 +6,10 @@ import (
"io/fs" "io/fs"
"net/netip" "net/netip"
"net/url" "net/url"
"os"
"strings" "strings"
"time" "time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/prometheus/common/model"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -26,13 +24,6 @@ const (
JSONLogFormat = "json" JSONLogFormat = "json"
TextLogFormat = "text" TextLogFormat = "text"
defaultOIDCExpiryTime = 180 * 24 * time.Hour // 180 Days
maxDuration time.Duration = 1<<63 - 1
)
var errOidcMutuallyExclusive = errors.New(
"oidc_client_secret and oidc_client_secret_path are mutually exclusive",
) )
// Config contains the initial Headscale configuration. // Config contains the initial Headscale configuration.
@@ -105,10 +96,7 @@ type OIDCConfig struct {
ExtraParams map[string]string ExtraParams map[string]string
AllowedDomains []string AllowedDomains []string
AllowedUsers []string AllowedUsers []string
AllowedGroups []string
StripEmaildomain bool StripEmaildomain bool
Expiry time.Duration
UseExpiryFromToken bool
} }
type DERPConfig struct { type DERPConfig struct {
@@ -183,13 +171,9 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("cli.timeout", "5s") viper.SetDefault("cli.timeout", "5s")
viper.SetDefault("cli.insecure", false) viper.SetDefault("cli.insecure", false)
viper.SetDefault("db_ssl", false)
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"}) viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
viper.SetDefault("oidc.strip_email_domain", true) viper.SetDefault("oidc.strip_email_domain", true)
viper.SetDefault("oidc.only_start_if_oidc_is_available", true) viper.SetDefault("oidc.only_start_if_oidc_is_available", true)
viper.SetDefault("oidc.expiry", "180d")
viper.SetDefault("oidc.use_expiry_from_token", false)
viper.SetDefault("logtail.enabled", false) viper.SetDefault("logtail.enabled", false)
viper.SetDefault("randomize_client_port", false) viper.SetDefault("randomize_client_port", false)
@@ -421,37 +405,39 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
} }
if viper.IsSet("dns_config.restricted_nameservers") { if viper.IsSet("dns_config.restricted_nameservers") {
dnsConfig.Routes = make(map[string][]*dnstype.Resolver) if len(dnsConfig.Nameservers) > 0 {
domains := []string{} dnsConfig.Routes = make(map[string][]*dnstype.Resolver)
restrictedDNS := viper.GetStringMapStringSlice( restrictedDNS := viper.GetStringMapStringSlice(
"dns_config.restricted_nameservers", "dns_config.restricted_nameservers",
)
for domain, restrictedNameservers := range restrictedDNS {
restrictedResolvers := make(
[]*dnstype.Resolver,
len(restrictedNameservers),
) )
for index, nameserverStr := range restrictedNameservers { for domain, restrictedNameservers := range restrictedDNS {
nameserver, err := netip.ParseAddr(nameserverStr) restrictedResolvers := make(
if err != nil { []*dnstype.Resolver,
log.Error(). len(restrictedNameservers),
Str("func", "getDNSConfig"). )
Err(err). for index, nameserverStr := range restrictedNameservers {
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr) nameserver, err := netip.ParseAddr(nameserverStr)
} if err != nil {
restrictedResolvers[index] = &dnstype.Resolver{ log.Error().
Addr: nameserver.String(), Str("func", "getDNSConfig").
Err(err).
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
}
restrictedResolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(),
}
} }
dnsConfig.Routes[domain] = restrictedResolvers
} }
dnsConfig.Routes[domain] = restrictedResolvers } else {
domains = append(domains, domain) log.Warn().
Msg("Warning: dns_config.restricted_nameservers is set, but no nameservers are configured. Ignoring restricted_nameservers.")
} }
dnsConfig.Domains = domains
} }
if viper.IsSet("dns_config.domains") { if viper.IsSet("dns_config.domains") {
domains := viper.GetStringSlice("dns_config.domains") domains := viper.GetStringSlice("dns_config.domains")
if len(dnsConfig.Resolvers) > 0 { if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Domains = domains dnsConfig.Domains = domains
} else if domains != nil { } else if domains != nil {
log.Warn(). log.Warn().
@@ -459,20 +445,6 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
} }
} }
if viper.IsSet("dns_config.extra_records") {
var extraRecords []tailcfg.DNSRecord
err := viper.UnmarshalKey("dns_config.extra_records", &extraRecords)
if err != nil {
log.Error().
Str("func", "getDNSConfig").
Err(err).
Msgf("Could not parse dns_config.extra_records")
}
dnsConfig.ExtraRecords = extraRecords
}
if viper.IsSet("dns_config.magic_dns") { if viper.IsSet("dns_config.magic_dns") {
dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns")
} }
@@ -539,19 +511,6 @@ func GetHeadscaleConfig() (*Config, error) {
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes) Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
} }
oidcClientSecret := viper.GetString("oidc.client_secret")
oidcClientSecretPath := viper.GetString("oidc.client_secret_path")
if oidcClientSecretPath != "" && oidcClientSecret != "" {
return nil, errOidcMutuallyExclusive
}
if oidcClientSecretPath != "" {
secretBytes, err := os.ReadFile(os.ExpandEnv(oidcClientSecretPath))
if err != nil {
return nil, err
}
oidcClientSecret = string(secretBytes)
}
return &Config{ return &Config{
ServerURL: viper.GetString("server_url"), ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"), Addr: viper.GetString("listen_addr"),
@@ -604,29 +563,12 @@ func GetHeadscaleConfig() (*Config, error) {
), ),
Issuer: viper.GetString("oidc.issuer"), Issuer: viper.GetString("oidc.issuer"),
ClientID: viper.GetString("oidc.client_id"), ClientID: viper.GetString("oidc.client_id"),
ClientSecret: oidcClientSecret, ClientSecret: viper.GetString("oidc.client_secret"),
Scope: viper.GetStringSlice("oidc.scope"), Scope: viper.GetStringSlice("oidc.scope"),
ExtraParams: viper.GetStringMapString("oidc.extra_params"), ExtraParams: viper.GetStringMapString("oidc.extra_params"),
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"), AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"), AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"), StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
Expiry: func() time.Duration {
// if set to 0, we assume no expiry
if value := viper.GetString("oidc.expiry"); value == "0" {
return maxDuration
} else {
expiry, err := model.ParseDuration(value)
if err != nil {
log.Warn().Msg("failed to parse oidc.expiry, defaulting back to 180 days")
return defaultOIDCExpiryTime
}
return time.Duration(expiry)
}
}(),
UseExpiryFromToken: viper.GetBool("oidc.use_expiry_from_token"),
}, },
LogTail: logConfig, LogTail: logConfig,

109
db.go
View File

@@ -18,10 +18,8 @@ import (
) )
const ( const (
dbVersion = "1" dbVersion = "1"
errValueNotFound = Error("not found")
errValueNotFound = Error("not found")
ErrCannotParsePrefix = Error("cannot parse prefix")
) )
// KV is a key-value store in a psql table. For future use... // KV is a key-value store in a psql table. For future use...
@@ -41,16 +39,6 @@ func (h *Headscale) initDB() error {
db.Exec(`create extension if not exists "uuid-ossp";`) db.Exec(`create extension if not exists "uuid-ossp";`)
} }
_ = db.Migrator().RenameTable("namespaces", "users")
err = db.AutoMigrate(&User{})
if err != nil {
return err
}
_ = db.Migrator().RenameColumn(&Machine{}, "namespace_id", "user_id")
_ = db.Migrator().RenameColumn(&PreAuthKey{}, "namespace_id", "user_id")
_ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses") _ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")
_ = db.Migrator().RenameColumn(&Machine{}, "name", "hostname") _ = db.Migrator().RenameColumn(&Machine{}, "name", "hostname")
@@ -91,70 +79,6 @@ func (h *Headscale) initDB() error {
} }
} }
err = db.AutoMigrate(&Route{})
if err != nil {
return err
}
if db.Migrator().HasColumn(&Machine{}, "enabled_routes") {
log.Info().Msgf("Database has legacy enabled_routes column in machine, migrating...")
type MachineAux struct {
ID uint64
EnabledRoutes IPPrefixes
}
machinesAux := []MachineAux{}
err := db.Table("machines").Select("id, enabled_routes").Scan(&machinesAux).Error
if err != nil {
log.Fatal().Err(err).Msg("Error accessing db")
}
for _, machine := range machinesAux {
for _, prefix := range machine.EnabledRoutes {
if err != nil {
log.Error().
Err(err).
Str("enabled_route", prefix.String()).
Msg("Error parsing enabled_route")
continue
}
err = db.Preload("Machine").
Where("machine_id = ? AND prefix = ?", machine.ID, IPPrefix(prefix)).
First(&Route{}).
Error
if err == nil {
log.Info().
Str("enabled_route", prefix.String()).
Msg("Route already migrated to new table, skipping")
continue
}
route := Route{
MachineID: machine.ID,
Advertised: true,
Enabled: true,
Prefix: IPPrefix(prefix),
}
if err := h.db.Create(&route).Error; err != nil {
log.Error().Err(err).Msg("Error creating route")
} else {
log.Info().
Uint64("machine_id", route.MachineID).
Str("prefix", prefix.String()).
Msg("Route migrated")
}
}
}
err = db.Migrator().DropColumn(&Machine{}, "enabled_routes")
if err != nil {
log.Error().Err(err).Msg("Error dropping enabled_routes column")
}
}
err = db.AutoMigrate(&Machine{}) err = db.AutoMigrate(&Machine{})
if err != nil { if err != nil {
return err return err
@@ -197,6 +121,11 @@ func (h *Headscale) initDB() error {
return err return err
} }
err = db.AutoMigrate(&Namespace{})
if err != nil {
return err
}
err = db.AutoMigrate(&PreAuthKey{}) err = db.AutoMigrate(&PreAuthKey{})
if err != nil { if err != nil {
return err return err
@@ -335,30 +264,6 @@ func (hi HostInfo) Value() (driver.Value, error) {
return string(bytes), err return string(bytes), err
} }
type IPPrefix netip.Prefix
func (i *IPPrefix) Scan(destination interface{}) error {
switch value := destination.(type) {
case string:
prefix, err := netip.ParsePrefix(value)
if err != nil {
return err
}
*i = IPPrefix(prefix)
return nil
default:
return fmt.Errorf("%w: unexpected data type %T", ErrCannotParsePrefix, destination)
}
}
// Value return json value, implement driver.Valuer interface.
func (i IPPrefix) Value() (driver.Value, error) {
prefixStr := netip.Prefix(i).String()
return prefixStr, nil
}
type IPPrefixes []netip.Prefix type IPPrefixes []netip.Prefix
func (i *IPPrefixes) Scan(destination interface{}) error { func (i *IPPrefixes) Scan(destination interface{}) error {

View File

@@ -10,7 +10,7 @@ import (
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v2"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )

View File

@@ -157,14 +157,14 @@ func (h *Headscale) DERPHandler(
if !fastStart { if !fastStart {
pubKey := h.privateKey.Public() pubKey := h.privateKey.Public()
pubKeyStr, _ := pubKey.MarshalText() //nolint pubKeyStr := pubKey.UntypedHexString() //nolint
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+ fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
"Upgrade: DERP\r\n"+ "Upgrade: DERP\r\n"+
"Connection: Upgrade\r\n"+ "Connection: Upgrade\r\n"+
"Derp-Version: %v\r\n"+ "Derp-Version: %v\r\n"+
"Derp-Public-Key: %s\r\n\r\n", "Derp-Public-Key: %s\r\n\r\n",
derp.ProtocolVersion, derp.ProtocolVersion,
string(pubKeyStr)) pubKeyStr)
} }
h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String()) h.DERPServer.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String())

14
dns.go
View File

@@ -190,23 +190,23 @@ func getMapResponseDNSConfig(
) *tailcfg.DNSConfig { ) *tailcfg.DNSConfig {
var dnsConfig *tailcfg.DNSConfig = dnsConfigOrig.Clone() var dnsConfig *tailcfg.DNSConfig = dnsConfigOrig.Clone()
if dnsConfigOrig != nil && dnsConfigOrig.Proxied { // if MagicDNS is enabled if dnsConfigOrig != nil && dnsConfigOrig.Proxied { // if MagicDNS is enabled
// Only inject the Search Domain of the current user - shared nodes should use their full FQDN // Only inject the Search Domain of the current namespace - shared nodes should use their full FQDN
dnsConfig.Domains = append( dnsConfig.Domains = append(
dnsConfig.Domains, dnsConfig.Domains,
fmt.Sprintf( fmt.Sprintf(
"%s.%s", "%s.%s",
machine.User.Name, machine.Namespace.Name,
baseDomain, baseDomain,
), ),
) )
userSet := mapset.NewSet[User]() namespaceSet := mapset.NewSet[Namespace]()
userSet.Add(machine.User) namespaceSet.Add(machine.Namespace)
for _, p := range peers { for _, p := range peers {
userSet.Add(p.User) namespaceSet.Add(p.Namespace)
} }
for _, user := range userSet.ToSlice() { for _, namespace := range namespaceSet.ToSlice() {
dnsRoute := fmt.Sprintf("%v.%v", user.Name, baseDomain) dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain)
dnsConfig.Routes[dnsRoute] = nil dnsConfig.Routes[dnsRoute] = nil
} }
} else { } else {

View File

@@ -112,17 +112,17 @@ func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) {
} }
func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
userShared1, err := app.CreateUser("shared1") namespaceShared1, err := app.CreateNamespace("shared1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
userShared2, err := app.CreateUser("shared2") namespaceShared2, err := app.CreateNamespace("shared2")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
userShared3, err := app.CreateUser("shared3") namespaceShared3, err := app.CreateNamespace("shared3")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared1, err := app.CreatePreAuthKey( preAuthKeyInShared1, err := app.CreatePreAuthKey(
userShared1.Name, namespaceShared1.Name,
false, false,
false, false,
nil, nil,
@@ -131,7 +131,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared2, err := app.CreatePreAuthKey( preAuthKeyInShared2, err := app.CreatePreAuthKey(
userShared2.Name, namespaceShared2.Name,
false, false,
false, false,
nil, nil,
@@ -140,7 +140,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared3, err := app.CreatePreAuthKey( preAuthKeyInShared3, err := app.CreatePreAuthKey(
userShared3.Name, namespaceShared3.Name,
false, false,
false, false,
nil, nil,
@@ -149,7 +149,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
PreAuthKey2InShared1, err := app.CreatePreAuthKey( PreAuthKey2InShared1, err := app.CreatePreAuthKey(
userShared1.Name, namespaceShared1.Name,
false, false,
false, false,
nil, nil,
@@ -157,7 +157,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine(userShared1.Name, "test_get_shared_nodes_1") _, err = app.GetMachine(namespaceShared1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
machineInShared1 := &Machine{ machineInShared1 := &Machine{
@@ -166,15 +166,15 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1", Hostname: "test_get_shared_nodes_1",
UserID: userShared1.ID, NamespaceID: namespaceShared1.ID,
User: *userShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
app.db.Save(machineInShared1) app.db.Save(machineInShared1)
_, err = app.GetMachine(userShared1.Name, machineInShared1.Hostname) _, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machineInShared2 := &Machine{ machineInShared2 := &Machine{
@@ -183,15 +183,15 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2", Hostname: "test_get_shared_nodes_2",
UserID: userShared2.ID, NamespaceID: namespaceShared2.ID,
User: *userShared2, Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
app.db.Save(machineInShared2) app.db.Save(machineInShared2)
_, err = app.GetMachine(userShared2.Name, machineInShared2.Hostname) _, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machineInShared3 := &Machine{ machineInShared3 := &Machine{
@@ -200,15 +200,15 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3", Hostname: "test_get_shared_nodes_3",
UserID: userShared3.ID, NamespaceID: namespaceShared3.ID,
User: *userShared3, Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
app.db.Save(machineInShared3) app.db.Save(machineInShared3)
_, err = app.GetMachine(userShared3.Name, machineInShared3.Hostname) _, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machine2InShared1 := &Machine{ machine2InShared1 := &Machine{
@@ -217,8 +217,8 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4", Hostname: "test_get_shared_nodes_4",
UserID: userShared1.ID, NamespaceID: namespaceShared1.ID,
User: *userShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
AuthKeyID: uint(PreAuthKey2InShared1.ID), AuthKeyID: uint(PreAuthKey2InShared1.ID),
@@ -245,31 +245,31 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
c.Assert(len(dnsConfig.Routes), check.Equals, 3) c.Assert(len(dnsConfig.Routes), check.Equals, 3)
domainRouteShared1 := fmt.Sprintf("%s.%s", userShared1.Name, baseDomain) domainRouteShared1 := fmt.Sprintf("%s.%s", namespaceShared1.Name, baseDomain)
_, ok := dnsConfig.Routes[domainRouteShared1] _, ok := dnsConfig.Routes[domainRouteShared1]
c.Assert(ok, check.Equals, true) c.Assert(ok, check.Equals, true)
domainRouteShared2 := fmt.Sprintf("%s.%s", userShared2.Name, baseDomain) domainRouteShared2 := fmt.Sprintf("%s.%s", namespaceShared2.Name, baseDomain)
_, ok = dnsConfig.Routes[domainRouteShared2] _, ok = dnsConfig.Routes[domainRouteShared2]
c.Assert(ok, check.Equals, true) c.Assert(ok, check.Equals, true)
domainRouteShared3 := fmt.Sprintf("%s.%s", userShared3.Name, baseDomain) domainRouteShared3 := fmt.Sprintf("%s.%s", namespaceShared3.Name, baseDomain)
_, ok = dnsConfig.Routes[domainRouteShared3] _, ok = dnsConfig.Routes[domainRouteShared3]
c.Assert(ok, check.Equals, true) c.Assert(ok, check.Equals, true)
} }
func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
userShared1, err := app.CreateUser("shared1") namespaceShared1, err := app.CreateNamespace("shared1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
userShared2, err := app.CreateUser("shared2") namespaceShared2, err := app.CreateNamespace("shared2")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
userShared3, err := app.CreateUser("shared3") namespaceShared3, err := app.CreateNamespace("shared3")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared1, err := app.CreatePreAuthKey( preAuthKeyInShared1, err := app.CreatePreAuthKey(
userShared1.Name, namespaceShared1.Name,
false, false,
false, false,
nil, nil,
@@ -278,7 +278,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared2, err := app.CreatePreAuthKey( preAuthKeyInShared2, err := app.CreatePreAuthKey(
userShared2.Name, namespaceShared2.Name,
false, false,
false, false,
nil, nil,
@@ -287,7 +287,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKeyInShared3, err := app.CreatePreAuthKey( preAuthKeyInShared3, err := app.CreatePreAuthKey(
userShared3.Name, namespaceShared3.Name,
false, false,
false, false,
nil, nil,
@@ -296,7 +296,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
preAuthKey2InShared1, err := app.CreatePreAuthKey( preAuthKey2InShared1, err := app.CreatePreAuthKey(
userShared1.Name, namespaceShared1.Name,
false, false,
false, false,
nil, nil,
@@ -304,7 +304,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine(userShared1.Name, "test_get_shared_nodes_1") _, err = app.GetMachine(namespaceShared1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
machineInShared1 := &Machine{ machineInShared1 := &Machine{
@@ -313,15 +313,15 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1", Hostname: "test_get_shared_nodes_1",
UserID: userShared1.ID, NamespaceID: namespaceShared1.ID,
User: *userShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
} }
app.db.Save(machineInShared1) app.db.Save(machineInShared1)
_, err = app.GetMachine(userShared1.Name, machineInShared1.Hostname) _, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machineInShared2 := &Machine{ machineInShared2 := &Machine{
@@ -330,15 +330,15 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2", Hostname: "test_get_shared_nodes_2",
UserID: userShared2.ID, NamespaceID: namespaceShared2.ID,
User: *userShared2, Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
} }
app.db.Save(machineInShared2) app.db.Save(machineInShared2)
_, err = app.GetMachine(userShared2.Name, machineInShared2.Hostname) _, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machineInShared3 := &Machine{ machineInShared3 := &Machine{
@@ -347,15 +347,15 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3", Hostname: "test_get_shared_nodes_3",
UserID: userShared3.ID, NamespaceID: namespaceShared3.ID,
User: *userShared3, Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
} }
app.db.Save(machineInShared3) app.db.Save(machineInShared3)
_, err = app.GetMachine(userShared3.Name, machineInShared3.Hostname) _, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Hostname)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machine2InShared1 := &Machine{ machine2InShared1 := &Machine{
@@ -364,8 +364,8 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4", Hostname: "test_get_shared_nodes_4",
UserID: userShared1.ID, NamespaceID: namespaceShared1.ID,
User: *userShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
AuthKeyID: uint(preAuthKey2InShared1.ID), AuthKeyID: uint(preAuthKey2InShared1.ID),

54
docs/README.md Normal file
View File

@@ -0,0 +1,54 @@
# headscale documentation
This page contains the official and community contributed documentation for `headscale`.
If you are having trouble with following the documentation or get unexpected results,
please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Issue.
## Official documentation
### How-to
- [Running headscale on Linux](running-headscale-linux.md)
- [Control headscale remotely](remote-cli.md)
- [Using a Windows client with headscale](windows-client.md)
### References
- [Configuration](../config-example.yaml)
- [Glossary](glossary.md)
- [TLS](tls.md)
## Community documentation
Community documentation is not actively maintained by the headscale authors and is
written by community members. It is _not_ verified by `headscale` developers.
**It might be outdated and it might miss necessary steps**.
- [Running headscale in a container](running-headscale-container.md)
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
- [Running headscale behind a reverse proxy](reverse-proxy.md)
## Misc
### Policy ACLs
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
For instance, instead of referring to users when defining groups you must
use namespaces (which are the equivalent to user/logins in Tailscale.com).
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
When using ACL's the Namespace borders are no longer applied. All machines
whichever the Namespace have the ability to communicate with other hosts as
long as the ACL's permits this exchange.
The [ACLs](acls.md) document should help understand a fictional case of setting
up ACLs in a small company. All concepts presented in this document could be
applied outside of business oriented usage.
### Apple devices
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.

View File

@@ -1,15 +1,4 @@
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment. # ACLs use case example
For instance, instead of referring to users when defining groups you must
use users (which are the equivalent to user/logins in Tailscale.com).
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
When using ACL's the User borders are no longer applied. All machines
whichever the User have the ability to communicate with other hosts as
long as the ACL's permits this exchange.
## ACLs use case example
Let's build an example use case for a small business (It may be the place where Let's build an example use case for a small business (It may be the place where
ACL's are the most useful). ACL's are the most useful).
@@ -40,21 +29,19 @@ servers.
## ACL setup ## ACL setup
Note: Users will be created automatically when users authenticate with the Note: Namespaces will be created automatically when users authenticate with the
Headscale server. Headscale server.
ACLs could be written either on [huJSON](https://github.com/tailscale/hujson) ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
or YAML. Check the [test ACLs](../tests/acls) for further information. or YAML. Check the [test ACLs](../tests/acls) for further information.
When registering the servers we will need to add the flag When registering the servers we will need to add the flag
`--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user that is `--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
registering the server should be allowed to do it. Since anyone can add tags to registering the server should be allowed to do it. Since anyone can add tags to
a server they can register, the check of the tags is done on headscale server a server they can register, the check of the tags is done on headscale server
and only valid tags are applied. A tag is valid if the user that is and only valid tags are applied. A tag is valid if the namespace that is
registering it is allowed to do it. registering it is allowed to do it.
To use ACLs in headscale, you must edit your config.yaml file. In there you will find a `acl_policy_path: ""` parameter. This will need to point to your ACL file. More info on how these policies are written can be found [here](https://tailscale.com/kb/1018/acls/).
Here are the ACL's to implement the same permissions as above: Here are the ACL's to implement the same permissions as above:
```json ```json
@@ -177,8 +164,8 @@ Here are the ACL's to implement the same permissions as above:
"dst": ["tag:dev-app-servers:80,443"] "dst": ["tag:dev-app-servers:80,443"]
}, },
// We still have to allow internal users communications since nothing guarantees that each user have // We still have to allow internal namespaces communications since nothing guarantees that each user have
// their own users. // their own namespaces.
{ "action": "accept", "src": ["boss"], "dst": ["boss:*"] }, { "action": "accept", "src": ["boss"], "dst": ["boss:*"] },
{ "action": "accept", "src": ["dev1"], "dst": ["dev1:*"] }, { "action": "accept", "src": ["dev1"], "dst": ["dev1:*"] },
{ "action": "accept", "src": ["dev2"], "dst": ["dev2:*"] }, { "action": "accept", "src": ["dev2"], "dst": ["dev2:*"] },

View File

@@ -1,90 +0,0 @@
# Setting custom DNS records
!!! warning "Community documentation"
This page is not actively maintained by the headscale authors and is
written by community members. It is _not_ verified by `headscale` developers.
**It might be outdated and it might miss necessary steps**.
## Goal
This documentation has the goal of showing how a user can set custom DNS records with `headscale`s magic dns.
An example use case is to serve apps on the same host via a reverse proxy like NGINX, in this case a Prometheus monitoring stack. This allows to nicely access the service with "http://grafana.myvpn.example.com" instead of the hostname and portnum combination "http://hostname-in-magic-dns.myvpn.example.com:3000".
## Setup
### 1. Change the configuration
1. Change the `config.yaml` to contain the desired records like so:
```yaml
dns_config:
...
extra_records:
- name: "prometheus.myvpn.example.com"
type: "A"
value: "100.64.0.3"
- name: "grafana.myvpn.example.com"
type: "A"
value: "100.64.0.3"
...
```
2. Restart your headscale instance.
Beware of the limitations listed later on!
### 2. Verify that the records are set
You can use a DNS querying tool of your choice on one of your hosts to verify that your newly set records are actually available in MagicDNS, here we used [`dig`](https://man.archlinux.org/man/dig.1.en):
```
$ dig grafana.myvpn.example.com
; <<>> DiG 9.18.10 <<>> grafana.myvpn.example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44054
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;grafana.myvpn.example.com. IN A
;; ANSWER SECTION:
grafana.myvpn.example.com. 593 IN A 100.64.0.3
;; Query time: 0 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
;; WHEN: Sat Dec 31 11:46:55 CET 2022
;; MSG SIZE rcvd: 66
```
### 3. Optional: Setup the reverse proxy
The motivating example here was to be able to access internal monitoring services on the same host without specifying a port:
```
server {
listen 80;
listen [::]:80;
server_name grafana.myvpn.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## Limitations
[Not all types of records are supported](https://github.com/tailscale/tailscale/blob/6edf357b96b28ee1be659a70232c0135b2ffedfd/ipn/ipnlocal/local.go#L2989-L3007), especially no CNAME records.

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