mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Merge branch 'main' into metrics-listen
This commit is contained in:
		
						commit
						d27f2bc538
					
				
							
								
								
									
										38
									
								
								.github/renovate.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								.github/renovate.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| { | ||||
|   "baseBranches": ["main"], | ||||
|   "username": "renovate-release", | ||||
|   "gitAuthor": "Renovate Bot <bot@renovateapp.com>", | ||||
|   "branchPrefix": "renovateaction/", | ||||
|   "onboarding": false, | ||||
|   "extends": ["config:base", ":rebaseStalePrs"], | ||||
|   "ignorePresets": [":prHourlyLimit2"], | ||||
|   "enabledManagers": ["dockerfile", "gomod", "github-actions","regex" ], | ||||
|   "includeForks": true, | ||||
|   "repositories": ["juanfont/headscale"], | ||||
|   "platform": "github", | ||||
|   "packageRules": [ | ||||
|     { | ||||
|         "matchDatasources": ["go"], | ||||
|         "groupName": "Go modules", | ||||
|         "groupSlug": "gomod", | ||||
|         "separateMajorMinor": false | ||||
|     }, | ||||
|     { | ||||
|         "matchDatasources": ["docker"], | ||||
|         "groupName": "Dockerfiles", | ||||
|         "groupSlug": "dockerfiles" | ||||
|     }  | ||||
|   ], | ||||
|   "regexManagers": [ | ||||
|     { | ||||
|       "fileMatch": [ | ||||
|           ".github/workflows/.*.yml$" | ||||
|       ], | ||||
|       "matchStrings": [ | ||||
|         "\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n" | ||||
|       ], | ||||
|       "datasourceTemplate": "golang-version", | ||||
|       "depNameTemplate": "actions/go-version" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										27
									
								
								.github/workflows/renovatebot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/renovatebot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| --- | ||||
| name: Renovate | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: "* * 5,20 * *" # Every 5th and 20th of the month | ||||
|   workflow_dispatch: | ||||
| jobs: | ||||
|   renovate: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Get token | ||||
|         id: get_token | ||||
|         uses: machine-learning-apps/actions-app-token@master | ||||
|         with: | ||||
|           APP_PEM: ${{ secrets.RENOVATEBOT_SECRET }} | ||||
|           APP_ID: ${{ secrets.RENOVATEBOT_APP_ID }} | ||||
| 
 | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2.0.0 | ||||
| 
 | ||||
|       - name: Self-hosted Renovate | ||||
|         uses: renovatebot/github-action@v31.81.3 | ||||
|         with: | ||||
|           configurationFile: .github/renovate.json | ||||
|           token: "x-access-token:${{ steps.get_token.outputs.app_token }}" | ||||
|         # env: | ||||
|         #  LOG_LEVEL: "debug" | ||||
| @ -48,6 +48,7 @@ linters-settings: | ||||
|       - ip | ||||
|       - ok | ||||
|       - c | ||||
|       - tt | ||||
| 
 | ||||
|   gocritic: | ||||
|     disabled-checks: | ||||
|  | ||||
| @ -10,15 +10,12 @@ builds: | ||||
|   - id: darwin-amd64 | ||||
|     main: ./cmd/headscale/headscale.go | ||||
|     mod_timestamp: "{{ .CommitTimestamp }}" | ||||
|     env: | ||||
|       - CGO_ENABLED=0 | ||||
|     goos: | ||||
|       - darwin | ||||
|     goarch: | ||||
|       - amd64 | ||||
|     env: | ||||
|       - PKG_CONFIG_SYSROOT_DIR=/sysroot/macos/amd64 | ||||
|       - PKG_CONFIG_PATH=/sysroot/macos/amd64/usr/local/lib/pkgconfig | ||||
|       - CC=o64-clang | ||||
|       - CXX=o64-clang++ | ||||
|     flags: | ||||
|       - -mod=readonly | ||||
|     ldflags: | ||||
| @ -27,46 +24,40 @@ builds: | ||||
|   - id: linux-armhf | ||||
|     main: ./cmd/headscale/headscale.go | ||||
|     mod_timestamp: "{{ .CommitTimestamp }}" | ||||
|     env: | ||||
|       - CGO_ENABLED=0 | ||||
|     goos: | ||||
|       - linux | ||||
|     goarch: | ||||
|       - arm | ||||
|     goarm: | ||||
|       - "7" | ||||
|     env: | ||||
|       - CC=arm-linux-gnueabihf-gcc | ||||
|       - CXX=arm-linux-gnueabihf-g++ | ||||
|       - CGO_FLAGS=--sysroot=/sysroot/linux/armhf | ||||
|       - CGO_LDFLAGS=--sysroot=/sysroot/linux/armhf | ||||
|       - PKG_CONFIG_SYSROOT_DIR=/sysroot/linux/armhf | ||||
|       - PKG_CONFIG_PATH=/sysroot/linux/armhf/opt/vc/lib/pkgconfig:/sysroot/linux/armhf/usr/lib/arm-linux-gnueabihf/pkgconfig:/sysroot/linux/armhf/usr/lib/pkgconfig:/sysroot/linux/armhf/usr/local/lib/pkgconfig | ||||
|     flags: | ||||
|       - -mod=readonly | ||||
|     ldflags: | ||||
|       - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} | ||||
| 
 | ||||
|   - id: linux-amd64 | ||||
|     mod_timestamp: "{{ .CommitTimestamp }}" | ||||
|     env: | ||||
|       - CGO_ENABLED=1 | ||||
|       - CGO_ENABLED=0 | ||||
|     goos: | ||||
|       - linux | ||||
|     goarch: | ||||
|       - amd64 | ||||
|     main: ./cmd/headscale/headscale.go | ||||
|     mod_timestamp: "{{ .CommitTimestamp }}" | ||||
|     ldflags: | ||||
|       - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} | ||||
| 
 | ||||
|   - id: linux-arm64 | ||||
|     mod_timestamp: "{{ .CommitTimestamp }}" | ||||
|     env: | ||||
|       - CGO_ENABLED=0 | ||||
|     goos: | ||||
|       - linux | ||||
|     goarch: | ||||
|       - arm64 | ||||
|     env: | ||||
|       - CGO_ENABLED=1 | ||||
|       - CC=aarch64-linux-gnu-gcc | ||||
|     main: ./cmd/headscale/headscale.go | ||||
|     mod_timestamp: "{{ .CommitTimestamp }}" | ||||
|     ldflags: | ||||
|       - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										22
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -2,6 +2,28 @@ | ||||
| 
 | ||||
| **TBD (TBD):** | ||||
| 
 | ||||
| **0.14.0 (2022-xx-xx):** | ||||
| 
 | ||||
| **UPCOMING BREAKING**: | ||||
| From the **next** version (`0.15.0`), all machines will be able to communicate regardless of | ||||
| if they are in the same namespace. This means that the behaviour currently limited to ACLs | ||||
| will become default. From version `0.15.0`, all limitation of communications must be done | ||||
| with ACLs. | ||||
| 
 | ||||
| This is a part of aligning `headscale`'s behaviour with Tailscale's upstream behaviour. | ||||
| 
 | ||||
| **BREAKING**: | ||||
| 
 | ||||
| - ACLs have been rewritten to align with the bevaviour Tailscale Control Panel provides. **NOTE:** This is only active if you use ACLs | ||||
|   - Namespaces are now treated as Users | ||||
|   - All machines can communicate with all machines by default | ||||
|   - Tags should now work correctly and adding a host to Headscale should now reload the rules. | ||||
|   - The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features | ||||
| 
 | ||||
| **Changes**: | ||||
| 
 | ||||
| - Remove dependency on CGO (switch from CGO SQLite to pure Go) [#346](https://github.com/juanfont/headscale/pull/346) | ||||
| 
 | ||||
| **0.13.0 (2022-02-18):** | ||||
| 
 | ||||
| **Features**: | ||||
|  | ||||
| @ -8,7 +8,7 @@ RUN go mod download | ||||
| 
 | ||||
| COPY . . | ||||
| 
 | ||||
| RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale | ||||
| RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale | ||||
| RUN strip /go/bin/headscale | ||||
| RUN test -e /go/bin/headscale | ||||
| 
 | ||||
|  | ||||
| @ -9,7 +9,7 @@ RUN go mod download | ||||
| 
 | ||||
| COPY . . | ||||
| 
 | ||||
| RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale | ||||
| RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale | ||||
| RUN strip /go/bin/headscale | ||||
| RUN test -e /go/bin/headscale | ||||
| 
 | ||||
|  | ||||
| @ -8,7 +8,7 @@ RUN go mod download | ||||
| 
 | ||||
| COPY . . | ||||
| 
 | ||||
| RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale | ||||
| RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale | ||||
| RUN test -e /go/bin/headscale | ||||
| 
 | ||||
| # Debug image | ||||
|  | ||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @ -10,7 +10,7 @@ PROTO_SOURCES = $(call rwildcard,,*.proto) | ||||
| 
 | ||||
| 
 | ||||
| build: | ||||
| 	go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go | ||||
| 	GGO_ENABLED=0 go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go | ||||
| 
 | ||||
| dev: lint test build | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										53
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								README.md
									
									
									
									
									
								
							| @ -47,6 +47,7 @@ If you would like to sponsor features, bugs or prioritisation, reach out to one | ||||
| | ------- | ----------------------------------------------------------------------------------------------------------------- | | ||||
| | Linux   | Yes                                                                                                               | | ||||
| | OpenBSD | Yes                                                                                                               | | ||||
| | FreeBSD | Yes                                                                                                               | | ||||
| | macOS   | Yes (see `/apple` on your headscale for more information)                                                         | | ||||
| | Windows | Yes [docs](./docs/windows-client.md)                                                                              | | ||||
| | Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) | | ||||
| @ -150,6 +151,13 @@ make build | ||||
|             <sub style="font-size:14px"><b>ohdearaugustin</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/restanrm> | ||||
|             <img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/ItalyPaleAle> | ||||
|             <img src=https://avatars.githubusercontent.com/u/43508?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Alessandro (Ale) Segala/> | ||||
| @ -157,6 +165,8 @@ make build | ||||
|             <sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/unreality> | ||||
|             <img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/> | ||||
| @ -164,8 +174,6 @@ make build | ||||
|             <sub style="font-size:14px"><b>unreality</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/negbie> | ||||
|             <img src=https://avatars.githubusercontent.com/u/20154956?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Eugen Biegler/> | ||||
| @ -201,6 +209,8 @@ make build | ||||
|             <sub style="font-size:14px"><b>Michael G.</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/ptman> | ||||
|             <img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/> | ||||
| @ -208,8 +218,6 @@ make build | ||||
|             <sub style="font-size:14px"><b>Paul Tötterman</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/cmars> | ||||
|             <img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/> | ||||
| @ -245,6 +253,8 @@ make build | ||||
|             <sub style="font-size:14px"><b>thomas</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/aberoham> | ||||
|             <img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/> | ||||
| @ -252,15 +262,6 @@ make build | ||||
|             <sub style="font-size:14px"><b>Abraham Ingersoll</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/restanrm> | ||||
|             <img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/artemklevtsov> | ||||
|             <img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/> | ||||
| @ -305,6 +306,13 @@ make build | ||||
|             <sub style="font-size:14px"><b>JJGadgets</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/madjam002> | ||||
|             <img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Jamie Greeff</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/jimt> | ||||
|             <img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/> | ||||
| @ -333,6 +341,8 @@ make build | ||||
|             <sub style="font-size:14px"><b>Ryan Fowler</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/shaananc> | ||||
|             <img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/> | ||||
| @ -340,8 +350,6 @@ make build | ||||
|             <sub style="font-size:14px"><b>Shaanan Cohney</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/m-tanner-dev0> | ||||
|             <img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/> | ||||
| @ -377,6 +385,8 @@ make build | ||||
|             <sub style="font-size:14px"><b>Tjerk Woudsma</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/zekker6> | ||||
|             <img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/> | ||||
| @ -384,8 +394,6 @@ make build | ||||
|             <sub style="font-size:14px"><b>Zakhar Bessarab</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/Bpazy> | ||||
|             <img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ZiYuan/> | ||||
| @ -421,6 +429,15 @@ make build | ||||
|             <sub style="font-size:14px"><b>lion24</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/pernila> | ||||
|             <img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=pernila/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>pernila</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/Wakeful-Cloud> | ||||
|             <img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/> | ||||
| @ -428,8 +445,6 @@ make build | ||||
|             <sub style="font-size:14px"><b>Wakeful-Cloud</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/xpzouying> | ||||
|             <img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/> | ||||
|  | ||||
							
								
								
									
										233
									
								
								acls.go
									
									
									
									
									
								
							
							
						
						
									
										233
									
								
								acls.go
									
									
									
									
									
								
							| @ -20,7 +20,6 @@ const ( | ||||
| 	errInvalidUserSection = Error("invalid user section") | ||||
| 	errInvalidGroup       = Error("invalid group") | ||||
| 	errInvalidTag         = Error("invalid tag") | ||||
| 	errInvalidNamespace   = Error("invalid namespace") | ||||
| 	errInvalidPortFormat  = Error("invalid port format") | ||||
| ) | ||||
| 
 | ||||
| @ -69,13 +68,17 @@ func (h *Headscale) LoadACLPolicy(path string) error { | ||||
| 	} | ||||
| 
 | ||||
| 	h.aclPolicy = &policy | ||||
| 
 | ||||
| 	return h.UpdateACLRules() | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) UpdateACLRules() error { | ||||
| 	rules, err := h.generateACLRules() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	h.aclRules = rules | ||||
| 
 | ||||
| 	log.Trace().Interface("ACL", rules).Msg("ACL rules generated") | ||||
| 	h.aclRules = rules | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| @ -83,16 +86,23 @@ func (h *Headscale) LoadACLPolicy(path string) error { | ||||
| func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) { | ||||
| 	rules := []tailcfg.FilterRule{} | ||||
| 
 | ||||
| 	if h.aclPolicy == nil { | ||||
| 		return nil, errEmptyPolicy | ||||
| 	} | ||||
| 
 | ||||
| 	machines, err := h.ListAllMachines() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	for index, acl := range h.aclPolicy.ACLs { | ||||
| 		if acl.Action != "accept" { | ||||
| 			return nil, errInvalidAction | ||||
| 		} | ||||
| 
 | ||||
| 		filterRule := tailcfg.FilterRule{} | ||||
| 
 | ||||
| 		srcIPs := []string{} | ||||
| 		for innerIndex, user := range acl.Users { | ||||
| 			srcs, err := h.generateACLPolicySrcIP(user) | ||||
| 			srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user) | ||||
| 			if err != nil { | ||||
| 				log.Error(). | ||||
| 					Msgf("Error parsing ACL %d, User %d", index, innerIndex) | ||||
| @ -101,11 +111,10 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) { | ||||
| 			} | ||||
| 			srcIPs = append(srcIPs, srcs...) | ||||
| 		} | ||||
| 		filterRule.SrcIPs = srcIPs | ||||
| 
 | ||||
| 		destPorts := []tailcfg.NetPortRange{} | ||||
| 		for innerIndex, ports := range acl.Ports { | ||||
| 			dests, err := h.generateACLPolicyDestPorts(ports) | ||||
| 			dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports) | ||||
| 			if err != nil { | ||||
| 				log.Error(). | ||||
| 					Msgf("Error parsing ACL %d, Port %d", index, innerIndex) | ||||
| @ -124,11 +133,17 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) { | ||||
| 	return rules, nil | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) { | ||||
| 	return h.expandAlias(u) | ||||
| func (h *Headscale) generateACLPolicySrcIP( | ||||
| 	machines []Machine, | ||||
| 	aclPolicy ACLPolicy, | ||||
| 	u string, | ||||
| ) ([]string, error) { | ||||
| 	return expandAlias(machines, aclPolicy, u) | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) generateACLPolicyDestPorts( | ||||
| 	machines []Machine, | ||||
| 	aclPolicy ACLPolicy, | ||||
| 	d string, | ||||
| ) ([]tailcfg.NetPortRange, error) { | ||||
| 	tokens := strings.Split(d, ":") | ||||
| @ -149,11 +164,11 @@ func (h *Headscale) generateACLPolicyDestPorts( | ||||
| 		alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1]) | ||||
| 	} | ||||
| 
 | ||||
| 	expanded, err := h.expandAlias(alias) | ||||
| 	expanded, err := expandAlias(machines, aclPolicy, alias) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	ports, err := h.expandPorts(tokens[len(tokens)-1]) | ||||
| 	ports, err := expandPorts(tokens[len(tokens)-1]) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @ -172,21 +187,28 @@ func (h *Headscale) generateACLPolicyDestPorts( | ||||
| 	return dests, nil | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) expandAlias(alias string) ([]string, error) { | ||||
| // expandalias has an input of either
 | ||||
| // - a namespace
 | ||||
| // - a group
 | ||||
| // - a tag
 | ||||
| // and transform these in IPAddresses.
 | ||||
| func expandAlias( | ||||
| 	machines []Machine, | ||||
| 	aclPolicy ACLPolicy, | ||||
| 	alias string, | ||||
| ) ([]string, error) { | ||||
| 	ips := []string{} | ||||
| 	if alias == "*" { | ||||
| 		return []string{"*"}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	if strings.HasPrefix(alias, "group:") { | ||||
| 		if _, ok := h.aclPolicy.Groups[alias]; !ok { | ||||
| 			return nil, errInvalidGroup | ||||
| 		namespaces, err := expandGroup(aclPolicy, alias) | ||||
| 		if err != nil { | ||||
| 			return ips, err | ||||
| 		} | ||||
| 		ips := []string{} | ||||
| 		for _, n := range h.aclPolicy.Groups[alias] { | ||||
| 			nodes, err := h.ListMachinesInNamespace(n) | ||||
| 			if err != nil { | ||||
| 				return nil, errInvalidNamespace | ||||
| 			} | ||||
| 		for _, n := range namespaces { | ||||
| 			nodes := filterMachinesByNamespace(machines, n) | ||||
| 			for _, node := range nodes { | ||||
| 				ips = append(ips, node.IPAddresses.ToStringSlice()...) | ||||
| 			} | ||||
| @ -196,35 +218,23 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) { | ||||
| 	} | ||||
| 
 | ||||
| 	if strings.HasPrefix(alias, "tag:") { | ||||
| 		if _, ok := h.aclPolicy.TagOwners[alias]; !ok { | ||||
| 			return nil, errInvalidTag | ||||
| 		owners, err := expandTagOwners(aclPolicy, alias) | ||||
| 		if err != nil { | ||||
| 			return ips, err | ||||
| 		} | ||||
| 
 | ||||
| 		// This will have HORRIBLE performance.
 | ||||
| 		// We need to change the data model to better store tags
 | ||||
| 		machines := []Machine{} | ||||
| 		if err := h.db.Where("registered").Find(&machines).Error; err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		ips := []string{} | ||||
| 		for _, machine := range machines { | ||||
| 			hostinfo := tailcfg.Hostinfo{} | ||||
| 			if len(machine.HostInfo) != 0 { | ||||
| 				hi, err := machine.HostInfo.MarshalJSON() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 		for _, namespace := range owners { | ||||
| 			machines := filterMachinesByNamespace(machines, namespace) | ||||
| 			for _, machine := range machines { | ||||
| 				if len(machine.HostInfo) == 0 { | ||||
| 					continue | ||||
| 				} | ||||
| 				err = json.Unmarshal(hi, &hostinfo) | ||||
| 				hi, err := machine.GetHostInfo() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 					return ips, err | ||||
| 				} | ||||
| 
 | ||||
| 				// FIXME: Check TagOwners allows this
 | ||||
| 				for _, t := range hostinfo.RequestTags { | ||||
| 					if alias[4:] == t { | ||||
| 				for _, t := range hi.RequestTags { | ||||
| 					if alias == t { | ||||
| 						ips = append(ips, machine.IPAddresses.ToStringSlice()...) | ||||
| 
 | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| @ -233,38 +243,82 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) { | ||||
| 		return ips, nil | ||||
| 	} | ||||
| 
 | ||||
| 	n, err := h.GetNamespace(alias) | ||||
| 	if err == nil { | ||||
| 		nodes, err := h.ListMachinesInNamespace(n.Name) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		ips := []string{} | ||||
| 		for _, n := range nodes { | ||||
| 			ips = append(ips, n.IPAddresses.ToStringSlice()...) | ||||
| 		} | ||||
| 
 | ||||
| 	// if alias is a namespace
 | ||||
| 	nodes := filterMachinesByNamespace(machines, alias) | ||||
| 	nodes, err := excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias) | ||||
| 	if err != nil { | ||||
| 		return ips, err | ||||
| 	} | ||||
| 	for _, n := range nodes { | ||||
| 		ips = append(ips, n.IPAddresses.ToStringSlice()...) | ||||
| 	} | ||||
| 	if len(ips) > 0 { | ||||
| 		return ips, nil | ||||
| 	} | ||||
| 
 | ||||
| 	if h, ok := h.aclPolicy.Hosts[alias]; ok { | ||||
| 	// if alias is an host
 | ||||
| 	if h, ok := aclPolicy.Hosts[alias]; ok { | ||||
| 		return []string{h.String()}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// if alias is an IP
 | ||||
| 	ip, err := netaddr.ParseIP(alias) | ||||
| 	if err == nil { | ||||
| 		return []string{ip.String()}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// if alias is an CIDR
 | ||||
| 	cidr, err := netaddr.ParseIPPrefix(alias) | ||||
| 	if err == nil { | ||||
| 		return []string{cidr.String()}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, errInvalidUserSection | ||||
| 	return ips, errInvalidUserSection | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) { | ||||
| // excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
 | ||||
| // that are correctly tagged since they should not be listed as being in the namespace
 | ||||
| // we assume in this function that we only have nodes from 1 namespace.
 | ||||
| func excludeCorrectlyTaggedNodes( | ||||
| 	aclPolicy ACLPolicy, | ||||
| 	nodes []Machine, | ||||
| 	namespace string, | ||||
| ) ([]Machine, error) { | ||||
| 	out := []Machine{} | ||||
| 	tags := []string{} | ||||
| 	for tag, ns := range aclPolicy.TagOwners { | ||||
| 		if containsString(ns, namespace) { | ||||
| 			tags = append(tags, tag) | ||||
| 		} | ||||
| 	} | ||||
| 	// for each machine if tag is in tags list, don't append it.
 | ||||
| 	for _, machine := range nodes { | ||||
| 		if len(machine.HostInfo) == 0 { | ||||
| 			out = append(out, machine) | ||||
| 
 | ||||
| 			continue | ||||
| 		} | ||||
| 		hi, err := machine.GetHostInfo() | ||||
| 		if err != nil { | ||||
| 			return out, err | ||||
| 		} | ||||
| 		found := false | ||||
| 		for _, t := range hi.RequestTags { | ||||
| 			if containsString(tags, t) { | ||||
| 				found = true | ||||
| 
 | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !found { | ||||
| 			out = append(out, machine) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return out, nil | ||||
| } | ||||
| 
 | ||||
| func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) { | ||||
| 	if portsStr == "*" { | ||||
| 		return &[]tailcfg.PortRange{ | ||||
| 			{First: portRangeBegin, Last: portRangeEnd}, | ||||
| @ -306,3 +360,64 @@ func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) { | ||||
| 
 | ||||
| 	return &ports, nil | ||||
| } | ||||
| 
 | ||||
| func filterMachinesByNamespace(machines []Machine, namespace string) []Machine { | ||||
| 	out := []Machine{} | ||||
| 	for _, machine := range machines { | ||||
| 		if machine.Namespace.Name == namespace { | ||||
| 			out = append(out, machine) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return out | ||||
| } | ||||
| 
 | ||||
| // expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
 | ||||
| // a group cannot be composed of groups.
 | ||||
| func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) { | ||||
| 	var owners []string | ||||
| 	ows, ok := aclPolicy.TagOwners[tag] | ||||
| 	if !ok { | ||||
| 		return []string{}, fmt.Errorf( | ||||
| 			"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners", | ||||
| 			errInvalidTag, | ||||
| 			tag, | ||||
| 		) | ||||
| 	} | ||||
| 	for _, owner := range ows { | ||||
| 		if strings.HasPrefix(owner, "group:") { | ||||
| 			gs, err := expandGroup(aclPolicy, owner) | ||||
| 			if err != nil { | ||||
| 				return []string{}, err | ||||
| 			} | ||||
| 			owners = append(owners, gs...) | ||||
| 		} else { | ||||
| 			owners = append(owners, owner) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return owners, nil | ||||
| } | ||||
| 
 | ||||
| // expandGroup will return the list of namespace inside the group
 | ||||
| // after some validation.
 | ||||
| func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) { | ||||
| 	groups, ok := aclPolicy.Groups[group] | ||||
| 	if !ok { | ||||
| 		return []string{}, fmt.Errorf( | ||||
| 			"group %v isn't registered. %w", | ||||
| 			group, | ||||
| 			errInvalidGroup, | ||||
| 		) | ||||
| 	} | ||||
| 	for _, g := range groups { | ||||
| 		if strings.HasPrefix(g, "group:") { | ||||
| 			return []string{}, fmt.Errorf( | ||||
| 				"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups", | ||||
| 				errInvalidGroup, | ||||
| 			) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return groups, nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										968
									
								
								acls_test.go
									
									
									
									
									
								
							
							
						
						
									
										968
									
								
								acls_test.go
									
									
									
									
									
								
							| @ -1,7 +1,14 @@ | ||||
| package headscale | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"gopkg.in/check.v1" | ||||
| 	"gorm.io/datatypes" | ||||
| 	"inet.af/netaddr" | ||||
| 	"tailscale.com/tailcfg" | ||||
| ) | ||||
| 
 | ||||
| func (s *Suite) TestWrongPath(c *check.C) { | ||||
| @ -52,6 +59,245 @@ func (s *Suite) TestBasicRule(c *check.C) { | ||||
| 	c.Assert(rules, check.NotNil) | ||||
| } | ||||
| 
 | ||||
| // TODO(kradalby): Make tests values safe, independent and descriptive.
 | ||||
| func (s *Suite) TestInvalidAction(c *check.C) { | ||||
| 	app.aclPolicy = &ACLPolicy{ | ||||
| 		ACLs: []ACL{ | ||||
| 			{Action: "invalidAction", Users: []string{"*"}, Ports: []string{"*:*"}}, | ||||
| 		}, | ||||
| 	} | ||||
| 	err := app.UpdateACLRules() | ||||
| 	c.Assert(errors.Is(err, errInvalidAction), check.Equals, true) | ||||
| } | ||||
| 
 | ||||
| func (s *Suite) TestInvalidGroupInGroup(c *check.C) { | ||||
| 	// this ACL is wrong because the group in users sections doesn't exist
 | ||||
| 	app.aclPolicy = &ACLPolicy{ | ||||
| 		Groups: Groups{ | ||||
| 			"group:test":  []string{"foo"}, | ||||
| 			"group:error": []string{"foo", "group:test"}, | ||||
| 		}, | ||||
| 		ACLs: []ACL{ | ||||
| 			{Action: "accept", Users: []string{"group:error"}, Ports: []string{"*:*"}}, | ||||
| 		}, | ||||
| 	} | ||||
| 	err := app.UpdateACLRules() | ||||
| 	c.Assert(errors.Is(err, errInvalidGroup), check.Equals, true) | ||||
| } | ||||
| 
 | ||||
| func (s *Suite) TestInvalidTagOwners(c *check.C) { | ||||
| 	// this ACL is wrong because no tagOwners own the requested tag for the server
 | ||||
| 	app.aclPolicy = &ACLPolicy{ | ||||
| 		ACLs: []ACL{ | ||||
| 			{Action: "accept", Users: []string{"tag:foo"}, Ports: []string{"*:*"}}, | ||||
| 		}, | ||||
| 	} | ||||
| 	err := app.UpdateACLRules() | ||||
| 	c.Assert(errors.Is(err, errInvalidTag), check.Equals, true) | ||||
| } | ||||
| 
 | ||||
| // this test should validate that we can expand a group in a TagOWner section and
 | ||||
| // match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
 | ||||
| // the tag is matched in the Users section.
 | ||||
| func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) { | ||||
| 	namespace, err := app.CreateNamespace("user1") | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	_, err = app.GetMachine("user1", "testmachine") | ||||
| 	c.Assert(err, check.NotNil) | ||||
| 	hostInfo := []byte( | ||||
| 		"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}", | ||||
| 	) | ||||
| 	machine := Machine{ | ||||
| 		ID:             0, | ||||
| 		MachineKey:     "foo", | ||||
| 		NodeKey:        "bar", | ||||
| 		DiscoKey:       "faa", | ||||
| 		Name:           "testmachine", | ||||
| 		IPAddresses:    MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, | ||||
| 		NamespaceID:    namespace.ID, | ||||
| 		Registered:     true, | ||||
| 		RegisterMethod: RegisterMethodAuthKey, | ||||
| 		AuthKeyID:      uint(pak.ID), | ||||
| 		HostInfo:       datatypes.JSON(hostInfo), | ||||
| 	} | ||||
| 	app.db.Save(&machine) | ||||
| 
 | ||||
| 	app.aclPolicy = &ACLPolicy{ | ||||
| 		Groups:    Groups{"group:test": []string{"user1", "user2"}}, | ||||
| 		TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}}, | ||||
| 		ACLs: []ACL{ | ||||
| 			{Action: "accept", Users: []string{"tag:test"}, Ports: []string{"*:*"}}, | ||||
| 		}, | ||||
| 	} | ||||
| 	err = app.UpdateACLRules() | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 	c.Assert(app.aclRules, check.HasLen, 1) | ||||
| 	c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1) | ||||
| 	c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1") | ||||
| } | ||||
| 
 | ||||
| // this test should validate that we can expand a group in a TagOWner section and
 | ||||
| // match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
 | ||||
| // the tag is matched in the Ports section.
 | ||||
| func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) { | ||||
| 	namespace, err := app.CreateNamespace("user1") | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	_, err = app.GetMachine("user1", "testmachine") | ||||
| 	c.Assert(err, check.NotNil) | ||||
| 	hostInfo := []byte( | ||||
| 		"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}", | ||||
| 	) | ||||
| 	machine := Machine{ | ||||
| 		ID:             1, | ||||
| 		MachineKey:     "12345", | ||||
| 		NodeKey:        "bar", | ||||
| 		DiscoKey:       "faa", | ||||
| 		Name:           "testmachine", | ||||
| 		IPAddresses:    MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, | ||||
| 		NamespaceID:    namespace.ID, | ||||
| 		Registered:     true, | ||||
| 		RegisterMethod: RegisterMethodAuthKey, | ||||
| 		AuthKeyID:      uint(pak.ID), | ||||
| 		HostInfo:       datatypes.JSON(hostInfo), | ||||
| 	} | ||||
| 	app.db.Save(&machine) | ||||
| 
 | ||||
| 	app.aclPolicy = &ACLPolicy{ | ||||
| 		Groups:    Groups{"group:test": []string{"user1", "user2"}}, | ||||
| 		TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}}, | ||||
| 		ACLs: []ACL{ | ||||
| 			{Action: "accept", Users: []string{"*"}, Ports: []string{"tag:test:*"}}, | ||||
| 		}, | ||||
| 	} | ||||
| 	err = app.UpdateACLRules() | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 	c.Assert(app.aclRules, check.HasLen, 1) | ||||
| 	c.Assert(app.aclRules[0].DstPorts, check.HasLen, 1) | ||||
| 	c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1") | ||||
| } | ||||
| 
 | ||||
| // need a test with:
 | ||||
| // tag on a host that isn't owned by a tag owners. So the namespace
 | ||||
| // of the host should be valid.
 | ||||
| func (s *Suite) TestInvalidTagValidNamespace(c *check.C) { | ||||
| 	namespace, err := app.CreateNamespace("user1") | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	_, err = app.GetMachine("user1", "testmachine") | ||||
| 	c.Assert(err, check.NotNil) | ||||
| 	hostInfo := []byte( | ||||
| 		"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:foo\"]}", | ||||
| 	) | ||||
| 	machine := Machine{ | ||||
| 		ID:             1, | ||||
| 		MachineKey:     "12345", | ||||
| 		NodeKey:        "bar", | ||||
| 		DiscoKey:       "faa", | ||||
| 		Name:           "testmachine", | ||||
| 		IPAddresses:    MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, | ||||
| 		NamespaceID:    namespace.ID, | ||||
| 		Registered:     true, | ||||
| 		RegisterMethod: RegisterMethodAuthKey, | ||||
| 		AuthKeyID:      uint(pak.ID), | ||||
| 		HostInfo:       datatypes.JSON(hostInfo), | ||||
| 	} | ||||
| 	app.db.Save(&machine) | ||||
| 
 | ||||
| 	app.aclPolicy = &ACLPolicy{ | ||||
| 		TagOwners: TagOwners{"tag:test": []string{"user1"}}, | ||||
| 		ACLs: []ACL{ | ||||
| 			{Action: "accept", Users: []string{"user1"}, Ports: []string{"*:*"}}, | ||||
| 		}, | ||||
| 	} | ||||
| 	err = app.UpdateACLRules() | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 	c.Assert(app.aclRules, check.HasLen, 1) | ||||
| 	c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1) | ||||
| 	c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1") | ||||
| } | ||||
| 
 | ||||
| // tag on a host is owned by a tag owner, the tag is valid.
 | ||||
| // an ACL rule is matching the tag to a namespace. It should not be valid since the
 | ||||
| // host should be tied to the tag now.
 | ||||
| func (s *Suite) TestValidTagInvalidNamespace(c *check.C) { | ||||
| 	namespace, err := app.CreateNamespace("user1") | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	_, err = app.GetMachine("user1", "webserver") | ||||
| 	c.Assert(err, check.NotNil) | ||||
| 	hostInfo := []byte( | ||||
| 		"{\"OS\":\"centos\",\"Hostname\":\"webserver\",\"RequestTags\":[\"tag:webapp\"]}", | ||||
| 	) | ||||
| 	machine := Machine{ | ||||
| 		ID:             1, | ||||
| 		MachineKey:     "12345", | ||||
| 		NodeKey:        "bar", | ||||
| 		DiscoKey:       "faa", | ||||
| 		Name:           "webserver", | ||||
| 		IPAddresses:    MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, | ||||
| 		NamespaceID:    namespace.ID, | ||||
| 		Registered:     true, | ||||
| 		RegisterMethod: RegisterMethodAuthKey, | ||||
| 		AuthKeyID:      uint(pak.ID), | ||||
| 		HostInfo:       datatypes.JSON(hostInfo), | ||||
| 	} | ||||
| 	app.db.Save(&machine) | ||||
| 	_, err = app.GetMachine("user1", "user") | ||||
| 	hostInfo = []byte("{\"OS\":\"debian\",\"Hostname\":\"user\"}") | ||||
| 	c.Assert(err, check.NotNil) | ||||
| 	machine = Machine{ | ||||
| 		ID:             2, | ||||
| 		MachineKey:     "56789", | ||||
| 		NodeKey:        "bar2", | ||||
| 		DiscoKey:       "faab", | ||||
| 		Name:           "user", | ||||
| 		IPAddresses:    MachineAddresses{netaddr.MustParseIP("100.64.0.2")}, | ||||
| 		NamespaceID:    namespace.ID, | ||||
| 		Registered:     true, | ||||
| 		RegisterMethod: RegisterMethodAuthKey, | ||||
| 		AuthKeyID:      uint(pak.ID), | ||||
| 		HostInfo:       datatypes.JSON(hostInfo), | ||||
| 	} | ||||
| 	app.db.Save(&machine) | ||||
| 
 | ||||
| 	app.aclPolicy = &ACLPolicy{ | ||||
| 		TagOwners: TagOwners{"tag:webapp": []string{"user1"}}, | ||||
| 		ACLs: []ACL{ | ||||
| 			{ | ||||
| 				Action: "accept", | ||||
| 				Users:  []string{"user1"}, | ||||
| 				Ports:  []string{"tag:webapp:80,443"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	err = app.UpdateACLRules() | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 	c.Assert(app.aclRules, check.HasLen, 1) | ||||
| 	c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1) | ||||
| 	c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.2") | ||||
| 	c.Assert(app.aclRules[0].DstPorts, check.HasLen, 2) | ||||
| 	c.Assert(app.aclRules[0].DstPorts[0].Ports.First, check.Equals, uint16(80)) | ||||
| 	c.Assert(app.aclRules[0].DstPorts[0].Ports.Last, check.Equals, uint16(80)) | ||||
| 	c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1") | ||||
| 	c.Assert(app.aclRules[0].DstPorts[1].Ports.First, check.Equals, uint16(443)) | ||||
| 	c.Assert(app.aclRules[0].DstPorts[1].Ports.Last, check.Equals, uint16(443)) | ||||
| 	c.Assert(app.aclRules[0].DstPorts[1].IP, check.Equals, "100.64.0.1") | ||||
| } | ||||
| 
 | ||||
| func (s *Suite) TestPortRange(c *check.C) { | ||||
| 	err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson") | ||||
| 	c.Assert(err, check.IsNil) | ||||
| @ -94,7 +340,7 @@ func (s *Suite) TestPortNamespace(c *check.C) { | ||||
| 	ips, _ := app.getAvailableIPs() | ||||
| 	machine := Machine{ | ||||
| 		ID:             0, | ||||
| 		MachineKey:     "foo", | ||||
| 		MachineKey:     "12345", | ||||
| 		NodeKey:        "bar", | ||||
| 		DiscoKey:       "faa", | ||||
| 		Name:           "testmachine", | ||||
| @ -165,3 +411,723 @@ func (s *Suite) TestPortGroup(c *check.C) { | ||||
| 	c.Assert(len(ips), check.Equals, 1) | ||||
| 	c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String()) | ||||
| } | ||||
| 
 | ||||
| func Test_expandGroup(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		aclPolicy ACLPolicy | ||||
| 		group     string | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name    string | ||||
| 		args    args | ||||
| 		want    []string | ||||
| 		wantErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "simple test", | ||||
| 			args: args{ | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					Groups: Groups{ | ||||
| 						"group:test": []string{"user1", "user2", "user3"}, | ||||
| 						"group:foo":  []string{"user2", "user3"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				group: "group:test", | ||||
| 			}, | ||||
| 			want:    []string{"user1", "user2", "user3"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "InexistantGroup", | ||||
| 			args: args{ | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					Groups: Groups{ | ||||
| 						"group:test": []string{"user1", "user2", "user3"}, | ||||
| 						"group:foo":  []string{"user2", "user3"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				group: "group:undefined", | ||||
| 			}, | ||||
| 			want:    []string{}, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			got, err := expandGroup(test.args.aclPolicy, test.args.group) | ||||
| 			if (err != nil) != test.wantErr { | ||||
| 				t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 			if !reflect.DeepEqual(got, test.want) { | ||||
| 				t.Errorf("expandGroup() = %v, want %v", got, test.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Test_expandTagOwners(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		aclPolicy ACLPolicy | ||||
| 		tag       string | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name    string | ||||
| 		args    args | ||||
| 		want    []string | ||||
| 		wantErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "simple tag expansion", | ||||
| 			args: args{ | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					TagOwners: TagOwners{"tag:test": []string{"user1"}}, | ||||
| 				}, | ||||
| 				tag: "tag:test", | ||||
| 			}, | ||||
| 			want:    []string{"user1"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expand with tag and group", | ||||
| 			args: args{ | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					Groups:    Groups{"group:foo": []string{"user1", "user2"}}, | ||||
| 					TagOwners: TagOwners{"tag:test": []string{"group:foo"}}, | ||||
| 				}, | ||||
| 				tag: "tag:test", | ||||
| 			}, | ||||
| 			want:    []string{"user1", "user2"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expand with namespace and group", | ||||
| 			args: args{ | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					Groups:    Groups{"group:foo": []string{"user1", "user2"}}, | ||||
| 					TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}}, | ||||
| 				}, | ||||
| 				tag: "tag:test", | ||||
| 			}, | ||||
| 			want:    []string{"user1", "user2", "user3"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "invalid tag", | ||||
| 			args: args{ | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}}, | ||||
| 				}, | ||||
| 				tag: "tag:test", | ||||
| 			}, | ||||
| 			want:    []string{}, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "invalid group", | ||||
| 			args: args{ | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					Groups:    Groups{"group:bar": []string{"user1", "user2"}}, | ||||
| 					TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}}, | ||||
| 				}, | ||||
| 				tag: "tag:test", | ||||
| 			}, | ||||
| 			want:    []string{}, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			got, err := expandTagOwners(test.args.aclPolicy, test.args.tag) | ||||
| 			if (err != nil) != test.wantErr { | ||||
| 				t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 			if !reflect.DeepEqual(got, test.want) { | ||||
| 				t.Errorf("expandTagOwners() = %v, want %v", got, test.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Test_expandPorts(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		portsStr string | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name    string | ||||
| 		args    args | ||||
| 		want    *[]tailcfg.PortRange | ||||
| 		wantErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "wildcard", | ||||
| 			args: args{portsStr: "*"}, | ||||
| 			want: &[]tailcfg.PortRange{ | ||||
| 				{First: portRangeBegin, Last: portRangeEnd}, | ||||
| 			}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "two ports", | ||||
| 			args: args{portsStr: "80,443"}, | ||||
| 			want: &[]tailcfg.PortRange{ | ||||
| 				{First: 80, Last: 80}, | ||||
| 				{First: 443, Last: 443}, | ||||
| 			}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "a range and a port", | ||||
| 			args: args{portsStr: "80-1024,443"}, | ||||
| 			want: &[]tailcfg.PortRange{ | ||||
| 				{First: 80, Last: 1024}, | ||||
| 				{First: 443, Last: 443}, | ||||
| 			}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:    "out of bounds", | ||||
| 			args:    args{portsStr: "854038"}, | ||||
| 			want:    nil, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:    "wrong port", | ||||
| 			args:    args{portsStr: "85a38"}, | ||||
| 			want:    nil, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:    "wrong port in first", | ||||
| 			args:    args{portsStr: "a-80"}, | ||||
| 			want:    nil, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:    "wrong port in last", | ||||
| 			args:    args{portsStr: "80-85a38"}, | ||||
| 			want:    nil, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:    "wrong port format", | ||||
| 			args:    args{portsStr: "80-85a38-3"}, | ||||
| 			want:    nil, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			got, err := expandPorts(test.args.portsStr) | ||||
| 			if (err != nil) != test.wantErr { | ||||
| 				t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 			if !reflect.DeepEqual(got, test.want) { | ||||
| 				t.Errorf("expandPorts() = %v, want %v", got, test.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Test_listMachinesInNamespace(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		machines  []Machine | ||||
| 		namespace string | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name string | ||||
| 		args args | ||||
| 		want []Machine | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "1 machine in namespace", | ||||
| 			args: args{ | ||||
| 				machines: []Machine{ | ||||
| 					{Namespace: Namespace{Name: "joe"}}, | ||||
| 				}, | ||||
| 				namespace: "joe", | ||||
| 			}, | ||||
| 			want: []Machine{ | ||||
| 				{Namespace: Namespace{Name: "joe"}}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "3 machines, 2 in namespace", | ||||
| 			args: args{ | ||||
| 				machines: []Machine{ | ||||
| 					{ID: 1, Namespace: Namespace{Name: "joe"}}, | ||||
| 					{ID: 2, Namespace: Namespace{Name: "marc"}}, | ||||
| 					{ID: 3, Namespace: Namespace{Name: "marc"}}, | ||||
| 				}, | ||||
| 				namespace: "marc", | ||||
| 			}, | ||||
| 			want: []Machine{ | ||||
| 				{ID: 2, Namespace: Namespace{Name: "marc"}}, | ||||
| 				{ID: 3, Namespace: Namespace{Name: "marc"}}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "5 machines, 0 in namespace", | ||||
| 			args: args{ | ||||
| 				machines: []Machine{ | ||||
| 					{ID: 1, Namespace: Namespace{Name: "joe"}}, | ||||
| 					{ID: 2, Namespace: Namespace{Name: "marc"}}, | ||||
| 					{ID: 3, Namespace: Namespace{Name: "marc"}}, | ||||
| 					{ID: 4, Namespace: Namespace{Name: "marc"}}, | ||||
| 					{ID: 5, Namespace: Namespace{Name: "marc"}}, | ||||
| 				}, | ||||
| 				namespace: "mickael", | ||||
| 			}, | ||||
| 			want: []Machine{}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			if got := filterMachinesByNamespace(test.args.machines, test.args.namespace); !reflect.DeepEqual( | ||||
| 				got, | ||||
| 				test.want, | ||||
| 			) { | ||||
| 				t.Errorf("listMachinesInNamespace() = %v, want %v", got, test.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // nolint
 | ||||
| func Test_expandAlias(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		machines  []Machine | ||||
| 		aclPolicy ACLPolicy | ||||
| 		alias     string | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name    string | ||||
| 		args    args | ||||
| 		want    []string | ||||
| 		wantErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "wildcard", | ||||
| 			args: args{ | ||||
| 				alias: "*", | ||||
| 				machines: []Machine{ | ||||
| 					{IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.78.84.227"), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				aclPolicy: ACLPolicy{}, | ||||
| 			}, | ||||
| 			want:    []string{"*"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "simple group", | ||||
| 			args: args{ | ||||
| 				alias: "group:accountant", | ||||
| 				machines: []Machine{ | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.3"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "marc"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.4"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "mickael"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					Groups: Groups{"group:accountant": []string{"joe", "marc"}}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "wrong group", | ||||
| 			args: args{ | ||||
| 				alias: "group:hr", | ||||
| 				machines: []Machine{ | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.3"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "marc"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.4"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "mickael"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					Groups: Groups{"group:accountant": []string{"joe", "marc"}}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    []string{}, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "simple ipaddress", | ||||
| 			args: args{ | ||||
| 				alias:     "10.0.0.3", | ||||
| 				machines:  []Machine{}, | ||||
| 				aclPolicy: ACLPolicy{}, | ||||
| 			}, | ||||
| 			want:    []string{"10.0.0.3"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "private network", | ||||
| 			args: args{ | ||||
| 				alias:    "homeNetwork", | ||||
| 				machines: []Machine{}, | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					Hosts: Hosts{ | ||||
| 						"homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    []string{"192.168.1.0/24"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "simple host", | ||||
| 			args: args{ | ||||
| 				alias:     "10.0.0.1", | ||||
| 				machines:  []Machine{}, | ||||
| 				aclPolicy: ACLPolicy{}, | ||||
| 			}, | ||||
| 			want:    []string{"10.0.0.1"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "simple CIDR", | ||||
| 			args: args{ | ||||
| 				alias:     "10.0.0.0/16", | ||||
| 				machines:  []Machine{}, | ||||
| 				aclPolicy: ACLPolicy{}, | ||||
| 			}, | ||||
| 			want:    []string{"10.0.0.0/16"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "simple tag", | ||||
| 			args: args{ | ||||
| 				alias: "tag:hr-webserver", | ||||
| 				machines: []Machine{ | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 						HostInfo: []byte( | ||||
| 							"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}", | ||||
| 						), | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 						HostInfo: []byte( | ||||
| 							"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}", | ||||
| 						), | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.3"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "marc"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.4"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    []string{"100.64.0.1", "100.64.0.2"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "No tag defined", | ||||
| 			args: args{ | ||||
| 				alias: "tag:hr-webserver", | ||||
| 				machines: []Machine{ | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.3"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "marc"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.4"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "mickael"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					Groups: Groups{"group:accountant": []string{"joe", "marc"}}, | ||||
| 					TagOwners: TagOwners{ | ||||
| 						"tag:accountant-webserver": []string{"group:accountant"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    []string{}, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "list host in namespace without correctly tagged servers", | ||||
| 			args: args{ | ||||
| 				alias: "joe", | ||||
| 				machines: []Machine{ | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 						HostInfo: []byte( | ||||
| 							"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}", | ||||
| 						), | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 						HostInfo: []byte( | ||||
| 							"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}", | ||||
| 						), | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.3"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "marc"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.4"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    []string{"100.64.0.4"}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			got, err := expandAlias( | ||||
| 				test.args.machines, | ||||
| 				test.args.aclPolicy, | ||||
| 				test.args.alias, | ||||
| 			) | ||||
| 			if (err != nil) != test.wantErr { | ||||
| 				t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 			if !reflect.DeepEqual(got, test.want) { | ||||
| 				t.Errorf("expandAlias() = %v, want %v", got, test.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Test_excludeCorrectlyTaggedNodes(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		aclPolicy ACLPolicy | ||||
| 		nodes     []Machine | ||||
| 		namespace string | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name    string | ||||
| 		args    args | ||||
| 		want    []Machine | ||||
| 		wantErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "exclude nodes with valid tags", | ||||
| 			args: args{ | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, | ||||
| 				}, | ||||
| 				nodes: []Machine{ | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 						HostInfo: []byte( | ||||
| 							"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}", | ||||
| 						), | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 						HostInfo: []byte( | ||||
| 							"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}", | ||||
| 						), | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.4"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				namespace: "joe", | ||||
| 			}, | ||||
| 			want: []Machine{ | ||||
| 				{ | ||||
| 					IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")}, | ||||
| 					Namespace:   Namespace{Name: "joe"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "all nodes have invalid tags, don't exclude them", | ||||
| 			args: args{ | ||||
| 				aclPolicy: ACLPolicy{ | ||||
| 					TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, | ||||
| 				}, | ||||
| 				nodes: []Machine{ | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 						HostInfo: []byte( | ||||
| 							"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}", | ||||
| 						), | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 						HostInfo: []byte( | ||||
| 							"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}", | ||||
| 						), | ||||
| 					}, | ||||
| 					{ | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.4"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				namespace: "joe", | ||||
| 			}, | ||||
| 			want: []Machine{ | ||||
| 				{ | ||||
| 					IPAddresses: MachineAddresses{ | ||||
| 						netaddr.MustParseIP("100.64.0.1"), | ||||
| 					}, | ||||
| 					Namespace: Namespace{Name: "joe"}, | ||||
| 					HostInfo: []byte( | ||||
| 						"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}", | ||||
| 					), | ||||
| 				}, | ||||
| 				{ | ||||
| 					IPAddresses: MachineAddresses{ | ||||
| 						netaddr.MustParseIP("100.64.0.2"), | ||||
| 					}, | ||||
| 					Namespace: Namespace{Name: "joe"}, | ||||
| 					HostInfo: []byte( | ||||
| 						"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}", | ||||
| 					), | ||||
| 				}, | ||||
| 				{ | ||||
| 					IPAddresses: MachineAddresses{ | ||||
| 						netaddr.MustParseIP("100.64.0.4"), | ||||
| 					}, | ||||
| 					Namespace: Namespace{Name: "joe"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			got, err := excludeCorrectlyTaggedNodes( | ||||
| 				test.args.aclPolicy, | ||||
| 				test.args.nodes, | ||||
| 				test.args.namespace, | ||||
| 			) | ||||
| 			if (err != nil) != test.wantErr { | ||||
| 				t.Errorf( | ||||
| 					"excludeCorrectlyTaggedNodes() error = %v, wantErr %v", | ||||
| 					err, | ||||
| 					test.wantErr, | ||||
| 				) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 			if !reflect.DeepEqual(got, test.want) { | ||||
| 				t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										22
									
								
								api.go
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								api.go
									
									
									
									
									
								
							| @ -261,7 +261,16 @@ func (h *Headscale) getMapResponse( | ||||
| 
 | ||||
| 	var respBody []byte | ||||
| 	if req.Compress == "zstd" { | ||||
| 		src, _ := json.Marshal(resp) | ||||
| 		src, err := json.Marshal(resp) | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Caller(). | ||||
| 				Str("func", "getMapResponse"). | ||||
| 				Err(err). | ||||
| 				Msg("Failed to marshal response for the client") | ||||
| 
 | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		encoder, _ := zstd.NewWriter(nil) | ||||
| 		srcCompressed := encoder.EncodeAll(src, nil) | ||||
| @ -290,7 +299,16 @@ func (h *Headscale) getMapKeepAliveResponse( | ||||
| 	var respBody []byte | ||||
| 	var err error | ||||
| 	if mapRequest.Compress == "zstd" { | ||||
| 		src, _ := json.Marshal(mapResponse) | ||||
| 		src, err := json.Marshal(mapResponse) | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Caller(). | ||||
| 				Str("func", "getMapKeepAliveResponse"). | ||||
| 				Err(err). | ||||
| 				Msg("Failed to marshal keepalive response for the client") | ||||
| 
 | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		encoder, _ := zstd.NewWriter(nil) | ||||
| 		srcCompressed := encoder.EncodeAll(src, nil) | ||||
| 		respBody = h.privateKey.SealTo(machineKey, srcCompressed) | ||||
|  | ||||
							
								
								
									
										25
									
								
								db.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								db.go
									
									
									
									
									
								
							| @ -2,9 +2,10 @@ package headscale | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/glebarez/sqlite" | ||||
| 	"gorm.io/driver/postgres" | ||||
| 	"gorm.io/driver/sqlite" | ||||
| 	"gorm.io/gorm" | ||||
| 	"gorm.io/gorm/logger" | ||||
| ) | ||||
| @ -81,10 +82,24 @@ func (h *Headscale) openDB() (*gorm.DB, error) { | ||||
| 
 | ||||
| 	switch h.dbType { | ||||
| 	case Sqlite: | ||||
| 		db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{ | ||||
| 			DisableForeignKeyConstraintWhenMigrating: true, | ||||
| 			Logger:                                   log, | ||||
| 		}) | ||||
| 		db, err = gorm.Open( | ||||
| 			sqlite.Open(h.dbString+"?_synchronous=1&_journal_mode=WAL"), | ||||
| 			&gorm.Config{ | ||||
| 				DisableForeignKeyConstraintWhenMigrating: true, | ||||
| 				Logger:                                   log, | ||||
| 			}, | ||||
| 		) | ||||
| 
 | ||||
| 		db.Exec("PRAGMA foreign_keys=ON") | ||||
| 
 | ||||
| 		// The pure Go SQLite library does not handle locking in
 | ||||
| 		// the same way as the C based one and we cant use the gorm
 | ||||
| 		// connection pool as of 2022/02/23.
 | ||||
| 		sqlDB, _ := db.DB() | ||||
| 		sqlDB.SetMaxIdleConns(1) | ||||
| 		sqlDB.SetMaxOpenConns(1) | ||||
| 		sqlDB.SetConnMaxIdleTime(time.Hour) | ||||
| 
 | ||||
| 	case Postgres: | ||||
| 		db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{ | ||||
| 			DisableForeignKeyConstraintWhenMigrating: true, | ||||
|  | ||||
							
								
								
									
										20
									
								
								dns.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								dns.go
									
									
									
									
									
								
							| @ -163,7 +163,15 @@ func getMapResponseDNSConfig( | ||||
| 		dnsConfig = dnsConfigOrig.Clone() | ||||
| 		dnsConfig.Domains = append( | ||||
| 			dnsConfig.Domains, | ||||
| 			fmt.Sprintf("%s.%s", machine.Namespace.Name, baseDomain), | ||||
| 			fmt.Sprintf( | ||||
| 				"%s.%s", | ||||
| 				strings.ReplaceAll( | ||||
| 					machine.Namespace.Name, | ||||
| 					"@", | ||||
| 					".", | ||||
| 				), // Replace @ with . for valid domain for machine
 | ||||
| 				baseDomain, | ||||
| 			), | ||||
| 		) | ||||
| 
 | ||||
| 		namespaceSet := set.New(set.ThreadSafe) | ||||
| @ -171,8 +179,14 @@ func getMapResponseDNSConfig( | ||||
| 		for _, p := range peers { | ||||
| 			namespaceSet.Add(p.Namespace) | ||||
| 		} | ||||
| 		for _, namespace := range namespaceSet.List() { | ||||
| 			dnsRoute := fmt.Sprintf("%s.%s", namespace.(Namespace).Name, baseDomain) | ||||
| 		for _, ns := range namespaceSet.List() { | ||||
| 			namespace, ok := ns.(Namespace) | ||||
| 			if !ok { | ||||
| 				dnsConfig = dnsConfigOrig | ||||
| 
 | ||||
| 				continue | ||||
| 			} | ||||
| 			dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain) | ||||
| 			dnsConfig.Routes[dnsRoute] = nil | ||||
| 		} | ||||
| 	} else { | ||||
|  | ||||
| @ -39,6 +39,14 @@ use namespaces (which are the equivalent to user/logins in Tailscale.com). | ||||
| 
 | ||||
| Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples. | ||||
| 
 | ||||
| When using ACL's the Namespace borders are no longer applied. All machines | ||||
| whichever the Namespace have the ability to communicate with other hosts as | ||||
| long as the ACL's permits this exchange. | ||||
| 
 | ||||
| The [ACLs](acls.md) document should help understand a fictional case of setting | ||||
| up ACLs in a small company. All concepts presented in this document could be | ||||
| applied outside of business oriented usage. | ||||
| 
 | ||||
| ### Apple devices | ||||
| 
 | ||||
| An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance. | ||||
|  | ||||
							
								
								
									
										141
									
								
								docs/acls.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								docs/acls.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,141 @@ | ||||
| # ACLs use case example | ||||
| 
 | ||||
| Let's build an example use case for a small business (It may be the place where | ||||
| ACL's are the most useful). | ||||
| 
 | ||||
| We have a small company with a boss, an admin, two developers and an intern. | ||||
| 
 | ||||
| The boss should have access to all servers but not to the users hosts. Admin | ||||
| should also have access to all hosts except that their permissions should be | ||||
| limited to maintaining the hosts (for example purposes). The developers can do | ||||
| anything they want on dev hosts, but only watch on productions hosts. Intern | ||||
| can only interact with the development servers. | ||||
| 
 | ||||
| Each user have at least a device connected to the network and we have some | ||||
| servers. | ||||
| 
 | ||||
| - database.prod | ||||
| - database.dev | ||||
| - app-server1.prod | ||||
| - app-server1.dev | ||||
| - billing.internal | ||||
| 
 | ||||
| ## Setup of the network | ||||
| 
 | ||||
| Let's create the namespaces. Each user should have his own namespace. The users | ||||
| here are represented as namespaces. | ||||
| 
 | ||||
| ```bash | ||||
| headscale namespaces create boss | ||||
| headscale namespaces create admin1 | ||||
| headscale namespaces create dev1 | ||||
| headscale namespaces create dev2 | ||||
| headscale namespaces create intern1 | ||||
| ``` | ||||
| 
 | ||||
| We don't need to create namespaces for the servers because the servers will be | ||||
| tagged. When registering the servers we will need to add the flag | ||||
| `--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is | ||||
| registering the server should be allowed to do it. Since anyone can add tags to | ||||
| a server they can register, the check of the tags is done on headscale server | ||||
| and only valid tags are applied. A tag is valid if the namespace that is | ||||
| registering it is allowed to do it. | ||||
| 
 | ||||
| Here are the ACL's to implement the same permissions as above: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   // groups are collections of users having a common scope. A user can be in multiple groups | ||||
|   // groups cannot be composed of groups | ||||
|   "groups": { | ||||
|     "group:boss": ["boss"], | ||||
|     "group:dev": ["dev1", "dev2"], | ||||
|     "group:admin": ["admin1"], | ||||
|     "group:intern": ["intern1"] | ||||
|   }, | ||||
|   // tagOwners in tailscale is an association between a TAG and the people allowed to set this TAG on a server. | ||||
|   // This is documented [here](https://tailscale.com/kb/1068/acl-tags#defining-a-tag) | ||||
|   // and explained [here](https://tailscale.com/blog/rbac-like-it-was-meant-to-be/) | ||||
|   "tagOwners": { | ||||
|     // the administrators can add servers in production | ||||
|     "tag:prod-databases": ["group:admin"], | ||||
|     "tag:prod-app-servers": ["group:admin"], | ||||
| 
 | ||||
|     // the boss can tag any server as internal | ||||
|     "tag:internal": ["group:boss"], | ||||
| 
 | ||||
|     // dev can add servers for dev purposes as well as admins | ||||
|     "tag:dev-databases": ["group:admin", "group:dev"], | ||||
|     "tag:dev-app-servers": ["group:admin", "group:dev"] | ||||
| 
 | ||||
|     // interns cannot add servers | ||||
|   }, | ||||
|   "acls": [ | ||||
|     // boss have access to all servers | ||||
|     { | ||||
|       "action": "accept", | ||||
|       "users": ["group:boss"], | ||||
|       "ports": [ | ||||
|         "tag:prod-databases:*", | ||||
|         "tag:prod-app-servers:*", | ||||
|         "tag:internal:*", | ||||
|         "tag:dev-databases:*", | ||||
|         "tag:dev-app-servers:*" | ||||
|       ] | ||||
|     }, | ||||
| 
 | ||||
|     // admin have only access to administrative ports of the servers | ||||
|     { | ||||
|       "action": "accept", | ||||
|       "users": ["group:admin"], | ||||
|       "ports": [ | ||||
|         "tag:prod-databases:22", | ||||
|         "tag:prod-app-servers:22", | ||||
|         "tag:internal:22", | ||||
|         "tag:dev-databases:22", | ||||
|         "tag:dev-app-servers:22" | ||||
|       ] | ||||
|     }, | ||||
| 
 | ||||
|     // developers have access to databases servers and application servers on all ports | ||||
|     // they can only view the applications servers in prod and have no access to databases servers in production | ||||
|     { | ||||
|       "action": "accept", | ||||
|       "users": ["group:dev"], | ||||
|       "ports": [ | ||||
|         "tag:dev-databases:*", | ||||
|         "tag:dev-app-servers:*", | ||||
|         "tag:prod-app-servers:80,443" | ||||
|       ] | ||||
|     }, | ||||
| 
 | ||||
|     // servers should be able to talk to database. Database should not be able to initiate connections to | ||||
|     // applications servers | ||||
|     { | ||||
|       "action": "accept", | ||||
|       "users": ["tag:dev-app-servers"], | ||||
|       "ports": ["tag:dev-databases:5432"] | ||||
|     }, | ||||
|     { | ||||
|       "action": "accept", | ||||
|       "users": ["tag:prod-app-servers"], | ||||
|       "ports": ["tag:prod-databases:5432"] | ||||
|     }, | ||||
| 
 | ||||
|     // interns have access to dev-app-servers only in reading mode | ||||
|     { | ||||
|       "action": "accept", | ||||
|       "users": ["group:intern"], | ||||
|       "ports": ["tag:dev-app-servers:80,443"] | ||||
|     }, | ||||
| 
 | ||||
|     // We still have to allow internal namespaces communications since nothing guarantees that each user have | ||||
|     // their own namespaces. | ||||
|     { "action": "accept", "users": ["boss"], "ports": ["boss:*"] }, | ||||
|     { "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] }, | ||||
|     { "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] }, | ||||
|     { "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] }, | ||||
|     { "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] } | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
							
								
								
									
										21
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								go.mod
									
									
									
									
									
								
							| @ -8,6 +8,7 @@ require ( | ||||
| 	github.com/efekarakus/termcolor v1.0.1 | ||||
| 	github.com/fatih/set v0.2.1 | ||||
| 	github.com/gin-gonic/gin v1.7.7 | ||||
| 	github.com/glebarez/sqlite v1.3.5 | ||||
| 	github.com/gofrs/uuid v4.2.0+incompatible | ||||
| 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 | ||||
| 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 | ||||
| @ -19,14 +20,13 @@ require ( | ||||
| 	github.com/prometheus/client_golang v1.12.1 | ||||
| 	github.com/pterm/pterm v0.12.36 | ||||
| 	github.com/rs/zerolog v1.26.1 | ||||
| 	github.com/soheilhy/cmux v0.1.5 | ||||
| 	github.com/spf13/cobra v1.3.0 | ||||
| 	github.com/spf13/viper v1.10.1 | ||||
| 	github.com/stretchr/testify v1.7.0 | ||||
| 	github.com/tailscale/hujson v0.0.0-20211215203138-ffd971c5f362 | ||||
| 	github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e | ||||
| 	github.com/zsais/go-gin-prometheus v0.1.0 | ||||
| 	golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 | ||||
| 	golang.org/x/crypto v0.0.0-20220214200702-86341886e292 | ||||
| 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 | ||||
| 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c | ||||
| 	google.golang.org/genproto v0.0.0-20220210181026-6fee9acbd336 | ||||
| @ -36,9 +36,8 @@ require ( | ||||
| 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c | ||||
| 	gopkg.in/yaml.v2 v2.4.0 | ||||
| 	gorm.io/datatypes v1.0.5 | ||||
| 	gorm.io/driver/postgres v1.2.3 | ||||
| 	gorm.io/driver/sqlite v1.2.6 | ||||
| 	gorm.io/gorm v1.22.5 | ||||
| 	gorm.io/driver/postgres v1.3.1 | ||||
| 	gorm.io/gorm v1.23.1 | ||||
| 	inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 | ||||
| 	tailscale.com v1.20.4 | ||||
| ) | ||||
| @ -58,8 +57,8 @@ require ( | ||||
| 	github.com/docker/go-connections v0.4.0 // indirect | ||||
| 	github.com/docker/go-units v0.4.0 // indirect | ||||
| 	github.com/fsnotify/fsnotify v1.5.1 // indirect | ||||
| 	github.com/ghodss/yaml v1.0.0 // indirect | ||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||
| 	github.com/glebarez/go-sqlite v1.14.7 // indirect | ||||
| 	github.com/go-playground/locales v0.14.0 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.0 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.10.0 // indirect | ||||
| @ -70,6 +69,7 @@ require ( | ||||
| 	github.com/google/go-github v17.0.0+incompatible // indirect | ||||
| 	github.com/google/go-querystring v1.1.0 // indirect | ||||
| 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect | ||||
| 	github.com/google/uuid v1.3.0 // indirect | ||||
| 	github.com/gookit/color v1.5.0 // indirect | ||||
| 	github.com/hashicorp/go-version v1.4.0 // indirect | ||||
| 	github.com/hashicorp/hcl v1.0.0 // indirect | ||||
| @ -112,6 +112,7 @@ require ( | ||||
| 	github.com/prometheus/client_model v0.2.0 // indirect | ||||
| 	github.com/prometheus/common v0.32.1 // indirect | ||||
| 	github.com/prometheus/procfs v0.7.3 // indirect | ||||
| 	github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect | ||||
| 	github.com/rivo/uniseg v0.2.0 // indirect | ||||
| 	github.com/rogpeppe/go-internal v1.8.1 // indirect | ||||
| 	github.com/sirupsen/logrus v1.8.1 // indirect | ||||
| @ -136,6 +137,12 @@ require ( | ||||
| 	gopkg.in/ini.v1 v1.66.4 // indirect | ||||
| 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect | ||||
| 	gorm.io/driver/mysql v1.2.3 // indirect | ||||
| 	gorm.io/driver/mysql v1.3.2 // indirect | ||||
| 	gorm.io/driver/sqlite v1.3.1 // indirect | ||||
| 	gorm.io/driver/sqlserver v1.3.1 // indirect | ||||
| 	modernc.org/libc v1.14.3 // indirect | ||||
| 	modernc.org/mathutil v1.4.1 // indirect | ||||
| 	modernc.org/memory v1.0.5 // indirect | ||||
| 	modernc.org/sqlite v1.14.5 // indirect | ||||
| 	sigs.k8s.io/yaml v1.3.0 // indirect | ||||
| ) | ||||
|  | ||||
							
								
								
									
										178
									
								
								machine.go
									
									
									
									
									
								
							
							
						
						
									
										178
									
								
								machine.go
									
									
									
									
									
								
							| @ -119,6 +119,103 @@ func (machine Machine) isExpired() bool { | ||||
| 	return time.Now().UTC().After(*machine.Expiry) | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) ListAllMachines() ([]Machine, error) { | ||||
| 	machines := []Machine{} | ||||
| 	if err := h.db.Preload("AuthKey"). | ||||
| 		Preload("AuthKey.Namespace"). | ||||
| 		Preload("Namespace"). | ||||
| 		Where("registered"). | ||||
| 		Find(&machines).Error; err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return machines, nil | ||||
| } | ||||
| 
 | ||||
| func containsAddresses(inputs []string, addrs []string) bool { | ||||
| 	for _, addr := range addrs { | ||||
| 		if containsString(inputs, addr) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // matchSourceAndDestinationWithRule.
 | ||||
| func matchSourceAndDestinationWithRule( | ||||
| 	ruleSources []string, | ||||
| 	ruleDestinations []string, | ||||
| 	source []string, | ||||
| 	destination []string, | ||||
| ) bool { | ||||
| 	return containsAddresses(ruleSources, source) && | ||||
| 		containsAddresses(ruleDestinations, destination) | ||||
| } | ||||
| 
 | ||||
| // getFilteredByACLPeerss should return the list of peers authorized to be accessed from machine.
 | ||||
| func getFilteredByACLPeers( | ||||
| 	machines []Machine, | ||||
| 	rules []tailcfg.FilterRule, | ||||
| 	machine *Machine, | ||||
| ) Machines { | ||||
| 	log.Trace(). | ||||
| 		Caller(). | ||||
| 		Str("machine", machine.Name). | ||||
| 		Msg("Finding peers filtered by ACLs") | ||||
| 
 | ||||
| 	peers := make(map[uint64]Machine) | ||||
| 	// Aclfilter peers here. We are itering through machines in all namespaces and search through the computed aclRules
 | ||||
| 	// for match between rule SrcIPs and DstPorts. If the rule is a match we allow the machine to be viewable.
 | ||||
| 	for _, peer := range machines { | ||||
| 		if peer.ID == machine.ID { | ||||
| 			continue | ||||
| 		} | ||||
| 		for _, rule := range rules { | ||||
| 			var dst []string | ||||
| 			for _, d := range rule.DstPorts { | ||||
| 				dst = append(dst, d.IP) | ||||
| 			} | ||||
| 			if matchSourceAndDestinationWithRule( | ||||
| 				rule.SrcIPs, | ||||
| 				dst, | ||||
| 				machine.IPAddresses.ToStringSlice(), | ||||
| 				peer.IPAddresses.ToStringSlice(), | ||||
| 			) || // match source and destination
 | ||||
| 				matchSourceAndDestinationWithRule( | ||||
| 					rule.SrcIPs, | ||||
| 					dst, | ||||
| 					machine.IPAddresses.ToStringSlice(), | ||||
| 					[]string{"*"}, | ||||
| 				) || // match source and all destination
 | ||||
| 				matchSourceAndDestinationWithRule( | ||||
| 					rule.SrcIPs, | ||||
| 					dst, | ||||
| 					peer.IPAddresses.ToStringSlice(), | ||||
| 					machine.IPAddresses.ToStringSlice(), | ||||
| 				) { // match return path
 | ||||
| 				peers[peer.ID] = peer | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	authorizedPeers := make([]Machine, 0, len(peers)) | ||||
| 	for _, m := range peers { | ||||
| 		authorizedPeers = append(authorizedPeers, m) | ||||
| 	} | ||||
| 	sort.Slice( | ||||
| 		authorizedPeers, | ||||
| 		func(i, j int) bool { return authorizedPeers[i].ID < authorizedPeers[j].ID }, | ||||
| 	) | ||||
| 
 | ||||
| 	log.Trace(). | ||||
| 		Caller(). | ||||
| 		Str("machine", machine.Name). | ||||
| 		Msgf("Found some machines: %v", machines) | ||||
| 
 | ||||
| 	return authorizedPeers | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) { | ||||
| 	log.Trace(). | ||||
| 		Caller(). | ||||
| @ -206,39 +303,54 @@ func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) { | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) getPeers(machine *Machine) (Machines, error) { | ||||
| 	direct, err := h.getDirectPeers(machine) | ||||
| 	if err != nil { | ||||
| 		log.Error(). | ||||
| 			Caller(). | ||||
| 			Err(err). | ||||
| 			Msg("Cannot fetch peers") | ||||
| 	var peers Machines | ||||
| 	var err error | ||||
| 
 | ||||
| 		return Machines{}, err | ||||
| 	// If ACLs rules are defined, filter visible host list with the ACLs
 | ||||
| 	// else use the classic namespace scope
 | ||||
| 	if h.aclPolicy != nil { | ||||
| 		var machines []Machine | ||||
| 		machines, err = h.ListAllMachines() | ||||
| 		if err != nil { | ||||
| 			log.Error().Err(err).Msg("Error retrieving list of machines") | ||||
| 
 | ||||
| 			return Machines{}, err | ||||
| 		} | ||||
| 		peers = getFilteredByACLPeers(machines, h.aclRules, machine) | ||||
| 	} else { | ||||
| 		direct, err := h.getDirectPeers(machine) | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Caller(). | ||||
| 				Err(err). | ||||
| 				Msg("Cannot fetch peers") | ||||
| 
 | ||||
| 			return Machines{}, err | ||||
| 		} | ||||
| 
 | ||||
| 		shared, err := h.getShared(machine) | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Caller(). | ||||
| 				Err(err). | ||||
| 				Msg("Cannot fetch peers") | ||||
| 
 | ||||
| 			return Machines{}, err | ||||
| 		} | ||||
| 
 | ||||
| 		sharedTo, err := h.getSharedTo(machine) | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Caller(). | ||||
| 				Err(err). | ||||
| 				Msg("Cannot fetch peers") | ||||
| 
 | ||||
| 			return Machines{}, err | ||||
| 		} | ||||
| 		peers = append(direct, shared...) | ||||
| 		peers = append(peers, sharedTo...) | ||||
| 	} | ||||
| 
 | ||||
| 	shared, err := h.getShared(machine) | ||||
| 	if err != nil { | ||||
| 		log.Error(). | ||||
| 			Caller(). | ||||
| 			Err(err). | ||||
| 			Msg("Cannot fetch peers") | ||||
| 
 | ||||
| 		return Machines{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	sharedTo, err := h.getSharedTo(machine) | ||||
| 	if err != nil { | ||||
| 		log.Error(). | ||||
| 			Caller(). | ||||
| 			Err(err). | ||||
| 			Msg("Cannot fetch peers") | ||||
| 
 | ||||
| 		return Machines{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	peers := append(direct, shared...) | ||||
| 	peers = append(peers, sharedTo...) | ||||
| 
 | ||||
| 	sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID }) | ||||
| 
 | ||||
| 	log.Trace(). | ||||
| @ -597,7 +709,11 @@ func (machine Machine) toNode( | ||||
| 		hostname = fmt.Sprintf( | ||||
| 			"%s.%s.%s", | ||||
| 			machine.Name, | ||||
| 			machine.Namespace.Name, | ||||
| 			strings.ReplaceAll( | ||||
| 				machine.Namespace.Name, | ||||
| 				"@", | ||||
| 				".", | ||||
| 			), // Replace @ with . for valid domain for machine
 | ||||
| 			baseDomain, | ||||
| 		) | ||||
| 	} else { | ||||
|  | ||||
							
								
								
									
										262
									
								
								machine_test.go
									
									
									
									
									
								
							
							
						
						
									
										262
									
								
								machine_test.go
									
									
									
									
									
								
							| @ -1,11 +1,15 @@ | ||||
| package headscale | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"gopkg.in/check.v1" | ||||
| 	"inet.af/netaddr" | ||||
| 	"tailscale.com/tailcfg" | ||||
| ) | ||||
| 
 | ||||
| func (s *Suite) TestGetMachine(c *check.C) { | ||||
| @ -154,6 +158,89 @@ func (s *Suite) TestGetDirectPeers(c *check.C) { | ||||
| 	c.Assert(peersOfMachine0[8].Name, check.Equals, "testmachine10") | ||||
| } | ||||
| 
 | ||||
| func (s *Suite) TestGetACLFilteredPeers(c *check.C) { | ||||
| 	type base struct { | ||||
| 		namespace *Namespace | ||||
| 		key       *PreAuthKey | ||||
| 	} | ||||
| 
 | ||||
| 	stor := make([]base, 0) | ||||
| 
 | ||||
| 	for _, name := range []string{"test", "admin"} { | ||||
| 		namespace, err := app.CreateNamespace(name) | ||||
| 		c.Assert(err, check.IsNil) | ||||
| 		pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) | ||||
| 		c.Assert(err, check.IsNil) | ||||
| 		stor = append(stor, base{namespace, pak}) | ||||
| 	} | ||||
| 
 | ||||
| 	_, err := app.GetMachineByID(0) | ||||
| 	c.Assert(err, check.NotNil) | ||||
| 
 | ||||
| 	for index := 0; index <= 10; index++ { | ||||
| 		machine := Machine{ | ||||
| 			ID:         uint64(index), | ||||
| 			MachineKey: "foo" + strconv.Itoa(index), | ||||
| 			NodeKey:    "bar" + strconv.Itoa(index), | ||||
| 			DiscoKey:   "faa" + strconv.Itoa(index), | ||||
| 			IPAddresses: MachineAddresses{ | ||||
| 				netaddr.MustParseIP(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))), | ||||
| 			}, | ||||
| 			Name:           "testmachine" + strconv.Itoa(index), | ||||
| 			NamespaceID:    stor[index%2].namespace.ID, | ||||
| 			Registered:     true, | ||||
| 			RegisterMethod: RegisterMethodAuthKey, | ||||
| 			AuthKeyID:      uint(stor[index%2].key.ID), | ||||
| 		} | ||||
| 		app.db.Save(&machine) | ||||
| 	} | ||||
| 
 | ||||
| 	app.aclPolicy = &ACLPolicy{ | ||||
| 		Groups: map[string][]string{ | ||||
| 			"group:test": {"admin"}, | ||||
| 		}, | ||||
| 		Hosts:     map[string]netaddr.IPPrefix{}, | ||||
| 		TagOwners: map[string][]string{}, | ||||
| 		ACLs: []ACL{ | ||||
| 			{Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}}, | ||||
| 			{Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}}, | ||||
| 		}, | ||||
| 		Tests: []ACLTest{}, | ||||
| 	} | ||||
| 
 | ||||
| 	err = app.UpdateACLRules() | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	adminMachine, err := app.GetMachineByID(1) | ||||
| 	c.Logf("Machine(%v), namespace: %v", adminMachine.Name, adminMachine.Namespace) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	testMachine, err := app.GetMachineByID(2) | ||||
| 	c.Logf("Machine(%v), namespace: %v", testMachine.Name, testMachine.Namespace) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	_, err = testMachine.GetHostInfo() | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	machines, err := app.ListAllMachines() | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	peersOfTestMachine := getFilteredByACLPeers(machines, app.aclRules, testMachine) | ||||
| 	peersOfAdminMachine := getFilteredByACLPeers(machines, app.aclRules, adminMachine) | ||||
| 
 | ||||
| 	c.Log(peersOfTestMachine) | ||||
| 	c.Assert(len(peersOfTestMachine), check.Equals, 4) | ||||
| 	c.Assert(peersOfTestMachine[0].Name, check.Equals, "testmachine4") | ||||
| 	c.Assert(peersOfTestMachine[1].Name, check.Equals, "testmachine6") | ||||
| 	c.Assert(peersOfTestMachine[3].Name, check.Equals, "testmachine10") | ||||
| 
 | ||||
| 	c.Log(peersOfAdminMachine) | ||||
| 	c.Assert(len(peersOfAdminMachine), check.Equals, 9) | ||||
| 	c.Assert(peersOfAdminMachine[0].Name, check.Equals, "testmachine2") | ||||
| 	c.Assert(peersOfAdminMachine[2].Name, check.Equals, "testmachine4") | ||||
| 	c.Assert(peersOfAdminMachine[5].Name, check.Equals, "testmachine7") | ||||
| } | ||||
| 
 | ||||
| func (s *Suite) TestExpireMachine(c *check.C) { | ||||
| 	namespace, err := app.CreateNamespace("test") | ||||
| 	c.Assert(err, check.IsNil) | ||||
| @ -208,3 +295,178 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) { | ||||
| 		c.Assert(deserialized[i], check.Equals, input[i]) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Test_getFilteredByACLPeers(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		machines []Machine | ||||
| 		rules    []tailcfg.FilterRule | ||||
| 		machine  *Machine | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name string | ||||
| 		args args | ||||
| 		want Machines | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "all hosts can talk to each other", | ||||
| 			args: args{ | ||||
| 				machines: []Machine{ // list of all machines in the database
 | ||||
| 					{ | ||||
| 						ID: 1, | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						ID: 2, | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "marc"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						ID: 3, | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.3"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "mickael"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | ||||
| 					{ | ||||
| 						SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, | ||||
| 						DstPorts: []tailcfg.NetPortRange{ | ||||
| 							{IP: "*"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				machine: &Machine{ // current machine
 | ||||
| 					ID:          1, | ||||
| 					IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, | ||||
| 					Namespace:   Namespace{Name: "joe"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: Machines{ | ||||
| 				{ | ||||
| 					ID:          2, | ||||
| 					IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")}, | ||||
| 					Namespace:   Namespace{Name: "marc"}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ID:          3, | ||||
| 					IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")}, | ||||
| 					Namespace:   Namespace{Name: "mickael"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "One host can talk to another, but not all hosts", | ||||
| 			args: args{ | ||||
| 				machines: []Machine{ // list of all machines in the database
 | ||||
| 					{ | ||||
| 						ID: 1, | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						ID: 2, | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "marc"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						ID: 3, | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.3"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "mickael"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | ||||
| 					{ | ||||
| 						SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, | ||||
| 						DstPorts: []tailcfg.NetPortRange{ | ||||
| 							{IP: "100.64.0.2"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				machine: &Machine{ // current machine
 | ||||
| 					ID:          1, | ||||
| 					IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, | ||||
| 					Namespace:   Namespace{Name: "joe"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: Machines{ | ||||
| 				{ | ||||
| 					ID:          2, | ||||
| 					IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")}, | ||||
| 					Namespace:   Namespace{Name: "marc"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "host cannot directly talk to destination, but return path is authorized", | ||||
| 			args: args{ | ||||
| 				machines: []Machine{ // list of all machines in the database
 | ||||
| 					{ | ||||
| 						ID: 1, | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.1"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "joe"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						ID: 2, | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.2"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "marc"}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						ID: 3, | ||||
| 						IPAddresses: MachineAddresses{ | ||||
| 							netaddr.MustParseIP("100.64.0.3"), | ||||
| 						}, | ||||
| 						Namespace: Namespace{Name: "mickael"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | ||||
| 					{ | ||||
| 						SrcIPs: []string{"100.64.0.3"}, | ||||
| 						DstPorts: []tailcfg.NetPortRange{ | ||||
| 							{IP: "100.64.0.2"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				machine: &Machine{ // current machine
 | ||||
| 					ID:          1, | ||||
| 					IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")}, | ||||
| 					Namespace:   Namespace{Name: "marc"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: Machines{ | ||||
| 				{ | ||||
| 					ID:          3, | ||||
| 					IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")}, | ||||
| 					Namespace:   Namespace{Name: "mickael"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := getFilteredByACLPeers( | ||||
| 				tt.args.machines, | ||||
| 				tt.args.rules, | ||||
| 				tt.args.machine, | ||||
| 			) | ||||
| 			if !reflect.DeepEqual(got, tt.want) { | ||||
| 				t.Errorf("getFilteredByACLPeers() = %v, want %v", got, tt.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										28
									
								
								poll.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								poll.go
									
									
									
									
									
								
							| @ -85,12 +85,26 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { | ||||
| 		Str("machine", machine.Name). | ||||
| 		Msg("Found machine in database") | ||||
| 
 | ||||
| 	hostinfo, _ := json.Marshal(req.Hostinfo) | ||||
| 	hostinfo, err := json.Marshal(req.Hostinfo) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	machine.Name = req.Hostinfo.Hostname | ||||
| 	machine.HostInfo = datatypes.JSON(hostinfo) | ||||
| 	machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey) | ||||
| 	now := time.Now().UTC() | ||||
| 
 | ||||
| 	// update ACLRules with peer informations (to update server tags if necessary)
 | ||||
| 	if h.aclPolicy != nil { | ||||
| 		err = h.UpdateACLRules() | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Caller(). | ||||
| 				Str("func", "handleAuthKey"). | ||||
| 				Str("machine", machine.Name). | ||||
| 				Err(err) | ||||
| 		} | ||||
| 	} | ||||
| 	// From Tailscale client:
 | ||||
| 	//
 | ||||
| 	// ReadOnly is whether the client just wants to fetch the MapResponse,
 | ||||
| @ -100,7 +114,17 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { | ||||
| 	// The intended use is for clients to discover the DERP map at start-up
 | ||||
| 	// before their first real endpoint update.
 | ||||
| 	if !req.ReadOnly { | ||||
| 		endpoints, _ := json.Marshal(req.Endpoints) | ||||
| 		endpoints, err := json.Marshal(req.Endpoints) | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Caller(). | ||||
| 				Str("func", "PollNetMapHandler"). | ||||
| 				Err(err). | ||||
| 				Msg("Failed to mashal requested endpoints for the client") | ||||
| 			ctx.String(http.StatusInternalServerError, ":(") | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 		machine.Endpoints = datatypes.JSON(endpoints) | ||||
| 		machine.LastSeen = &now | ||||
| 	} | ||||
|  | ||||
							
								
								
									
										10
									
								
								utils.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								utils.go
									
									
									
									
									
								
							| @ -212,6 +212,16 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) { | ||||
| 	return ips, nil | ||||
| } | ||||
| 
 | ||||
| func containsString(ss []string, s string) bool { | ||||
| 	for _, v := range ss { | ||||
| 		if v == s { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool { | ||||
| 	for _, v := range ips { | ||||
| 		if v == ip { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user