mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-19 19:37:31 +00:00
Compare commits
80 Commits
v0.16.0-be
...
v0.16.0-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
891815634b | ||
![]() |
8650328922 | ||
![]() |
7bd07e3b9b | ||
![]() |
76195bb3ac | ||
![]() |
6afd492095 | ||
![]() |
c95bce4aea | ||
![]() |
fd3a1c13e3 | ||
![]() |
95824ac2ec | ||
![]() |
a050158d11 | ||
![]() |
e0ef601123 | ||
![]() |
9c5d485fdd | ||
![]() |
cb88b16207 | ||
![]() |
257c025975 | ||
![]() |
50bdf9d3b9 | ||
![]() |
8d58894daa | ||
![]() |
43fa7f9fd5 | ||
![]() |
f2a8bfeb9f | ||
![]() |
06bbeea37f | ||
![]() |
e5f26f819a | ||
![]() |
a058f17946 | ||
![]() |
a4b4fc8b6c | ||
![]() |
ab35baaa29 | ||
![]() |
883bb92991 | ||
![]() |
bfb58de7b8 | ||
![]() |
6faf2d63d0 | ||
![]() |
569f3caab9 | ||
![]() |
7cd0f5e8a4 | ||
![]() |
02cc6bcc05 | ||
![]() |
9ff09b73ad | ||
![]() |
f93cf4b980 | ||
![]() |
3d7be5b287 | ||
![]() |
cdf41bd500 | ||
![]() |
735a6aaa39 | ||
![]() |
0c2648c188 | ||
![]() |
7e6291c21c | ||
![]() |
3f7749c6d4 | ||
![]() |
586c5411f1 | ||
![]() |
2be16b581c | ||
![]() |
06e22bf878 | ||
![]() |
0b4b530809 | ||
![]() |
efca3daa5c | ||
![]() |
fdefe46c40 | ||
![]() |
34be10840c | ||
![]() |
80ad1db228 | ||
![]() |
e918ea89a3 | ||
![]() |
19b968849f | ||
![]() |
5bc11891f5 | ||
![]() |
818d26b5f9 | ||
![]() |
c47354bdc3 | ||
![]() |
86ce0e0c66 | ||
![]() |
39f03b86c8 | ||
![]() |
8287ba24b9 | ||
![]() |
ab1aac9f3e | ||
![]() |
3e353004b8 | ||
![]() |
bcb04d38a5 | ||
![]() |
de0e2bf828 | ||
![]() |
8fed47a2be | ||
![]() |
17d4968425 | ||
![]() |
54acee6880 | ||
![]() |
a4e05d4db3 | ||
![]() |
b0acbed329 | ||
![]() |
1b2967320b | ||
![]() |
90f6be0c98 | ||
![]() |
78ed610b50 | ||
![]() |
af891808f6 | ||
![]() |
0c5a402206 | ||
![]() |
8744eeeb19 | ||
![]() |
ce13596077 | ||
![]() |
402a29e50c | ||
![]() |
0363e58467 | ||
![]() |
c8a14ccabb | ||
![]() |
1de29fd4e6 | ||
![]() |
75a0155f73 | ||
![]() |
c3db5ed749 | ||
![]() |
24c9530eee | ||
![]() |
aee8aa1c61 | ||
![]() |
5514a862dc | ||
![]() |
1ea8bb782c | ||
![]() |
35722cd5aa | ||
![]() |
533ecee252 |
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## 0.16.0 (2022-xx-xx)
|
## 0.16.0 (2022-xx-xx)
|
||||||
|
|
||||||
|
### BREAKING
|
||||||
|
|
||||||
|
- Old ACL syntax is no longer supported ("users" & "ports" -> "src" & "dst"). Please check [the new syntax](https://tailscale.com/kb/1018/acls/).
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- **Drop** armhf (32-bit ARM) support. [#609](https://github.com/juanfont/headscale/pull/609)
|
- **Drop** armhf (32-bit ARM) support. [#609](https://github.com/juanfont/headscale/pull/609)
|
||||||
@@ -22,6 +26,10 @@
|
|||||||
- This change disables the logs by default
|
- This change disables the logs by default
|
||||||
- Use [Prometheus]'s duration parser, supporting days (`d`), weeks (`w`) and years (`y`) [#598](https://github.com/juanfont/headscale/pull/598)
|
- Use [Prometheus]'s duration parser, supporting days (`d`), weeks (`w`) and years (`y`) [#598](https://github.com/juanfont/headscale/pull/598)
|
||||||
- Add support for reloading ACLs with SIGHUP [#601](https://github.com/juanfont/headscale/pull/601)
|
- Add support for reloading ACLs with SIGHUP [#601](https://github.com/juanfont/headscale/pull/601)
|
||||||
|
- Use new ACL syntax [#618](https://github.com/juanfont/headscale/pull/618)
|
||||||
|
- Add -c option to specify config file from command line [#285](https://github.com/juanfont/headscale/issues/285) [#612](https://github.com/juanfont/headscale/pull/601)
|
||||||
|
- Add configuration option to allow Tailscale clients to use a random WireGuard port. [kb/1181/firewalls](https://tailscale.com/kb/1181/firewalls) [#624](https://github.com/juanfont/headscale/pull/624)
|
||||||
|
- Improve obtuse UX regarding missing configuration (`ephemeral_node_inactivity_timeout` not set) [#639](https://github.com/juanfont/headscale/pull/639)
|
||||||
|
|
||||||
## 0.15.0 (2022-03-20)
|
## 0.15.0 (2022-03-20)
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
RUN CGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
||||||
RUN strip /go/bin/headscale
|
RUN strip /go/bin/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
RUN CGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
||||||
RUN strip /go/bin/headscale
|
RUN strip /go/bin/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
RUN CGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
# Debug image
|
# Debug image
|
||||||
|
2
Makefile
2
Makefile
@@ -1,5 +1,5 @@
|
|||||||
# Calculate version
|
# Calculate version
|
||||||
version = $(git describe --always --tags --dirty)
|
version ?= $(shell git describe --always --tags --dirty)
|
||||||
|
|
||||||
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
|
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
|
||||||
|
|
||||||
|
76
README.md
76
README.md
@@ -218,6 +218,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
|
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/huskyii>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/5499746?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jiang Zhu/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Jiang Zhu</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/unreality>
|
<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/>
|
<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/>
|
||||||
@@ -239,6 +246,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
|
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Niek>
|
<a href=https://github.com/Niek>
|
||||||
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
|
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
|
||||||
@@ -246,8 +255,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Niek van der Maas</b></sub>
|
<sub style="font-size:14px"><b>Niek van der Maas</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/negbie>
|
<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/>
|
<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/>
|
||||||
@@ -283,6 +290,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>bravechamp</b></sub>
|
<sub style="font-size:14px"><b>bravechamp</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/deonthomasgy>
|
<a href=https://github.com/deonthomasgy>
|
||||||
<img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/>
|
<img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/>
|
||||||
@@ -290,8 +299,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Deon Thomas</b></sub>
|
<sub style="font-size:14px"><b>Deon Thomas</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/mevansam>
|
<a href=https://github.com/mevansam>
|
||||||
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
|
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
|
||||||
@@ -313,6 +320,22 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
|
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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/iSchluff>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Anton Schubert</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/artemklevtsov>
|
<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/>
|
<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/>
|
||||||
@@ -334,8 +357,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub>
|
<sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/SilverBut>
|
<a href=https://github.com/SilverBut>
|
||||||
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
|
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
|
||||||
@@ -343,13 +364,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Silver Bullet</b></sub>
|
<sub style="font-size:14px"><b>Silver Bullet</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/lachy2849>
|
<a href=https://github.com/lachy2849>
|
||||||
<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=lachy2849/>
|
<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=lachy2849/>
|
||||||
@@ -364,6 +378,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>thomas</b></sub>
|
<sub style="font-size:14px"><b>thomas</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/aberoham>
|
<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/>
|
<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/>
|
||||||
@@ -378,15 +394,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Antoine POPINEAU</b></sub>
|
<sub style="font-size:14px"><b>Antoine POPINEAU</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
|
||||||
<a href=https://github.com/iSchluff>
|
|
||||||
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
|
|
||||||
<br />
|
|
||||||
<sub style="font-size:14px"><b>Anton Schubert</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/aofei>
|
<a href=https://github.com/aofei>
|
||||||
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
|
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
|
||||||
@@ -415,6 +422,15 @@ make build
|
|||||||
<sub style="font-size:14px"><b> Carson Yang</b></sub>
|
<sub style="font-size:14px"><b> Carson Yang</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/kundel>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/10158899?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kundel/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>kundel</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/fkr>
|
<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/>
|
<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/>
|
||||||
@@ -422,8 +438,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
|
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/felixonmars>
|
<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/>
|
<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/>
|
||||||
@@ -452,6 +466,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
|
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/piec>
|
<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/>
|
<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/>
|
||||||
@@ -466,8 +482,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>rcursaru</b></sub>
|
<sub style="font-size:14px"><b>rcursaru</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/renovate-bot>
|
<a href=https://github.com/renovate-bot>
|
||||||
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
|
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
|
||||||
@@ -496,6 +510,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Tanner</b></sub>
|
<sub style="font-size:14px"><b>Tanner</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Teteros>
|
<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/>
|
<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/>
|
||||||
@@ -510,8 +526,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>The Gitter Badger</b></sub>
|
<sub style="font-size:14px"><b>The Gitter Badger</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/tianon>
|
<a href=https://github.com/tianon>
|
||||||
<img src=https://avatars.githubusercontent.com/u/161631?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tianon Gravi/>
|
<img src=https://avatars.githubusercontent.com/u/161631?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tianon Gravi/>
|
||||||
@@ -540,6 +554,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
|
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Bpazy>
|
<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/>
|
<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/>
|
||||||
@@ -554,8 +570,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>derelm</b></sub>
|
<sub style="font-size:14px"><b>derelm</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/nning>
|
<a href=https://github.com/nning>
|
||||||
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
|
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
|
||||||
@@ -584,6 +598,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>pernila</b></sub>
|
<sub style="font-size:14px"><b>pernila</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Wakeful-Cloud>
|
<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/>
|
<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/>
|
||||||
|
109
acls.go
109
acls.go
@@ -23,6 +23,7 @@ const (
|
|||||||
errInvalidGroup = Error("invalid group")
|
errInvalidGroup = Error("invalid group")
|
||||||
errInvalidTag = Error("invalid tag")
|
errInvalidTag = Error("invalid tag")
|
||||||
errInvalidPortFormat = Error("invalid port format")
|
errInvalidPortFormat = Error("invalid port format")
|
||||||
|
errWildcardIsNeeded = Error("wildcard as port is required for the protocol")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -36,6 +37,23 @@ const (
|
|||||||
expectedTokenItems = 2
|
expectedTokenItems = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// For some reason golang.org/x/net/internal/iana is an internal package
|
||||||
|
const (
|
||||||
|
protocolICMP = 1 // Internet Control Message
|
||||||
|
protocolIGMP = 2 // Internet Group Management
|
||||||
|
protocolIPv4 = 4 // IPv4 encapsulation
|
||||||
|
protocolTCP = 6 // Transmission Control
|
||||||
|
protocolEGP = 8 // Exterior Gateway Protocol
|
||||||
|
protocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
|
||||||
|
protocolUDP = 17 // User Datagram
|
||||||
|
protocolGRE = 47 // Generic Routing Encapsulation
|
||||||
|
protocolESP = 50 // Encap Security Payload
|
||||||
|
protocolAH = 51 // Authentication Header
|
||||||
|
protocolIPv6ICMP = 58 // ICMP for IPv6
|
||||||
|
protocolSCTP = 132 // Stream Control Transmission Protocol
|
||||||
|
ProtocolFC = 133 // Fibre Channel
|
||||||
|
)
|
||||||
|
|
||||||
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
|
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
|
||||||
func (h *Headscale) LoadACLPolicy(path string) error {
|
func (h *Headscale) LoadACLPolicy(path string) error {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
@@ -123,23 +141,31 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
srcIPs := []string{}
|
srcIPs := []string{}
|
||||||
for innerIndex, user := range acl.Users {
|
for innerIndex, src := range acl.Sources {
|
||||||
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
|
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
|
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
srcIPs = append(srcIPs, srcs...)
|
srcIPs = append(srcIPs, srcs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
destPorts := []tailcfg.NetPortRange{}
|
protocols, needsWildcard, err := parseProtocol(acl.Protocol)
|
||||||
for innerIndex, ports := range acl.Ports {
|
|
||||||
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
|
Msgf("Error parsing ACL %d. protocol unknown %s", index, acl.Protocol)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
destPorts := []tailcfg.NetPortRange{}
|
||||||
|
for innerIndex, dest := range acl.Destinations {
|
||||||
|
dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -149,6 +175,7 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
|||||||
rules = append(rules, tailcfg.FilterRule{
|
rules = append(rules, tailcfg.FilterRule{
|
||||||
SrcIPs: srcIPs,
|
SrcIPs: srcIPs,
|
||||||
DstPorts: destPorts,
|
DstPorts: destPorts,
|
||||||
|
IPProto: protocols,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,17 +185,18 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
|||||||
func (h *Headscale) generateACLPolicySrcIP(
|
func (h *Headscale) generateACLPolicySrcIP(
|
||||||
machines []Machine,
|
machines []Machine,
|
||||||
aclPolicy ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
u string,
|
src string,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
return expandAlias(machines, aclPolicy, u, h.cfg.OIDC.StripEmaildomain)
|
return expandAlias(machines, aclPolicy, src, h.cfg.OIDC.StripEmaildomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) generateACLPolicyDestPorts(
|
func (h *Headscale) generateACLPolicyDest(
|
||||||
machines []Machine,
|
machines []Machine,
|
||||||
aclPolicy ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
d string,
|
dest string,
|
||||||
|
needsWildcard bool,
|
||||||
) ([]tailcfg.NetPortRange, error) {
|
) ([]tailcfg.NetPortRange, error) {
|
||||||
tokens := strings.Split(d, ":")
|
tokens := strings.Split(dest, ":")
|
||||||
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
|
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
|
||||||
return nil, errInvalidPortFormat
|
return nil, errInvalidPortFormat
|
||||||
}
|
}
|
||||||
@@ -195,7 +223,7 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ports, err := expandPorts(tokens[len(tokens)-1])
|
ports, err := expandPorts(tokens[len(tokens)-1], needsWildcard)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -214,6 +242,54 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
|||||||
return dests, nil
|
return dests, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseProtocol reads the proto field of the ACL and generates a list of
|
||||||
|
// protocols that will be allowed, following the IANA IP protocol number
|
||||||
|
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
|
||||||
|
//
|
||||||
|
// If the ACL proto field is empty, it allows ICMPv4, ICMPv6, TCP, and UDP,
|
||||||
|
// as per Tailscale behaviour (see tailcfg.FilterRule).
|
||||||
|
//
|
||||||
|
// Also returns a boolean indicating if the protocol
|
||||||
|
// requires all the destinations to use wildcard as port number (only TCP,
|
||||||
|
// UDP and SCTP support specifying ports).
|
||||||
|
func parseProtocol(protocol string) ([]int, bool, error) {
|
||||||
|
switch protocol {
|
||||||
|
case "":
|
||||||
|
return []int{protocolICMP, protocolIPv6ICMP, protocolTCP, protocolUDP}, false, nil
|
||||||
|
case "igmp":
|
||||||
|
return []int{protocolIGMP}, true, nil
|
||||||
|
case "ipv4", "ip-in-ip":
|
||||||
|
return []int{protocolIPv4}, true, nil
|
||||||
|
case "tcp":
|
||||||
|
return []int{protocolTCP}, false, nil
|
||||||
|
case "egp":
|
||||||
|
return []int{protocolEGP}, true, nil
|
||||||
|
case "igp":
|
||||||
|
return []int{protocolIGP}, true, nil
|
||||||
|
case "udp":
|
||||||
|
return []int{protocolUDP}, false, nil
|
||||||
|
case "gre":
|
||||||
|
return []int{protocolGRE}, true, nil
|
||||||
|
case "esp":
|
||||||
|
return []int{protocolESP}, true, nil
|
||||||
|
case "ah":
|
||||||
|
return []int{protocolAH}, true, nil
|
||||||
|
case "sctp":
|
||||||
|
return []int{protocolSCTP}, false, nil
|
||||||
|
case "icmp":
|
||||||
|
return []int{protocolICMP, protocolIPv6ICMP}, true, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
protocolNumber, err := strconv.Atoi(protocol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
needsWildcard := protocolNumber != protocolTCP && protocolNumber != protocolUDP && protocolNumber != protocolSCTP
|
||||||
|
|
||||||
|
return []int{protocolNumber}, needsWildcard, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// expandalias has an input of either
|
// expandalias has an input of either
|
||||||
// - a namespace
|
// - a namespace
|
||||||
// - a group
|
// - a group
|
||||||
@@ -268,6 +344,7 @@ func expandAlias(
|
|||||||
alias,
|
alias,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ips, nil
|
return ips, nil
|
||||||
} else {
|
} else {
|
||||||
return ips, err
|
return ips, err
|
||||||
@@ -359,13 +436,17 @@ func excludeCorrectlyTaggedNodes(
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
|
||||||
if portsStr == "*" {
|
if portsStr == "*" {
|
||||||
return &[]tailcfg.PortRange{
|
return &[]tailcfg.PortRange{
|
||||||
{First: portRangeBegin, Last: portRangeEnd},
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if needsWildcard {
|
||||||
|
return nil, errWildcardIsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
ports := []tailcfg.PortRange{}
|
ports := []tailcfg.PortRange{}
|
||||||
for _, portStr := range strings.Split(portsStr, ",") {
|
for _, portStr := range strings.Split(portsStr, ",") {
|
||||||
rang := strings.Split(portStr, "-")
|
rang := strings.Split(portStr, "-")
|
||||||
|
75
acls_test.go
75
acls_test.go
@@ -62,7 +62,7 @@ func (s *Suite) TestBasicRule(c *check.C) {
|
|||||||
func (s *Suite) TestInvalidAction(c *check.C) {
|
func (s *Suite) TestInvalidAction(c *check.C) {
|
||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "invalidAction", Users: []string{"*"}, Ports: []string{"*:*"}},
|
{Action: "invalidAction", Sources: []string{"*"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := app.UpdateACLRules()
|
err := app.UpdateACLRules()
|
||||||
@@ -70,14 +70,14 @@ func (s *Suite) TestInvalidAction(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
|
func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
|
||||||
// this ACL is wrong because the group in users sections doesn't exist
|
// this ACL is wrong because the group in Sources sections doesn't exist
|
||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
"group:test": []string{"foo"},
|
"group:test": []string{"foo"},
|
||||||
"group:error": []string{"foo", "group:test"},
|
"group:error": []string{"foo", "group:test"},
|
||||||
},
|
},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"group:error"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"group:error"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := app.UpdateACLRules()
|
err := app.UpdateACLRules()
|
||||||
@@ -88,7 +88,7 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
|
|||||||
// this ACL is wrong because no tagOwners own the requested tag for the server
|
// this ACL is wrong because no tagOwners own the requested tag for the server
|
||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"tag:foo"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"tag:foo"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := app.UpdateACLRules()
|
err := app.UpdateACLRules()
|
||||||
@@ -97,8 +97,8 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
|
|||||||
|
|
||||||
// this test should validate that we can expand a group in a TagOWner section and
|
// 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.
|
// 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.
|
// the tag is matched in the Sources section.
|
||||||
func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
|
func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
|
||||||
namespace, err := app.CreateNamespace("user1")
|
namespace, err := app.CreateNamespace("user1")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
|
|||||||
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
||||||
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"tag:test"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"tag:test"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = app.UpdateACLRules()
|
err = app.UpdateACLRules()
|
||||||
@@ -143,8 +143,8 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
|
|||||||
|
|
||||||
// this test should validate that we can expand a group in a TagOWner section and
|
// 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.
|
// 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.
|
// the tag is matched in the Destinations section.
|
||||||
func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
|
func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
|
||||||
namespace, err := app.CreateNamespace("user1")
|
namespace, err := app.CreateNamespace("user1")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
|
|||||||
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
||||||
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"*"}, Ports: []string{"tag:test:*"}},
|
{Action: "accept", Sources: []string{"*"}, Destinations: []string{"tag:test:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = app.UpdateACLRules()
|
err = app.UpdateACLRules()
|
||||||
@@ -222,7 +222,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
|
|||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
TagOwners: TagOwners{"tag:test": []string{"user1"}},
|
TagOwners: TagOwners{"tag:test": []string{"user1"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"user1"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"user1"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = app.UpdateACLRules()
|
err = app.UpdateACLRules()
|
||||||
@@ -288,8 +288,8 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
|
|||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Users: []string{"user1"},
|
Sources: []string{"user1"},
|
||||||
Ports: []string{"tag:webapp:80,443"},
|
Destinations: []string{"tag:webapp:80,443"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -321,6 +321,20 @@ func (s *Suite) TestPortRange(c *check.C) {
|
|||||||
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500))
|
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestProtocolParsing(c *check.C) {
|
||||||
|
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_protocols.hujson")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
rules, err := app.generateACLRules()
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(rules, check.NotNil)
|
||||||
|
|
||||||
|
c.Assert(rules, check.HasLen, 3)
|
||||||
|
c.Assert(rules[0].IPProto[0], check.Equals, protocolTCP)
|
||||||
|
c.Assert(rules[1].IPProto[0], check.Equals, protocolUDP)
|
||||||
|
c.Assert(rules[2].IPProto[1], check.Equals, protocolIPv6ICMP)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Suite) TestPortWildcard(c *check.C) {
|
func (s *Suite) TestPortWildcard(c *check.C) {
|
||||||
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
|
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
@@ -629,6 +643,7 @@ func Test_expandTagOwners(t *testing.T) {
|
|||||||
func Test_expandPorts(t *testing.T) {
|
func Test_expandPorts(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
portsStr string
|
portsStr string
|
||||||
|
needsWildcard bool
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -638,15 +653,29 @@ func Test_expandPorts(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "wildcard",
|
name: "wildcard",
|
||||||
args: args{portsStr: "*"},
|
args: args{portsStr: "*", needsWildcard: true},
|
||||||
want: &[]tailcfg.PortRange{
|
want: &[]tailcfg.PortRange{
|
||||||
{First: portRangeBegin, Last: portRangeEnd},
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "two ports",
|
name: "needs wildcard but does not require it",
|
||||||
args: args{portsStr: "80,443"},
|
args: args{portsStr: "*", needsWildcard: false},
|
||||||
|
want: &[]tailcfg.PortRange{
|
||||||
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "needs wildcard but gets port",
|
||||||
|
args: args{portsStr: "80,443", needsWildcard: true},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two Destinations",
|
||||||
|
args: args{portsStr: "80,443", needsWildcard: false},
|
||||||
want: &[]tailcfg.PortRange{
|
want: &[]tailcfg.PortRange{
|
||||||
{First: 80, Last: 80},
|
{First: 80, Last: 80},
|
||||||
{First: 443, Last: 443},
|
{First: 443, Last: 443},
|
||||||
@@ -655,7 +684,7 @@ func Test_expandPorts(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "a range and a port",
|
name: "a range and a port",
|
||||||
args: args{portsStr: "80-1024,443"},
|
args: args{portsStr: "80-1024,443", needsWildcard: false},
|
||||||
want: &[]tailcfg.PortRange{
|
want: &[]tailcfg.PortRange{
|
||||||
{First: 80, Last: 1024},
|
{First: 80, Last: 1024},
|
||||||
{First: 443, Last: 443},
|
{First: 443, Last: 443},
|
||||||
@@ -664,38 +693,38 @@ func Test_expandPorts(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "out of bounds",
|
name: "out of bounds",
|
||||||
args: args{portsStr: "854038"},
|
args: args{portsStr: "854038", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong port",
|
name: "wrong port",
|
||||||
args: args{portsStr: "85a38"},
|
args: args{portsStr: "85a38", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong port in first",
|
name: "wrong port in first",
|
||||||
args: args{portsStr: "a-80"},
|
args: args{portsStr: "a-80", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong port in last",
|
name: "wrong port in last",
|
||||||
args: args{portsStr: "80-85a38"},
|
args: args{portsStr: "80-85a38", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong port format",
|
name: "wrong port format",
|
||||||
args: args{portsStr: "80-85a38-3"},
|
args: args{portsStr: "80-85a38-3", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
got, err := expandPorts(test.args.portsStr)
|
got, err := expandPorts(test.args.portsStr, test.args.needsWildcard)
|
||||||
if (err != nil) != test.wantErr {
|
if (err != nil) != test.wantErr {
|
||||||
t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)
|
t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)
|
||||||
|
|
||||||
|
@@ -11,18 +11,19 @@ import (
|
|||||||
|
|
||||||
// ACLPolicy represents a Tailscale ACL Policy.
|
// ACLPolicy represents a Tailscale ACL Policy.
|
||||||
type ACLPolicy struct {
|
type ACLPolicy struct {
|
||||||
Groups Groups `json:"Groups" yaml:"Groups"`
|
Groups Groups `json:"groups" yaml:"groups"`
|
||||||
Hosts Hosts `json:"Hosts" yaml:"Hosts"`
|
Hosts Hosts `json:"hosts" yaml:"hosts"`
|
||||||
TagOwners TagOwners `json:"TagOwners" yaml:"TagOwners"`
|
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
|
||||||
ACLs []ACL `json:"ACLs" yaml:"ACLs"`
|
ACLs []ACL `json:"acls" yaml:"acls"`
|
||||||
Tests []ACLTest `json:"Tests" yaml:"Tests"`
|
Tests []ACLTest `json:"tests" yaml:"tests"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACL is a basic rule for the ACL Policy.
|
// ACL is a basic rule for the ACL Policy.
|
||||||
type ACL struct {
|
type ACL struct {
|
||||||
Action string `json:"Action" yaml:"Action"`
|
Action string `json:"action" yaml:"action"`
|
||||||
Users []string `json:"Users" yaml:"Users"`
|
Protocol string `json:"proto" yaml:"proto"`
|
||||||
Ports []string `json:"Ports" yaml:"Ports"`
|
Sources []string `json:"src" yaml:"src"`
|
||||||
|
Destinations []string `json:"dst" yaml:"dst"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Groups references a series of alias in the ACL rules.
|
// Groups references a series of alias in the ACL rules.
|
||||||
@@ -36,9 +37,9 @@ type TagOwners map[string][]string
|
|||||||
|
|
||||||
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
|
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
|
||||||
type ACLTest struct {
|
type ACLTest struct {
|
||||||
User string `json:"User" yaml:"User"`
|
Source string `json:"src" yaml:"src"`
|
||||||
Allow []string `json:"Allow" yaml:"Allow"`
|
Accept []string `json:"accept" yaml:"accept"`
|
||||||
Deny []string `json:"Deny,omitempty" yaml:"Deny,omitempty"`
|
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
|
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
|
||||||
|
1
api.go
1
api.go
@@ -280,6 +280,7 @@ func (h *Headscale) getMapResponse(
|
|||||||
UserProfiles: profiles,
|
UserProfiles: profiles,
|
||||||
Debug: &tailcfg.Debug{
|
Debug: &tailcfg.Debug{
|
||||||
DisableLogTail: !h.cfg.LogTail.Enabled,
|
DisableLogTail: !h.cfg.LogTail.Enabled,
|
||||||
|
RandomizeClientPort: h.cfg.RandomizeClientPort,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
16
api_key.go
16
api_key.go
@@ -13,7 +13,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
apiPrefixLength = 7
|
apiPrefixLength = 7
|
||||||
apiKeyLength = 32
|
apiKeyLength = 32
|
||||||
apiKeyParts = 2
|
|
||||||
|
|
||||||
errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
|
errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
|
||||||
)
|
)
|
||||||
@@ -115,9 +114,9 @@ func (h *Headscale) ExpireAPIKey(key *APIKey) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
|
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
|
||||||
prefix, hash, err := splitAPIKey(keyStr)
|
prefix, hash, found := strings.Cut(keyStr, ".")
|
||||||
if err != nil {
|
if !found {
|
||||||
return false, fmt.Errorf("failed to validate api key: %w", err)
|
return false, errAPIKeyFailedToParse
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := h.GetAPIKey(prefix)
|
key, err := h.GetAPIKey(prefix)
|
||||||
@@ -136,15 +135,6 @@ func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitAPIKey(key string) (string, string, error) {
|
|
||||||
parts := strings.Split(key, ".")
|
|
||||||
if len(parts) != apiKeyParts {
|
|
||||||
return "", "", errAPIKeyFailedToParse
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts[0], parts[1], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (key *APIKey) toProto() *v1.ApiKey {
|
func (key *APIKey) toProto() *v1.ApiKey {
|
||||||
protoKey := v1.ApiKey{
|
protoKey := v1.ApiKey{
|
||||||
Id: key.ID,
|
Id: key.ID,
|
||||||
|
133
app.go
133
app.go
@@ -6,10 +6,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -42,7 +40,6 @@ import (
|
|||||||
"google.golang.org/grpc/reflection"
|
"google.golang.org/grpc/reflection"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"inet.af/netaddr"
|
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
@@ -72,95 +69,9 @@ const (
|
|||||||
EnforcedClientAuth = "enforced"
|
EnforcedClientAuth = "enforced"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config contains the initial Headscale configuration.
|
|
||||||
type Config struct {
|
|
||||||
ServerURL string
|
|
||||||
Addr string
|
|
||||||
MetricsAddr string
|
|
||||||
GRPCAddr string
|
|
||||||
GRPCAllowInsecure bool
|
|
||||||
EphemeralNodeInactivityTimeout time.Duration
|
|
||||||
IPPrefixes []netaddr.IPPrefix
|
|
||||||
PrivateKeyPath string
|
|
||||||
BaseDomain string
|
|
||||||
|
|
||||||
DERP DERPConfig
|
|
||||||
|
|
||||||
DBtype string
|
|
||||||
DBpath string
|
|
||||||
DBhost string
|
|
||||||
DBport int
|
|
||||||
DBname string
|
|
||||||
DBuser string
|
|
||||||
DBpass string
|
|
||||||
|
|
||||||
TLSLetsEncryptListen string
|
|
||||||
TLSLetsEncryptHostname string
|
|
||||||
TLSLetsEncryptCacheDir string
|
|
||||||
TLSLetsEncryptChallengeType string
|
|
||||||
|
|
||||||
TLSCertPath string
|
|
||||||
TLSKeyPath string
|
|
||||||
TLSClientAuthMode tls.ClientAuthType
|
|
||||||
|
|
||||||
ACMEURL string
|
|
||||||
ACMEEmail string
|
|
||||||
|
|
||||||
DNSConfig *tailcfg.DNSConfig
|
|
||||||
|
|
||||||
UnixSocket string
|
|
||||||
UnixSocketPermission fs.FileMode
|
|
||||||
|
|
||||||
OIDC OIDCConfig
|
|
||||||
|
|
||||||
LogTail LogTailConfig
|
|
||||||
|
|
||||||
CLI CLIConfig
|
|
||||||
|
|
||||||
ACL ACLConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type OIDCConfig struct {
|
|
||||||
Issuer string
|
|
||||||
ClientID string
|
|
||||||
ClientSecret string
|
|
||||||
Scope []string
|
|
||||||
ExtraParams map[string]string
|
|
||||||
AllowedDomains []string
|
|
||||||
AllowedUsers []string
|
|
||||||
StripEmaildomain bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type DERPConfig struct {
|
|
||||||
ServerEnabled bool
|
|
||||||
ServerRegionID int
|
|
||||||
ServerRegionCode string
|
|
||||||
ServerRegionName string
|
|
||||||
STUNAddr string
|
|
||||||
URLs []url.URL
|
|
||||||
Paths []string
|
|
||||||
AutoUpdate bool
|
|
||||||
UpdateFrequency time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogTailConfig struct {
|
|
||||||
Enabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type CLIConfig struct {
|
|
||||||
Address string
|
|
||||||
APIKey string
|
|
||||||
Timeout time.Duration
|
|
||||||
Insecure bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ACLConfig struct {
|
|
||||||
PolicyPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headscale represents the base app of the service.
|
// Headscale represents the base app of the service.
|
||||||
type Headscale struct {
|
type Headscale struct {
|
||||||
cfg Config
|
cfg *Config
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
dbString string
|
dbString string
|
||||||
dbType string
|
dbType string
|
||||||
@@ -204,7 +115,7 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHeadscale(cfg Config) (*Headscale, error) {
|
func NewHeadscale(cfg *Config) (*Headscale, error) {
|
||||||
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read or create private key: %w", err)
|
return nil, fmt.Errorf("failed to read or create private key: %w", err)
|
||||||
@@ -257,7 +168,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
|||||||
magicDNSDomains := generateMagicDNSRootDomains(app.cfg.IPPrefixes)
|
magicDNSDomains := generateMagicDNSRootDomains(app.cfg.IPPrefixes)
|
||||||
// we might have routes already from Split DNS
|
// we might have routes already from Split DNS
|
||||||
if app.cfg.DNSConfig.Routes == nil {
|
if app.cfg.DNSConfig.Routes == nil {
|
||||||
app.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver)
|
app.cfg.DNSConfig.Routes = make(map[string][]*dnstype.Resolver)
|
||||||
}
|
}
|
||||||
for _, d := range magicDNSDomains {
|
for _, d := range magicDNSDomains {
|
||||||
app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
|
app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
|
||||||
@@ -746,7 +657,9 @@ func (h *Headscale) Serve() error {
|
|||||||
}
|
}
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("path", aclPath).
|
Str("path", aclPath).
|
||||||
Msg("ACL policy successfully reloaded")
|
Msg("ACL policy successfully reloaded, notifying nodes of change")
|
||||||
|
|
||||||
|
h.setLastStateChangeToNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -778,7 +691,7 @@ func (h *Headscale) Serve() error {
|
|||||||
|
|
||||||
func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
||||||
var err error
|
var err error
|
||||||
if h.cfg.TLSLetsEncryptHostname != "" {
|
if h.cfg.TLS.LetsEncrypt.Hostname != "" {
|
||||||
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
||||||
log.Warn().
|
log.Warn().
|
||||||
Msg("Listening with TLS but ServerURL does not start with https://")
|
Msg("Listening with TLS but ServerURL does not start with https://")
|
||||||
@@ -786,15 +699,15 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||||||
|
|
||||||
certManager := autocert.Manager{
|
certManager := autocert.Manager{
|
||||||
Prompt: autocert.AcceptTOS,
|
Prompt: autocert.AcceptTOS,
|
||||||
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
|
HostPolicy: autocert.HostWhitelist(h.cfg.TLS.LetsEncrypt.Hostname),
|
||||||
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
|
Cache: autocert.DirCache(h.cfg.TLS.LetsEncrypt.CacheDir),
|
||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
DirectoryURL: h.cfg.ACMEURL,
|
DirectoryURL: h.cfg.ACMEURL,
|
||||||
},
|
},
|
||||||
Email: h.cfg.ACMEEmail,
|
Email: h.cfg.ACMEEmail,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch h.cfg.TLSLetsEncryptChallengeType {
|
switch h.cfg.TLS.LetsEncrypt.ChallengeType {
|
||||||
case "TLS-ALPN-01":
|
case "TLS-ALPN-01":
|
||||||
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
|
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
|
||||||
// The RFC requires that the validation is done on port 443; in other words, headscale
|
// The RFC requires that the validation is done on port 443; in other words, headscale
|
||||||
@@ -808,7 +721,7 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||||||
go func() {
|
go func() {
|
||||||
log.Fatal().
|
log.Fatal().
|
||||||
Caller().
|
Caller().
|
||||||
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
|
Err(http.ListenAndServe(h.cfg.TLS.LetsEncrypt.Listen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
|
||||||
Msg("failed to set up a HTTP server")
|
Msg("failed to set up a HTTP server")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -817,7 +730,7 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||||||
default:
|
default:
|
||||||
return nil, errUnsupportedLetsEncryptChallengeType
|
return nil, errUnsupportedLetsEncryptChallengeType
|
||||||
}
|
}
|
||||||
} else if h.cfg.TLSCertPath == "" {
|
} else if h.cfg.TLS.CertPath == "" {
|
||||||
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
|
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
|
||||||
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://")
|
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://")
|
||||||
}
|
}
|
||||||
@@ -830,28 +743,40 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||||||
|
|
||||||
log.Info().Msg(fmt.Sprintf(
|
log.Info().Msg(fmt.Sprintf(
|
||||||
"Client authentication (mTLS) is \"%s\". See the docs to learn about configuring this setting.",
|
"Client authentication (mTLS) is \"%s\". See the docs to learn about configuring this setting.",
|
||||||
h.cfg.TLSClientAuthMode))
|
h.cfg.TLS.ClientAuthMode))
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
ClientAuth: h.cfg.TLSClientAuthMode,
|
ClientAuth: h.cfg.TLS.ClientAuthMode,
|
||||||
NextProtos: []string{"http/1.1"},
|
NextProtos: []string{"http/1.1"},
|
||||||
Certificates: make([]tls.Certificate, 1),
|
Certificates: make([]tls.Certificate, 1),
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
|
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath)
|
||||||
|
|
||||||
return tlsConfig, err
|
return tlsConfig, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) setLastStateChangeToNow(namespace string) {
|
func (h *Headscale) setLastStateChangeToNow(namespaces ...string) {
|
||||||
|
var err error
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
lastStateUpdate.WithLabelValues("", "headscale").Set(float64(now.Unix()))
|
|
||||||
|
if len(namespaces) == 0 {
|
||||||
|
namespaces, err = h.ListNamespacesStr()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Caller().Err(err).Msg("failed to fetch all namespaces, failing to update last changed state.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, namespace := range namespaces {
|
||||||
|
lastStateUpdate.WithLabelValues(namespace, "headscale").Set(float64(now.Unix()))
|
||||||
if h.lastStateChange == nil {
|
if h.lastStateChange == nil {
|
||||||
h.lastStateChange = xsync.NewMapOf[time.Time]()
|
h.lastStateChange = xsync.NewMapOf[time.Time]()
|
||||||
}
|
}
|
||||||
h.lastStateChange.Store(namespace, now)
|
h.lastStateChange.Store(namespace, now)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
|
func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
|
||||||
|
@@ -46,7 +46,7 @@ func (s *Suite) ResetDB(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app = Headscale{
|
app = Headscale{
|
||||||
cfg: cfg,
|
cfg: &cfg,
|
||||||
dbType: "sqlite3",
|
dbType: "sqlite3",
|
||||||
dbString: tmpDir + "/headscale_test.db",
|
dbString: tmpDir + "/headscale_test.db",
|
||||||
}
|
}
|
||||||
|
28
cmd/headscale/cli/dump_config.go
Normal file
28
cmd/headscale/cli/dump_config.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(dumpConfigCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dumpConfigCmd = &cobra.Command{
|
||||||
|
Use: "dumpConfig",
|
||||||
|
Short: "dump current config to /etc/headscale/config.dump.yaml, integration test only",
|
||||||
|
Hidden: true,
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := viper.WriteConfigAs("/etc/headscale/config.dump.yaml")
|
||||||
|
if err != nil {
|
||||||
|
//nolint
|
||||||
|
fmt.Println("Failed to dump config")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
@@ -3,17 +3,75 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/tcnksm/go-latest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cfgFile string = ""
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
cobra.OnInitialize(initConfig)
|
||||||
|
rootCmd.PersistentFlags().
|
||||||
|
StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
|
||||||
rootCmd.PersistentFlags().
|
rootCmd.PersistentFlags().
|
||||||
StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'")
|
StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'")
|
||||||
rootCmd.PersistentFlags().
|
rootCmd.PersistentFlags().
|
||||||
Bool("force", false, "Disable prompts and forces the execution")
|
Bool("force", false, "Disable prompts and forces the execution")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initConfig() {
|
||||||
|
if cfgFile != "" {
|
||||||
|
err := headscale.LoadConfig(cfgFile, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Caller().Err(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := headscale.LoadConfig("", false)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Caller().Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := headscale.GetHeadscaleConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Caller().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
machineOutput := HasMachineOutputFlag()
|
||||||
|
|
||||||
|
zerolog.SetGlobalLevel(cfg.LogLevel)
|
||||||
|
|
||||||
|
// If the user has requested a "machine" readable format,
|
||||||
|
// then disable login so the output remains valid.
|
||||||
|
if machineOutput {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.DisableUpdateCheck && !machineOutput {
|
||||||
|
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
||||||
|
Version != "dev" {
|
||||||
|
githubTag := &latest.GithubTag{
|
||||||
|
Owner: "juanfont",
|
||||||
|
Repository: "headscale",
|
||||||
|
}
|
||||||
|
res, err := latest.Check(githubTag, Version)
|
||||||
|
if err == nil && res.Outdated {
|
||||||
|
//nolint
|
||||||
|
fmt.Printf(
|
||||||
|
"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n",
|
||||||
|
res.Current,
|
||||||
|
Version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "headscale",
|
Use: "headscale",
|
||||||
Short: "headscale - a Tailscale control server",
|
Short: "headscale - a Tailscale control server",
|
||||||
|
@@ -16,12 +16,12 @@ var serveCmd = &cobra.Command{
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
h, err := getHeadscaleApp()
|
app, err := getHeadscaleApp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Caller().Err(err).Msg("Error initializing")
|
log.Fatal().Caller().Err(err).Msg("Error initializing")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.Serve()
|
err = app.Serve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Caller().Err(err).Msg("Error starting server")
|
log.Fatal().Caller().Err(err).Msg("Error starting server")
|
||||||
}
|
}
|
||||||
|
@@ -4,416 +4,29 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/juanfont/headscale"
|
"github.com/juanfont/headscale"
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/viper"
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"inet.af/netaddr"
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
"tailscale.com/types/dnstype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PermissionFallback = 0o700
|
|
||||||
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
|
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadConfig(path string) error {
|
|
||||||
viper.SetConfigName("config")
|
|
||||||
if path == "" {
|
|
||||||
viper.AddConfigPath("/etc/headscale/")
|
|
||||||
viper.AddConfigPath("$HOME/.headscale")
|
|
||||||
viper.AddConfigPath(".")
|
|
||||||
} else {
|
|
||||||
// For testing
|
|
||||||
viper.AddConfigPath(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
viper.SetEnvPrefix("headscale")
|
|
||||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
||||||
viper.AutomaticEnv()
|
|
||||||
|
|
||||||
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
|
|
||||||
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
|
|
||||||
viper.SetDefault("tls_client_auth_mode", "relaxed")
|
|
||||||
|
|
||||||
viper.SetDefault("log_level", "info")
|
|
||||||
|
|
||||||
viper.SetDefault("dns_config", nil)
|
|
||||||
|
|
||||||
viper.SetDefault("derp.server.enabled", false)
|
|
||||||
viper.SetDefault("derp.server.stun.enabled", true)
|
|
||||||
|
|
||||||
viper.SetDefault("unix_socket", "/var/run/headscale.sock")
|
|
||||||
viper.SetDefault("unix_socket_permission", "0o770")
|
|
||||||
|
|
||||||
viper.SetDefault("grpc_listen_addr", ":50443")
|
|
||||||
viper.SetDefault("grpc_allow_insecure", false)
|
|
||||||
|
|
||||||
viper.SetDefault("cli.timeout", "5s")
|
|
||||||
viper.SetDefault("cli.insecure", false)
|
|
||||||
|
|
||||||
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
|
|
||||||
viper.SetDefault("oidc.strip_email_domain", true)
|
|
||||||
|
|
||||||
viper.SetDefault("logtail.enabled", false)
|
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
|
||||||
return fmt.Errorf("fatal error reading config file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect any validation errors and return them all at once
|
|
||||||
var errorText string
|
|
||||||
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
|
|
||||||
((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) {
|
|
||||||
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
|
|
||||||
(viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") &&
|
|
||||||
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
|
|
||||||
// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
|
|
||||||
log.Warn().
|
|
||||||
Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") &&
|
|
||||||
(viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") {
|
|
||||||
errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(viper.GetString("server_url"), "http://") &&
|
|
||||||
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
|
|
||||||
errorText += "Fatal config error: server_url must start with https:// or http://\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
_, authModeValid := headscale.LookupTLSClientAuthMode(
|
|
||||||
viper.GetString("tls_client_auth_mode"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if !authModeValid {
|
|
||||||
errorText += fmt.Sprintf(
|
|
||||||
"Invalid tls_client_auth_mode supplied: %s. Accepted values: %s, %s, %s.",
|
|
||||||
viper.GetString("tls_client_auth_mode"),
|
|
||||||
headscale.DisabledClientAuth,
|
|
||||||
headscale.RelaxedClientAuth,
|
|
||||||
headscale.EnforcedClientAuth)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errorText != "" {
|
|
||||||
//nolint
|
|
||||||
return errors.New(strings.TrimSuffix(errorText, "\n"))
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDERPConfig() headscale.DERPConfig {
|
|
||||||
serverEnabled := viper.GetBool("derp.server.enabled")
|
|
||||||
serverRegionID := viper.GetInt("derp.server.region_id")
|
|
||||||
serverRegionCode := viper.GetString("derp.server.region_code")
|
|
||||||
serverRegionName := viper.GetString("derp.server.region_name")
|
|
||||||
stunAddr := viper.GetString("derp.server.stun_listen_addr")
|
|
||||||
|
|
||||||
if serverEnabled && stunAddr == "" {
|
|
||||||
log.Fatal().
|
|
||||||
Msg("derp.server.stun_listen_addr must be set if derp.server.enabled is true")
|
|
||||||
}
|
|
||||||
|
|
||||||
urlStrs := viper.GetStringSlice("derp.urls")
|
|
||||||
|
|
||||||
urls := make([]url.URL, len(urlStrs))
|
|
||||||
for index, urlStr := range urlStrs {
|
|
||||||
urlAddr, err := url.Parse(urlStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Str("url", urlStr).
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to parse url, ignoring...")
|
|
||||||
}
|
|
||||||
|
|
||||||
urls[index] = *urlAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
paths := viper.GetStringSlice("derp.paths")
|
|
||||||
|
|
||||||
autoUpdate := viper.GetBool("derp.auto_update_enabled")
|
|
||||||
updateFrequency := viper.GetDuration("derp.update_frequency")
|
|
||||||
|
|
||||||
return headscale.DERPConfig{
|
|
||||||
ServerEnabled: serverEnabled,
|
|
||||||
ServerRegionID: serverRegionID,
|
|
||||||
ServerRegionCode: serverRegionCode,
|
|
||||||
ServerRegionName: serverRegionName,
|
|
||||||
STUNAddr: stunAddr,
|
|
||||||
URLs: urls,
|
|
||||||
Paths: paths,
|
|
||||||
AutoUpdate: autoUpdate,
|
|
||||||
UpdateFrequency: updateFrequency,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogTailConfig() headscale.LogTailConfig {
|
|
||||||
enabled := viper.GetBool("logtail.enabled")
|
|
||||||
|
|
||||||
return headscale.LogTailConfig{
|
|
||||||
Enabled: enabled,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetACLConfig() headscale.ACLConfig {
|
|
||||||
policyPath := viper.GetString("acl_policy_path")
|
|
||||||
|
|
||||||
return headscale.ACLConfig{
|
|
||||||
PolicyPath: policyPath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
|
||||||
if viper.IsSet("dns_config") {
|
|
||||||
dnsConfig := &tailcfg.DNSConfig{}
|
|
||||||
|
|
||||||
if viper.IsSet("dns_config.nameservers") {
|
|
||||||
nameserversStr := viper.GetStringSlice("dns_config.nameservers")
|
|
||||||
|
|
||||||
nameservers := make([]netaddr.IP, len(nameserversStr))
|
|
||||||
resolvers := make([]dnstype.Resolver, len(nameserversStr))
|
|
||||||
|
|
||||||
for index, nameserverStr := range nameserversStr {
|
|
||||||
nameserver, err := netaddr.ParseIP(nameserverStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Str("func", "getDNSConfig").
|
|
||||||
Err(err).
|
|
||||||
Msgf("Could not parse nameserver IP: %s", nameserverStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
nameservers[index] = nameserver
|
|
||||||
resolvers[index] = dnstype.Resolver{
|
|
||||||
Addr: nameserver.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsConfig.Nameservers = nameservers
|
|
||||||
dnsConfig.Resolvers = resolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
if viper.IsSet("dns_config.restricted_nameservers") {
|
|
||||||
if len(dnsConfig.Nameservers) > 0 {
|
|
||||||
dnsConfig.Routes = make(map[string][]dnstype.Resolver)
|
|
||||||
restrictedDNS := viper.GetStringMapStringSlice(
|
|
||||||
"dns_config.restricted_nameservers",
|
|
||||||
)
|
|
||||||
for domain, restrictedNameservers := range restrictedDNS {
|
|
||||||
restrictedResolvers := make(
|
|
||||||
[]dnstype.Resolver,
|
|
||||||
len(restrictedNameservers),
|
|
||||||
)
|
|
||||||
for index, nameserverStr := range restrictedNameservers {
|
|
||||||
nameserver, err := netaddr.ParseIP(nameserverStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Str("func", "getDNSConfig").
|
|
||||||
Err(err).
|
|
||||||
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
|
|
||||||
}
|
|
||||||
restrictedResolvers[index] = dnstype.Resolver{
|
|
||||||
Addr: nameserver.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dnsConfig.Routes[domain] = restrictedResolvers
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Warn().
|
|
||||||
Msg("Warning: dns_config.restricted_nameservers is set, but no nameservers are configured. Ignoring restricted_nameservers.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if viper.IsSet("dns_config.domains") {
|
|
||||||
dnsConfig.Domains = viper.GetStringSlice("dns_config.domains")
|
|
||||||
}
|
|
||||||
|
|
||||||
if viper.IsSet("dns_config.magic_dns") {
|
|
||||||
magicDNS := viper.GetBool("dns_config.magic_dns")
|
|
||||||
if len(dnsConfig.Nameservers) > 0 {
|
|
||||||
dnsConfig.Proxied = magicDNS
|
|
||||||
} else if magicDNS {
|
|
||||||
log.Warn().
|
|
||||||
Msg("Warning: dns_config.magic_dns is set, but no nameservers are configured. Ignoring magic_dns.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var baseDomain string
|
|
||||||
if viper.IsSet("dns_config.base_domain") {
|
|
||||||
baseDomain = viper.GetString("dns_config.base_domain")
|
|
||||||
} else {
|
|
||||||
baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
return dnsConfig, baseDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetHeadscaleConfig() headscale.Config {
|
|
||||||
dnsConfig, baseDomain := GetDNSConfig()
|
|
||||||
derpConfig := GetDERPConfig()
|
|
||||||
logConfig := GetLogTailConfig()
|
|
||||||
|
|
||||||
configuredPrefixes := viper.GetStringSlice("ip_prefixes")
|
|
||||||
parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1)
|
|
||||||
|
|
||||||
legacyPrefixField := viper.GetString("ip_prefix")
|
|
||||||
if len(legacyPrefixField) > 0 {
|
|
||||||
log.
|
|
||||||
Warn().
|
|
||||||
Msgf(
|
|
||||||
"%s, %s",
|
|
||||||
"use of 'ip_prefix' for configuration is deprecated",
|
|
||||||
"please see 'ip_prefixes' in the shipped example.",
|
|
||||||
)
|
|
||||||
legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("failed to parse ip_prefix: %w", err))
|
|
||||||
}
|
|
||||||
parsedPrefixes = append(parsedPrefixes, legacyPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, prefixInConfig := range configuredPrefixes {
|
|
||||||
prefix, err := netaddr.ParseIPPrefix(prefixInConfig)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
|
|
||||||
}
|
|
||||||
parsedPrefixes = append(parsedPrefixes, prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
prefixes := make([]netaddr.IPPrefix, 0, len(parsedPrefixes))
|
|
||||||
{
|
|
||||||
// dedup
|
|
||||||
normalizedPrefixes := make(map[string]int, len(parsedPrefixes))
|
|
||||||
for i, p := range parsedPrefixes {
|
|
||||||
normalized, _ := p.Range().Prefix()
|
|
||||||
normalizedPrefixes[normalized.String()] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert back to list
|
|
||||||
for _, i := range normalizedPrefixes {
|
|
||||||
prefixes = append(prefixes, parsedPrefixes[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(prefixes) < 1 {
|
|
||||||
prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10"))
|
|
||||||
log.Warn().
|
|
||||||
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsClientAuthMode, _ := headscale.LookupTLSClientAuthMode(
|
|
||||||
viper.GetString("tls_client_auth_mode"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return headscale.Config{
|
|
||||||
ServerURL: viper.GetString("server_url"),
|
|
||||||
Addr: viper.GetString("listen_addr"),
|
|
||||||
MetricsAddr: viper.GetString("metrics_listen_addr"),
|
|
||||||
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
|
||||||
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
|
|
||||||
|
|
||||||
IPPrefixes: prefixes,
|
|
||||||
PrivateKeyPath: headscale.AbsolutePathFromConfigPath(viper.GetString("private_key_path")),
|
|
||||||
BaseDomain: baseDomain,
|
|
||||||
|
|
||||||
DERP: derpConfig,
|
|
||||||
|
|
||||||
EphemeralNodeInactivityTimeout: viper.GetDuration(
|
|
||||||
"ephemeral_node_inactivity_timeout",
|
|
||||||
),
|
|
||||||
|
|
||||||
DBtype: viper.GetString("db_type"),
|
|
||||||
DBpath: headscale.AbsolutePathFromConfigPath(viper.GetString("db_path")),
|
|
||||||
DBhost: viper.GetString("db_host"),
|
|
||||||
DBport: viper.GetInt("db_port"),
|
|
||||||
DBname: viper.GetString("db_name"),
|
|
||||||
DBuser: viper.GetString("db_user"),
|
|
||||||
DBpass: viper.GetString("db_pass"),
|
|
||||||
|
|
||||||
TLSLetsEncryptHostname: viper.GetString("tls_letsencrypt_hostname"),
|
|
||||||
TLSLetsEncryptListen: viper.GetString("tls_letsencrypt_listen"),
|
|
||||||
TLSLetsEncryptCacheDir: headscale.AbsolutePathFromConfigPath(
|
|
||||||
viper.GetString("tls_letsencrypt_cache_dir"),
|
|
||||||
),
|
|
||||||
TLSLetsEncryptChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
|
|
||||||
|
|
||||||
TLSCertPath: headscale.AbsolutePathFromConfigPath(viper.GetString("tls_cert_path")),
|
|
||||||
TLSKeyPath: headscale.AbsolutePathFromConfigPath(viper.GetString("tls_key_path")),
|
|
||||||
TLSClientAuthMode: tlsClientAuthMode,
|
|
||||||
|
|
||||||
DNSConfig: dnsConfig,
|
|
||||||
|
|
||||||
ACMEEmail: viper.GetString("acme_email"),
|
|
||||||
ACMEURL: viper.GetString("acme_url"),
|
|
||||||
|
|
||||||
UnixSocket: viper.GetString("unix_socket"),
|
|
||||||
UnixSocketPermission: GetFileMode("unix_socket_permission"),
|
|
||||||
|
|
||||||
OIDC: headscale.OIDCConfig{
|
|
||||||
Issuer: viper.GetString("oidc.issuer"),
|
|
||||||
ClientID: viper.GetString("oidc.client_id"),
|
|
||||||
ClientSecret: viper.GetString("oidc.client_secret"),
|
|
||||||
Scope: viper.GetStringSlice("oidc.scope"),
|
|
||||||
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
|
|
||||||
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
|
|
||||||
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
|
|
||||||
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
|
||||||
},
|
|
||||||
|
|
||||||
LogTail: logConfig,
|
|
||||||
|
|
||||||
CLI: headscale.CLIConfig{
|
|
||||||
Address: viper.GetString("cli.address"),
|
|
||||||
APIKey: viper.GetString("cli.api_key"),
|
|
||||||
Timeout: viper.GetDuration("cli.timeout"),
|
|
||||||
Insecure: viper.GetBool("cli.insecure"),
|
|
||||||
},
|
|
||||||
|
|
||||||
ACL: GetACLConfig(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHeadscaleApp() (*headscale.Headscale, error) {
|
func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||||
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
|
cfg, err := headscale.GetHeadscaleConfig()
|
||||||
// to avoid races
|
if err != nil {
|
||||||
minInactivityTimeout, _ := time.ParseDuration("65s")
|
return nil, fmt.Errorf("failed to load configuration while creating headscale instance: %w", err)
|
||||||
if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout {
|
|
||||||
// TODO: Find a better way to return this text
|
|
||||||
//nolint
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s",
|
|
||||||
viper.GetString("ephemeral_node_inactivity_timeout"),
|
|
||||||
minInactivityTimeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := GetHeadscaleConfig()
|
|
||||||
|
|
||||||
app, err := headscale.NewHeadscale(cfg)
|
app, err := headscale.NewHeadscale(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -436,7 +49,13 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
|
func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
|
||||||
cfg := GetHeadscaleConfig()
|
cfg, err := headscale.GetHeadscaleConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().
|
||||||
|
Err(err).
|
||||||
|
Caller().
|
||||||
|
Msgf("Failed to load configuration")
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Dur("timeout", cfg.CLI.Timeout).
|
Dur("timeout", cfg.CLI.Timeout).
|
||||||
@@ -570,17 +189,6 @@ func (tokenAuth) RequireTransportSecurity() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFileMode(key string) fs.FileMode {
|
|
||||||
modeStr := viper.GetString(key)
|
|
||||||
|
|
||||||
mode, err := strconv.ParseUint(modeStr, headscale.Base8, headscale.BitSize64)
|
|
||||||
if err != nil {
|
|
||||||
return PermissionFallback
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs.FileMode(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains[T string](ts []T, t T) bool {
|
func contains[T string](ts []T, t T) bool {
|
||||||
for _, v := range ts {
|
for _, v := range ts {
|
||||||
if reflect.DeepEqual(v, t) {
|
if reflect.DeepEqual(v, t) {
|
||||||
|
@@ -1,17 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/efekarakus/termcolor"
|
"github.com/efekarakus/termcolor"
|
||||||
"github.com/juanfont/headscale/cmd/headscale/cli"
|
"github.com/juanfont/headscale/cmd/headscale/cli"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/tcnksm/go-latest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -43,44 +39,5 @@ func main() {
|
|||||||
NoColor: !colors,
|
NoColor: !colors,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := cli.LoadConfig(""); err != nil {
|
|
||||||
log.Fatal().Caller().Err(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
machineOutput := cli.HasMachineOutputFlag()
|
|
||||||
|
|
||||||
logLevel := viper.GetString("log_level")
|
|
||||||
level, err := zerolog.ParseLevel(logLevel)
|
|
||||||
if err != nil {
|
|
||||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
||||||
} else {
|
|
||||||
zerolog.SetGlobalLevel(level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user has requested a "machine" readable format,
|
|
||||||
// then disable login so the output remains valid.
|
|
||||||
if machineOutput {
|
|
||||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viper.GetBool("disable_check_updates") && !machineOutput {
|
|
||||||
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
|
||||||
cli.Version != "dev" {
|
|
||||||
githubTag := &latest.GithubTag{
|
|
||||||
Owner: "juanfont",
|
|
||||||
Repository: "headscale",
|
|
||||||
}
|
|
||||||
res, err := latest.Check(githubTag, cli.Version)
|
|
||||||
if err == nil && res.Outdated {
|
|
||||||
//nolint
|
|
||||||
fmt.Printf(
|
|
||||||
"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n",
|
|
||||||
res.Current,
|
|
||||||
cli.Version,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cli.Execute()
|
cli.Execute()
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/cmd/headscale/cli"
|
"github.com/juanfont/headscale"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
)
|
)
|
||||||
@@ -27,6 +27,51 @@ func (s *Suite) SetUpSuite(c *check.C) {
|
|||||||
func (s *Suite) TearDownSuite(c *check.C) {
|
func (s *Suite) TearDownSuite(c *check.C) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*Suite) TestConfigFileLoading(c *check.C) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
path, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile := filepath.Join(tmpDir, "config.yaml")
|
||||||
|
|
||||||
|
// Symlink the example config file
|
||||||
|
err = os.Symlink(
|
||||||
|
filepath.Clean(path+"/../../config-example.yaml"),
|
||||||
|
cfgFile,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load example config, it should load without validation errors
|
||||||
|
err = headscale.LoadConfig(cfgFile, true)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
// Test that config file was interpreted correctly
|
||||||
|
c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080")
|
||||||
|
c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080")
|
||||||
|
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
|
||||||
|
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
|
||||||
|
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
|
||||||
|
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
|
||||||
|
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
|
||||||
|
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
|
||||||
|
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
||||||
|
c.Assert(
|
||||||
|
headscale.GetFileMode("unix_socket_permission"),
|
||||||
|
check.Equals,
|
||||||
|
fs.FileMode(0o770),
|
||||||
|
)
|
||||||
|
c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false)
|
||||||
|
}
|
||||||
|
|
||||||
func (*Suite) TestConfigLoading(c *check.C) {
|
func (*Suite) TestConfigLoading(c *check.C) {
|
||||||
tmpDir, err := ioutil.TempDir("", "headscale")
|
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -49,7 +94,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load example config, it should load without validation errors
|
// Load example config, it should load without validation errors
|
||||||
err = cli.LoadConfig(tmpDir)
|
err = headscale.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
// Test that config file was interpreted correctly
|
// Test that config file was interpreted correctly
|
||||||
@@ -63,11 +108,12 @@ func (*Suite) TestConfigLoading(c *check.C) {
|
|||||||
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
|
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
|
||||||
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
||||||
c.Assert(
|
c.Assert(
|
||||||
cli.GetFileMode("unix_socket_permission"),
|
headscale.GetFileMode("unix_socket_permission"),
|
||||||
check.Equals,
|
check.Equals,
|
||||||
fs.FileMode(0o770),
|
fs.FileMode(0o770),
|
||||||
)
|
)
|
||||||
c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false)
|
c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false)
|
||||||
|
c.Assert(viper.GetBool("randomize_client_port"), check.Equals, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Suite) TestDNSConfigLoading(c *check.C) {
|
func (*Suite) TestDNSConfigLoading(c *check.C) {
|
||||||
@@ -92,10 +138,10 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load example config, it should load without validation errors
|
// Load example config, it should load without validation errors
|
||||||
err = cli.LoadConfig(tmpDir)
|
err = headscale.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
dnsConfig, baseDomain := cli.GetDNSConfig()
|
dnsConfig, baseDomain := headscale.GetDNSConfig()
|
||||||
|
|
||||||
c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1")
|
c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1")
|
||||||
c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1")
|
c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1")
|
||||||
@@ -125,7 +171,7 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
|
|||||||
writeConfig(c, tmpDir, configYaml)
|
writeConfig(c, tmpDir, configYaml)
|
||||||
|
|
||||||
// Check configuration validation errors (1)
|
// Check configuration validation errors (1)
|
||||||
err = cli.LoadConfig(tmpDir)
|
err = headscale.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
// check.Matches can not handle multiline strings
|
// check.Matches can not handle multiline strings
|
||||||
tmp := strings.ReplaceAll(err.Error(), "\n", "***")
|
tmp := strings.ReplaceAll(err.Error(), "\n", "***")
|
||||||
@@ -150,6 +196,6 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
|
|||||||
"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
|
"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
|
||||||
)
|
)
|
||||||
writeConfig(c, tmpDir, configYaml)
|
writeConfig(c, tmpDir, configYaml)
|
||||||
err = cli.LoadConfig(tmpDir)
|
err = headscale.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
}
|
}
|
||||||
|
@@ -244,3 +244,8 @@ logtail:
|
|||||||
# As there is currently no support for overriding the log server in headscale, this is
|
# As there is currently no support for overriding the log server in headscale, this is
|
||||||
# disabled by default. Enabling this will make your clients send logs to Tailscale Inc.
|
# disabled by default. Enabling this will make your clients send logs to Tailscale Inc.
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
# Enabling this option makes devices prefer a random port for WireGuard traffic over the
|
||||||
|
# default static port 41641. This option is intended as a workaround for some buggy
|
||||||
|
# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
|
||||||
|
randomize_client_port: false
|
||||||
|
522
config.go
Normal file
522
config.go
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/dnstype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains the initial Headscale configuration.
|
||||||
|
type Config struct {
|
||||||
|
ServerURL string
|
||||||
|
Addr string
|
||||||
|
MetricsAddr string
|
||||||
|
GRPCAddr string
|
||||||
|
GRPCAllowInsecure bool
|
||||||
|
EphemeralNodeInactivityTimeout time.Duration
|
||||||
|
IPPrefixes []netaddr.IPPrefix
|
||||||
|
PrivateKeyPath string
|
||||||
|
BaseDomain string
|
||||||
|
LogLevel zerolog.Level
|
||||||
|
DisableUpdateCheck bool
|
||||||
|
|
||||||
|
DERP DERPConfig
|
||||||
|
|
||||||
|
DBtype string
|
||||||
|
DBpath string
|
||||||
|
DBhost string
|
||||||
|
DBport int
|
||||||
|
DBname string
|
||||||
|
DBuser string
|
||||||
|
DBpass string
|
||||||
|
|
||||||
|
TLS TLSConfig
|
||||||
|
|
||||||
|
ACMEURL string
|
||||||
|
ACMEEmail string
|
||||||
|
|
||||||
|
DNSConfig *tailcfg.DNSConfig
|
||||||
|
|
||||||
|
UnixSocket string
|
||||||
|
UnixSocketPermission fs.FileMode
|
||||||
|
|
||||||
|
OIDC OIDCConfig
|
||||||
|
|
||||||
|
LogTail LogTailConfig
|
||||||
|
RandomizeClientPort bool
|
||||||
|
|
||||||
|
CLI CLIConfig
|
||||||
|
|
||||||
|
ACL ACLConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type TLSConfig struct {
|
||||||
|
CertPath string
|
||||||
|
KeyPath string
|
||||||
|
ClientAuthMode tls.ClientAuthType
|
||||||
|
|
||||||
|
LetsEncrypt LetsEncryptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetsEncryptConfig struct {
|
||||||
|
Listen string
|
||||||
|
Hostname string
|
||||||
|
CacheDir string
|
||||||
|
ChallengeType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCConfig struct {
|
||||||
|
Issuer string
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
Scope []string
|
||||||
|
ExtraParams map[string]string
|
||||||
|
AllowedDomains []string
|
||||||
|
AllowedUsers []string
|
||||||
|
StripEmaildomain bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type DERPConfig struct {
|
||||||
|
ServerEnabled bool
|
||||||
|
ServerRegionID int
|
||||||
|
ServerRegionCode string
|
||||||
|
ServerRegionName string
|
||||||
|
STUNAddr string
|
||||||
|
URLs []url.URL
|
||||||
|
Paths []string
|
||||||
|
AutoUpdate bool
|
||||||
|
UpdateFrequency time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogTailConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type CLIConfig struct {
|
||||||
|
Address string
|
||||||
|
APIKey string
|
||||||
|
Timeout time.Duration
|
||||||
|
Insecure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ACLConfig struct {
|
||||||
|
PolicyPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(path string, isFile bool) error {
|
||||||
|
if isFile {
|
||||||
|
viper.SetConfigFile(path)
|
||||||
|
} else {
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
if path == "" {
|
||||||
|
viper.AddConfigPath("/etc/headscale/")
|
||||||
|
viper.AddConfigPath("$HOME/.headscale")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
} else {
|
||||||
|
// For testing
|
||||||
|
viper.AddConfigPath(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetEnvPrefix("headscale")
|
||||||
|
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
|
||||||
|
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
|
||||||
|
viper.SetDefault("tls_client_auth_mode", "relaxed")
|
||||||
|
|
||||||
|
viper.SetDefault("log_level", "info")
|
||||||
|
|
||||||
|
viper.SetDefault("dns_config", nil)
|
||||||
|
|
||||||
|
viper.SetDefault("derp.server.enabled", false)
|
||||||
|
viper.SetDefault("derp.server.stun.enabled", true)
|
||||||
|
|
||||||
|
viper.SetDefault("unix_socket", "/var/run/headscale.sock")
|
||||||
|
viper.SetDefault("unix_socket_permission", "0o770")
|
||||||
|
|
||||||
|
viper.SetDefault("grpc_listen_addr", ":50443")
|
||||||
|
viper.SetDefault("grpc_allow_insecure", false)
|
||||||
|
|
||||||
|
viper.SetDefault("cli.timeout", "5s")
|
||||||
|
viper.SetDefault("cli.insecure", false)
|
||||||
|
|
||||||
|
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
|
||||||
|
viper.SetDefault("oidc.strip_email_domain", true)
|
||||||
|
|
||||||
|
viper.SetDefault("logtail.enabled", false)
|
||||||
|
viper.SetDefault("randomize_client_port", false)
|
||||||
|
|
||||||
|
viper.SetDefault("ephemeral_node_inactivity_timeout", "120s")
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to read configuration from disk")
|
||||||
|
|
||||||
|
return fmt.Errorf("fatal error reading config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect any validation errors and return them all at once
|
||||||
|
var errorText string
|
||||||
|
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
|
||||||
|
((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) {
|
||||||
|
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
|
||||||
|
(viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") &&
|
||||||
|
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
|
||||||
|
// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
|
||||||
|
log.Warn().
|
||||||
|
Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") &&
|
||||||
|
(viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") {
|
||||||
|
errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(viper.GetString("server_url"), "http://") &&
|
||||||
|
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
|
||||||
|
errorText += "Fatal config error: server_url must start with https:// or http://\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, authModeValid := LookupTLSClientAuthMode(
|
||||||
|
viper.GetString("tls_client_auth_mode"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if !authModeValid {
|
||||||
|
errorText += fmt.Sprintf(
|
||||||
|
"Invalid tls_client_auth_mode supplied: %s. Accepted values: %s, %s, %s.",
|
||||||
|
viper.GetString("tls_client_auth_mode"),
|
||||||
|
DisabledClientAuth,
|
||||||
|
RelaxedClientAuth,
|
||||||
|
EnforcedClientAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
|
||||||
|
// to avoid races
|
||||||
|
minInactivityTimeout, _ := time.ParseDuration("65s")
|
||||||
|
if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout {
|
||||||
|
errorText += fmt.Sprintf(
|
||||||
|
"Fatal config error: ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s",
|
||||||
|
viper.GetString("ephemeral_node_inactivity_timeout"),
|
||||||
|
minInactivityTimeout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorText != "" {
|
||||||
|
//nolint
|
||||||
|
return errors.New(strings.TrimSuffix(errorText, "\n"))
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTLSConfig() TLSConfig {
|
||||||
|
tlsClientAuthMode, _ := LookupTLSClientAuthMode(
|
||||||
|
viper.GetString("tls_client_auth_mode"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return TLSConfig{
|
||||||
|
LetsEncrypt: LetsEncryptConfig{
|
||||||
|
Hostname: viper.GetString("tls_letsencrypt_hostname"),
|
||||||
|
Listen: viper.GetString("tls_letsencrypt_listen"),
|
||||||
|
CacheDir: AbsolutePathFromConfigPath(
|
||||||
|
viper.GetString("tls_letsencrypt_cache_dir"),
|
||||||
|
),
|
||||||
|
ChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
|
||||||
|
},
|
||||||
|
CertPath: AbsolutePathFromConfigPath(
|
||||||
|
viper.GetString("tls_cert_path"),
|
||||||
|
),
|
||||||
|
KeyPath: AbsolutePathFromConfigPath(
|
||||||
|
viper.GetString("tls_key_path"),
|
||||||
|
),
|
||||||
|
ClientAuthMode: tlsClientAuthMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDERPConfig() DERPConfig {
|
||||||
|
serverEnabled := viper.GetBool("derp.server.enabled")
|
||||||
|
serverRegionID := viper.GetInt("derp.server.region_id")
|
||||||
|
serverRegionCode := viper.GetString("derp.server.region_code")
|
||||||
|
serverRegionName := viper.GetString("derp.server.region_name")
|
||||||
|
stunAddr := viper.GetString("derp.server.stun_listen_addr")
|
||||||
|
|
||||||
|
if serverEnabled && stunAddr == "" {
|
||||||
|
log.Fatal().
|
||||||
|
Msg("derp.server.stun_listen_addr must be set if derp.server.enabled is true")
|
||||||
|
}
|
||||||
|
|
||||||
|
urlStrs := viper.GetStringSlice("derp.urls")
|
||||||
|
|
||||||
|
urls := make([]url.URL, len(urlStrs))
|
||||||
|
for index, urlStr := range urlStrs {
|
||||||
|
urlAddr, err := url.Parse(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Str("url", urlStr).
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to parse url, ignoring...")
|
||||||
|
}
|
||||||
|
|
||||||
|
urls[index] = *urlAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := viper.GetStringSlice("derp.paths")
|
||||||
|
|
||||||
|
autoUpdate := viper.GetBool("derp.auto_update_enabled")
|
||||||
|
updateFrequency := viper.GetDuration("derp.update_frequency")
|
||||||
|
|
||||||
|
return DERPConfig{
|
||||||
|
ServerEnabled: serverEnabled,
|
||||||
|
ServerRegionID: serverRegionID,
|
||||||
|
ServerRegionCode: serverRegionCode,
|
||||||
|
ServerRegionName: serverRegionName,
|
||||||
|
STUNAddr: stunAddr,
|
||||||
|
URLs: urls,
|
||||||
|
Paths: paths,
|
||||||
|
AutoUpdate: autoUpdate,
|
||||||
|
UpdateFrequency: updateFrequency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogTailConfig() LogTailConfig {
|
||||||
|
enabled := viper.GetBool("logtail.enabled")
|
||||||
|
|
||||||
|
return LogTailConfig{
|
||||||
|
Enabled: enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetACLConfig() ACLConfig {
|
||||||
|
policyPath := viper.GetString("acl_policy_path")
|
||||||
|
|
||||||
|
return ACLConfig{
|
||||||
|
PolicyPath: policyPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
||||||
|
if viper.IsSet("dns_config") {
|
||||||
|
dnsConfig := &tailcfg.DNSConfig{}
|
||||||
|
|
||||||
|
if viper.IsSet("dns_config.nameservers") {
|
||||||
|
nameserversStr := viper.GetStringSlice("dns_config.nameservers")
|
||||||
|
|
||||||
|
nameservers := make([]netaddr.IP, len(nameserversStr))
|
||||||
|
resolvers := make([]*dnstype.Resolver, len(nameserversStr))
|
||||||
|
|
||||||
|
for index, nameserverStr := range nameserversStr {
|
||||||
|
nameserver, err := netaddr.ParseIP(nameserverStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Str("func", "getDNSConfig").
|
||||||
|
Err(err).
|
||||||
|
Msgf("Could not parse nameserver IP: %s", nameserverStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameservers[index] = nameserver
|
||||||
|
resolvers[index] = &dnstype.Resolver{
|
||||||
|
Addr: nameserver.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsConfig.Nameservers = nameservers
|
||||||
|
dnsConfig.Resolvers = resolvers
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.IsSet("dns_config.restricted_nameservers") {
|
||||||
|
if len(dnsConfig.Nameservers) > 0 {
|
||||||
|
dnsConfig.Routes = make(map[string][]*dnstype.Resolver)
|
||||||
|
restrictedDNS := viper.GetStringMapStringSlice(
|
||||||
|
"dns_config.restricted_nameservers",
|
||||||
|
)
|
||||||
|
for domain, restrictedNameservers := range restrictedDNS {
|
||||||
|
restrictedResolvers := make(
|
||||||
|
[]*dnstype.Resolver,
|
||||||
|
len(restrictedNameservers),
|
||||||
|
)
|
||||||
|
for index, nameserverStr := range restrictedNameservers {
|
||||||
|
nameserver, err := netaddr.ParseIP(nameserverStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Str("func", "getDNSConfig").
|
||||||
|
Err(err).
|
||||||
|
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
|
||||||
|
}
|
||||||
|
restrictedResolvers[index] = &dnstype.Resolver{
|
||||||
|
Addr: nameserver.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dnsConfig.Routes[domain] = restrictedResolvers
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warn().
|
||||||
|
Msg("Warning: dns_config.restricted_nameservers is set, but no nameservers are configured. Ignoring restricted_nameservers.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.IsSet("dns_config.domains") {
|
||||||
|
dnsConfig.Domains = viper.GetStringSlice("dns_config.domains")
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.IsSet("dns_config.magic_dns") {
|
||||||
|
magicDNS := viper.GetBool("dns_config.magic_dns")
|
||||||
|
if len(dnsConfig.Nameservers) > 0 {
|
||||||
|
dnsConfig.Proxied = magicDNS
|
||||||
|
} else if magicDNS {
|
||||||
|
log.Warn().
|
||||||
|
Msg("Warning: dns_config.magic_dns is set, but no nameservers are configured. Ignoring magic_dns.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseDomain string
|
||||||
|
if viper.IsSet("dns_config.base_domain") {
|
||||||
|
baseDomain = viper.GetString("dns_config.base_domain")
|
||||||
|
} else {
|
||||||
|
baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return dnsConfig, baseDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHeadscaleConfig() (*Config, error) {
|
||||||
|
dnsConfig, baseDomain := GetDNSConfig()
|
||||||
|
derpConfig := GetDERPConfig()
|
||||||
|
logConfig := GetLogTailConfig()
|
||||||
|
randomizeClientPort := viper.GetBool("randomize_client_port")
|
||||||
|
|
||||||
|
configuredPrefixes := viper.GetStringSlice("ip_prefixes")
|
||||||
|
parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1)
|
||||||
|
|
||||||
|
logLevelStr := viper.GetString("log_level")
|
||||||
|
logLevel, err := zerolog.ParseLevel(logLevelStr)
|
||||||
|
if err != nil {
|
||||||
|
logLevel = zerolog.DebugLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyPrefixField := viper.GetString("ip_prefix")
|
||||||
|
if len(legacyPrefixField) > 0 {
|
||||||
|
log.
|
||||||
|
Warn().
|
||||||
|
Msgf(
|
||||||
|
"%s, %s",
|
||||||
|
"use of 'ip_prefix' for configuration is deprecated",
|
||||||
|
"please see 'ip_prefixes' in the shipped example.",
|
||||||
|
)
|
||||||
|
legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to parse ip_prefix: %w", err))
|
||||||
|
}
|
||||||
|
parsedPrefixes = append(parsedPrefixes, legacyPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, prefixInConfig := range configuredPrefixes {
|
||||||
|
prefix, err := netaddr.ParseIPPrefix(prefixInConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
|
||||||
|
}
|
||||||
|
parsedPrefixes = append(parsedPrefixes, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixes := make([]netaddr.IPPrefix, 0, len(parsedPrefixes))
|
||||||
|
{
|
||||||
|
// dedup
|
||||||
|
normalizedPrefixes := make(map[string]int, len(parsedPrefixes))
|
||||||
|
for i, p := range parsedPrefixes {
|
||||||
|
normalized, _ := p.Range().Prefix()
|
||||||
|
normalizedPrefixes[normalized.String()] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert back to list
|
||||||
|
for _, i := range normalizedPrefixes {
|
||||||
|
prefixes = append(prefixes, parsedPrefixes[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(prefixes) < 1 {
|
||||||
|
prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10"))
|
||||||
|
log.Warn().
|
||||||
|
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
ServerURL: viper.GetString("server_url"),
|
||||||
|
Addr: viper.GetString("listen_addr"),
|
||||||
|
MetricsAddr: viper.GetString("metrics_listen_addr"),
|
||||||
|
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
||||||
|
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
|
||||||
|
DisableUpdateCheck: viper.GetBool("disable_check_updates"),
|
||||||
|
LogLevel: logLevel,
|
||||||
|
|
||||||
|
IPPrefixes: prefixes,
|
||||||
|
PrivateKeyPath: AbsolutePathFromConfigPath(
|
||||||
|
viper.GetString("private_key_path"),
|
||||||
|
),
|
||||||
|
BaseDomain: baseDomain,
|
||||||
|
|
||||||
|
DERP: derpConfig,
|
||||||
|
|
||||||
|
EphemeralNodeInactivityTimeout: viper.GetDuration(
|
||||||
|
"ephemeral_node_inactivity_timeout",
|
||||||
|
),
|
||||||
|
|
||||||
|
DBtype: viper.GetString("db_type"),
|
||||||
|
DBpath: AbsolutePathFromConfigPath(viper.GetString("db_path")),
|
||||||
|
DBhost: viper.GetString("db_host"),
|
||||||
|
DBport: viper.GetInt("db_port"),
|
||||||
|
DBname: viper.GetString("db_name"),
|
||||||
|
DBuser: viper.GetString("db_user"),
|
||||||
|
DBpass: viper.GetString("db_pass"),
|
||||||
|
|
||||||
|
TLS: GetTLSConfig(),
|
||||||
|
|
||||||
|
DNSConfig: dnsConfig,
|
||||||
|
|
||||||
|
ACMEEmail: viper.GetString("acme_email"),
|
||||||
|
ACMEURL: viper.GetString("acme_url"),
|
||||||
|
|
||||||
|
UnixSocket: viper.GetString("unix_socket"),
|
||||||
|
UnixSocketPermission: GetFileMode("unix_socket_permission"),
|
||||||
|
|
||||||
|
OIDC: OIDCConfig{
|
||||||
|
Issuer: viper.GetString("oidc.issuer"),
|
||||||
|
ClientID: viper.GetString("oidc.client_id"),
|
||||||
|
ClientSecret: viper.GetString("oidc.client_secret"),
|
||||||
|
Scope: viper.GetStringSlice("oidc.scope"),
|
||||||
|
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
|
||||||
|
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
|
||||||
|
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
|
||||||
|
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
||||||
|
},
|
||||||
|
|
||||||
|
LogTail: logConfig,
|
||||||
|
RandomizeClientPort: randomizeClientPort,
|
||||||
|
|
||||||
|
CLI: CLIConfig{
|
||||||
|
Address: viper.GetString("cli.address"),
|
||||||
|
APIKey: viper.GetString("cli.api_key"),
|
||||||
|
Timeout: viper.GetDuration("cli.timeout"),
|
||||||
|
Insecure: viper.GetBool("cli.insecure"),
|
||||||
|
},
|
||||||
|
|
||||||
|
ACL: GetACLConfig(),
|
||||||
|
}, nil
|
||||||
|
}
|
11
derp.go
11
derp.go
@@ -152,16 +152,7 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
|
|||||||
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
||||||
}
|
}
|
||||||
|
|
||||||
namespaces, err := h.ListNamespaces()
|
h.setLastStateChangeToNow()
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to fetch namespaces")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, namespace := range namespaces {
|
|
||||||
h.setLastStateChangeToNow(namespace.Name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -223,7 +223,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
|||||||
|
|
||||||
baseDomain := "foobar.headscale.net"
|
baseDomain := "foobar.headscale.net"
|
||||||
dnsConfigOrig := tailcfg.DNSConfig{
|
dnsConfigOrig := tailcfg.DNSConfig{
|
||||||
Routes: make(map[string][]dnstype.Resolver),
|
Routes: make(map[string][]*dnstype.Resolver),
|
||||||
Domains: []string{baseDomain},
|
Domains: []string{baseDomain},
|
||||||
Proxied: true,
|
Proxied: true,
|
||||||
}
|
}
|
||||||
@@ -366,7 +366,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
|||||||
|
|
||||||
baseDomain := "foobar.headscale.net"
|
baseDomain := "foobar.headscale.net"
|
||||||
dnsConfigOrig := tailcfg.DNSConfig{
|
dnsConfigOrig := tailcfg.DNSConfig{
|
||||||
Routes: make(map[string][]dnstype.Resolver),
|
Routes: make(map[string][]*dnstype.Resolver),
|
||||||
Domains: []string{baseDomain},
|
Domains: []string{baseDomain},
|
||||||
Proxied: false,
|
Proxied: false,
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,7 @@ written by community members. It is _not_ verified by `headscale` developers.
|
|||||||
**It might be outdated and it might miss necessary steps**.
|
**It might be outdated and it might miss necessary steps**.
|
||||||
|
|
||||||
- [Running headscale in a container](running-headscale-container.md)
|
- [Running headscale in a container](running-headscale-container.md)
|
||||||
|
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
|
||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
|
|
||||||
|
60
docs/acls.md
60
docs/acls.md
@@ -33,7 +33,7 @@ Note: Namespaces will be created automatically when users authenticate with the
|
|||||||
Headscale server.
|
Headscale server.
|
||||||
|
|
||||||
ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
|
ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
|
||||||
or Yaml. Check the [test ACLs](../tests/acls) for further information.
|
or YAML. Check the [test ACLs](../tests/acls) for further information.
|
||||||
|
|
||||||
When registering the servers we will need to add the flag
|
When registering the servers we will need to add the flag
|
||||||
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
||||||
@@ -83,8 +83,8 @@ Here are the ACL's to implement the same permissions as above:
|
|||||||
// boss have access to all servers
|
// boss have access to all servers
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:boss"],
|
"src": ["group:boss"],
|
||||||
"ports": [
|
"dst": [
|
||||||
"tag:prod-databases:*",
|
"tag:prod-databases:*",
|
||||||
"tag:prod-app-servers:*",
|
"tag:prod-app-servers:*",
|
||||||
"tag:internal:*",
|
"tag:internal:*",
|
||||||
@@ -93,11 +93,12 @@ Here are the ACL's to implement the same permissions as above:
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// admin have only access to administrative ports of the servers
|
// admin have only access to administrative ports of the servers, in tcp/22
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:admin"],
|
"src": ["group:admin"],
|
||||||
"ports": [
|
"proto": "tcp",
|
||||||
|
"dst": [
|
||||||
"tag:prod-databases:22",
|
"tag:prod-databases:22",
|
||||||
"tag:prod-app-servers:22",
|
"tag:prod-app-servers:22",
|
||||||
"tag:internal:22",
|
"tag:internal:22",
|
||||||
@@ -106,12 +107,26 @@ Here are the ACL's to implement the same permissions as above:
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// we also allow admin to ping the servers
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["group:admin"],
|
||||||
|
"proto": "icmp",
|
||||||
|
"dst": [
|
||||||
|
"tag:prod-databases:*",
|
||||||
|
"tag:prod-app-servers:*",
|
||||||
|
"tag:internal:*",
|
||||||
|
"tag:dev-databases:*",
|
||||||
|
"tag:dev-app-servers:*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// developers have access to databases servers and application servers on all ports
|
// 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
|
// they can only view the applications servers in prod and have no access to databases servers in production
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:dev"],
|
"src": ["group:dev"],
|
||||||
"ports": [
|
"dst": [
|
||||||
"tag:dev-databases:*",
|
"tag:dev-databases:*",
|
||||||
"tag:dev-app-servers:*",
|
"tag:dev-app-servers:*",
|
||||||
"tag:prod-app-servers:80,443"
|
"tag:prod-app-servers:80,443"
|
||||||
@@ -124,37 +139,38 @@ Here are the ACL's to implement the same permissions as above:
|
|||||||
// https://github.com/juanfont/headscale/issues/502
|
// https://github.com/juanfont/headscale/issues/502
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:dev"],
|
"src": ["group:dev"],
|
||||||
"ports": ["10.20.0.0/16:443,5432", "router.internal:0"]
|
"dst": ["10.20.0.0/16:443,5432", "router.internal:0"]
|
||||||
},
|
},
|
||||||
|
|
||||||
// servers should be able to talk to database. Database should not be able to initiate connections to
|
// servers should be able to talk to database in tcp/5432. Database should not be able to initiate connections to
|
||||||
// applications servers
|
// applications servers
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["tag:dev-app-servers"],
|
"src": ["tag:dev-app-servers"],
|
||||||
"ports": ["tag:dev-databases:5432"]
|
"proto": "tcp",
|
||||||
|
"dst": ["tag:dev-databases:5432"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["tag:prod-app-servers"],
|
"src": ["tag:prod-app-servers"],
|
||||||
"ports": ["tag:prod-databases:5432"]
|
"dst": ["tag:prod-databases:5432"]
|
||||||
},
|
},
|
||||||
|
|
||||||
// interns have access to dev-app-servers only in reading mode
|
// interns have access to dev-app-servers only in reading mode
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:intern"],
|
"src": ["group:intern"],
|
||||||
"ports": ["tag:dev-app-servers:80,443"]
|
"dst": ["tag:dev-app-servers:80,443"]
|
||||||
},
|
},
|
||||||
|
|
||||||
// We still have to allow internal namespaces communications since nothing guarantees that each user have
|
// We still have to allow internal namespaces communications since nothing guarantees that each user have
|
||||||
// their own namespaces.
|
// their own namespaces.
|
||||||
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
|
{ "action": "accept", "src": ["boss"], "dst": ["boss:*"] },
|
||||||
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
|
{ "action": "accept", "src": ["dev1"], "dst": ["dev1:*"] },
|
||||||
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
|
{ "action": "accept", "src": ["dev2"], "dst": ["dev2:*"] },
|
||||||
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
|
{ "action": "accept", "src": ["admin1"], "dst": ["admin1:*"] },
|
||||||
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
|
{ "action": "accept", "src": ["intern1"], "dst": ["intern1:*"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
206
docs/running-headscale-openbsd.md
Normal file
206
docs/running-headscale-openbsd.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Running headscale on OpenBSD
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD 7.1.
|
||||||
|
In additional to the "get up and running section", there is an optional [rc.d section](#running-headscale-in-the-background-with-rcd)
|
||||||
|
describing how to make `headscale` run properly in a server environment.
|
||||||
|
|
||||||
|
## Install `headscale`
|
||||||
|
|
||||||
|
1. Install from ports (Not Recommend)
|
||||||
|
|
||||||
|
As of OpenBSD 7.1, there's a headscale in ports collection, however, it's severely outdated(v0.12.4).
|
||||||
|
You can install it via `pkg_add headscale`.
|
||||||
|
|
||||||
|
2. Install from source on OpenBSD 7.1
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Install prerequistes
|
||||||
|
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
|
||||||
|
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
||||||
|
pkg_add -D snap go
|
||||||
|
pkg_add gmake
|
||||||
|
|
||||||
|
git clone https://github.com/juanfont/headscale.git
|
||||||
|
|
||||||
|
cd headscale
|
||||||
|
|
||||||
|
# optionally checkout a release
|
||||||
|
# option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest
|
||||||
|
# option b. get latest tag, this may be a beta release
|
||||||
|
latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||||
|
|
||||||
|
git checkout $latestTag
|
||||||
|
|
||||||
|
gmake build
|
||||||
|
|
||||||
|
# make it executable
|
||||||
|
chmod a+x headscale
|
||||||
|
|
||||||
|
# copy it to /usr/local/sbin
|
||||||
|
cp headscale /usr/local/sbin
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install from source via cross compile
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Install prerequistes
|
||||||
|
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
|
||||||
|
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
||||||
|
|
||||||
|
git clone https://github.com/juanfont/headscale.git
|
||||||
|
|
||||||
|
cd headscale
|
||||||
|
|
||||||
|
# optionally checkout a release
|
||||||
|
# option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest
|
||||||
|
# option b. get latest tag, this may be a beta release
|
||||||
|
latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||||
|
|
||||||
|
git checkout $latestTag
|
||||||
|
|
||||||
|
make build GOOS=openbsd
|
||||||
|
|
||||||
|
# copy headscale to openbsd machine and put it in /usr/local/sbin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure and run `headscale`
|
||||||
|
|
||||||
|
1. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Directory for configuration
|
||||||
|
|
||||||
|
mkdir -p /etc/headscale
|
||||||
|
|
||||||
|
# Directory for Database, and other variable data (like certificates)
|
||||||
|
mkdir -p /var/lib/headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create an empty SQLite database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /var/lib/headscale/db.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a `headscale` configuration:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /etc/headscale/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
It is **strongly recommended** to copy and modify the [example configuration](../config-example.yaml)
|
||||||
|
from the [headscale repository](../)
|
||||||
|
|
||||||
|
4. Start the headscale server:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale serve
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will start `headscale` in the current terminal session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To continue the tutorial, open a new terminal and let it run in the background.
|
||||||
|
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux).
|
||||||
|
|
||||||
|
To run `headscale` in the background, please follow the steps in the [rc.d section](#running-headscale-in-the-background-with-rcd) before continuing.
|
||||||
|
|
||||||
|
5. Verify `headscale` is running:
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale namespaces create myfirstnamespace
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register a machine (normal login)
|
||||||
|
|
||||||
|
On a client machine, execute the `tailscale` login command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server YOUR_HEADSCALE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
Register the machine:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register machine using a pre authenticated key
|
||||||
|
|
||||||
|
Generate a key using the command line:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --namespace myfirstnamespace preauthkeys create --reusable --expiration 24h
|
||||||
|
```
|
||||||
|
|
||||||
|
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running `headscale` in the background with rc.d
|
||||||
|
|
||||||
|
This section demonstrates how to run `headscale` as a service in the background with [rc.d](https://man.openbsd.org/rc.d).
|
||||||
|
|
||||||
|
1. Create a rc.d service at `/etc/rc.d/headscale` containing:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
#!/bin/ksh
|
||||||
|
|
||||||
|
daemon="/usr/local/sbin/headscale"
|
||||||
|
daemon_logger="daemon.info"
|
||||||
|
daemon_user="root"
|
||||||
|
daemon_flags="serve"
|
||||||
|
daemon_timeout=60
|
||||||
|
|
||||||
|
. /etc/rc.d/rc.subr
|
||||||
|
|
||||||
|
rc_bg=YES
|
||||||
|
rc_reload=NO
|
||||||
|
|
||||||
|
rc_cmd $1
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `/etc/rc.d/headscale` needs execute permission:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
chmod a+x /etc/rc.d/headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start `headscale` service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
rcctl start headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Make `headscale` service start at boot:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
rcctl enable headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Verify the headscale service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
rcctl check headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
`headscale` will now run in the background and start at boot.
|
12
flake.lock
generated
12
flake.lock
generated
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1644229661,
|
"lastModified": 1653893745,
|
||||||
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
|
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
|
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1653733789,
|
"lastModified": 1654847188,
|
||||||
"narHash": "sha256-VIYazYCWNvcFNns2XQkHx/mVmCZ3oebZv8W2LS1gLQE=",
|
"narHash": "sha256-MC+eP7XOGE1LAswOPqdcGoUqY9mEQ3ZaaxamVTbc0hM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d1086907f56c5a6c33c0c2e8dc9f42ef6988294f",
|
"rev": "8b66e3f2ebcc644b78cec9d6f152192f4e7d322f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
||||||
# update this if you have a mismatch after doing a change to thos files.
|
# update this if you have a mismatch after doing a change to thos files.
|
||||||
vendorSha256 = "sha256-b6qPOO/NmcXsAsSRWZlYXZKyRAF++DsL4TEZzRhQhME=";
|
vendorSha256 = "sha256-j/hI6vP92UmcexFfzCe5fkGE8QUdQdNajSxMGib175Q=";
|
||||||
|
|
||||||
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
|
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
|
||||||
};
|
};
|
||||||
|
30
go.mod
30
go.mod
@@ -13,22 +13,24 @@ require (
|
|||||||
github.com/gofrs/uuid v4.2.0+incompatible
|
github.com/gofrs/uuid v4.2.0+incompatible
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
|
||||||
github.com/klauspost/compress v1.15.1
|
github.com/klauspost/compress v1.15.4
|
||||||
github.com/ory/dockertest/v3 v3.8.1
|
github.com/ory/dockertest/v3 v3.8.1
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/philip-bui/grpc-zerolog v1.0.1
|
github.com/philip-bui/grpc-zerolog v1.0.1
|
||||||
github.com/prometheus/client_golang v1.12.1
|
github.com/prometheus/client_golang v1.12.1
|
||||||
|
github.com/prometheus/common v0.32.1
|
||||||
github.com/pterm/pterm v0.12.41
|
github.com/pterm/pterm v0.12.41
|
||||||
|
github.com/puzpuzpuz/xsync v1.2.1
|
||||||
github.com/rs/zerolog v1.26.1
|
github.com/rs/zerolog v1.26.1
|
||||||
github.com/spf13/cobra v1.4.0
|
github.com/spf13/cobra v1.4.0
|
||||||
github.com/spf13/viper v1.11.0
|
github.com/spf13/viper v1.11.0
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.7.1
|
||||||
github.com/tailscale/hujson v0.0.0-20220421170326-6583d0610064
|
github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17
|
||||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||||
github.com/zsais/go-gin-prometheus v0.1.0
|
github.com/zsais/go-gin-prometheus v0.1.0
|
||||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
|
||||||
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
|
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
|
||||||
google.golang.org/grpc v1.46.0
|
google.golang.org/grpc v1.46.0
|
||||||
google.golang.org/protobuf v1.28.0
|
google.golang.org/protobuf v1.28.0
|
||||||
@@ -38,12 +40,12 @@ require (
|
|||||||
gorm.io/driver/postgres v1.3.5
|
gorm.io/driver/postgres v1.3.5
|
||||||
gorm.io/gorm v1.23.4
|
gorm.io/gorm v1.23.4
|
||||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||||
tailscale.com v1.24.0
|
tailscale.com v1.26.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
github.com/akutz/memconn v0.1.0 // indirect
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||||
@@ -53,8 +55,8 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 // indirect
|
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/docker/cli v20.10.11+incompatible // indirect
|
github.com/docker/cli v20.10.16+incompatible // indirect
|
||||||
github.com/docker/docker v20.10.7+incompatible // indirect
|
github.com/docker/docker v20.10.16+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/docker/go-units v0.4.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||||
@@ -65,7 +67,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/go-cmp v0.5.7 // indirect
|
github.com/google/go-cmp v0.5.8 // indirect
|
||||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
@@ -105,17 +107,15 @@ require (
|
|||||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
|
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
|
||||||
github.com/opencontainers/runc v1.0.2 // indirect
|
github.com/opencontainers/runc v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
github.com/puzpuzpuz/xsync v1.2.1 // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
||||||
@@ -133,8 +133,8 @@ require (
|
|||||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||||
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect
|
golang.org/x/net v0.0.0-20220516155154-20f960328961 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
|
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect
|
||||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
|
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||||
|
@@ -1721,3 +1721,43 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
|
|||||||
|
|
||||||
assert.Equal(s.T(), machine.Namespace, oldNamespace)
|
assert.Equal(s.T(), machine.Namespace, oldNamespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
|
||||||
|
// TODO: make sure defaultConfig is not same as altConfig
|
||||||
|
defaultConfig, err := os.ReadFile("integration_test/etc/config.dump.gold.yaml")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
_, err = ExecuteCommand(
|
||||||
|
&s.headscale,
|
||||||
|
[]string{
|
||||||
|
"headscale",
|
||||||
|
"dumpConfig",
|
||||||
|
},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
defaultDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig))
|
||||||
|
|
||||||
|
_, err = ExecuteCommand(
|
||||||
|
&s.headscale,
|
||||||
|
[]string{
|
||||||
|
"headscale",
|
||||||
|
"-c",
|
||||||
|
"/etc/headscale/alt-config.yaml",
|
||||||
|
"dumpConfig",
|
||||||
|
},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
altDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
|
||||||
|
}
|
||||||
|
@@ -25,7 +25,8 @@ var (
|
|||||||
tailscaleVersions = []string{
|
tailscaleVersions = []string{
|
||||||
"head",
|
"head",
|
||||||
"unstable",
|
"unstable",
|
||||||
"1.24.0",
|
"1.26.0",
|
||||||
|
"1.24.2",
|
||||||
"1.22.2",
|
"1.22.2",
|
||||||
"1.20.4",
|
"1.20.4",
|
||||||
"1.18.2",
|
"1.18.2",
|
||||||
|
46
integration_test/etc/alt-config.dump.gold.yaml
Normal file
46
integration_test/etc/alt-config.dump.gold.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
acl_policy_path: ""
|
||||||
|
cli:
|
||||||
|
insecure: false
|
||||||
|
timeout: 5s
|
||||||
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
|
db_type: sqlite3
|
||||||
|
derp:
|
||||||
|
auto_update_enabled: false
|
||||||
|
server:
|
||||||
|
enabled: false
|
||||||
|
stun:
|
||||||
|
enabled: true
|
||||||
|
update_frequency: 1m
|
||||||
|
urls:
|
||||||
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
|
dns_config:
|
||||||
|
base_domain: headscale.net
|
||||||
|
domains: []
|
||||||
|
magic_dns: true
|
||||||
|
nameservers:
|
||||||
|
- 1.1.1.1
|
||||||
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
|
grpc_allow_insecure: false
|
||||||
|
grpc_listen_addr: :50443
|
||||||
|
ip_prefixes:
|
||||||
|
- fd7a:115c:a1e0::/48
|
||||||
|
- 100.64.0.0/10
|
||||||
|
listen_addr: 0.0.0.0:18080
|
||||||
|
log_level: disabled
|
||||||
|
logtail:
|
||||||
|
enabled: false
|
||||||
|
metrics_listen_addr: 127.0.0.1:19090
|
||||||
|
oidc:
|
||||||
|
scope:
|
||||||
|
- openid
|
||||||
|
- profile
|
||||||
|
- email
|
||||||
|
strip_email_domain: true
|
||||||
|
private_key_path: private.key
|
||||||
|
server_url: http://headscale:18080
|
||||||
|
tls_client_auth_mode: relaxed
|
||||||
|
tls_letsencrypt_cache_dir: /var/www/.cache
|
||||||
|
tls_letsencrypt_challenge_type: HTTP-01
|
||||||
|
unix_socket: /var/run/headscale.sock
|
||||||
|
unix_socket_permission: "0o770"
|
||||||
|
randomize_client_port: false
|
24
integration_test/etc/alt-config.yaml
Normal file
24
integration_test/etc/alt-config.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
log_level: trace
|
||||||
|
acl_policy_path: ""
|
||||||
|
db_type: sqlite3
|
||||||
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
|
ip_prefixes:
|
||||||
|
- fd7a:115c:a1e0::/48
|
||||||
|
- 100.64.0.0/10
|
||||||
|
dns_config:
|
||||||
|
base_domain: headscale.net
|
||||||
|
magic_dns: true
|
||||||
|
domains: []
|
||||||
|
nameservers:
|
||||||
|
- 1.1.1.1
|
||||||
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
|
private_key_path: private.key
|
||||||
|
listen_addr: 0.0.0.0:18080
|
||||||
|
metrics_listen_addr: 127.0.0.1:19090
|
||||||
|
server_url: http://headscale:18080
|
||||||
|
|
||||||
|
derp:
|
||||||
|
urls:
|
||||||
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
|
auto_update_enabled: false
|
||||||
|
update_frequency: 1m
|
46
integration_test/etc/config.dump.gold.yaml
Normal file
46
integration_test/etc/config.dump.gold.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
acl_policy_path: ""
|
||||||
|
cli:
|
||||||
|
insecure: false
|
||||||
|
timeout: 5s
|
||||||
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
|
db_type: sqlite3
|
||||||
|
derp:
|
||||||
|
auto_update_enabled: false
|
||||||
|
server:
|
||||||
|
enabled: false
|
||||||
|
stun:
|
||||||
|
enabled: true
|
||||||
|
update_frequency: 1m
|
||||||
|
urls:
|
||||||
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
|
dns_config:
|
||||||
|
base_domain: headscale.net
|
||||||
|
domains: []
|
||||||
|
magic_dns: true
|
||||||
|
nameservers:
|
||||||
|
- 1.1.1.1
|
||||||
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
|
grpc_allow_insecure: false
|
||||||
|
grpc_listen_addr: :50443
|
||||||
|
ip_prefixes:
|
||||||
|
- fd7a:115c:a1e0::/48
|
||||||
|
- 100.64.0.0/10
|
||||||
|
listen_addr: 0.0.0.0:8080
|
||||||
|
log_level: disabled
|
||||||
|
logtail:
|
||||||
|
enabled: false
|
||||||
|
metrics_listen_addr: 127.0.0.1:9090
|
||||||
|
oidc:
|
||||||
|
scope:
|
||||||
|
- openid
|
||||||
|
- profile
|
||||||
|
- email
|
||||||
|
strip_email_domain: true
|
||||||
|
private_key_path: private.key
|
||||||
|
server_url: http://headscale:8080
|
||||||
|
tls_client_auth_mode: relaxed
|
||||||
|
tls_letsencrypt_cache_dir: /var/www/.cache
|
||||||
|
tls_letsencrypt_challenge_type: HTTP-01
|
||||||
|
unix_socket: /var/run/headscale.sock
|
||||||
|
unix_socket_permission: "0o770"
|
||||||
|
randomize_client_port: false
|
@@ -188,8 +188,8 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
|
|||||||
Hosts: map[string]netaddr.IPPrefix{},
|
Hosts: map[string]netaddr.IPPrefix{},
|
||||||
TagOwners: map[string][]string{},
|
TagOwners: map[string][]string{},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"admin"}, Destinations: []string{"*:*"}},
|
||||||
{Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}},
|
{Action: "accept", Sources: []string{"test"}, Destinations: []string{"test:*"}},
|
||||||
},
|
},
|
||||||
Tests: []ACLTest{},
|
Tests: []ACLTest{},
|
||||||
}
|
}
|
||||||
@@ -821,7 +821,7 @@ func TestHeadscale_GenerateGivenName(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "simple machine name generation",
|
name: "simple machine name generation",
|
||||||
h: &Headscale{
|
h: &Headscale{
|
||||||
cfg: Config{
|
cfg: &Config{
|
||||||
OIDC: OIDCConfig{
|
OIDC: OIDCConfig{
|
||||||
StripEmaildomain: true,
|
StripEmaildomain: true,
|
||||||
},
|
},
|
||||||
@@ -836,7 +836,7 @@ func TestHeadscale_GenerateGivenName(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "machine name with 53 chars",
|
name: "machine name with 53 chars",
|
||||||
h: &Headscale{
|
h: &Headscale{
|
||||||
cfg: Config{
|
cfg: &Config{
|
||||||
OIDC: OIDCConfig{
|
OIDC: OIDCConfig{
|
||||||
StripEmaildomain: true,
|
StripEmaildomain: true,
|
||||||
},
|
},
|
||||||
@@ -851,7 +851,7 @@ func TestHeadscale_GenerateGivenName(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "machine name with 60 chars",
|
name: "machine name with 60 chars",
|
||||||
h: &Headscale{
|
h: &Headscale{
|
||||||
cfg: Config{
|
cfg: &Config{
|
||||||
OIDC: OIDCConfig{
|
OIDC: OIDCConfig{
|
||||||
StripEmaildomain: true,
|
StripEmaildomain: true,
|
||||||
},
|
},
|
||||||
@@ -866,7 +866,7 @@ func TestHeadscale_GenerateGivenName(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "machine name with 63 chars",
|
name: "machine name with 63 chars",
|
||||||
h: &Headscale{
|
h: &Headscale{
|
||||||
cfg: Config{
|
cfg: &Config{
|
||||||
OIDC: OIDCConfig{
|
OIDC: OIDCConfig{
|
||||||
StripEmaildomain: true,
|
StripEmaildomain: true,
|
||||||
},
|
},
|
||||||
@@ -881,7 +881,7 @@ func TestHeadscale_GenerateGivenName(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "machine name with 64 chars",
|
name: "machine name with 64 chars",
|
||||||
h: &Headscale{
|
h: &Headscale{
|
||||||
cfg: Config{
|
cfg: &Config{
|
||||||
OIDC: OIDCConfig{
|
OIDC: OIDCConfig{
|
||||||
StripEmaildomain: true,
|
StripEmaildomain: true,
|
||||||
},
|
},
|
||||||
@@ -896,7 +896,7 @@ func TestHeadscale_GenerateGivenName(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "machine name with 73 chars",
|
name: "machine name with 73 chars",
|
||||||
h: &Headscale{
|
h: &Headscale{
|
||||||
cfg: Config{
|
cfg: &Config{
|
||||||
OIDC: OIDCConfig{
|
OIDC: OIDCConfig{
|
||||||
StripEmaildomain: true,
|
StripEmaildomain: true,
|
||||||
},
|
},
|
||||||
|
@@ -148,6 +148,21 @@ func (h *Headscale) ListNamespaces() ([]Namespace, error) {
|
|||||||
return namespaces, nil
|
return namespaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) ListNamespacesStr() ([]string, error) {
|
||||||
|
namespaces, err := h.ListNamespaces()
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaceStrs := make([]string, len(namespaces))
|
||||||
|
|
||||||
|
for index, namespace := range namespaces {
|
||||||
|
namespaceStrs[index] = namespace.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespaceStrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListMachinesInNamespace gets all the nodes in a given namespace.
|
// ListMachinesInNamespace gets all the nodes in a given namespace.
|
||||||
func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
|
func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
|
||||||
err := CheckForFQDNRules(name)
|
err := CheckForFQDNRules(name)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
// Declare static groups of users beyond those in the identity service.
|
// Declare static groups of users beyond those in the identity service.
|
||||||
"Groups": {
|
"groups": {
|
||||||
"group:example": [
|
"group:example": [
|
||||||
"user1@example.com",
|
"user1@example.com",
|
||||||
"user2@example.com",
|
"user2@example.com",
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Declare hostname aliases to use in place of IP addresses or subnets.
|
// Declare hostname aliases to use in place of IP addresses or subnets.
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"example-host-1": "100.100.100.100",
|
"example-host-1": "100.100.100.100",
|
||||||
"example-host-2": "100.100.101.100/24",
|
"example-host-2": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
// Define who is allowed to use which tags.
|
// Define who is allowed to use which tags.
|
||||||
"TagOwners": {
|
"tagOwners": {
|
||||||
// Everyone in the montreal-admins or global-admins group are
|
// Everyone in the montreal-admins or global-admins group are
|
||||||
// allowed to tag servers as montreal-webserver.
|
// allowed to tag servers as montreal-webserver.
|
||||||
"tag:montreal-webserver": [
|
"tag:montreal-webserver": [
|
||||||
@@ -29,17 +29,17 @@
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Access control lists.
|
// Access control lists.
|
||||||
"ACLs": [
|
"acls": [
|
||||||
// Engineering users, plus the president, can access port 22 (ssh)
|
// Engineering users, plus the president, can access port 22 (ssh)
|
||||||
// and port 3389 (remote desktop protocol) on all servers, and all
|
// and port 3389 (remote desktop protocol) on all servers, and all
|
||||||
// ports on git-server or ci-server.
|
// ports on git-server or ci-server.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:example2",
|
"group:example2",
|
||||||
"192.168.1.0/24"
|
"192.168.1.0/24"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:22,3389",
|
"*:22,3389",
|
||||||
"git-server:*",
|
"git-server:*",
|
||||||
"ci-server:*"
|
"ci-server:*"
|
||||||
@@ -48,22 +48,22 @@
|
|||||||
// Allow engineer users to access any port on a device tagged with
|
// Allow engineer users to access any port on a device tagged with
|
||||||
// tag:production.
|
// tag:production.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:example"
|
"group:example"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:production:*"
|
"tag:production:*"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
||||||
// on both networks.
|
// on both networks.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"example-host-2",
|
"example-host-2",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"example-host-1:*",
|
"example-host-1:*",
|
||||||
"192.168.1.0/24:*"
|
"192.168.1.0/24:*"
|
||||||
],
|
],
|
||||||
@@ -72,22 +72,22 @@
|
|||||||
// Comment out this section if you want to define specific ACL
|
// Comment out this section if you want to define specific ACL
|
||||||
// restrictions above.
|
// restrictions above.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:*"
|
"*:*"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// All users in Montreal are allowed to access the Montreal web
|
// All users in Montreal are allowed to access the Montreal web
|
||||||
// servers.
|
// servers.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"example-host-1"
|
"example-host-1"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:montreal-webserver:80,443"
|
"tag:montreal-webserver:80,443"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -96,30 +96,30 @@
|
|||||||
// In contrast, this doesn't grant API servers the right to initiate
|
// In contrast, this doesn't grant API servers the right to initiate
|
||||||
// any connections.
|
// any connections.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"tag:montreal-webserver"
|
"tag:montreal-webserver"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:api-server:443"
|
"tag:api-server:443"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// Declare tests to check functionality of ACL rules
|
// Declare tests to check functionality of ACL rules
|
||||||
"Tests": [
|
"tests": [
|
||||||
{
|
{
|
||||||
"User": "user1@example.com",
|
"src": "user1@example.com",
|
||||||
"Allow": [
|
"accept": [
|
||||||
"example-host-1:22",
|
"example-host-1:22",
|
||||||
"example-host-2:80"
|
"example-host-2:80"
|
||||||
],
|
],
|
||||||
"Deny": [
|
"deny": [
|
||||||
"exapmle-host-2:100"
|
"exapmle-host-2:100"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"User": "user2@example.com",
|
"src": "user2@example.com",
|
||||||
"Allow": [
|
"accept": [
|
||||||
"100.60.3.4:22"
|
"100.60.3.4:22"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -3,19 +3,19 @@
|
|||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"subnet-1",
|
"subnet-1",
|
||||||
"192.168.1.0/24"
|
"192.168.1.0/24"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:22,3389",
|
"*:22,3389",
|
||||||
"host-1:*",
|
"host-1:*",
|
||||||
],
|
],
|
||||||
|
@@ -1,24 +1,24 @@
|
|||||||
// This ACL is used to test group expansion
|
// This ACL is used to test group expansion
|
||||||
|
|
||||||
{
|
{
|
||||||
"Groups": {
|
"groups": {
|
||||||
"group:example": [
|
"group:example": [
|
||||||
"testnamespace",
|
"testnamespace",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:example",
|
"group:example",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"host-1:*",
|
"host-1:*",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
// This ACL is used to test namespace expansion
|
// This ACL is used to test namespace expansion
|
||||||
|
|
||||||
{
|
{
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"testnamespace",
|
"testnamespace",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"host-1:*",
|
"host-1:*",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
41
tests/acls/acl_policy_basic_protocols.hujson
Normal file
41
tests/acls/acl_policy_basic_protocols.hujson
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// This ACL is used to test wildcards
|
||||||
|
|
||||||
|
{
|
||||||
|
"hosts": {
|
||||||
|
"host-1": "100.100.100.100",
|
||||||
|
"subnet-1": "100.100.101.100/24",
|
||||||
|
},
|
||||||
|
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"src": [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
"proto": "tcp",
|
||||||
|
"dst": [
|
||||||
|
"host-1:*",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"src": [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
"proto": "udp",
|
||||||
|
"dst": [
|
||||||
|
"host-1:53",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"src": [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
"proto": "icmp",
|
||||||
|
"dst": [
|
||||||
|
"host-1:*",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
@@ -1,18 +1,18 @@
|
|||||||
// This ACL is used to test the port range expansion
|
// This ACL is used to test the port range expansion
|
||||||
|
|
||||||
{
|
{
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"subnet-1",
|
"subnet-1",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"host-1:5400-5500",
|
"host-1:5400-5500",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
// This ACL is used to test wildcards
|
// This ACL is used to test wildcards
|
||||||
|
|
||||||
{
|
{
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"Action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"*",
|
"*",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"host-1:*",
|
"host-1:*",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
Hosts:
|
hosts:
|
||||||
host-1: 100.100.100.100/32
|
host-1: 100.100.100.100/32
|
||||||
subnet-1: 100.100.101.100/24
|
subnet-1: 100.100.101.100/24
|
||||||
ACLs:
|
acls:
|
||||||
- Action: accept
|
- action: accept
|
||||||
Users:
|
src:
|
||||||
- "*"
|
- "*"
|
||||||
Ports:
|
dst:
|
||||||
- host-1:*
|
- host-1:*
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
// Declare static groups of users beyond those in the identity service.
|
// Declare static groups of users beyond those in the identity service.
|
||||||
"Groups": {
|
"groups": {
|
||||||
"group:example": [
|
"group:example": [
|
||||||
"user1@example.com",
|
"user1@example.com",
|
||||||
"user2@example.com",
|
"user2@example.com",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Declare hostname aliases to use in place of IP addresses or subnets.
|
// Declare hostname aliases to use in place of IP addresses or subnets.
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"example-host-1": "100.100.100.100",
|
"example-host-1": "100.100.100.100",
|
||||||
"example-host-2": "100.100.101.100/24",
|
"example-host-2": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
// Define who is allowed to use which tags.
|
// Define who is allowed to use which tags.
|
||||||
"TagOwners": {
|
"tagOwners": {
|
||||||
// Everyone in the montreal-admins or global-admins group are
|
// Everyone in the montreal-admins or global-admins group are
|
||||||
// allowed to tag servers as montreal-webserver.
|
// allowed to tag servers as montreal-webserver.
|
||||||
"tag:montreal-webserver": [
|
"tag:montreal-webserver": [
|
||||||
@@ -26,17 +26,17 @@
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Access control lists.
|
// Access control lists.
|
||||||
"ACLs": [
|
"acls": [
|
||||||
// Engineering users, plus the president, can access port 22 (ssh)
|
// Engineering users, plus the president, can access port 22 (ssh)
|
||||||
// and port 3389 (remote desktop protocol) on all servers, and all
|
// and port 3389 (remote desktop protocol) on all servers, and all
|
||||||
// ports on git-server or ci-server.
|
// ports on git-server or ci-server.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:engineering",
|
"group:engineering",
|
||||||
"president@example.com"
|
"president@example.com"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:22,3389",
|
"*:22,3389",
|
||||||
"git-server:*",
|
"git-server:*",
|
||||||
"ci-server:*"
|
"ci-server:*"
|
||||||
@@ -45,23 +45,23 @@
|
|||||||
// Allow engineer users to access any port on a device tagged with
|
// Allow engineer users to access any port on a device tagged with
|
||||||
// tag:production.
|
// tag:production.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:engineers"
|
"group:engineers"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:production:*"
|
"tag:production:*"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
||||||
// on both networks.
|
// on both networks.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"my-subnet",
|
"my-subnet",
|
||||||
"192.168.1.0/24"
|
"192.168.1.0/24"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"my-subnet:*",
|
"my-subnet:*",
|
||||||
"192.168.1.0/24:*"
|
"192.168.1.0/24:*"
|
||||||
],
|
],
|
||||||
@@ -70,22 +70,22 @@
|
|||||||
// Comment out this section if you want to define specific ACL
|
// Comment out this section if you want to define specific ACL
|
||||||
// restrictions above.
|
// restrictions above.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:*"
|
"*:*"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// All users in Montreal are allowed to access the Montreal web
|
// All users in Montreal are allowed to access the Montreal web
|
||||||
// servers.
|
// servers.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:montreal-users"
|
"group:montreal-users"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:montreal-webserver:80,443"
|
"tag:montreal-webserver:80,443"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -94,30 +94,30 @@
|
|||||||
// In contrast, this doesn't grant API servers the right to initiate
|
// In contrast, this doesn't grant API servers the right to initiate
|
||||||
// any connections.
|
// any connections.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"tag:montreal-webserver"
|
"tag:montreal-webserver"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:api-server:443"
|
"tag:api-server:443"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// Declare tests to check functionality of ACL rules
|
// Declare tests to check functionality of ACL rules
|
||||||
"Tests": [
|
"tests": [
|
||||||
{
|
{
|
||||||
"User": "user1@example.com",
|
"src": "user1@example.com",
|
||||||
"Allow": [
|
"accept": [
|
||||||
"example-host-1:22",
|
"example-host-1:22",
|
||||||
"example-host-2:80"
|
"example-host-2:80"
|
||||||
],
|
],
|
||||||
"Deny": [
|
"deny": [
|
||||||
"exapmle-host-2:100"
|
"exapmle-host-2:100"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"User": "user2@example.com",
|
"src": "user2@example.com",
|
||||||
"Allow": [
|
"accept": [
|
||||||
"100.60.3.4:22"
|
"100.60.3.4:22"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
15
utils.go
15
utils.go
@@ -11,10 +11,12 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -55,6 +57,8 @@ const (
|
|||||||
|
|
||||||
// privateKey prefix.
|
// privateKey prefix.
|
||||||
privateHexPrefix = "privkey:"
|
privateHexPrefix = "privkey:"
|
||||||
|
|
||||||
|
PermissionFallback = 0o700
|
||||||
)
|
)
|
||||||
|
|
||||||
func MachinePublicKeyStripPrefix(machineKey key.MachinePublic) string {
|
func MachinePublicKeyStripPrefix(machineKey key.MachinePublic) string {
|
||||||
@@ -350,3 +354,14 @@ func AbsolutePathFromConfigPath(path string) string {
|
|||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFileMode(key string) fs.FileMode {
|
||||||
|
modeStr := viper.GetString(key)
|
||||||
|
|
||||||
|
mode, err := strconv.ParseUint(modeStr, Base8, BitSize64)
|
||||||
|
if err != nil {
|
||||||
|
return PermissionFallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.FileMode(mode)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user