mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-20 03:47:27 +00:00
Compare commits
138 Commits
v0.22.0-nf
...
logs-to-st
Author | SHA1 | Date | |
---|---|---|---|
![]() |
043be13e6d | ||
![]() |
fb203a2e45 | ||
![]() |
6567af7730 | ||
![]() |
23a3adf8d2 | ||
![]() |
665a3cc666 | ||
![]() |
fe75b71620 | ||
![]() |
19dc0ac702 | ||
![]() |
155cc072f7 | ||
![]() |
e2c08db3b5 | ||
![]() |
fcdc7a6f7d | ||
![]() |
88ca2501d1 | ||
![]() |
2675ff4b94 | ||
![]() |
717abe89c1 | ||
![]() |
161243c787 | ||
![]() |
9c425a1c08 | ||
![]() |
db6cf4ac0a | ||
![]() |
35770278f7 | ||
![]() |
36c9b5ce74 | ||
![]() |
0562260fe0 | ||
![]() |
c1218ad3c2 | ||
![]() |
d36336a572 | ||
![]() |
80ea87c032 | ||
![]() |
8c4c4c8633 | ||
![]() |
2289a2acbf | ||
![]() |
c72401a99b | ||
![]() |
725bbd7408 | ||
![]() |
084d1d5d6e | ||
![]() |
f9f6e1557a | ||
![]() |
5bad48a24e | ||
![]() |
bce8427423 | ||
![]() |
f7f472ae07 | ||
![]() |
699655a93f | ||
![]() |
feb15365b5 | ||
![]() |
14e29a7bee | ||
![]() |
b01f1f1867 | ||
![]() |
c027ef0f6c | ||
![]() |
db97a7ab10 | ||
![]() |
252342a0a5 | ||
![]() |
cdf3c47d63 | ||
![]() |
61a2915f17 | ||
![]() |
a16f0c9f60 | ||
![]() |
52ad138c32 | ||
![]() |
d2413d0a2f | ||
![]() |
51dc0d5784 | ||
![]() |
2d365c8c9c | ||
![]() |
f2c1d1b8f9 | ||
![]() |
2d6356fa13 | ||
![]() |
3bfc598ccc | ||
![]() |
3683d3e82f | ||
![]() |
4a7921ead5 | ||
![]() |
22e397e0b6 | ||
![]() |
c7db99d6ca | ||
![]() |
f73354b4f4 | ||
![]() |
4c8f8c6a1c | ||
![]() |
997e93455d | ||
![]() |
9f381256c4 | ||
![]() |
f60c5a1398 | ||
![]() |
5706f84cb0 | ||
![]() |
9478c288f6 | ||
![]() |
6043ec87cf | ||
![]() |
dcf2439c61 | ||
![]() |
ba45d7dbd3 | ||
![]() |
bab4e14828 | ||
![]() |
526e568e1e | ||
![]() |
02ab0df2de | ||
![]() |
7338775de7 | ||
![]() |
00c514608e | ||
![]() |
6c5723a463 | ||
![]() |
57fd5cf310 | ||
![]() |
f113cc7846 | ||
![]() |
ca54fb9f56 | ||
![]() |
735b185e7f | ||
![]() |
1a7ae11697 | ||
![]() |
644be822d5 | ||
![]() |
56b63c6e10 | ||
![]() |
ccedf276ab | ||
![]() |
10320a5f1f | ||
![]() |
ecd62fb785 | ||
![]() |
0d24e878d0 | ||
![]() |
889d5a1b29 | ||
![]() |
1700a747f6 | ||
![]() |
200e3b88cc | ||
![]() |
5bbbe437df | ||
![]() |
6de53e2f8d | ||
![]() |
b23a9153df | ||
![]() |
80772033ee | ||
![]() |
a2b760834f | ||
![]() |
493bcfcf18 | ||
![]() |
df72508089 | ||
![]() |
0f8d8fc2d8 | ||
![]() |
744e5a11b6 | ||
![]() |
3ea1750ea0 | ||
![]() |
a45777d22e | ||
![]() |
56dd734300 | ||
![]() |
d0113732fe | ||
![]() |
6215eb6471 | ||
![]() |
1d2b4bca8a | ||
![]() |
96f9680afd | ||
![]() |
b465592c07 | ||
![]() |
991ff25362 | ||
![]() |
eacd687dbf | ||
![]() |
549f5a164d | ||
![]() |
bb07aec82c | ||
![]() |
a5afe4bd06 | ||
![]() |
a71cc81fe7 | ||
![]() |
679305c3e4 | ||
![]() |
c0680f34f1 | ||
![]() |
64ebe6b0c8 | ||
![]() |
e6b26499f7 | ||
![]() |
977eb1dee3 | ||
![]() |
b2e2b02210 | ||
![]() |
2abff4bb08 | ||
![]() |
54c00645d1 | ||
![]() |
cad5ce0ebd | ||
![]() |
b12a167fa2 | ||
![]() |
667295e15e | ||
![]() |
bea52678e3 | ||
![]() |
307cfc3304 | ||
![]() |
5e74ca9414 | ||
![]() |
9836b097a4 | ||
![]() |
d0b3b1bfc4 | ||
![]() |
6eea96eabc | ||
![]() |
d08fee78c3 | ||
![]() |
bb5f0d456c | ||
![]() |
c186c49e25 | ||
![]() |
4ec6894773 | ||
![]() |
dd9b4b1cb7 | ||
![]() |
a43bb9c958 | ||
![]() |
ba905ff6fc | ||
![]() |
99bd09f688 | ||
![]() |
a6bc792a61 | ||
![]() |
6381d3660a | ||
![]() |
66c5f74d78 | ||
![]() |
1723a6bf40 | ||
![]() |
353f191e4f | ||
![]() |
8d865bb61b | ||
![]() |
c6815c5334 | ||
![]() |
b684ac0668 |
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,19 +6,24 @@ labels: ["bug"]
|
|||||||
assignees: ""
|
assignees: ""
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the bug report in this language. -->
|
<!--
|
||||||
|
Before posting a bug report, discuss the behaviour you are expecting with the Discord community
|
||||||
|
to make sure that it is truly a bug.
|
||||||
|
The issue tracker is not the place to ask for support or how to set up Headscale.
|
||||||
|
|
||||||
**Bug description**
|
Bug reports without the sufficient information will be closed.
|
||||||
|
|
||||||
|
Headscale is a multinational community across the globe. Our language is English.
|
||||||
|
All bug reports needs to be in English.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Bug description
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. Describe the expected bahavior
|
<!-- A clear and concise description of what the bug is. Describe the expected bahavior
|
||||||
and how it is currently different. If you are unsure if it is a bug, consider discussing
|
and how it is currently different. If you are unsure if it is a bug, consider discussing
|
||||||
it on our Discord server first. -->
|
it on our Discord server first. -->
|
||||||
|
|
||||||
**To Reproduce**
|
## Environment
|
||||||
|
|
||||||
<!-- Steps to reproduce the behavior. -->
|
|
||||||
|
|
||||||
**Context info**
|
|
||||||
|
|
||||||
<!-- Please add relevant information about your system. For example:
|
<!-- Please add relevant information about your system. For example:
|
||||||
- Version of headscale used
|
- Version of headscale used
|
||||||
@@ -28,3 +33,20 @@ assignees: ""
|
|||||||
- The relevant config parameters you used
|
- The relevant config parameters you used
|
||||||
- Log output
|
- Log output
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
- OS:
|
||||||
|
- Headscale version:
|
||||||
|
- Tailscale version:
|
||||||
|
|
||||||
|
<!--
|
||||||
|
We do not support running Headscale in a container nor behind a (reverse) proxy.
|
||||||
|
If either of these are true for your environment, ask the community in Discord
|
||||||
|
instead of filing a bug report.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] Headscale is behind a (reverse) proxy
|
||||||
|
- [ ] Headscale runs in a container
|
||||||
|
|
||||||
|
## To Reproduce
|
||||||
|
|
||||||
|
<!-- Steps to reproduce the behavior. -->
|
||||||
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -6,12 +6,21 @@ labels: ["enhancement"]
|
|||||||
assignees: ""
|
assignees: ""
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the feature request in this language. -->
|
<!--
|
||||||
|
We typically have a clear roadmap for what we want to improve and reserve the right
|
||||||
|
to close feature requests that does not fit in the roadmap, or fit with the scope
|
||||||
|
of the project, or we actually want to implement ourselves.
|
||||||
|
|
||||||
**Feature request**
|
Headscale is a multinational community across the globe. Our language is English.
|
||||||
|
All bug reports needs to be in English.
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- A clear and precise description of what new or changed feature you want. -->
|
## Why
|
||||||
|
|
||||||
<!-- Please include the reason, why you would need the feature. E.g. what problem
|
<!-- Include the reason, why you would need the feature. E.g. what problem
|
||||||
does it solve? Or which workflow is currently frustrating and will be improved by
|
does it solve? Or which workflow is currently frustrating and will be improved by
|
||||||
this? -->
|
this? -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- A clear and precise description of what new or changed feature you want. -->
|
||||||
|
30
.github/ISSUE_TEMPLATE/other_issue.md
vendored
30
.github/ISSUE_TEMPLATE/other_issue.md
vendored
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Other issue"
|
|
||||||
about: "Report a different issue"
|
|
||||||
title: ""
|
|
||||||
labels: ["bug"]
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the issue in this language. -->
|
|
||||||
|
|
||||||
<!-- If you have a question, please consider using our Discord for asking questions -->
|
|
||||||
|
|
||||||
**Issue description**
|
|
||||||
|
|
||||||
<!-- Please add your issue description. -->
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
|
|
||||||
<!-- Steps to reproduce the behavior. -->
|
|
||||||
|
|
||||||
**Context info**
|
|
||||||
|
|
||||||
<!-- Please add relevant information about your system. For example:
|
|
||||||
- Version of headscale used
|
|
||||||
- Version of tailscale client
|
|
||||||
- OS (e.g. Linux, Mac, Cygwin, WSL, etc.) and version
|
|
||||||
- Kernel version
|
|
||||||
- The relevant config parameters you used
|
|
||||||
- Log output
|
|
||||||
-->
|
|
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -1,3 +1,15 @@
|
|||||||
|
<!--
|
||||||
|
Headscale is "Open Source, acknowledged contribution", this means that any
|
||||||
|
contribution will have to be discussed with the Maintainers before being submitted.
|
||||||
|
|
||||||
|
This model has been chosen to reduce the risk of burnout by limiting the
|
||||||
|
maintenance overhead of reviewing and validating third-party code.
|
||||||
|
|
||||||
|
Headscale is open to code contributions for bug fixes without discussion.
|
||||||
|
|
||||||
|
If you find mistakes in the documentation, please submit a fix to the documentation.
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Please tick if the following things apply. You… -->
|
<!-- Please tick if the following things apply. You… -->
|
||||||
|
|
||||||
- [ ] read the [CONTRIBUTING guidelines](README.md#contributing)
|
- [ ] read the [CONTRIBUTING guidelines](README.md#contributing)
|
||||||
|
24
.github/renovate.json
vendored
24
.github/renovate.json
vendored
@@ -6,31 +6,27 @@
|
|||||||
"onboarding": false,
|
"onboarding": false,
|
||||||
"extends": ["config:base", ":rebaseStalePrs"],
|
"extends": ["config:base", ":rebaseStalePrs"],
|
||||||
"ignorePresets": [":prHourlyLimit2"],
|
"ignorePresets": [":prHourlyLimit2"],
|
||||||
"enabledManagers": ["dockerfile", "gomod", "github-actions","regex" ],
|
"enabledManagers": ["dockerfile", "gomod", "github-actions", "regex"],
|
||||||
"includeForks": true,
|
"includeForks": true,
|
||||||
"repositories": ["juanfont/headscale"],
|
"repositories": ["juanfont/headscale"],
|
||||||
"platform": "github",
|
"platform": "github",
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchDatasources": ["go"],
|
"matchDatasources": ["go"],
|
||||||
"groupName": "Go modules",
|
"groupName": "Go modules",
|
||||||
"groupSlug": "gomod",
|
"groupSlug": "gomod",
|
||||||
"separateMajorMinor": false
|
"separateMajorMinor": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchDatasources": ["docker"],
|
"matchDatasources": ["docker"],
|
||||||
"groupName": "Dockerfiles",
|
"groupName": "Dockerfiles",
|
||||||
"groupSlug": "dockerfiles"
|
"groupSlug": "dockerfiles"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"regexManagers": [
|
"regexManagers": [
|
||||||
{
|
{
|
||||||
"fileMatch": [
|
"fileMatch": [".github/workflows/.*.yml$"],
|
||||||
".github/workflows/.*.yml$"
|
"matchStrings": ["\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"],
|
||||||
],
|
|
||||||
"matchStrings": [
|
|
||||||
"\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"
|
|
||||||
],
|
|
||||||
"datasourceTemplate": "golang-version",
|
"datasourceTemplate": "golang-version",
|
||||||
"depNameTemplate": "actions/go-version"
|
"depNameTemplate": "actions/go-version"
|
||||||
}
|
}
|
||||||
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -33,7 +33,9 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
|
45
.github/workflows/docs.yml
vendored
Normal file
45
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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
|
138
.github/workflows/release-docker.yml
vendored
Normal file
138
.github/workflows/release-docker.yml
vendored
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
---
|
||||||
|
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
|
134
.github/workflows/release.yml
vendored
134
.github/workflows/release.yml
vendored
@@ -16,138 +16,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Run goreleaser
|
- name: Run goreleaser
|
||||||
run: nix develop --command -- goreleaser release --rm-dist
|
run: nix develop --command -- goreleaser release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
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
|
|
||||||
|
35
.github/workflows/test-integration-cli.yml
vendored
35
.github/workflows/test-integration-cli.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: Integration Test CLI
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
integration-test-cli:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Set Swap Space
|
|
||||||
uses: pierotofy/set-swap-space@master
|
|
||||||
with:
|
|
||||||
swap-size-gb: 10
|
|
||||||
|
|
||||||
- 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@v16
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run CLI integration tests
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: nix develop --command -- make test_integration_cli
|
|
35
.github/workflows/test-integration-derp.yml
vendored
35
.github/workflows/test-integration-derp.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: Integration Test DERP
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
integration-test-derp:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Set Swap Space
|
|
||||||
uses: pierotofy/set-swap-space@master
|
|
||||||
with:
|
|
||||||
swap-size-gb: 10
|
|
||||||
|
|
||||||
- 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@v16
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run Embedded DERP server integration tests
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: nix develop --command -- make test_integration_derp
|
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
65
.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-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"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
65
.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-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"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
65
.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-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"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
65
.github/workflows/test-integration-v2-TestApiKeyCommand.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestApiKeyCommand.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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 - TestApiKeyCommand
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestApiKeyCommand$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
65
.github/workflows/test-integration-v2-TestDERPServerScenario.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestDERPServerScenario.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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 - TestDERPServerScenario
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestDERPServerScenario$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
65
.github/workflows/test-integration-v2-TestNodeCommand.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestNodeCommand.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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 - TestNodeCommand
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestNodeCommand$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
65
.github/workflows/test-integration-v2-TestNodeExpireCommand.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestNodeExpireCommand.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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 - TestNodeExpireCommand
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestNodeExpireCommand$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
65
.github/workflows/test-integration-v2-TestNodeMoveCommand.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestNodeMoveCommand.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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 - TestNodeMoveCommand
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestNodeMoveCommand$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
65
.github/workflows/test-integration-v2-TestNodeRenameCommand.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestNodeRenameCommand.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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 - TestNodeRenameCommand
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestNodeRenameCommand$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
65
.github/workflows/test-integration-v2-TestNodeTagCommand.yaml
vendored
Normal file
65
.github/workflows/test-integration-v2-TestNodeTagCommand.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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 - TestNodeTagCommand
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- 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 -- 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 run gotest.tools/gotestsum@latest -- ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestNodeTagCommand$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -29,9 +34,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -55,3 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -26,7 +26,9 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v16
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
ignored/
|
||||||
|
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
@@ -12,8 +14,9 @@
|
|||||||
*.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
|
||||||
@@ -34,3 +37,9 @@ result
|
|||||||
.direnv/
|
.direnv/
|
||||||
|
|
||||||
integration_test/etc/config.dump.yaml
|
integration_test/etc/config.dump.yaml
|
||||||
|
|
||||||
|
# MkDocs
|
||||||
|
.cache
|
||||||
|
/site
|
||||||
|
|
||||||
|
__debug_bin
|
||||||
|
115
.goreleaser.yml
115
.goreleaser.yml
@@ -1,21 +1,28 @@
|
|||||||
---
|
---
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod tidy -compat=1.19
|
- go mod tidy -compat=1.20
|
||||||
|
- go mod vendor
|
||||||
|
|
||||||
release:
|
release:
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: darwin-amd64
|
- id: headscale
|
||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos:
|
targets:
|
||||||
- darwin
|
- darwin_amd64
|
||||||
goarch:
|
- darwin_arm64
|
||||||
- amd64
|
- freebsd_amd64
|
||||||
|
- linux_386
|
||||||
|
- linux_amd64
|
||||||
|
- linux_arm64
|
||||||
|
- linux_arm_5
|
||||||
|
- linux_arm_6
|
||||||
|
- linux_arm_7
|
||||||
flags:
|
flags:
|
||||||
- -mod=readonly
|
- -mod=readonly
|
||||||
ldflags:
|
ldflags:
|
||||||
@@ -23,60 +30,56 @@ 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
|
||||||
builds:
|
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
- 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: /usr/lib/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:
|
||||||
|
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,10 +1,50 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## 0.22.0 (2023-XX-XX)
|
## 0.23.0 (2023-XX-XX)
|
||||||
|
|
||||||
|
### BREAKING
|
||||||
|
|
||||||
|
- Code reorganisation, a lot of code has moved, please review the following PRs accordingly [#1444](https://github.com/juanfont/headscale/pull/1444)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
|
- Make the OIDC callback page better [#1484](https://github.com/juanfont/headscale/pull/1484)
|
||||||
|
- SSH is no longer experimental [#1487](https://github.com/juanfont/headscale/pull/1487)
|
||||||
|
|
||||||
|
## 0.22.3 (2023-05-12)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Added missing ca-certificates in Docker image [#1463](https://github.com/juanfont/headscale/pull/1463)
|
||||||
|
|
||||||
|
## 0.22.2 (2023-05-10)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Add environment flags to enable pprof (profiling) [#1382](https://github.com/juanfont/headscale/pull/1382)
|
||||||
|
- Profiles are continously generated in our integration tests.
|
||||||
|
- Fix systemd service file location in `.deb` packages [#1391](https://github.com/juanfont/headscale/pull/1391)
|
||||||
|
- Improvements on Noise implementation [#1379](https://github.com/juanfont/headscale/pull/1379)
|
||||||
|
- Replace node filter logic, ensuring nodes with access can see eachother [#1381](https://github.com/juanfont/headscale/pull/1381)
|
||||||
|
- Disable (or delete) both exit routes at the same time [#1428](https://github.com/juanfont/headscale/pull/1428)
|
||||||
|
- Ditch distroless for Docker image, create default socket dir in `/var/run/headscale` [#1450](https://github.com/juanfont/headscale/pull/1450)
|
||||||
|
|
||||||
|
## 0.22.1 (2023-04-20)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Fix issue where systemd could not bind to port 80 [#1365](https://github.com/juanfont/headscale/pull/1365)
|
||||||
|
|
||||||
|
## 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 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)
|
## 0.21.0 (2023-03-20)
|
||||||
|
|
||||||
|
11
Dockerfile
11
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Builder image
|
# Builder image
|
||||||
FROM docker.io/golang:1.19-bullseye AS build
|
FROM docker.io/golang:1.20-bullseye AS build
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ENV GOPATH /go
|
ENV GOPATH /go
|
||||||
WORKDIR /go/src/headscale
|
WORKDIR /go/src/headscale
|
||||||
@@ -14,10 +14,17 @@ RUN strip /go/bin/headscale
|
|||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
# Production image
|
# Production image
|
||||||
FROM gcr.io/distroless/base-debian11
|
FROM docker.io/debian:bullseye-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& apt-get clean
|
||||||
|
|
||||||
COPY --from=build /go/bin/headscale /bin/headscale
|
COPY --from=build /go/bin/headscale /bin/headscale
|
||||||
ENV TZ UTC
|
ENV TZ UTC
|
||||||
|
|
||||||
|
RUN mkdir -p /var/run/headscale
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
CMD ["headscale"]
|
CMD ["headscale"]
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
# Builder image
|
# Builder image
|
||||||
FROM docker.io/golang:1.19-bullseye AS build
|
FROM docker.io/golang:1.20-bullseye AS build
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ENV GOPATH /go
|
ENV GOPATH /go
|
||||||
WORKDIR /go/src/headscale
|
WORKDIR /go/src/headscale
|
||||||
@@ -13,11 +13,13 @@ 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.19.0-bullseye
|
FROM docker.io/golang:1.20.0-bullseye
|
||||||
|
|
||||||
COPY --from=build /go/bin/headscale /bin/headscale
|
COPY --from=build /go/bin/headscale /bin/headscale
|
||||||
ENV TZ UTC
|
ENV TZ UTC
|
||||||
|
|
||||||
|
RUN mkdir -p /var/run/headscale
|
||||||
|
|
||||||
# Need to reset the entrypoint or everything will run as a busybox script
|
# Need to reset the entrypoint or everything will run as a busybox script
|
||||||
ENTRYPOINT []
|
ENTRYPOINT []
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
@@ -1,19 +1,16 @@
|
|||||||
FROM ubuntu:latest
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
ARG TAILSCALE_VERSION=*
|
ARG TAILSCALE_VERSION=*
|
||||||
ARG TAILSCALE_CHANNEL=stable
|
ARG TAILSCALE_CHANNEL=stable
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y gnupg curl ssh \
|
&& apt-get install -y gnupg curl ssh dnsutils ca-certificates \
|
||||||
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \
|
&& adduser --shell=/bin/bash ssh-it-user
|
||||||
|
|
||||||
|
# Tailscale is deliberately split into a second stage so we can cash utils as a seperate layer.
|
||||||
|
RUN curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \
|
||||||
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
|
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
|
&& apt-get install -y tailscale=${TAILSCALE_VERSION} \
|
||||||
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN adduser --shell=/bin/bash ssh-it-user
|
|
||||||
|
|
||||||
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
|
|
||||||
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
|
|
||||||
|
|
||||||
RUN update-ca-certificates
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
FROM golang:latest
|
FROM golang:latest
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y ca-certificates dnsutils git iptables ssh \
|
&& apt-get install -y dnsutils git iptables ssh ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN useradd --shell=/bin/bash --create-home ssh-it-user
|
RUN useradd --shell=/bin/bash --create-home ssh-it-user
|
||||||
@@ -10,15 +10,8 @@ RUN git clone https://github.com/tailscale/tailscale.git
|
|||||||
|
|
||||||
WORKDIR /go/tailscale
|
WORKDIR /go/tailscale
|
||||||
|
|
||||||
RUN git checkout main
|
RUN git checkout main \
|
||||||
|
&& sh build_dist.sh tailscale.com/cmd/tailscale \
|
||||||
RUN sh build_dist.sh tailscale.com/cmd/tailscale
|
&& sh build_dist.sh tailscale.com/cmd/tailscaled \
|
||||||
RUN sh build_dist.sh tailscale.com/cmd/tailscaled
|
&& cp tailscale /usr/local/bin/ \
|
||||||
|
&& cp tailscaled /usr/local/bin/
|
||||||
RUN cp tailscale /usr/local/bin/
|
|
||||||
RUN cp tailscaled /usr/local/bin/
|
|
||||||
|
|
||||||
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
|
|
||||||
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
|
|
||||||
|
|
||||||
RUN update-ca-certificates
|
|
||||||
|
43
Makefile
43
Makefile
@@ -24,31 +24,9 @@ build:
|
|||||||
dev: lint test build
|
dev: lint test build
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test $(TAGS) -short -coverprofile=coverage.out ./...
|
gotestsum -- $(TAGS) -short -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
test_integration: test_integration_cli test_integration_derp test_integration_v2_general
|
test_integration:
|
||||||
|
|
||||||
test_integration_cli:
|
|
||||||
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
|
|
||||||
docker network create headscale-test || true
|
|
||||||
docker run -t --rm \
|
|
||||||
--network headscale-test \
|
|
||||||
-v ~/.cache/hs-integration-go:/go \
|
|
||||||
-v $$PWD:$$PWD -w $$PWD \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
|
||||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
|
|
||||||
|
|
||||||
test_integration_derp:
|
|
||||||
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
|
|
||||||
docker network create headscale-test || true
|
|
||||||
docker run -t --rm \
|
|
||||||
--network headscale-test \
|
|
||||||
-v ~/.cache/hs-integration-go:/go \
|
|
||||||
-v $$PWD:$$PWD -w $$PWD \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
|
||||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
|
|
||||||
|
|
||||||
test_integration_v2_general:
|
|
||||||
docker run \
|
docker run \
|
||||||
-t --rm \
|
-t --rm \
|
||||||
-v ~/.cache/hs-integration-go:/go \
|
-v ~/.cache/hs-integration-go:/go \
|
||||||
@@ -56,13 +34,7 @@ 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 test $(TAGS) -failfast ./... -timeout 120m -parallel 8
|
go run gotest.tools/gotestsum@latest -- $(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
|
||||||
@@ -80,11 +52,4 @@ compress: build
|
|||||||
|
|
||||||
generate:
|
generate:
|
||||||
rm -rf gen
|
rm -rf gen
|
||||||
go run github.com/bufbuild/buf/cmd/buf generate proto
|
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
|
|
||||||
|
268
README.md
268
README.md
@@ -32,22 +32,18 @@ organisation.
|
|||||||
|
|
||||||
## Design goal
|
## Design goal
|
||||||
|
|
||||||
`headscale` aims to implement a self-hosted, open source alternative to the Tailscale
|
Headscale aims to implement a self-hosted, open source alternative to the Tailscale
|
||||||
control server. `headscale` has a narrower scope and an instance of `headscale`
|
control server.
|
||||||
implements a _single_ Tailnet, which is typically what a single organisation, or
|
Headscale's goal is to provide self-hosters and hobbyists with an open-source
|
||||||
home/personal setup would use.
|
server they can use for their projects and labs.
|
||||||
|
It implements a narrow scope, a single Tailnet, suitable for a personal use, or a small
|
||||||
|
open-source organisation.
|
||||||
|
|
||||||
`headscale` uses terms that maps to Tailscale's control server, consult the
|
## Supporting Headscale
|
||||||
[glossary](./docs/glossary.md) for explainations.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
If you like `headscale` and find it useful, there is a sponsorship and donation
|
If you like `headscale` and find it useful, there is a sponsorship and donation
|
||||||
buttons available in the repo.
|
buttons available in the repo.
|
||||||
|
|
||||||
If you would like to sponsor features, bugs or prioritisation, reach out to
|
|
||||||
one of the maintainers.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Full "base" support of Tailscale's features
|
- Full "base" support of Tailscale's features
|
||||||
@@ -79,16 +75,10 @@ one of the maintainers.
|
|||||||
|
|
||||||
## Running headscale
|
## Running headscale
|
||||||
|
|
||||||
Please have a look at the documentation under [`docs/`](docs/).
|
**Please note that we do not support nor encourage the use of reverse proxies
|
||||||
|
and container to run Headscale.**
|
||||||
|
|
||||||
## Graphical Control Panels
|
Please have a look at the [`documentation`](https://headscale.net/).
|
||||||
|
|
||||||
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
|
## Talks
|
||||||
|
|
||||||
@@ -97,11 +87,23 @@ These are community projects not directly affiliated with the Headscale project.
|
|||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
1. We have nothing to do with Tailscale, or Tailscale Inc.
|
1. This project is not associated with Tailscale Inc.
|
||||||
2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel.
|
2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
Headscale is "Open Source, acknowledged contribution", this means that any
|
||||||
|
contribution will have to be discussed with the Maintainers before being submitted.
|
||||||
|
|
||||||
|
This model has been chosen to reduce the risk of burnout by limiting the
|
||||||
|
maintenance overhead of reviewing and validating third-party code.
|
||||||
|
|
||||||
|
Headscale is open to code contributions for bug fixes without discussion.
|
||||||
|
|
||||||
|
If you find mistakes in the documentation, please submit a fix to the documentation.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
|
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
|
||||||
and [Buf](https://buf.build)(Protobuf generator).
|
and [Buf](https://buf.build)(Protobuf generator).
|
||||||
|
|
||||||
@@ -109,8 +111,6 @@ We recommend using [Nix](https://nixos.org/) to setup a development environment.
|
|||||||
be done with `nix develop`, which will install the tools and give you a shell.
|
be done with `nix develop`, which will install the tools and give you a shell.
|
||||||
This guarantees that you will have the same dev env as `headscale` maintainers.
|
This guarantees that you will have the same dev env as `headscale` maintainers.
|
||||||
|
|
||||||
PRs and suggestions are welcome.
|
|
||||||
|
|
||||||
### Code style
|
### Code style
|
||||||
|
|
||||||
To ensure we have some consistency with a growing number of contributions,
|
To ensure we have some consistency with a growing number of contributions,
|
||||||
@@ -188,13 +188,6 @@ 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/>
|
||||||
@@ -216,8 +209,6 @@ 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/>
|
||||||
@@ -225,6 +216,8 @@ 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">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/evenh>
|
<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/>
|
<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/>
|
||||||
@@ -240,7 +233,7 @@ 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/arch4ngel>
|
<a href=https://github.com/ImpostorKeanu>
|
||||||
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
|
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>Justin Angel</b></sub>
|
<sub style="font-size:14px"><b>Justin Angel</b></sub>
|
||||||
@@ -253,6 +246,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
|
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</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/ohdearaugustin>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/unreality>
|
<a href=https://github.com/unreality>
|
||||||
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
|
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
|
||||||
@@ -262,13 +262,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/ohdearaugustin>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/mpldr>
|
<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/>
|
<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/>
|
||||||
@@ -276,6 +269,20 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Moritz Poldrack</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">
|
||||||
|
<a href=https://github.com/Orhideous>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/2265184?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andriy Kushnir/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Andriy Kushnir</b></sub>
|
||||||
|
</a>
|
||||||
|
</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/GrigoriyMikhalkin>
|
<a href=https://github.com/GrigoriyMikhalkin>
|
||||||
<img src=https://avatars.githubusercontent.com/u/3637857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=GrigoriyMikhalkin/>
|
<img src=https://avatars.githubusercontent.com/u/3637857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=GrigoriyMikhalkin/>
|
||||||
@@ -297,6 +304,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Mike Lloyd</b></sub>
|
<sub style="font-size:14px"><b>Mike Lloyd</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/iSchluff>
|
<a href=https://github.com/iSchluff>
|
||||||
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
|
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
|
||||||
@@ -304,8 +313,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/>
|
||||||
@@ -341,6 +348,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Igor Perepilitsyn</b></sub>
|
<sub style="font-size:14px"><b>Igor Perepilitsyn</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/Aluxima>
|
<a href=https://github.com/Aluxima>
|
||||||
<img src=https://avatars.githubusercontent.com/u/16262531?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Laurent Marchaud/>
|
<img src=https://avatars.githubusercontent.com/u/16262531?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Laurent Marchaud/>
|
||||||
@@ -348,8 +357,13 @@ 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>
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<tr>
|
<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/>
|
||||||
@@ -364,13 +378,6 @@ 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>
|
||||||
<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/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,6 +392,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>bravechamp</b></sub>
|
<sub style="font-size:14px"><b>bravechamp</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/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/>
|
||||||
@@ -392,8 +401,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Deon Thomas</b></sub>
|
<sub style="font-size:14px"><b>Deon Thomas</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/madjam002>
|
<a href=https://github.com/madjam002>
|
||||||
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
|
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
|
||||||
@@ -401,6 +408,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
|
<sub style="font-size:14px"><b>Jamie Greeff</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/jonathanspw>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/8390543?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan Wright/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Jonathan Wright</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/ChibangLW>
|
<a href=https://github.com/ChibangLW>
|
||||||
<img src=https://avatars.githubusercontent.com/u/22293464?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ChibangLW/>
|
<img src=https://avatars.githubusercontent.com/u/22293464?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ChibangLW/>
|
||||||
@@ -408,6 +422,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>ChibangLW</b></sub>
|
<sub style="font-size:14px"><b>ChibangLW</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/majabojarska>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/33836570?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Maja Bojarska/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Maja Bojarska</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/mevansam>
|
<a href=https://github.com/mevansam>
|
||||||
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
|
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
|
||||||
@@ -415,6 +436,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 +459,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>
|
||||||
</tr>
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<tr>
|
<a href=https://github.com/loprima-l>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/69201633?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=loprima-l/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>loprima-l</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,6 +480,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Snack</b></sub>
|
<sub style="font-size:14px"><b>Snack</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/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/>
|
||||||
@@ -480,8 +510,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Josh Taylor</b></sub>
|
<sub style="font-size:14px"><b>Josh Taylor</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/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,6 +524,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Motiejus Jakštys</b></sub>
|
<sub style="font-size:14px"><b>Motiejus Jakštys</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/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,15 +554,6 @@ 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,6 +561,15 @@ 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>
|
||||||
|
</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/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/>
|
||||||
@@ -568,8 +598,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Anoop Sundaresh</b></sub>
|
<sub style="font-size:14px"><b>Anoop Sundaresh</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/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/>
|
||||||
@@ -577,6 +605,15 @@ 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>
|
||||||
|
</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/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/>
|
||||||
@@ -612,8 +649,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
|
<sub style="font-size:14px"><b>Bryan Stenson</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/yangchuansheng>
|
<a href=https://github.com/yangchuansheng>
|
||||||
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
|
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
|
||||||
@@ -621,11 +656,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b> Carson Yang</b></sub>
|
<sub style="font-size:14px"><b> Carson Yang</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/kundel>
|
<a href=https://github.com/kundel>
|
||||||
<img src=https://avatars.githubusercontent.com/u/10158899?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kundel/>
|
<img src=https://avatars.githubusercontent.com/u/10158899?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Darrell Kundel/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>kundel</b></sub>
|
<sub style="font-size:14px"><b>Darrell Kundel</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">
|
||||||
@@ -649,6 +686,13 @@ 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>
|
||||||
|
<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/>
|
||||||
@@ -693,6 +737,22 @@ 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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<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">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/foxtrot>
|
<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/>
|
<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/>
|
||||||
@@ -700,8 +760,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Marc</b></sub>
|
<sub style="font-size:14px"><b>Marc</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/magf>
|
<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/>
|
<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/>
|
||||||
@@ -709,6 +767,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Maxim Gajdaj</b></sub>
|
<sub style="font-size:14px"><b>Maxim Gajdaj</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/mhameed>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/447017?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mesar Hameed/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Mesar Hameed</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/mikejsavage>
|
<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/>
|
<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/>
|
||||||
@@ -716,6 +781,15 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Michael Savage</b></sub>
|
<sub style="font-size:14px"><b>Michael Savage</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/pkrivanec>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/25530641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Philipp Krivanec/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Philipp Krivanec</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/>
|
||||||
@@ -744,8 +818,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>rcursaru</b></sub>
|
<sub style="font-size:14px"><b>rcursaru</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/renovate-bot>
|
<a href=https://github.com/renovate-bot>
|
||||||
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mend Renovate/>
|
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mend Renovate/>
|
||||||
@@ -760,11 +832,13 @@ 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/linsomniac>
|
<a href=https://github.com/muzy>
|
||||||
<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/>
|
<img src=https://avatars.githubusercontent.com/u/321723?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Sebastian Muszytowski/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>Sean Reifschneider</b></sub>
|
<sub style="font-size:14px"><b>Sebastian Muszytowski</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">
|
||||||
@@ -774,6 +848,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
|
<sub style="font-size:14px"><b>Shaanan Cohney</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/6ixfalls>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/23470032?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Six/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Six</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/stefanvanburen>
|
<a href=https://github.com/stefanvanburen>
|
||||||
<img src=https://avatars.githubusercontent.com/u/622527?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan VanBuren/>
|
<img src=https://avatars.githubusercontent.com/u/622527?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan VanBuren/>
|
||||||
@@ -788,8 +869,6 @@ 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/>
|
||||||
@@ -797,6 +876,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Tanner</b></sub>
|
<sub style="font-size:14px"><b>Tanner</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/Teteros>
|
<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/>
|
<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/>
|
||||||
@@ -832,8 +913,6 @@ 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/>
|
||||||
@@ -841,6 +920,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Yang Bin</b></sub>
|
<sub style="font-size:14px"><b>Yang Bin</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/gozssky>
|
<a href=https://github.com/gozssky>
|
||||||
<img src=https://avatars.githubusercontent.com/u/17199941?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yujie Xia/>
|
<img src=https://avatars.githubusercontent.com/u/17199941?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yujie Xia/>
|
||||||
@@ -876,8 +957,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>
|
||||||
</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/caelansar>
|
<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/>
|
<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/>
|
||||||
@@ -885,6 +964,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>caelansar</b></sub>
|
<sub style="font-size:14px"><b>caelansar</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/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/>
|
||||||
@@ -920,8 +1001,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>jimyag</b></sub>
|
<sub style="font-size:14px"><b>jimyag</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/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/>
|
||||||
@@ -929,6 +1008,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>suhelen</b></sub>
|
<sub style="font-size:14px"><b>suhelen</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/lion24>
|
<a href=https://github.com/lion24>
|
||||||
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sharkonet/>
|
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sharkonet/>
|
||||||
@@ -951,10 +1032,17 @@ 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/pernila>
|
<a href=https://github.com/nicholas-yap>
|
||||||
<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/>
|
<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 />
|
<br />
|
||||||
<sub style="font-size:14px"><b>pernila</b></sub>
|
<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">
|
||||||
|
<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/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Tommi Pernila</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">
|
||||||
|
780
acls.go
780
acls.go
@@ -1,780 +0,0 @@
|
|||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/tailscale/hujson"
|
|
||||||
"go4.org/netipx"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"tailscale.com/envknob"
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
errEmptyPolicy = Error("empty policy")
|
|
||||||
errInvalidAction = Error("invalid action")
|
|
||||||
errInvalidGroup = Error("invalid group")
|
|
||||||
errInvalidTag = Error("invalid tag")
|
|
||||||
errInvalidPortFormat = Error("invalid port format")
|
|
||||||
errWildcardIsNeeded = Error("wildcard as port is required for the protocol")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Base8 = 8
|
|
||||||
Base10 = 10
|
|
||||||
BitSize16 = 16
|
|
||||||
BitSize32 = 32
|
|
||||||
BitSize64 = 64
|
|
||||||
portRangeBegin = 0
|
|
||||||
portRangeEnd = 65535
|
|
||||||
expectedTokenItems = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// For some reason golang.org/x/net/internal/iana is an internal package.
|
|
||||||
const (
|
|
||||||
protocolICMP = 1 // Internet Control Message
|
|
||||||
protocolIGMP = 2 // Internet Group Management
|
|
||||||
protocolIPv4 = 4 // IPv4 encapsulation
|
|
||||||
protocolTCP = 6 // Transmission Control
|
|
||||||
protocolEGP = 8 // Exterior Gateway Protocol
|
|
||||||
protocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
|
|
||||||
protocolUDP = 17 // User Datagram
|
|
||||||
protocolGRE = 47 // Generic Routing Encapsulation
|
|
||||||
protocolESP = 50 // Encap Security Payload
|
|
||||||
protocolAH = 51 // Authentication Header
|
|
||||||
protocolIPv6ICMP = 58 // ICMP for IPv6
|
|
||||||
protocolSCTP = 132 // Stream Control Transmission Protocol
|
|
||||||
ProtocolFC = 133 // Fibre Channel
|
|
||||||
)
|
|
||||||
|
|
||||||
var featureEnableSSH = envknob.RegisterBool("HEADSCALE_EXPERIMENTAL_FEATURE_SSH")
|
|
||||||
|
|
||||||
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
|
|
||||||
func (h *Headscale) LoadACLPolicy(path string) error {
|
|
||||||
log.Debug().
|
|
||||||
Str("func", "LoadACLPolicy").
|
|
||||||
Str("path", path).
|
|
||||||
Msg("Loading ACL policy from path")
|
|
||||||
|
|
||||||
policyFile, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer policyFile.Close()
|
|
||||||
|
|
||||||
var policy ACLPolicy
|
|
||||||
policyBytes, err := io.ReadAll(policyFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch filepath.Ext(path) {
|
|
||||||
case ".yml", ".yaml":
|
|
||||||
log.Debug().
|
|
||||||
Str("path", path).
|
|
||||||
Bytes("file", policyBytes).
|
|
||||||
Msg("Loading ACLs from YAML")
|
|
||||||
|
|
||||||
err := yaml.Unmarshal(policyBytes, &policy)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace().
|
|
||||||
Interface("policy", policy).
|
|
||||||
Msg("Loaded policy from YAML")
|
|
||||||
|
|
||||||
default:
|
|
||||||
ast, err := hujson.Parse(policyBytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ast.Standardize()
|
|
||||||
policyBytes = ast.Pack()
|
|
||||||
err = json.Unmarshal(policyBytes, &policy)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.IsZero() {
|
|
||||||
return errEmptyPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
h.aclPolicy = &policy
|
|
||||||
|
|
||||||
return h.UpdateACLRules()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Headscale) UpdateACLRules() error {
|
|
||||||
machines, err := h.ListMachines()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.aclPolicy == nil {
|
|
||||||
return errEmptyPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
rules, err := generateACLRules(machines, *h.aclPolicy, h.cfg.OIDC.StripEmaildomain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
|
||||||
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() {
|
|
||||||
sshRules, err := h.generateSSHRules()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Trace().Interface("SSH", sshRules).Msg("SSH rules generated")
|
|
||||||
if h.sshPolicy == nil {
|
|
||||||
h.sshPolicy = &tailcfg.SSHPolicy{}
|
|
||||||
}
|
|
||||||
h.sshPolicy.Rules = sshRules
|
|
||||||
} else if h.aclPolicy != nil && len(h.aclPolicy.SSHs) > 0 {
|
|
||||||
log.Info().Msg("SSH ACLs has been defined, but HEADSCALE_EXPERIMENTAL_FEATURE_SSH is not enabled, this is a unstable feature, check docs before activating")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateACLPeerCacheMap takes a list of Tailscale filter rules and generates a map
|
|
||||||
// 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{}
|
|
||||||
|
|
||||||
for index, acl := range aclPolicy.ACLs {
|
|
||||||
if acl.Action != "accept" {
|
|
||||||
return nil, errInvalidAction
|
|
||||||
}
|
|
||||||
|
|
||||||
srcIPs := []string{}
|
|
||||||
for innerIndex, src := range acl.Sources {
|
|
||||||
srcs, err := generateACLPolicySrc(machines, aclPolicy, src, stripEmaildomain)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
srcIPs = append(srcIPs, srcs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
protocols, needsWildcard, err := parseProtocol(acl.Protocol)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Msgf("Error parsing ACL %d. protocol unknown %s", index, acl.Protocol)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
destPorts := []tailcfg.NetPortRange{}
|
|
||||||
for innerIndex, dest := range acl.Destinations {
|
|
||||||
dests, err := generateACLPolicyDest(
|
|
||||||
machines,
|
|
||||||
aclPolicy,
|
|
||||||
dest,
|
|
||||||
needsWildcard,
|
|
||||||
stripEmaildomain,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
destPorts = append(destPorts, dests...)
|
|
||||||
}
|
|
||||||
|
|
||||||
rules = append(rules, tailcfg.FilterRule{
|
|
||||||
SrcIPs: srcIPs,
|
|
||||||
DstPorts: destPorts,
|
|
||||||
IPProto: protocols,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return rules, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) {
|
|
||||||
rules := []*tailcfg.SSHRule{}
|
|
||||||
|
|
||||||
if h.aclPolicy == nil {
|
|
||||||
return nil, errEmptyPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
machines, err := h.ListMachines()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptAction := tailcfg.SSHAction{
|
|
||||||
Message: "",
|
|
||||||
Reject: false,
|
|
||||||
Accept: true,
|
|
||||||
SessionDuration: 0,
|
|
||||||
AllowAgentForwarding: false,
|
|
||||||
HoldAndDelegate: "",
|
|
||||||
AllowLocalPortForwarding: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
rejectAction := tailcfg.SSHAction{
|
|
||||||
Message: "",
|
|
||||||
Reject: true,
|
|
||||||
Accept: false,
|
|
||||||
SessionDuration: 0,
|
|
||||||
AllowAgentForwarding: false,
|
|
||||||
HoldAndDelegate: "",
|
|
||||||
AllowLocalPortForwarding: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
for index, sshACL := range h.aclPolicy.SSHs {
|
|
||||||
action := rejectAction
|
|
||||||
switch sshACL.Action {
|
|
||||||
case "accept":
|
|
||||||
action = acceptAction
|
|
||||||
case "check":
|
|
||||||
checkAction, err := sshCheckAction(sshACL.CheckPeriod)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Msgf("Error parsing SSH %d, check action with unparsable duration '%s'", index, sshACL.CheckPeriod)
|
|
||||||
} else {
|
|
||||||
action = *checkAction
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Error().
|
|
||||||
Msgf("Error parsing SSH %d, unknown action '%s'", index, sshACL.Action)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
|
|
||||||
for innerIndex, rawSrc := range sshACL.Sources {
|
|
||||||
expandedSrcs, err := expandAlias(
|
|
||||||
machines,
|
|
||||||
*h.aclPolicy,
|
|
||||||
rawSrc,
|
|
||||||
h.cfg.OIDC.StripEmaildomain,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Msgf("Error parsing SSH %d, Source %d", index, innerIndex)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, expandedSrc := range expandedSrcs {
|
|
||||||
principals = append(principals, &tailcfg.SSHPrincipal{
|
|
||||||
NodeIP: expandedSrc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
userMap := make(map[string]string, len(sshACL.Users))
|
|
||||||
for _, user := range sshACL.Users {
|
|
||||||
userMap[user] = "="
|
|
||||||
}
|
|
||||||
rules = append(rules, &tailcfg.SSHRule{
|
|
||||||
RuleExpires: nil,
|
|
||||||
Principals: principals,
|
|
||||||
SSHUsers: userMap,
|
|
||||||
Action: &action,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return rules, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sshCheckAction(duration string) (*tailcfg.SSHAction, error) {
|
|
||||||
sessionLength, err := time.ParseDuration(duration)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &tailcfg.SSHAction{
|
|
||||||
Message: "",
|
|
||||||
Reject: false,
|
|
||||||
Accept: true,
|
|
||||||
SessionDuration: sessionLength,
|
|
||||||
AllowAgentForwarding: false,
|
|
||||||
HoldAndDelegate: "",
|
|
||||||
AllowLocalPortForwarding: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateACLPolicySrc(
|
|
||||||
machines []Machine,
|
|
||||||
aclPolicy ACLPolicy,
|
|
||||||
src string,
|
|
||||||
stripEmaildomain bool,
|
|
||||||
) ([]string, error) {
|
|
||||||
return expandAlias(machines, aclPolicy, src, stripEmaildomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateACLPolicyDest(
|
|
||||||
machines []Machine,
|
|
||||||
aclPolicy ACLPolicy,
|
|
||||||
dest string,
|
|
||||||
needsWildcard bool,
|
|
||||||
stripEmaildomain bool,
|
|
||||||
) ([]tailcfg.NetPortRange, error) {
|
|
||||||
tokens := strings.Split(dest, ":")
|
|
||||||
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
|
|
||||||
return nil, errInvalidPortFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
var alias string
|
|
||||||
// We can have here stuff like:
|
|
||||||
// git-server:*
|
|
||||||
// 192.168.1.0/24:22
|
|
||||||
// tag:montreal-webserver:80,443
|
|
||||||
// tag:api-server:443
|
|
||||||
// example-host-1:*
|
|
||||||
if len(tokens) == expectedTokenItems {
|
|
||||||
alias = tokens[0]
|
|
||||||
} else {
|
|
||||||
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
expanded, err := expandAlias(
|
|
||||||
machines,
|
|
||||||
aclPolicy,
|
|
||||||
alias,
|
|
||||||
stripEmaildomain,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ports, err := expandPorts(tokens[len(tokens)-1], needsWildcard)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dests := []tailcfg.NetPortRange{}
|
|
||||||
for _, d := range expanded {
|
|
||||||
for _, p := range *ports {
|
|
||||||
pr := tailcfg.NetPortRange{
|
|
||||||
IP: d,
|
|
||||||
Ports: p,
|
|
||||||
}
|
|
||||||
dests = append(dests, pr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dests, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseProtocol reads the proto field of the ACL and generates a list of
|
|
||||||
// protocols that will be allowed, following the IANA IP protocol number
|
|
||||||
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
|
|
||||||
//
|
|
||||||
// If the ACL proto field is empty, it allows ICMPv4, ICMPv6, TCP, and UDP,
|
|
||||||
// as per Tailscale behaviour (see tailcfg.FilterRule).
|
|
||||||
//
|
|
||||||
// Also returns a boolean indicating if the protocol
|
|
||||||
// requires all the destinations to use wildcard as port number (only TCP,
|
|
||||||
// UDP and SCTP support specifying ports).
|
|
||||||
func parseProtocol(protocol string) ([]int, bool, error) {
|
|
||||||
switch protocol {
|
|
||||||
case "":
|
|
||||||
return nil, false, nil
|
|
||||||
case "igmp":
|
|
||||||
return []int{protocolIGMP}, true, nil
|
|
||||||
case "ipv4", "ip-in-ip":
|
|
||||||
return []int{protocolIPv4}, true, nil
|
|
||||||
case "tcp":
|
|
||||||
return []int{protocolTCP}, false, nil
|
|
||||||
case "egp":
|
|
||||||
return []int{protocolEGP}, true, nil
|
|
||||||
case "igp":
|
|
||||||
return []int{protocolIGP}, true, nil
|
|
||||||
case "udp":
|
|
||||||
return []int{protocolUDP}, false, nil
|
|
||||||
case "gre":
|
|
||||||
return []int{protocolGRE}, true, nil
|
|
||||||
case "esp":
|
|
||||||
return []int{protocolESP}, true, nil
|
|
||||||
case "ah":
|
|
||||||
return []int{protocolAH}, true, nil
|
|
||||||
case "sctp":
|
|
||||||
return []int{protocolSCTP}, false, nil
|
|
||||||
case "icmp":
|
|
||||||
return []int{protocolICMP, protocolIPv6ICMP}, true, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
protocolNumber, err := strconv.Atoi(protocol)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
needsWildcard := protocolNumber != protocolTCP &&
|
|
||||||
protocolNumber != protocolUDP &&
|
|
||||||
protocolNumber != protocolSCTP
|
|
||||||
|
|
||||||
return []int{protocolNumber}, needsWildcard, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// expandalias has an input of either
|
|
||||||
// - a user
|
|
||||||
// - a group
|
|
||||||
// - a tag
|
|
||||||
// - a host
|
|
||||||
// and transform these in IPAddresses.
|
|
||||||
func expandAlias(
|
|
||||||
machines []Machine,
|
|
||||||
aclPolicy ACLPolicy,
|
|
||||||
alias string,
|
|
||||||
stripEmailDomain bool,
|
|
||||||
) ([]string, error) {
|
|
||||||
ips := []string{}
|
|
||||||
if alias == "*" {
|
|
||||||
return []string{"*"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().
|
|
||||||
Str("alias", alias).
|
|
||||||
Msg("Expanding")
|
|
||||||
|
|
||||||
if strings.HasPrefix(alias, "group:") {
|
|
||||||
users, err := expandGroup(aclPolicy, alias, stripEmailDomain)
|
|
||||||
if err != nil {
|
|
||||||
return ips, err
|
|
||||||
}
|
|
||||||
for _, n := range users {
|
|
||||||
nodes := filterMachinesByUser(machines, n)
|
|
||||||
for _, node := range nodes {
|
|
||||||
ips = append(ips, node.IPAddresses.ToStringSlice()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(alias, "tag:") {
|
|
||||||
// check for forced tags
|
|
||||||
for _, machine := range machines {
|
|
||||||
if contains(machine.ForcedTags, alias) {
|
|
||||||
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// find tag owners
|
|
||||||
owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, errInvalidTag) {
|
|
||||||
if len(ips) == 0 {
|
|
||||||
return ips, fmt.Errorf(
|
|
||||||
"%w. %v isn't owned by a TagOwner and no forced tags are defined",
|
|
||||||
errInvalidTag,
|
|
||||||
alias,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
} else {
|
|
||||||
return ips, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter out machines per tag owner
|
|
||||||
for _, user := range owners {
|
|
||||||
machines := filterMachinesByUser(machines, user)
|
|
||||||
for _, machine := range machines {
|
|
||||||
hi := machine.GetHostInfo()
|
|
||||||
if contains(hi.RequestTags, alias) {
|
|
||||||
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if alias is a user
|
|
||||||
nodes := filterMachinesByUser(machines, alias)
|
|
||||||
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain)
|
|
||||||
|
|
||||||
for _, n := range nodes {
|
|
||||||
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
|
||||||
}
|
|
||||||
if len(ips) > 0 {
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if alias is an host
|
|
||||||
if h, ok := aclPolicy.Hosts[alias]; ok {
|
|
||||||
return []string{h.String()}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if alias is an IP
|
|
||||||
ip, err := netip.ParseAddr(alias)
|
|
||||||
if err == nil {
|
|
||||||
return []string{ip.String()}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if alias is an CIDR
|
|
||||||
cidr, err := netip.ParsePrefix(alias)
|
|
||||||
if err == nil {
|
|
||||||
return []string{cidr.String()}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// we assume in this function that we only have nodes from 1 user.
|
|
||||||
func excludeCorrectlyTaggedNodes(
|
|
||||||
aclPolicy ACLPolicy,
|
|
||||||
nodes []Machine,
|
|
||||||
user string,
|
|
||||||
stripEmailDomain bool,
|
|
||||||
) []Machine {
|
|
||||||
out := []Machine{}
|
|
||||||
tags := []string{}
|
|
||||||
for tag := range aclPolicy.TagOwners {
|
|
||||||
owners, _ := expandTagOwners(aclPolicy, user, stripEmailDomain)
|
|
||||||
ns := append(owners, user)
|
|
||||||
if contains(ns, user) {
|
|
||||||
tags = append(tags, tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// for each machine if tag is in tags list, don't append it.
|
|
||||||
for _, machine := range nodes {
|
|
||||||
hi := machine.GetHostInfo()
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, t := range hi.RequestTags {
|
|
||||||
if contains(tags, t) {
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(machine.ForcedTags) > 0 {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
out = append(out, machine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
|
|
||||||
if portsStr == "*" {
|
|
||||||
return &[]tailcfg.PortRange{
|
|
||||||
{First: portRangeBegin, Last: portRangeEnd},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if needsWildcard {
|
|
||||||
return nil, errWildcardIsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
ports := []tailcfg.PortRange{}
|
|
||||||
for _, portStr := range strings.Split(portsStr, ",") {
|
|
||||||
rang := strings.Split(portStr, "-")
|
|
||||||
switch len(rang) {
|
|
||||||
case 1:
|
|
||||||
port, err := strconv.ParseUint(rang[0], Base10, BitSize16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ports = append(ports, tailcfg.PortRange{
|
|
||||||
First: uint16(port),
|
|
||||||
Last: uint16(port),
|
|
||||||
})
|
|
||||||
|
|
||||||
case expectedTokenItems:
|
|
||||||
start, err := strconv.ParseUint(rang[0], Base10, BitSize16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
last, err := strconv.ParseUint(rang[1], Base10, BitSize16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ports = append(ports, tailcfg.PortRange{
|
|
||||||
First: uint16(start),
|
|
||||||
Last: uint16(last),
|
|
||||||
})
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, errInvalidPortFormat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ports, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterMachinesByUser(machines []Machine, user string) []Machine {
|
|
||||||
out := []Machine{}
|
|
||||||
for _, machine := range machines {
|
|
||||||
if machine.User.Name == user {
|
|
||||||
out = append(out, machine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// expandTagOwners will return a list of user. An owner can be either a user or a group
|
|
||||||
// a group cannot be composed of groups.
|
|
||||||
func expandTagOwners(
|
|
||||||
aclPolicy ACLPolicy,
|
|
||||||
tag string,
|
|
||||||
stripEmailDomain bool,
|
|
||||||
) ([]string, error) {
|
|
||||||
var owners []string
|
|
||||||
ows, ok := aclPolicy.TagOwners[tag]
|
|
||||||
if !ok {
|
|
||||||
return []string{}, fmt.Errorf(
|
|
||||||
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
|
|
||||||
errInvalidTag,
|
|
||||||
tag,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for _, owner := range ows {
|
|
||||||
if strings.HasPrefix(owner, "group:") {
|
|
||||||
gs, err := expandGroup(aclPolicy, owner, stripEmailDomain)
|
|
||||||
if err != nil {
|
|
||||||
return []string{}, err
|
|
||||||
}
|
|
||||||
owners = append(owners, gs...)
|
|
||||||
} else {
|
|
||||||
owners = append(owners, owner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return owners, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// expandGroup will return the list of user inside the group
|
|
||||||
// after some validation.
|
|
||||||
func expandGroup(
|
|
||||||
aclPolicy ACLPolicy,
|
|
||||||
group string,
|
|
||||||
stripEmailDomain bool,
|
|
||||||
) ([]string, error) {
|
|
||||||
outGroups := []string{}
|
|
||||||
aclGroups, ok := aclPolicy.Groups[group]
|
|
||||||
if !ok {
|
|
||||||
return []string{}, fmt.Errorf(
|
|
||||||
"group %v isn't registered. %w",
|
|
||||||
group,
|
|
||||||
errInvalidGroup,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for _, group := range aclGroups {
|
|
||||||
if strings.HasPrefix(group, "group:") {
|
|
||||||
return []string{}, fmt.Errorf(
|
|
||||||
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
|
|
||||||
errInvalidGroup,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
grp, err := NormalizeToFQDNRules(group, stripEmailDomain)
|
|
||||||
if err != nil {
|
|
||||||
return []string{}, fmt.Errorf(
|
|
||||||
"failed to normalize group %q, err: %w",
|
|
||||||
group,
|
|
||||||
errInvalidGroup,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
outGroups = append(outGroups, grp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outGroups, nil
|
|
||||||
}
|
|
1693
acls_test.go
1693
acls_test.go
File diff suppressed because it is too large
Load Diff
113
api_common.go
113
api_common.go
@@ -1,113 +0,0 @@
|
|||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *Headscale) generateMapResponse(
|
|
||||||
mapRequest tailcfg.MapRequest,
|
|
||||||
machine *Machine,
|
|
||||||
) (*tailcfg.MapResponse, error) {
|
|
||||||
log.Trace().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Str("machine", mapRequest.Hostinfo.Hostname).
|
|
||||||
Msg("Creating Map response")
|
|
||||||
node, err := h.toNode(*machine, h.cfg.BaseDomain, h.cfg.DNSConfig)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Err(err).
|
|
||||||
Msg("Cannot convert to node")
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
peers, err := h.getValidPeers(machine)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Err(err).
|
|
||||||
Msg("Cannot fetch peers")
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles := h.getMapResponseUserProfiles(*machine, peers)
|
|
||||||
|
|
||||||
nodePeers, err := h.toNodes(peers, h.cfg.BaseDomain, h.cfg.DNSConfig)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to convert peers to Tailscale nodes")
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsConfig := getMapResponseDNSConfig(
|
|
||||||
h.cfg.DNSConfig,
|
|
||||||
h.cfg.BaseDomain,
|
|
||||||
*machine,
|
|
||||||
peers,
|
|
||||||
)
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
resp := tailcfg.MapResponse{
|
|
||||||
KeepAlive: false,
|
|
||||||
Node: node,
|
|
||||||
|
|
||||||
// TODO: Only send if updated
|
|
||||||
DERPMap: h.DERPMap,
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
|
|
||||||
UserProfiles: profiles,
|
|
||||||
|
|
||||||
// TODO: Only send if updated
|
|
||||||
SSHPolicy: h.sshPolicy,
|
|
||||||
|
|
||||||
ControlTime: &now,
|
|
||||||
|
|
||||||
Debug: &tailcfg.Debug{
|
|
||||||
DisableLogTail: !h.cfg.LogTail.Enabled,
|
|
||||||
RandomizeClientPort: h.cfg.RandomizeClientPort,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace().
|
|
||||||
Str("func", "generateMapResponse").
|
|
||||||
Str("machine", mapRequest.Hostinfo.Hostname).
|
|
||||||
// Interface("payload", resp).
|
|
||||||
Msgf("Generated map response: %s", tailMapResponseToString(resp))
|
|
||||||
|
|
||||||
return &resp, nil
|
|
||||||
}
|
|
157
api_key.go
157
api_key.go
@@ -1,157 +0,0 @@
|
|||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
apiPrefixLength = 7
|
|
||||||
apiKeyLength = 32
|
|
||||||
|
|
||||||
ErrAPIKeyFailedToParse = Error("Failed to parse ApiKey")
|
|
||||||
)
|
|
||||||
|
|
||||||
// APIKey describes the datamodel for API keys used to remotely authenticate with
|
|
||||||
// headscale.
|
|
||||||
type APIKey struct {
|
|
||||||
ID uint64 `gorm:"primary_key"`
|
|
||||||
Prefix string `gorm:"uniqueIndex"`
|
|
||||||
Hash []byte
|
|
||||||
|
|
||||||
CreatedAt *time.Time
|
|
||||||
Expiration *time.Time
|
|
||||||
LastSeen *time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAPIKey creates a new ApiKey in a user, and returns it.
|
|
||||||
func (h *Headscale) CreateAPIKey(
|
|
||||||
expiration *time.Time,
|
|
||||||
) (string, *APIKey, error) {
|
|
||||||
prefix, err := GenerateRandomStringURLSafe(apiPrefixLength)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
toBeHashed, err := GenerateRandomStringURLSafe(apiKeyLength)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key to return to user, this will only be visible _once_
|
|
||||||
keyStr := prefix + "." + toBeHashed
|
|
||||||
|
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(toBeHashed), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
key := APIKey{
|
|
||||||
Prefix: prefix,
|
|
||||||
Hash: hash,
|
|
||||||
Expiration: expiration,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.db.Save(&key).Error; err != nil {
|
|
||||||
return "", nil, fmt.Errorf("failed to save API key to database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyStr, &key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAPIKeys returns the list of ApiKeys for a user.
|
|
||||||
func (h *Headscale) ListAPIKeys() ([]APIKey, error) {
|
|
||||||
keys := []APIKey{}
|
|
||||||
if err := h.db.Find(&keys).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAPIKey returns a ApiKey for a given key.
|
|
||||||
func (h *Headscale) GetAPIKey(prefix string) (*APIKey, error) {
|
|
||||||
key := APIKey{}
|
|
||||||
if result := h.db.First(&key, "prefix = ?", prefix); result.Error != nil {
|
|
||||||
return nil, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return &key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAPIKeyByID returns a ApiKey for a given id.
|
|
||||||
func (h *Headscale) GetAPIKeyByID(id uint64) (*APIKey, error) {
|
|
||||||
key := APIKey{}
|
|
||||||
if result := h.db.Find(&APIKey{ID: id}).First(&key); result.Error != nil {
|
|
||||||
return nil, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return &key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestroyAPIKey destroys a ApiKey. Returns error if the ApiKey
|
|
||||||
// does not exist.
|
|
||||||
func (h *Headscale) DestroyAPIKey(key APIKey) error {
|
|
||||||
if result := h.db.Unscoped().Delete(key); result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpireAPIKey marks a ApiKey as expired.
|
|
||||||
func (h *Headscale) ExpireAPIKey(key *APIKey) error {
|
|
||||||
if err := h.db.Model(&key).Update("Expiration", time.Now()).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
|
|
||||||
prefix, hash, found := strings.Cut(keyStr, ".")
|
|
||||||
if !found {
|
|
||||||
return false, ErrAPIKeyFailedToParse
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := h.GetAPIKey(prefix)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to validate api key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if key.Expiration.Before(time.Now()) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bcrypt.CompareHashAndPassword(key.Hash, []byte(hash)); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (key *APIKey) toProto() *v1.ApiKey {
|
|
||||||
protoKey := v1.ApiKey{
|
|
||||||
Id: key.ID,
|
|
||||||
Prefix: key.Prefix,
|
|
||||||
}
|
|
||||||
|
|
||||||
if key.Expiration != nil {
|
|
||||||
protoKey.Expiration = timestamppb.New(*key.Expiration)
|
|
||||||
}
|
|
||||||
|
|
||||||
if key.CreatedAt != nil {
|
|
||||||
protoKey.CreatedAt = timestamppb.New(*key.CreatedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
if key.LastSeen != nil {
|
|
||||||
protoKey.LastSeen = timestamppb.New(*key.LastSeen)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &protoKey
|
|
||||||
}
|
|
47
cmd/build-docker-img/main.go
Normal file
47
cmd/build-docker-img/main.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/integration"
|
||||||
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Printf("creating docker pool")
|
||||||
|
pool, err := dockertest.NewPool("")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not connect to docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("creating docker network")
|
||||||
|
network, err := pool.CreateNetwork("docker-integration-net")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create or get network: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, version := range integration.TailscaleVersions {
|
||||||
|
log.Printf("creating container image for Tailscale (%s)", version)
|
||||||
|
|
||||||
|
tsClient, err := tsic.New(
|
||||||
|
pool,
|
||||||
|
version,
|
||||||
|
network,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create tailscale node: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tsClient.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to shut down container: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
network.Close()
|
||||||
|
err = pool.RemoveNetwork(network)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to remove network: %s", err)
|
||||||
|
}
|
||||||
|
}
|
@@ -39,6 +39,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: satackey/action-docker-layer-caching@main
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v34
|
uses: tj-actions/changed-files@v34
|
||||||
@@ -50,9 +55,6 @@ jobs:
|
|||||||
integration_test/
|
integration_test/
|
||||||
config-example.yaml
|
config-example.yaml
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v18
|
|
||||||
if: {{ "${{ env.ACT }}" }} || steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
|
|
||||||
- name: Run general integration tests
|
- name: Run general integration tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -64,7 +66,7 @@ jobs:
|
|||||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--volume $PWD/control_logs:/tmp/control \
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test ./... \
|
go run gotest.tools/gotestsum@latest -- ./... \
|
||||||
-tags ts2019 \
|
-tags ts2019 \
|
||||||
-failfast \
|
-failfast \
|
||||||
-timeout 120m \
|
-timeout 120m \
|
||||||
@@ -76,6 +78,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: logs
|
name: logs
|
||||||
path: "control_logs/*.log"
|
path: "control_logs/*.log"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: pprof
|
||||||
|
path: "control_logs/*.pprof.tar"
|
||||||
`),
|
`),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@@ -5,8 +5,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale"
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
"github.com/pterm/pterm"
|
"github.com/pterm/pterm"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -83,7 +83,7 @@ var listAPIKeys = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
tableData = append(tableData, []string{
|
tableData = append(tableData, []string{
|
||||||
strconv.FormatUint(key.GetId(), headscale.Base10),
|
strconv.FormatUint(key.GetId(), util.Base10),
|
||||||
key.GetPrefix(),
|
key.GetPrefix(),
|
||||||
expiration,
|
expiration,
|
||||||
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||||
|
@@ -3,8 +3,8 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/juanfont/headscale"
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
@@ -93,7 +93,7 @@ var createNodeCmd = &cobra.Command{
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !headscale.NodePublicKeyRegex.Match([]byte(machineKey)) {
|
if !util.NodePublicKeyRegex.Match([]byte(machineKey)) {
|
||||||
err = errPreAuthKeyMalformed
|
err = errPreAuthKeyMalformed
|
||||||
ErrorOutput(
|
ErrorOutput(
|
||||||
err,
|
err,
|
||||||
|
@@ -9,8 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
survey "github.com/AlecAivazis/survey/v2"
|
survey "github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/juanfont/headscale"
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"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"
|
||||||
@@ -529,7 +529,7 @@ func nodesToPtables(
|
|||||||
|
|
||||||
var machineKey key.MachinePublic
|
var machineKey key.MachinePublic
|
||||||
err := machineKey.UnmarshalText(
|
err := machineKey.UnmarshalText(
|
||||||
[]byte(headscale.MachinePublicKeyEnsurePrefix(machine.MachineKey)),
|
[]byte(util.MachinePublicKeyEnsurePrefix(machine.MachineKey)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
machineKey = key.MachinePublic{}
|
machineKey = key.MachinePublic{}
|
||||||
@@ -537,7 +537,7 @@ func nodesToPtables(
|
|||||||
|
|
||||||
var nodeKey key.NodePublic
|
var nodeKey key.NodePublic
|
||||||
err = nodeKey.UnmarshalText(
|
err = nodeKey.UnmarshalText(
|
||||||
[]byte(headscale.NodePublicKeyEnsurePrefix(machine.NodeKey)),
|
[]byte(util.NodePublicKeyEnsurePrefix(machine.NodeKey)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -596,7 +596,7 @@ func nodesToPtables(
|
|||||||
}
|
}
|
||||||
|
|
||||||
nodeData := []string{
|
nodeData := []string{
|
||||||
strconv.FormatUint(machine.Id, headscale.Base10),
|
strconv.FormatUint(machine.Id, util.Base10),
|
||||||
machine.Name,
|
machine.Name,
|
||||||
machine.GetGivenName(),
|
machine.GetGivenName(),
|
||||||
machineKey.ShortString(),
|
machineKey.ShortString(),
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/juanfont/headscale"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -38,18 +38,18 @@ func initConfig() {
|
|||||||
cfgFile = os.Getenv("HEADSCALE_CONFIG")
|
cfgFile = os.Getenv("HEADSCALE_CONFIG")
|
||||||
}
|
}
|
||||||
if cfgFile != "" {
|
if cfgFile != "" {
|
||||||
err := headscale.LoadConfig(cfgFile, true)
|
err := types.LoadConfig(cfgFile, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Caller().Err(err).Msgf("Error loading config file %s", cfgFile)
|
log.Fatal().Caller().Err(err).Msgf("Error loading config file %s", cfgFile)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err := headscale.LoadConfig("", false)
|
err := types.LoadConfig("", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Caller().Err(err).Msgf("Error loading config")
|
log.Fatal().Caller().Err(err).Msgf("Error loading config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := headscale.GetHeadscaleConfig()
|
cfg, err := types.GetHeadscaleConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Caller().Err(err)
|
log.Fatal().Caller().Err(err)
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ func initConfig() {
|
|||||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Log.Format == headscale.JSONLogFormat {
|
if cfg.Log.Format == types.JSONLogFormat {
|
||||||
log.Logger = log.Output(os.Stdout)
|
log.Logger = log.Output(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,8 +6,8 @@ import (
|
|||||||
"net/netip"
|
"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/juanfont/headscale/hscontrol/types"
|
||||||
"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"
|
||||||
@@ -277,7 +277,7 @@ func routesToPtables(routes []*v1.Route) pterm.TableData {
|
|||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if prefix == headscale.ExitRouteV4 || prefix == headscale.ExitRouteV6 {
|
if prefix == types.ExitRouteV4 || prefix == types.ExitRouteV6 {
|
||||||
isPrimaryStr = "-"
|
isPrimaryStr = "-"
|
||||||
} else {
|
} else {
|
||||||
isPrimaryStr = strconv.FormatBool(route.IsPrimary)
|
isPrimaryStr = strconv.FormatBool(route.IsPrimary)
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
survey "github.com/AlecAivazis/survey/v2"
|
survey "github.com/AlecAivazis/survey/v2"
|
||||||
"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/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -20,9 +20,7 @@ func init() {
|
|||||||
userCmd.AddCommand(renameUserCmd)
|
userCmd.AddCommand(renameUserCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
var errMissingParameter = errors.New("missing parameters")
|
||||||
errMissingParameter = headscale.Error("missing parameters")
|
|
||||||
)
|
|
||||||
|
|
||||||
var userCmd = &cobra.Command{
|
var userCmd = &cobra.Command{
|
||||||
Use: "users",
|
Use: "users",
|
||||||
|
@@ -8,8 +8,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/juanfont/headscale"
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
|
"github.com/juanfont/headscale/hscontrol"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/policy"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
@@ -22,8 +25,8 @@ const (
|
|||||||
SocketWritePermissions = 0o666
|
SocketWritePermissions = 0o666
|
||||||
)
|
)
|
||||||
|
|
||||||
func getHeadscaleApp() (*headscale.Headscale, error) {
|
func getHeadscaleApp() (*hscontrol.Headscale, error) {
|
||||||
cfg, err := headscale.GetHeadscaleConfig()
|
cfg, err := types.GetHeadscaleConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"failed to load configuration while creating headscale instance: %w",
|
"failed to load configuration while creating headscale instance: %w",
|
||||||
@@ -31,7 +34,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := headscale.NewHeadscale(cfg)
|
app, err := hscontrol.NewHeadscale(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -39,21 +42,23 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
|||||||
// We are doing this here, as in the future could be cool to have it also hot-reload
|
// We are doing this here, as in the future could be cool to have it also hot-reload
|
||||||
|
|
||||||
if cfg.ACL.PolicyPath != "" {
|
if cfg.ACL.PolicyPath != "" {
|
||||||
aclPath := headscale.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath)
|
aclPath := util.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath)
|
||||||
err = app.LoadACLPolicy(aclPath)
|
pol, err := policy.LoadACLPolicyFromPath(aclPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().
|
log.Fatal().
|
||||||
Str("path", aclPath).
|
Str("path", aclPath).
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Could not load the ACL policy")
|
Msg("Could not load the ACL policy")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.ACLPolicy = pol
|
||||||
}
|
}
|
||||||
|
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
|
func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
|
||||||
cfg, err := headscale.GetHeadscaleConfig()
|
cfg, err := types.GetHeadscaleConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().
|
log.Fatal().
|
||||||
Err(err).
|
Err(err).
|
||||||
@@ -74,7 +79,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
|
|||||||
|
|
||||||
address := cfg.CLI.Address
|
address := cfg.CLI.Address
|
||||||
|
|
||||||
// If the address is not set, we assume that we are on the server hosting headscale.
|
// If the address is not set, we assume that we are on the server hosting hscontrol.
|
||||||
if address == "" {
|
if address == "" {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("socket", cfg.UnixSocket).
|
Str("socket", cfg.UnixSocket).
|
||||||
@@ -98,7 +103,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
|
|||||||
grpcOptions = append(
|
grpcOptions = append(
|
||||||
grpcOptions,
|
grpcOptions,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
grpc.WithContextDialer(headscale.GrpcSocketDialer),
|
grpc.WithContextDialer(util.GrpcSocketDialer),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// If we are not connecting to a local server, require an API key for authentication
|
// If we are not connecting to a local server, require an API key for authentication
|
||||||
|
@@ -6,11 +6,25 @@ import (
|
|||||||
|
|
||||||
"github.com/efekarakus/termcolor"
|
"github.com/efekarakus/termcolor"
|
||||||
"github.com/juanfont/headscale/cmd/headscale/cli"
|
"github.com/juanfont/headscale/cmd/headscale/cli"
|
||||||
|
"github.com/pkg/profile"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if _, enableProfile := os.LookupEnv("HEADSCALE_PROFILING_ENABLED"); enableProfile {
|
||||||
|
if profilePath, ok := os.LookupEnv("HEADSCALE_PROFILING_PATH"); ok {
|
||||||
|
err := os.MkdirAll(profilePath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed to create profiling directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer profile.Start(profile.ProfilePath(profilePath)).Stop()
|
||||||
|
} else {
|
||||||
|
defer profile.Start().Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var colors bool
|
var colors bool
|
||||||
switch l := termcolor.SupportLevel(os.Stderr); l {
|
switch l := termcolor.SupportLevel(os.Stderr); l {
|
||||||
case termcolor.Level16M:
|
case termcolor.Level16M:
|
||||||
@@ -34,7 +48,7 @@ func main() {
|
|||||||
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{
|
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||||
Out: os.Stdout,
|
Out: os.Stderr,
|
||||||
TimeFormat: time.RFC3339,
|
TimeFormat: time.RFC3339,
|
||||||
NoColor: !colors,
|
NoColor: !colors,
|
||||||
})
|
})
|
||||||
|
@@ -7,7 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/juanfont/headscale"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
)
|
)
|
||||||
@@ -50,7 +51,7 @@ func (*Suite) TestConfigFileLoading(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load example config, it should load without validation errors
|
// Load example config, it should load without validation errors
|
||||||
err = headscale.LoadConfig(cfgFile, true)
|
err = types.LoadConfig(cfgFile, true)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
// Test that config file was interpreted correctly
|
// Test that config file was interpreted correctly
|
||||||
@@ -58,13 +59,13 @@ 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, "./db.sqlite")
|
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/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")
|
||||||
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
||||||
c.Assert(
|
c.Assert(
|
||||||
headscale.GetFileMode("unix_socket_permission"),
|
util.GetFileMode("unix_socket_permission"),
|
||||||
check.Equals,
|
check.Equals,
|
||||||
fs.FileMode(0o770),
|
fs.FileMode(0o770),
|
||||||
)
|
)
|
||||||
@@ -93,7 +94,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load example config, it should load without validation errors
|
// Load example config, it should load without validation errors
|
||||||
err = headscale.LoadConfig(tmpDir, false)
|
err = types.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
// Test that config file was interpreted correctly
|
// Test that config file was interpreted correctly
|
||||||
@@ -101,13 +102,13 @@ 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, "./db.sqlite")
|
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/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")
|
||||||
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
||||||
c.Assert(
|
c.Assert(
|
||||||
headscale.GetFileMode("unix_socket_permission"),
|
util.GetFileMode("unix_socket_permission"),
|
||||||
check.Equals,
|
check.Equals,
|
||||||
fs.FileMode(0o770),
|
fs.FileMode(0o770),
|
||||||
)
|
)
|
||||||
@@ -137,10 +138,10 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load example config, it should load without validation errors
|
// Load example config, it should load without validation errors
|
||||||
err = headscale.LoadConfig(tmpDir, false)
|
err = types.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
dnsConfig, baseDomain := headscale.GetDNSConfig()
|
dnsConfig, baseDomain := types.GetDNSConfig()
|
||||||
|
|
||||||
c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1")
|
c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1")
|
||||||
c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1")
|
c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1")
|
||||||
@@ -172,7 +173,7 @@ noise:
|
|||||||
writeConfig(c, tmpDir, configYaml)
|
writeConfig(c, tmpDir, configYaml)
|
||||||
|
|
||||||
// Check configuration validation errors (1)
|
// Check configuration validation errors (1)
|
||||||
err = headscale.LoadConfig(tmpDir, false)
|
err = types.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
// check.Matches can not handle multiline strings
|
// check.Matches can not handle multiline strings
|
||||||
tmp := strings.ReplaceAll(err.Error(), "\n", "***")
|
tmp := strings.ReplaceAll(err.Error(), "\n", "***")
|
||||||
@@ -201,6 +202,6 @@ tls_letsencrypt_hostname: example.com
|
|||||||
tls_letsencrypt_challenge_type: TLS-ALPN-01
|
tls_letsencrypt_challenge_type: TLS-ALPN-01
|
||||||
`)
|
`)
|
||||||
writeConfig(c, tmpDir, configYaml)
|
writeConfig(c, tmpDir, configYaml)
|
||||||
err = headscale.LoadConfig(tmpDir, false)
|
err = types.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
}
|
}
|
||||||
|
@@ -44,9 +44,7 @@ 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.
|
||||||
#
|
#
|
||||||
# For production:
|
private_key_path: /var/lib/headscale/private.key
|
||||||
# /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
|
||||||
@@ -55,19 +53,17 @@ 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
|
# It must be within IP ranges supported by the Tailscale
|
||||||
# needs to be within IP ranges supported by the Tailscale
|
# client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48.
|
||||||
# client.
|
# See below:
|
||||||
# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
|
# 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
|
# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
|
||||||
|
# Any other range is NOT supported, and it will cause unexpected issues.
|
||||||
ip_prefixes:
|
ip_prefixes:
|
||||||
- fd7a:115c:a1e0::/48
|
- fd7a:115c:a1e0::/48
|
||||||
- 100.64.0.0/10
|
- 100.64.0.0/10
|
||||||
@@ -137,8 +133,7 @@ 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.
|
||||||
@@ -172,8 +167,7 @@ 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
|
||||||
@@ -263,8 +257,7 @@ dns_config:
|
|||||||
|
|
||||||
# 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.sock
|
unix_socket: /var/run/headscale/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,
|
||||||
|
404
db.go
404
db.go
@@ -1,404 +0,0 @@
|
|||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql/driver"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dbVersion = "1"
|
|
||||||
|
|
||||||
errValueNotFound = Error("not found")
|
|
||||||
ErrCannotParsePrefix = Error("cannot parse prefix")
|
|
||||||
)
|
|
||||||
|
|
||||||
// KV is a key-value store in a psql table. For future use...
|
|
||||||
type KV struct {
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Headscale) initDB() error {
|
|
||||||
db, err := h.openDB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h.db = db
|
|
||||||
|
|
||||||
if h.dbType == Postgres {
|
|
||||||
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{}, "name", "hostname")
|
|
||||||
|
|
||||||
// GivenName is used as the primary source of DNS names, make sure
|
|
||||||
// the field is populated and normalized if it was not when the
|
|
||||||
// machine was registered.
|
|
||||||
_ = db.Migrator().RenameColumn(&Machine{}, "nickname", "given_name")
|
|
||||||
|
|
||||||
// If the Machine table has a column for registered,
|
|
||||||
// find all occourences of "false" and drop them. Then
|
|
||||||
// remove the column.
|
|
||||||
if db.Migrator().HasColumn(&Machine{}, "registered") {
|
|
||||||
log.Info().
|
|
||||||
Msg(`Database has legacy "registered" column in machine, removing...`)
|
|
||||||
|
|
||||||
machines := Machines{}
|
|
||||||
if err := h.db.Not("registered").Find(&machines).Error; err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error accessing db")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, machine := range machines {
|
|
||||||
log.Info().
|
|
||||||
Str("machine", machine.Hostname).
|
|
||||||
Str("machine_key", machine.MachineKey).
|
|
||||||
Msg("Deleting unregistered machine")
|
|
||||||
if err := h.db.Delete(&Machine{}, machine.ID).Error; err != nil {
|
|
||||||
log.Error().
|
|
||||||
Err(err).
|
|
||||||
Str("machine", machine.Hostname).
|
|
||||||
Str("machine_key", machine.MachineKey).
|
|
||||||
Msg("Error deleting unregistered machine")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := db.Migrator().DropColumn(&Machine{}, "registered")
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error dropping registered column")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.AutoMigrate(&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{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if db.Migrator().HasColumn(&Machine{}, "given_name") {
|
|
||||||
machines := Machines{}
|
|
||||||
if err := h.db.Find(&machines).Error; err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error accessing db")
|
|
||||||
}
|
|
||||||
|
|
||||||
for item, machine := range machines {
|
|
||||||
if machine.GivenName == "" {
|
|
||||||
normalizedHostname, err := NormalizeToFQDNRules(
|
|
||||||
machine.Hostname,
|
|
||||||
h.cfg.OIDC.StripEmaildomain,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Str("hostname", machine.Hostname).
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to normalize machine hostname in DB migration")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.RenameMachine(&machines[item], normalizedHostname)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Str("hostname", machine.Hostname).
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to save normalized machine name in DB migration")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.AutoMigrate(&KV{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.AutoMigrate(&PreAuthKey{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.AutoMigrate(&PreAuthKeyACLTag{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = db.Migrator().DropTable("shared_machines")
|
|
||||||
|
|
||||||
err = db.AutoMigrate(&APIKey{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.setValue("db_version", dbVersion)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Headscale) openDB() (*gorm.DB, error) {
|
|
||||||
var db *gorm.DB
|
|
||||||
var err error
|
|
||||||
|
|
||||||
var log logger.Interface
|
|
||||||
if h.dbDebug {
|
|
||||||
log = logger.Default
|
|
||||||
} else {
|
|
||||||
log = logger.Default.LogMode(logger.Silent)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch h.dbType {
|
|
||||||
case Sqlite:
|
|
||||||
db, err = gorm.Open(
|
|
||||||
sqlite.Open(h.dbString+"?_synchronous=1&_journal_mode=WAL"),
|
|
||||||
&gorm.Config{
|
|
||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
|
||||||
Logger: log,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
db.Exec("PRAGMA foreign_keys=ON")
|
|
||||||
|
|
||||||
// The pure Go SQLite library does not handle locking in
|
|
||||||
// the same way as the C based one and we cant use the gorm
|
|
||||||
// connection pool as of 2022/02/23.
|
|
||||||
sqlDB, _ := db.DB()
|
|
||||||
sqlDB.SetMaxIdleConns(1)
|
|
||||||
sqlDB.SetMaxOpenConns(1)
|
|
||||||
sqlDB.SetConnMaxIdleTime(time.Hour)
|
|
||||||
|
|
||||||
case Postgres:
|
|
||||||
db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{
|
|
||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
|
||||||
Logger: log,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getValue returns the value for the given key in KV.
|
|
||||||
func (h *Headscale) getValue(key string) (string, error) {
|
|
||||||
var row KV
|
|
||||||
if result := h.db.First(&row, "key = ?", key); errors.Is(
|
|
||||||
result.Error,
|
|
||||||
gorm.ErrRecordNotFound,
|
|
||||||
) {
|
|
||||||
return "", errValueNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return row.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setValue sets value for the given key in KV.
|
|
||||||
func (h *Headscale) setValue(key string, value string) error {
|
|
||||||
keyValue := KV{
|
|
||||||
Key: key,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := h.getValue(key); err == nil {
|
|
||||||
h.db.Model(&keyValue).Where("key = ?", key).Update("value", value)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.db.Create(keyValue).Error; err != nil {
|
|
||||||
return fmt.Errorf("failed to create key value pair in the database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Headscale) pingDB(ctx context.Context) error {
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
|
||||||
defer cancel()
|
|
||||||
db, err := h.db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.PingContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a "wrapper" type around tailscales
|
|
||||||
// Hostinfo to allow us to add database "serialization"
|
|
||||||
// methods. This allows us to use a typed values throughout
|
|
||||||
// the code and not have to marshal/unmarshal and error
|
|
||||||
// check all over the code.
|
|
||||||
type HostInfo tailcfg.Hostinfo
|
|
||||||
|
|
||||||
func (hi *HostInfo) Scan(destination interface{}) error {
|
|
||||||
switch value := destination.(type) {
|
|
||||||
case []byte:
|
|
||||||
return json.Unmarshal(value, hi)
|
|
||||||
|
|
||||||
case string:
|
|
||||||
return json.Unmarshal([]byte(value), hi)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value return json value, implement driver.Valuer interface.
|
|
||||||
func (hi HostInfo) Value() (driver.Value, error) {
|
|
||||||
bytes, err := json.Marshal(hi)
|
|
||||||
|
|
||||||
return string(bytes), err
|
|
||||||
}
|
|
||||||
|
|
||||||
type 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
|
|
||||||
|
|
||||||
func (i *IPPrefixes) Scan(destination interface{}) error {
|
|
||||||
switch value := destination.(type) {
|
|
||||||
case []byte:
|
|
||||||
return json.Unmarshal(value, i)
|
|
||||||
|
|
||||||
case string:
|
|
||||||
return json.Unmarshal([]byte(value), i)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value return json value, implement driver.Valuer interface.
|
|
||||||
func (i IPPrefixes) Value() (driver.Value, error) {
|
|
||||||
bytes, err := json.Marshal(i)
|
|
||||||
|
|
||||||
return string(bytes), err
|
|
||||||
}
|
|
||||||
|
|
||||||
type StringList []string
|
|
||||||
|
|
||||||
func (i *StringList) Scan(destination interface{}) error {
|
|
||||||
switch value := destination.(type) {
|
|
||||||
case []byte:
|
|
||||||
return json.Unmarshal(value, i)
|
|
||||||
|
|
||||||
case string:
|
|
||||||
return json.Unmarshal([]byte(value), i)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w: unexpected data type %T", ErrMachineAddressesInvalid, destination)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value return json value, implement driver.Valuer interface.
|
|
||||||
func (i StringList) Value() (driver.Value, error) {
|
|
||||||
bytes, err := json.Marshal(i)
|
|
||||||
|
|
||||||
return string(bytes), err
|
|
||||||
}
|
|
394
dns_test.go
394
dns_test.go
@@ -1,394 +0,0 @@
|
|||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
"tailscale.com/types/dnstype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Suite) TestMagicDNSRootDomains100(c *check.C) {
|
|
||||||
prefixes := []netip.Prefix{
|
|
||||||
netip.MustParsePrefix("100.64.0.0/10"),
|
|
||||||
}
|
|
||||||
domains := generateMagicDNSRootDomains(prefixes)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, domain := range domains {
|
|
||||||
if domain == "64.100.in-addr.arpa." {
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Assert(found, check.Equals, true)
|
|
||||||
|
|
||||||
found = false
|
|
||||||
for _, domain := range domains {
|
|
||||||
if domain == "100.100.in-addr.arpa." {
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Assert(found, check.Equals, true)
|
|
||||||
|
|
||||||
found = false
|
|
||||||
for _, domain := range domains {
|
|
||||||
if domain == "127.100.in-addr.arpa." {
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Assert(found, check.Equals, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Suite) TestMagicDNSRootDomains172(c *check.C) {
|
|
||||||
prefixes := []netip.Prefix{
|
|
||||||
netip.MustParsePrefix("172.16.0.0/16"),
|
|
||||||
}
|
|
||||||
domains := generateMagicDNSRootDomains(prefixes)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, domain := range domains {
|
|
||||||
if domain == "0.16.172.in-addr.arpa." {
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Assert(found, check.Equals, true)
|
|
||||||
|
|
||||||
found = false
|
|
||||||
for _, domain := range domains {
|
|
||||||
if domain == "255.16.172.in-addr.arpa." {
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Assert(found, check.Equals, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Happens when netmask is a multiple of 4 bits (sounds likely).
|
|
||||||
func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) {
|
|
||||||
prefixes := []netip.Prefix{
|
|
||||||
netip.MustParsePrefix("fd7a:115c:a1e0::/48"),
|
|
||||||
}
|
|
||||||
domains := generateMagicDNSRootDomains(prefixes)
|
|
||||||
|
|
||||||
c.Assert(len(domains), check.Equals, 1)
|
|
||||||
c.Assert(
|
|
||||||
domains[0].WithTrailingDot(),
|
|
||||||
check.Equals,
|
|
||||||
"0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) {
|
|
||||||
prefixes := []netip.Prefix{
|
|
||||||
netip.MustParsePrefix("fd7a:115c:a1e0::/50"),
|
|
||||||
}
|
|
||||||
domains := generateMagicDNSRootDomains(prefixes)
|
|
||||||
|
|
||||||
yieldsRoot := func(dom string) bool {
|
|
||||||
for _, candidate := range domains {
|
|
||||||
if candidate.WithTrailingDot() == dom {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Assert(len(domains), check.Equals, 4)
|
|
||||||
c.Assert(yieldsRoot("0.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true)
|
|
||||||
c.Assert(yieldsRoot("1.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true)
|
|
||||||
c.Assert(yieldsRoot("2.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true)
|
|
||||||
c.Assert(yieldsRoot("3.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|
||||||
userShared1, err := app.CreateUser("shared1")
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
userShared2, err := app.CreateUser("shared2")
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
userShared3, err := app.CreateUser("shared3")
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
preAuthKeyInShared1, err := app.CreatePreAuthKey(
|
|
||||||
userShared1.Name,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
preAuthKeyInShared2, err := app.CreatePreAuthKey(
|
|
||||||
userShared2.Name,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
preAuthKeyInShared3, err := app.CreatePreAuthKey(
|
|
||||||
userShared3.Name,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
PreAuthKey2InShared1, err := app.CreatePreAuthKey(
|
|
||||||
userShared1.Name,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
_, err = app.GetMachine(userShared1.Name, "test_get_shared_nodes_1")
|
|
||||||
c.Assert(err, check.NotNil)
|
|
||||||
|
|
||||||
machineInShared1 := &Machine{
|
|
||||||
ID: 1,
|
|
||||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
|
||||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
|
||||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
|
||||||
Hostname: "test_get_shared_nodes_1",
|
|
||||||
UserID: userShared1.ID,
|
|
||||||
User: *userShared1,
|
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
|
|
||||||
AuthKeyID: uint(preAuthKeyInShared1.ID),
|
|
||||||
}
|
|
||||||
app.db.Save(machineInShared1)
|
|
||||||
|
|
||||||
_, err = app.GetMachine(userShared1.Name, machineInShared1.Hostname)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
machineInShared2 := &Machine{
|
|
||||||
ID: 2,
|
|
||||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
Hostname: "test_get_shared_nodes_2",
|
|
||||||
UserID: userShared2.ID,
|
|
||||||
User: *userShared2,
|
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
|
|
||||||
AuthKeyID: uint(preAuthKeyInShared2.ID),
|
|
||||||
}
|
|
||||||
app.db.Save(machineInShared2)
|
|
||||||
|
|
||||||
_, err = app.GetMachine(userShared2.Name, machineInShared2.Hostname)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
machineInShared3 := &Machine{
|
|
||||||
ID: 3,
|
|
||||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
Hostname: "test_get_shared_nodes_3",
|
|
||||||
UserID: userShared3.ID,
|
|
||||||
User: *userShared3,
|
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
|
|
||||||
AuthKeyID: uint(preAuthKeyInShared3.ID),
|
|
||||||
}
|
|
||||||
app.db.Save(machineInShared3)
|
|
||||||
|
|
||||||
_, err = app.GetMachine(userShared3.Name, machineInShared3.Hostname)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
machine2InShared1 := &Machine{
|
|
||||||
ID: 4,
|
|
||||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
Hostname: "test_get_shared_nodes_4",
|
|
||||||
UserID: userShared1.ID,
|
|
||||||
User: *userShared1,
|
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
|
|
||||||
AuthKeyID: uint(PreAuthKey2InShared1.ID),
|
|
||||||
}
|
|
||||||
app.db.Save(machine2InShared1)
|
|
||||||
|
|
||||||
baseDomain := "foobar.headscale.net"
|
|
||||||
dnsConfigOrig := tailcfg.DNSConfig{
|
|
||||||
Routes: make(map[string][]*dnstype.Resolver),
|
|
||||||
Domains: []string{baseDomain},
|
|
||||||
Proxied: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
peersOfMachineInShared1, err := app.getPeers(machineInShared1)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
dnsConfig := getMapResponseDNSConfig(
|
|
||||||
&dnsConfigOrig,
|
|
||||||
baseDomain,
|
|
||||||
*machineInShared1,
|
|
||||||
peersOfMachineInShared1,
|
|
||||||
)
|
|
||||||
c.Assert(dnsConfig, check.NotNil)
|
|
||||||
|
|
||||||
c.Assert(len(dnsConfig.Routes), check.Equals, 3)
|
|
||||||
|
|
||||||
domainRouteShared1 := fmt.Sprintf("%s.%s", userShared1.Name, baseDomain)
|
|
||||||
_, ok := dnsConfig.Routes[domainRouteShared1]
|
|
||||||
c.Assert(ok, check.Equals, true)
|
|
||||||
|
|
||||||
domainRouteShared2 := fmt.Sprintf("%s.%s", userShared2.Name, baseDomain)
|
|
||||||
_, ok = dnsConfig.Routes[domainRouteShared2]
|
|
||||||
c.Assert(ok, check.Equals, true)
|
|
||||||
|
|
||||||
domainRouteShared3 := fmt.Sprintf("%s.%s", userShared3.Name, baseDomain)
|
|
||||||
_, ok = dnsConfig.Routes[domainRouteShared3]
|
|
||||||
c.Assert(ok, check.Equals, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|
||||||
userShared1, err := app.CreateUser("shared1")
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
userShared2, err := app.CreateUser("shared2")
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
userShared3, err := app.CreateUser("shared3")
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
preAuthKeyInShared1, err := app.CreatePreAuthKey(
|
|
||||||
userShared1.Name,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
preAuthKeyInShared2, err := app.CreatePreAuthKey(
|
|
||||||
userShared2.Name,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
preAuthKeyInShared3, err := app.CreatePreAuthKey(
|
|
||||||
userShared3.Name,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
preAuthKey2InShared1, err := app.CreatePreAuthKey(
|
|
||||||
userShared1.Name,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
_, err = app.GetMachine(userShared1.Name, "test_get_shared_nodes_1")
|
|
||||||
c.Assert(err, check.NotNil)
|
|
||||||
|
|
||||||
machineInShared1 := &Machine{
|
|
||||||
ID: 1,
|
|
||||||
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
|
||||||
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
|
||||||
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
|
||||||
Hostname: "test_get_shared_nodes_1",
|
|
||||||
UserID: userShared1.ID,
|
|
||||||
User: *userShared1,
|
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
|
|
||||||
AuthKeyID: uint(preAuthKeyInShared1.ID),
|
|
||||||
}
|
|
||||||
app.db.Save(machineInShared1)
|
|
||||||
|
|
||||||
_, err = app.GetMachine(userShared1.Name, machineInShared1.Hostname)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
machineInShared2 := &Machine{
|
|
||||||
ID: 2,
|
|
||||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
Hostname: "test_get_shared_nodes_2",
|
|
||||||
UserID: userShared2.ID,
|
|
||||||
User: *userShared2,
|
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
|
|
||||||
AuthKeyID: uint(preAuthKeyInShared2.ID),
|
|
||||||
}
|
|
||||||
app.db.Save(machineInShared2)
|
|
||||||
|
|
||||||
_, err = app.GetMachine(userShared2.Name, machineInShared2.Hostname)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
machineInShared3 := &Machine{
|
|
||||||
ID: 3,
|
|
||||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
Hostname: "test_get_shared_nodes_3",
|
|
||||||
UserID: userShared3.ID,
|
|
||||||
User: *userShared3,
|
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
|
|
||||||
AuthKeyID: uint(preAuthKeyInShared3.ID),
|
|
||||||
}
|
|
||||||
app.db.Save(machineInShared3)
|
|
||||||
|
|
||||||
_, err = app.GetMachine(userShared3.Name, machineInShared3.Hostname)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
machine2InShared1 := &Machine{
|
|
||||||
ID: 4,
|
|
||||||
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
|
||||||
Hostname: "test_get_shared_nodes_4",
|
|
||||||
UserID: userShared1.ID,
|
|
||||||
User: *userShared1,
|
|
||||||
RegisterMethod: RegisterMethodAuthKey,
|
|
||||||
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.4")},
|
|
||||||
AuthKeyID: uint(preAuthKey2InShared1.ID),
|
|
||||||
}
|
|
||||||
app.db.Save(machine2InShared1)
|
|
||||||
|
|
||||||
baseDomain := "foobar.headscale.net"
|
|
||||||
dnsConfigOrig := tailcfg.DNSConfig{
|
|
||||||
Routes: make(map[string][]*dnstype.Resolver),
|
|
||||||
Domains: []string{baseDomain},
|
|
||||||
Proxied: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
peersOfMachine1Shared1, err := app.getPeers(machineInShared1)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
dnsConfig := getMapResponseDNSConfig(
|
|
||||||
&dnsConfigOrig,
|
|
||||||
baseDomain,
|
|
||||||
*machineInShared1,
|
|
||||||
peersOfMachine1Shared1,
|
|
||||||
)
|
|
||||||
c.Assert(dnsConfig, check.NotNil)
|
|
||||||
c.Assert(len(dnsConfig.Routes), check.Equals, 0)
|
|
||||||
c.Assert(len(dnsConfig.Domains), check.Equals, 1)
|
|
||||||
}
|
|
@@ -1,56 +0,0 @@
|
|||||||
# 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)
|
|
||||||
- [Configuring OIDC](oidc.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)
|
|
||||||
- [Set Custom DNS records](dns-records.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 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.
|
|
||||||
|
|
||||||
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.
|
|
13
docs/acls.md
13
docs/acls.md
@@ -1,4 +1,15 @@
|
|||||||
# ACLs use case example
|
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 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).
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
# Setting custom DNS records
|
# 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
|
## Goal
|
||||||
|
|
||||||
This documentation has the goal of showing how a user can set custom DNS records with `headscale`s magic dns.
|
This documentation has the goal of showing how a user can set custom DNS records with `headscale`s magic dns.
|
||||||
|
@@ -14,6 +14,8 @@ If the node is already registered, it can advertise exit capabilities like this:
|
|||||||
$ sudo tailscale set --advertise-exit-node
|
$ sudo tailscale set --advertise-exit-node
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To use a node as an exit node, IP forwarding must be enabled on the node. Check the official [Tailscale documentation](https://tailscale.com/kb/1019/subnets/?tab=linux#enable-ip-forwarding) for how to enable IP fowarding.
|
||||||
|
|
||||||
## On the control server
|
## On the control server
|
||||||
|
|
||||||
```console
|
```console
|
||||||
|
53
docs/faq.md
Normal file
53
docs/faq.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
hide:
|
||||||
|
- navigation
|
||||||
|
---
|
||||||
|
|
||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
## What is the design goal of headscale?
|
||||||
|
|
||||||
|
`headscale` aims to implement a self-hosted, open source alternative to the [Tailscale](https://tailscale.com/)
|
||||||
|
control server.
|
||||||
|
`headscale`'s goal is to provide self-hosters and hobbyists with an open-source
|
||||||
|
server they can use for their projects and labs.
|
||||||
|
It implements a narrow scope, a _single_ Tailnet, suitable for a personal use, or a small
|
||||||
|
open-source organisation.
|
||||||
|
|
||||||
|
## How can I contribute?
|
||||||
|
|
||||||
|
Headscale is "Open Source, acknowledged contribution", this means that any
|
||||||
|
contribution will have to be discussed with the Maintainers before being submitted.
|
||||||
|
|
||||||
|
Headscale is open to code contributions for bug fixes without discussion.
|
||||||
|
|
||||||
|
If you find mistakes in the documentation, please also submit a fix to the documentation.
|
||||||
|
|
||||||
|
## Why is 'acknowledged contribution' the chosen model?
|
||||||
|
|
||||||
|
Both maintainers have full-time jobs and families, and we want to avoid burnout. We also want to avoid frustration from contributors when their PRs are not accepted.
|
||||||
|
|
||||||
|
We are more than happy to exchange emails, or to have dedicated calls before a PR is submitted.
|
||||||
|
|
||||||
|
## When/Why is Feature X going to be implemented?
|
||||||
|
|
||||||
|
We don't know. We might be working on it. If you want to help, please send us a PR.
|
||||||
|
|
||||||
|
Please be aware that there are a number of reasons why we might not accept specific contributions:
|
||||||
|
|
||||||
|
- It is not possible to implement the feature in a way that makes sense in a self-hosted environment.
|
||||||
|
- Given that we are reverse-engineering Tailscale to satify our own curiosity, we might be interested in implementing the feature ourselves.
|
||||||
|
- You are not sending unit and integration tests with it.
|
||||||
|
|
||||||
|
## Do you support Y method of deploying Headscale?
|
||||||
|
|
||||||
|
We currently support deploying `headscale` using our binaries and the DEB packages. Both can be found in the
|
||||||
|
[GitHub releases page](https://github.com/juanfont/headscale/releases).
|
||||||
|
|
||||||
|
In addition to that, there are semi-official RPM packages by the Fedora infra team https://copr.fedorainfracloud.org/coprs/jonathanspw/headscale/
|
||||||
|
|
||||||
|
For convenience, we also build Docker images with `headscale`. But **please be aware that we don't officially support deploying `headscale` using Docker**. We have a [Discord channel](https://discord.com/channels/896711691637780480/1070619770942148618) where you can ask for Docker-specific help to the community.
|
||||||
|
|
||||||
|
## Why is my reverse proxy not working with Headscale?
|
||||||
|
|
||||||
|
We don't know. We don't use reverse proxies with `headscale` ourselves, so we don't have any experience with them. We have [community documentation](https://headscale.net/reverse-proxy/) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community.
|
@@ -12,6 +12,11 @@ Ensure that the installed version is at least 1.38.1, as that is the first relea
|
|||||||
|
|
||||||
## Configuring the headscale URL
|
## Configuring the headscale URL
|
||||||
|
|
||||||
|
!!! info "Apple devices"
|
||||||
|
|
||||||
|
An endpoint with information on how to connect your Apple devices
|
||||||
|
(currently macOS only) is available at `/apple` on your running instance.
|
||||||
|
|
||||||
Ensure that the tailscale app is logged out before proceeding.
|
Ensure that the tailscale app is logged out before proceeding.
|
||||||
|
|
||||||
Go to iOS settings, scroll down past game center and tv provider to the tailscale app and select it. The headscale URL can be entered into the _"ALTERNATE COORDINATION SERVER URL"_ box.
|
Go to iOS settings, scroll down past game center and tv provider to the tailscale app and select it. The headscale URL can be entered into the _"ALTERNATE COORDINATION SERVER URL"_ box.
|
||||||
|
43
docs/index.md
Normal file
43
docs/index.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
hide:
|
||||||
|
- navigation
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
# headscale
|
||||||
|
|
||||||
|
`headscale` is an open source, self-hosted implementation of the Tailscale control server.
|
||||||
|
|
||||||
|
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](/faq/).
|
||||||
|
|
||||||
|
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat and community support.
|
||||||
|
|
||||||
|
## Design goal
|
||||||
|
|
||||||
|
Headscale aims to implement a self-hosted, open source alternative to the Tailscale
|
||||||
|
control server.
|
||||||
|
Headscale's goal is to provide self-hosters and hobbyists with an open-source
|
||||||
|
server they can use for their projects and labs.
|
||||||
|
It implements a narrower scope, a single Tailnet, suitable for a personal use, or a small
|
||||||
|
open-source organisation.
|
||||||
|
|
||||||
|
## Supporting headscale
|
||||||
|
|
||||||
|
If you like `headscale` and find it useful, there is a sponsorship and donation
|
||||||
|
buttons available in the repo.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Headscale is "Open Source, acknowledged contribution", this means that any
|
||||||
|
contribution will have to be discussed with the Maintainers before being submitted.
|
||||||
|
|
||||||
|
This model has been chosen to reduce the risk of burnout by limiting the
|
||||||
|
maintenance overhead of reviewing and validating third-party code.
|
||||||
|
|
||||||
|
Headscale is open to code contributions for bug fixes without discussion.
|
||||||
|
|
||||||
|
If you find mistakes in the documentation, please submit a fix to the documentation.
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
`headscale` is maintained by [Kristoffer Dalby](https://kradalby.no/) and [Juan Font](https://font.eu).
|
5
docs/packaging/README.md
Normal file
5
docs/packaging/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Packaging
|
||||||
|
|
||||||
|
We use [nFPM](https://nfpm.goreleaser.com/) for making `.deb`, `.rpm` and `.apk`.
|
||||||
|
|
||||||
|
This folder contains files we need to package with these releases.
|
52
docs/packaging/headscale.systemd.service
Normal file
52
docs/packaging/headscale.systemd.service
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
[Unit]
|
||||||
|
After=syslog.target
|
||||||
|
After=network.target
|
||||||
|
Description=headscale coordination server for Tailscale
|
||||||
|
X-Restart-Triggers=/etc/headscale/config.yaml
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=headscale
|
||||||
|
Group=headscale
|
||||||
|
ExecStart=/usr/bin/headscale serve
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
WorkingDirectory=/var/lib/headscale
|
||||||
|
ReadWritePaths=/var/lib/headscale /var/run
|
||||||
|
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN
|
||||||
|
LockPersonality=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
PrivateMounts=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProcSubset=pid
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectHome=yes
|
||||||
|
ProtectHostname=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectProc=invisible
|
||||||
|
ProtectSystem=strict
|
||||||
|
RemoveIPC=true
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RuntimeDirectory=headscale
|
||||||
|
RuntimeDirectoryMode=0750
|
||||||
|
StateDirectory=headscale
|
||||||
|
StateDirectoryMode=0750
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
SystemCallFilter=@chown
|
||||||
|
SystemCallFilter=@system-service
|
||||||
|
SystemCallFilter=~@privileged
|
||||||
|
UMask=0077
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
86
docs/packaging/postinstall.sh
Normal file
86
docs/packaging/postinstall.sh
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Determine OS platform
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. /etc/os-release
|
||||||
|
|
||||||
|
HEADSCALE_EXE="/usr/bin/headscale"
|
||||||
|
BSD_HIER=""
|
||||||
|
HEADSCALE_RUN_DIR="/var/run/headscale"
|
||||||
|
HEADSCALE_USER="headscale"
|
||||||
|
HEADSCALE_GROUP="headscale"
|
||||||
|
|
||||||
|
ensure_sudo() {
|
||||||
|
if [ "$(id -u)" = "0" ]; then
|
||||||
|
echo "Sudo permissions detected"
|
||||||
|
else
|
||||||
|
echo "No sudo permission detected, please run as sudo"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_headscale_path() {
|
||||||
|
if [ ! -f "$HEADSCALE_EXE" ]; then
|
||||||
|
echo "headscale not in default path, exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "Found headscale %s\n" "$HEADSCALE_EXE"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_headscale_user() {
|
||||||
|
printf "PostInstall: Adding headscale user %s\n" "$HEADSCALE_USER"
|
||||||
|
useradd -s /bin/sh -c "headscale default user" headscale
|
||||||
|
}
|
||||||
|
|
||||||
|
create_headscale_group() {
|
||||||
|
if command -V systemctl >/dev/null 2>&1; then
|
||||||
|
printf "PostInstall: Adding headscale group %s\n" "$HEADSCALE_GROUP"
|
||||||
|
groupadd "$HEADSCALE_GROUP"
|
||||||
|
|
||||||
|
printf "PostInstall: Adding headscale user %s to group %s\n" "$HEADSCALE_USER" "$HEADSCALE_GROUP"
|
||||||
|
usermod -a -G "$HEADSCALE_GROUP" "$HEADSCALE_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ID" = "alpine" ]; then
|
||||||
|
printf "PostInstall: Adding headscale group %s\n" "$HEADSCALE_GROUP"
|
||||||
|
addgroup "$HEADSCALE_GROUP"
|
||||||
|
|
||||||
|
printf "PostInstall: Adding headscale user %s to group %s\n" "$HEADSCALE_USER" "$HEADSCALE_GROUP"
|
||||||
|
addgroup "$HEADSCALE_USER" "$HEADSCALE_GROUP"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_run_dir() {
|
||||||
|
printf "PostInstall: Creating headscale run directory \n"
|
||||||
|
mkdir -p "$HEADSCALE_RUN_DIR"
|
||||||
|
|
||||||
|
printf "PostInstall: Modifying group ownership of headscale run directory \n"
|
||||||
|
chown "$HEADSCALE_USER":"$HEADSCALE_GROUP" "$HEADSCALE_RUN_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
summary() {
|
||||||
|
echo "----------------------------------------------------------------------"
|
||||||
|
echo " headscale package has been successfully installed."
|
||||||
|
echo ""
|
||||||
|
echo " Please follow the next steps to start the software:"
|
||||||
|
echo ""
|
||||||
|
echo " sudo systemctl enable headscale"
|
||||||
|
echo " sudo systemctl start headscale"
|
||||||
|
echo ""
|
||||||
|
echo " Configuration settings can be adjusted here:"
|
||||||
|
echo " ${BSD_HIER}/etc/headscale/config.yaml"
|
||||||
|
echo ""
|
||||||
|
echo "----------------------------------------------------------------------"
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Main body of the script
|
||||||
|
#
|
||||||
|
{
|
||||||
|
ensure_sudo
|
||||||
|
ensure_headscale_path
|
||||||
|
create_headscale_user
|
||||||
|
create_headscale_group
|
||||||
|
create_run_dir
|
||||||
|
summary
|
||||||
|
}
|
15
docs/packaging/postremove.sh
Normal file
15
docs/packaging/postremove.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Determine OS platform
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. /etc/os-release
|
||||||
|
|
||||||
|
if command -V systemctl >/dev/null 2>&1; then
|
||||||
|
echo "Stop and disable headscale service"
|
||||||
|
systemctl stop headscale >/dev/null 2>&1 || true
|
||||||
|
systemctl disable headscale >/dev/null 2>&1 || true
|
||||||
|
echo "Running daemon-reload"
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Removing run directory"
|
||||||
|
rm -rf "/var/run/headscale.sock"
|
@@ -1,5 +1,12 @@
|
|||||||
# Running headscale behind a reverse proxy
|
# Running headscale behind a reverse proxy
|
||||||
|
|
||||||
|
!!! 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**.
|
||||||
|
|
||||||
Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.
|
Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.
|
||||||
|
|
||||||
### WebSockets
|
### WebSockets
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
# Running headscale in a container
|
# Running headscale in a container
|
||||||
|
|
||||||
**Note:** the container documentation is maintained by the _community_ and there is no guarentee
|
!!! warning "Community documentation"
|
||||||
it is up to date, or working.
|
|
||||||
|
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
|
## Goal
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ cd ./headscale
|
|||||||
touch ./config/db.sqlite
|
touch ./config/db.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **(Strongly Recommended)** Download a copy of the [example configuration](../config-example.yaml) from the [headscale repository](https://github.com/juanfont/headscale/).
|
3. **(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
|
||||||
|
|
||||||
Using wget:
|
Using wget:
|
||||||
|
|
||||||
|
198
docs/running-headscale-linux-manual.md
Normal file
198
docs/running-headscale-linux-manual.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Running headscale on Linux
|
||||||
|
|
||||||
|
## Note: Outdated and "advanced"
|
||||||
|
|
||||||
|
This documentation is considered the "legacy"/advanced/manual version of the documentation, you most likely do not
|
||||||
|
want to use this documentation and rather look at the distro specific documentation (TODO LINK)[].
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
This documentation has the goal of showing a user how-to set up and run `headscale` on Linux.
|
||||||
|
In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd)
|
||||||
|
describing how to make `headscale` run properly in a server environment.
|
||||||
|
|
||||||
|
## Configure and run `headscale`
|
||||||
|
|
||||||
|
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
wget --output-document=/usr/local/bin/headscale \
|
||||||
|
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make `headscale` executable:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
chmod +x /usr/local/bin/headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Directory for configuration
|
||||||
|
|
||||||
|
mkdir -p /etc/headscale
|
||||||
|
|
||||||
|
# Directory for Database, and other variable data (like certificates)
|
||||||
|
mkdir -p /var/lib/headscale
|
||||||
|
# or if you create a headscale user:
|
||||||
|
useradd \
|
||||||
|
--create-home \
|
||||||
|
--home-dir /var/lib/headscale/ \
|
||||||
|
--system \
|
||||||
|
--user-group \
|
||||||
|
--shell /usr/bin/nologin \
|
||||||
|
headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create an empty SQLite database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /var/lib/headscale/db.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Create a `headscale` configuration:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /etc/headscale/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
|
||||||
|
|
||||||
|
6. Start the headscale server:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale serve
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will start `headscale` in the current terminal session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To continue the tutorial, open a new terminal and let it run in the background.
|
||||||
|
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/).
|
||||||
|
|
||||||
|
To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing.
|
||||||
|
|
||||||
|
7. Verify `headscale` is running:
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale users create myfirstuser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register a machine (normal login)
|
||||||
|
|
||||||
|
On a client machine, execute the `tailscale` login command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server YOUR_HEADSCALE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
Register the machine:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --user myfirstuser nodes register --key <YOUR_MACHINE_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register machine using a pre authenticated key
|
||||||
|
|
||||||
|
Generate a key using the command line:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
|
||||||
|
```
|
||||||
|
|
||||||
|
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running `headscale` in the background with SystemD
|
||||||
|
|
||||||
|
:warning: **Deprecated**: This part is very outdated and you should use the [pre-packaged Headscale for this](./running-headscale-linux.md
|
||||||
|
|
||||||
|
This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/).
|
||||||
|
This should work on most modern Linux distributions.
|
||||||
|
|
||||||
|
1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing:
|
||||||
|
|
||||||
|
```systemd
|
||||||
|
[Unit]
|
||||||
|
Description=headscale controller
|
||||||
|
After=syslog.target
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=headscale
|
||||||
|
Group=headscale
|
||||||
|
ExecStart=/usr/local/bin/headscale serve
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# Optional security enhancements
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
WorkingDirectory=/var/lib/headscale
|
||||||
|
ReadWritePaths=/var/lib/headscale /var/run/headscale
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
RuntimeDirectory=headscale
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that when running as the headscale user ensure that, either you add your current user to the headscale group:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
usermod -a -G headscale current_user
|
||||||
|
```
|
||||||
|
|
||||||
|
or run all headscale commands as the headscale user:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
su - headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
unix_socket: /var/run/headscale/headscale.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Reload SystemD to load the new configuration file:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Enable and start the new `headscale` service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl enable --now headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Verify the headscale service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl status headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
`headscale` will now run in the background and start at boot.
|
@@ -1,84 +1,65 @@
|
|||||||
# Running headscale on Linux
|
# Running headscale on Linux
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Ubuntu 20.04 or newer, Debian 11 or newer.
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
This documentation has the goal of showing a user how-to set up and run `headscale` on Linux.
|
Get Headscale up and running.
|
||||||
In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd)
|
|
||||||
describing how to make `headscale` run properly in a server environment.
|
|
||||||
|
|
||||||
## Configure and run `headscale`
|
This includes running Headscale with SystemD.
|
||||||
|
|
||||||
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
|
## Migrating from manual install
|
||||||
|
|
||||||
|
If you are migrating from the old manual install, the best thing would be to remove
|
||||||
|
the files installed by following [the guide in reverse](./running-headscale-linux-manual.md).
|
||||||
|
|
||||||
|
You should _not_ delete the database (`/var/headscale/db.sqlite`) and the
|
||||||
|
configuration (`/etc/headscale/config.yaml`).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page](https://github.com/juanfont/headscale/releases):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
wget --output-document=/usr/local/bin/headscale \
|
wget --output-document=headscale.deb \
|
||||||
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>
|
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Make `headscale` executable:
|
2. Install Headscale:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
chmod +x /usr/local/bin/headscale
|
sudo dpkg --install headscale.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
|
3. Enable Headscale service, this will start Headscale at boot:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Directory for configuration
|
sudo systemctl enable headscale
|
||||||
|
|
||||||
mkdir -p /etc/headscale
|
|
||||||
|
|
||||||
# Directory for Database, and other variable data (like certificates)
|
|
||||||
mkdir -p /var/lib/headscale
|
|
||||||
# or if you create a headscale user:
|
|
||||||
useradd \
|
|
||||||
--create-home \
|
|
||||||
--home-dir /var/lib/headscale/ \
|
|
||||||
--system \
|
|
||||||
--user-group \
|
|
||||||
--shell /usr/bin/nologin \
|
|
||||||
headscale
|
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create an empty SQLite database:
|
4. Configure Headscale by editing the configuration file:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
touch /var/lib/headscale/db.sqlite
|
nano /etc/headscale/config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Create a `headscale` configuration:
|
5. Start Headscale:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
touch /etc/headscale/config.yaml
|
sudo systemctl start headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
It is **strongly recommended** to copy and modify the [example configuration](../config-example.yaml)
|
6. Check that Headscale is running as intended:
|
||||||
from the [headscale repository](../)
|
|
||||||
|
|
||||||
6. Start the headscale server:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale serve
|
systemctl status headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will start `headscale` in the current terminal session.
|
## Using Headscale
|
||||||
|
|
||||||
---
|
### Create a user
|
||||||
|
|
||||||
To continue the tutorial, open a new terminal and let it run in the background.
|
|
||||||
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/).
|
|
||||||
|
|
||||||
To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing.
|
|
||||||
|
|
||||||
7. Verify `headscale` is running:
|
|
||||||
|
|
||||||
Verify `headscale` is available:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
curl http://127.0.0.1:9090/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
8. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale users create myfirstuser
|
headscale users create myfirstuser
|
||||||
@@ -86,16 +67,16 @@ headscale users create myfirstuser
|
|||||||
|
|
||||||
### Register a machine (normal login)
|
### Register a machine (normal login)
|
||||||
|
|
||||||
On a client machine, execute the `tailscale` login command:
|
On a client machine, run the `tailscale` login command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tailscale up --login-server YOUR_HEADSCALE_URL
|
tailscale up --login-server <YOUR_HEADSCALE_URL>
|
||||||
```
|
```
|
||||||
|
|
||||||
Register the machine:
|
Register the machine:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale --user myfirstuser nodes register --key <YOU_+MACHINE_KEY>
|
headscale --user myfirstuser nodes register --key <YOUR_MACHINE_KEY>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Register machine using a pre authenticated key
|
### Register machine using a pre authenticated key
|
||||||
@@ -106,87 +87,9 @@ Generate a key using the command line:
|
|||||||
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
|
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
|
||||||
```
|
```
|
||||||
|
|
||||||
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
|
This will return a pre-authenticated key that is used to
|
||||||
|
connect a node to `headscale` during the `tailscale` command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running `headscale` in the background with SystemD
|
|
||||||
|
|
||||||
This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/).
|
|
||||||
This should work on most modern Linux distributions.
|
|
||||||
|
|
||||||
1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing:
|
|
||||||
|
|
||||||
```systemd
|
|
||||||
[Unit]
|
|
||||||
Description=headscale controller
|
|
||||||
After=syslog.target
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=headscale
|
|
||||||
Group=headscale
|
|
||||||
ExecStart=/usr/local/bin/headscale serve
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
# Optional security enhancements
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
PrivateTmp=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=yes
|
|
||||||
WorkingDirectory=/var/lib/headscale
|
|
||||||
ReadWritePaths=/var/lib/headscale /var/run/headscale
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
||||||
RuntimeDirectory=headscale
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that when running as the headscale user ensure that, either you add your current user to the headscale group:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
usermod -a -G headscale current_user
|
|
||||||
```
|
|
||||||
|
|
||||||
or run all headscale commands as the headscale user:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
su - headscale
|
|
||||||
```
|
|
||||||
|
|
||||||
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
unix_socket: /var/run/headscale/headscale.sock
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Reload SystemD to load the new configuration file:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl daemon-reload
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Enable and start the new `headscale` service:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl enable --now headscale
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Verify the headscale service:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl status headscale
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify `headscale` is available:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
curl http://127.0.0.1:9090/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
`headscale` will now run in the background and start at boot.
|
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
# Running headscale on OpenBSD
|
# Running headscale on OpenBSD
|
||||||
|
|
||||||
|
!!! 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
|
## Goal
|
||||||
|
|
||||||
This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD 7.1.
|
This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD 7.1.
|
||||||
@@ -43,7 +50,7 @@ cp headscale /usr/local/sbin
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Install prerequistes
|
# Install prerequistes
|
||||||
# 1. go v1.19+: headscale newer than 0.17 needs go 1.19+ to compile
|
# 1. go v1.20+: headscale newer than 0.21 needs go 1.20+ to compile
|
||||||
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
||||||
|
|
||||||
git clone https://github.com/juanfont/headscale.git
|
git clone https://github.com/juanfont/headscale.git
|
||||||
@@ -87,8 +94,7 @@ touch /var/lib/headscale/db.sqlite
|
|||||||
touch /etc/headscale/config.yaml
|
touch /etc/headscale/config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
It is **strongly recommended** to copy and modify the [example configuration](../config-example.yaml)
|
**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
|
||||||
from the [headscale repository](../)
|
|
||||||
|
|
||||||
4. Start the headscale server:
|
4. Start the headscale server:
|
||||||
|
|
||||||
|
14
docs/web-ui.md
Normal file
14
docs/web-ui.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Headscale web interface
|
||||||
|
|
||||||
|
!!! warning "Community contributions"
|
||||||
|
|
||||||
|
This page contains community contributions. The projects listed here are not
|
||||||
|
maintained by the Headscale authors and are written by community members.
|
||||||
|
|
||||||
|
| Name | Repository Link | Description | Status |
|
||||||
|
| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------- | ------ |
|
||||||
|
| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple Headscale web UI for small-scale deployments. | Alpha |
|
||||||
|
| headscale-ui | [Github](https://github.com/gurucomputing/headscale-ui) | A web frontend for the headscale Tailscale-compatible coordination server | Alpha |
|
||||||
|
| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend enviroment required | Alpha |
|
||||||
|
|
||||||
|
You can ask for support on our dedicated [Discord channel](https://discord.com/channels/896711691637780480/1105842846386356294).
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user