mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-17 06:19:51 +02:00
Compare commits
13 Commits
v0.22.0-al
...
v0.22.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64ebe6b0c8 | ||
|
|
e6b26499f7 | ||
|
|
977eb1dee3 | ||
|
|
b2e2b02210 | ||
|
|
2abff4bb08 | ||
|
|
54c00645d1 | ||
|
|
cad5ce0ebd | ||
|
|
b12a167fa2 | ||
|
|
667295e15e | ||
|
|
bea52678e3 | ||
|
|
307cfc3304 | ||
|
|
5e74ca9414 | ||
|
|
9836b097a4 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -19,6 +19,6 @@ jobs:
|
|||||||
- uses: cachix/install-nix-action@v16
|
- uses: cachix/install-nix-action@v16
|
||||||
|
|
||||||
- name: Run goreleaser
|
- name: Run goreleaser
|
||||||
run: nix develop --command -- goreleaser release --rm-dist
|
run: nix develop --command -- goreleaser release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
57
.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestACLDevice1CanAccessDevice2
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestACLDevice1CanAccessDevice2$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
57
.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestACLNamedHostsCanReach
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestACLNamedHostsCanReach$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
57
.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml
vendored
Normal file
57
.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
|
||||||
|
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
|
||||||
|
|
||||||
|
name: Integration Test v2 - TestACLNamedHostsCanReachBySubnet
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v34
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.nix
|
||||||
|
go.*
|
||||||
|
**/*.go
|
||||||
|
integration_test/
|
||||||
|
config-example.yaml
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v18
|
||||||
|
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
|
||||||
|
- name: Run general integration tests
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
nix develop --command -- docker run \
|
||||||
|
--tty --rm \
|
||||||
|
--volume ~/.cache/hs-integration-go:/go \
|
||||||
|
--name headscale-test-suite \
|
||||||
|
--volume $PWD:$PWD -w $PWD/integration \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
--volume $PWD/control_logs:/tmp/control \
|
||||||
|
golang:1 \
|
||||||
|
go test ./... \
|
||||||
|
-tags ts2019 \
|
||||||
|
-failfast \
|
||||||
|
-timeout 120m \
|
||||||
|
-parallel 1 \
|
||||||
|
-run "^TestACLNamedHostsCanReachBySubnet$"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: "control_logs/*.log"
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
ignored/
|
||||||
|
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
@@ -12,7 +14,7 @@
|
|||||||
*.out
|
*.out
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
vendor/
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
/headscale
|
/headscale
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod tidy -compat=1.20
|
- go mod tidy -compat=1.20
|
||||||
|
- go mod vendor
|
||||||
|
|
||||||
release:
|
release:
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
@@ -44,6 +45,13 @@ archives:
|
|||||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
format: binary
|
format: binary
|
||||||
|
|
||||||
|
source:
|
||||||
|
enabled: true
|
||||||
|
name_template: "{{ .ProjectName }}_{{ .Version }}"
|
||||||
|
format: tar.gz
|
||||||
|
files:
|
||||||
|
- "vendor/"
|
||||||
|
|
||||||
nfpms:
|
nfpms:
|
||||||
# Configure nFPM for .deb and .rpm releases
|
# Configure nFPM for .deb and .rpm releases
|
||||||
#
|
#
|
||||||
@@ -65,7 +73,7 @@ nfpms:
|
|||||||
bindir: /usr/bin
|
bindir: /usr/bin
|
||||||
formats:
|
formats:
|
||||||
- deb
|
- deb
|
||||||
- rpm
|
# - rpm
|
||||||
contents:
|
contents:
|
||||||
- src: ./config-example.yaml
|
- src: ./config-example.yaml
|
||||||
dst: /etc/headscale/config.yaml
|
dst: /etc/headscale/config.yaml
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,12 +1,18 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## 0.22.0 (2023-XX-XX)
|
## 0.23.0 (2023-XX-XX)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Add `.deb` and `.rpm` packages to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
|
## 0.22.0 (2023-04-20)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Add `.deb` packages to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
|
||||||
|
- Update and simplify the documentation to use new `.deb` packages [#1349](https://github.com/juanfont/headscale/pull/1349)
|
||||||
- Add 32-bit Arm platforms to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
|
- Add 32-bit Arm platforms to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
|
||||||
- Fix longstanding bug that would prevent "\*" from working properly in ACLs (issue [#699](https://github.com/juanfont/headscale/issues/699)) [#1279](https://github.com/juanfont/headscale/pull/1279)
|
- Fix longstanding bug that would prevent "\*" from working properly in ACLs (issue [#699](https://github.com/juanfont/headscale/issues/699)) [#1279](https://github.com/juanfont/headscale/pull/1279)
|
||||||
|
- Fix issue where IPv6 could not be used in, or while using ACLs (part of [#809](https://github.com/juanfont/headscale/issues/809)) [#1339](https://github.com/juanfont/headscale/pull/1339)
|
||||||
- Target Go 1.20 and Tailscale 1.38 for Headscale [#1323](https://github.com/juanfont/headscale/pull/1323)
|
- Target Go 1.20 and Tailscale 1.38 for Headscale [#1323](https://github.com/juanfont/headscale/pull/1323)
|
||||||
|
|
||||||
## 0.21.0 (2023-03-20)
|
## 0.21.0 (2023-03-20)
|
||||||
|
|||||||
21
Makefile
21
Makefile
@@ -36,7 +36,7 @@ test_integration_cli:
|
|||||||
-v ~/.cache/hs-integration-go:/go \
|
-v ~/.cache/hs-integration-go:/go \
|
||||||
-v $$PWD:$$PWD -w $$PWD \
|
-v $$PWD:$$PWD -w $$PWD \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
||||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
|
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
|
||||||
|
|
||||||
test_integration_derp:
|
test_integration_derp:
|
||||||
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
|
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
|
||||||
@@ -46,7 +46,7 @@ test_integration_derp:
|
|||||||
-v ~/.cache/hs-integration-go:/go \
|
-v ~/.cache/hs-integration-go:/go \
|
||||||
-v $$PWD:$$PWD -w $$PWD \
|
-v $$PWD:$$PWD -w $$PWD \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
||||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
|
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
|
||||||
|
|
||||||
test_integration_v2_general:
|
test_integration_v2_general:
|
||||||
docker run \
|
docker run \
|
||||||
@@ -56,13 +56,7 @@ test_integration_v2_general:
|
|||||||
-v $$PWD:$$PWD -w $$PWD/integration \
|
-v $$PWD:$$PWD -w $$PWD/integration \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
golang:1 \
|
golang:1 \
|
||||||
go test $(TAGS) -failfast ./... -timeout 120m -parallel 8
|
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast ./... -timeout 120m -parallel 8
|
||||||
|
|
||||||
coverprofile_func:
|
|
||||||
go tool cover -func=coverage.out
|
|
||||||
|
|
||||||
coverprofile_html:
|
|
||||||
go tool cover -html=coverage.out
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run --fix --timeout 10m
|
golangci-lint run --fix --timeout 10m
|
||||||
@@ -80,11 +74,4 @@ compress: build
|
|||||||
|
|
||||||
generate:
|
generate:
|
||||||
rm -rf gen
|
rm -rf gen
|
||||||
go run github.com/bufbuild/buf/cmd/buf generate proto
|
buf generate proto
|
||||||
|
|
||||||
install-protobuf-plugins:
|
|
||||||
go install \
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
|
|
||||||
google.golang.org/protobuf/cmd/protoc-gen-go \
|
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc
|
|
||||||
|
|||||||
72
acls.go
72
acls.go
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/tailscale/hujson"
|
"github.com/tailscale/hujson"
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -407,15 +408,40 @@ func generateACLPolicyDest(
|
|||||||
needsWildcard bool,
|
needsWildcard bool,
|
||||||
stripEmaildomain bool,
|
stripEmaildomain bool,
|
||||||
) ([]tailcfg.NetPortRange, error) {
|
) ([]tailcfg.NetPortRange, error) {
|
||||||
tokens := strings.Split(dest, ":")
|
var tokens []string
|
||||||
|
|
||||||
|
log.Trace().Str("destination", dest).Msg("generating policy destination")
|
||||||
|
|
||||||
|
// Check if there is a IPv4/6:Port combination, IPv6 has more than
|
||||||
|
// three ":".
|
||||||
|
tokens = strings.Split(dest, ":")
|
||||||
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
|
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
|
||||||
return nil, errInvalidPortFormat
|
port := tokens[len(tokens)-1]
|
||||||
|
|
||||||
|
maybeIPv6Str := strings.TrimSuffix(dest, ":"+port)
|
||||||
|
log.Trace().Str("maybeIPv6Str", maybeIPv6Str).Msg("")
|
||||||
|
|
||||||
|
if maybeIPv6, err := netip.ParseAddr(maybeIPv6Str); err != nil && !maybeIPv6.Is6() {
|
||||||
|
log.Trace().Err(err).Msg("trying to parse as IPv6")
|
||||||
|
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"failed to parse destination, tokens %v: %w",
|
||||||
|
tokens,
|
||||||
|
errInvalidPortFormat,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tokens = []string{maybeIPv6Str, port}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Trace().Strs("tokens", tokens).Msg("generating policy destination")
|
||||||
|
|
||||||
var alias string
|
var alias string
|
||||||
// We can have here stuff like:
|
// We can have here stuff like:
|
||||||
// git-server:*
|
// git-server:*
|
||||||
// 192.168.1.0/24:22
|
// 192.168.1.0/24:22
|
||||||
|
// fd7a:115c:a1e0::2:22
|
||||||
|
// fd7a:115c:a1e0::2/128:22
|
||||||
// tag:montreal-webserver:80,443
|
// tag:montreal-webserver:80,443
|
||||||
// tag:api-server:443
|
// tag:api-server:443
|
||||||
// example-host-1:*
|
// example-host-1:*
|
||||||
@@ -508,9 +534,11 @@ func parseProtocol(protocol string) ([]int, bool, error) {
|
|||||||
// - a group
|
// - a group
|
||||||
// - a tag
|
// - a tag
|
||||||
// - a host
|
// - a host
|
||||||
|
// - an ip
|
||||||
|
// - a cidr
|
||||||
// and transform these in IPAddresses.
|
// and transform these in IPAddresses.
|
||||||
func expandAlias(
|
func expandAlias(
|
||||||
machines []Machine,
|
machines Machines,
|
||||||
aclPolicy ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
alias string,
|
alias string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
@@ -592,19 +620,40 @@ func expandAlias(
|
|||||||
|
|
||||||
// if alias is an host
|
// if alias is an host
|
||||||
if h, ok := aclPolicy.Hosts[alias]; ok {
|
if h, ok := aclPolicy.Hosts[alias]; ok {
|
||||||
return []string{h.String()}, nil
|
log.Trace().Str("host", h.String()).Msg("expandAlias got hosts entry")
|
||||||
|
|
||||||
|
return expandAlias(machines, aclPolicy, h.String(), stripEmailDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an IP
|
// if alias is an IP
|
||||||
ip, err := netip.ParseAddr(alias)
|
if ip, err := netip.ParseAddr(alias); err == nil {
|
||||||
if err == nil {
|
log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip")
|
||||||
return []string{ip.String()}, nil
|
ips := []string{ip.String()}
|
||||||
|
matches := machines.FilterByIP(ip)
|
||||||
|
|
||||||
|
for _, machine := range matches {
|
||||||
|
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Uniq(ips), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an CIDR
|
if cidr, err := netip.ParsePrefix(alias); err == nil {
|
||||||
cidr, err := netip.ParsePrefix(alias)
|
log.Trace().Str("cidr", cidr.String()).Msg("expandAlias got cidr")
|
||||||
if err == nil {
|
val := []string{cidr.String()}
|
||||||
return []string{cidr.String()}, nil
|
// This is suboptimal and quite expensive, but if we only add the cidr, we will miss all the relevant IPv6
|
||||||
|
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
|
||||||
|
for _, machine := range machines {
|
||||||
|
for _, ip := range machine.IPAddresses {
|
||||||
|
// log.Trace().
|
||||||
|
// Msgf("checking if machine ip (%s) is part of cidr (%s): %v, is single ip cidr (%v), addr: %s", ip.String(), cidr.String(), cidr.Contains(ip), cidr.IsSingleIP(), cidr.Addr().String())
|
||||||
|
if cidr.Contains(ip) {
|
||||||
|
val = append(val, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Uniq(val), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
||||||
@@ -666,6 +715,7 @@ func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, err
|
|||||||
|
|
||||||
ports := []tailcfg.PortRange{}
|
ports := []tailcfg.PortRange{}
|
||||||
for _, portStr := range strings.Split(portsStr, ",") {
|
for _, portStr := range strings.Split(portsStr, ",") {
|
||||||
|
log.Trace().Msgf("parsing portstring: %s", portStr)
|
||||||
rang := strings.Split(portStr, "-")
|
rang := strings.Split(portStr, "-")
|
||||||
switch len(rang) {
|
switch len(rang) {
|
||||||
case 1:
|
case 1:
|
||||||
|
|||||||
88
acls_test.go
88
acls_test.go
@@ -1026,22 +1026,7 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "private network",
|
name: "simple host by ip passed through",
|
||||||
args: args{
|
|
||||||
alias: "homeNetwork",
|
|
||||||
machines: []Machine{},
|
|
||||||
aclPolicy: ACLPolicy{
|
|
||||||
Hosts: Hosts{
|
|
||||||
"homeNetwork": netip.MustParsePrefix("192.168.1.0/24"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stripEmailDomain: true,
|
|
||||||
},
|
|
||||||
want: []string{"192.168.1.0/24"},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simple host by ip",
|
|
||||||
args: args{
|
args: args{
|
||||||
alias: "10.0.0.1",
|
alias: "10.0.0.1",
|
||||||
machines: []Machine{},
|
machines: []Machine{},
|
||||||
@@ -1051,6 +1036,62 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
want: []string{"10.0.0.1"},
|
want: []string{"10.0.0.1"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "simple host by ipv4 single ipv4",
|
||||||
|
args: args{
|
||||||
|
alias: "10.0.0.1",
|
||||||
|
machines: []Machine{
|
||||||
|
{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
},
|
||||||
|
User: User{Name: "mickael"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aclPolicy: ACLPolicy{},
|
||||||
|
stripEmailDomain: true,
|
||||||
|
},
|
||||||
|
want: []string{"10.0.0.1"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple host by ipv4 single dual stack",
|
||||||
|
args: args{
|
||||||
|
alias: "10.0.0.1",
|
||||||
|
machines: []Machine{
|
||||||
|
{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
|
||||||
|
},
|
||||||
|
User: User{Name: "mickael"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aclPolicy: ACLPolicy{},
|
||||||
|
stripEmailDomain: true,
|
||||||
|
},
|
||||||
|
want: []string{"10.0.0.1", "fd7a:115c:a1e0:ab12:4843:2222:6273:2222"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple host by ipv6 single dual stack",
|
||||||
|
args: args{
|
||||||
|
alias: "fd7a:115c:a1e0:ab12:4843:2222:6273:2222",
|
||||||
|
machines: []Machine{
|
||||||
|
{
|
||||||
|
IPAddresses: MachineAddresses{
|
||||||
|
netip.MustParseAddr("10.0.0.1"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
|
||||||
|
},
|
||||||
|
User: User{Name: "mickael"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aclPolicy: ACLPolicy{},
|
||||||
|
stripEmailDomain: true,
|
||||||
|
},
|
||||||
|
want: []string{"fd7a:115c:a1e0:ab12:4843:2222:6273:2222", "10.0.0.1"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "simple host by hostname alias",
|
name: "simple host by hostname alias",
|
||||||
args: args{
|
args: args{
|
||||||
@@ -1066,6 +1107,21 @@ func Test_expandAlias(t *testing.T) {
|
|||||||
want: []string{"10.0.0.132/32"},
|
want: []string{"10.0.0.132/32"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "private network",
|
||||||
|
args: args{
|
||||||
|
alias: "homeNetwork",
|
||||||
|
machines: []Machine{},
|
||||||
|
aclPolicy: ACLPolicy{
|
||||||
|
Hosts: Hosts{
|
||||||
|
"homeNetwork": netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stripEmailDomain: true,
|
||||||
|
},
|
||||||
|
want: []string{"192.168.1.0/24"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "simple CIDR",
|
name: "simple CIDR",
|
||||||
args: args{
|
args: args{
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ summary() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo " Please follow the next steps to start the software:"
|
echo " Please follow the next steps to start the software:"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo " sudo systemctl enable headscale"
|
||||||
echo " sudo systemctl start headscale"
|
echo " sudo systemctl start headscale"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Configuration settings can be adjusted here:"
|
echo " Configuration settings can be adjusted here:"
|
||||||
|
|||||||
198
docs/running-headscale-linux-manual.md
Normal file
198
docs/running-headscale-linux-manual.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Running headscale on Linux
|
||||||
|
|
||||||
|
## Note: Outdated and "advanced"
|
||||||
|
|
||||||
|
This documentation is considered the "legacy"/advanced/manual version of the documentation, you most likely do not
|
||||||
|
want to use this documentation and rather look at the distro specific documentation (TODO LINK)[].
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
This documentation has the goal of showing a user how-to set up and run `headscale` on Linux.
|
||||||
|
In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd)
|
||||||
|
describing how to make `headscale` run properly in a server environment.
|
||||||
|
|
||||||
|
## Configure and run `headscale`
|
||||||
|
|
||||||
|
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
wget --output-document=/usr/local/bin/headscale \
|
||||||
|
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make `headscale` executable:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
chmod +x /usr/local/bin/headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Directory for configuration
|
||||||
|
|
||||||
|
mkdir -p /etc/headscale
|
||||||
|
|
||||||
|
# Directory for Database, and other variable data (like certificates)
|
||||||
|
mkdir -p /var/lib/headscale
|
||||||
|
# or if you create a headscale user:
|
||||||
|
useradd \
|
||||||
|
--create-home \
|
||||||
|
--home-dir /var/lib/headscale/ \
|
||||||
|
--system \
|
||||||
|
--user-group \
|
||||||
|
--shell /usr/bin/nologin \
|
||||||
|
headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create an empty SQLite database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /var/lib/headscale/db.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Create a `headscale` configuration:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /etc/headscale/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
|
||||||
|
|
||||||
|
6. Start the headscale server:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale serve
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will start `headscale` in the current terminal session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To continue the tutorial, open a new terminal and let it run in the background.
|
||||||
|
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/).
|
||||||
|
|
||||||
|
To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing.
|
||||||
|
|
||||||
|
7. Verify `headscale` is running:
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale users create myfirstuser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register a machine (normal login)
|
||||||
|
|
||||||
|
On a client machine, execute the `tailscale` login command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server YOUR_HEADSCALE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
Register the machine:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --user myfirstuser nodes register --key <YOUR_MACHINE_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register machine using a pre authenticated key
|
||||||
|
|
||||||
|
Generate a key using the command line:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
|
||||||
|
```
|
||||||
|
|
||||||
|
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running `headscale` in the background with SystemD
|
||||||
|
|
||||||
|
:warning: **Deprecated**: This part is very outdated and you should use the [pre-packaged Headscale for this](./running-headscale-linux.md
|
||||||
|
|
||||||
|
This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/).
|
||||||
|
This should work on most modern Linux distributions.
|
||||||
|
|
||||||
|
1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing:
|
||||||
|
|
||||||
|
```systemd
|
||||||
|
[Unit]
|
||||||
|
Description=headscale controller
|
||||||
|
After=syslog.target
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=headscale
|
||||||
|
Group=headscale
|
||||||
|
ExecStart=/usr/local/bin/headscale serve
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# Optional security enhancements
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
WorkingDirectory=/var/lib/headscale
|
||||||
|
ReadWritePaths=/var/lib/headscale /var/run/headscale
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
RuntimeDirectory=headscale
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that when running as the headscale user ensure that, either you add your current user to the headscale group:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
usermod -a -G headscale current_user
|
||||||
|
```
|
||||||
|
|
||||||
|
or run all headscale commands as the headscale user:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
su - headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
unix_socket: /var/run/headscale/headscale.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Reload SystemD to load the new configuration file:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Enable and start the new `headscale` service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl enable --now headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Verify the headscale service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl status headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
`headscale` will now run in the background and start at boot.
|
||||||
@@ -1,83 +1,65 @@
|
|||||||
# Running headscale on Linux
|
# Running headscale on Linux
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Ubuntu 20.04 or newer, Debian 11 or newer.
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
This documentation has the goal of showing a user how-to set up and run `headscale` on Linux.
|
Get Headscale up and running.
|
||||||
In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd)
|
|
||||||
describing how to make `headscale` run properly in a server environment.
|
|
||||||
|
|
||||||
## Configure and run `headscale`
|
This includes running Headscale with SystemD.
|
||||||
|
|
||||||
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
|
## Migrating from manual install
|
||||||
|
|
||||||
|
If you are migrating from the old manual install, the best thing would be to remove
|
||||||
|
the files installed by following [the guide in reverse](./running-headscale-linux-manual.md).
|
||||||
|
|
||||||
|
You should _not_ delete the database (`/var/headscale/db.sqlite`) and the
|
||||||
|
configuration (`/etc/headscale/config.yaml`).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page]():
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
wget --output-document=/usr/local/bin/headscale \
|
wget --output-document=headscale.deb \
|
||||||
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>
|
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Make `headscale` executable:
|
2. Install Headscale:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
chmod +x /usr/local/bin/headscale
|
sudo dpkg --install headscale.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
|
3. Enable Headscale service, this will start Headscale at boot:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Directory for configuration
|
sudo systemctl enable headscale
|
||||||
|
|
||||||
mkdir -p /etc/headscale
|
|
||||||
|
|
||||||
# Directory for Database, and other variable data (like certificates)
|
|
||||||
mkdir -p /var/lib/headscale
|
|
||||||
# or if you create a headscale user:
|
|
||||||
useradd \
|
|
||||||
--create-home \
|
|
||||||
--home-dir /var/lib/headscale/ \
|
|
||||||
--system \
|
|
||||||
--user-group \
|
|
||||||
--shell /usr/bin/nologin \
|
|
||||||
headscale
|
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create an empty SQLite database:
|
4. Configure Headscale by editing the configuration file:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
touch /var/lib/headscale/db.sqlite
|
nano /etc/headscale/config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Create a `headscale` configuration:
|
5. Start Headscale:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
touch /etc/headscale/config.yaml
|
sudo systemctl start headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
**(Strongly Recommended)** Download a copy of the [example configuration][config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository.
|
6. Check that Headscale is running as intended:
|
||||||
|
|
||||||
6. Start the headscale server:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale serve
|
systemctl status headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will start `headscale` in the current terminal session.
|
## Using Headscale
|
||||||
|
|
||||||
---
|
### Create a user
|
||||||
|
|
||||||
To continue the tutorial, open a new terminal and let it run in the background.
|
|
||||||
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/).
|
|
||||||
|
|
||||||
To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing.
|
|
||||||
|
|
||||||
7. Verify `headscale` is running:
|
|
||||||
|
|
||||||
Verify `headscale` is available:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
curl http://127.0.0.1:9090/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
8. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale users create myfirstuser
|
headscale users create myfirstuser
|
||||||
@@ -85,16 +67,16 @@ headscale users create myfirstuser
|
|||||||
|
|
||||||
### Register a machine (normal login)
|
### Register a machine (normal login)
|
||||||
|
|
||||||
On a client machine, execute the `tailscale` login command:
|
On a client machine, run the `tailscale` login command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tailscale up --login-server YOUR_HEADSCALE_URL
|
tailscale up --login-server <YOUR_HEADSCALE_URL>
|
||||||
```
|
```
|
||||||
|
|
||||||
Register the machine:
|
Register the machine:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale --user myfirstuser nodes register --key <YOU_+MACHINE_KEY>
|
headscale --user myfirstuser nodes register --key <YOUR_MACHINE_KEY>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Register machine using a pre authenticated key
|
### Register machine using a pre authenticated key
|
||||||
@@ -105,87 +87,9 @@ Generate a key using the command line:
|
|||||||
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
|
headscale --user myfirstuser preauthkeys create --reusable --expiration 24h
|
||||||
```
|
```
|
||||||
|
|
||||||
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
|
This will return a pre-authenticated key that is used to
|
||||||
|
connect a node to `headscale` during the `tailscale` command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running `headscale` in the background with SystemD
|
|
||||||
|
|
||||||
This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/).
|
|
||||||
This should work on most modern Linux distributions.
|
|
||||||
|
|
||||||
1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing:
|
|
||||||
|
|
||||||
```systemd
|
|
||||||
[Unit]
|
|
||||||
Description=headscale controller
|
|
||||||
After=syslog.target
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=headscale
|
|
||||||
Group=headscale
|
|
||||||
ExecStart=/usr/local/bin/headscale serve
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
# Optional security enhancements
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
PrivateTmp=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=yes
|
|
||||||
WorkingDirectory=/var/lib/headscale
|
|
||||||
ReadWritePaths=/var/lib/headscale /var/run/headscale
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
||||||
RuntimeDirectory=headscale
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that when running as the headscale user ensure that, either you add your current user to the headscale group:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
usermod -a -G headscale current_user
|
|
||||||
```
|
|
||||||
|
|
||||||
or run all headscale commands as the headscale user:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
su - headscale
|
|
||||||
```
|
|
||||||
|
|
||||||
2. In `/etc/headscale/config.yaml`, override the default `headscale` unix socket with path that is writable by the `headscale` user or group:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
unix_socket: /var/run/headscale/headscale.sock
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Reload SystemD to load the new configuration file:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl daemon-reload
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Enable and start the new `headscale` service:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl enable --now headscale
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Verify the headscale service:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl status headscale
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify `headscale` is available:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
curl http://127.0.0.1:9090/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
`headscale` will now run in the background and start at boot.
|
|
||||||
|
|||||||
30
flake.lock
generated
30
flake.lock
generated
@@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1680776469,
|
"lastModified": 1681202837,
|
||||||
"narHash": "sha256-3CXUDK/3q/kieWtdsYpDOBJw3Gw4Af6x+2EiSnIkNQw=",
|
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "411e8764155aa9354dbcd6d5faaeb97e9e3dce24",
|
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -17,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1680789907,
|
"lastModified": 1681753173,
|
||||||
"narHash": "sha256-0AOMkabjbOauxspnqfzqgLKhB2gSh3sLkz1p/jIckcs=",
|
"narHash": "sha256-MrGmzZWLUqh2VstoikKLFFIELXm/lsf/G9U9zR96VD4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9de84cd029054adc54fdc6442e121fbc5ac33baf",
|
"rev": "0a4206a51b386e5cda731e8ac78d76ad924c7125",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -36,6 +39,21 @@
|
|||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -129,6 +129,14 @@
|
|||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export GOFLAGS=-tags="ts2019"
|
export GOFLAGS=-tags="ts2019"
|
||||||
|
export PATH="$PWD/result/bin:$PATH"
|
||||||
|
|
||||||
|
mkdir -p ./ignored
|
||||||
|
export HEADSCALE_PRIVATE_KEY_PATH="./ignored/private.key"
|
||||||
|
export HEADSCALE_NOISE_PRIVATE_KEY_PATH="./ignored/noise_private.key"
|
||||||
|
export HEADSCALE_DB_PATH="./ignored/db.sqlite"
|
||||||
|
export HEADSCALE_TLS_LETSENCRYPT_CACHE_DIR="./ignored/cache"
|
||||||
|
export HEADSCALE_UNIX_SOCKET="./ignored/headscale.sock"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,14 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const numberOfTestClients = 2
|
func aclScenario(t *testing.T, policy *headscale.ACLPolicy, clientsPerUser int) *Scenario {
|
||||||
|
|
||||||
func aclScenario(t *testing.T, policy headscale.ACLPolicy) *Scenario {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
scenario, err := NewScenario()
|
scenario, err := NewScenario()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": numberOfTestClients,
|
"user1": clientsPerUser,
|
||||||
"user2": numberOfTestClients,
|
"user2": clientsPerUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec,
|
err = scenario.CreateHeadscaleEnv(spec,
|
||||||
@@ -29,18 +27,15 @@ func aclScenario(t *testing.T, policy headscale.ACLPolicy) *Scenario {
|
|||||||
tsic.WithDockerEntrypoint([]string{
|
tsic.WithDockerEntrypoint([]string{
|
||||||
"/bin/bash",
|
"/bin/bash",
|
||||||
"-c",
|
"-c",
|
||||||
"/bin/sleep 3 ; update-ca-certificates ; python3 -m http.server 80 & tailscaled --tun=tsdev",
|
"/bin/sleep 3 ; update-ca-certificates ; python3 -m http.server --bind :: 80 & tailscaled --tun=tsdev",
|
||||||
}),
|
}),
|
||||||
tsic.WithDockerWorkdir("/"),
|
tsic.WithDockerWorkdir("/"),
|
||||||
},
|
},
|
||||||
hsic.WithACLPolicy(&policy),
|
hsic.WithACLPolicy(policy),
|
||||||
hsic.WithTestName("acl"),
|
hsic.WithTestName("acl"),
|
||||||
)
|
)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// allClients, err := scenario.ListTailscaleClients()
|
|
||||||
// assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSync()
|
err = scenario.WaitForTailscaleSync()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -230,7 +225,7 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
ACLs: []headscale.ACL{
|
ACLs: []headscale.ACL{
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
@@ -239,6 +234,7 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
@@ -285,7 +281,7 @@ func TestACLDenyAllPort80(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
Groups: map[string][]string{
|
Groups: map[string][]string{
|
||||||
"group:integration-acl-test": {"user1", "user2"},
|
"group:integration-acl-test": {"user1", "user2"},
|
||||||
},
|
},
|
||||||
@@ -297,6 +293,7 @@ func TestACLDenyAllPort80(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
4,
|
||||||
)
|
)
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
@@ -333,7 +330,7 @@ func TestACLAllowUserDst(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
ACLs: []headscale.ACL{
|
ACLs: []headscale.ACL{
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
@@ -342,6 +339,7 @@ func TestACLAllowUserDst(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
@@ -390,7 +388,7 @@ func TestACLAllowStarDst(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
ACLs: []headscale.ACL{
|
ACLs: []headscale.ACL{
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
@@ -399,6 +397,7 @@ func TestACLAllowStarDst(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
@@ -441,155 +440,6 @@ func TestACLAllowStarDst(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This test aims to cover cases where individual hosts are allowed and denied
|
|
||||||
// access based on their assigned hostname
|
|
||||||
// https://github.com/juanfont/headscale/issues/941
|
|
||||||
|
|
||||||
// ACL = [{
|
|
||||||
// "DstPorts": [{
|
|
||||||
// "Bits": null,
|
|
||||||
// "IP": "100.64.0.3/32",
|
|
||||||
// "Ports": {
|
|
||||||
// "First": 0,
|
|
||||||
// "Last": 65535
|
|
||||||
// }
|
|
||||||
// }],
|
|
||||||
// "SrcIPs": ["*"]
|
|
||||||
// }, {
|
|
||||||
//
|
|
||||||
// "DstPorts": [{
|
|
||||||
// "Bits": null,
|
|
||||||
// "IP": "100.64.0.2/32",
|
|
||||||
// "Ports": {
|
|
||||||
// "First": 0,
|
|
||||||
// "Last": 65535
|
|
||||||
// }
|
|
||||||
// }],
|
|
||||||
// "SrcIPs": ["100.64.0.1/32"]
|
|
||||||
// }]
|
|
||||||
//
|
|
||||||
// ACL Cache Map= {
|
|
||||||
// "*": {
|
|
||||||
// "100.64.0.3/32": {}
|
|
||||||
// },
|
|
||||||
// "100.64.0.1/32": {
|
|
||||||
// "100.64.0.2/32": {}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
func TestACLNamedHostsCanReach(t *testing.T) {
|
|
||||||
IntegrationSkip(t)
|
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
|
||||||
headscale.ACLPolicy{
|
|
||||||
Hosts: headscale.Hosts{
|
|
||||||
"test1": netip.MustParsePrefix("100.64.0.1/32"),
|
|
||||||
"test2": netip.MustParsePrefix("100.64.0.2/32"),
|
|
||||||
"test3": netip.MustParsePrefix("100.64.0.3/32"),
|
|
||||||
},
|
|
||||||
ACLs: []headscale.ACL{
|
|
||||||
// Everyone can curl test3
|
|
||||||
{
|
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"*"},
|
|
||||||
Destinations: []string{"test3:*"},
|
|
||||||
},
|
|
||||||
// test1 can curl test2
|
|
||||||
{
|
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"test1"},
|
|
||||||
Destinations: []string{"test2:*"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Since user/users dont matter here, we basically expect that some clients
|
|
||||||
// will be assigned these ips and that we can pick them up for our own use.
|
|
||||||
test1ip := netip.MustParseAddr("100.64.0.1")
|
|
||||||
test1, err := scenario.FindTailscaleClientByIP(test1ip)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
test1fqdn, err := test1.FQDN()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
test1ipURL := fmt.Sprintf("http://%s/etc/hostname", test1ip.String())
|
|
||||||
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
|
||||||
|
|
||||||
test2ip := netip.MustParseAddr("100.64.0.2")
|
|
||||||
test2, err := scenario.FindTailscaleClientByIP(test2ip)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
test2fqdn, err := test2.FQDN()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
test2ipURL := fmt.Sprintf("http://%s/etc/hostname", test2ip.String())
|
|
||||||
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
|
||||||
|
|
||||||
test3ip := netip.MustParseAddr("100.64.0.3")
|
|
||||||
test3, err := scenario.FindTailscaleClientByIP(test3ip)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
test3fqdn, err := test3.FQDN()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
test3ipURL := fmt.Sprintf("http://%s/etc/hostname", test3ip.String())
|
|
||||||
test3fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test3fqdn)
|
|
||||||
|
|
||||||
// test1 can query test3
|
|
||||||
result, err := test1.Curl(test3ipURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
result, err = test1.Curl(test3fqdnURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// test2 can query test3
|
|
||||||
result, err = test2.Curl(test3ipURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
result, err = test2.Curl(test3fqdnURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// test3 cannot query test1
|
|
||||||
result, err = test3.Curl(test1ipURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
result, err = test3.Curl(test1fqdnURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
// test3 cannot query test2
|
|
||||||
result, err = test3.Curl(test2ipURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
result, err = test3.Curl(test2fqdnURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
// test1 can query test2
|
|
||||||
result, err = test1.Curl(test2ipURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
result, err = test1.Curl(test2fqdnURL)
|
|
||||||
assert.Len(t, result, 13)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// test2 cannot query test1
|
|
||||||
result, err = test2.Curl(test1ipURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
result, err = test2.Curl(test1fqdnURL)
|
|
||||||
assert.Empty(t, result)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
err = scenario.Shutdown()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestACLNamedHostsCanReachBySubnet is the same as
|
// TestACLNamedHostsCanReachBySubnet is the same as
|
||||||
// TestACLNamedHostsCanReach, but it tests if we expand a
|
// TestACLNamedHostsCanReach, but it tests if we expand a
|
||||||
// full CIDR correctly. All routes should work.
|
// full CIDR correctly. All routes should work.
|
||||||
@@ -597,7 +447,7 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
|||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
|
|
||||||
scenario := aclScenario(t,
|
scenario := aclScenario(t,
|
||||||
headscale.ACLPolicy{
|
&headscale.ACLPolicy{
|
||||||
Hosts: headscale.Hosts{
|
Hosts: headscale.Hosts{
|
||||||
"all": netip.MustParsePrefix("100.64.0.0/24"),
|
"all": netip.MustParsePrefix("100.64.0.0/24"),
|
||||||
},
|
},
|
||||||
@@ -610,6 +460,7 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
3,
|
||||||
)
|
)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
@@ -651,3 +502,450 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
|||||||
err = scenario.Shutdown()
|
err = scenario.Shutdown()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This test aims to cover cases where individual hosts are allowed and denied
|
||||||
|
// access based on their assigned hostname
|
||||||
|
// https://github.com/juanfont/headscale/issues/941
|
||||||
|
//
|
||||||
|
// ACL = [{
|
||||||
|
// "DstPorts": [{
|
||||||
|
// "Bits": null,
|
||||||
|
// "IP": "100.64.0.3/32",
|
||||||
|
// "Ports": {
|
||||||
|
// "First": 0,
|
||||||
|
// "Last": 65535
|
||||||
|
// }
|
||||||
|
// }],
|
||||||
|
// "SrcIPs": ["*"]
|
||||||
|
// }, {
|
||||||
|
//
|
||||||
|
// "DstPorts": [{
|
||||||
|
// "Bits": null,
|
||||||
|
// "IP": "100.64.0.2/32",
|
||||||
|
// "Ports": {
|
||||||
|
// "First": 0,
|
||||||
|
// "Last": 65535
|
||||||
|
// }
|
||||||
|
// }],
|
||||||
|
// "SrcIPs": ["100.64.0.1/32"]
|
||||||
|
// }]
|
||||||
|
//
|
||||||
|
// ACL Cache Map= {
|
||||||
|
// "*": {
|
||||||
|
// "100.64.0.3/32": {}
|
||||||
|
// },
|
||||||
|
// "100.64.0.1/32": {
|
||||||
|
// "100.64.0.2/32": {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// https://github.com/juanfont/headscale/issues/941
|
||||||
|
// Additionally verify ipv6 behaviour, part of
|
||||||
|
// https://github.com/juanfont/headscale/issues/809
|
||||||
|
func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
policy headscale.ACLPolicy
|
||||||
|
}{
|
||||||
|
"ipv4": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Hosts: headscale.Hosts{
|
||||||
|
"test1": netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
"test2": netip.MustParsePrefix("100.64.0.2/32"),
|
||||||
|
"test3": netip.MustParsePrefix("100.64.0.3/32"),
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
// Everyone can curl test3
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"*"},
|
||||||
|
Destinations: []string{"test3:*"},
|
||||||
|
},
|
||||||
|
// test1 can curl test2
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"test1"},
|
||||||
|
Destinations: []string{"test2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ipv6": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Hosts: headscale.Hosts{
|
||||||
|
"test1": netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
"test2": netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
|
||||||
|
"test3": netip.MustParsePrefix("fd7a:115c:a1e0::3/128"),
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
// Everyone can curl test3
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"*"},
|
||||||
|
Destinations: []string{"test3:*"},
|
||||||
|
},
|
||||||
|
// test1 can curl test2
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"test1"},
|
||||||
|
Destinations: []string{"test2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario := aclScenario(t,
|
||||||
|
&testCase.policy,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Since user/users dont matter here, we basically expect that some clients
|
||||||
|
// will be assigned these ips and that we can pick them up for our own use.
|
||||||
|
test1ip4 := netip.MustParseAddr("100.64.0.1")
|
||||||
|
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
||||||
|
test1, err := scenario.FindTailscaleClientByIP(test1ip6)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test1fqdn, err := test1.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test1ip4URL := fmt.Sprintf("http://%s/etc/hostname", test1ip4.String())
|
||||||
|
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
||||||
|
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
||||||
|
|
||||||
|
test2ip4 := netip.MustParseAddr("100.64.0.2")
|
||||||
|
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
||||||
|
test2, err := scenario.FindTailscaleClientByIP(test2ip6)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test2fqdn, err := test2.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test2ip4URL := fmt.Sprintf("http://%s/etc/hostname", test2ip4.String())
|
||||||
|
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
||||||
|
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
||||||
|
|
||||||
|
test3ip4 := netip.MustParseAddr("100.64.0.3")
|
||||||
|
test3ip6 := netip.MustParseAddr("fd7a:115c:a1e0::3")
|
||||||
|
test3, err := scenario.FindTailscaleClientByIP(test3ip6)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test3fqdn, err := test3.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test3ip4URL := fmt.Sprintf("http://%s/etc/hostname", test3ip4.String())
|
||||||
|
test3ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test3ip6.String())
|
||||||
|
test3fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test3fqdn)
|
||||||
|
|
||||||
|
// test1 can query test3
|
||||||
|
result, err := test1.Curl(test3ip4URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3ip4URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test3ip6URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3ip6URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test3fqdnURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3fqdnURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// test2 can query test3
|
||||||
|
result, err = test2.Curl(test3ip4URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3ip4URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test3ip6URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3ip6URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test3fqdnURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test3 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test3fqdnURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// test3 cannot query test1
|
||||||
|
result, err = test3.Curl(test1ip4URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test3.Curl(test1ip6URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test3.Curl(test1fqdnURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// test3 cannot query test2
|
||||||
|
result, err = test3.Curl(test2ip4URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test3.Curl(test2ip6URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test3.Curl(test2fqdnURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// test1 can query test2
|
||||||
|
result, err = test1.Curl(test2ip4URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test2 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2ip4URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
result, err = test1.Curl(test2ip6URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test2 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2ip6URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test2fqdnURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test2 with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2fqdnURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// test2 cannot query test1
|
||||||
|
result, err = test2.Curl(test1ip4URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1ip6URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1fqdnURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = scenario.Shutdown()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestACLDevice1CanAccessDevice2 is a table driven test that aims to test
|
||||||
|
// the various ways to achieve a connection between device1 and device2 where
|
||||||
|
// device1 can access device2, but not the other way around. This can be
|
||||||
|
// viewed as one of the most important tests here as it covers most of the
|
||||||
|
// syntax that can be used.
|
||||||
|
//
|
||||||
|
// Before adding new taste cases, consider if it can be reduced to a case
|
||||||
|
// in this function.
|
||||||
|
func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
policy headscale.ACLPolicy
|
||||||
|
}{
|
||||||
|
"ipv4": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"100.64.0.1"},
|
||||||
|
Destinations: []string{"100.64.0.2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ipv6": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"fd7a:115c:a1e0::1"},
|
||||||
|
Destinations: []string{"fd7a:115c:a1e0::2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hostv4cidr": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Hosts: headscale.Hosts{
|
||||||
|
"test1": netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
"test2": netip.MustParsePrefix("100.64.0.2/32"),
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"test1"},
|
||||||
|
Destinations: []string{"test2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hostv6cidr": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Hosts: headscale.Hosts{
|
||||||
|
"test1": netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
"test2": netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"test1"},
|
||||||
|
Destinations: []string{"test2:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
policy: headscale.ACLPolicy{
|
||||||
|
Groups: map[string][]string{
|
||||||
|
"group:one": {"user1"},
|
||||||
|
"group:two": {"user2"},
|
||||||
|
},
|
||||||
|
ACLs: []headscale.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"group:one"},
|
||||||
|
Destinations: []string{"group:two:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO(kradalby): Add similar tests for Tags, might need support
|
||||||
|
// in the scenario function when we create or join the clients.
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario := aclScenario(t, &testCase.policy, 1)
|
||||||
|
|
||||||
|
test1ip := netip.MustParseAddr("100.64.0.1")
|
||||||
|
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
||||||
|
test1, err := scenario.FindTailscaleClientByIP(test1ip)
|
||||||
|
assert.NotNil(t, test1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test1fqdn, err := test1.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test1ipURL := fmt.Sprintf("http://%s/etc/hostname", test1ip.String())
|
||||||
|
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
||||||
|
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
||||||
|
|
||||||
|
test2ip := netip.MustParseAddr("100.64.0.2")
|
||||||
|
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
||||||
|
test2, err := scenario.FindTailscaleClientByIP(test2ip)
|
||||||
|
assert.NotNil(t, test2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test2fqdn, err := test2.FQDN()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
test2ipURL := fmt.Sprintf("http://%s/etc/hostname", test2ip.String())
|
||||||
|
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
||||||
|
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
||||||
|
|
||||||
|
// test1 can query test2
|
||||||
|
result, err := test1.Curl(test2ipURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2ipURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test2ip6URL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2ip6URL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test1.Curl(test2fqdnURL)
|
||||||
|
assert.Lenf(
|
||||||
|
t,
|
||||||
|
result,
|
||||||
|
13,
|
||||||
|
"failed to connect from test1 to test with URL %s, expected hostname of 13 chars, got %s",
|
||||||
|
test2fqdnURL,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1ipURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1ip6URL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
result, err = test2.Curl(test1fqdnURL)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = scenario.Shutdown()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -362,6 +362,15 @@ func (s *AuthOIDCScenario) runTailscaleUp(
|
|||||||
|
|
||||||
user.joinWaitGroup.Wait()
|
user.joinWaitGroup.Wait()
|
||||||
|
|
||||||
|
for _, client := range user.Clients {
|
||||||
|
err := client.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("client %s was not ready: %s", client.Hostname(), err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to up tailscale node: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -274,6 +274,15 @@ func (s *AuthWebFlowScenario) runTailscaleUp(
|
|||||||
}
|
}
|
||||||
user.joinWaitGroup.Wait()
|
user.joinWaitGroup.Wait()
|
||||||
|
|
||||||
|
for _, client := range user.Clients {
|
||||||
|
err := client.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("client %s was not ready: %s", client.Hostname(), err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to up tailscale node: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -356,6 +356,15 @@ func (s *Scenario) RunTailscaleUp(
|
|||||||
|
|
||||||
user.joinWaitGroup.Wait()
|
user.joinWaitGroup.Wait()
|
||||||
|
|
||||||
|
for _, client := range user.Clients {
|
||||||
|
err := client.WaitForReady()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("client %s was not ready: %s", client.Hostname(), err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to up tailscale node: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -430,6 +430,15 @@ func (t *TailscaleInContainer) WaitForReady() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ipnstate.Status.CurrentTailnet was added in Tailscale 1.22.0
|
||||||
|
// https://github.com/tailscale/tailscale/pull/3865
|
||||||
|
//
|
||||||
|
// Before that, we can check the BackendState to see if the
|
||||||
|
// tailscaled daemon is connected to the control system.
|
||||||
|
if status.BackendState == "Running" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return errTailscaleNotConnected
|
return errTailscaleNotConnected
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,3 +46,35 @@ func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int
|
|||||||
//
|
//
|
||||||
// return failures
|
// return failures
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// // findPeerByIP takes an IP and a map of peers from status.Peer, and returns a *ipnstate.PeerStatus
|
||||||
|
// // if there is a peer with the given IP. If no peer is found, nil is returned.
|
||||||
|
// func findPeerByIP(
|
||||||
|
// ip netip.Addr,
|
||||||
|
// peers map[key.NodePublic]*ipnstate.PeerStatus,
|
||||||
|
// ) *ipnstate.PeerStatus {
|
||||||
|
// for _, peer := range peers {
|
||||||
|
// for _, peerIP := range peer.TailscaleIPs {
|
||||||
|
// if ip == peerIP {
|
||||||
|
// return peer
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // findPeerByHostname takes a hostname and a map of peers from status.Peer, and returns a *ipnstate.PeerStatus
|
||||||
|
// // if there is a peer with the given hostname. If no peer is found, nil is returned.
|
||||||
|
// func findPeerByHostname(
|
||||||
|
// hostname string,
|
||||||
|
// peers map[key.NodePublic]*ipnstate.PeerStatus,
|
||||||
|
// ) *ipnstate.PeerStatus {
|
||||||
|
// for _, peer := range peers {
|
||||||
|
// if hostname == peer.HostName {
|
||||||
|
// return peer
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|||||||
14
machine.go
14
machine.go
@@ -1267,3 +1267,17 @@ func (h *Headscale) GenerateGivenName(machineKey string, suppliedName string) (s
|
|||||||
|
|
||||||
return givenName, nil
|
return givenName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (machines Machines) FilterByIP(ip netip.Addr) Machines {
|
||||||
|
found := make(Machines, 0)
|
||||||
|
|
||||||
|
for _, machine := range machines {
|
||||||
|
for _, mIP := range machine.IPAddresses {
|
||||||
|
if ip == mIP {
|
||||||
|
found = append(found, machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user