Compare commits

..

92 Commits

Author SHA1 Message Date
Kristoffer Dalby
5596a0acef Merge pull request #297 from arch4ngel/configurable-mtls
Configurable mtls
2022-02-24 11:32:02 +00:00
Kristoffer Dalby
8c33907655 Sort lint 2022-02-24 11:10:40 +00:00
Kristoffer Dalby
afb67b6e75 Merge branch 'main' into configurable-mtls 2022-02-24 11:09:05 +00:00
Kristoffer Dalby
c46dfd761c Merge pull request #349 from kradalby/remove-cgo 2022-02-24 10:11:15 +00:00
Kristoffer Dalby
ed2175706c Merge branch 'remove-cgo' of github.com:kradalby/headscale into remove-cgo 2022-02-23 16:23:53 +00:00
Kristoffer Dalby
686e45cf27 Set all anti-cgo options and add comment 2022-02-23 16:15:20 +00:00
Kristoffer Dalby
f0a73632e0 Merge branch 'main' into remove-cgo 2022-02-23 09:01:38 +00:00
Kristoffer Dalby
823cc493f0 Merge branch 'main' into configurable-mtls 2022-02-23 07:29:31 +00:00
Juan Font
a86b33f1ff Merge pull request #345 from juanfont/update-contributors
docs(README): update contributors
2022-02-22 23:46:54 +01:00
Juan Font
28c2bbeb27 Merge branch 'main' into update-contributors 2022-02-22 23:35:34 +01:00
github-actions[bot]
d4761da27c docs(README): update contributors 2022-02-22 22:34:27 +00:00
Juan Font
b0c7ebeb7d Merge pull request #351 from pernila/patch-1
Added FreeBSD to the supported clients
2022-02-22 23:33:58 +01:00
Juan Font
5f375d69b5 Merge branch 'main' into update-contributors 2022-02-22 23:32:02 +01:00
Juan Font
9eb705a4fb Merge branch 'main' into patch-1 2022-02-22 23:31:29 +01:00
Juan Font
1b87396a8c Merge pull request #333 from ohdearaugustin/topic/renovatebot
Topic/renovatebot
2022-02-22 23:30:46 +01:00
Juan Font
bb14bcd4d2 Merge branch 'main' into topic/renovatebot 2022-02-22 23:29:08 +01:00
pernila
48c866b058 Added FreeBSD to the supported clients
Added FreeBSD to the supported clients
Now in ports: https://www.freshports.org/security/headscale/
2022-02-22 23:06:35 +02:00
Kristoffer Dalby
67f5c32b49 Only allow one connection to sqlite 2022-02-22 19:04:52 +00:00
Kristoffer Dalby
bfbcea35a0 Remove dependency on CGO
This commit changes the SQLite dependency to one that does not depend on
CGO. It uses a C-to-Go translated sqlite library that is Pure go.
2022-02-22 16:51:54 +00:00
Kristoffer Dalby
a37339fa54 Merge pull request #348 from restanrm/remove-comment
fix(machine): remove comment
2022-02-22 14:06:12 +00:00
Adrien Raffin-Caboisse
f2f8d834e8 fix(machine): remove comment
After some more tests in tailscale I couldn't replicate the behavior
described in there.

When adding a rule, allowing A to talk to B the reverse connection was
instantly added to B to allow communication to B.

The previous assumption was probably wrong.
2022-02-22 11:26:21 +01:00
Kristoffer Dalby
52db6188df Merge branch 'main' into update-contributors 2022-02-21 23:38:56 +00:00
Kristoffer Dalby
f562ad579a Merge branch 'main' into configurable-mtls 2022-02-21 21:44:49 +00:00
github-actions[bot]
bbadeb567a docs(README): update contributors 2022-02-21 21:41:48 +00:00
Kristoffer Dalby
69cdfbb56f Merge pull request #320 from restanrm/feat-improve-acls-usage
Improvements on the ACLs and bug fixing
2022-02-21 21:41:15 +00:00
Adrien Raffin-Caboisse
d971f0f0e6 fix(acls_test): fix comment in go code 2022-02-21 21:48:05 +01:00
Adrien Raffin-Caboisse
650108c7c7 chore(fmt): apply fmt 2022-02-21 21:46:40 +01:00
Adrien Raffin-Caboisse
baae266db0 Update acls_test.go
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-02-21 20:25:41 +01:00
Adrien Raffin-Caboisse
50af44bc2f fix: add error checking in acl and poll
If aclPolicy is not defined, in updateAclPolicy, return an error.
2022-02-21 20:06:31 +01:00
Justin Angel
b5a59d4e7a updating changelog and docs 2022-02-21 10:20:11 -05:00
Adrien Raffin-Caboisse
211fe4034a chore(linter): ignore tt var as it's generated code (vscode) 2022-02-21 16:10:20 +01:00
Justin Angel
daa75da277 Linting and updating tests 2022-02-21 10:09:23 -05:00
Adrien Raffin-Caboisse
25550f8866 chore(format): run prettier on repo 2022-02-21 16:06:20 +01:00
Adrien Raffin-Caboisse
4bbe0051f6 chore(machines): apply lint 2022-02-21 10:02:59 +01:00
Adrien Raffin-Caboisse
5ab62378ae tests(machines): test all combinations of peer filtering 2022-02-21 09:58:19 +01:00
Adrien Raffin-Caboisse
f006860136 feat(machines): untie dependency with class for filter func
The dependency to the `headscale` struct makes tests harder to do.

This change allow to easily add some tests for this quite sensible function.
2022-02-21 09:58:19 +01:00
Adrien Raffin-Caboisse
9c6ce02554 fix(machines): use ListAllMachines function
added a simple filter to remove the current node
2022-02-21 09:58:19 +01:00
Adrien Raffin-Caboisse
960412a335 fix(machines): simplify complex if check
This should fix the performance issue with computation of `dst` variable. It's also easier to read now.
2022-02-21 09:58:19 +01:00
Kristoffer Dalby
ecb3ee6bfa Merge branch 'main' into feat-improve-acls-usage 2022-02-21 08:51:21 +00:00
Adrien Raffin-Caboisse
5242025ab3 fix(machines): renaming following review comments 2022-02-20 23:50:08 +01:00
Adrien Raffin-Caboisse
b3d0fb7a93 fix(machine): revert modifications
Using h.ListAllMachines also listed the current machine in the result. It's unnecessary (I don't know if it's harmful).

