mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Merge branch 'main' into topic/renovatebot
This commit is contained in:
		
						commit
						bb14bcd4d2
					
				
							
								
								
									
										4
									
								
								.github/workflows/contributors.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/contributors.yml
									
									
									
									
										vendored
									
									
								
							| @ -4,13 +4,13 @@ on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
| 
 | ||||
|   workflow_dispatch: | ||||
| jobs: | ||||
|   add-contributors: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: BobAnkh/add-contributors@master | ||||
|       - uses: BobAnkh/add-contributors@v0.2.2 | ||||
|         with: | ||||
|           CONTRIBUTOR: "## Contributors" | ||||
|           COLUMN_PER_ROW: "6" | ||||
|  | ||||
| @ -48,6 +48,7 @@ linters-settings: | ||||
|       - ip | ||||
|       - ok | ||||
|       - c | ||||
|       - tt | ||||
| 
 | ||||
|   gocritic: | ||||
|     disabled-checks: | ||||
|  | ||||
							
								
								
									
										18
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -2,6 +2,24 @@ | ||||
| 
 | ||||
| **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 | ||||
| 
 | ||||
| **0.13.0 (2022-02-18):** | ||||
| 
 | ||||
| **Features**: | ||||
|  | ||||
							
								
								
									
										181
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										181
									
								
								README.md
									
									
									
									
									
								
							| @ -122,13 +122,6 @@ make build | ||||
| 
 | ||||
| <table> | ||||
| <tr> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/juanfont> | ||||
|             <img src=https://avatars.githubusercontent.com/u/181059?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Juan Font/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Juan Font</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/kradalby> | ||||
|             <img src=https://avatars.githubusercontent.com/u/98431?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kristoffer Dalby/> | ||||
| @ -136,6 +129,13 @@ make build | ||||
|             <sub style="font-size:14px"><b>Kristoffer Dalby</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/juanfont> | ||||
|             <img src=https://avatars.githubusercontent.com/u/181059?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Juan Font/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Juan Font</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/cure> | ||||
|             <img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/> | ||||
| @ -150,6 +150,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/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/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <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/> | ||||
| @ -157,6 +164,15 @@ 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/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Eugen Biegler</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/qbit> | ||||
|             <img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/> | ||||
| @ -164,8 +180,27 @@ make build | ||||
|             <sub style="font-size:14px"><b>Aaron Bieber</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/fdelucchijr> | ||||
|             <img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Fernando De Lucchi</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/hdhoang> | ||||
|             <img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Hoàng Đức Hiếu/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/dragetd> | ||||
|             <img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Michael G.</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <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/> | ||||
| @ -173,6 +208,8 @@ 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/> | ||||
| @ -187,6 +224,20 @@ make build | ||||
|             <sub style="font-size:14px"><b>Silver Bullet</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/majst01> | ||||
|             <img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Stefan Majer</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/lachy-2849> | ||||
|             <img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy-2849/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>lachy-2849</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/t56k> | ||||
|             <img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/> | ||||
| @ -194,6 +245,29 @@ make build | ||||
|             <sub style="font-size:14px"><b>thomas</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <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/> | ||||
|             <br /> | ||||
|             <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/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Artem Klevtsov</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/awoimbee> | ||||
|             <img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/> | ||||
| @ -201,6 +275,13 @@ make build | ||||
|             <sub style="font-size:14px"><b>Arthur Woimbée</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/stensonb> | ||||
|             <img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Bryan Stenson</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/fkr> | ||||
|             <img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/> | ||||
| @ -208,8 +289,6 @@ make build | ||||
|             <sub style="font-size:14px"><b>Felix Kronlage-Dammers</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/felixonmars> | ||||
|             <img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/> | ||||
| @ -217,6 +296,43 @@ make build | ||||
|             <sub style="font-size:14px"><b>Felix Yan</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/JJGadgets> | ||||
|             <img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/> | ||||
|             <br /> | ||||
|             <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/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/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Jim Tittsler</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/piec> | ||||
|             <img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Pierre Carru</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/rcursaru> | ||||
|             <img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>rcursaru</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/ryanfowler> | ||||
|             <img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Ryan Fowler</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <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/> | ||||
| @ -224,6 +340,15 @@ 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/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>Tanner</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/Teteros> | ||||
|             <img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/> | ||||
| @ -252,8 +377,6 @@ 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/> | ||||
| @ -261,6 +384,15 @@ 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/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>ZiYuan</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/derelm> | ||||
|             <img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/> | ||||
| @ -268,6 +400,13 @@ make build | ||||
|             <sub style="font-size:14px"><b>derelm</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/e-zk> | ||||
|             <img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>e-zk</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/ignoramous> | ||||
|             <img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/> | ||||
| @ -275,6 +414,22 @@ make build | ||||
|             <sub style="font-size:14px"><b>ignoramous</b></sub> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0"> | ||||
|         <a href=https://github.com/lion24> | ||||
|             <img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lion24/> | ||||
|             <br /> | ||||
|             <sub style="font-size:14px"><b>lion24</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/> | ||||
|             <br /> | ||||
|             <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) | ||||
|  | ||||
							
								
								
									
										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:*"] } | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
							
								
								
									
										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