Merge branch 'main' into metrics-listen
This commit is contained in:
commit
d27f2bc538
23 changed files with 2101 additions and 419 deletions
38
.github/renovate.json
vendored
Normal file
38
.github/renovate.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
27
.github/workflows/renovatebot.yml
vendored
Normal file
27
.github/workflows/renovatebot.yml
vendored
Normal 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"
|
|
@ -48,6 +48,7 @@ linters-settings:
|
||||||
- ip
|
- ip
|
||||||
- ok
|
- ok
|
||||||
- c
|
- c
|
||||||
|
- tt
|
||||||
|
|
||||||
gocritic:
|
gocritic:
|
||||||
disabled-checks:
|
disabled-checks:
|
||||||
|
|
|
@ -10,15 +10,12 @@ builds:
|
||||||
- id: darwin-amd64
|
- id: darwin-amd64
|
||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
- darwin
|
- darwin
|
||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- 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:
|
flags:
|
||||||
- -mod=readonly
|
- -mod=readonly
|
||||||
ldflags:
|
ldflags:
|
||||||
|
@ -27,46 +24,40 @@ builds:
|
||||||
- id: linux-armhf
|
- id: linux-armhf
|
||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
goarch:
|
goarch:
|
||||||
- arm
|
- arm
|
||||||
goarm:
|
goarm:
|
||||||
- "7"
|
- "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:
|
flags:
|
||||||
- -mod=readonly
|
- -mod=readonly
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
|
|
||||||
- id: linux-amd64
|
- id: linux-amd64
|
||||||
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
|
|
||||||
- id: linux-arm64
|
- id: linux-arm64
|
||||||
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
goarch:
|
goarch:
|
||||||
- arm64
|
- arm64
|
||||||
env:
|
|
||||||
- CGO_ENABLED=1
|
|
||||||
- CC=aarch64-linux-gnu-gcc
|
|
||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
|
|
||||||
|
|
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -2,6 +2,28 @@
|
||||||
|
|
||||||
**TBD (TBD):**
|
**TBD (TBD):**
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
**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):**
|
**0.13.0 (2022-02-18):**
|
||||||
|
|
||||||
**Features**:
|
**Features**:
|
||||||
|
|
|
@ -8,7 +8,7 @@ RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
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 strip /go/bin/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
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 strip /go/bin/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
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
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
# Debug image
|
# Debug image
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -10,7 +10,7 @@ PROTO_SOURCES = $(call rwildcard,,*.proto)
|
||||||
|
|
||||||
|
|
||||||
build:
|
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
|
dev: lint test build
|
||||||
|
|
||||||
|
|
53
README.md
53
README.md
|
@ -47,6 +47,7 @@ If you would like to sponsor features, bugs or prioritisation, reach out to one
|
||||||
| ------- | ----------------------------------------------------------------------------------------------------------------- |
|
| ------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| Linux | Yes |
|
| Linux | Yes |
|
||||||
| OpenBSD | Yes |
|
| OpenBSD | Yes |
|
||||||
|
| FreeBSD | Yes |
|
||||||
| macOS | Yes (see `/apple` on your headscale for more information) |
|
| macOS | Yes (see `/apple` on your headscale for more information) |
|
||||||
| Windows | Yes [docs](./docs/windows-client.md) |
|
| Windows | Yes [docs](./docs/windows-client.md) |
|
||||||
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
|
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
|
||||||
|
@ -150,6 +151,13 @@ make build
|
||||||
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
|
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/restanrm>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ItalyPaleAle>
|
<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/>
|
<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/>
|
||||||
|
@ -157,6 +165,8 @@ make build
|
||||||
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
|
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/unreality>
|
<a href=https://github.com/unreality>
|
||||||
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
|
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
|
||||||
|
@ -164,8 +174,6 @@ make build
|
||||||
<sub style="font-size:14px"><b>unreality</b></sub>
|
<sub style="font-size:14px"><b>unreality</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/negbie>
|
<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/>
|
<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/>
|
||||||
|
@ -201,6 +209,8 @@ make build
|
||||||
<sub style="font-size:14px"><b>Michael G.</b></sub>
|
<sub style="font-size:14px"><b>Michael G.</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/ptman>
|
<a href=https://github.com/ptman>
|
||||||
<img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/>
|
<img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/>
|
||||||
|
@ -208,8 +218,6 @@ make build
|
||||||
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
|
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/cmars>
|
<a href=https://github.com/cmars>
|
||||||
<img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/>
|
<img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/>
|
||||||
|
@ -245,6 +253,8 @@ make build
|
||||||
<sub style="font-size:14px"><b>thomas</b></sub>
|
<sub style="font-size:14px"><b>thomas</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/aberoham>
|
<a href=https://github.com/aberoham>
|
||||||
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
|
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
|
||||||
|
@ -252,15 +262,6 @@ make build
|
||||||
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
|
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/restanrm>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/artemklevtsov>
|
<a href=https://github.com/artemklevtsov>
|
||||||
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
|
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
|
||||||
|
@ -305,6 +306,13 @@ make build
|
||||||
<sub style="font-size:14px"><b>JJGadgets</b></sub>
|
<sub style="font-size:14px"><b>JJGadgets</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/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">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/jimt>
|
<a href=https://github.com/jimt>
|
||||||
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
|
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
|
||||||
|
@ -333,6 +341,8 @@ make build
|
||||||
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
|
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/shaananc>
|
<a href=https://github.com/shaananc>
|
||||||
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
|
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
|
||||||
|
@ -340,8 +350,6 @@ make build
|
||||||
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
|
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/m-tanner-dev0>
|
<a href=https://github.com/m-tanner-dev0>
|
||||||
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
|
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
|
||||||
|
@ -377,6 +385,8 @@ make build
|
||||||
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
|
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/zekker6>
|
<a href=https://github.com/zekker6>
|
||||||
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
|
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
|
||||||
|
@ -384,8 +394,6 @@ make build
|
||||||
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
|
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Bpazy>
|
<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/>
|
<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/>
|
||||||
|
@ -421,6 +429,15 @@ make build
|
||||||
<sub style="font-size:14px"><b>lion24</b></sub>
|
<sub style="font-size:14px"><b>lion24</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/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">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Wakeful-Cloud>
|
<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/>
|
<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/>
|
||||||
|
@ -428,8 +445,6 @@ make build
|
||||||
<sub style="font-size:14px"><b>Wakeful-Cloud</b></sub>
|
<sub style="font-size:14px"><b>Wakeful-Cloud</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/xpzouying>
|
<a href=https://github.com/xpzouying>
|
||||||
<img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/>
|
<img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/>
|
||||||
|
|
233
acls.go
233
acls.go
|
@ -20,7 +20,6 @@ const (
|
||||||
errInvalidUserSection = Error("invalid user section")
|
errInvalidUserSection = Error("invalid user section")
|
||||||
errInvalidGroup = Error("invalid group")
|
errInvalidGroup = Error("invalid group")
|
||||||
errInvalidTag = Error("invalid tag")
|
errInvalidTag = Error("invalid tag")
|
||||||
errInvalidNamespace = Error("invalid namespace")
|
|
||||||
errInvalidPortFormat = Error("invalid port format")
|
errInvalidPortFormat = Error("invalid port format")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,13 +68,17 @@ func (h *Headscale) LoadACLPolicy(path string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
h.aclPolicy = &policy
|
h.aclPolicy = &policy
|
||||||
|
|
||||||
|
return h.UpdateACLRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) UpdateACLRules() error {
|
||||||
rules, err := h.generateACLRules()
|
rules, err := h.generateACLRules()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h.aclRules = rules
|
|
||||||
|
|
||||||
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
||||||
|
h.aclRules = rules
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -83,16 +86,23 @@ func (h *Headscale) LoadACLPolicy(path string) error {
|
||||||
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||||
rules := []tailcfg.FilterRule{}
|
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 {
|
for index, acl := range h.aclPolicy.ACLs {
|
||||||
if acl.Action != "accept" {
|
if acl.Action != "accept" {
|
||||||
return nil, errInvalidAction
|
return nil, errInvalidAction
|
||||||
}
|
}
|
||||||
|
|
||||||
filterRule := tailcfg.FilterRule{}
|
|
||||||
|
|
||||||
srcIPs := []string{}
|
srcIPs := []string{}
|
||||||
for innerIndex, user := range acl.Users {
|
for innerIndex, user := range acl.Users {
|
||||||
srcs, err := h.generateACLPolicySrcIP(user)
|
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
|
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
|
||||||
|
@ -101,11 +111,10 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||||
}
|
}
|
||||||
srcIPs = append(srcIPs, srcs...)
|
srcIPs = append(srcIPs, srcs...)
|
||||||
}
|
}
|
||||||
filterRule.SrcIPs = srcIPs
|
|
||||||
|
|
||||||
destPorts := []tailcfg.NetPortRange{}
|
destPorts := []tailcfg.NetPortRange{}
|
||||||
for innerIndex, ports := range acl.Ports {
|
for innerIndex, ports := range acl.Ports {
|
||||||
dests, err := h.generateACLPolicyDestPorts(ports)
|
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
|
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
|
||||||
|
@ -124,11 +133,17 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||||
return rules, nil
|
return rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) {
|
func (h *Headscale) generateACLPolicySrcIP(
|
||||||
return h.expandAlias(u)
|
machines []Machine,
|
||||||
|
aclPolicy ACLPolicy,
|
||||||
|
u string,
|
||||||
|
) ([]string, error) {
|
||||||
|
return expandAlias(machines, aclPolicy, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) generateACLPolicyDestPorts(
|
func (h *Headscale) generateACLPolicyDestPorts(
|
||||||
|
machines []Machine,
|
||||||
|
aclPolicy ACLPolicy,
|
||||||
d string,
|
d string,
|
||||||
) ([]tailcfg.NetPortRange, error) {
|
) ([]tailcfg.NetPortRange, error) {
|
||||||
tokens := strings.Split(d, ":")
|
tokens := strings.Split(d, ":")
|
||||||
|
@ -149,11 +164,11 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
||||||
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
expanded, err := h.expandAlias(alias)
|
expanded, err := expandAlias(machines, aclPolicy, alias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ports, err := h.expandPorts(tokens[len(tokens)-1])
|
ports, err := expandPorts(tokens[len(tokens)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -172,21 +187,28 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
||||||
return dests, nil
|
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 == "*" {
|
if alias == "*" {
|
||||||
return []string{"*"}, nil
|
return []string{"*"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(alias, "group:") {
|
if strings.HasPrefix(alias, "group:") {
|
||||||
if _, ok := h.aclPolicy.Groups[alias]; !ok {
|
namespaces, err := expandGroup(aclPolicy, alias)
|
||||||
return nil, errInvalidGroup
|
if err != nil {
|
||||||
|
return ips, err
|
||||||
}
|
}
|
||||||
ips := []string{}
|
for _, n := range namespaces {
|
||||||
for _, n := range h.aclPolicy.Groups[alias] {
|
nodes := filterMachinesByNamespace(machines, n)
|
||||||
nodes, err := h.ListMachinesInNamespace(n)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errInvalidNamespace
|
|
||||||
}
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
ips = append(ips, node.IPAddresses.ToStringSlice()...)
|
ips = append(ips, node.IPAddresses.ToStringSlice()...)
|
||||||
}
|
}
|
||||||
|
@ -196,35 +218,23 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(alias, "tag:") {
|
if strings.HasPrefix(alias, "tag:") {
|
||||||
if _, ok := h.aclPolicy.TagOwners[alias]; !ok {
|
owners, err := expandTagOwners(aclPolicy, alias)
|
||||||
return nil, errInvalidTag
|
if err != nil {
|
||||||
|
return ips, err
|
||||||
}
|
}
|
||||||
|
for _, namespace := range owners {
|
||||||
// This will have HORRIBLE performance.
|
machines := filterMachinesByNamespace(machines, namespace)
|
||||||
// We need to change the data model to better store tags
|
for _, machine := range machines {
|
||||||
machines := []Machine{}
|
if len(machine.HostInfo) == 0 {
|
||||||
if err := h.db.Where("registered").Find(&machines).Error; err != nil {
|
continue
|
||||||
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
|
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(hi, &hostinfo)
|
hi, err := machine.GetHostInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return ips, err
|
||||||
}
|
}
|
||||||
|
for _, t := range hi.RequestTags {
|
||||||
// FIXME: Check TagOwners allows this
|
if alias == t {
|
||||||
for _, t := range hostinfo.RequestTags {
|
|
||||||
if alias[4:] == t {
|
|
||||||
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,38 +243,82 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
||||||
return ips, nil
|
return ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := h.GetNamespace(alias)
|
// if alias is a namespace
|
||||||
if err == nil {
|
nodes := filterMachinesByNamespace(machines, alias)
|
||||||
nodes, err := h.ListMachinesInNamespace(n.Name)
|
nodes, err := excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return ips, err
|
||||||
}
|
}
|
||||||
ips := []string{}
|
for _, n := range nodes {
|
||||||
for _, n := range nodes {
|
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
||||||
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
}
|
||||||
}
|
if len(ips) > 0 {
|
||||||
|
|
||||||
return ips, nil
|
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
|
return []string{h.String()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if alias is an IP
|
||||||
ip, err := netaddr.ParseIP(alias)
|
ip, err := netaddr.ParseIP(alias)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return []string{ip.String()}, nil
|
return []string{ip.String()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if alias is an CIDR
|
||||||
cidr, err := netaddr.ParseIPPrefix(alias)
|
cidr, err := netaddr.ParseIPPrefix(alias)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return []string{cidr.String()}, 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 == "*" {
|
if portsStr == "*" {
|
||||||
return &[]tailcfg.PortRange{
|
return &[]tailcfg.PortRange{
|
||||||
{First: portRangeBegin, Last: portRangeEnd},
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
|
@ -306,3 +360,64 @@ func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
||||||
|
|
||||||
return &ports, nil
|
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
|
||||||
|
}
|
||||||
|
|
968
acls_test.go
968
acls_test.go
|
@ -1,7 +1,14 @@
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Suite) TestWrongPath(c *check.C) {
|
func (s *Suite) TestWrongPath(c *check.C) {
|
||||||
|
@ -52,6 +59,245 @@ func (s *Suite) TestBasicRule(c *check.C) {
|
||||||
c.Assert(rules, check.NotNil)
|
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) {
|
func (s *Suite) TestPortRange(c *check.C) {
|
||||||
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
|
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
@ -94,7 +340,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
|
||||||
ips, _ := app.getAvailableIPs()
|
ips, _ := app.getAvailableIPs()
|
||||||
machine := Machine{
|
machine := Machine{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
MachineKey: "foo",
|
MachineKey: "12345",
|
||||||
NodeKey: "bar",
|
NodeKey: "bar",
|
||||||
DiscoKey: "faa",
|
DiscoKey: "faa",
|
||||||
Name: "testmachine",
|
Name: "testmachine",
|
||||||
|
@ -165,3 +411,723 @@ func (s *Suite) TestPortGroup(c *check.C) {
|
||||||
c.Assert(len(ips), check.Equals, 1)
|
c.Assert(len(ips), check.Equals, 1)
|
||||||
c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String())
|
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
22
api.go
|
@ -261,7 +261,16 @@ func (h *Headscale) getMapResponse(
|
||||||
|
|
||||||
var respBody []byte
|
var respBody []byte
|
||||||
if req.Compress == "zstd" {
|
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)
|
encoder, _ := zstd.NewWriter(nil)
|
||||||
srcCompressed := encoder.EncodeAll(src, nil)
|
srcCompressed := encoder.EncodeAll(src, nil)
|
||||||
|
@ -290,7 +299,16 @@ func (h *Headscale) getMapKeepAliveResponse(
|
||||||
var respBody []byte
|
var respBody []byte
|
||||||
var err error
|
var err error
|
||||||
if mapRequest.Compress == "zstd" {
|
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)
|
encoder, _ := zstd.NewWriter(nil)
|
||||||
srcCompressed := encoder.EncodeAll(src, nil)
|
srcCompressed := encoder.EncodeAll(src, nil)
|
||||||
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
||||||
|
|
25
db.go
25
db.go
|
@ -2,9 +2,10 @@ package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
@ -81,10 +82,24 @@ func (h *Headscale) openDB() (*gorm.DB, error) {
|
||||||
|
|
||||||
switch h.dbType {
|
switch h.dbType {
|
||||||
case Sqlite:
|
case Sqlite:
|
||||||
db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{
|
db, err = gorm.Open(
|
||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
sqlite.Open(h.dbString+"?_synchronous=1&_journal_mode=WAL"),
|
||||||
Logger: log,
|
&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:
|
case Postgres:
|
||||||
db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{
|
db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{
|
||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
|
|
20
dns.go
20
dns.go
|
@ -163,7 +163,15 @@ func getMapResponseDNSConfig(
|
||||||
dnsConfig = dnsConfigOrig.Clone()
|
dnsConfig = dnsConfigOrig.Clone()
|
||||||
dnsConfig.Domains = append(
|
dnsConfig.Domains = append(
|
||||||
dnsConfig.Domains,
|
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)
|
namespaceSet := set.New(set.ThreadSafe)
|
||||||
|
@ -171,8 +179,14 @@ func getMapResponseDNSConfig(
|
||||||
for _, p := range peers {
|
for _, p := range peers {
|
||||||
namespaceSet.Add(p.Namespace)
|
namespaceSet.Add(p.Namespace)
|
||||||
}
|
}
|
||||||
for _, namespace := range namespaceSet.List() {
|
for _, ns := range namespaceSet.List() {
|
||||||
dnsRoute := fmt.Sprintf("%s.%s", namespace.(Namespace).Name, baseDomain)
|
namespace, ok := ns.(Namespace)
|
||||||
|
if !ok {
|
||||||
|
dnsConfig = dnsConfigOrig
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain)
|
||||||
dnsConfig.Routes[dnsRoute] = nil
|
dnsConfig.Routes[dnsRoute] = nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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.
|
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
|
### Apple devices
|
||||||
|
|
||||||
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.
|
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
141
docs/acls.md
Normal 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:*"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
21
go.mod
21
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/efekarakus/termcolor v1.0.1
|
github.com/efekarakus/termcolor v1.0.1
|
||||||
github.com/fatih/set v0.2.1
|
github.com/fatih/set v0.2.1
|
||||||
github.com/gin-gonic/gin v1.7.7
|
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/gofrs/uuid v4.2.0+incompatible
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3
|
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/prometheus/client_golang v1.12.1
|
||||||
github.com/pterm/pterm v0.12.36
|
github.com/pterm/pterm v0.12.36
|
||||||
github.com/rs/zerolog v1.26.1
|
github.com/rs/zerolog v1.26.1
|
||||||
github.com/soheilhy/cmux v0.1.5
|
|
||||||
github.com/spf13/cobra v1.3.0
|
github.com/spf13/cobra v1.3.0
|
||||||
github.com/spf13/viper v1.10.1
|
github.com/spf13/viper v1.10.1
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/tailscale/hujson v0.0.0-20211215203138-ffd971c5f362
|
github.com/tailscale/hujson v0.0.0-20211215203138-ffd971c5f362
|
||||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||||
github.com/zsais/go-gin-prometheus v0.1.0
|
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/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
google.golang.org/genproto v0.0.0-20220210181026-6fee9acbd336
|
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/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
gorm.io/datatypes v1.0.5
|
gorm.io/datatypes v1.0.5
|
||||||
gorm.io/driver/postgres v1.2.3
|
gorm.io/driver/postgres v1.3.1
|
||||||
gorm.io/driver/sqlite v1.2.6
|
gorm.io/gorm v1.23.1
|
||||||
gorm.io/gorm v1.22.5
|
|
||||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||||
tailscale.com v1.20.4
|
tailscale.com v1.20.4
|
||||||
)
|
)
|
||||||
|
@ -58,8 +57,8 @@ require (
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/docker/go-units v0.4.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.5.1 // 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/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/locales v0.14.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.10.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-github v17.0.0+incompatible // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gookit/color v1.5.0 // indirect
|
github.com/gookit/color v1.5.0 // indirect
|
||||||
github.com/hashicorp/go-version v1.4.0 // indirect
|
github.com/hashicorp/go-version v1.4.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.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/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
github.com/prometheus/common v0.32.1 // indirect
|
||||||
github.com/prometheus/procfs v0.7.3 // 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/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||||
github.com/sirupsen/logrus 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/ini.v1 v1.66.4 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // 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
|
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
178
machine.go
178
machine.go
|
@ -119,6 +119,103 @@ func (machine Machine) isExpired() bool {
|
||||||
return time.Now().UTC().After(*machine.Expiry)
|
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) {
|
func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
|
@ -206,39 +303,54 @@ func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
|
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
|
||||||
direct, err := h.getDirectPeers(machine)
|
var peers Machines
|
||||||
if err != nil {
|
var err error
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Err(err).
|
|
||||||
Msg("Cannot fetch peers")
|
|
||||||
|
|
||||||
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 })
|
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
|
||||||
|
|
||||||
log.Trace().
|
log.Trace().
|
||||||
|
@ -597,7 +709,11 @@ func (machine Machine) toNode(
|
||||||
hostname = fmt.Sprintf(
|
hostname = fmt.Sprintf(
|
||||||
"%s.%s.%s",
|
"%s.%s.%s",
|
||||||
machine.Name,
|
machine.Name,
|
||||||
machine.Namespace.Name,
|
strings.ReplaceAll(
|
||||||
|
machine.Namespace.Name,
|
||||||
|
"@",
|
||||||
|
".",
|
||||||
|
), // Replace @ with . for valid domain for machine
|
||||||
baseDomain,
|
baseDomain,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
262
machine_test.go
262
machine_test.go
|
@ -1,11 +1,15 @@
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Suite) TestGetMachine(c *check.C) {
|
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")
|
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) {
|
func (s *Suite) TestExpireMachine(c *check.C) {
|
||||||
namespace, err := app.CreateNamespace("test")
|
namespace, err := app.CreateNamespace("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
@ -208,3 +295,178 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
|
||||||
c.Assert(deserialized[i], check.Equals, input[i])
|
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
28
poll.go
|
@ -85,12 +85,26 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
||||||
Str("machine", machine.Name).
|
Str("machine", machine.Name).
|
||||||
Msg("Found machine in database")
|
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.Name = req.Hostinfo.Hostname
|
||||||
machine.HostInfo = datatypes.JSON(hostinfo)
|
machine.HostInfo = datatypes.JSON(hostinfo)
|
||||||
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
|
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
|
||||||
now := time.Now().UTC()
|
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:
|
// From Tailscale client:
|
||||||
//
|
//
|
||||||
// ReadOnly is whether the client just wants to fetch the MapResponse,
|
// 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
|
// The intended use is for clients to discover the DERP map at start-up
|
||||||
// before their first real endpoint update.
|
// before their first real endpoint update.
|
||||||
if !req.ReadOnly {
|
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.Endpoints = datatypes.JSON(endpoints)
|
||||||
machine.LastSeen = &now
|
machine.LastSeen = &now
|
||||||
}
|
}
|
||||||
|
|
10
utils.go
10
utils.go
|
@ -212,6 +212,16 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
|
||||||
return ips, nil
|
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 {
|
func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool {
|
||||||
for _, v := range ips {
|
for _, v := range ips {
|
||||||
if v == ip {
|
if v == ip {
|
||||||
|
|
Loading…
Reference in a new issue