Breaking the check with the `matchSourceAndDestinationWithRule` broke the tests. We have a specificity with the '*' destination that isn't symetrical.
I need to think of a better way to do this. It too hard to read.
2022-02-20 23:47:04 +01:00
Adrien Raffin-Caboisse
5e167cc00a fix(tests): fix naming issues related to code review 2022-02-20 23:00:31 +01:00
Adrien Raffin-Caboisse
d00251c63e fix(acls,machines): apply code review suggestions 2022-02-20 21:26:20 +01:00
Adrien Raffin-Caboisse
4f9ece14c5 Apply suggestions from code review on changelog
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-02-20 20:47:12 +01:00
Kristoffer Dalby
7bf2a91dd0 Merge branch 'main' into configurable-mtls 2022-02-20 14:33:23 +00:00
Justin Angel
385dd9cc34 refactoring 2022-02-20 09:06:14 -05:00
Kristoffer Dalby
602291df61 Merge pull request #338 from juanfont/update-contributors 2022-02-19 23:13:08 +00:00
github-actions[bot]
5245f1accc docs(README): update contributors 2022-02-19 22:48:26 +00:00
Kristoffer Dalby
91babb5130 Merge pull request #336 from ohdearaugustin/topic/fix-contributors-action 2022-02-19 21:08:53 +00:00
ohdearaugustin
8798efd353 contributor: set specific version 2022-02-19 21:36:08 +01:00
Kristoffer Dalby
66a12004e7 Merge branch 'main' into topic/renovatebot 2022-02-19 19:34:30 +00:00
Kristoffer Dalby
74621e2750 Merge pull request #332 from e-zk/main
Fix spelling error
2022-02-19 19:34:24 +00:00
Kristoffer Dalby
74c3c6bb60 Merge branch 'main' into main 2022-02-19 19:32:34 +00:00
Kristoffer Dalby
84b98e716a Merge pull request #334 from ohdearaugustin/topic/renovatebot-codeowner
CODEOWNER: add renovate config ohdearaugustin
2022-02-19 19:32:21 +00:00
ohdearaugustin
e9f13b6031 CODEOWNER: add renovate config ohdearaugustin 2022-02-19 20:28:08 +01:00
ohdearaugustin
fe6d47030f renovatebot: configure 2022-02-19 20:15:53 +01:00
ohdearaugustin
a19550adbf prettier: renovatebot.yml 2022-02-19 19:49:12 +01:00
ohdearaugustin
3db88d27de github/workflows: init renovatebot 2022-02-19 19:49:12 +01:00
e-zk
a6b7bc5939 Fix spelling error 2022-02-20 03:14:51 +10:00
Kristoffer Dalby
7d5e6d3f0f Merge pull request #330 from kradalby/codeowners
Add ohdearaugustin to CODEOWNERS for config and docs
2022-02-18 20:12:29 +00:00
Kristoffer Dalby
7a90c2fba1 Merge branch 'main' into codeowners 2022-02-18 20:11:33 +00:00
Kristoffer Dalby
5cf215a44b Merge pull request #325 from juanfont/kradalby-patch-4
Update changelog for 0.13.0
2022-02-18 20:11:03 +00:00
Kristoffer Dalby
7916fa8b45 Add ohdearaugustin to CODEOWNERS for config and docs 2022-02-18 19:57:03 +00:00
Kristoffer Dalby
5fbef07627 Update changelog for 0.13.0 2022-02-18 18:54:27 +00:00
Kristoffer Dalby
21df798f07 Merge branch 'main' into feat-improve-acls-usage 2022-02-18 17:19:19 +00:00
Kristoffer Dalby
67bb1fc9dd Merge pull request #324 from m-tanner-dev0/patch-1 2022-02-18 07:18:22 +00:00
Tanner
61bfa79be2 Update README.md
change flippant language
2022-02-17 17:55:40 -08:00
Adrien Raffin-Caboisse
f073d8f43c chore(lint): ignore linting on test_expandalias
This is a false positive on the way the function is built.
Small tests cases are all inside this functions, making it big.
2022-02-17 09:32:55 +01:00
Adrien Raffin-Caboisse
5f642eef76 chore(lint): more lint fixing 2022-02-17 09:32:54 +01:00
Adrien Raffin-Caboisse
d8c4c3163b chore(fmt): apply make fmt command 2022-02-17 09:32:54 +01:00
Adrien Raffin-Caboisse
9cedbbafd4 chore(all): update some files for linter 2022-02-17 09:32:51 +01:00
Adrien Raffin-Caboisse
aceaba60f1 docs(changelog): bump changelog 2022-02-17 09:30:09 +01:00
Adrien Raffin-Caboisse
7b5ba9f781 docs(acl): add configuration example to explain acls 2022-02-17 09:30:09 +01:00
Adrien Raffin
de59946447 feat(acls): rewrite functions to be testable
Rewrite some function to get rid of the dependency on Headscale object. This allows us
to write succinct test that are more easy to review and implement.

The improvements of the tests allowed to write the removal of the tagged hosts
from the namespace as specified here: https://tailscale.com/kb/1068/acl-tags/
2022-02-17 09:30:09 +01:00
Adrien Raffin
97eac3b938 feat(acl): update frequently the aclRules
This call should be done quite at each modification of a server resources like RequestTags.
When a server changes it's tag we should rebuild the ACL rules.

When a server is added to headscale we also should update the ACLRules.
2022-02-17 09:30:08 +01:00
Adrien Raffin
fb45138fc1 feat(acls): check acl owners and add bunch of tests 2022-02-17 09:30:08 +01:00
Adrien Raffin
e9949b4c70 feat(acls): simplify updating rules 2022-02-17 09:30:08 +01:00
Adrien Raffin
e482dfeed4 feat(machine): add ACLFilter if ACL's are enabled.
This commit change the default behaviour and remove the notion of namespaces between the hosts. It allows all namespaces to be only filtered by the ACLs. This behavior is closer to tailsnet.
2022-02-17 09:30:05 +01:00
Jamie Greeff
9b7d657cbe Return all peers instead of peers in same namespace 2022-02-17 09:27:59 +01:00
Justin Angel
1b2fff4337 Merge branch 'main' into configurable-mtls 2022-02-02 11:54:49 -05:00
Justin Angel
af25aa75d9 Merge branch 'configurable-mtls' of github.com:arch4ngel/headscale into configurable-mtls 2022-01-31 10:27:57 -05:00
Justin Angel
da5250ea32 linting again 2022-01-31 10:27:43 -05:00
Kristoffer Dalby
168b1bd579 Merge branch 'main' into configurable-mtls 2022-01-31 12:28:00 +00:00
Justin Angel
9de5c7f8b8 updating default 2022-01-31 07:22:17 -05:00
Justin Angel
52db80ab0d Merge branch 'configurable-mtls' of github.com:arch4ngel/headscale into configurable-mtls 2022-01-31 07:19:14 -05:00
Justin Angel
0c3fd16113 refining and adding tests 2022-01-31 07:18:50 -05:00
Justin Angel
310e7b15c7 making alternatives constants 2022-01-30 10:46:57 -05:00
Justin Angel
d44b2a7c01 adding default for tls_client_auth_mode 2022-01-30 07:26:28 -05:00
Kristoffer Dalby
0609c97459 Merge branch 'main' into configurable-mtls 2022-01-29 20:15:58 +00:00
Justin Angel
c98a559b4d linting/formatting 2022-01-29 14:15:33 -05:00
Justin Angel
5935b13b67 refining 2022-01-29 13:35:08 -05:00
Justin Angel
9e619fc020 Making client authentication mode configurable 2022-01-29 12:59:31 -05:00
30 changed files with 2361 additions and 420 deletions

10
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,10 @@
* @juanfont @kradalby
*.md @ohdearaugustin
*.yml @ohdearaugustin
*.yaml @ohdearaugustin
Dockerfile* @ohdearaugustin
.goreleaser.yaml @ohdearaugustin
/docs/ @ohdearaugustin
/.github/workflows/ @ohdearaugustin
/.github/renovate.json @ohdearaugustin

38
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,38 @@
{
"baseBranches": ["main"],
"username": "renovate-release",
"gitAuthor": "Renovate Bot <bot@renovateapp.com>",
"branchPrefix": "renovateaction/",
"onboarding": false,
"extends": ["config:base", ":rebaseStalePrs"],
"ignorePresets": [":prHourlyLimit2"],
"enabledManagers": ["dockerfile", "gomod", "github-actions","regex" ],
"includeForks": true,
"repositories": ["juanfont/headscale"],
"platform": "github",
"packageRules": [
{
"matchDatasources": ["go"],
"groupName": "Go modules",
"groupSlug": "gomod",
"separateMajorMinor": false
},
{
"matchDatasources": ["docker"],
"groupName": "Dockerfiles",
"groupSlug": "dockerfiles"
}
],
"regexManagers": [
{
"fileMatch": [
".github/workflows/.*.yml$"
],
"matchStrings": [
"\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"
],
"datasourceTemplate": "golang-version",
"depNameTemplate": "actions/go-version"
}
]
}

View File

@@ -4,13 +4,13 @@ on:
push:
branches:
- main
workflow_dispatch:
jobs:
add-contributors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: BobAnkh/add-contributors@master
- uses: BobAnkh/add-contributors@v0.2.2
with:
CONTRIBUTOR: "## Contributors"
COLUMN_PER_ROW: "6"

27
.github/workflows/renovatebot.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Renovate
on:
schedule:
- cron: "* * 5,20 * *" # Every 5th and 20th of the month
workflow_dispatch:
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- name: Get token
id: get_token
uses: machine-learning-apps/actions-app-token@master
with:
APP_PEM: ${{ secrets.RENOVATEBOT_SECRET }}
APP_ID: ${{ secrets.RENOVATEBOT_APP_ID }}
- name: Checkout
uses: actions/checkout@v2.0.0
- name: Self-hosted Renovate
uses: renovatebot/github-action@v31.81.3
with:
configurationFile: .github/renovate.json
token: "x-access-token:${{ steps.get_token.outputs.app_token }}"
# env:
# LOG_LEVEL: "debug"

View File

@@ -48,6 +48,7 @@ linters-settings:
- ip
- ok
- c
- tt
gocritic:
disabled-checks:

View File

@@ -10,15 +10,12 @@ builds:
- id: darwin-amd64
main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- darwin
goarch:
- amd64
env:
- PKG_CONFIG_SYSROOT_DIR=/sysroot/macos/amd64
- PKG_CONFIG_PATH=/sysroot/macos/amd64/usr/local/lib/pkgconfig
- CC=o64-clang
- CXX=o64-clang++
flags:
- -mod=readonly
ldflags:
@@ -27,46 +24,40 @@ builds:
- id: linux-armhf
main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- arm
goarm:
- "7"
env:
- CC=arm-linux-gnueabihf-gcc
- CXX=arm-linux-gnueabihf-g++
- CGO_FLAGS=--sysroot=/sysroot/linux/armhf
- CGO_LDFLAGS=--sysroot=/sysroot/linux/armhf
- PKG_CONFIG_SYSROOT_DIR=/sysroot/linux/armhf
- PKG_CONFIG_PATH=/sysroot/linux/armhf/opt/vc/lib/pkgconfig:/sysroot/linux/armhf/usr/lib/arm-linux-gnueabihf/pkgconfig:/sysroot/linux/armhf/usr/lib/pkgconfig:/sysroot/linux/armhf/usr/local/lib/pkgconfig
flags:
- -mod=readonly
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
- id: linux-amd64
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
- id: linux-arm64
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- arm64
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc
main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}

