mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-31 21:17:43 +09:00 
			
		
		
		
	Fix IPv6 in ACLs (#1339)
This commit is contained in:
		
							
								
								
									
										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 $$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()...) | ||||
| 		} | ||||
|  | ||||
| 	// if alias is an CIDR | ||||
| 	cidr, err := netip.ParsePrefix(alias) | ||||
| 	if err == nil { | ||||
| 		return []string{cidr.String()}, nil | ||||
| 		return lo.Uniq(ips), 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{ | ||||
|   | ||||
| @@ -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) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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