mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-31 04:57:45 +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 ~/.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 | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user