View File

@@ -2,7 +2,33 @@
**TBD (TBD):**
**0.13.0 (2022-xx-xx):**
**0.14.0 (2022-xx-xx):**
**UPCOMING BREAKING**:
From the **next** version (`0.15.0`), all machines will be able to communicate regardless of
if they are in the same namespace. This means that the behaviour currently limited to ACLs
will become default. From version `0.15.0`, all limitation of communications must be done
with ACLs.
This is a part of aligning `headscale`'s behaviour with Tailscale's upstream behaviour.
**BREAKING**:
- ACLs have been rewritten to align with the bevaviour Tailscale Control Panel provides. **NOTE:** This is only active if you use ACLs
- Namespaces are now treated as Users
- All machines can communicate with all machines by default
- Tags should now work correctly and adding a host to Headscale should now reload the rules.
- The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features
**Features**:
- Add support for configurable mTLS [docs](docs/tls.md#configuring-mutual-tls-authentication-mtls) [#297](https://github.com/juanfont/headscale/pull/297)
**Changes**:
- Remove dependency on CGO (switch from CGO SQLite to pure Go) [#346](https://github.com/juanfont/headscale/pull/346)
**0.13.0 (2022-02-18):**
**Features**:

View File

@@ -8,7 +8,7 @@ RUN go mod download
COPY . .
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale

View File

@@ -9,7 +9,7 @@ RUN go mod download
COPY . .
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale

View File

@@ -8,7 +8,7 @@ RUN go mod download
COPY . .
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
RUN test -e /go/bin/headscale
# Debug image

View File

@@ -10,7 +10,7 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
build:
go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
GGO_ENABLED=0 go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
dev: lint test build

192
README.md
View File

@@ -10,7 +10,7 @@ Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat.
## Overview
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using all kinds of [NAT traversal sorcery](https://tailscale.com/blog/how-nat-traversal-works/).
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using [NAT traversal](https://tailscale.com/blog/how-nat-traversal-works/).
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS (Windows and macOS/iOS), and the 'coordination/control server'.
@@ -47,12 +47,13 @@ If you would like to sponsor features, bugs or prioritisation, reach out to one
| ------- | ----------------------------------------------------------------------------------------------------------------- |
| Linux | Yes |
| OpenBSD | Yes |
| FreeBSD | Yes |
| macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes [docs](./docs/windows-client.md) |
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
| iOS | Not yet |
## Roadmap 🤷
## Roadmap
Suggestions/PRs welcomed!
@@ -122,13 +123,6 @@ make build
<table>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/juanfont>
<img src=https://avatars.githubusercontent.com/u/181059?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Juan Font/>
<br />
<sub style="font-size:14px"><b>Juan Font</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kradalby>
<img src=https://avatars.githubusercontent.com/u/98431?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kristoffer Dalby/>
@@ -136,6 +130,13 @@ make build
<sub style="font-size:14px"><b>Kristoffer Dalby</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/juanfont>
<img src=https://avatars.githubusercontent.com/u/181059?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Juan Font/>
<br />
<sub style="font-size:14px"><b>Juan Font</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cure>
<img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/>
@@ -150,6 +151,22 @@ make build
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/restanrm>
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
<br />
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ItalyPaleAle>
<img src=https://avatars.githubusercontent.com/u/43508?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Alessandro (Ale) Segala/>
<br />
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/unreality>
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
@@ -157,6 +174,13 @@ make build
<sub style="font-size:14px"><b>unreality</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/negbie>
<img src=https://avatars.githubusercontent.com/u/20154956?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Eugen Biegler/>
<br />
<sub style="font-size:14px"><b>Eugen Biegler</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/qbit>
<img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/>
@@ -164,6 +188,27 @@ make build
<sub style="font-size:14px"><b>Aaron Bieber</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fdelucchijr>
<img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/>
<br />
<sub style="font-size:14px"><b>Fernando De Lucchi</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/hdhoang>
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Hoàng Đức Hiếu/>
<br />
<sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dragetd>
<img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./>
<br />
<sub style="font-size:14px"><b>Michael G.</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
@@ -187,6 +232,20 @@ make build
<sub style="font-size:14px"><b>Silver Bullet</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/majst01>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lachy-2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy-2849/>
<br />
<sub style="font-size:14px"><b>lachy-2849</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/t56k>
<img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/>
@@ -194,6 +253,22 @@ make build
<sub style="font-size:14px"><b>thomas</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aberoham>
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
<br />
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/artemklevtsov>
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
<br />
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/awoimbee>
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
@@ -201,6 +276,13 @@ make build
<sub style="font-size:14px"><b>Arthur Woimbée</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/stensonb>
<img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/>
<br />
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fkr>
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
@@ -208,8 +290,6 @@ make build
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/felixonmars>
<img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/>
@@ -217,6 +297,52 @@ make build
<sub style="font-size:14px"><b>Felix Yan</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/JJGadgets>
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
<br />
<sub style="font-size:14px"><b>JJGadgets</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/madjam002>
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
<br />
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jimt>
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
<br />
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/piec>
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
<br />
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/rcursaru>
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
<br />
<sub style="font-size:14px"><b>rcursaru</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ryanfowler>
<img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/>
<br />
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/shaananc>
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
@@ -224,6 +350,13 @@ make build
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/m-tanner-dev0>
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
<br />
<sub style="font-size:14px"><b>Tanner</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Teteros>
<img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/>
@@ -261,6 +394,13 @@ make build
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Bpazy>
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ZiYuan/>
<br />
<sub style="font-size:14px"><b>ZiYuan</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/derelm>
<img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/>
@@ -268,6 +408,13 @@ make build
<sub style="font-size:14px"><b>derelm</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
<br />
<sub style="font-size:14px"><b>e-zk</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ignoramous>
<img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
@@ -275,6 +422,29 @@ make build
<sub style="font-size:14px"><b>ignoramous</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lion24>
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lion24/>
<br />
<sub style="font-size:14px"><b>lion24</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pernila>
<img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=pernila/>
<br />
<sub style="font-size:14px"><b>pernila</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Wakeful-Cloud>
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
<br />
<sub style="font-size:14px"><b>Wakeful-Cloud</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/xpzouying>
<img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/>

233
acls.go
View File

@@ -20,7 +20,6 @@ const (
errInvalidUserSection = Error("invalid user section")
errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag")
errInvalidNamespace = Error("invalid namespace")
errInvalidPortFormat = Error("invalid port format")
)
@@ -69,13 +68,17 @@ func (h *Headscale) LoadACLPolicy(path string) error {
}
h.aclPolicy = &policy
return h.UpdateACLRules()
}
func (h *Headscale) UpdateACLRules() error {
rules, err := h.generateACLRules()
if err != nil {
return err
}
h.aclRules = rules
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
h.aclRules = rules
return nil
}
@@ -83,16 +86,23 @@ func (h *Headscale) LoadACLPolicy(path string) error {
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
rules := []tailcfg.FilterRule{}
if h.aclPolicy == nil {
return nil, errEmptyPolicy
}
machines, err := h.ListAllMachines()
if err != nil {
return nil, err
}
for index, acl := range h.aclPolicy.ACLs {
if acl.Action != "accept" {
return nil, errInvalidAction
}
filterRule := tailcfg.FilterRule{}
srcIPs := []string{}
for innerIndex, user := range acl.Users {
srcs, err := h.generateACLPolicySrcIP(user)
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
@@ -101,11 +111,10 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
}
srcIPs = append(srcIPs, srcs...)
}
filterRule.SrcIPs = srcIPs
destPorts := []tailcfg.NetPortRange{}
for innerIndex, ports := range acl.Ports {
dests, err := h.generateACLPolicyDestPorts(ports)
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
@@ -124,11 +133,17 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
return rules, nil
}
func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) {
return h.expandAlias(u)
func (h *Headscale) generateACLPolicySrcIP(
machines []Machine,
aclPolicy ACLPolicy,
u string,
) ([]string, error) {
return expandAlias(machines, aclPolicy, u)
}
func (h *Headscale) generateACLPolicyDestPorts(
machines []Machine,
aclPolicy ACLPolicy,
d string,
) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(d, ":")
@@ -149,11 +164,11 @@ func (h *Headscale) generateACLPolicyDestPorts(
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
}
expanded, err := h.expandAlias(alias)
expanded, err := expandAlias(machines, aclPolicy, alias)
if err != nil {
return nil, err
}
ports, err := h.expandPorts(tokens[len(tokens)-1])
ports, err := expandPorts(tokens[len(tokens)-1])
if err != nil {
return nil, err
}
@@ -172,21 +187,28 @@ func (h *Headscale) generateACLPolicyDestPorts(
return dests, nil
}
func (h *Headscale) expandAlias(alias string) ([]string, error) {
// expandalias has an input of either
// - a namespace
// - a group
// - a tag
// and transform these in IPAddresses.
func expandAlias(
machines []Machine,
aclPolicy ACLPolicy,
alias string,
) ([]string, error) {
ips := []string{}
if alias == "*" {
return []string{"*"}, nil
}
if strings.HasPrefix(alias, "group:") {
if _, ok := h.aclPolicy.Groups[alias]; !ok {
return nil, errInvalidGroup
namespaces, err := expandGroup(aclPolicy, alias)
if err != nil {
return ips, err
}
ips := []string{}
for _, n := range h.aclPolicy.Groups[alias] {
nodes, err := h.ListMachinesInNamespace(n)
if err != nil {
return nil, errInvalidNamespace
}
for _, n := range namespaces {
nodes := filterMachinesByNamespace(machines, n)
for _, node := range nodes {
ips = append(ips, node.IPAddresses.ToStringSlice()...)
}
@@ -196,35 +218,23 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
}
if strings.HasPrefix(alias, "tag:") {
if _, ok := h.aclPolicy.TagOwners[alias]; !ok {
return nil, errInvalidTag
owners, err := expandTagOwners(aclPolicy, alias)
if err != nil {
return ips, err
}
// This will have HORRIBLE performance.
// We need to change the data model to better store tags
machines := []Machine{}
if err := h.db.Where("registered").Find(&machines).Error; err != nil {
return nil, err
}
ips := []string{}
for _, machine := range machines {
hostinfo := tailcfg.Hostinfo{}
if len(machine.HostInfo) != 0 {
hi, err := machine.HostInfo.MarshalJSON()
if err != nil {
return nil, err
for _, namespace := range owners {
machines := filterMachinesByNamespace(machines, namespace)
for _, machine := range machines {
if len(machine.HostInfo) == 0 {
continue
}
err = json.Unmarshal(hi, &hostinfo)
hi, err := machine.GetHostInfo()
if err != nil {
return nil, err
return ips, err
}
// FIXME: Check TagOwners allows this
for _, t := range hostinfo.RequestTags {
if alias[4:] == t {
for _, t := range hi.RequestTags {
if alias == t {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
break
}
}
}
@@ -233,38 +243,82 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
return ips, nil
}
n, err := h.GetNamespace(alias)
if err == nil {
nodes, err := h.ListMachinesInNamespace(n.Name)
if err != nil {
return nil, err
}
ips := []string{}
for _, n := range nodes {
ips = append(ips, n.IPAddresses.ToStringSlice()...)
}
// if alias is a namespace
nodes := filterMachinesByNamespace(machines, alias)
nodes, err := excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
if err != nil {
return ips, err
}
for _, n := range nodes {
ips = append(ips, n.IPAddresses.ToStringSlice()...)
}
if len(ips) > 0 {
return ips, nil
}
if h, ok := h.aclPolicy.Hosts[alias]; ok {
// if alias is an host
if h, ok := aclPolicy.Hosts[alias]; ok {
return []string{h.String()}, nil
}
// if alias is an IP
ip, err := netaddr.ParseIP(alias)
if err == nil {
return []string{ip.String()}, nil
}
// if alias is an CIDR
cidr, err := netaddr.ParseIPPrefix(alias)
if err == nil {
return []string{cidr.String()}, nil
}
return nil, errInvalidUserSection
return ips, errInvalidUserSection
}
func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
// 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 namespace
// we assume in this function that we only have nodes from 1 namespace.
func excludeCorrectlyTaggedNodes(
aclPolicy ACLPolicy,
nodes []Machine,
namespace string,
) ([]Machine, error) {
out := []Machine{}
tags := []string{}
for tag, ns := range aclPolicy.TagOwners {
if containsString(ns, namespace) {
tags = append(tags, tag)
}
}
// for each machine if tag is in tags list, don't append it.
for _, machine := range nodes {
if len(machine.HostInfo) == 0 {
out = append(out, machine)
continue
}
hi, err := machine.GetHostInfo()
if err != nil {
return out, err
}
found := false
for _, t := range hi.RequestTags {
if containsString(tags, t) {
found = true
break
}
}
if !found {
out = append(out, machine)
}
}
return out, nil
}
func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
if portsStr == "*" {
return &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
@@ -306,3 +360,64 @@ func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
return &ports, nil
}
func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
out := []Machine{}
for _, machine := range machines {
if machine.Namespace.Name == namespace {
out = append(out, machine)
}
}
return out
}
// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
// a group cannot be composed of groups.
func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
var owners []string
ows, ok := aclPolicy.TagOwners[tag]
if !ok {
return []string{}, fmt.Errorf(
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
errInvalidTag,
tag,
)
}
for _, owner := range ows {
if strings.HasPrefix(owner, "group:") {
gs, err := expandGroup(aclPolicy, owner)
if err != nil {
return []string{}, err
}
owners = append(owners, gs...)
} else {
owners = append(owners, owner)
}
}
return owners, nil
}
// expandGroup will return the list of namespace inside the group
// after some validation.
func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) {
groups, ok := aclPolicy.Groups[group]
if !ok {
return []string{}, fmt.Errorf(
"group %v isn't registered. %w",
group,
errInvalidGroup,
)
}
for _, g := range groups {
if strings.HasPrefix(g, "group:") {
return []string{}, fmt.Errorf(
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
errInvalidGroup,
)
}
}
return groups, nil
}

View File

@@ -1,7 +1,14 @@
package headscale
import (
"errors"
"reflect"
"testing"
"gopkg.in/check.v1"
"gorm.io/datatypes"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
func (s *Suite) TestWrongPath(c *check.C) {
@@ -52,6 +59,245 @@ func (s *Suite) TestBasicRule(c *check.C) {
c.Assert(rules, check.NotNil)
}
// TODO(kradalby): Make tests values safe, independent and descriptive.
func (s *Suite) TestInvalidAction(c *check.C) {
app.aclPolicy = &ACLPolicy{
ACLs: []ACL{
{Action: "invalidAction", Users: []string{"*"}, Ports: []string{"*:*"}},
},
}
err := app.UpdateACLRules()
c.Assert(errors.Is(err, errInvalidAction), check.Equals, true)
}
func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
// this ACL is wrong because the group in users sections doesn't exist
app.aclPolicy = &ACLPolicy{
Groups: Groups{
"group:test": []string{"foo"},
"group:error": []string{"foo", "group:test"},
},
ACLs: []ACL{
{Action: "accept", Users: []string{"group:error"}, Ports: []string{"*:*"}},
},
}
err := app.UpdateACLRules()
c.Assert(errors.Is(err, errInvalidGroup), check.Equals, true)
}
func (s *Suite) TestInvalidTagOwners(c *check.C) {
// this ACL is wrong because no tagOwners own the requested tag for the server
app.aclPolicy = &ACLPolicy{
ACLs: []ACL{
{Action: "accept", Users: []string{"tag:foo"}, Ports: []string{"*:*"}},
},
}
err := app.UpdateACLRules()
c.Assert(errors.Is(err, errInvalidTag), check.Equals, true)
}
// this test should validate that we can expand a group in a TagOWner section and
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
// the tag is matched in the Users section.
func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
)
machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
}
app.db.Save(&machine)
app.aclPolicy = &ACLPolicy{
Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{
{Action: "accept", Users: []string{"tag:test"}, Ports: []string{"*:*"}},
},
}
err = app.UpdateACLRules()
c.Assert(err, check.IsNil)
c.Assert(app.aclRules, check.HasLen, 1)
c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1)
c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1")
}
// this test should validate that we can expand a group in a TagOWner section and
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
// the tag is matched in the Ports section.
func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
)
machine := Machine{
ID: 1,
MachineKey: "12345",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
}
app.db.Save(&machine)
app.aclPolicy = &ACLPolicy{
Groups: Groups{"group:test": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
ACLs: []ACL{
{Action: "accept", Users: []string{"*"}, Ports: []string{"tag:test:*"}},
},
}
err = app.UpdateACLRules()
c.Assert(err, check.IsNil)
c.Assert(app.aclRules, check.HasLen, 1)
c.Assert(app.aclRules[0].DstPorts, check.HasLen, 1)
c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1")
}
// need a test with:
// tag on a host that isn't owned by a tag owners. So the namespace
// of the host should be valid.
func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:foo\"]}",
)
machine := Machine{
ID: 1,
MachineKey: "12345",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
}
app.db.Save(&machine)
app.aclPolicy = &ACLPolicy{
TagOwners: TagOwners{"tag:test": []string{"user1"}},
ACLs: []ACL{
{Action: "accept", Users: []string{"user1"}, Ports: []string{"*:*"}},
},
}
err = app.UpdateACLRules()
c.Assert(err, check.IsNil)
c.Assert(app.aclRules, check.HasLen, 1)
c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1)
c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1")
}
// tag on a host is owned by a tag owner, the tag is valid.
// an ACL rule is matching the tag to a namespace. It should not be valid since the
// host should be tied to the tag now.
func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "webserver")
c.Assert(err, check.NotNil)
hostInfo := []byte(
"{\"OS\":\"centos\",\"Hostname\":\"webserver\",\"RequestTags\":[\"tag:webapp\"]}",
)
machine := Machine{
ID: 1,
MachineKey: "12345",
NodeKey: "bar",
DiscoKey: "faa",
Name: "webserver",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
}
app.db.Save(&machine)
_, err = app.GetMachine("user1", "user")
hostInfo = []byte("{\"OS\":\"debian\",\"Hostname\":\"user\"}")
c.Assert(err, check.NotNil)
machine = Machine{
ID: 2,
MachineKey: "56789",
NodeKey: "bar2",
DiscoKey: "faab",
Name: "user",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostInfo),
}
app.db.Save(&machine)
app.aclPolicy = &ACLPolicy{
TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
ACLs: []ACL{
{
Action: "accept",
Users: []string{"user1"},
Ports: []string{"tag:webapp:80,443"},
},
},
}
err = app.UpdateACLRules()
c.Assert(err, check.IsNil)
c.Assert(app.aclRules, check.HasLen, 1)
c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1)
c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.2")
c.Assert(app.aclRules[0].DstPorts, check.HasLen, 2)
c.Assert(app.aclRules[0].DstPorts[0].Ports.First, check.Equals, uint16(80))
c.Assert(app.aclRules[0].DstPorts[0].Ports.Last, check.Equals, uint16(80))
c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1")
c.Assert(app.aclRules[0].DstPorts[1].Ports.First, check.Equals, uint16(443))
c.Assert(app.aclRules[0].DstPorts[1].Ports.Last, check.Equals, uint16(443))
c.Assert(app.aclRules[0].DstPorts[1].IP, check.Equals, "100.64.0.1")
}
func (s *Suite) TestPortRange(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
c.Assert(err, check.IsNil)
@@ -94,7 +340,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
ips, _ := app.getAvailableIPs()
machine := Machine{
ID: 0,
MachineKey: "foo",
MachineKey: "12345",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
@@ -165,3 +411,723 @@ func (s *Suite) TestPortGroup(c *check.C) {
c.Assert(len(ips), check.Equals, 1)
c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String())
}
func Test_expandGroup(t *testing.T) {
type args struct {
aclPolicy ACLPolicy
group string
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{
name: "simple test",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:test": []string{"user1", "user2", "user3"},
"group:foo": []string{"user2", "user3"},
},
},
group: "group:test",
},
want: []string{"user1", "user2", "user3"},
wantErr: false,
},
{
name: "InexistantGroup",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:test": []string{"user1", "user2", "user3"},
"group:foo": []string{"user2", "user3"},
},
},
group: "group:undefined",
},
want: []string{},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := expandGroup(test.args.aclPolicy, test.args.group)
if (err != nil) != test.wantErr {
t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("expandGroup() = %v, want %v", got, test.want)
}
})
}
}
func Test_expandTagOwners(t *testing.T) {
type args struct {
aclPolicy ACLPolicy
tag string
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{
name: "simple tag expansion",
args: args{
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:test": []string{"user1"}},
},
tag: "tag:test",
},
want: []string{"user1"},
wantErr: false,
},
{
name: "expand with tag and group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{"group:foo": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo"}},
},
tag: "tag:test",
},
want: []string{"user1", "user2"},
wantErr: false,
},
{
name: "expand with namespace and group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{"group:foo": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}},
},
tag: "tag:test",
},
want: []string{"user1", "user2", "user3"},
wantErr: false,
},
{
name: "invalid tag",
args: args{
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}},
},
tag: "tag:test",
},
want: []string{},
wantErr: true,
},
{
name: "invalid group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{"group:bar": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}},
},
tag: "tag:test",
},
want: []string{},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := expandTagOwners(test.args.aclPolicy, test.args.tag)
if (err != nil) != test.wantErr {
t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("expandTagOwners() = %v, want %v", got, test.want)
}
})
}
}
func Test_expandPorts(t *testing.T) {
type args struct {
portsStr string
}
tests := []struct {
name string
args args
want *[]tailcfg.PortRange
wantErr bool
}{
{
name: "wildcard",
args: args{portsStr: "*"},
want: &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
},
wantErr: false,
},
{
name: "two ports",
args: args{portsStr: "80,443"},
want: &[]tailcfg.PortRange{
{First: 80, Last: 80},
{First: 443, Last: 443},
},
wantErr: false,
},
{
name: "a range and a port",
args: args{portsStr: "80-1024,443"},
want: &[]tailcfg.PortRange{
{First: 80, Last: 1024},
{First: 443, Last: 443},
},
wantErr: false,
},
{
name: "out of bounds",
args: args{portsStr: "854038"},
want: nil,
wantErr: true,
},
{
name: "wrong port",
args: args{portsStr: "85a38"},
want: nil,
wantErr: true,
},
{
name: "wrong port in first",
args: args{portsStr: "a-80"},
want: nil,
wantErr: true,
},
{
name: "wrong port in last",
args: args{portsStr: "80-85a38"},
want: nil,
wantErr: true,
},
{
name: "wrong port format",
args: args{portsStr: "80-85a38-3"},
want: nil,
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := expandPorts(test.args.portsStr)
if (err != nil) != test.wantErr {
t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("expandPorts() = %v, want %v", got, test.want)
}
})
}
}
func Test_listMachinesInNamespace(t *testing.T) {
type args struct {
machines []Machine
namespace string
}
tests := []struct {
name string
args args
want []Machine
}{
{
name: "1 machine in namespace",
args: args{
machines: []Machine{
{Namespace: Namespace{Name: "joe"}},
},
namespace: "joe",
},
want: []Machine{
{Namespace: Namespace{Name: "joe"}},
},
},
{
name: "3 machines, 2 in namespace",
args: args{
machines: []Machine{
{ID: 1, Namespace: Namespace{Name: "joe"}},
{ID: 2, Namespace: Namespace{Name: "marc"}},
{ID: 3, Namespace: Namespace{Name: "marc"}},
},
namespace: "marc",
},
want: []Machine{
{ID: 2, Namespace: Namespace{Name: "marc"}},
{ID: 3, Namespace: Namespace{Name: "marc"}},
},
},
{
name: "5 machines, 0 in namespace",
args: args{
machines: []Machine{
{ID: 1, Namespace: Namespace{Name: "joe"}},
{ID: 2, Namespace: Namespace{Name: "marc"}},
{ID: 3, Namespace: Namespace{Name: "marc"}},
{ID: 4, Namespace: Namespace{Name: "marc"}},
{ID: 5, Namespace: Namespace{Name: "marc"}},
},
namespace: "mickael",
},
want: []Machine{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := filterMachinesByNamespace(test.args.machines, test.args.namespace); !reflect.DeepEqual(
got,
test.want,
) {
t.Errorf("listMachinesInNamespace() = %v, want %v", got, test.want)
}
})
}
}
// nolint
func Test_expandAlias(t *testing.T) {
type args struct {
machines []Machine
aclPolicy ACLPolicy
alias string
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{
name: "wildcard",
args: args{
alias: "*",
machines: []Machine{
{IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.78.84.227"),
},
},
},
aclPolicy: ACLPolicy{},
},
want: []string{"*"},
wantErr: false,
},
{
name: "simple group",
args: args{
alias: "group:accountant",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
},
aclPolicy: ACLPolicy{
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
},
},
want: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
wantErr: false,
},
{
name: "wrong group",
args: args{
alias: "group:hr",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
},
aclPolicy: ACLPolicy{
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
},
},
want: []string{},
wantErr: true,
},
{
name: "simple ipaddress",
args: args{
alias: "10.0.0.3",
machines: []Machine{},
aclPolicy: ACLPolicy{},
},
want: []string{"10.0.0.3"},
wantErr: false,
},
{
name: "private network",
args: args{
alias: "homeNetwork",
machines: []Machine{},
aclPolicy: ACLPolicy{
Hosts: Hosts{
"homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"),
},
},
},
want: []string{"192.168.1.0/24"},
wantErr: false,
},
{
name: "simple host",
args: args{
alias: "10.0.0.1",
machines: []Machine{},
aclPolicy: ACLPolicy{},
},
want: []string{"10.0.0.1"},
wantErr: false,
},
{
name: "simple CIDR",
args: args{
alias: "10.0.0.0/16",
machines: []Machine{},
aclPolicy: ACLPolicy{},
},
want: []string{"10.0.0.0/16"},
wantErr: false,
},
{
name: "simple tag",
args: args{
alias: "tag:hr-webserver",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}},
},
},
want: []string{"100.64.0.1", "100.64.0.2"},
wantErr: false,
},
{
name: "No tag defined",
args: args{
alias: "tag:hr-webserver",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "mickael"},
},
},
aclPolicy: ACLPolicy{
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
TagOwners: TagOwners{
"tag:accountant-webserver": []string{"group:accountant"},
},
},
},
want: []string{},
wantErr: true,
},
{
name: "list host in namespace without correctly tagged servers",
args: args{
alias: "joe",
machines: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "marc"},
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
},
},
want: []string{"100.64.0.4"},
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := expandAlias(
test.args.machines,
test.args.aclPolicy,
test.args.alias,
)
if (err != nil) != test.wantErr {
t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("expandAlias() = %v, want %v", got, test.want)
}
})
}
}
func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
type args struct {
aclPolicy ACLPolicy
nodes []Machine
namespace string
}
tests := []struct {
name string
args args
want []Machine
wantErr bool
}{
{
name: "exclude nodes with valid tags",
args: args{
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
},
nodes: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
namespace: "joe",
},
want: []Machine{
{
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
Namespace: Namespace{Name: "joe"},
},
},
wantErr: false,
},
{
name: "all nodes have invalid tags, don't exclude them",
args: args{
aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
},
nodes: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
namespace: "joe",
},
want: []Machine{
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "joe"},
HostInfo: []byte(
"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
),
},
{
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.4"),
},
Namespace: Namespace{Name: "joe"},
},
},
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := excludeCorrectlyTaggedNodes(
test.args.aclPolicy,
test.args.nodes,
test.args.namespace,
)
if (err != nil) != test.wantErr {
t.Errorf(
"excludeCorrectlyTaggedNodes() error = %v, wantErr %v",
err,
test.wantErr,
)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)
}
})
}
}

