mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-18 06:09:30 +00:00
Compare commits
16 Commits
v0.22.0-al
...
v0.22.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a71cc81fe7 | ||
![]() |
679305c3e4 | ||
![]() |
c0680f34f1 | ||
![]() |
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
|
||||
|
||||
- name: Run goreleaser
|
||||
run: nix develop --command -- goreleaser release --rm-dist
|
||||
run: nix develop --command -- goreleaser release --clean
|
||||
env:
|
||||
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
|
||||
*.exe
|
||||
*.exe~
|
||||
@@ -12,7 +14,7 @@
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
vendor/
|
||||
|
||||
dist/
|
||||
/headscale
|
||||
|
@@ -2,6 +2,7 @@
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy -compat=1.20
|
||||
- go mod vendor
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
@@ -31,19 +32,16 @@ builds:
|
||||
|
||||
archives:
|
||||
- id: golang-cross
|
||||
builds:
|
||||
- darwin-amd64
|
||||
- darwin-arm64
|
||||
- freebsd-amd64
|
||||
- linux-386
|
||||
- linux-amd64
|
||||
- linux-arm64
|
||||
- linux-arm-5
|
||||
- linux-arm-6
|
||||
- linux-arm-7
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
format: binary
|
||||
|
||||
source:
|
||||
enabled: true
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}"
|
||||
format: tar.gz
|
||||
files:
|
||||
- "vendor/"
|
||||
|
||||
nfpms:
|
||||
# Configure nFPM for .deb and .rpm releases
|
||||
#
|
||||
@@ -65,7 +63,7 @@ nfpms:
|
||||
bindir: /usr/bin
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
# - rpm
|
||||
contents:
|
||||
- src: ./config-example.yaml
|
||||
dst: /etc/headscale/config.yaml
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,12 +1,18 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 0.22.0 (2023-XX-XX)
|
||||
## 0.23.0 (2023-XX-XX)
|
||||
|
||||
### 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)
|
||||
- Fix longstanding bug that would prevent "\*" from working properly in ACLs (issue [#699](https://github.com/juanfont/headscale/issues/699)) [#1279](https://github.com/juanfont/headscale/pull/1279)
|
||||
- Fix issue where IPv6 could not be used in, or while using ACLs (part of [#809](https://github.com/juanfont/headscale/issues/809)) [#1339](https://github.com/juanfont/headscale/pull/1339)
|
||||
- Target Go 1.20 and Tailscale 1.38 for Headscale [#1323](https://github.com/juanfont/headscale/pull/1323)
|
||||
|
||||
## 0.21.0 (2023-03-20)
|
||||
|
21
Makefile
21
Makefile
@@ -36,7 +36,7 @@ test_integration_cli:
|
||||
-v ~/.cache/hs-integration-go:/go \
|
||||
-v $$PWD:$$PWD -w $$PWD \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
|
||||
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
|
||||
|
||||
test_integration_derp:
|
||||
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
|
||||
@@ -46,7 +46,7 @@ test_integration_derp:
|
||||
-v ~/.cache/hs-integration-go:/go \
|
||||
-v $$PWD:$$PWD -w $$PWD \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
|
||||
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
|
||||
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
|
||||
|
||||
test_integration_v2_general:
|
||||
docker run \
|
||||
@@ -56,13 +56,7 @@ test_integration_v2_general:
|
||||
-v $$PWD:$$PWD -w $$PWD/integration \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
golang:1 \
|
||||
go test $(TAGS) -failfast ./... -timeout 120m -parallel 8
|
||||
|
||||
coverprofile_func:
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
coverprofile_html:
|
||||
go tool cover -html=coverage.out
|
||||
go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast ./... -timeout 120m -parallel 8
|
||||
|
||||
lint:
|
||||
golangci-lint run --fix --timeout 10m
|
||||
@@ -80,11 +74,4 @@ compress: build
|
||||
|
||||
generate:
|
||||
rm -rf gen
|
||||
go run github.com/bufbuild/buf/cmd/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
|
||||
buf generate proto
|
||||
|
72
acls.go
72
acls.go
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/tailscale/hujson"
|
||||
"go4.org/netipx"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -407,15 +408,40 @@ func generateACLPolicyDest(
|
||||
needsWildcard bool,
|
||||
stripEmaildomain bool,
|
||||
) ([]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 {
|
||||
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
|
||||
// We can have here stuff like:
|
||||
// git-server:*
|
||||
// 192.168.1.0/24:22
|
||||
// fd7a:115c:a1e0::2:22
|
||||
// fd7a:115c:a1e0::2/128:22
|
||||
// tag:montreal-webserver:80,443
|
||||
// tag:api-server:443
|
||||
// example-host-1:*
|
||||
@@ -508,9 +534,11 @@ func parseProtocol(protocol string) ([]int, bool, error) {
|
||||
// - a group
|
||||
// - a tag
|
||||
// - a host
|
||||
// - an ip
|
||||
// - a cidr
|
||||
// and transform these in IPAddresses.
|
||||
func expandAlias(
|
||||
machines []Machine,
|
||||
machines Machines,
|
||||
aclPolicy ACLPolicy,
|
||||
alias string,
|
||||
stripEmailDomain bool,
|
||||
@@ -592,19 +620,40 @@ func expandAlias(
|
||||
|
||||
// if alias is an host
|
||||
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
|
||||
ip, err := netip.ParseAddr(alias)
|
||||
if err == nil {
|
||||
return []string{ip.String()}, nil
|
||||
if ip, err := netip.ParseAddr(alias); err == nil {
|
||||
log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip")
|
||||
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
|
||||
cidr, err := netip.ParsePrefix(alias)
|
||||
if err == nil {
|
||||
return []string{cidr.String()}, nil
|
||||
if cidr, err := netip.ParsePrefix(alias); err == nil {
|
||||
log.Trace().Str("cidr", cidr.String()).Msg("expandAlias got cidr")
|
||||
val := []string{cidr.String()}
|
||||
// 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)
|
||||
@@ -666,6 +715,7 @@ func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, err
|
||||
|
||||
ports := []tailcfg.PortRange{}
|
||||
for _, portStr := range strings.Split(portsStr, ",") {
|
||||
log.Trace().Msgf("parsing portstring: %s", portStr)
|
||||
rang := strings.Split(portStr, "-")
|
||||
switch len(rang) {
|
||||
case 1:
|
||||
|
88
acls_test.go
88
acls_test.go
@@ -1026,22 +1026,7 @@ func Test_expandAlias(t *testing.T) {
|
||||
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 host by ip",
|
||||
name: "simple host by ip passed through",
|
||||
args: args{
|
||||
alias: "10.0.0.1",
|
||||
machines: []Machine{},
|
||||
@@ -1051,6 +1036,62 @@ func Test_expandAlias(t *testing.T) {
|
||||
want: []string{"10.0.0.1"},
|
||||
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",
|
||||
args: args{
|
||||
@@ -1066,6 +1107,21 @@ func Test_expandAlias(t *testing.T) {
|
||||
want: []string{"10.0.0.132/32"},
|
||||
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",
|
||||
args: args{
|
||||
|
@@ -64,6 +64,7 @@ summary() {
|
||||
echo ""
|
||||
echo " Please follow the next steps to start the software:"
|
||||
echo ""
|
||||
echo " sudo systemctl enable headscale"
|
||||
echo " sudo systemctl start headscale"
|
||||
echo ""
|
||||
echo " Configuration settings can be adjusted here:"
|
||||
|
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
|
||||
|
||||
## Requirements
|
||||
|
||||
- Ubuntu 20.04 or newer, Debian 11 or newer.
|
||||
|
||||
## 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.
|
||||
Get Headscale up and running.
|
||||
|
||||
## 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
|
||||
wget --output-document=/usr/local/bin/headscale \
|
||||
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>
|
||||
wget --output-document=headscale.deb \
|
||||
https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>.deb
|
||||
```
|
||||
|
||||
2. Make `headscale` executable:
|
||||
2. Install Headscale:
|
||||
|
||||
```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
|
||||
# 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
|
||||
sudo systemctl enable headscale
|
||||
```
|
||||
|
||||
4. Create an empty SQLite database:
|
||||
4. Configure Headscale by editing the configuration file:
|
||||
|
||||
```shell
|
||||
touch /var/lib/headscale/db.sqlite
|
||||
nano /etc/headscale/config.yaml
|
||||
```
|
||||
|
||||
5. Create a `headscale` configuration:
|
||||
5. Start Headscale:
|
||||
|
||||
```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. Start the headscale server:
|
||||
6. Check that Headscale is running as intended:
|
||||
|
||||
```shell
|
||||
headscale serve
|
||||
systemctl status headscale
|
||||
```
|
||||
|
||||
This command will start `headscale` in the current terminal session.
|
||||
## Using Headscale
|
||||
|
||||
---
|
||||
|
||||
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/)):
|
||||
### Create a user
|
||||
|
||||
```shell
|
||||
headscale users create myfirstuser
|
||||
@@ -85,16 +67,16 @@ headscale users create myfirstuser
|
||||
|
||||
### Register a machine (normal login)
|
||||
|
||||
On a client machine, execute the `tailscale` login command:
|
||||
On a client machine, run the `tailscale` login command:
|
||||
|
||||
```shell
|
||||
tailscale up --login-server YOUR_HEADSCALE_URL
|
||||
tailscale up --login-server <YOUR_HEADSCALE_URL>
|
||||
```
|
||||
|
||||
Register the machine:
|
||||
|
||||
```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
|
||||
@@ -105,87 +87,9 @@ Generate a key using the command line:
|
||||
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
|
||||
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": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1680776469,
|
||||
"narHash": "sha256-3CXUDK/3q/kieWtdsYpDOBJw3Gw4Af6x+2EiSnIkNQw=",
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "411e8764155aa9354dbcd6d5faaeb97e9e3dce24",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -17,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1680789907,
|
||||
"narHash": "sha256-0AOMkabjbOauxspnqfzqgLKhB2gSh3sLkz1p/jIckcs=",
|
||||
"lastModified": 1681753173,
|
||||
"narHash": "sha256-MrGmzZWLUqh2VstoikKLFFIELXm/lsf/G9U9zR96VD4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9de84cd029054adc54fdc6442e121fbc5ac33baf",
|
||||
"rev": "0a4206a51b386e5cda731e8ac78d76ad924c7125",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -36,6 +39,21 @@
|
||||
"flake-utils": "flake-utils",
|
||||
"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",
|
||||
|
@@ -129,6 +129,14 @@
|
||||
|
||||
shellHook = ''
|
||||
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"
|
||||
)
|
||||
|
||||
const numberOfTestClients = 2
|
||||
|
||||
func aclScenario(t *testing.T, policy headscale.ACLPolicy) *Scenario {
|
||||
func aclScenario(t *testing.T, policy *headscale.ACLPolicy, clientsPerUser int) *Scenario {
|
||||
t.Helper()
|
||||
scenario, err := NewScenario()
|
||||
assert.NoError(t, err)
|
||||
|
||||
spec := map[string]int{
|
||||
"user1": numberOfTestClients,
|
||||
"user2": numberOfTestClients,
|
||||
"user1": clientsPerUser,
|
||||
"user2": clientsPerUser,
|
||||
}
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(spec,
|
||||
@@ -29,18 +27,15 @@ func aclScenario(t *testing.T, policy headscale.ACLPolicy) *Scenario {
|
||||
tsic.WithDockerEntrypoint([]string{
|
||||
"/bin/bash",
|
||||
"-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("/"),
|
||||
},
|
||||
hsic.WithACLPolicy(&policy),
|
||||
hsic.WithACLPolicy(policy),
|
||||
hsic.WithTestName("acl"),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// allClients, err := scenario.ListTailscaleClients()
|
||||
// assert.NoError(t, err)
|
||||
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -230,7 +225,7 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
scenario := aclScenario(t,
|
||||
headscale.ACLPolicy{
|
||||
&headscale.ACLPolicy{
|
||||
ACLs: []headscale.ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
@@ -239,6 +234,7 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
1,
|
||||
)
|
||||
|
||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||
@@ -285,7 +281,7 @@ func TestACLDenyAllPort80(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
scenario := aclScenario(t,
|
||||
headscale.ACLPolicy{
|
||||
&headscale.ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:integration-acl-test": {"user1", "user2"},
|
||||
},
|
||||
@@ -297,6 +293,7 @@ func TestACLDenyAllPort80(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
4,
|
||||
)
|
||||
|
||||
allClients, err := scenario.ListTailscaleClients()
|
||||
@@ -333,7 +330,7 @@ func TestACLAllowUserDst(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
scenario := aclScenario(t,
|
||||
headscale.ACLPolicy{
|
||||
&headscale.ACLPolicy{
|
||||
ACLs: []headscale.ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
@@ -342,6 +339,7 @@ func TestACLAllowUserDst(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
2,
|
||||
)
|
||||
|
||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||
@@ -390,7 +388,7 @@ func TestACLAllowStarDst(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
scenario := aclScenario(t,
|
||||
headscale.ACLPolicy{
|
||||
&headscale.ACLPolicy{
|
||||
ACLs: []headscale.ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
@@ -399,6 +397,7 @@ func TestACLAllowStarDst(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
2,
|
||||
)
|
||||
|
||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||
@@ -441,155 +440,6 @@ func TestACLAllowStarDst(t *testing.T) {
|
||||
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
|
||||
// TestACLNamedHostsCanReach, but it tests if we expand a
|
||||
// full CIDR correctly. All routes should work.
|
||||
@@ -597,7 +447,7 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
scenario := aclScenario(t,
|
||||
headscale.ACLPolicy{
|
||||
&headscale.ACLPolicy{
|
||||
Hosts: headscale.Hosts{
|
||||
"all": netip.MustParsePrefix("100.64.0.0/24"),
|
||||
},
|
||||
@@ -610,6 +460,7 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
3,
|
||||
)
|
||||
|
||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||
@@ -651,3 +502,450 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
||||
err = scenario.Shutdown()
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -274,6 +274,15 @@ func (s *AuthWebFlowScenario) runTailscaleUp(
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -356,6 +356,15 @@ func (s *Scenario) RunTailscaleUp(
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -430,6 +430,15 @@ func (t *TailscaleInContainer) WaitForReady() error {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
@@ -46,3 +46,35 @@ func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
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