mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-17 06:19:51 +02:00
Compare commits
52 Commits
v0.22.0-al
...
revert-141
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f90ebb8567 | ||
|
|
7564c4548c | ||
|
|
5f80d1b155 | ||
|
|
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 |
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)
|
||||||
|
|||||||
26
.github/renovate.json
vendored
26
.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"
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
|
||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
63
.github/workflows/test-integration-v2-TestDERPServerScenario.yaml
vendored
Normal file
63
.github/workflows/test-integration-v2-TestDERPServerScenario.yaml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^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"
|
||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -55,3 +55,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"
|
||||||
|
|||||||
@@ -32,17 +32,7 @@ builds:
|
|||||||
|
|
||||||
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
|
|
||||||
- freebsd-amd64
|
|
||||||
- linux-386
|
|
||||||
- linux-amd64
|
|
||||||
- linux-arm64
|
|
||||||
- linux-arm-5
|
|
||||||
- linux-arm-6
|
|
||||||
- linux-arm-7
|
|
||||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
|
||||||
format: binary
|
format: binary
|
||||||
|
|
||||||
source:
|
source:
|
||||||
@@ -81,7 +71,7 @@ nfpms:
|
|||||||
file_info:
|
file_info:
|
||||||
mode: 0644
|
mode: 0644
|
||||||
- src: ./docs/packaging/headscale.systemd.service
|
- src: ./docs/packaging/headscale.systemd.service
|
||||||
dst: /etc/systemd/system/headscale.service
|
dst: /usr/lib/systemd/system/headscale.service
|
||||||
- dst: /var/lib/headscale
|
- dst: /var/lib/headscale
|
||||||
type: dir
|
type: dir
|
||||||
- dst: /var/run/headscale
|
- dst: /var/run/headscale
|
||||||
|
|||||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -4,6 +4,19 @@
|
|||||||
|
|
||||||
### Changes
|
### 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)
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
- Disable (or delete) both exit routes at the same time [#1428](https://github.com/juanfont/headscale/pull/1428)
|
||||||
|
|
||||||
## 0.22.0 (2023-04-20)
|
## 0.22.0 (2023-04-20)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -38,16 +38,6 @@ test_integration_cli:
|
|||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
||||||
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
|
go run gotest.tools/gotestsum@latest -- $(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 run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
|
|
||||||
|
|
||||||
test_integration_v2_general:
|
test_integration_v2_general:
|
||||||
docker run \
|
docker run \
|
||||||
-t --rm \
|
-t --rm \
|
||||||
|
|||||||
39
README.md
39
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,7 +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.**
|
||||||
|
|
||||||
|
Please have a look at the [`documentation`](https://headscale.net/).
|
||||||
|
|
||||||
## Graphical Control Panels
|
## Graphical Control Panels
|
||||||
|
|
||||||
@@ -97,11 +96,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 +120,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,
|
||||||
|
|||||||
495
acls.go
495
acls.go
@@ -13,7 +13,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/samber/lo"
|
|
||||||
"github.com/tailscale/hujson"
|
"github.com/tailscale/hujson"
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -128,21 +127,14 @@ func (h *Headscale) UpdateACLRules() error {
|
|||||||
return errEmptyPolicy
|
return errEmptyPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
rules, err := generateACLRules(machines, *h.aclPolicy, h.cfg.OIDC.StripEmaildomain)
|
rules, err := h.aclPolicy.generateFilterRules(machines, h.cfg.OIDC.StripEmaildomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
||||||
h.aclRules = rules
|
h.aclRules = rules
|
||||||
|
|
||||||
// Precompute a map of which sources can reach each destination, this is
|
|
||||||
// to provide quicker lookup when we calculate the peerlist for the map
|
|
||||||
// response to nodes.
|
|
||||||
aclPeerCacheMap := generateACLPeerCacheMap(rules)
|
|
||||||
h.aclPeerCacheMapRW.Lock()
|
|
||||||
h.aclPeerCacheMap = aclPeerCacheMap
|
|
||||||
h.aclPeerCacheMapRW.Unlock()
|
|
||||||
|
|
||||||
if featureEnableSSH() {
|
if featureEnableSSH() {
|
||||||
sshRules, err := h.generateSSHRules()
|
sshRules, err := h.generateSSHRules()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -160,91 +152,28 @@ func (h *Headscale) UpdateACLRules() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateACLPeerCacheMap takes a list of Tailscale filter rules and generates a map
|
// generateFilterRules takes a set of machines and an ACLPolicy and generates a
|
||||||
// of which Sources ("*" and IPs) can access destinations. This is to speed up the
|
// set of Tailscale compatible FilterRules used to allow traffic on clients.
|
||||||
// process of generating MapResponses when deciding which Peers to inform nodes about.
|
func (pol *ACLPolicy) generateFilterRules(
|
||||||
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,
|
machines []Machine,
|
||||||
aclPolicy ACLPolicy,
|
stripEmailDomain bool,
|
||||||
stripEmaildomain bool,
|
|
||||||
) ([]tailcfg.FilterRule, error) {
|
) ([]tailcfg.FilterRule, error) {
|
||||||
rules := []tailcfg.FilterRule{}
|
rules := []tailcfg.FilterRule{}
|
||||||
|
|
||||||
for index, acl := range aclPolicy.ACLs {
|
for index, acl := range pol.ACLs {
|
||||||
if acl.Action != "accept" {
|
if acl.Action != "accept" {
|
||||||
return nil, errInvalidAction
|
return nil, errInvalidAction
|
||||||
}
|
}
|
||||||
|
|
||||||
srcIPs := []string{}
|
srcIPs := []string{}
|
||||||
for innerIndex, src := range acl.Sources {
|
for srcIndex, src := range acl.Sources {
|
||||||
srcs, err := generateACLPolicySrc(machines, aclPolicy, src, stripEmaildomain)
|
srcs, err := pol.getIPsFromSource(src, machines, stripEmailDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
|
Interface("src", src).
|
||||||
|
Int("ACL index", index).
|
||||||
|
Int("Src index", srcIndex).
|
||||||
|
Msgf("Error parsing ACL")
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -260,17 +189,19 @@ func generateACLRules(
|
|||||||
}
|
}
|
||||||
|
|
||||||
destPorts := []tailcfg.NetPortRange{}
|
destPorts := []tailcfg.NetPortRange{}
|
||||||
for innerIndex, dest := range acl.Destinations {
|
for destIndex, dest := range acl.Destinations {
|
||||||
dests, err := generateACLPolicyDest(
|
dests, err := pol.getNetPortRangeFromDestination(
|
||||||
machines,
|
|
||||||
aclPolicy,
|
|
||||||
dest,
|
dest,
|
||||||
|
machines,
|
||||||
needsWildcard,
|
needsWildcard,
|
||||||
stripEmaildomain,
|
stripEmailDomain,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
|
Interface("dest", dest).
|
||||||
|
Int("ACL index", index).
|
||||||
|
Int("dest index", destIndex).
|
||||||
|
Msgf("Error parsing ACL")
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -341,22 +272,41 @@ func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) {
|
|||||||
|
|
||||||
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
|
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
|
||||||
for innerIndex, rawSrc := range sshACL.Sources {
|
for innerIndex, rawSrc := range sshACL.Sources {
|
||||||
expandedSrcs, err := expandAlias(
|
if isWildcard(rawSrc) {
|
||||||
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{
|
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||||
NodeIP: expandedSrc,
|
Any: true,
|
||||||
})
|
})
|
||||||
|
} else if isGroup(rawSrc) {
|
||||||
|
users, err := h.aclPolicy.getUsersInGroup(rawSrc, h.cfg.OIDC.StripEmaildomain)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Msgf("Error parsing SSH %d, Source %d", index, innerIndex)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||||
|
UserLogin: user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expandedSrcs, err := h.aclPolicy.expandAlias(
|
||||||
|
machines,
|
||||||
|
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.Prefixes() {
|
||||||
|
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||||
|
NodeIP: expandedSrc.Addr().String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,10 +315,9 @@ func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) {
|
|||||||
userMap[user] = "="
|
userMap[user] = "="
|
||||||
}
|
}
|
||||||
rules = append(rules, &tailcfg.SSHRule{
|
rules = append(rules, &tailcfg.SSHRule{
|
||||||
RuleExpires: nil,
|
Principals: principals,
|
||||||
Principals: principals,
|
SSHUsers: userMap,
|
||||||
SSHUsers: userMap,
|
Action: &action,
|
||||||
Action: &action,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,19 +341,32 @@ func sshCheckAction(duration string) (*tailcfg.SSHAction, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateACLPolicySrc(
|
// getIPsFromSource returns a set of Source IPs that would be associated
|
||||||
machines []Machine,
|
// with the given src alias.
|
||||||
aclPolicy ACLPolicy,
|
func (pol *ACLPolicy) getIPsFromSource(
|
||||||
src string,
|
src string,
|
||||||
|
machines []Machine,
|
||||||
stripEmaildomain bool,
|
stripEmaildomain bool,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
return expandAlias(machines, aclPolicy, src, stripEmaildomain)
|
ipSet, err := pol.expandAlias(machines, src, stripEmaildomain)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixes := []string{}
|
||||||
|
|
||||||
|
for _, prefix := range ipSet.Prefixes() {
|
||||||
|
prefixes = append(prefixes, prefix.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefixes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateACLPolicyDest(
|
// getNetPortRangeFromDestination returns a set of tailcfg.NetPortRange
|
||||||
machines []Machine,
|
// which are associated with the dest alias.
|
||||||
aclPolicy ACLPolicy,
|
func (pol *ACLPolicy) getNetPortRangeFromDestination(
|
||||||
dest string,
|
dest string,
|
||||||
|
machines []Machine,
|
||||||
needsWildcard bool,
|
needsWildcard bool,
|
||||||
stripEmaildomain bool,
|
stripEmaildomain bool,
|
||||||
) ([]tailcfg.NetPortRange, error) {
|
) ([]tailcfg.NetPortRange, error) {
|
||||||
@@ -451,9 +413,8 @@ func generateACLPolicyDest(
|
|||||||
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
expanded, err := expandAlias(
|
expanded, err := pol.expandAlias(
|
||||||
machines,
|
machines,
|
||||||
aclPolicy,
|
|
||||||
alias,
|
alias,
|
||||||
stripEmaildomain,
|
stripEmaildomain,
|
||||||
)
|
)
|
||||||
@@ -466,11 +427,11 @@ func generateACLPolicyDest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
dests := []tailcfg.NetPortRange{}
|
dests := []tailcfg.NetPortRange{}
|
||||||
for _, d := range expanded {
|
for _, dest := range expanded.Prefixes() {
|
||||||
for _, p := range *ports {
|
for _, port := range *ports {
|
||||||
pr := tailcfg.NetPortRange{
|
pr := tailcfg.NetPortRange{
|
||||||
IP: d,
|
IP: dest.String(),
|
||||||
Ports: p,
|
Ports: port,
|
||||||
}
|
}
|
||||||
dests = append(dests, pr)
|
dests = append(dests, pr)
|
||||||
}
|
}
|
||||||
@@ -537,135 +498,64 @@ func parseProtocol(protocol string) ([]int, bool, error) {
|
|||||||
// - an ip
|
// - an ip
|
||||||
// - a cidr
|
// - a cidr
|
||||||
// and transform these in IPAddresses.
|
// and transform these in IPAddresses.
|
||||||
func expandAlias(
|
func (pol *ACLPolicy) expandAlias(
|
||||||
machines Machines,
|
machines Machines,
|
||||||
aclPolicy ACLPolicy,
|
|
||||||
alias string,
|
alias string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
) ([]string, error) {
|
) (*netipx.IPSet, error) {
|
||||||
ips := []string{}
|
if isWildcard(alias) {
|
||||||
if alias == "*" {
|
return parseIPSet("*", nil)
|
||||||
return []string{"*"}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
build := netipx.IPSetBuilder{}
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("alias", alias).
|
Str("alias", alias).
|
||||||
Msg("Expanding")
|
Msg("Expanding")
|
||||||
|
|
||||||
if strings.HasPrefix(alias, "group:") {
|
// if alias is a group
|
||||||
users, err := expandGroup(aclPolicy, alias, stripEmailDomain)
|
if isGroup(alias) {
|
||||||
if err != nil {
|
return pol.getIPsFromGroup(alias, machines, stripEmailDomain)
|
||||||
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:") {
|
// if alias is a tag
|
||||||
// check for forced tags
|
if isTag(alias) {
|
||||||
for _, machine := range machines {
|
return pol.getIPsFromTag(alias, machines, stripEmailDomain)
|
||||||
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
|
// if alias is a user
|
||||||
nodes := filterMachinesByUser(machines, alias)
|
if ips, err := pol.getIPsForUser(alias, machines, stripEmailDomain); ips != nil {
|
||||||
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain)
|
return ips, err
|
||||||
|
|
||||||
for _, n := range nodes {
|
|
||||||
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
|
||||||
}
|
|
||||||
if len(ips) > 0 {
|
|
||||||
return ips, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an host
|
// if alias is an host
|
||||||
if h, ok := aclPolicy.Hosts[alias]; ok {
|
// Note, this is recursive.
|
||||||
|
if h, ok := pol.Hosts[alias]; ok {
|
||||||
log.Trace().Str("host", h.String()).Msg("expandAlias got hosts entry")
|
log.Trace().Str("host", h.String()).Msg("expandAlias got hosts entry")
|
||||||
|
|
||||||
return expandAlias(machines, aclPolicy, h.String(), stripEmailDomain)
|
return pol.expandAlias(machines, h.String(), stripEmailDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an IP
|
// if alias is an IP
|
||||||
if ip, err := netip.ParseAddr(alias); err == nil {
|
if ip, err := netip.ParseAddr(alias); err == nil {
|
||||||
log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip")
|
return pol.getIPsFromSingleIP(ip, machines)
|
||||||
ips := []string{ip.String()}
|
|
||||||
matches := machines.FilterByIP(ip)
|
|
||||||
|
|
||||||
for _, machine := range matches {
|
|
||||||
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lo.Uniq(ips), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cidr, err := netip.ParsePrefix(alias); err == nil {
|
// if alias is an IP Prefix (CIDR)
|
||||||
log.Trace().Str("cidr", cidr.String()).Msg("expandAlias got cidr")
|
if prefix, err := netip.ParsePrefix(alias); err == nil {
|
||||||
val := []string{cidr.String()}
|
return pol.getIPsFromIPPrefix(prefix, machines)
|
||||||
// This is suboptimal and quite expensive, but if we only add the cidr, we will miss all the relevant IPv6
|
|
||||||
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
|
|
||||||
for _, machine := range machines {
|
|
||||||
for _, ip := range machine.IPAddresses {
|
|
||||||
// log.Trace().
|
|
||||||
// Msgf("checking if machine ip (%s) is part of cidr (%s): %v, is single ip cidr (%v), addr: %s", ip.String(), cidr.String(), cidr.Contains(ip), cidr.IsSingleIP(), cidr.Addr().String())
|
|
||||||
if cidr.Contains(ip) {
|
|
||||||
val = append(val, machine.IPAddresses.ToStringSlice()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lo.Uniq(val), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
||||||
|
|
||||||
return ips, nil
|
return build.IPSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
|
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
|
||||||
// that are correctly tagged since they should not be listed as being in the user
|
// that are correctly tagged since they should not be listed as being in the user
|
||||||
// we assume in this function that we only have nodes from 1 user.
|
// we assume in this function that we only have nodes from 1 user.
|
||||||
func excludeCorrectlyTaggedNodes(
|
func excludeCorrectlyTaggedNodes(
|
||||||
aclPolicy ACLPolicy,
|
aclPolicy *ACLPolicy,
|
||||||
nodes []Machine,
|
nodes []Machine,
|
||||||
user string,
|
user string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
@@ -673,7 +563,7 @@ func excludeCorrectlyTaggedNodes(
|
|||||||
out := []Machine{}
|
out := []Machine{}
|
||||||
tags := []string{}
|
tags := []string{}
|
||||||
for tag := range aclPolicy.TagOwners {
|
for tag := range aclPolicy.TagOwners {
|
||||||
owners, _ := expandTagOwners(aclPolicy, user, stripEmailDomain)
|
owners, _ := getTagOwners(aclPolicy, user, stripEmailDomain)
|
||||||
ns := append(owners, user)
|
ns := append(owners, user)
|
||||||
if contains(ns, user) {
|
if contains(ns, user) {
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
@@ -703,7 +593,7 @@ func excludeCorrectlyTaggedNodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
|
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
|
||||||
if portsStr == "*" {
|
if isWildcard(portsStr) {
|
||||||
return &[]tailcfg.PortRange{
|
return &[]tailcfg.PortRange{
|
||||||
{First: portRangeBegin, Last: portRangeEnd},
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -761,15 +651,15 @@ func filterMachinesByUser(machines []Machine, user string) []Machine {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// expandTagOwners will return a list of user. An owner can be either a user or a group
|
// getTagOwners will return a list of user. An owner can be either a user or a group
|
||||||
// a group cannot be composed of groups.
|
// a group cannot be composed of groups.
|
||||||
func expandTagOwners(
|
func getTagOwners(
|
||||||
aclPolicy ACLPolicy,
|
pol *ACLPolicy,
|
||||||
tag string,
|
tag string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
var owners []string
|
var owners []string
|
||||||
ows, ok := aclPolicy.TagOwners[tag]
|
ows, ok := pol.TagOwners[tag]
|
||||||
if !ok {
|
if !ok {
|
||||||
return []string{}, fmt.Errorf(
|
return []string{}, fmt.Errorf(
|
||||||
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
|
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
|
||||||
@@ -778,8 +668,8 @@ func expandTagOwners(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
for _, owner := range ows {
|
for _, owner := range ows {
|
||||||
if strings.HasPrefix(owner, "group:") {
|
if isGroup(owner) {
|
||||||
gs, err := expandGroup(aclPolicy, owner, stripEmailDomain)
|
gs, err := pol.getUsersInGroup(owner, stripEmailDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
}
|
}
|
||||||
@@ -792,15 +682,15 @@ func expandTagOwners(
|
|||||||
return owners, nil
|
return owners, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// expandGroup will return the list of user inside the group
|
// getUsersInGroup will return the list of user inside the group
|
||||||
// after some validation.
|
// after some validation.
|
||||||
func expandGroup(
|
func (pol *ACLPolicy) getUsersInGroup(
|
||||||
aclPolicy ACLPolicy,
|
|
||||||
group string,
|
group string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
outGroups := []string{}
|
users := []string{}
|
||||||
aclGroups, ok := aclPolicy.Groups[group]
|
log.Trace().Caller().Interface("pol", pol).Msg("test")
|
||||||
|
aclGroups, ok := pol.Groups[group]
|
||||||
if !ok {
|
if !ok {
|
||||||
return []string{}, fmt.Errorf(
|
return []string{}, fmt.Errorf(
|
||||||
"group %v isn't registered. %w",
|
"group %v isn't registered. %w",
|
||||||
@@ -809,7 +699,7 @@ func expandGroup(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
for _, group := range aclGroups {
|
for _, group := range aclGroups {
|
||||||
if strings.HasPrefix(group, "group:") {
|
if isGroup(group) {
|
||||||
return []string{}, fmt.Errorf(
|
return []string{}, fmt.Errorf(
|
||||||
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
|
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
|
||||||
errInvalidGroup,
|
errInvalidGroup,
|
||||||
@@ -823,8 +713,151 @@ func expandGroup(
|
|||||||
errInvalidGroup,
|
errInvalidGroup,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
outGroups = append(outGroups, grp)
|
users = append(users, grp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return outGroups, nil
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pol *ACLPolicy) getIPsFromGroup(
|
||||||
|
group string,
|
||||||
|
machines Machines,
|
||||||
|
stripEmailDomain bool,
|
||||||
|
) (*netipx.IPSet, error) {
|
||||||
|
build := netipx.IPSetBuilder{}
|
||||||
|
|
||||||
|
users, err := pol.getUsersInGroup(group, stripEmailDomain)
|
||||||
|
if err != nil {
|
||||||
|
return &netipx.IPSet{}, err
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
filteredMachines := filterMachinesByUser(machines, user)
|
||||||
|
for _, machine := range filteredMachines {
|
||||||
|
machine.IPAddresses.AppendToIPSet(&build)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return build.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pol *ACLPolicy) getIPsFromTag(
|
||||||
|
alias string,
|
||||||
|
machines Machines,
|
||||||
|
stripEmailDomain bool,
|
||||||
|
) (*netipx.IPSet, error) {
|
||||||
|
build := netipx.IPSetBuilder{}
|
||||||
|
|
||||||
|
// check for forced tags
|
||||||
|
for _, machine := range machines {
|
||||||
|
if contains(machine.ForcedTags, alias) {
|
||||||
|
machine.IPAddresses.AppendToIPSet(&build)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find tag owners
|
||||||
|
owners, err := getTagOwners(pol, alias, stripEmailDomain)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errInvalidTag) {
|
||||||
|
ipSet, _ := build.IPSet()
|
||||||
|
if len(ipSet.Prefixes()) == 0 {
|
||||||
|
return ipSet, fmt.Errorf(
|
||||||
|
"%w. %v isn't owned by a TagOwner and no forced tags are defined",
|
||||||
|
errInvalidTag,
|
||||||
|
alias,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return build.IPSet()
|
||||||
|
} else {
|
||||||
|
return nil, 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) {
|
||||||
|
machine.IPAddresses.AppendToIPSet(&build)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return build.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pol *ACLPolicy) getIPsForUser(
|
||||||
|
user string,
|
||||||
|
machines Machines,
|
||||||
|
stripEmailDomain bool,
|
||||||
|
) (*netipx.IPSet, error) {
|
||||||
|
build := netipx.IPSetBuilder{}
|
||||||
|
|
||||||
|
filteredMachines := filterMachinesByUser(machines, user)
|
||||||
|
filteredMachines = excludeCorrectlyTaggedNodes(pol, filteredMachines, user, stripEmailDomain)
|
||||||
|
|
||||||
|
// shortcurcuit if we have no machines to get ips from.
|
||||||
|
if len(filteredMachines) == 0 {
|
||||||
|
return nil, nil //nolint
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, machine := range filteredMachines {
|
||||||
|
machine.IPAddresses.AppendToIPSet(&build)
|
||||||
|
}
|
||||||
|
|
||||||
|
return build.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pol *ACLPolicy) getIPsFromSingleIP(
|
||||||
|
ip netip.Addr,
|
||||||
|
machines Machines,
|
||||||
|
) (*netipx.IPSet, error) {
|
||||||
|
log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip")
|
||||||
|
|
||||||
|
matches := machines.FilterByIP(ip)
|
||||||
|
|
||||||
|
build := netipx.IPSetBuilder{}
|
||||||
|
build.Add(ip)
|
||||||
|
|
||||||
|
for _, machine := range matches {
|
||||||
|
machine.IPAddresses.AppendToIPSet(&build)
|
||||||
|
}
|
||||||
|
|
||||||
|
return build.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pol *ACLPolicy) getIPsFromIPPrefix(
|
||||||
|
prefix netip.Prefix,
|
||||||
|
machines Machines,
|
||||||
|
) (*netipx.IPSet, error) {
|
||||||
|
log.Trace().Str("prefix", prefix.String()).Msg("expandAlias got prefix")
|
||||||
|
build := netipx.IPSetBuilder{}
|
||||||
|
build.AddPrefix(prefix)
|
||||||
|
|
||||||
|
// This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6
|
||||||
|
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
|
||||||
|
for _, machine := range machines {
|
||||||
|
for _, ip := range machine.IPAddresses {
|
||||||
|
// log.Trace().
|
||||||
|
// Msgf("checking if machine ip (%s) is part of prefix (%s): %v, is single ip prefix (%v), addr: %s", ip.String(), prefix.String(), prefix.Contains(ip), prefix.IsSingleIP(), prefix.Addr().String())
|
||||||
|
if prefix.Contains(ip) {
|
||||||
|
machine.IPAddresses.AppendToIPSet(&build)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return build.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWildcard(str string) bool {
|
||||||
|
return str == "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGroup(str string) bool {
|
||||||
|
return strings.HasPrefix(str, "group:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTag(str string) bool {
|
||||||
|
return strings.HasPrefix(str, "tag:")
|
||||||
}
|
}
|
||||||
|
|||||||
557
acls_test.go
557
acls_test.go
File diff suppressed because it is too large
Load Diff
@@ -111,8 +111,8 @@ func (hosts *Hosts) UnmarshalYAML(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsZero is perhaps a bit naive here.
|
// IsZero is perhaps a bit naive here.
|
||||||
func (policy ACLPolicy) IsZero() bool {
|
func (pol ACLPolicy) IsZero() bool {
|
||||||
if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 {
|
if len(pol.Groups) == 0 && len(pol.Hosts) == 0 && len(pol.ACLs) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
app.go
9
app.go
@@ -84,11 +84,9 @@ type Headscale struct {
|
|||||||
DERPMap *tailcfg.DERPMap
|
DERPMap *tailcfg.DERPMap
|
||||||
DERPServer *DERPServer
|
DERPServer *DERPServer
|
||||||
|
|
||||||
aclPolicy *ACLPolicy
|
aclPolicy *ACLPolicy
|
||||||
aclRules []tailcfg.FilterRule
|
aclRules []tailcfg.FilterRule
|
||||||
aclPeerCacheMapRW sync.RWMutex
|
sshPolicy *tailcfg.SSHPolicy
|
||||||
aclPeerCacheMap map[string]map[string]struct{}
|
|
||||||
sshPolicy *tailcfg.SSHPolicy
|
|
||||||
|
|
||||||
lastStateChange *xsync.MapOf[string, time.Time]
|
lastStateChange *xsync.MapOf[string, time.Time]
|
||||||
|
|
||||||
@@ -820,7 +818,6 @@ func (h *Headscale) Serve() error {
|
|||||||
|
|
||||||
// And we're done:
|
// And we're done:
|
||||||
cancel()
|
cancel()
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,6 +76,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"
|
||||||
`),
|
`),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -58,11 +58,12 @@ noise:
|
|||||||
# 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
|
||||||
|
|||||||
24
config.go
24
config.go
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
)
|
)
|
||||||
@@ -515,6 +516,29 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
|
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prefix.Addr().Is4() {
|
||||||
|
builder := netipx.IPSetBuilder{}
|
||||||
|
builder.AddPrefix(tsaddr.CGNATRange())
|
||||||
|
ipSet, _ := builder.IPSet()
|
||||||
|
if !ipSet.ContainsPrefix(prefix) {
|
||||||
|
log.Warn().
|
||||||
|
Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.",
|
||||||
|
prefixInConfig, tsaddr.CGNATRange())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix.Addr().Is6() {
|
||||||
|
builder := netipx.IPSetBuilder{}
|
||||||
|
builder.AddPrefix(tsaddr.TailscaleULARange())
|
||||||
|
ipSet, _ := builder.IPSet()
|
||||||
|
if !ipSet.ContainsPrefix(prefix) {
|
||||||
|
log.Warn().
|
||||||
|
Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.",
|
||||||
|
prefixInConfig, tsaddr.TailscaleULARange())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parsedPrefixes = append(parsedPrefixes, prefix)
|
parsedPrefixes = append(parsedPrefixes, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -4,9 +4,40 @@ hide:
|
|||||||
- toc
|
- toc
|
||||||
---
|
---
|
||||||
|
|
||||||
# headscale documentation
|
# headscale
|
||||||
|
|
||||||
This site contains the official and community contributed documentation for `headscale`.
|
`headscale` is an open source, self-hosted implementation of the Tailscale control server.
|
||||||
|
|
||||||
If you are having trouble with following the documentation or get unexpected results,
|
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](/faq/).
|
||||||
please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Issue.
|
|
||||||
|
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).
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ WorkingDirectory=/var/lib/headscale
|
|||||||
ReadWritePaths=/var/lib/headscale /var/run
|
ReadWritePaths=/var/lib/headscale /var/run
|
||||||
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN
|
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN
|
||||||
CapabilityBoundingSet=CAP_CHOWN
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN
|
||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
PrivateDevices=true
|
PrivateDevices=true
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ configuration (`/etc/headscale/config.yaml`).
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page]():
|
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=headscale.deb \
|
wget --output-document=headscale.deb \
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
||||||
# update this if you have a mismatch after doing a change to thos files.
|
# update this if you have a mismatch after doing a change to thos files.
|
||||||
vendorSha256 = "sha256-+JxS4Q6rTpdBwms2nkVDY/Kluv2qu2T0BaOIjfeX85M=";
|
vendorSha256 = "sha256-cmDNYWYTgQp6CPgpL4d3TbkpAe7rhNAF+o8njJsgL7E=";
|
||||||
|
|
||||||
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
|
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
|
||||||
};
|
};
|
||||||
@@ -99,6 +99,11 @@
|
|||||||
goreleaser
|
goreleaser
|
||||||
nfpm
|
nfpm
|
||||||
gotestsum
|
gotestsum
|
||||||
|
gotests
|
||||||
|
|
||||||
|
# 'dot' is needed for pprof graphs
|
||||||
|
# go tool pprof -http=: <source>
|
||||||
|
graphviz
|
||||||
|
|
||||||
# Protobuf dependencies
|
# Protobuf dependencies
|
||||||
protobuf
|
protobuf
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -4,7 +4,6 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||||
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029
|
|
||||||
github.com/cenkalti/backoff/v4 v4.2.0
|
github.com/cenkalti/backoff/v4 v4.2.0
|
||||||
github.com/coreos/go-oidc/v3 v3.5.0
|
github.com/coreos/go-oidc/v3 v3.5.0
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
@@ -12,6 +11,7 @@ require (
|
|||||||
github.com/efekarakus/termcolor v1.0.1
|
github.com/efekarakus/termcolor v1.0.1
|
||||||
github.com/glebarez/sqlite v1.7.0
|
github.com/glebarez/sqlite v1.7.0
|
||||||
github.com/gofrs/uuid/v5 v5.0.0
|
github.com/gofrs/uuid/v5 v5.0.0
|
||||||
|
github.com/google/go-cmp v0.5.9
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
|
||||||
@@ -20,6 +20,7 @@ require (
|
|||||||
github.com/ory/dockertest/v3 v3.9.1
|
github.com/ory/dockertest/v3 v3.9.1
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/philip-bui/grpc-zerolog v1.0.1
|
github.com/philip-bui/grpc-zerolog v1.0.1
|
||||||
|
github.com/pkg/profile v1.7.0
|
||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/prometheus/client_golang v1.14.0
|
||||||
github.com/prometheus/common v0.42.0
|
github.com/prometheus/common v0.42.0
|
||||||
github.com/pterm/pterm v0.12.58
|
github.com/pterm/pterm v0.12.58
|
||||||
@@ -64,6 +65,7 @@ require (
|
|||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/felixge/fgprof v0.9.3 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.20.3 // indirect
|
github.com/glebarez/go-sqlite v1.20.3 // indirect
|
||||||
@@ -72,9 +74,9 @@ require (
|
|||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
|
||||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gookit/color v1.5.3 // indirect
|
github.com/gookit/color v1.5.3 // indirect
|
||||||
@@ -141,6 +143,7 @@ require (
|
|||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gotest.tools/v3 v3.4.0 // indirect
|
||||||
modernc.org/libc v1.22.2 // indirect
|
modernc.org/libc v1.22.2 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
|||||||
13
go.sum
13
go.sum
@@ -74,8 +74,6 @@ github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkU
|
|||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 h1:POmUHfxXdeyM8Aomg4tKDcwATCFuW+cYLkj6pwsw9pc=
|
|
||||||
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029/go.mod h1:Rpr5n9cGHYdM3S3IK8ROSUUUYjQOu+MSUCZDcJbYWi8=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
@@ -129,6 +127,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||||
|
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
@@ -238,7 +238,9 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
|||||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
@@ -272,6 +274,7 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u
|
|||||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
@@ -384,6 +387,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
|
|||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||||
|
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
@@ -669,6 +674,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -894,7 +900,8 @@ gorm.io/driver/postgres v1.4.8/go.mod h1:O9MruWGNLUBUWVYfWuBClpf3HeGjOoybY0SNmCs
|
|||||||
gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||||
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
|
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
|
||||||
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
|
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
|
||||||
|
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
@@ -12,6 +12,39 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var veryLargeDestination = []string{
|
||||||
|
"0.0.0.0/5:*",
|
||||||
|
"8.0.0.0/7:*",
|
||||||
|
"11.0.0.0/8:*",
|
||||||
|
"12.0.0.0/6:*",
|
||||||
|
"16.0.0.0/4:*",
|
||||||
|
"32.0.0.0/3:*",
|
||||||
|
"64.0.0.0/2:*",
|
||||||
|
"128.0.0.0/3:*",
|
||||||
|
"160.0.0.0/5:*",
|
||||||
|
"168.0.0.0/6:*",
|
||||||
|
"172.0.0.0/12:*",
|
||||||
|
"172.32.0.0/11:*",
|
||||||
|
"172.64.0.0/10:*",
|
||||||
|
"172.128.0.0/9:*",
|
||||||
|
"173.0.0.0/8:*",
|
||||||
|
"174.0.0.0/7:*",
|
||||||
|
"176.0.0.0/4:*",
|
||||||
|
"192.0.0.0/9:*",
|
||||||
|
"192.128.0.0/11:*",
|
||||||
|
"192.160.0.0/13:*",
|
||||||
|
"192.169.0.0/16:*",
|
||||||
|
"192.170.0.0/15:*",
|
||||||
|
"192.172.0.0/14:*",
|
||||||
|
"192.176.0.0/12:*",
|
||||||
|
"192.192.0.0/10:*",
|
||||||
|
"193.0.0.0/8:*",
|
||||||
|
"194.0.0.0/7:*",
|
||||||
|
"196.0.0.0/6:*",
|
||||||
|
"200.0.0.0/5:*",
|
||||||
|
"208.0.0.0/4:*",
|
||||||
|
}
|
||||||
|
|
||||||
func aclScenario(t *testing.T, policy *headscale.ACLPolicy, clientsPerUser int) *Scenario {
|
func aclScenario(t *testing.T, policy *headscale.ACLPolicy, clientsPerUser int) *Scenario {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
scenario, err := NewScenario()
|
scenario, err := NewScenario()
|
||||||
@@ -176,6 +209,34 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
|||||||
"user2": 3, // ns1 + ns2 (return path)
|
"user2": 3, // ns1 + ns2 (return path)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"very-large-destination-prefix-1372": {
|
||||||
|
users: map[string]int{
|
||||||
|
"user1": 2,
|
||||||
|
"user2": 2,
|
||||||
|
},
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"user1"},
|
||||||
|
Destinations: append([]string{"user1:*"}, veryLargeDestination...),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"user2"},
|
||||||
|
Destinations: append([]string{"user2:*"}, veryLargeDestination...),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"user1"},
|
||||||
|
Destinations: append([]string{"user2:*"}, veryLargeDestination...),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, want: map[string]int{
|
||||||
|
"user1": 3, // ns1 + ns2
|
||||||
|
"user2": 3, // ns1 + ns2 (return path)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, testCase := range tests {
|
for name, testCase := range tests {
|
||||||
@@ -188,7 +249,6 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
|||||||
err = scenario.CreateHeadscaleEnv(spec,
|
err = scenario.CreateHeadscaleEnv(spec,
|
||||||
[]tsic.Option{},
|
[]tsic.Option{},
|
||||||
hsic.WithACLPolicy(&testCase.policy),
|
hsic.WithACLPolicy(&testCase.policy),
|
||||||
// hsic.WithTestName(fmt.Sprintf("aclinnetmap%s", name)),
|
|
||||||
)
|
)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -198,9 +258,6 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
|||||||
err = scenario.WaitForTailscaleSync()
|
err = scenario.WaitForTailscaleSync()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// allHostnames, err := scenario.ListTailscaleClientsFQDNs()
|
|
||||||
// assert.NoError(t, err)
|
|
||||||
|
|
||||||
for _, client := range allClients {
|
for _, client := range allClients {
|
||||||
status, err := client.Status()
|
status, err := client.Status()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ControlServer interface {
|
type ControlServer interface {
|
||||||
Shutdown() error
|
Shutdown() error
|
||||||
SaveLog(string) error
|
SaveLog(string) error
|
||||||
|
SaveProfile(string) error
|
||||||
Execute(command []string) (string, error)
|
Execute(command []string) (string, error)
|
||||||
|
ConnectToNetwork(network *dockertest.Network) error
|
||||||
GetHealthEndpoint() string
|
GetHealthEndpoint() string
|
||||||
GetEndpoint() string
|
GetEndpoint() string
|
||||||
WaitForReady() error
|
WaitForReady() error
|
||||||
|
|||||||
236
integration/embedded_derp_test.go
Normal file
236
integration/embedded_derp_test.go
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale"
|
||||||
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmbeddedDERPServerScenario struct {
|
||||||
|
*Scenario
|
||||||
|
|
||||||
|
tsicNetworks map[string]*dockertest.Network
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDERPServerScenario(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
// t.Parallel()
|
||||||
|
|
||||||
|
baseScenario, err := NewScenario()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create scenario: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenario := EmbeddedDERPServerScenario{
|
||||||
|
Scenario: baseScenario,
|
||||||
|
tsicNetworks: map[string]*dockertest.Network{},
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := map[string]int{
|
||||||
|
"user1": len(TailscaleVersions),
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleConfig := map[string]string{}
|
||||||
|
headscaleConfig["HEADSCALE_DERP_URLS"] = ""
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_ENABLED"] = "true"
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_REGION_ID"] = "999"
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_REGION_CODE"] = "headscale"
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_REGION_NAME"] = "Headscale Embedded DERP"
|
||||||
|
headscaleConfig["HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR"] = "0.0.0.0:3478"
|
||||||
|
|
||||||
|
err = scenario.CreateHeadscaleEnv(
|
||||||
|
spec,
|
||||||
|
hsic.WithConfigEnv(headscaleConfig),
|
||||||
|
hsic.WithTestName("derpserver"),
|
||||||
|
hsic.WithExtraPorts([]string{"3478/udp"}),
|
||||||
|
hsic.WithTLS(),
|
||||||
|
hsic.WithHostnameAsServerURL(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create headscale environment: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get clients: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allIps, err := scenario.ListTailscaleClientsIPs()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get clients: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scenario.WaitForTailscaleSync()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed wait for tailscale clients to be in sync: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allHostnames, err := scenario.ListTailscaleClientsFQDNs()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get FQDNs: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
success := pingDerpAllHelper(t, allClients, allHostnames)
|
||||||
|
|
||||||
|
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||||
|
|
||||||
|
err = scenario.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to tear down scenario: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv(
|
||||||
|
users map[string]int,
|
||||||
|
opts ...hsic.Option,
|
||||||
|
) error {
|
||||||
|
hsServer, err := s.Headscale(opts...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleEndpoint := hsServer.GetEndpoint()
|
||||||
|
headscaleURL, err := url.Parse(headscaleEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleURL.Host = fmt.Sprintf("%s:%s", hsServer.GetHostname(), headscaleURL.Port())
|
||||||
|
|
||||||
|
err = hsServer.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for userName, clientCount := range users {
|
||||||
|
err = s.CreateUser(userName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.CreateTailscaleIsolatedNodesInUser(
|
||||||
|
hash,
|
||||||
|
userName,
|
||||||
|
"all",
|
||||||
|
clientCount,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := s.CreatePreAuthKey(userName, true, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.RunTailscaleUp(userName, headscaleURL.String(), key.GetKey())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EmbeddedDERPServerScenario) CreateTailscaleIsolatedNodesInUser(
|
||||||
|
hash string,
|
||||||
|
userStr string,
|
||||||
|
requestedVersion string,
|
||||||
|
count int,
|
||||||
|
opts ...tsic.Option,
|
||||||
|
) error {
|
||||||
|
hsServer, err := s.Headscale()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user, ok := s.users[userStr]; ok {
|
||||||
|
for clientN := 0; clientN < count; clientN++ {
|
||||||
|
networkName := fmt.Sprintf("tsnet-%s-%s-%d",
|
||||||
|
hash,
|
||||||
|
userStr,
|
||||||
|
clientN,
|
||||||
|
)
|
||||||
|
network, err := dockertestutil.GetFirstOrCreateNetwork(
|
||||||
|
s.pool,
|
||||||
|
networkName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create or get %s network: %w", networkName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.tsicNetworks[networkName] = network
|
||||||
|
|
||||||
|
err = hsServer.ConnectToNetwork(network)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect headscale to %s network: %w", networkName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
version := requestedVersion
|
||||||
|
if requestedVersion == "all" {
|
||||||
|
version = TailscaleVersions[clientN%len(TailscaleVersions)]
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := hsServer.GetCert()
|
||||||
|
|
||||||
|
user.createWaitGroup.Add(1)
|
||||||
|
|
||||||
|
opts = append(opts,
|
||||||
|
tsic.WithHeadscaleTLS(cert),
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer user.createWaitGroup.Done()
|
||||||
|
|
||||||
|
// TODO(kradalby): error handle this
|
||||||
|
tsClient, err := tsic.New(
|
||||||
|
s.pool,
|
||||||
|
version,
|
||||||
|
network,
|
||||||
|
opts...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// return fmt.Errorf("failed to add tailscale node: %w", err)
|
||||||
|
log.Printf("failed to create tailscale node: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tsClient.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
// return fmt.Errorf("failed to add tailscale node: %w", err)
|
||||||
|
log.Printf("failed to wait for tailscaled: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Clients[tsClient.Hostname()] = tsClient
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
user.createWaitGroup.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to add tailscale node: %w", errNoUserAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EmbeddedDERPServerScenario) Shutdown() error {
|
||||||
|
for _, network := range s.tsicNetworks {
|
||||||
|
err := s.pool.RemoveNetwork(network)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Scenario.Shutdown()
|
||||||
|
}
|
||||||
@@ -15,6 +15,10 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
@@ -23,6 +27,7 @@ import (
|
|||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
"github.com/juanfont/headscale/integration/integrationutil"
|
"github.com/juanfont/headscale/integration/integrationutil"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/ory/dockertest/v3/docker"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -52,6 +57,8 @@ type HeadscaleInContainer struct {
|
|||||||
|
|
||||||
// optional config
|
// optional config
|
||||||
port int
|
port int
|
||||||
|
extraPorts []string
|
||||||
|
hostPortBindings map[string][]string
|
||||||
aclPolicy *headscale.ACLPolicy
|
aclPolicy *headscale.ACLPolicy
|
||||||
env map[string]string
|
env map[string]string
|
||||||
tlsCert []byte
|
tlsCert []byte
|
||||||
@@ -77,7 +84,7 @@ func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
|||||||
// WithTLS creates certificates and enables HTTPS.
|
// WithTLS creates certificates and enables HTTPS.
|
||||||
func WithTLS() Option {
|
func WithTLS() Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
cert, key, err := createCertificate()
|
cert, key, err := createCertificate(hsic.hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create certificates for headscale test: %s", err)
|
log.Fatalf("failed to create certificates for headscale test: %s", err)
|
||||||
}
|
}
|
||||||
@@ -108,6 +115,19 @@ func WithPort(port int) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithExtraPorts exposes additional ports on the container (e.g. 3478/udp for STUN).
|
||||||
|
func WithExtraPorts(ports []string) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.extraPorts = ports
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHostPortBindings(bindings map[string][]string) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.hostPortBindings = bindings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithTestName sets a name for the test, this will be reflected
|
// WithTestName sets a name for the test, this will be reflected
|
||||||
// in the Docker container name.
|
// in the Docker container name.
|
||||||
func WithTestName(testName string) Option {
|
func WithTestName(testName string) Option {
|
||||||
@@ -173,12 +193,25 @@ func New(
|
|||||||
|
|
||||||
portProto := fmt.Sprintf("%d/tcp", hsic.port)
|
portProto := fmt.Sprintf("%d/tcp", hsic.port)
|
||||||
|
|
||||||
|
serverURL, err := url.Parse(hsic.env["HEADSCALE_SERVER_URL"])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hsic.tlsCert) != 0 && len(hsic.tlsKey) != 0 {
|
||||||
|
serverURL.Scheme = "https"
|
||||||
|
hsic.env["HEADSCALE_SERVER_URL"] = serverURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
Dockerfile: "Dockerfile.debug",
|
Dockerfile: "Dockerfile.debug",
|
||||||
ContextDir: dockerContextPath,
|
ContextDir: dockerContextPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
env := []string{}
|
env := []string{
|
||||||
|
"HEADSCALE_PROFILING_ENABLED=1",
|
||||||
|
"HEADSCALE_PROFILING_PATH=/tmp/profile",
|
||||||
|
}
|
||||||
for key, value := range hsic.env {
|
for key, value := range hsic.env {
|
||||||
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
||||||
}
|
}
|
||||||
@@ -187,15 +220,27 @@ func New(
|
|||||||
|
|
||||||
runOptions := &dockertest.RunOptions{
|
runOptions := &dockertest.RunOptions{
|
||||||
Name: hsic.hostname,
|
Name: hsic.hostname,
|
||||||
ExposedPorts: []string{portProto},
|
ExposedPorts: append([]string{portProto}, hsic.extraPorts...),
|
||||||
Networks: []*dockertest.Network{network},
|
Networks: []*dockertest.Network{network},
|
||||||
// Cmd: []string{"headscale", "serve"},
|
// Cmd: []string{"headscale", "serve"},
|
||||||
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
||||||
// to inject the headscale configuration further down.
|
// to inject the headscale configuration further down.
|
||||||
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve"},
|
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve ; /bin/sleep 30"},
|
||||||
Env: env,
|
Env: env,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(hsic.hostPortBindings) > 0 {
|
||||||
|
runOptions.PortBindings = map[docker.Port][]docker.PortBinding{}
|
||||||
|
for port, hostPorts := range hsic.hostPortBindings {
|
||||||
|
runOptions.PortBindings[docker.Port(port)] = []docker.PortBinding{}
|
||||||
|
for _, hostPort := range hostPorts {
|
||||||
|
runOptions.PortBindings[docker.Port(port)] = append(
|
||||||
|
runOptions.PortBindings[docker.Port(port)],
|
||||||
|
docker.PortBinding{HostPort: hostPort})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dockertest isnt very good at handling containers that has already
|
// dockertest isnt very good at handling containers that has already
|
||||||
// been created, this is an attempt to make sure this container isnt
|
// been created, this is an attempt to make sure this container isnt
|
||||||
// present.
|
// present.
|
||||||
@@ -256,12 +301,43 @@ func New(
|
|||||||
return hsic, nil
|
return hsic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *HeadscaleInContainer) ConnectToNetwork(network *dockertest.Network) error {
|
||||||
|
return t.container.ConnectToNetwork(network)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *HeadscaleInContainer) hasTLS() bool {
|
func (t *HeadscaleInContainer) hasTLS() bool {
|
||||||
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
|
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown stops and cleans up the Headscale container.
|
// Shutdown stops and cleans up the Headscale container.
|
||||||
func (t *HeadscaleInContainer) Shutdown() error {
|
func (t *HeadscaleInContainer) Shutdown() error {
|
||||||
|
err := t.SaveLog("/tmp/control")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"Failed to save log from control: %s",
|
||||||
|
fmt.Errorf("failed to save log from control: %w", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a interrupt signal to the "headscale" process inside the container
|
||||||
|
// allowing it to shut down gracefully and flush the profile to disk.
|
||||||
|
// The container will live for a bit longer due to the sleep at the end.
|
||||||
|
err = t.SendInterrupt()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"Failed to send graceful interrupt to control: %s",
|
||||||
|
fmt.Errorf("failed to send graceful interrupt to control: %w", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.SaveProfile("/tmp/control")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"Failed to save profile from control: %s",
|
||||||
|
fmt.Errorf("failed to save profile from control: %w", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return t.pool.Purge(t.container)
|
return t.pool.Purge(t.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +347,24 @@ func (t *HeadscaleInContainer) SaveLog(path string) error {
|
|||||||
return dockertestutil.SaveLog(t.pool, t.container, path)
|
return dockertestutil.SaveLog(t.pool, t.container, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *HeadscaleInContainer) SaveProfile(savePath string) error {
|
||||||
|
tarFile, err := t.FetchPath("/tmp/profile")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(
|
||||||
|
path.Join(savePath, t.hostname+".pprof.tar"),
|
||||||
|
tarFile,
|
||||||
|
os.ModePerm,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Execute runs a command inside the Headscale container and returns the
|
// Execute runs a command inside the Headscale container and returns the
|
||||||
// result of stdout as a string.
|
// result of stdout as a string.
|
||||||
func (t *HeadscaleInContainer) Execute(
|
func (t *HeadscaleInContainer) Execute(
|
||||||
@@ -455,8 +549,28 @@ func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
|||||||
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchPath gets a path from inside the Headscale container and returns a tar
|
||||||
|
// file as byte array.
|
||||||
|
func (t *HeadscaleInContainer) FetchPath(path string) ([]byte, error) {
|
||||||
|
return integrationutil.FetchPathFromContainer(t.pool, t.container, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HeadscaleInContainer) SendInterrupt() error {
|
||||||
|
pid, err := t.Execute([]string{"pidof", "headscale"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = t.Execute([]string{"kill", "-2", strings.Trim(pid, "'\n")})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// nolint
|
// nolint
|
||||||
func createCertificate() ([]byte, []byte, error) {
|
func createCertificate(hostname string) ([]byte, []byte, error) {
|
||||||
// From:
|
// From:
|
||||||
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
||||||
|
|
||||||
@@ -468,7 +582,7 @@ func createCertificate() ([]byte, []byte, error) {
|
|||||||
Locality: []string{"Leiden"},
|
Locality: []string{"Leiden"},
|
||||||
},
|
},
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
NotAfter: time.Now().Add(30 * time.Minute),
|
NotAfter: time.Now().Add(60 * time.Minute),
|
||||||
IsCA: true,
|
IsCA: true,
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||||
x509.ExtKeyUsageClientAuth,
|
x509.ExtKeyUsageClientAuth,
|
||||||
@@ -486,16 +600,17 @@ func createCertificate() ([]byte, []byte, error) {
|
|||||||
cert := &x509.Certificate{
|
cert := &x509.Certificate{
|
||||||
SerialNumber: big.NewInt(1658),
|
SerialNumber: big.NewInt(1658),
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
|
CommonName: hostname,
|
||||||
Organization: []string{"Headscale testing INC"},
|
Organization: []string{"Headscale testing INC"},
|
||||||
Country: []string{"NL"},
|
Country: []string{"NL"},
|
||||||
Locality: []string{"Leiden"},
|
Locality: []string{"Leiden"},
|
||||||
},
|
},
|
||||||
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
NotAfter: time.Now().Add(30 * time.Minute),
|
NotAfter: time.Now().Add(60 * time.Minute),
|
||||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
DNSNames: []string{hostname},
|
||||||
}
|
}
|
||||||
|
|
||||||
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
|||||||
@@ -72,3 +72,24 @@ func WriteFileToContainer(
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FetchPathFromContainer(
|
||||||
|
pool *dockertest.Pool,
|
||||||
|
container *dockertest.Resource,
|
||||||
|
path string,
|
||||||
|
) ([]byte, error) {
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
|
||||||
|
err := pool.Client.DownloadFromContainer(
|
||||||
|
container.Container.ID,
|
||||||
|
docker.DownloadFromContainerOptions{
|
||||||
|
OutputStream: buf,
|
||||||
|
Path: path,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ var (
|
|||||||
tailscaleVersions2021 = []string{
|
tailscaleVersions2021 = []string{
|
||||||
"head",
|
"head",
|
||||||
"unstable",
|
"unstable",
|
||||||
|
"1.40.0",
|
||||||
"1.38.4",
|
"1.38.4",
|
||||||
"1.36.2",
|
"1.36.2",
|
||||||
"1.34.2",
|
"1.34.2",
|
||||||
@@ -149,15 +150,7 @@ func NewScenario() (*Scenario, error) {
|
|||||||
// environment running the tests.
|
// environment running the tests.
|
||||||
func (s *Scenario) Shutdown() error {
|
func (s *Scenario) Shutdown() error {
|
||||||
s.controlServers.Range(func(_ string, control ControlServer) bool {
|
s.controlServers.Range(func(_ string, control ControlServer) bool {
|
||||||
err := control.SaveLog("/tmp/control")
|
err := control.Shutdown()
|
||||||
if err != nil {
|
|
||||||
log.Printf(
|
|
||||||
"Failed to save log from control: %s",
|
|
||||||
fmt.Errorf("failed to save log from control: %w", err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = control.Shutdown()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"Failed to shut down control: %s",
|
"Failed to shut down control: %s",
|
||||||
@@ -287,7 +280,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||||||
|
|
||||||
headscale, err := s.Headscale()
|
headscale, err := s.Headscale()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create tailscale node: %w", err)
|
return fmt.Errorf("failed to create tailscale node (version: %s): %w", version, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cert := headscale.GetCert()
|
cert := headscale.GetCert()
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ func TestSSUserOnlyIsolation(t *testing.T) {
|
|||||||
// TODO(kradalby,evenh): ACLs do currently not cover reject
|
// TODO(kradalby,evenh): ACLs do currently not cover reject
|
||||||
// cases properly, and currently will accept all incomming connections
|
// cases properly, and currently will accept all incomming connections
|
||||||
// as long as a rule is present.
|
// as long as a rule is present.
|
||||||
//
|
|
||||||
// for _, client := range ssh1Clients {
|
// for _, client := range ssh1Clients {
|
||||||
// for _, peer := range ssh2Clients {
|
// for _, peer := range ssh2Clients {
|
||||||
// if client.Hostname() == peer.Hostname() {
|
// if client.Hostname() == peer.Hostname() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
)
|
)
|
||||||
@@ -13,7 +14,7 @@ type TailscaleClient interface {
|
|||||||
Hostname() string
|
Hostname() string
|
||||||
Shutdown() error
|
Shutdown() error
|
||||||
Version() string
|
Version() string
|
||||||
Execute(command []string) (string, string, error)
|
Execute(command []string, options ...dockertestutil.ExecuteCommandOption) (string, string, error)
|
||||||
Up(loginServer, authKey string) error
|
Up(loginServer, authKey string) error
|
||||||
UpWithLoginURL(loginServer string) (*url.URL, error)
|
UpWithLoginURL(loginServer string) (*url.URL, error)
|
||||||
Logout() error
|
Logout() error
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
errTailscalePingFailed = errors.New("ping failed")
|
errTailscalePingFailed = errors.New("ping failed")
|
||||||
|
errTailscalePingNotDERP = errors.New("ping not via DERP")
|
||||||
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
|
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
|
||||||
errTailscaleWrongPeerCount = errors.New("wrong peer count")
|
errTailscaleWrongPeerCount = errors.New("wrong peer count")
|
||||||
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
|
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
|
||||||
@@ -56,6 +57,7 @@ type TailscaleInContainer struct {
|
|||||||
withSSH bool
|
withSSH bool
|
||||||
withTags []string
|
withTags []string
|
||||||
withEntrypoint []string
|
withEntrypoint []string
|
||||||
|
withExtraHosts []string
|
||||||
workdir string
|
workdir string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +126,12 @@ func WithDockerWorkdir(dir string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithExtraHosts(hosts []string) Option {
|
||||||
|
return func(tsic *TailscaleInContainer) {
|
||||||
|
tsic.withExtraHosts = hosts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithDockerEntrypoint allows the docker entrypoint of the container
|
// WithDockerEntrypoint allows the docker entrypoint of the container
|
||||||
// to be overridden. This is a dangerous option which can make
|
// to be overridden. This is a dangerous option which can make
|
||||||
// the container not work as intended as a typo might prevent
|
// the container not work as intended as a typo might prevent
|
||||||
@@ -169,11 +177,12 @@ func New(
|
|||||||
|
|
||||||
tailscaleOptions := &dockertest.RunOptions{
|
tailscaleOptions := &dockertest.RunOptions{
|
||||||
Name: hostname,
|
Name: hostname,
|
||||||
Networks: []*dockertest.Network{network},
|
Networks: []*dockertest.Network{tsic.network},
|
||||||
// Cmd: []string{
|
// Cmd: []string{
|
||||||
// "tailscaled", "--tun=tsdev",
|
// "tailscaled", "--tun=tsdev",
|
||||||
// },
|
// },
|
||||||
Entrypoint: tsic.withEntrypoint,
|
Entrypoint: tsic.withEntrypoint,
|
||||||
|
ExtraHosts: tsic.withExtraHosts,
|
||||||
}
|
}
|
||||||
|
|
||||||
if tsic.headscaleHostname != "" {
|
if tsic.headscaleHostname != "" {
|
||||||
@@ -203,7 +212,11 @@ func New(
|
|||||||
dockertestutil.DockerAllowNetworkAdministration,
|
dockertestutil.DockerAllowNetworkAdministration,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not start tailscale container: %w", err)
|
return nil, fmt.Errorf(
|
||||||
|
"could not start tailscale container (version: %s): %w",
|
||||||
|
version,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
log.Printf("Created %s container\n", hostname)
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
@@ -248,11 +261,13 @@ func (t *TailscaleInContainer) ID() string {
|
|||||||
// result of stdout as a string.
|
// result of stdout as a string.
|
||||||
func (t *TailscaleInContainer) Execute(
|
func (t *TailscaleInContainer) Execute(
|
||||||
command []string,
|
command []string,
|
||||||
|
options ...dockertestutil.ExecuteCommandOption,
|
||||||
) (string, string, error) {
|
) (string, string, error) {
|
||||||
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
||||||
t.container,
|
t.container,
|
||||||
command,
|
command,
|
||||||
[]string{},
|
[]string{},
|
||||||
|
options...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("command stderr: %s\n", stderr)
|
log.Printf("command stderr: %s\n", stderr)
|
||||||
@@ -477,7 +492,7 @@ func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// PingOption repreent optional settings that can be given
|
// PingOption represent optional settings that can be given
|
||||||
// to ping another host.
|
// to ping another host.
|
||||||
PingOption = func(args *pingArgs)
|
PingOption = func(args *pingArgs)
|
||||||
|
|
||||||
@@ -535,7 +550,12 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
|
|||||||
command = append(command, hostnameOrIP)
|
command = append(command, hostnameOrIP)
|
||||||
|
|
||||||
return t.pool.Retry(func() error {
|
return t.pool.Retry(func() error {
|
||||||
result, _, err := t.Execute(command)
|
result, _, err := t.Execute(
|
||||||
|
command,
|
||||||
|
dockertestutil.ExecuteCommandTimeout(
|
||||||
|
time.Duration(int64(args.timeout)*int64(args.count)),
|
||||||
|
),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"failed to run ping command from %s to %s, err: %s",
|
"failed to run ping command from %s to %s, err: %s",
|
||||||
@@ -547,10 +567,22 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(result, "pong") && !strings.Contains(result, "is local") {
|
if strings.Contains(result, "is local") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(result, "pong") {
|
||||||
return backoff.Permanent(errTailscalePingFailed)
|
return backoff.Permanent(errTailscalePingFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !args.direct {
|
||||||
|
if strings.Contains(result, "via DERP") {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return backoff.Permanent(errTailscalePingNotDERP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
derpPingTimeout = 2 * time.Second
|
||||||
|
derpPingCount = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||||
@@ -22,6 +30,52 @@ func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int
|
|||||||
return success
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pingDerpAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||||
|
t.Helper()
|
||||||
|
success := 0
|
||||||
|
|
||||||
|
for _, client := range clients {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if isSelfClient(client, addr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.Ping(
|
||||||
|
addr,
|
||||||
|
tsic.WithPingTimeout(derpPingTimeout),
|
||||||
|
tsic.WithPingCount(derpPingCount),
|
||||||
|
tsic.WithPingUntilDirect(false),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to ping %s from %s: %s", addr, client.Hostname(), err)
|
||||||
|
} else {
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSelfClient(client TailscaleClient, addr string) bool {
|
||||||
|
if addr == client.Hostname() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ips, err := client.IPs()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.String() == addr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// pingAllNegativeHelper is intended to have 1 or more nodes timeing out from the ping,
|
// pingAllNegativeHelper is intended to have 1 or more nodes timeing out from the ping,
|
||||||
// it counts failures instead of successes.
|
// it counts failures instead of successes.
|
||||||
// func pingAllNegativeHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
// func pingAllNegativeHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||||
|
|||||||
@@ -1,453 +0,0 @@
|
|||||||
// nolint
|
|
||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ccding/go-stun/stun"
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
"github.com/ory/dockertest/v3/docker"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
headscaleDerpHostname = "headscale-derp"
|
|
||||||
userName = "derpuser"
|
|
||||||
totalContainers = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
type IntegrationDERPTestSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
stats *suite.SuiteInformation
|
|
||||||
|
|
||||||
pool dockertest.Pool
|
|
||||||
network dockertest.Network
|
|
||||||
containerNetworks map[int]dockertest.Network // so we keep the containers isolated
|
|
||||||
headscale dockertest.Resource
|
|
||||||
saveLogs bool
|
|
||||||
|
|
||||||
tailscales map[string]dockertest.Resource
|
|
||||||
joinWaitGroup sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntegrationDERPTestSuite(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping integration tests due to short flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLogs, err := GetEnvBool("HEADSCALE_INTEGRATION_SAVE_LOG")
|
|
||||||
if err != nil {
|
|
||||||
saveLogs = false
|
|
||||||
}
|
|
||||||
|
|
||||||
s := new(IntegrationDERPTestSuite)
|
|
||||||
|
|
||||||
s.tailscales = make(map[string]dockertest.Resource)
|
|
||||||
s.containerNetworks = make(map[int]dockertest.Network)
|
|
||||||
s.saveLogs = saveLogs
|
|
||||||
|
|
||||||
suite.Run(t, s)
|
|
||||||
|
|
||||||
// HandleStats, which allows us to check if we passed and save logs
|
|
||||||
// is called after TearDown, so we cannot tear down containers before
|
|
||||||
// we have potentially saved the logs.
|
|
||||||
if s.saveLogs {
|
|
||||||
for _, tailscale := range s.tailscales {
|
|
||||||
if err := s.pool.Purge(&tailscale); err != nil {
|
|
||||||
log.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.stats.Passed() {
|
|
||||||
err := s.saveLog(&s.headscale, "test_output")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Could not save log: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.pool.Purge(&s.headscale); err != nil {
|
|
||||||
log.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, network := range s.containerNetworks {
|
|
||||||
if err := network.Close(); err != nil {
|
|
||||||
log.Printf("Could not close network: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) SetupSuite() {
|
|
||||||
if ppool, err := dockertest.NewPool(""); err == nil {
|
|
||||||
s.pool = *ppool
|
|
||||||
} else {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
network, err := GetFirstOrCreateNetwork(&s.pool, headscaleNetwork)
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(fmt.Sprintf("Failed to create or get network: %s", err), "")
|
|
||||||
}
|
|
||||||
s.network = network
|
|
||||||
|
|
||||||
for i := 0; i < totalContainers; i++ {
|
|
||||||
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
|
|
||||||
s.containerNetworks[i] = *pnetwork
|
|
||||||
} else {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not create network: %s", err), "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headscaleBuildOptions := &dockertest.BuildOptions{
|
|
||||||
Dockerfile: "Dockerfile",
|
|
||||||
ContextDir: ".",
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPath, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
headscaleOptions := &dockertest.RunOptions{
|
|
||||||
Name: headscaleDerpHostname,
|
|
||||||
Mounts: []string{
|
|
||||||
fmt.Sprintf(
|
|
||||||
"%s/integration_test/etc_embedded_derp:/etc/headscale",
|
|
||||||
currentPath,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
Cmd: []string{"headscale", "serve"},
|
|
||||||
Networks: []*dockertest.Network{&s.network},
|
|
||||||
ExposedPorts: []string{"8443/tcp", "3478/udp"},
|
|
||||||
PortBindings: map[docker.Port][]docker.PortBinding{
|
|
||||||
"8443/tcp": {{HostPort: "8443"}},
|
|
||||||
"3478/udp": {{HostPort: "3478"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.pool.RemoveContainerByName(headscaleDerpHostname)
|
|
||||||
if err != nil {
|
|
||||||
s.FailNow(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Could not remove existing container before building test: %s",
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Creating headscale container for DERP integration tests")
|
|
||||||
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
|
||||||
s.headscale = *pheadscale
|
|
||||||
} else {
|
|
||||||
s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "")
|
|
||||||
}
|
|
||||||
log.Println("Created headscale container for embedded DERP tests")
|
|
||||||
|
|
||||||
log.Println("Creating tailscale containers for embedded DERP tests")
|
|
||||||
|
|
||||||
for i := 0; i < totalContainers; i++ {
|
|
||||||
version := tailscaleVersions[i%len(tailscaleVersions)]
|
|
||||||
hostname, container := s.tailscaleContainer(
|
|
||||||
fmt.Sprint(i),
|
|
||||||
version,
|
|
||||||
s.containerNetworks[i],
|
|
||||||
)
|
|
||||||
s.tailscales[hostname] = *container
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Waiting for headscale to be ready for embedded DERP tests")
|
|
||||||
hostEndpoint := fmt.Sprintf("%s:%s",
|
|
||||||
s.headscale.GetIPInNetwork(&s.network),
|
|
||||||
s.headscale.GetPort("8443/tcp"))
|
|
||||||
|
|
||||||
if err := s.pool.Retry(func() error {
|
|
||||||
url := fmt.Sprintf("https://%s/health", hostEndpoint)
|
|
||||||
insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
||||||
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
client := &http.Client{Transport: insecureTransport}
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("headscale for embedded DERP tests is not ready: %s\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("status code not OK")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
|
|
||||||
// test setup, we need to abort and tear down. However, testify does not seem to
|
|
||||||
// support that at the moment:
|
|
||||||
// https://github.com/stretchr/testify/issues/849
|
|
||||||
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
|
||||||
}
|
|
||||||
log.Println("headscale container is ready for embedded DERP tests")
|
|
||||||
|
|
||||||
log.Printf("Creating headscale user: %s\n", userName)
|
|
||||||
result, _, err := ExecuteCommand(
|
|
||||||
&s.headscale,
|
|
||||||
[]string{"headscale", "users", "create", userName},
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
log.Println("headscale create user result: ", result)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
log.Printf("Creating pre auth key for %s\n", userName)
|
|
||||||
preAuthResult, _, err := ExecuteCommand(
|
|
||||||
&s.headscale,
|
|
||||||
[]string{
|
|
||||||
"headscale",
|
|
||||||
"--user",
|
|
||||||
userName,
|
|
||||||
"preauthkeys",
|
|
||||||
"create",
|
|
||||||
"--reusable",
|
|
||||||
"--expiration",
|
|
||||||
"24h",
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
},
|
|
||||||
[]string{"LOG_LEVEL=error"},
|
|
||||||
)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
var preAuthKey v1.PreAuthKey
|
|
||||||
err = json.Unmarshal([]byte(preAuthResult), &preAuthKey)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
assert.True(s.T(), preAuthKey.Reusable)
|
|
||||||
|
|
||||||
headscaleEndpoint := fmt.Sprintf(
|
|
||||||
"https://headscale:%s",
|
|
||||||
s.headscale.GetPort("8443/tcp"),
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Printf(
|
|
||||||
"Joining tailscale containers to headscale at %s\n",
|
|
||||||
headscaleEndpoint,
|
|
||||||
)
|
|
||||||
for hostname, tailscale := range s.tailscales {
|
|
||||||
s.joinWaitGroup.Add(1)
|
|
||||||
go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.joinWaitGroup.Wait()
|
|
||||||
|
|
||||||
// The nodes need a bit of time to get their updated maps from headscale
|
|
||||||
// TODO: See if we can have a more deterministic wait here.
|
|
||||||
time.Sleep(60 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) Join(
|
|
||||||
endpoint, key, hostname string,
|
|
||||||
tailscale dockertest.Resource,
|
|
||||||
) {
|
|
||||||
defer s.joinWaitGroup.Done()
|
|
||||||
|
|
||||||
command := []string{
|
|
||||||
"tailscale",
|
|
||||||
"up",
|
|
||||||
"-login-server",
|
|
||||||
endpoint,
|
|
||||||
"--authkey",
|
|
||||||
key,
|
|
||||||
"--hostname",
|
|
||||||
hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Join command:", command)
|
|
||||||
log.Printf("Running join command for %s\n", hostname)
|
|
||||||
_, _, err := ExecuteCommand(
|
|
||||||
&tailscale,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
log.Printf("%s joined\n", hostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) tailscaleContainer(
|
|
||||||
identifier, version string,
|
|
||||||
network dockertest.Network,
|
|
||||||
) (string, *dockertest.Resource) {
|
|
||||||
tailscaleBuildOptions := getDockerBuildOptions(version)
|
|
||||||
|
|
||||||
hostname := fmt.Sprintf(
|
|
||||||
"tailscale-%s-%s",
|
|
||||||
strings.Replace(version, ".", "-", -1),
|
|
||||||
identifier,
|
|
||||||
)
|
|
||||||
tailscaleOptions := &dockertest.RunOptions{
|
|
||||||
Name: hostname,
|
|
||||||
Networks: []*dockertest.Network{&network},
|
|
||||||
Cmd: []string{
|
|
||||||
"tailscaled", "--tun=tsdev",
|
|
||||||
},
|
|
||||||
|
|
||||||
// expose the host IP address, so we can access it from inside the container
|
|
||||||
ExtraHosts: []string{
|
|
||||||
"host.docker.internal:host-gateway",
|
|
||||||
"headscale:host-gateway",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pts, err := s.pool.BuildAndRunWithBuildOptions(
|
|
||||||
tailscaleBuildOptions,
|
|
||||||
tailscaleOptions,
|
|
||||||
DockerRestartPolicy,
|
|
||||||
DockerAllowLocalIPv6,
|
|
||||||
DockerAllowNetworkAdministration,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Could not start tailscale container version %s: %s", version, err)
|
|
||||||
}
|
|
||||||
log.Printf("Created %s container\n", hostname)
|
|
||||||
|
|
||||||
return hostname, pts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) TearDownSuite() {
|
|
||||||
if !s.saveLogs {
|
|
||||||
for _, tailscale := range s.tailscales {
|
|
||||||
if err := s.pool.Purge(&tailscale); err != nil {
|
|
||||||
log.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.pool.Purge(&s.headscale); err != nil {
|
|
||||||
log.Printf("Could not purge resource: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, network := range s.containerNetworks {
|
|
||||||
if err := network.Close(); err != nil {
|
|
||||||
log.Printf("Could not close network: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) HandleStats(
|
|
||||||
suiteName string,
|
|
||||||
stats *suite.SuiteInformation,
|
|
||||||
) {
|
|
||||||
s.stats = stats
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) saveLog(
|
|
||||||
resource *dockertest.Resource,
|
|
||||||
basePath string,
|
|
||||||
) error {
|
|
||||||
err := os.MkdirAll(basePath, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
|
|
||||||
err = s.pool.Client.Logs(
|
|
||||||
docker.LogsOptions{
|
|
||||||
Context: context.TODO(),
|
|
||||||
Container: resource.Container.ID,
|
|
||||||
OutputStream: &stdout,
|
|
||||||
ErrorStream: &stderr,
|
|
||||||
Tail: "all",
|
|
||||||
RawTerminal: false,
|
|
||||||
Stdout: true,
|
|
||||||
Stderr: true,
|
|
||||||
Follow: false,
|
|
||||||
Timestamps: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
|
||||||
|
|
||||||
err = os.WriteFile(
|
|
||||||
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
|
||||||
stderr.Bytes(),
|
|
||||||
0o644,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(
|
|
||||||
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
|
||||||
stderr.Bytes(),
|
|
||||||
0o644,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
|
|
||||||
hostnames, err := getDNSNames(&s.headscale)
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
|
|
||||||
log.Printf("Hostnames: %#v\n", hostnames)
|
|
||||||
|
|
||||||
for hostname, tailscale := range s.tailscales {
|
|
||||||
for _, peername := range hostnames {
|
|
||||||
if strings.Contains(peername, hostname) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
|
||||||
command := []string{
|
|
||||||
"tailscale", "ping",
|
|
||||||
"--timeout=10s",
|
|
||||||
"--c=5",
|
|
||||||
"--until-direct=false",
|
|
||||||
peername,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf(
|
|
||||||
"Pinging using hostname from %s to %s\n",
|
|
||||||
hostname,
|
|
||||||
peername,
|
|
||||||
)
|
|
||||||
log.Println(command)
|
|
||||||
result, _, err := ExecuteCommand(
|
|
||||||
&tailscale,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
log.Printf("Result for %s: %s\n", hostname, result)
|
|
||||||
assert.Contains(t, result, "via DERP(headscale)")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationDERPTestSuite) TestDERPSTUN() {
|
|
||||||
headscaleSTUNAddr := fmt.Sprintf("%s:%s",
|
|
||||||
s.headscale.GetIPInNetwork(&s.network),
|
|
||||||
s.headscale.GetPort("3478/udp"))
|
|
||||||
client := stun.NewClient()
|
|
||||||
client.SetVerbose(true)
|
|
||||||
client.SetVVerbose(true)
|
|
||||||
client.SetServerAddr(headscaleSTUNAddr)
|
|
||||||
_, _, err := client.Discover()
|
|
||||||
assert.Nil(s.T(), err)
|
|
||||||
}
|
|
||||||
141
machine.go
141
machine.go
@@ -8,12 +8,12 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
"go4.org/netipx"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@@ -98,6 +98,14 @@ func (ma MachineAddresses) ToStringSlice() []string {
|
|||||||
return strSlice
|
return strSlice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppendToIPSet adds the individual ips in MachineAddresses to a
|
||||||
|
// given netipx.IPSetBuilder.
|
||||||
|
func (ma MachineAddresses) AppendToIPSet(build *netipx.IPSetBuilder) {
|
||||||
|
for _, ip := range ma {
|
||||||
|
build.Add(ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (ma *MachineAddresses) Scan(destination interface{}) error {
|
func (ma *MachineAddresses) Scan(destination interface{}) error {
|
||||||
switch value := destination.(type) {
|
switch value := destination.(type) {
|
||||||
case string:
|
case string:
|
||||||
@@ -161,125 +169,48 @@ func (machine *Machine) isEphemeral() bool {
|
|||||||
return machine.AuthKey != nil && machine.AuthKey.Ephemeral
|
return machine.AuthKey != nil && machine.AuthKey.Ephemeral
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (machine *Machine) canAccess(filter []tailcfg.FilterRule, machine2 *Machine) bool {
|
||||||
|
for _, rule := range filter {
|
||||||
|
// TODO(kradalby): Cache or pregen this
|
||||||
|
matcher := MatchFromFilterRule(rule)
|
||||||
|
|
||||||
|
if !matcher.SrcsContainsIPs([]netip.Addr(machine.IPAddresses)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if matcher.DestsContainsIP([]netip.Addr(machine2.IPAddresses)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// filterMachinesByACL wrapper function to not have devs pass around locks and maps
|
// filterMachinesByACL wrapper function to not have devs pass around locks and maps
|
||||||
// related to the application outside of tests.
|
// related to the application outside of tests.
|
||||||
func (h *Headscale) filterMachinesByACL(currentMachine *Machine, peers Machines) Machines {
|
func (h *Headscale) filterMachinesByACL(currentMachine *Machine, peers Machines) Machines {
|
||||||
return filterMachinesByACL(currentMachine, peers, &h.aclPeerCacheMapRW, h.aclPeerCacheMap)
|
return filterMachinesByACL(currentMachine, peers, h.aclRules)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterMachinesByACL returns the list of peers authorized to be accessed from a given machine.
|
// filterMachinesByACL returns the list of peers authorized to be accessed from a given machine.
|
||||||
func filterMachinesByACL(
|
func filterMachinesByACL(
|
||||||
machine *Machine,
|
machine *Machine,
|
||||||
machines Machines,
|
machines Machines,
|
||||||
lock *sync.RWMutex,
|
filter []tailcfg.FilterRule,
|
||||||
aclPeerCacheMap map[string]map[string]struct{},
|
|
||||||
) Machines {
|
) Machines {
|
||||||
log.Trace().
|
result := Machines{}
|
||||||
Caller().
|
|
||||||
Str("self", machine.Hostname).
|
|
||||||
Str("input", machines.String()).
|
|
||||||
Msg("Finding peers filtered by ACLs")
|
|
||||||
|
|
||||||
peers := make(map[uint64]Machine)
|
for index, peer := range machines {
|
||||||
// Aclfilter peers here. We are itering through machines in all users and search through the computed aclRules
|
|
||||||
// for match between rule SrcIPs and DstPorts. If the rule is a match we allow the machine to be viewable.
|
|
||||||
machineIPs := machine.IPAddresses.ToStringSlice()
|
|
||||||
|
|
||||||
// TODO(kradalby): Remove this lock, I suspect its not a good idea, and might not be necessary,
|
|
||||||
// we only set this at startup atm (reading ACLs) and it might become a bottleneck.
|
|
||||||
lock.RLock()
|
|
||||||
|
|
||||||
for _, peer := range machines {
|
|
||||||
if peer.ID == machine.ID {
|
if peer.ID == machine.ID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
peerIPs := peer.IPAddresses.ToStringSlice()
|
|
||||||
|
|
||||||
if dstMap, ok := aclPeerCacheMap["*"]; ok {
|
if machine.canAccess(filter, &machines[index]) || peer.canAccess(filter, machine) {
|
||||||
// match source and all destination
|
result = append(result, peer)
|
||||||
if _, dstOk := dstMap["*"]; dstOk {
|
|
||||||
peers[peer.ID] = peer
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// match source and all destination
|
|
||||||
for _, peerIP := range peerIPs {
|
|
||||||
if _, dstOk := dstMap[peerIP]; dstOk {
|
|
||||||
peers[peer.ID] = peer
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// match all sources and source
|
|
||||||
for _, machineIP := range machineIPs {
|
|
||||||
if _, dstOk := dstMap[machineIP]; dstOk {
|
|
||||||
peers[peer.ID] = peer
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, machineIP := range machineIPs {
|
|
||||||
if dstMap, ok := aclPeerCacheMap[machineIP]; ok {
|
|
||||||
// match source and all destination
|
|
||||||
if _, dstOk := dstMap["*"]; dstOk {
|
|
||||||
peers[peer.ID] = peer
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// match source and destination
|
|
||||||
for _, peerIP := range peerIPs {
|
|
||||||
if _, dstOk := dstMap[peerIP]; dstOk {
|
|
||||||
peers[peer.ID] = peer
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, peerIP := range peerIPs {
|
|
||||||
if dstMap, ok := aclPeerCacheMap[peerIP]; ok {
|
|
||||||
// match source and all destination
|
|
||||||
if _, dstOk := dstMap["*"]; dstOk {
|
|
||||||
peers[peer.ID] = peer
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// match return path
|
|
||||||
for _, machineIP := range machineIPs {
|
|
||||||
if _, dstOk := dstMap[machineIP]; dstOk {
|
|
||||||
peers[peer.ID] = peer
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock.RUnlock()
|
return result
|
||||||
|
|
||||||
authorizedPeers := make(Machines, 0, len(peers))
|
|
||||||
for _, m := range peers {
|
|
||||||
authorizedPeers = append(authorizedPeers, m)
|
|
||||||
}
|
|
||||||
sort.Slice(
|
|
||||||
authorizedPeers,
|
|
||||||
func(i, j int) bool { return authorizedPeers[i].ID < authorizedPeers[j].ID },
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Trace().
|
|
||||||
Caller().
|
|
||||||
Str("self", machine.Hostname).
|
|
||||||
Str("peers", authorizedPeers.String()).
|
|
||||||
Msg("Authorized peers")
|
|
||||||
|
|
||||||
return authorizedPeers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
|
func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
|
||||||
@@ -868,7 +799,7 @@ func getTags(
|
|||||||
validTagMap := make(map[string]bool)
|
validTagMap := make(map[string]bool)
|
||||||
invalidTagMap := make(map[string]bool)
|
invalidTagMap := make(map[string]bool)
|
||||||
for _, tag := range machine.HostInfo.RequestTags {
|
for _, tag := range machine.HostInfo.RequestTags {
|
||||||
owners, err := expandTagOwners(*aclPolicy, tag, stripEmailDomain)
|
owners, err := getTagOwners(aclPolicy, tag, stripEmailDomain)
|
||||||
if errors.Is(err, errInvalidTag) {
|
if errors.Is(err, errInvalidTag) {
|
||||||
invalidTagMap[tag] = true
|
invalidTagMap[tag] = true
|
||||||
|
|
||||||
@@ -1182,7 +1113,7 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error {
|
|||||||
if approvedAlias == machine.User.Name {
|
if approvedAlias == machine.User.Name {
|
||||||
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
||||||
} else {
|
} else {
|
||||||
approvedIps, err := expandAlias([]Machine{*machine}, *h.aclPolicy, approvedAlias, h.cfg.OIDC.StripEmaildomain)
|
approvedIps, err := h.aclPolicy.expandAlias([]Machine{*machine}, approvedAlias, h.cfg.OIDC.StripEmaildomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).
|
log.Err(err).
|
||||||
Str("alias", approvedAlias).
|
Str("alias", approvedAlias).
|
||||||
@@ -1192,7 +1123,7 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// approvedIPs should contain all of machine's IPs if it matches the rule, so check for first
|
// approvedIPs should contain all of machine's IPs if it matches the rule, so check for first
|
||||||
if contains(approvedIps, machine.IPAddresses[0].String()) {
|
if approvedIps.Contains(machine.IPAddresses[0]) {
|
||||||
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
135
machine_test.go
135
machine_test.go
@@ -6,7 +6,6 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -1041,16 +1040,12 @@ func Test_getFilteredByACLPeers(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
var lock sync.RWMutex
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
aclRulesMap := generateACLPeerCacheMap(tt.args.rules)
|
|
||||||
|
|
||||||
got := filterMachinesByACL(
|
got := filterMachinesByACL(
|
||||||
tt.args.machine,
|
tt.args.machine,
|
||||||
tt.args.machines,
|
tt.args.machines,
|
||||||
&lock,
|
tt.args.rules,
|
||||||
aclRulesMap,
|
|
||||||
)
|
)
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("filterMachinesByACL() = %v, want %v", got, tt.want)
|
t.Errorf("filterMachinesByACL() = %v, want %v", got, tt.want)
|
||||||
@@ -1264,3 +1259,131 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
|
|||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(enabledRoutes, check.HasLen, 3)
|
c.Assert(enabledRoutes, check.HasLen, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMachine_canAccess(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
filter []tailcfg.FilterRule
|
||||||
|
machine2 *Machine
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
machine Machine
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no-rules",
|
||||||
|
machine: Machine{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: []tailcfg.FilterRule{},
|
||||||
|
machine2: &Machine{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard",
|
||||||
|
machine: Machine{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"*"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
|
{
|
||||||
|
IP: "*",
|
||||||
|
Ports: tailcfg.PortRange{
|
||||||
|
First: 0,
|
||||||
|
Last: 65535,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
machine2: &Machine{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit-m1-to-m2",
|
||||||
|
machine: Machine{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"10.0.0.1"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
|
{
|
||||||
|
IP: "10.0.0.2",
|
||||||
|
Ports: tailcfg.PortRange{
|
||||||
|
First: 0,
|
||||||
|
Last: 65535,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
machine2: &Machine{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit-m2-to-m1",
|
||||||
|
machine: Machine{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"10.0.0.2"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
|
{
|
||||||
|
IP: "10.0.0.1",
|
||||||
|
Ports: tailcfg.PortRange{
|
||||||
|
First: 0,
|
||||||
|
Last: 65535,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
machine2: &Machine{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := tt.machine.canAccess(tt.args.filter, tt.args.machine2); got != tt.want {
|
||||||
|
t.Errorf("Machine.canAccess() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
142
matcher.go
Normal file
142
matcher.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go4.org/netipx"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is borrowed from, and updated to use IPSet
|
||||||
|
// https://github.com/tailscale/tailscale/blob/71029cea2ddf82007b80f465b256d027eab0f02d/wgengine/filter/tailcfg.go#L97-L162
|
||||||
|
// TODO(kradalby): contribute upstream and make public.
|
||||||
|
var (
|
||||||
|
zeroIP4 = netip.AddrFrom4([4]byte{})
|
||||||
|
zeroIP6 = netip.AddrFrom16([16]byte{})
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseIPSet parses arg as one:
|
||||||
|
//
|
||||||
|
// - an IP address (IPv4 or IPv6)
|
||||||
|
// - the string "*" to match everything (both IPv4 & IPv6)
|
||||||
|
// - a CIDR (e.g. "192.168.0.0/16")
|
||||||
|
// - a range of two IPs, inclusive, separated by hyphen ("2eff::1-2eff::0800")
|
||||||
|
//
|
||||||
|
// bits, if non-nil, is the legacy SrcBits CIDR length to make a IP
|
||||||
|
// address (without a slash) treated as a CIDR of *bits length.
|
||||||
|
// nolint
|
||||||
|
func parseIPSet(arg string, bits *int) (*netipx.IPSet, error) {
|
||||||
|
var ipSet netipx.IPSetBuilder
|
||||||
|
if arg == "*" {
|
||||||
|
ipSet.AddPrefix(netip.PrefixFrom(zeroIP4, 0))
|
||||||
|
ipSet.AddPrefix(netip.PrefixFrom(zeroIP6, 0))
|
||||||
|
|
||||||
|
return ipSet.IPSet()
|
||||||
|
}
|
||||||
|
if strings.Contains(arg, "/") {
|
||||||
|
pfx, err := netip.ParsePrefix(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if pfx != pfx.Masked() {
|
||||||
|
return nil, fmt.Errorf("%v contains non-network bits set", pfx)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipSet.AddPrefix(pfx)
|
||||||
|
|
||||||
|
return ipSet.IPSet()
|
||||||
|
}
|
||||||
|
if strings.Count(arg, "-") == 1 {
|
||||||
|
ip1s, ip2s, _ := strings.Cut(arg, "-")
|
||||||
|
|
||||||
|
ip1, err := netip.ParseAddr(ip1s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ip2, err := netip.ParseAddr(ip2s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := netipx.IPRangeFrom(ip1, ip2)
|
||||||
|
if !r.IsValid() {
|
||||||
|
return nil, fmt.Errorf("invalid IP range %q", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prefix := range r.Prefixes() {
|
||||||
|
ipSet.AddPrefix(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipSet.IPSet()
|
||||||
|
}
|
||||||
|
ip, err := netip.ParseAddr(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid IP address %q", arg)
|
||||||
|
}
|
||||||
|
bits8 := uint8(ip.BitLen())
|
||||||
|
if bits != nil {
|
||||||
|
if *bits < 0 || *bits > int(bits8) {
|
||||||
|
return nil, fmt.Errorf("invalid CIDR size %d for IP %q", *bits, arg)
|
||||||
|
}
|
||||||
|
bits8 = uint8(*bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipSet.AddPrefix(netip.PrefixFrom(ip, int(bits8)))
|
||||||
|
|
||||||
|
return ipSet.IPSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Match struct {
|
||||||
|
Srcs *netipx.IPSet
|
||||||
|
Dests *netipx.IPSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchFromFilterRule(rule tailcfg.FilterRule) Match {
|
||||||
|
srcs := new(netipx.IPSetBuilder)
|
||||||
|
dests := new(netipx.IPSetBuilder)
|
||||||
|
|
||||||
|
for _, srcIP := range rule.SrcIPs {
|
||||||
|
set, _ := parseIPSet(srcIP, nil)
|
||||||
|
|
||||||
|
srcs.AddSet(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dest := range rule.DstPorts {
|
||||||
|
set, _ := parseIPSet(dest.IP, nil)
|
||||||
|
|
||||||
|
dests.AddSet(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcsSet, _ := srcs.IPSet()
|
||||||
|
destsSet, _ := dests.IPSet()
|
||||||
|
|
||||||
|
match := Match{
|
||||||
|
Srcs: srcsSet,
|
||||||
|
Dests: destsSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Match) SrcsContainsIPs(ips []netip.Addr) bool {
|
||||||
|
for _, ip := range ips {
|
||||||
|
if m.Srcs.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Match) DestsContainsIP(ips []netip.Addr) bool {
|
||||||
|
for _, ip := range ips {
|
||||||
|
if m.Dests.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
119
matcher_test.go
Normal file
119
matcher_test.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go4.org/netipx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_parseIPSet(t *testing.T) {
|
||||||
|
set := func(ips []string, prefixes []string) *netipx.IPSet {
|
||||||
|
var builder netipx.IPSetBuilder
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
builder.Add(netip.MustParseAddr(ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pre := range prefixes {
|
||||||
|
builder.AddPrefix(netip.MustParsePrefix(pre))
|
||||||
|
}
|
||||||
|
|
||||||
|
s, _ := builder.IPSet()
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
arg string
|
||||||
|
bits *int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *netipx.IPSet
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple ip4",
|
||||||
|
args: args{
|
||||||
|
arg: "10.0.0.1",
|
||||||
|
bits: nil,
|
||||||
|
},
|
||||||
|
want: set([]string{
|
||||||
|
"10.0.0.1",
|
||||||
|
}, []string{}),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple ip6",
|
||||||
|
args: args{
|
||||||
|
arg: "2001:db8:abcd:1234::2",
|
||||||
|
bits: nil,
|
||||||
|
},
|
||||||
|
want: set([]string{
|
||||||
|
"2001:db8:abcd:1234::2",
|
||||||
|
}, []string{}),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard",
|
||||||
|
args: args{
|
||||||
|
arg: "*",
|
||||||
|
bits: nil,
|
||||||
|
},
|
||||||
|
want: set([]string{}, []string{
|
||||||
|
"0.0.0.0/0",
|
||||||
|
"::/0",
|
||||||
|
}),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefix4",
|
||||||
|
args: args{
|
||||||
|
arg: "192.168.0.0/16",
|
||||||
|
bits: nil,
|
||||||
|
},
|
||||||
|
want: set([]string{}, []string{
|
||||||
|
"192.168.0.0/16",
|
||||||
|
}),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefix6",
|
||||||
|
args: args{
|
||||||
|
arg: "2001:db8:abcd:1234::/64",
|
||||||
|
bits: nil,
|
||||||
|
},
|
||||||
|
want: set([]string{}, []string{
|
||||||
|
"2001:db8:abcd:1234::/64",
|
||||||
|
}),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "range4",
|
||||||
|
args: args{
|
||||||
|
arg: "192.168.0.0-192.168.255.255",
|
||||||
|
bits: nil,
|
||||||
|
},
|
||||||
|
want: set([]string{}, []string{
|
||||||
|
"192.168.0.0/16",
|
||||||
|
}),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := parseIPSet(tt.args.arg, tt.args.bits)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("parseIPSet() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("parseIPSet() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
site_name: Headscale
|
site_name: Headscale
|
||||||
site_url: https://juanfont.github.io/headscale
|
site_url: https://juanfont.github.io/headscale
|
||||||
|
edit_uri: blob/main/docs/ # Change the master branch to main as we are using main as a main branch
|
||||||
site_author: Headscale authors
|
site_author: Headscale authors
|
||||||
site_description: >-
|
site_description: >-
|
||||||
An open source, self-hosted implementation of the Tailscale control server.
|
An open source, self-hosted implementation of the Tailscale control server.
|
||||||
@@ -121,6 +122,7 @@ markdown_extensions:
|
|||||||
# Page tree
|
# Page tree
|
||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
|
- FAQ: faq.md
|
||||||
- Getting started:
|
- Getting started:
|
||||||
- Installation:
|
- Installation:
|
||||||
- Linux: running-headscale-linux.md
|
- Linux: running-headscale-linux.md
|
||||||
|
|||||||
114
noise.go
114
noise.go
@@ -1,6 +1,9 @@
|
|||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -9,18 +12,37 @@ import (
|
|||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
"tailscale.com/control/controlbase"
|
"tailscale.com/control/controlbase"
|
||||||
"tailscale.com/control/controlhttp"
|
"tailscale.com/control/controlhttp"
|
||||||
"tailscale.com/net/netutil"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade.
|
// ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade.
|
||||||
ts2021UpgradePath = "/ts2021"
|
ts2021UpgradePath = "/ts2021"
|
||||||
|
|
||||||
|
// The first 9 bytes from the server to client over Noise are either an HTTP/2
|
||||||
|
// settings frame (a normal HTTP/2 setup) or, as Tailscale added later, an "early payload"
|
||||||
|
// header that's also 9 bytes long: 5 bytes (earlyPayloadMagic) followed by 4 bytes
|
||||||
|
// of length. Then that many bytes of JSON-encoded tailcfg.EarlyNoise.
|
||||||
|
// The early payload is optional. Some servers may not send it... But we do!
|
||||||
|
earlyPayloadMagic = "\xff\xff\xffTS"
|
||||||
|
|
||||||
|
// EarlyNoise was added in protocol version 49.
|
||||||
|
earlyNoiseCapabilityVersion = 49
|
||||||
)
|
)
|
||||||
|
|
||||||
type ts2021App struct {
|
type noiseServer struct {
|
||||||
headscale *Headscale
|
headscale *Headscale
|
||||||
|
|
||||||
conn *controlbase.Conn
|
httpBaseConfig *http.Server
|
||||||
|
http2Server *http2.Server
|
||||||
|
conn *controlbase.Conn
|
||||||
|
machineKey key.MachinePublic
|
||||||
|
nodeKey key.NodePublic
|
||||||
|
|
||||||
|
// EarlyNoise-related stuff
|
||||||
|
challenge key.ChallengePrivate
|
||||||
|
protocolVersion int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn
|
// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn
|
||||||
@@ -44,7 +66,18 @@ func (h *Headscale) NoiseUpgradeHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
noiseConn, err := controlhttp.AcceptHTTP(req.Context(), writer, req, *h.noisePrivateKey, nil)
|
noiseServer := noiseServer{
|
||||||
|
headscale: h,
|
||||||
|
challenge: key.NewChallenge(),
|
||||||
|
}
|
||||||
|
|
||||||
|
noiseConn, err := controlhttp.AcceptHTTP(
|
||||||
|
req.Context(),
|
||||||
|
writer,
|
||||||
|
req,
|
||||||
|
*h.noisePrivateKey,
|
||||||
|
noiseServer.earlyNoise,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("noise upgrade failed")
|
log.Error().Err(err).Msg("noise upgrade failed")
|
||||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||||
@@ -52,10 +85,9 @@ func (h *Headscale) NoiseUpgradeHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ts2021App := ts2021App{
|
noiseServer.conn = noiseConn
|
||||||
headscale: h,
|
noiseServer.machineKey = noiseServer.conn.Peer()
|
||||||
conn: noiseConn,
|
noiseServer.protocolVersion = noiseServer.conn.ProtocolVersion()
|
||||||
}
|
|
||||||
|
|
||||||
// This router is served only over the Noise connection, and exposes only the new API.
|
// This router is served only over the Noise connection, and exposes only the new API.
|
||||||
//
|
//
|
||||||
@@ -63,16 +95,70 @@ func (h *Headscale) NoiseUpgradeHandler(
|
|||||||
// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
|
// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|
||||||
router.HandleFunc("/machine/register", ts2021App.NoiseRegistrationHandler).
|
router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler).
|
||||||
Methods(http.MethodPost)
|
Methods(http.MethodPost)
|
||||||
router.HandleFunc("/machine/map", ts2021App.NoisePollNetMapHandler)
|
router.HandleFunc("/machine/map", noiseServer.NoisePollNetMapHandler)
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
ReadTimeout: HTTPReadTimeout,
|
ReadTimeout: HTTPReadTimeout,
|
||||||
}
|
}
|
||||||
server.Handler = h2c.NewHandler(router, &http2.Server{})
|
|
||||||
err = server.Serve(netutil.NewOneConnListener(noiseConn, nil))
|
noiseServer.httpBaseConfig = &http.Server{
|
||||||
if err != nil {
|
Handler: router,
|
||||||
log.Info().Err(err).Msg("The HTTP2 server was closed")
|
ReadHeaderTimeout: HTTPReadTimeout,
|
||||||
}
|
}
|
||||||
|
noiseServer.http2Server = &http2.Server{}
|
||||||
|
|
||||||
|
server.Handler = h2c.NewHandler(router, noiseServer.http2Server)
|
||||||
|
|
||||||
|
noiseServer.http2Server.ServeConn(
|
||||||
|
noiseConn,
|
||||||
|
&http2.ServeConnOpts{
|
||||||
|
BaseConfig: noiseServer.httpBaseConfig,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error {
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Int("protocol_version", protocolVersion).
|
||||||
|
Str("challenge", ns.challenge.Public().String()).
|
||||||
|
Msg("earlyNoise called")
|
||||||
|
|
||||||
|
if protocolVersion < earlyNoiseCapabilityVersion {
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Msgf("protocol version %d does not support early noise", protocolVersion)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
earlyJSON, err := json.Marshal(&tailcfg.EarlyNoise{
|
||||||
|
NodeKeyChallenge: ns.challenge.Public(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5 bytes that won't be mistaken for an HTTP/2 frame:
|
||||||
|
// https://httpwg.org/specs/rfc7540.html#rfc.section.4.1 (Especially not
|
||||||
|
// an HTTP/2 settings frame, which isn't of type 'T')
|
||||||
|
var notH2Frame [5]byte
|
||||||
|
copy(notH2Frame[:], earlyPayloadMagic)
|
||||||
|
var lenBuf [4]byte
|
||||||
|
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(earlyJSON)))
|
||||||
|
// These writes are all buffered by caller, so fine to do them
|
||||||
|
// separately:
|
||||||
|
if _, err := writer.Write(notH2Frame[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := writer.Write(lenBuf[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := writer.Write(earlyJSON); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// // NoiseRegistrationHandler handles the actual registration process of a machine.
|
// // NoiseRegistrationHandler handles the actual registration process of a machine.
|
||||||
func (t *ts2021App) NoiseRegistrationHandler(
|
func (ns *noiseServer) NoiseRegistrationHandler(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
) {
|
) {
|
||||||
@@ -20,6 +20,11 @@ func (t *ts2021App) NoiseRegistrationHandler(
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Trace().
|
||||||
|
Any("headers", req.Header).
|
||||||
|
Msg("Headers")
|
||||||
|
|
||||||
body, _ := io.ReadAll(req.Body)
|
body, _ := io.ReadAll(req.Body)
|
||||||
registerRequest := tailcfg.RegisterRequest{}
|
registerRequest := tailcfg.RegisterRequest{}
|
||||||
if err := json.Unmarshal(body, ®isterRequest); err != nil {
|
if err := json.Unmarshal(body, ®isterRequest); err != nil {
|
||||||
@@ -33,5 +38,7 @@ func (t *ts2021App) NoiseRegistrationHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.headscale.handleRegisterCommon(writer, req, registerRequest, t.conn.Peer(), true)
|
ns.nodeKey = registerRequest.NodeKey
|
||||||
|
|
||||||
|
ns.headscale.handleRegisterCommon(writer, req, registerRequest, ns.conn.Peer(), true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,18 @@ import (
|
|||||||
// only after their first request (marked with the ReadOnly field).
|
// only after their first request (marked with the ReadOnly field).
|
||||||
//
|
//
|
||||||
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
|
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
|
||||||
func (t *ts2021App) NoisePollNetMapHandler(
|
func (ns *noiseServer) NoisePollNetMapHandler(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
) {
|
) {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Str("handler", "NoisePollNetMap").
|
Str("handler", "NoisePollNetMap").
|
||||||
Msg("PollNetMapHandler called")
|
Msg("PollNetMapHandler called")
|
||||||
|
|
||||||
|
log.Trace().
|
||||||
|
Any("headers", req.Header).
|
||||||
|
Msg("Headers")
|
||||||
|
|
||||||
body, _ := io.ReadAll(req.Body)
|
body, _ := io.ReadAll(req.Body)
|
||||||
|
|
||||||
mapRequest := tailcfg.MapRequest{}
|
mapRequest := tailcfg.MapRequest{}
|
||||||
@@ -41,7 +46,9 @@ func (t *ts2021App) NoisePollNetMapHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
machine, err := t.headscale.GetMachineByAnyKey(t.conn.Peer(), mapRequest.NodeKey, key.NodePublic{})
|
ns.nodeKey = mapRequest.NodeKey
|
||||||
|
|
||||||
|
machine, err := ns.headscale.GetMachineByAnyKey(ns.conn.Peer(), mapRequest.NodeKey, key.NodePublic{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.Warn().
|
log.Warn().
|
||||||
@@ -63,5 +70,5 @@ func (t *ts2021App) NoisePollNetMapHandler(
|
|||||||
Str("machine", machine.Hostname).
|
Str("machine", machine.Hostname).
|
||||||
Msg("A machine is entering polling via the Noise protocol")
|
Msg("A machine is entering polling via the Noise protocol")
|
||||||
|
|
||||||
t.headscale.handlePollCommon(writer, req.Context(), machine, mapRequest, true)
|
ns.headscale.handlePollCommon(writer, req.Context(), machine, mapRequest, true)
|
||||||
}
|
}
|
||||||
|
|||||||
54
routes.go
54
routes.go
@@ -106,13 +106,36 @@ func (h *Headscale) DisableRoute(id uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
route.Enabled = false
|
// Tailscale requires both IPv4 and IPv6 exit routes to
|
||||||
route.IsPrimary = false
|
// be enabled at the same time, as per
|
||||||
err = h.db.Save(route).Error
|
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
|
||||||
|
if !route.isExitRoute() {
|
||||||
|
route.Enabled = false
|
||||||
|
route.IsPrimary = false
|
||||||
|
err = h.db.Save(route).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.handlePrimarySubnetFailover()
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := h.GetMachineRoutes(&route.Machine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range routes {
|
||||||
|
if routes[i].isExitRoute() {
|
||||||
|
routes[i].Enabled = false
|
||||||
|
routes[i].IsPrimary = false
|
||||||
|
err = h.db.Save(&routes[i]).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return h.handlePrimarySubnetFailover()
|
return h.handlePrimarySubnetFailover()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +145,30 @@ func (h *Headscale) DeleteRoute(id uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.Unscoped().Delete(&route).Error; err != nil {
|
// Tailscale requires both IPv4 and IPv6 exit routes to
|
||||||
|
// be enabled at the same time, as per
|
||||||
|
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
|
||||||
|
if !route.isExitRoute() {
|
||||||
|
if err := h.db.Unscoped().Delete(&route).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.handlePrimarySubnetFailover()
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := h.GetMachineRoutes(&route.Machine)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
routesToDelete := []Route{}
|
||||||
|
for _, r := range routes {
|
||||||
|
if r.isExitRoute() {
|
||||||
|
routesToDelete = append(routesToDelete, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.Unscoped().Delete(&routesToDelete).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -457,6 +457,37 @@ func (s *Suite) TestAllowedIPRoutes(c *check.C) {
|
|||||||
|
|
||||||
c.Assert(foundExitNodeV4, check.Equals, true)
|
c.Assert(foundExitNodeV4, check.Equals, true)
|
||||||
c.Assert(foundExitNodeV6, check.Equals, true)
|
c.Assert(foundExitNodeV6, check.Equals, true)
|
||||||
|
|
||||||
|
// Now we disable only one of the exit routes
|
||||||
|
// and we see if both are disabled
|
||||||
|
var exitRouteV4 Route
|
||||||
|
for _, route := range routes {
|
||||||
|
if route.isExitRoute() && netip.Prefix(route.Prefix) == prefixExitNodeV4 {
|
||||||
|
exitRouteV4 = route
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.DisableRoute(uint64(exitRouteV4.ID))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
enabledRoutes1, err = app.GetEnabledRoutes(&machine1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(enabledRoutes1), check.Equals, 1)
|
||||||
|
|
||||||
|
// and now we delete only one of the exit routes
|
||||||
|
// and we check if both are deleted
|
||||||
|
routes, err = app.GetMachineRoutes(&machine1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(routes), check.Equals, 4)
|
||||||
|
|
||||||
|
err = app.DeleteRoute(uint64(exitRouteV4.ID))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
routes, err = app.GetMachineRoutes(&machine1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(routes), check.Equals, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestDeleteRoutes(c *check.C) {
|
func (s *Suite) TestDeleteRoutes(c *check.C) {
|
||||||
|
|||||||
Reference in New Issue
Block a user