22
api.go
View File

@@ -261,7 +261,16 @@ func (h *Headscale) getMapResponse(
var respBody []byte
if req.Compress == "zstd" {
src, _ := json.Marshal(resp)
src, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Str("func", "getMapResponse").
Err(err).
Msg("Failed to marshal response for the client")
return nil, err
}
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
@@ -290,7 +299,16 @@ func (h *Headscale) getMapKeepAliveResponse(
var respBody []byte
var err error
if mapRequest.Compress == "zstd" {
src, _ := json.Marshal(mapResponse)
src, err := json.Marshal(mapResponse)
if err != nil {
log.Error().
Caller().
Str("func", "getMapKeepAliveResponse").
Err(err).
Msg("Failed to marshal keepalive response for the client")
return nil, err
}
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
respBody = h.privateKey.SealTo(machineKey, srcCompressed)

38
app.go
View File

@@ -62,6 +62,10 @@ const (
errUnsupportedLetsEncryptChallengeType = Error(
"unknown value for Lets Encrypt challenge type",
)
DisabledClientAuth = "disabled"
RelaxedClientAuth = "relaxed"
EnforcedClientAuth = "enforced"
)
// Config contains the initial Headscale configuration.
@@ -90,8 +94,9 @@ type Config struct {
TLSLetsEncryptCacheDir string
TLSLetsEncryptChallengeType string
TLSCertPath string
TLSKeyPath string
TLSCertPath string
TLSKeyPath string
TLSClientAuthMode tls.ClientAuthType
ACMEURL string
ACMEEmail string
@@ -150,6 +155,27 @@ type Headscale struct {
requestedExpiryCache *cache.Cache
}
// Look up the TLS constant relative to user-supplied TLS client
// authentication mode. If an unknown mode is supplied, the default
// value, tls.RequireAnyClientCert, is returned. The returned boolean
// indicates if the supplied mode was valid.
func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
switch mode {
case DisabledClientAuth:
// Client cert is _not_ required.
return tls.NoClientCert, true
case RelaxedClientAuth:
// Client cert required, but _not verified_.
return tls.RequireAnyClientCert, true
case EnforcedClientAuth:
// Client cert is _required and verified_.
return tls.RequireAndVerifyClientCert, true
default:
// Return the default when an unknown value is supplied.
return tls.RequireAnyClientCert, false
}
}
// NewHeadscale returns the Headscale app.
func NewHeadscale(cfg Config) (*Headscale, error) {
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
@@ -676,12 +702,18 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
}
log.Info().Msg(fmt.Sprintf(
"Client authentication (mTLS) is \"%s\". See the docs to learn about configuring this setting.",
h.cfg.TLSClientAuthMode))
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAnyClientCert,
ClientAuth: h.cfg.TLSClientAuthMode,
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12,
}
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
return tlsConfig, err

View File

@@ -65,3 +65,20 @@ func (s *Suite) ResetDB(c *check.C) {
}
app.db = db
}
// Enusre an error is returned when an invalid auth mode
// is supplied.
func (s *Suite) TestInvalidClientAuthMode(c *check.C) {
_, isValid := LookupTLSClientAuthMode("invalid")
c.Assert(isValid, check.Equals, false)
}
// Ensure that all client auth modes return a nil error.
func (s *Suite) TestAuthModes(c *check.C) {
modes := []string{"disabled", "relaxed", "enforced"}
for _, v := range modes {
_, isValid := LookupTLSClientAuthMode(v)
c.Assert(isValid, check.Equals, true)
}
}

View File

@@ -50,6 +50,7 @@ func LoadConfig(path string) error {
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
viper.SetDefault("tls_client_auth_mode", "relaxed")
viper.SetDefault("log_level", "info")
@@ -92,6 +93,20 @@ func LoadConfig(path string) error {
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
errorText += "Fatal config error: server_url must start with https:// or http://\n"
}
_, authModeValid := headscale.LookupTLSClientAuthMode(
viper.GetString("tls_client_auth_mode"),
)
if !authModeValid {
errorText += fmt.Sprintf(
"Invalid tls_client_auth_mode supplied: %s. Accepted values: %s, %s, %s.",
viper.GetString("tls_client_auth_mode"),
headscale.DisabledClientAuth,
headscale.RelaxedClientAuth,
headscale.EnforcedClientAuth)
}
if errorText != "" {
//nolint
return errors.New(strings.TrimSuffix(errorText, "\n"))
@@ -281,6 +296,10 @@ func getHeadscaleConfig() headscale.Config {
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
}
tlsClientAuthMode, _ := headscale.LookupTLSClientAuthMode(
viper.GetString("tls_client_auth_mode"),
)
return headscale.Config{
ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"),
@@ -312,8 +331,9 @@ func getHeadscaleConfig() headscale.Config {
),
TLSLetsEncryptChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
TLSClientAuthMode: tlsClientAuthMode,
DNSConfig: dnsConfig,

View File

@@ -105,6 +105,13 @@ acme_email: ""
# Domain name to request a TLS certificate for:
tls_letsencrypt_hostname: ""
# Client (Tailscale/Browser) authentication mode (mTLS)
# Acceptable values:
# - disabled: client authentication disabled
# - relaxed: client certificate is required but not verified
# - enforced: client certificate is required and verified
tls_client_auth_mode: relaxed
# Path to store certificates and metadata needed by
# letsencrypt
tls_letsencrypt_cache_dir: /var/lib/headscale/cache

25
db.go
View File

@@ -2,9 +2,10 @@ package headscale
import (
"errors"
"time"
"github.com/glebarez/sqlite"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
@@ -81,10 +82,24 @@ func (h *Headscale) openDB() (*gorm.DB, error) {
switch h.dbType {
case Sqlite:
db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: log,
})
db, err = gorm.Open(
sqlite.Open(h.dbString+"?_synchronous=1&_journal_mode=WAL"),
&gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: log,
},
)
db.Exec("PRAGMA foreign_keys=ON")
// The pure Go SQLite library does not handle locking in
// the same way as the C based one and we cant use the gorm
// connection pool as of 2022/02/23.
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(1)
sqlDB.SetMaxOpenConns(1)
sqlDB.SetConnMaxIdleTime(time.Hour)
case Postgres:
db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,

20
dns.go
View File

@@ -163,7 +163,15 @@ func getMapResponseDNSConfig(
dnsConfig = dnsConfigOrig.Clone()
dnsConfig.Domains = append(
dnsConfig.Domains,
fmt.Sprintf("%s.%s", machine.Namespace.Name, baseDomain),
fmt.Sprintf(
"%s.%s",
strings.ReplaceAll(
machine.Namespace.Name,
"@",
".",
), // Replace @ with . for valid domain for machine
baseDomain,
),
)
namespaceSet := set.New(set.ThreadSafe)
@@ -171,8 +179,14 @@ func getMapResponseDNSConfig(
for _, p := range peers {
namespaceSet.Add(p.Namespace)
}
for _, namespace := range namespaceSet.List() {
dnsRoute := fmt.Sprintf("%s.%s", namespace.(Namespace).Name, baseDomain)
for _, ns := range namespaceSet.List() {
namespace, ok := ns.(Namespace)
if !ok {
dnsConfig = dnsConfigOrig
continue
}
dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain)
dnsConfig.Routes[dnsRoute] = nil
}
} else {

View File

@@ -10,7 +10,7 @@ please ask on [Discord](https://discord.gg/XcQxk2VHjx) instead of opening an Iss
### How-to
- [Running headscale on Linux](running-headscale-linux.md)
- [Control headscale remotly](remote-cli.md)
- [Control headscale remotely](remote-cli.md)
- [Using a Windows client with headscale](windows-client.md)
### References
@@ -39,6 +39,14 @@ use namespaces (which are the equivalent to user/logins in Tailscale.com).
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
When using ACL's the Namespace borders are no longer applied. All machines
whichever the Namespace have the ability to communicate with other hosts as
long as the ACL's permits this exchange.
The [ACLs](acls.md) document should help understand a fictional case of setting
up ACLs in a small company. All concepts presented in this document could be
applied outside of business oriented usage.
### Apple devices
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.

141
docs/acls.md Normal file
View File

@@ -0,0 +1,141 @@
# ACLs use case example
Let's build an example use case for a small business (It may be the place where
ACL's are the most useful).
We have a small company with a boss, an admin, two developers and an intern.
The boss should have access to all servers but not to the users hosts. Admin
should also have access to all hosts except that their permissions should be
limited to maintaining the hosts (for example purposes). The developers can do
anything they want on dev hosts, but only watch on productions hosts. Intern
can only interact with the development servers.
Each user have at least a device connected to the network and we have some
servers.
- database.prod
- database.dev
- app-server1.prod
- app-server1.dev
- billing.internal
## Setup of the network
Let's create the namespaces. Each user should have his own namespace. The users
here are represented as namespaces.
```bash
headscale namespaces create boss
headscale namespaces create admin1
headscale namespaces create dev1
headscale namespaces create dev2
headscale namespaces create intern1
```
We don't need to create namespaces for the servers because the servers will be
tagged. When registering the servers we will need to add the flag
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
registering the server should be allowed to do it. Since anyone can add tags to
a server they can register, the check of the tags is done on headscale server
and only valid tags are applied. A tag is valid if the namespace that is
registering it is allowed to do it.
Here are the ACL's to implement the same permissions as above:
```json
{
// groups are collections of users having a common scope. A user can be in multiple groups
// groups cannot be composed of groups
"groups": {
"group:boss": ["boss"],
"group:dev": ["dev1", "dev2"],
"group:admin": ["admin1"],
"group:intern": ["intern1"]
},
// tagOwners in tailscale is an association between a TAG and the people allowed to set this TAG on a server.
// This is documented [here](https://tailscale.com/kb/1068/acl-tags#defining-a-tag)
// and explained [here](https://tailscale.com/blog/rbac-like-it-was-meant-to-be/)
"tagOwners": {
// the administrators can add servers in production
"tag:prod-databases": ["group:admin"],
"tag:prod-app-servers": ["group:admin"],
// the boss can tag any server as internal
"tag:internal": ["group:boss"],
// dev can add servers for dev purposes as well as admins
"tag:dev-databases": ["group:admin", "group:dev"],
"tag:dev-app-servers": ["group:admin", "group:dev"]
// interns cannot add servers
},
"acls": [
// boss have access to all servers
{
"action": "accept",
"users": ["group:boss"],
"ports": [
"tag:prod-databases:*",
"tag:prod-app-servers:*",
"tag:internal:*",
"tag:dev-databases:*",
"tag:dev-app-servers:*"
]
},
// admin have only access to administrative ports of the servers
{
"action": "accept",
"users": ["group:admin"],
"ports": [
"tag:prod-databases:22",
"tag:prod-app-servers:22",
"tag:internal:22",
"tag:dev-databases:22",
"tag:dev-app-servers:22"
]
},
// developers have access to databases servers and application servers on all ports
// they can only view the applications servers in prod and have no access to databases servers in production
{
"action": "accept",
"users": ["group:dev"],
"ports": [
"tag:dev-databases:*",
"tag:dev-app-servers:*",
"tag:prod-app-servers:80,443"
]
},
// servers should be able to talk to database. Database should not be able to initiate connections to
// applications servers
{
"action": "accept",
"users": ["tag:dev-app-servers"],
"ports": ["tag:dev-databases:5432"]
},
{
"action": "accept",
"users": ["tag:prod-app-servers"],
"ports": ["tag:prod-databases:5432"]
},
// interns have access to dev-app-servers only in reading mode
{
"action": "accept",
"users": ["group:intern"],
"ports": ["tag:dev-app-servers:80,443"]
},
// We still have to allow internal namespaces communications since nothing guarantees that each user have
// their own namespaces.
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
]
}
```

View File

@@ -29,3 +29,17 @@ headscale can also be configured to expose its web service via TLS. To configure
tls_cert_path: ""
tls_key_path: ""
```
### Configuring Mutual TLS Authentication (mTLS)
mTLS is a method by which an HTTPS server authenticates clients, e.g. Tailscale, using TLS certificates. This can be configured by applying one of the following values to the `tls_client_auth_mode` setting in the configuration file.
| Value | Behavior |
| ------------------- | ---------------------------------------------------------- |
| `disabled` | Disable mTLS. |
| `relaxed` (default) | A client certificate is required, but it is not verified. |
| `enforced` | Requires clients to supply a certificate that is verified. |
```yaml
tls_client_auth_mode: ""
```

21
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/efekarakus/termcolor v1.0.1
github.com/fatih/set v0.2.1
github.com/gin-gonic/gin v1.7.7
github.com/glebarez/sqlite v1.3.5
github.com/gofrs/uuid v4.2.0+incompatible
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3
@@ -19,14 +20,13 @@ require (
github.com/prometheus/client_golang v1.12.1
github.com/pterm/pterm v0.12.36
github.com/rs/zerolog v1.26.1
github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/tailscale/hujson v0.0.0-20211215203138-ffd971c5f362
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
github.com/zsais/go-gin-prometheus v0.1.0
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
google.golang.org/genproto v0.0.0-20220210181026-6fee9acbd336
@@ -36,9 +36,8 @@ require (
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0
gorm.io/datatypes v1.0.5
gorm.io/driver/postgres v1.2.3
gorm.io/driver/sqlite v1.2.6
gorm.io/gorm v1.22.5
gorm.io/driver/postgres v1.3.1
gorm.io/gorm v1.23.1
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
tailscale.com v1.20.4
)
@@ -58,8 +57,8 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.14.7 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
@@ -70,6 +69,7 @@ require (
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gookit/color v1.5.0 // indirect
github.com/hashicorp/go-version v1.4.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@@ -112,6 +112,7 @@ require (
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
@@ -136,6 +137,12 @@ require (
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gorm.io/driver/mysql v1.2.3 // indirect
gorm.io/driver/mysql v1.3.2 // indirect
gorm.io/driver/sqlite v1.3.1 // indirect
gorm.io/driver/sqlserver v1.3.1 // indirect
modernc.org/libc v1.14.3 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.0.5 // indirect
modernc.org/sqlite v1.14.5 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

428
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -119,6 +119,103 @@ func (machine Machine) isExpired() bool {
return time.Now().UTC().After(*machine.Expiry)
}
func (h *Headscale) ListAllMachines() ([]Machine, error) {
machines := []Machine{}
if err := h.db.Preload("AuthKey").
Preload("AuthKey.Namespace").
Preload("Namespace").
Where("registered").
Find(&machines).Error; err != nil {
return nil, err
}
return machines, nil
}
func containsAddresses(inputs []string, addrs []string) bool {
for _, addr := range addrs {
if containsString(inputs, addr) {
return true
}
}
return false
}
// matchSourceAndDestinationWithRule.
func matchSourceAndDestinationWithRule(
ruleSources []string,
ruleDestinations []string,
source []string,
destination []string,
) bool {
return containsAddresses(ruleSources, source) &&
containsAddresses(ruleDestinations, destination)
}
// getFilteredByACLPeerss should return the list of peers authorized to be accessed from machine.
func getFilteredByACLPeers(
machines []Machine,
rules []tailcfg.FilterRule,
machine *Machine,
) Machines {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("Finding peers filtered by ACLs")
peers := make(map[uint64]Machine)
// Aclfilter peers here. We are itering through machines in all namespaces 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.
for _, peer := range machines {
if peer.ID == machine.ID {
continue
}
for _, rule := range rules {
var dst []string
for _, d := range rule.DstPorts {
dst = append(dst, d.IP)
}
if matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
machine.IPAddresses.ToStringSlice(),
peer.IPAddresses.ToStringSlice(),
) || // match source and destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
machine.IPAddresses.ToStringSlice(),
[]string{"*"},
) || // match source and all destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
peer.IPAddresses.ToStringSlice(),
machine.IPAddresses.ToStringSlice(),
) { // match return path
peers[peer.ID] = peer
}
}
}
authorizedPeers := make([]Machine, 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("machine", machine.Name).
Msgf("Found some machines: %v", machines)
return authorizedPeers
}
func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
log.Trace().
Caller().
@@ -206,39 +303,54 @@ func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
}
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
direct, err := h.getDirectPeers(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
var peers Machines
var err error
return Machines{}, err
// If ACLs rules are defined, filter visible host list with the ACLs
// else use the classic namespace scope
if h.aclPolicy != nil {
var machines []Machine
machines, err = h.ListAllMachines()
if err != nil {
log.Error().Err(err).Msg("Error retrieving list of machines")
return Machines{}, err
}
peers = getFilteredByACLPeers(machines, h.aclRules, machine)
} else {
direct, err := h.getDirectPeers(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
return Machines{}, err
}
shared, err := h.getShared(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
return Machines{}, err
}
sharedTo, err := h.getSharedTo(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
return Machines{}, err
}
peers = append(direct, shared...)
peers = append(peers, sharedTo...)
}
shared, err := h.getShared(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
return Machines{}, err
}
sharedTo, err := h.getSharedTo(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
return Machines{}, err
}
peers := append(direct, shared...)
peers = append(peers, sharedTo...)
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
log.Trace().
@@ -597,7 +709,11 @@ func (machine Machine) toNode(
hostname = fmt.Sprintf(
"%s.%s.%s",
machine.Name,
machine.Namespace.Name,
strings.ReplaceAll(
machine.Namespace.Name,
"@",
".",
), // Replace @ with . for valid domain for machine
baseDomain,
)
} else {

View File

@@ -1,11 +1,15 @@
package headscale
import (
"fmt"
"reflect"
"strconv"
"testing"
"time"
"gopkg.in/check.v1"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
func (s *Suite) TestGetMachine(c *check.C) {
@@ -154,6 +158,89 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
c.Assert(peersOfMachine0[8].Name, check.Equals, "testmachine10")
}
func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
type base struct {
namespace *Namespace
key *PreAuthKey
}
stor := make([]base, 0)
for _, name := range []string{"test", "admin"} {
namespace, err := app.CreateNamespace(name)
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
stor = append(stor, base{namespace, pak})
}
_, err := app.GetMachineByID(0)
c.Assert(err, check.NotNil)
for index := 0; index <= 10; index++ {
machine := Machine{
ID: uint64(index),
MachineKey: "foo" + strconv.Itoa(index),
NodeKey: "bar" + strconv.Itoa(index),
DiscoKey: "faa" + strconv.Itoa(index),
IPAddresses: MachineAddresses{
netaddr.MustParseIP(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))),
},
Name: "testmachine" + strconv.Itoa(index),
NamespaceID: stor[index%2].namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(stor[index%2].key.ID),
}
app.db.Save(&machine)
}
app.aclPolicy = &ACLPolicy{
Groups: map[string][]string{
"group:test": {"admin"},
},
Hosts: map[string]netaddr.IPPrefix{},
TagOwners: map[string][]string{},
ACLs: []ACL{
{Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}},
{Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}},
},
Tests: []ACLTest{},
}
err = app.UpdateACLRules()
c.Assert(err, check.IsNil)
adminMachine, err := app.GetMachineByID(1)
c.Logf("Machine(%v), namespace: %v", adminMachine.Name, adminMachine.Namespace)
c.Assert(err, check.IsNil)
testMachine, err := app.GetMachineByID(2)
c.Logf("Machine(%v), namespace: %v", testMachine.Name, testMachine.Namespace)
c.Assert(err, check.IsNil)
_, err = testMachine.GetHostInfo()
c.Assert(err, check.IsNil)
machines, err := app.ListAllMachines()
c.Assert(err, check.IsNil)
peersOfTestMachine := getFilteredByACLPeers(machines, app.aclRules, testMachine)
peersOfAdminMachine := getFilteredByACLPeers(machines, app.aclRules, adminMachine)
c.Log(peersOfTestMachine)
c.Assert(len(peersOfTestMachine), check.Equals, 4)
c.Assert(peersOfTestMachine[0].Name, check.Equals, "testmachine4")
c.Assert(peersOfTestMachine[1].Name, check.Equals, "testmachine6")
c.Assert(peersOfTestMachine[3].Name, check.Equals, "testmachine10")
c.Log(peersOfAdminMachine)
c.Assert(len(peersOfAdminMachine), check.Equals, 9)
c.Assert(peersOfAdminMachine[0].Name, check.Equals, "testmachine2")
c.Assert(peersOfAdminMachine[2].Name, check.Equals, "testmachine4")
c.Assert(peersOfAdminMachine[5].Name, check.Equals, "testmachine7")
}
func (s *Suite) TestExpireMachine(c *check.C) {
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
@@ -208,3 +295,178 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
c.Assert(deserialized[i], check.Equals, input[i])
}
}
func Test_getFilteredByACLPeers(t *testing.T) {
type args struct {
machines []Machine
rules []tailcfg.FilterRule
machine *Machine
}
tests := []struct {
name string
args args
want Machines
}{
{
name: "all hosts can talk to each other",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
DstPorts: []tailcfg.NetPortRange{
{IP: "*"},
},
},
},
machine: &Machine{ // current machine
ID: 1,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
Namespace: Namespace{Name: "joe"},
},
},
want: Machines{
{
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
Namespace: Namespace{Name: "mickael"},
},
},
},
{
name: "One host can talk to another, but not all hosts",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
machine: &Machine{ // current machine
ID: 1,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
Namespace: Namespace{Name: "joe"},
},
},
want: Machines{
{
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
},
{
name: "host cannot directly talk to destination, but return path is authorized",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"100.64.0.3"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
machine: &Machine{ // current machine
ID: 1,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{
{
ID: 3,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
Namespace: Namespace{Name: "mickael"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getFilteredByACLPeers(
tt.args.machines,
tt.args.rules,
tt.args.machine,
)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getFilteredByACLPeers() = %v, want %v", got, tt.want)
}
})
}
}

28
poll.go
View File

@@ -85,12 +85,26 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
Str("machine", machine.Name).
Msg("Found machine in database")
hostinfo, _ := json.Marshal(req.Hostinfo)
hostinfo, err := json.Marshal(req.Hostinfo)
if err != nil {
return
}
machine.Name = req.Hostinfo.Hostname
machine.HostInfo = datatypes.JSON(hostinfo)
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
now := time.Now().UTC()
// update ACLRules with peer informations (to update server tags if necessary)
if h.aclPolicy != nil {
err = h.UpdateACLRules()
if err != nil {
log.Error().
Caller().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Err(err)
}
}
// From Tailscale client:
//
// ReadOnly is whether the client just wants to fetch the MapResponse,
@@ -100,7 +114,17 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
// The intended use is for clients to discover the DERP map at start-up
// before their first real endpoint update.
if !req.ReadOnly {
endpoints, _ := json.Marshal(req.Endpoints)
endpoints, err := json.Marshal(req.Endpoints)
if err != nil {
log.Error().
Caller().
Str("func", "PollNetMapHandler").
Err(err).
Msg("Failed to mashal requested endpoints for the client")
ctx.String(http.StatusInternalServerError, ":(")
return
}
machine.Endpoints = datatypes.JSON(endpoints)
machine.LastSeen = &now
}

View File

@@ -212,6 +212,16 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
return ips, nil
}
func containsString(ss []string, s string) bool {
for _, v := range ss {
if v == s {
return true
}
}
return false
}
func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool {
for _, v := range ips {
if v == ip {