Fix IPv6 in ACLs (#1339)
This commit is contained in:
parent
9836b097a4
commit
5e74ca9414
9 changed files with 816 additions and 208 deletions
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"
|
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{
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue